7.2 KiB
Back/Forward support for the Tasks page
Goal
Make the titlebar back/forward buttons and the Cmd/Ctrl+Alt+←/→ shortcut
traverse Tasks visits in addition to worktree activations. Today both are
no-ops outside activeView === 'terminal' (see App.tsx:616, App.tsx:900).
Current shape
- History lives in
src/renderer/src/store/slices/worktree-nav-history.ts. - Entries are
string[](worktree IDs), recorded fromworktree-activation.ts:96viarecordWorktreeVisit. goBackWorktree/goForwardWorktreeskip dead worktrees viafindPrev/NextLiveWorktreeHistoryIndex, gated by anactivatorref andisNavigatingHistoryto prevent re-recording.- UI (pre-change): titlebar buttons hidden unless
activeView === 'terminal'; shortcut ignored outside terminal view.
Minimal implementation (downscoped)
-
History entry type →
string | 'tasks'.findPrev/NextLiveWorktreeHistoryIndexmust short-circuit on'tasks'before callingfindWorktreeById(which takes a worktree id, not a view tag) — Tasks entries are unconditionally live.- Keep
recordWorktreeVisit(worktreeId: string)signature to avoid churn interminals-hydration.test.ts. Add a siblingrecordViewVisit(entry: 'tasks')that shares the dedupe/truncate/cap logic. - Keep existing names (
worktreeNavHistory,goBackWorktree,canGoBackWorktreeHistory). They're now slight misnomers since entries may be'tasks', but renaming touches ~20 call sites for no behavioral win. Add a one-line comment at the top of the slice stating the entry type and the intentional keeping of the name, so future readers don't assume it's worktree-only.
-
Record Tasks visits → call
recordViewVisit('tasks')fromopenTaskPage(ui.ts:189). NoisNavigatingHistoryguard needed: back-to-Tasks routes throughsetActiveView('tasks')per step 3, which never touchesopenTaskPage. The slice's existing adjacent-entry dedupe covers any other re-entry. Note:openTaskPageis called with varyingtaskPageData(e.g.{ taskSource: 'github' }vs'linear'fromSidebarNav.tsx:88,102); all collapse to a single'tasks'entry and dedupe against the current entry, so toggling presets produces no extra history entries. This is consistent with the out-of-scope "no per-entry snapshotting" decision. -
Dispatch in goBack/goForward → based on entry kind:
- worktreeId → existing
activator(worktreeId)path. When current entry is'tasks', no extra view handling is needed:activateAndRevealWorktreeatworktree-activation.ts:82-84already switchesactiveViewback to'terminal'. 'tasks'→setActiveView('tasks')(notopenTaskPage— avoids mutatingpreviousViewBeforeTasksand the SWR prefetch).
- worktreeId → existing
-
Fix Esc-close history desync.
closeTaskPage(ui.ts:210) currently setsactiveViewtopreviousViewBeforeTaskswithout touching the history index, so afterA → Tasks → Escthe index still points at the'tasks'entry and Back becomes a visual no-op (activator re-activates A) while Forward re-opens Tasks. Fix: incloseTaskPage, if the current history entry is'tasks', move the index to the previous live entry (same scan asfindPrevLiveWorktreeHistoryIndex). Sub-case: if there is no previous live entry (e.g. user's first action was open Tasks, then Esc — history['tasks']at index 0), leave the index unchanged at 0. Accept the minor cost (Back becomes a visual no-op until a real visit records a new entry) rather than setting to -1, which would lose the only forward target. Guard withisNavigatingHistoryis unnecessary here —closeTaskPageis never invoked from the history path. -
Unhide UI on Tasks (allowlist, not denylist —
activeViewunion is'terminal' | 'settings' | 'tasks', but an allowlist won't silently include future views):- Replace the
activeView !== 'terminal'early-return atApp.tsx:616withactiveView !== 'terminal' && activeView !== 'tasks'. Update the adjacent comment (App.tsx:613-615) — it currently claims the shortcut is a no-op outside terminal because the buttons are hidden; replace with: back/forward traverse worktree + Tasks visits, so the shortcut is active whenever the button cluster is (terminal or Tasks); still suppressed elsewhere (Settings). - Change the titlebar cluster guard at
App.tsx:900toactiveView === 'terminal' || activeView === 'tasks'. Update the adjacent comment (App.tsx:896-899) — drop the "terminal view only" framing; explain the cluster is shown wherever the history shortcut is live, and hidden in Settings to keep that view modal-ish.
- Replace the
-
Tests: extend
worktree-nav-history.test.tsfor mixed entries:A → Tasks → B, back lands on Tasks, back again on A.- Tasks dedupe against current entry.
- Dead worktree between Tasks entries is skipped.
A → Tasks → closeTaskPage(): index moves back to A; subsequent Back is a no-op, Forward re-opens Tasks.Tasks (only entry) → closeTaskPage(): index stays at 0, Back is a visual no-op.
Explicitly out of scope
- Settings in history. Stays modal-ish; closes via
previousViewBeforeSettings. - Per-entry
taskPageDatasnapshotting. Back-to-Tasks opens Tasks with default filters/source. Revisit if users complain. - Session persistence of history. Keep in-memory only (it already is).
Edge cases handled
- Worktree deletion mid-session → skipped by existing live-check; Tasks entries always live.
- Dedupe semantics → same adjacency rule works for
'tasks'. - Activator failure on worktree → unchanged (
result !== falsegate). Tasks dispatch can't fail. - Editable-target guard at
App.tsx:600still fires first → typing in the Tasks search box +Cmd+Alt+←remains a no-op. MAX_HISTORY = 50→ Tasks entries consume slots; worst-case effective worktree depth halves to ~25. Acceptable.
Known residual quirks (accepted)
-
Prefetch loss on back-to-Tasks. Routing through
setActiveView('tasks')skips the SWR prefetch atui.ts:201-208; back-to-Tasks is ~300–800ms slower than a fresh open via the sidebar. Not a regression vs today (back-to-Tasks isn't possible at all today). -
Titlebar layout shift. Revealing the button cluster on Tasks changes the titlebar — needs a visual check that nothing Tasks-specific collides.
Files touched
src/renderer/src/store/slices/worktree-nav-history.ts— entry type,recordViewVisit, dispatch ingoBack/goForwardWorktree.src/renderer/src/store/slices/worktree-nav-history.test.ts— new cases.src/renderer/src/store/slices/ui.ts—openTaskPagerecords viarecordViewVisit;closeTaskPagerewinds history index when the current entry is'tasks'.src/renderer/src/App.tsx— widen the twoactiveView === 'terminal'guards to include'tasks', and update the adjacent "why" comments atApp.tsx:613-615andApp.tsx:896-899to reflect the new invariant.
Estimated diff: ~30 lines of slice logic, 1 call site in ui.ts, 2 guards
in App.tsx, plus tests.