orca/docs/back-button-tasks-page.md

7.2 KiB
Raw Blame History

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 from worktree-activation.ts:96 via recordWorktreeVisit.
  • goBackWorktree/goForwardWorktree skip dead worktrees via findPrev/NextLiveWorktreeHistoryIndex, gated by an activator ref and isNavigatingHistory to prevent re-recording.
  • UI (pre-change): titlebar buttons hidden unless activeView === 'terminal'; shortcut ignored outside terminal view.

Minimal implementation (downscoped)

  1. History entry typestring | 'tasks'.

    • findPrev/NextLiveWorktreeHistoryIndex must short-circuit on 'tasks' before calling findWorktreeById (which takes a worktree id, not a view tag) — Tasks entries are unconditionally live.
    • Keep recordWorktreeVisit(worktreeId: string) signature to avoid churn in terminals-hydration.test.ts. Add a sibling recordViewVisit(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.
  2. Record Tasks visits → call recordViewVisit('tasks') from openTaskPage (ui.ts:189). No isNavigatingHistory guard needed: back-to-Tasks routes through setActiveView('tasks') per step 3, which never touches openTaskPage. The slice's existing adjacent-entry dedupe covers any other re-entry. Note: openTaskPage is called with varying taskPageData (e.g. { taskSource: 'github' } vs 'linear' from SidebarNav.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.

  3. Dispatch in goBack/goForward → based on entry kind:

    • worktreeId → existing activator(worktreeId) path. When current entry is 'tasks', no extra view handling is needed: activateAndRevealWorktree at worktree-activation.ts:82-84 already switches activeView back to 'terminal'.
    • 'tasks'setActiveView('tasks') (not openTaskPage — avoids mutating previousViewBeforeTasks and the SWR prefetch).
  4. Fix Esc-close history desync. closeTaskPage (ui.ts:210) currently sets activeView to previousViewBeforeTasks without touching the history index, so after A → Tasks → Esc the index still points at the 'tasks' entry and Back becomes a visual no-op (activator re-activates A) while Forward re-opens Tasks. Fix: in closeTaskPage, if the current history entry is 'tasks', move the index to the previous live entry (same scan as findPrevLiveWorktreeHistoryIndex). 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 with isNavigatingHistory is unnecessary here — closeTaskPage is never invoked from the history path.

  5. Unhide UI on Tasks (allowlist, not denylist — activeView union is 'terminal' | 'settings' | 'tasks', but an allowlist won't silently include future views):

    • Replace the activeView !== 'terminal' early-return at App.tsx:616 with activeView !== '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:900 to activeView === '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.
  6. Tests: extend worktree-nav-history.test.ts for 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 taskPageData snapshotting. 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 !== false gate). Tasks dispatch can't fail.
  • Editable-target guard at App.tsx:600 still 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)

  1. Prefetch loss on back-to-Tasks. Routing through setActiveView('tasks') skips the SWR prefetch at ui.ts:201-208; back-to-Tasks is ~300800ms slower than a fresh open via the sidebar. Not a regression vs today (back-to-Tasks isn't possible at all today).

  2. 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 in goBack/goForwardWorktree.
  • src/renderer/src/store/slices/worktree-nav-history.test.ts — new cases.
  • src/renderer/src/store/slices/ui.tsopenTaskPage records via recordViewVisit; closeTaskPage rewinds history index when the current entry is 'tasks'.
  • src/renderer/src/App.tsx — widen the two activeView === 'terminal' guards to include 'tasks', and update the adjacent "why" comments at App.tsx:613-615 and App.tsx:896-899 to reflect the new invariant.

Estimated diff: ~30 lines of slice logic, 1 call site in ui.ts, 2 guards in App.tsx, plus tests.