Fix terminal session lookup: resolve project names to live tab names - #539
Closed
catomean wants to merge 18 commits into
Closed
Fix terminal session lookup: resolve project names to live tab names#539catomean wants to merge 18 commits into
catomean wants to merge 18 commits into
Conversation
…hints - Replace admin-facing 'Check box-runner service' message with actionable user guidance - Add automatic source switching when requested tab found on other source (cloud/machine) - Prevents 'Watch' and 'Focus terminal' dead-ends where session exists but on wrong source - Poll both sources in parallel to determine where session actually lives - Only show 'session not found' after checking both sources Fixes the three-way inconsistency where Terminal, Control, and Watch disagreed about session existence.
…hints - Make resolveTabAttachment case-insensitive so 'fleetcrown' matches 'Bitbaum' - Update deep link miss suppression to use case-insensitive check - Simplify stalled hints: no longer suggest trying other source - Reuses existing resolveEffectiveTab logic from Control API This fixes the core issue where Terminal, Focus terminal, Watch, and Open session all use different case-sensitive lookups and miss sessions that Control already knows about.
…erminal context - Terminal now resolves project names to live tab names before matching - Uses /api/terminal/context which already maps tabs→projectName - Resolves on BOTH sources before auto-switch decision - Fixes Watch/Focus/Open passing 'fleetcrown' when tab is 'Bitbaum' - Simplifies stalled hints to just 'Stream not responding.' This is the shared lookup: terminal/context maps tab↔project, same as Control uses.
The previous fix added a useEffect that called setState synchronously, triggering eslint error react-hooks/set-state-in-effect. The React way is to derive the state instead. Now: - userSelection tracks manual tab switches - selected = userSelection ?? resolvedInitialTab - When context loads and maps 'fleetcrown' → 'Bitbaum', selected automatically updates without an effect This fixes the 'selected is initialized before context has loaded' race where the first render used the un-resolved project name.
The issue: Watch links in ControlInbox built URLs with project names (e.g. 'fleetcrown') instead of the actual live tab names (e.g. 'Bitbaum'). When the Terminal page loaded with ?tab=fleetcrown, it resolved correctly client-side, but the user wanted the resolution to happen at the source. The fix: - Pass projects array (which has liveTab) to ControlInbox - Look up project.liveTab for each feedback item when building Watch links - Use liveTab ?? projectName so it falls back gracefully This uses Control's existing project-to-tab mapping (resolveEffectiveTab) instead of adding a second lookup. Watch links now pass the actual tab name that Zellij knows about. Activity Open session links still pass project names, but TerminalSurface already resolves those client-side (previous commit).
The issue: When Focus terminal was clicked and the agent wasn't running yet,
it launched the agent with project.tab ('fleetcrown') instead of workspaceTab
(which is project.liveTab ?? project.tab, i.e., 'Bitbaum').
This caused /api/agent/launch to create a Zellij tab named 'fleetcrown',
but then focus-tab looked for 'Bitbaum', resulting in 'tab not found'.
The fix: Use workspaceTab consistently for both the launch and focus paths.
Now the agent is launched with the correct tab name that matches what
Control already knows about (project.liveTab).
The issue: When feedback was implemented (Implement button clicked), the
dispatch route called injectPrompt with tab: row.projectName ('fleetcrown').
This enqueued a focus_tab command with the project slug, which failed when
the actual Zellij tab was named differently ('Bitbaum').
The fix: Resolve the project name to the live tab name using resolveEffectiveTab
(the same function Control uses) before calling injectPrompt. Now feedback
dispatch uses the actual tab name that Zellij knows about.
This is the source of the 'focus_tab → fleetcrown failed: tab not found'
banner the operator saw on Control after implementing feedback.
The issue: The previous fix tried to resolve the project name using resolveEffectiveTab, but that only works for case/punctuation variants and directory aliases. When project 'fleetcrown' runs as tab 'Bitbaum' (completely different names), resolveEffectiveTab can't match them. The fix: Use the liveTab that Control already stored in project_states.tabName. This is the SAME liveTab that Control API returns and that Watch links use. Now feedback dispatch uses project_states.tabName (liveTab) ?? projectName, ensuring it uses the actual tab name Zellij knows about. Changes: - getFeedbackWithProject now joins project_states and returns liveTab - dispatch route uses row.liveTab ?? row.projectName (same fallback as Control)
…name Foundation for project-based session identity. Gets the current session for a project from agent_sessions: the open turn, or the most recent closed session. This works for Claude, which reports sessions via UserPromptSubmit/Stop hooks. Cursor, Codex, Antigravity, and Grok do not report sessions yet - they would need similar hooks to participate in this model. Next: Update Watch/Focus/Implement to use sessionId instead of tab names.
Implement now checks if a session exists for the project before dispatching. Returns 404 if no session exists (needs 'start session' action). LIMITATION: Still passes tab name to injectPrompt because that's its interface. The full fix requires changing InjectParams to accept sessionId instead of tab. Next: Change injectPrompt interface or add sessionId passthrough.
Implement now passes sessionId to injectPrompt when a session exists. This is the session-based identity that replaces tab-name matching. Note: injectPrompt still requires tab parameter (for backwards compat) but will use sessionId when provided to look up the session directly. Next: Update injectPrompt to actually use the sessionId parameter.
When sessionId is provided in InjectParams, look up the project from agent_sessions instead of matching by tab name. This makes the session the target, not the tab string. When dispatch finds no current session, it passes undefined sessionId and injectPrompt starts a new session via the legacy tab path.
Add sessionId to InjectPayload and DispatchPayload so the runner receives it and can pass it to Claude for resuming the specific session. This completes the session-based identity path: 1. injectPrompt looks up project from agent_sessions when sessionId is provided 2. Passes sessionId through executeInject 3. sessionId lands in pending_commands payload 4. Runner receives it and can resume that Claude session When no session exists (getCurrentSessionForProject returns null), sessionId stays undefined and the legacy tab-based path starts a new session.
Complete the session-based delivery path by passing sessionId from the queued command payload through to Claude's CLI: 1. Add sessionId to AgentRuntimeConfig type 2. Update Claude adapter buildLaunchCommand to use --session <sessionId> 3. Update provisionAgentWorkspace to accept and pass sessionId 4. Update launchAgentPty to accept and pass sessionId 5. Update desktop poller dispatch case to read sessionId from payload and pass it to launchAgentPty 6. Update command validator to accept sessionId in DispatchCommand payload When sessionId is provided, Claude now launches with --session <sessionId> to resume that specific session instead of starting a new one.
1. Correct flag: --resume (not --session) per Claude documentation 2. When PTY is already live and sessionId is provided, terminate and relaunch with --resume <sessionId> to ensure we're in the correct session. Without this, we'd inject into whatever session was already running, not the one identified by agent_sessions.
Store which sessionId we launched in each tab (sessionIdByTab Map). Only terminate + relaunch when the running session is WRONG. If PTY is already running the correct sessionId, leave it alone. This prevents killing a live pane the person is watching when it's already the right session — restarting would look like a crash.
…l links - Change fleetSurfaceHref to generate /terminal?project= instead of ?tab= - Terminal accepts both ?project= (new) and ?tab= (legacy) - Update all Watch and Focus links to use project keys instead of tab names - Remove tab-name matching from Watch/Focus paths per user requirement - ProjectStatusChips already uses liveTab fallback for focus-tab API This removes tab name strings from URLs so Watch/Focus/Open session operate on the project's session identity, not guessed tab names.
Collaborator
Author
|
Closing: tab-name matching is obsolete. Prefer project session/repo/URL (see fleetSurfaceHref ?project=). Do not merge. |
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
Session-based dispatch (complete end-to-end)
getCurrentSessionForProjectretrieves the current session for a project fromagent_sessionsdispatch/route.tscalls it and passessessionIdtoinjectPromptinjectPromptusessessionIdfor project lookup instead of tab-name matching when providedsessionIdflows from web →pending_commands→ Fleet Runner → Claude CLI via--resumesessionIdByTabmap and only terminates/relaunches PTY when session IDs differsessionIdisundefinedand the legacy tab-based path starts a new sessionWatch and Focus now use project keys, not tab names (NEW)
fleetSurfaceHref("terminal", project)generates/terminal?project=Xinstead of/terminal?tab=X?project=(new) and?tab=(legacy) for backward compatibility?project=?project=workspaceTab = project.liveTab ?? project.tabfor resolutionresolveProjectToTablogic handles mapping project keys to actual tab namesThis removes tab name strings from all Watch/Focus URLs. The person clicks "Watch" on the "fleetcrown" project and lands on the session for "fleetcrown", regardless of whether the live tab is named "Bitbaum" or "fleetcrown" — the system resolves it.
Files changed
inject-core.ts,executor.ts,pending-commands.ts(schema),dispatch/route.tsagents/types.ts,agents/claude.ts,agent-registry.ts,agent-execution/launch.tsdesktop/src/main/pty-runtime.ts,poller.ts,command-validator.tsdb/queries/agent-sessions.ts(newgetCurrentSessionForProject)fleet-context.ts,TerminalPageClient.tsx,TerminalSurface.tsxProjectCard.tsx,ProjectStatusChips.tsx,NeedsYouCard.tsx,ActivityEventRow.tsx,RunModal.tsx,Transcript.tsxfleet-context.tsupdated for new URL formatTesting on live site
After the build stamp moves to this branch:
Feedback dispatch with session resume:
/feedback)Watch from Control:
/control)/terminal?project=fleetcrown(not?tab=Bitbaum)Watch from Activity:
/terminal?project=X(not?tab=X)Focus terminal:
New session start (no existing session):
Session reuse without PTY restart:
What's unproven
?project=URLs needs verification on the live siteresolveProjectToTabcorrectly maps all project names to their live tabs in productionliveTabfallback, notsessionIdfrom DB)Not in this PR
?session=<id>directly (still uses project → tab resolution)sessionIdfrom the database (relies onliveTabbeing set)agent_sessionsdoes not store the tab name (session → tab mapping happens via project key)