From a4bd45f8b726ea51f41eb8ee02d7d79068952b3b Mon Sep 17 00:00:00 2001 From: xarmian Date: Mon, 6 Jul 2026 14:18:02 +0000 Subject: [PATCH 1/3] feat(web): add "Recently deleted" section to workspace switcher Adds a collapsible "Recently deleted" section below the active-workspaces list in WorkspaceSwitcher, populated from api.workspaces.listDeleted() and loaded each time the switcher opens. Each row shows the workspace name, a subtle "N days left", and an inline Restore button that calls api.workspaces.restore() then refreshes both the deleted list and the active workspaces list so the restored workspace reappears; a success toast confirms. The whole section is hidden when there are no deleted workspaces, and fetch failures are swallowed quietly so the switcher never breaks. Renders in both the desktop dropdown and the mobile BottomSheet. Closes TASK-1974 Claude-Session: https://claude.ai/code/session_01HxBkAMiFBtCRJ2tKSCt3ST --- .../layout/WorkspaceSwitcher.svelte | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/web/src/lib/components/layout/WorkspaceSwitcher.svelte b/web/src/lib/components/layout/WorkspaceSwitcher.svelte index 309fb0af..04d4ee77 100644 --- a/web/src/lib/components/layout/WorkspaceSwitcher.svelte +++ b/web/src/lib/components/layout/WorkspaceSwitcher.svelte @@ -1,9 +1,12 @@ {#snippet workspaceList()} @@ -103,6 +152,46 @@ {/snippet} +{#snippet deletedSection()} + + {#if deleted.length > 0} +
+ + {#if deletedExpanded} +
+ {#each deleted as ws (ws.slug)} +
+ {ws.name} + + {ws.days_left} {ws.days_left === 1 ? 'day' : 'days'} left + + +
+ {/each} +
+ {/if} +
+ {/if} +{/snippet} +
@@ -203,4 +294,63 @@ font-size: 1em; border-radius: var(--radius-sm); } + + /* ── Recently deleted ─────────────────────────────────────────────── */ + .deleted-section { border-top: 1px solid var(--border); } + .deleted-header { + display: flex; + align-items: center; + gap: var(--space-2); + color: var(--text-muted); + font-size: 0.8em; + text-transform: uppercase; + letter-spacing: 0.03em; + } + .deleted-caret { font-size: 0.9em; flex-shrink: 0; } + .deleted-title { flex: 1; min-width: 0; } + .deleted-count { + flex-shrink: 0; + padding: 0 var(--space-2); + background: var(--bg-tertiary); + border-radius: var(--radius-sm); + font-size: 0.9em; + } + .deleted-list { display: flex; flex-direction: column; } + .deleted-item { + display: flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-2) var(--space-4); + font-size: 0.9em; + } + .deleted-name { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--text-secondary); + } + .deleted-days { + flex-shrink: 0; + color: var(--text-muted); + font-size: 0.85em; + white-space: nowrap; + } + .restore-btn { + flex-shrink: 0; + padding: var(--space-1) var(--space-2); + background: none; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--accent-blue); + cursor: pointer; + font-size: 0.85em; + } + .restore-btn:hover:not(:disabled) { background: var(--bg-hover); } + .restore-btn:disabled { opacity: 0.6; cursor: default; } + + /* Inside the mobile sheet, give the deleted rows the same roomier + padding as the workspace rows above. */ + .sheet-body .deleted-item { padding: var(--space-2) var(--space-3); } From 73ab27810cbcfdebf2632a45d778b68f0ffcc157 Mon Sep 17 00:00:00 2001 From: xarmian Date: Mon, 6 Jul 2026 14:21:10 +0000 Subject: [PATCH 2/3] fix(web): decouple restore success toast from list refresh A failing workspaceStore.loadAll() after a successful restore no longer shows a misleading "Couldn't restore" toast. The restore API call now has its own catch; the post-restore refresh is guarded separately so a reload failure stays silent (the restore already succeeded). Addresses Codex P2. Claude-Session: https://claude.ai/code/session_01HxBkAMiFBtCRJ2tKSCt3ST --- .../layout/WorkspaceSwitcher.svelte | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/web/src/lib/components/layout/WorkspaceSwitcher.svelte b/web/src/lib/components/layout/WorkspaceSwitcher.svelte index 04d4ee77..aaabea21 100644 --- a/web/src/lib/components/layout/WorkspaceSwitcher.svelte +++ b/web/src/lib/components/layout/WorkspaceSwitcher.svelte @@ -124,13 +124,25 @@ if (restoringSlug) return; restoringSlug = ws.slug; try { - await api.workspaces.restore(ws.slug); + try { + await api.workspaces.restore(ws.slug); + } catch { + // Only the restore call itself failing is a restore failure. + toastStore.show(`Couldn't restore "${ws.name}"`, 'error'); + return; + } + // Restore succeeded — confirm before refreshing so a failing + // reload can't masquerade as a restore failure. toastStore.show(`Restored "${ws.name}"`, 'success'); // Refresh both lists so the restored workspace drops out of the - // deleted section and reappears in the active list. - await Promise.all([loadDeleted(), workspaceStore.loadAll()]); - } catch { - toastStore.show(`Couldn't restore "${ws.name}"`, 'error'); + // deleted section and reappears in the active list. loadDeleted() + // swallows its own errors; guard loadAll() so a reload failure + // stays silent — the restore already went through. + try { + await Promise.all([loadDeleted(), workspaceStore.loadAll()]); + } catch { + // Reload failure is non-fatal. + } } finally { restoringSlug = null; } From 1aebefdf849ea482d56b8970810cf3ab02a147b3 Mon Sep 17 00:00:00 2001 From: xarmian Date: Mon, 6 Jul 2026 14:23:12 +0000 Subject: [PATCH 3/3] fix(web): sequence deleted-list fetches to avoid stale overwrite Adds a monotonic request token to loadDeleted() (mirroring workspaceStore's membershipSeq) so an older open-triggered listDeleted() response can no longer clobber the fresher post-restore refresh and re-surface a just-restored workspace with a live Restore button. Addresses Codex P2. Claude-Session: https://claude.ai/code/session_01HxBkAMiFBtCRJ2tKSCt3ST --- .../lib/components/layout/WorkspaceSwitcher.svelte | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/web/src/lib/components/layout/WorkspaceSwitcher.svelte b/web/src/lib/components/layout/WorkspaceSwitcher.svelte index aaabea21..5e38e571 100644 --- a/web/src/lib/components/layout/WorkspaceSwitcher.svelte +++ b/web/src/lib/components/layout/WorkspaceSwitcher.svelte @@ -101,15 +101,23 @@ // Slug currently being restored — drives the per-row disabled/pending // state so a double-click can't fire two restores. let restoringSlug = $state(null); + // Monotonic token guarding async listDeleted() responses against races — + // mirrors workspaceStore's membershipSeq. Each loadDeleted() call claims a + // token; a response only writes if its token is still current, so an older + // open-triggered fetch can't clobber the fresher post-restore refresh (and + // re-surface a just-restored workspace). + let deletedSeq = 0; async function loadDeleted() { + const seq = ++deletedSeq; try { - deleted = await api.workspaces.listDeleted(); + const list = await api.workspaces.listDeleted(); + if (seq === deletedSeq) deleted = list; } catch { // Swallow — the restore affordance is a bonus, never a blocker for // switching workspaces. Clear the list so a stale fetch doesn't // linger and the section stays hidden. - deleted = []; + if (seq === deletedSeq) deleted = []; } }