Skip to content

Fix terminal session lookup: resolve project names to live tab names - #539

Closed
catomean wants to merge 18 commits into
mainfrom
cursor/terminal-session-lookup-1d43
Closed

Fix terminal session lookup: resolve project names to live tab names#539
catomean wants to merge 18 commits into
mainfrom
cursor/terminal-session-lookup-1d43

Conversation

@catomean

@catomean catomean commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

What changed

Session-based dispatch (complete end-to-end)

  • getCurrentSessionForProject retrieves the current session for a project from agent_sessions
  • dispatch/route.ts calls it and passes sessionId to injectPrompt
  • injectPrompt uses sessionId for project lookup instead of tab-name matching when provided
  • sessionId flows from web → pending_commands → Fleet Runner → Claude CLI via --resume
  • Runner tracks launched sessions in sessionIdByTab map and only terminates/relaunches PTY when session IDs differ
  • If no session exists, sessionId is undefined and the legacy tab-based path starts a new session

Watch and Focus now use project keys, not tab names (NEW)

  • fleetSurfaceHref("terminal", project) generates /terminal?project=X instead of /terminal?tab=X
  • Terminal accepts both ?project= (new) and ?tab= (legacy) for backward compatibility
  • All Watch links (Control inbox, ProjectCard, Activity, Loki, RunModal) use ?project=
  • All "Open session" links use ?project=
  • Focus terminal already uses workspaceTab = project.liveTab ?? project.tab for resolution
  • Terminal's existing resolveProjectToTab logic handles mapping project keys to actual tab names

This 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

  • Web layer: inject-core.ts, executor.ts, pending-commands.ts (schema), dispatch/route.ts
  • Agent layer: agents/types.ts, agents/claude.ts, agent-registry.ts, agent-execution/launch.ts
  • Runner layer: desktop/src/main/pty-runtime.ts, poller.ts, command-validator.ts
  • Query layer: db/queries/agent-sessions.ts (new getCurrentSessionForProject)
  • URL layer (NEW): fleet-context.ts, TerminalPageClient.tsx, TerminalSurface.tsx
  • Component layer (NEW): ProjectCard.tsx, ProjectStatusChips.tsx, NeedsYouCard.tsx, ActivityEventRow.tsx, RunModal.tsx, Transcript.tsx
  • Test: fleet-context.ts updated for new URL format

Testing on live site

After the build stamp moves to this branch:

  1. Feedback dispatch with session resume:

    • Open Feedback (/feedback)
    • Find a report for a project that already has an open Claude session
    • Click Dispatch
    • Verify: the queued inject should resume the existing session (check the Claude CLI shows the same session ID, not a new one)
  2. Watch from Control:

    • Open Control (/control)
    • Find a project card (e.g., "fleetcrown")
    • Click "Watch" link
    • Verify: URL is /terminal?project=fleetcrown (not ?tab=Bitbaum)
    • Verify: Terminal shows the correct session output for that project
  3. Watch from Activity:

    • Open Activity with a failed event
    • Click "Open session" link
    • Verify: URL is /terminal?project=X (not ?tab=X)
    • Verify: Terminal shows the session
  4. Focus terminal:

    • Open Control
    • Click "Focus terminal" on a project card
    • Verify: no "tab not found" banner when project name differs from live tab name
    • Verify: the correct terminal is focused on your machine
  5. New session start (no existing session):

    • Dispatch to a project with no current Claude session
    • Verify: a new session starts (does not return "Start a session first" error)
  6. Session reuse without PTY restart:

    • Have a Claude session running in a PTY
    • Queue another inject to the same project
    • Verify: the PTY does NOT terminate and restart (should reuse the same Claude session without interruption)

What's unproven

  • The full Watch → Terminal → display flow for ?project= URLs needs verification on the live site
  • Whether resolveProjectToTab correctly maps all project names to their live tabs in production
  • Focus terminal with session-based lookup (currently still uses liveTab fallback, not sessionId from DB)
  • The desktop runner's PTY reuse logic (avoiding unnecessary restarts) needs production verification

Not in this PR

  • Terminal does not yet accept ?session=<id> directly (still uses project → tab resolution)
  • Focus terminal API does not yet look up sessionId from the database (relies on liveTab being set)
  • agent_sessions does not store the tab name (session → tab mapping happens via project key)
Open in Web Open in Cursor 

…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.
@cursor cursor Bot changed the title Fix terminal session lookup: auto-switch sources and improve stalled hints Fix terminal session lookup: case-insensitive matching across all entry points Sep 9, 2026
…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.
@cursor cursor Bot changed the title Fix terminal session lookup: case-insensitive matching across all entry points Fix terminal session lookup: resolve project names to live tab names Sep 9, 2026
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.
@catomean

Copy link
Copy Markdown
Collaborator Author

Closing: tab-name matching is obsolete. Prefer project session/repo/URL (see fleetSurfaceHref ?project=). Do not merge.

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.

2 participants