summaryrefslogtreecommitdiffhomepage
path: root/src/features/workspaces/ui
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-25 22:45:51 +0900
committerAdam Malczewski <[email protected]>2026-06-25 22:45:51 +0900
commit1285564f12238b22f6b39b9f3fbcecaca8456911 (patch)
tree9d8ca532969a5a11ee61b5c42135ac6f54159183 /src/features/workspaces/ui
parentc5ea2232f117adda740c7e3b8366e9f10f14d3cb (diff)
parentcdce5197abcf0f5b0576e847a701d3a317384a65 (diff)
downloaddispatch-web-1285564f12238b22f6b39b9f3fbcecaca8456911.tar.gz
dispatch-web-1285564f12238b22f6b39b9f3fbcecaca8456911.zip
Merge branch 'feature/ssh-support' into dev
# Conflicts: # src/features/workspaces/ui/WorkspaceCard.svelte # src/features/workspaces/ui/WorkspaceCard.test.ts # src/features/workspaces/ui/WorkspacesHome.svelte
Diffstat (limited to 'src/features/workspaces/ui')
-rw-r--r--src/features/workspaces/ui/WorkspaceCard.svelte42
-rw-r--r--src/features/workspaces/ui/WorkspaceCard.test.ts24
-rw-r--r--src/features/workspaces/ui/WorkspacesHome.svelte10
3 files changed, 67 insertions, 9 deletions
diff --git a/src/features/workspaces/ui/WorkspaceCard.svelte b/src/features/workspaces/ui/WorkspaceCard.svelte
index 81d6fe3..ff8a8ea 100644
--- a/src/features/workspaces/ui/WorkspaceCard.svelte
+++ b/src/features/workspaces/ui/WorkspaceCard.svelte
@@ -1,16 +1,22 @@
<script lang="ts">
- import type { WorkspaceEntry } from "@dispatch/wire";
+ import type { ComputerEntry, WorkspaceEntry } from "@dispatch/wire";
import { untrack } from "svelte";
import type { WorkspaceStore } from "../store.svelte";
import { relativeTime } from "../logic/view-model";
import { workspacePath } from "../logic/route";
+ import ComputerSelect from "../../computer/ui/ComputerSelect.svelte";
let {
ws,
store,
+ onNavigate,
+ computers,
}: {
ws: WorkspaceEntry;
store: WorkspaceStore;
+ onNavigate: (path: string) => void;
+ /** Discovered computers (`GET /computers`), for the default-computer dropdown. */
+ computers: readonly ComputerEntry[];
} = $props();
// ── Title: double-click to rename inline ──────────────────────────────────
@@ -62,6 +68,21 @@
if (!result.ok) cwdError = result.error;
}
+ // ── Default computer: dropdown (Local / discovered SSH aliases) ────────────
+ let savingComputer = $state(false);
+ let computerError = $state<string | null>(null);
+
+ async function saveComputer(computerId: string | null): Promise<void> {
+ if (savingComputer) return;
+ // No-op when unchanged (the select only fires on a real change, but guard).
+ if (computerId === (ws.defaultComputerId ?? null)) return;
+ savingComputer = true;
+ computerError = null;
+ const result = await store.setDefaultComputer(ws.id, computerId);
+ savingComputer = false;
+ if (!result.ok) computerError = result.error;
+ }
+
// ── Delete ─────────────────────────────────────────────────────────────────
let deleting = $state(false);
@@ -153,6 +174,19 @@
</button>
</div>
+ <div class="flex items-center gap-2">
+ <span class="w-8 shrink-0 text-xs opacity-60">ssh</span>
+ <ComputerSelect
+ value={ws.defaultComputerId}
+ {computers}
+ disabled={savingComputer}
+ onSelect={saveComputer}
+ />
+ {#if savingComputer}
+ <span class="loading loading-spinner loading-xs shrink-0"></span>
+ {/if}
+ </div>
+
<div class="flex justify-start">
<a
class="btn"
@@ -169,4 +203,10 @@
{:else if !cwdDirty && !ws.defaultCwd}
<p class="text-xs opacity-50">No default cwd set — conversations inherit the server default.</p>
{/if}
+
+ {#if computerError}
+ <p class="text-xs text-error">{computerError}</p>
+ {:else if !ws.defaultComputerId}
+ <p class="text-xs opacity-50">No default computer — conversations run locally (no SSH).</p>
+ {/if}
</li>
diff --git a/src/features/workspaces/ui/WorkspaceCard.test.ts b/src/features/workspaces/ui/WorkspaceCard.test.ts
index 0264d5f..f6ea432 100644
--- a/src/features/workspaces/ui/WorkspaceCard.test.ts
+++ b/src/features/workspaces/ui/WorkspaceCard.test.ts
@@ -11,6 +11,7 @@ function fakeEntry(overrides: Partial<WorkspaceEntry> = {}): WorkspaceEntry {
id: "my-ws",
title: "My Workspace",
defaultCwd: null,
+ defaultComputerId: null,
createdAt: 1,
lastActivityAt: 2,
conversationCount: 3,
@@ -33,6 +34,12 @@ function fakeStore() {
value: fakeEntry({ id, defaultCwd }),
}),
),
+ setDefaultComputer: vi.fn(
+ async (id: string, computerId: string | null): Promise<WorkspaceResult<WorkspaceEntry>> => ({
+ ok: true,
+ value: fakeEntry({ id, defaultComputerId: computerId }),
+ }),
+ ),
remove: vi.fn(
async (): Promise<WorkspaceResult<{ closedCount: number }>> => ({
ok: true,
@@ -46,7 +53,7 @@ describe("WorkspaceCard", () => {
it("renders the title, slug, and an Open link", () => {
const store = fakeStore() as unknown as WorkspaceStore;
render(WorkspaceCard, {
- props: { ws: fakeEntry(), store },
+ props: { ws: fakeEntry(), store, onNavigate, computers: [] },
});
expect(screen.getByText("My Workspace")).toBeInTheDocument();
expect(screen.getByText("/my-ws")).toBeInTheDocument();
@@ -56,7 +63,9 @@ describe("WorkspaceCard", () => {
it("double-clicking the title reveals an edit input", async () => {
const user = userEvent.setup();
const store = fakeStore() as unknown as WorkspaceStore;
- render(WorkspaceCard, { props: { ws: fakeEntry(), store } });
+ render(WorkspaceCard, {
+ props: { ws: fakeEntry(), store, onNavigate: vi.fn(), computers: [] },
+ });
await user.dblClick(screen.getByText("My Workspace"));
expect(screen.getByLabelText("Workspace title")).toHaveValue("My Workspace");
@@ -65,7 +74,9 @@ describe("WorkspaceCard", () => {
it("renames via the store on Enter", async () => {
const user = userEvent.setup();
const store = fakeStore() as unknown as WorkspaceStore;
- render(WorkspaceCard, { props: { ws: fakeEntry(), store } });
+ render(WorkspaceCard, {
+ props: { ws: fakeEntry(), store, onNavigate: vi.fn(), computers: [] },
+ });
await user.dblClick(screen.getByText("My Workspace"));
const input = screen.getByLabelText("Workspace title");
@@ -79,7 +90,7 @@ describe("WorkspaceCard", () => {
const user = userEvent.setup();
const store = fakeStore() as unknown as WorkspaceStore;
render(WorkspaceCard, {
- props: { ws: fakeEntry({ defaultCwd: "/old" }), store },
+ props: { ws: fakeEntry({ defaultCwd: "/old" }), store, onNavigate: vi.fn(), computers: [] },
});
const input = screen.getByLabelText("Default working directory");
@@ -98,7 +109,7 @@ describe("WorkspaceCard", () => {
const user = userEvent.setup();
const store = fakeStore() as unknown as WorkspaceStore;
render(WorkspaceCard, {
- props: { ws: fakeEntry({ defaultCwd: "/old" }), store },
+ props: { ws: fakeEntry({ defaultCwd: "/old" }), store, onNavigate: vi.fn(), computers: [] },
});
const input = screen.getByLabelText("Default working directory");
@@ -110,7 +121,8 @@ describe("WorkspaceCard", () => {
it("the Open link opens the workspace in a new browser tab (no same-tab navigation)", () => {
const store = fakeStore() as unknown as WorkspaceStore;
- render(WorkspaceCard, { props: { ws: fakeEntry(), store } });
+ const onNavigate = vi.fn();
+ render(WorkspaceCard, { props: { ws: fakeEntry(), store, onNavigate, computers: [] } });
const open = screen.getByRole("link", { name: "Open" });
// Native new-tab link: href points at the workspace, and target="_blank"
diff --git a/src/features/workspaces/ui/WorkspacesHome.svelte b/src/features/workspaces/ui/WorkspacesHome.svelte
index f71c3ba..bdfd023 100644
--- a/src/features/workspaces/ui/WorkspacesHome.svelte
+++ b/src/features/workspaces/ui/WorkspacesHome.svelte
@@ -1,10 +1,16 @@
<script lang="ts">
import { onMount } from "svelte";
+ import type { ComputerEntry } from "@dispatch/wire";
import type { WorkspaceStore } from "../store.svelte";
import { isValidSlug, workspacePath } from "../logic/route";
import WorkspaceCard from "./WorkspaceCard.svelte";
- let { store, onNavigate }: { store: WorkspaceStore; onNavigate: (path: string) => void } = $props();
+ let {
+ store,
+ onNavigate,
+ computers,
+ }: { store: WorkspaceStore; onNavigate: (path: string) => void; computers: readonly ComputerEntry[] } =
+ $props();
onMount(() => {
void store.refresh();
@@ -78,7 +84,7 @@
{:else}
<ul class="flex flex-col gap-2">
{#each store.list as ws (ws.id)}
- <WorkspaceCard {ws} {store} />
+ <WorkspaceCard {ws} {store} {onNavigate} {computers} />
{/each}
</ul>
{/if}