Skip to content

feat: implement lazy session restore feature with UI toggle#93

Open
quarterpound wants to merge 3 commits into
gi11es:masterfrom
quarterpound:master
Open

feat: implement lazy session restore feature with UI toggle#93
quarterpound wants to merge 3 commits into
gi11es:masterfrom
quarterpound:master

Conversation

@quarterpound

@quarterpound quarterpound commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

On launch, Deckard restored every saved session by immediately spawning its process for every tab in every project. With many projects open, that's dozens of forkpty + agent processes fired within seconds of opening the app, plus a burst of TCC prompts and CPU churn for sessions you may never look at.

This PR makes session startup lazy-capable: tabs and their terminal views are still created up front (the sidebar and tab bar look identical), but the underlying shell/agent process can be deferred until a tab is first opened.

"Restore sessions lazily" toggle controls it, defaulting off so existing behavior is preserved.

@quarterpound

Copy link
Copy Markdown
Contributor Author

@gi11es any chance you can take a look at this?

@gi11es gi11es left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — this is a well-constructed PR overall. A removed-behavior audit of the restructured restore path found the default (toggle-off) flow otherwise equivalent to the old eager code: tab order, suppressUnseen, onProcessExit wiring, selectedTabIndex clamping, and tmux name preservation all carry over. But verification confirmed one regression on the default path plus two UI issues, so requesting changes:

Confirmed — blocking

  1. Orphaned process spawn on the default path (startPendingTabsProgressively). The old createTabsProgressively built tabs from saved state one-by-one, so a tab couldn't be closed before it existed. The new code creates all TabItems up front and holds live references in remaining, walking them at 0.05s cadence. Closing a not-yet-started tab (closeTabById → handleSurfaceClosedById) removes it from workspace.tabs but never clears tab.pendingStart, so when its turn comes, startPendingShellIfNeeded happily calls surface.startShell — spawning an orphaned claude --resume/codex/tmux process attached to no tab; its onProcessExit → handleSurfaceClosedById is a no-op since the tab is gone. Fix options: clear pendingStart in handleSurfaceClosedById, and/or have startPendingShellIfNeeded verify the tab is still present in some workspace before starting.

  2. Tab bar shows pending tabs as live. The sidebar learned filled: !info.pending (hollow, non-pulsing dots), but HorizontalTabView in TabBarViews.swift constructs BadgeShapeView without filled: (defaults true) and still applies isBadgeAnimated. Background tabs of the selected workspace are pending yet appear in the tab bar with solid — and potentially pulsing — dots, contradicting the sidebar one pane over.

  3. Never-started codex tabs flip to "thinking". pollCodexBadgeStates builds codexTargets from all codex tabs with a sessionId — deferred tabs keep their restored id — and parseCodexActivityInfo happily reads a stale on-disk .jsonl whose last event is task_started, so applyCodexBadgeStates sets .codexThinking on a tab whose process was never spawned. Bounded (hollow dot, not persisted), but wrong: exclude pendingStart != nil tabs from codexTargets.

Worth addressing

  1. Eager/lazy start paths have already drifted: the eager branch guards codex discovery with tab.sessionId == nil || forkSession, the lazy copy drops || forkSession (and PendingStart doesn't capture forkSession). Latent today, but the duplication invites divergence — consider making the eager branch set pendingStart and immediately call startPendingShellIfNeeded so there's one start path.
  2. startPendingShellIfNeeded triggers a full rebuildSidebar() on each first-open (default refreshSidebar: true from showTab); cycling through N restored tabs costs N full sidebar rebuilds — a targeted badge refresh would do.
  3. Minor: PendingStart retains envVars/initialInput per never-opened tab indefinitely, and workingDirectory duplicates workspace.path.

One claim I explicitly checked and did NOT flag: closing a never-opened lazy terminal tab kills its tmux session — that matches the app's documented close-tab semantics, and closeWorkspace still takes the detach() branch thanks to the eager tmuxSessionName assignment. 👍

Also needs a rebase — the PR currently shows as conflicting with master.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants