summaryrefslogtreecommitdiffhomepage
path: root/src/features/workspaces/ui/WorkspaceCard.svelte
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-28 13:18:49 +0900
committerAdam Malczewski <[email protected]>2026-06-28 14:41:18 +0900
commit60aa5dc48b6af502f88befd7d1517ab52cf6c60f (patch)
tree4d4ea1eafed3ce0e56f67b23f1776f96f7f506bc /src/features/workspaces/ui/WorkspaceCard.svelte
parenta59200e786f7d97d7ba5b9cd2bee9ffef531dac2 (diff)
downloaddispatch-web-60aa5dc48b6af502f88befd7d1517ab52cf6c60f.tar.gz
dispatch-web-60aa5dc48b6af502f88befd7d1517ab52cf6c60f.zip
feat(workspaces): star toggle for concurrency priority
Backend (feature/workspace-star) shipped Workspace.starred: boolean (additive to [email protected], no version bump) + PUT/DELETE /workspaces/:id/star endpoints (no body; create-on-miss; return the updated Workspace). A starred workspace's agents jump ahead of non-starred ones in the concurrency limiter queue (oldest-agent-first within each group); takes effect immediately for already-queued agents. FE consumed: - adapter/http.ts: star(id)/unstar(id) -> WorkspaceResult<Workspace> (PUT/DELETE /workspaces/:id/star, no body). - logic/view-model.ts: pure sortWorkspaces (starred-first, then lastActivityAt desc, stable) + pure applyStarred (the optimistic apply/revert transform). - store.svelte.ts: setStarred(id, starred) — optimistic flip with error revert; the list is now a $derived sorted view (starred bubble to top reactively); no full refresh on success (avoids flicker). - ui/WorkspaceCard.svelte: star toggle button (filled gold when starred, outline when not; spinner in flight; aria-pressed/aria-label; tooltip notes concurrency priority). - Re-mirrored .dispatch/wire.reference.md (starred + delta note). - GLOSSARY.md: 'starred' term. Tests (+27): http star/unstar (5), view-model sort+applyStarred (12), store optimistic+revert+re-sort (6), WorkspaceCard star button (4). Verification: typecheck 0/0, 1045 tests green, biome clean, build OK. backend-handoff.md updated (workspace-star slice, no open backend asks).
Diffstat (limited to 'src/features/workspaces/ui/WorkspaceCard.svelte')
-rw-r--r--src/features/workspaces/ui/WorkspaceCard.svelte45
1 files changed, 45 insertions, 0 deletions
diff --git a/src/features/workspaces/ui/WorkspaceCard.svelte b/src/features/workspaces/ui/WorkspaceCard.svelte
index 0ffc975..561b06c 100644
--- a/src/features/workspaces/ui/WorkspaceCard.svelte
+++ b/src/features/workspaces/ui/WorkspaceCard.svelte
@@ -83,6 +83,28 @@
if (!result.ok) computerError = result.error;
}
+ // ── Star (concurrency priority) ──────────────────────────────────────────────
+ let savingStar = $state(false);
+ let starError = $state<string | null>(null);
+
+ async function toggleStar(): Promise<void> {
+ if (savingStar) return;
+ savingStar = true;
+ starError = null;
+ try {
+ // Optimistic: the store flips `starred` immediately and re-sorts; revert
+ // on error is handled there. We read `ws.starred` for the target value.
+ const result = await store.setStarred(ws.id, !ws.starred);
+ if (!result.ok) starError = result.error;
+ } catch (err) {
+ // A throw (e.g. a rejected effect) — surface it; the store already
+ // reverted the optimistic flip if it got far enough to apply it.
+ starError = err instanceof Error ? err.message : "Star toggle failed";
+ } finally {
+ savingStar = false;
+ }
+ }
+
// ── Delete ─────────────────────────────────────────────────────────────────
let deleting = $state(false);
@@ -123,6 +145,25 @@
>
{/if}
<span class="font-mono text-xs opacity-50">/{ws.id}</span>
+ <button
+ type="button"
+ class="btn btn-ghost btn-xs px-1"
+ disabled={savingStar}
+ aria-pressed={ws.starred}
+ aria-label={ws.starred ? "Unstar workspace" : "Star workspace"}
+ title={ws.starred
+ ? "Starred — its agents get concurrency priority. Click to unstar."
+ : "Star this workspace to give its agents concurrency priority."}
+ onclick={toggleStar}
+ >
+ {#if savingStar}
+ <span class="loading loading-spinner loading-xs"></span>
+ {:else if ws.starred}
+ <span class="text-warning" aria-hidden="true">★</span>
+ {:else}
+ <span class="opacity-40" aria-hidden="true">☆</span>
+ {/if}
+ </button>
<span class="ml-auto text-xs opacity-50">
{ws.conversationCount}
{ws.conversationCount === 1 ? "conversation" : "conversations"}
@@ -148,6 +189,10 @@
<p class="text-xs text-error">{titleError}</p>
{/if}
+ {#if starError}
+ <p class="text-xs text-error">{starError}</p>
+ {/if}
+
<div class="flex items-center gap-2">
<span class="w-8 shrink-0 text-xs opacity-60">cwd</span>
<input