Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions packages/app/src/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import ModelPickerModal from "./components/model-picker-modal";
import ResetModal from "./components/reset-modal";
import TemplateModal from "./components/template-modal";
import WorkspacePicker from "./components/workspace-picker";
import CreateRemoteWorkspaceModal from "./components/create-remote-workspace-modal";
import CreateWorkspaceModal from "./components/create-workspace-modal";
import McpAuthModal from "./components/mcp-auth-modal";
import OnboardingView from "./pages/onboarding";
Expand Down Expand Up @@ -1600,6 +1601,7 @@ export default function App() {
setWorkspaceSearch: workspaceStore.setWorkspaceSearch,
workspacePickerOpen: workspaceStore.workspacePickerOpen(),
setWorkspacePickerOpen: workspaceStore.setWorkspacePickerOpen,
connectingWorkspaceId: workspaceStore.connectingWorkspaceId(),
workspaces: workspaceStore.workspaces(),
filteredWorkspaces: workspaceStore.filteredWorkspaces(),
activeWorkspaceId: workspaceStore.activeWorkspaceId(),
Expand Down Expand Up @@ -1860,13 +1862,16 @@ export default function App() {

<WorkspacePicker
open={workspaceStore.workspacePickerOpen()}
workspaces={workspaceStore.filteredWorkspaces()}
workspaces={workspaceStore.workspaces()}
activeWorkspaceId={workspaceStore.activeWorkspaceId()}
search={workspaceStore.workspaceSearch()}
onSearch={workspaceStore.setWorkspaceSearch}
onClose={() => workspaceStore.setWorkspacePickerOpen(false)}
onSelect={workspaceStore.activateWorkspace}
onCreateNew={() => workspaceStore.setCreateWorkspaceOpen(true)}
onCreateLocal={() => workspaceStore.setCreateWorkspaceOpen(true)}
onCreateRemote={() => workspaceStore.setCreateRemoteWorkspaceOpen(true)}
onForget={workspaceStore.forgetWorkspace}
connectingWorkspaceId={workspaceStore.connectingWorkspaceId()}
/>

<CreateWorkspaceModal
Expand All @@ -1876,7 +1881,17 @@ export default function App() {
onConfirm={(preset, folder) =>
workspaceStore.createWorkspaceFlow(preset, folder)
}
submitting={busy() && busyLabel() === "Creating workspace"}
submitting={busy() && busyLabel() === "status.creating_workspace"}
/>

<CreateRemoteWorkspaceModal
open={workspaceStore.createRemoteWorkspaceOpen()}
onClose={() => workspaceStore.setCreateRemoteWorkspaceOpen(false)}
onConfirm={(input) => workspaceStore.createRemoteWorkspaceFlow(input)}
submitting={
busy() &&
(busyLabel() === "status.creating_workspace" || busyLabel() === "status.connecting")
}
/>
</>
);
Expand Down
133 changes: 133 additions & 0 deletions packages/app/src/app/components/create-remote-workspace-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { Show, createEffect, createMemo, createSignal } from "solid-js";

import { Globe, X } from "lucide-solid";
import { t, currentLocale } from "../../i18n";

import Button from "./button";
import TextInput from "./text-input";

export default function CreateRemoteWorkspaceModal(props: {
open: boolean;
onClose: () => void;
onConfirm: (input: { baseUrl: string; directory?: string | null; displayName?: string | null }) => void;
submitting?: boolean;
inline?: boolean;
showClose?: boolean;
title?: string;
subtitle?: string;
confirmLabel?: string;
}) {
const translate = (key: string) => t(key, currentLocale());

const [baseUrl, setBaseUrl] = createSignal("");
const [directory, setDirectory] = createSignal("");
const [displayName, setDisplayName] = createSignal("");

const showClose = () => props.showClose ?? true;
const title = () => props.title ?? translate("dashboard.create_remote_workspace_title");
const subtitle = () => props.subtitle ?? translate("dashboard.create_remote_workspace_subtitle");
const confirmLabel = () => props.confirmLabel ?? translate("dashboard.create_remote_workspace_confirm");
const isInline = () => props.inline ?? false;
const submitting = () => props.submitting ?? false;

const canSubmit = createMemo(() => baseUrl().trim().length > 0 && !submitting());

createEffect(() => {
if (!props.open) return;
setBaseUrl("");
setDirectory("");
setDisplayName("");
});

const content = (
<div class="bg-gray-2 border border-gray-6 w-full max-w-lg rounded-2xl shadow-2xl overflow-hidden flex flex-col max-h-[90vh]">
<div class="p-6 border-b border-gray-6 flex justify-between items-center bg-gray-1">
<div>
<h3 class="font-semibold text-gray-12 text-lg">{title()}</h3>
<p class="text-gray-10 text-sm">{subtitle()}</p>
</div>
<Show when={showClose()}>
<button
onClick={props.onClose}
disabled={submitting()}
class={`hover:bg-gray-4 p-1 rounded-full ${submitting() ? "opacity-50 cursor-not-allowed" : ""}`.trim()}
>
<X size={20} class="text-gray-10" />
</button>
</Show>
</div>

<div class="p-6 flex-1 overflow-y-auto space-y-6">
<div class="rounded-2xl border border-gray-6 bg-gray-1/40 p-4 flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-gray-3 flex items-center justify-center">
<Globe size={20} class="text-gray-12" />
</div>
<div>
<div class="text-sm font-medium text-gray-12">{translate("dashboard.remote_workspace_title")}</div>
<div class="text-xs text-gray-10">{translate("dashboard.remote_workspace_hint")}</div>
</div>
</div>

<div class="space-y-4">
<TextInput
label={translate("dashboard.remote_base_url_label")}
placeholder={translate("dashboard.remote_base_url_placeholder")}
value={baseUrl()}
onInput={(event) => setBaseUrl(event.currentTarget.value)}
disabled={submitting()}
/>
<TextInput
label={translate("dashboard.remote_directory_label")}
placeholder={translate("dashboard.remote_directory_placeholder")}
value={directory()}
onInput={(event) => setDirectory(event.currentTarget.value)}
hint={translate("dashboard.remote_directory_hint")}
disabled={submitting()}
/>
<TextInput
label={translate("dashboard.remote_display_name_label")}
placeholder={translate("dashboard.remote_display_name_placeholder")}
value={displayName()}
onInput={(event) => setDisplayName(event.currentTarget.value)}
disabled={submitting()}
/>
</div>
</div>

<div class="p-6 border-t border-gray-6 bg-gray-1 flex justify-end gap-3">
<Show when={showClose()}>
<Button variant="ghost" onClick={props.onClose} disabled={submitting()}>
{translate("common.cancel")}
</Button>
</Show>
<Button
onClick={() =>
props.onConfirm({
baseUrl: baseUrl().trim(),
directory: directory().trim() ? directory().trim() : null,
displayName: displayName().trim() ? displayName().trim() : null,
})
}
disabled={!canSubmit()}
title={!baseUrl().trim() ? translate("dashboard.remote_base_url_required") : undefined}
>
{confirmLabel()}
</Button>
</div>
</div>
);

return (
<Show when={props.open || isInline()}>
<div
class={
isInline()
? "w-full"
: "fixed inset-0 z-50 flex items-center justify-center bg-gray-1/60 backdrop-blur-sm p-4 animate-in fade-in duration-200"
}
>
{content}
</div>
</Show>
);
}
33 changes: 25 additions & 8 deletions packages/app/src/app/components/workspace-chip.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import type { WorkspaceInfo } from "../lib/tauri";

import { ChevronDown, Folder, Globe, Zap } from "lucide-solid";
import { t, currentLocale } from "../../i18n";

function iconForPreset(preset: string) {
import { ChevronDown, Folder, Globe, Loader2, Zap } from "lucide-solid";

function iconForWorkspace(preset: string, workspaceType: string) {
if (workspaceType === "remote") return Globe;
if (preset === "starter") return Zap;
if (preset === "automation") return Folder;
if (preset === "minimal") return Globe;
Expand All @@ -12,8 +15,14 @@ function iconForPreset(preset: string) {
export default function WorkspaceChip(props: {
workspace: WorkspaceInfo;
onClick: () => void;
connecting?: boolean;
}) {
const Icon = iconForPreset(props.workspace.preset);
const Icon = iconForWorkspace(props.workspace.preset, props.workspace.workspaceType);
const subtitle = () =>
props.workspace.workspaceType === "remote"
? props.workspace.baseUrl ?? props.workspace.path
: props.workspace.path;
const translate = (key: string) => t(key, currentLocale());

return (
<button
Expand All @@ -22,22 +31,30 @@ export default function WorkspaceChip(props: {
>
<div
class={`p-1 rounded ${
props.workspace.preset === "starter"
props.workspace.workspaceType !== "remote" && props.workspace.preset === "starter"
? "bg-amber-7/10 text-amber-6"
: "bg-indigo-7/10 text-indigo-6"
}`}
>
<Icon size={14} />
</div>
<div class="flex flex-col items-start mr-2 min-w-0">
<span class="text-xs font-medium text-gray-12 leading-none mb-0.5 truncate max-w-[9.5rem]">
{props.workspace.name}
</span>
<div class="flex items-center gap-2">
<span class="text-xs font-medium text-gray-12 leading-none truncate max-w-[9.5rem]">
{props.workspace.name}
</span>
{props.workspace.workspaceType === "remote" ? (
<span class="text-[9px] uppercase tracking-wide px-1.5 py-0.5 rounded-full bg-gray-4 text-gray-11">
{translate("dashboard.remote")}
</span>
) : null}
</div>
<span class="text-[10px] text-gray-10 font-mono leading-none max-w-[120px] truncate">
{props.workspace.path}
{subtitle()}
</span>
</div>
<ChevronDown size={14} class="text-gray-10 group-hover:text-gray-11" />
{props.connecting ? <Loader2 size={14} class="text-gray-10 animate-spin" /> : null}
</button>
);
}
Loading