Repository navigation
feat(terminals): add observable tmux sessions - #6475
arekhalpern wants to merge 1 commit into
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| timeoutBehavior: "timedOutResult", | ||
| }) | ||
| .pipe(Effect.option); | ||
| return Option.isSome(versionResult) && |
There was a problem hiding this comment.
🟡 Medium terminal/Manager.ts:1309
tmuxSessionLister reports status: "available" with an empty sessions array for any failed list-sessions call when tmux -V succeeds. This hides running sessions on timeouts, socket/permission failures, or malformed environments; only the specific no-server/no-sessions result should be treated as an available empty list, while other failures should return unavailable or propagate an error.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/terminal/Manager.ts around line 1309:
`tmuxSessionLister` reports `status: "available"` with an empty `sessions` array for any failed `list-sessions` call when `tmux -V` succeeds. This hides running sessions on timeouts, socket/permission failures, or malformed environments; only the specific no-server/no-sessions result should be treated as an available empty list, while other failures should return `unavailable` or propagate an error.
| timeoutBehavior: "timedOutResult", | ||
| }) | ||
| .pipe(Effect.option); | ||
| return Option.isSome(versionResult) && |
There was a problem hiding this comment.
🟡 Medium terminal/Manager.ts:1342
Failed tmux kill-session calls are reported as notFound whenever tmux -V succeeds, so timeouts, permission errors, and broken-server failures are misrepresented as a missing session. Inspect the kill command's exit/error output and return notFound only for an explicit missing-session result; preserve other failures as unavailable or an error.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/terminal/Manager.ts around line 1342:
Failed `tmux kill-session` calls are reported as `notFound` whenever `tmux -V` succeeds, so timeouts, permission errors, and broken-server failures are misrepresented as a missing session. Inspect the kill command's exit/error output and return `notFound` only for an explicit missing-session result; preserve other failures as `unavailable` or an error.
| ], | ||
| ); | ||
|
|
||
| useEffect(() => { |
There was a problem hiding this comment.
🟡 Medium terminal/ThreadTerminalRouteScreen.tsx:560
tmuxDiscovery remains stale for the lifetime of the mounted screen: creating or removing a detached tmux session after the initial query leaves the terminal menu showing the old list, including sessions that no longer exist. The effect at lines 560–575 only reruns when selectedThread or isEnvironmentReady changes, and opening either menu does not trigger a refresh; refresh the discovery when the menu is opened (or otherwise invalidate it before rendering the actions).
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx around line 560:
`tmuxDiscovery` remains stale for the lifetime of the mounted screen: creating or removing a detached tmux session after the initial query leaves the terminal menu showing the old list, including sessions that no longer exist. The effect at lines 560–575 only reruns when `selectedThread` or `isEnvironmentReady` changes, and opening either menu does not trigger a refresh; refresh the discovery when the menu is opened (or otherwise invalidate it before rendering the actions).
| (result) => { | ||
| if (!active) return; | ||
| setTmuxDiscovery( | ||
| result._tag === "Success" ? result.value : { status: "unavailable", sessions: [] }, |
There was a problem hiding this comment.
🟡 Medium terminal/ThreadTerminalRouteScreen.tsx:568
A failed listTmuxSessions RPC is rendered as “tmux is not installed,” so timeouts, disconnects, authorization failures, and server errors produce a false diagnosis that remains until the screen remounts. Preserve the RPC error as a distinct state and render a retry/error message instead of mapping every failure to status: "unavailable".
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx around line 568:
A failed `listTmuxSessions` RPC is rendered as “tmux is not installed,” so timeouts, disconnects, authorization failures, and server errors produce a false diagnosis that remains until the screen remounts. Preserve the RPC error as a distinct state and render a retry/error message instead of mapping every failure to `status: "unavailable"`.
| const [discovery, setDiscovery] = useState<TmuxSessionDiscovery | null>(null); | ||
| const [loading, setLoading] = useState(false); | ||
| const [pendingKillSessionName, setPendingKillSessionName] = useState<string | null>(null); | ||
| const [killInFlight, setKillInFlight] = useState(false); | ||
|
|
||
| const refresh = useCallback(async () => { | ||
| setLoading(true); | ||
| const result = await listTmuxSessions({ environmentId, input: {} }); | ||
| setDiscovery( | ||
| result._tag === "Success" ? result.value : { status: "unavailable", sessions: [] }, | ||
| ); | ||
| setLoading(false); | ||
| }, [environmentId, listTmuxSessions]); |
There was a problem hiding this comment.
🟡 Medium components/ThreadTerminalDrawer.tsx:946
Overlapping refresh calls can leave the menu displaying a stale tmux-session snapshot: if an older discovery completes after a newer one, its setDiscovery overwrites the newer result, so ended sessions remain visible or newly created sessions are omitted. Track the latest refresh request and ignore results from superseded requests (including their loading state).
+ const refreshRequestRef = useRef(0);
const [loading, setLoading] = useState(false);
const [pendingKillSessionName, setPendingKillSessionName] = useState<string | null>(null);
const [killInFlight, setKillInFlight] = useState(false);
const refresh = useCallback(async () => {
+ const requestId = ++refreshRequestRef.current;
setLoading(true);
const result = await listTmuxSessions({ environmentId, input: {} });
+ if (requestId !== refreshRequestRef.current) return;
setDiscovery(
result._tag === "Success" ? result.value : { status: "unavailable", sessions: [] },
);
setLoading(false);🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/ThreadTerminalDrawer.tsx around lines 946-958:
Overlapping `refresh` calls can leave the menu displaying a stale tmux-session snapshot: if an older discovery completes after a newer one, its `setDiscovery` overwrites the newer result, so ended sessions remain visible or newly created sessions are omitted. Track the latest refresh request and ignore results from superseded requests (including their loading state).
| const refresh = useCallback(async () => { | ||
| const refreshSequence = refreshSequenceRef.current + 1; | ||
| refreshSequenceRef.current = refreshSequence; | ||
| const result = await listTmuxSessions({ environmentId, input: {} }); |
There was a problem hiding this comment.
🟡 Medium chat/TmuxSessionStatusStrip.tsx:42
When environmentId changes, the strip renders the previous environment’s tmux sessions until discovery completes, so clicking it passes stale session names to onOpenSessions and attempts to attach them in the new environment. Associate the session state with environmentId or clear it immediately on environment changes.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/chat/TmuxSessionStatusStrip.tsx around line 42:
When `environmentId` changes, the strip renders the previous environment’s tmux sessions until discovery completes, so clicking it passes stale session names to `onOpenSessions` and attempts to attach them in the new environment. Associate the session state with `environmentId` or clear it immediately on environment changes.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 5b39660. Configure here.
| liveSession.cwd = input.cwd; | ||
| liveSession.worktreePath = nextWorktreePath; | ||
| liveSession.runtimeEnv = nextRuntimeEnv; | ||
| liveSession.launch = nextLaunch; |
There was a problem hiding this comment.
Omitted launch overwrites tmux
High Severity
resolveTerminalLaunch maps a missing launch to { kind: "shell" }, and open/restart treat that as a real launch change. Re-opening or restarting an existing tmux terminal without an explicit launch (including attach with restartIfNotRunning) stops the attach and spawns a normal shell instead of keeping the tmux session.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 5b39660. Configure here.
ApprovabilityVerdict: Needs human review 6 blocking correctness issues found. This PR introduces a new feature for observable tmux sessions with new RPC endpoints, UI components across mobile/web, and backend process management. The scope (1065 additions of new logic) and an unresolved High severity finding about launch parameter handling warrant human review. You can customize Macroscope's approvability policy. Learn more. |


What Changed
T3 Code now exposes intentional long-running work through the existing terminal experience:
Tmux remains the source of truth for process lifetime and scrollback. Discovery runs on lifecycle events and user actions, without a polling loop or a second persisted task model.
Why
Provider background shells make long-running processes difficult to observe, especially from remote clients. T3 Code already has a remotely accessible PTY surface, while tmux already provides detached lifetime, scrollback, and multi-client attachment. Connecting those systems gives users visibility and control with a small runtime model.
UI Changes
Interaction video: open all running sessions from the composer tab
Verification
Checklist
Built with GPT-5.6-Sol through the Codex harness in T3 Code.
Note
Medium Risk
Touches terminal lifecycle, host
tmuxexecution, and provider instructions; kill-session is destructive but scoped to exact session names with confirmation in the UI.Overview
Observable long-running work is wired through the existing terminal stack instead of a new task model. Codex and Claude get appended instructions to use named detached tmux sessions for servers, watchers, and long builds; Claude’s system prompt gets the same block.
Server & contracts:
TerminalOpenInputgains optionallaunch(shellvstmux+ session name). The terminal manager discovers sessions viatmux list-sessions, attaches withtmux attach-session(no shell fallback on spawn failure), and kills exact sessions viatmux kill-session. Snapshots/summaries carrylaunchand tmux-specific labels. New RPCsterminal.listTmuxSessionsandterminal.killTmuxSessionare authorized like other terminal operate methods.Web: Composer status strip when tmux sessions exist; clicking opens the drawer and attaches all (reusing drawer tabs when already open). Thread terminal drawer adds attach/stop-session menus, keeps the session rail visible for tmux-only cases, and closes tmux tabs without sending
exitto the PTY. Mobile mirrors attach via terminal menus with loading/unavailable/empty states.Docs: User-facing note on attach, detach-on-close, and stop-session behavior.
Reviewed by Cursor Bugbot for commit 5b39660. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add observable tmux sessions to terminal UI and AI provider instructions
TerminalLaunch(shell vs tmux),TmuxSessionDiscovery, and kill/list schemas; snapshots and summaries now include a requiredlaunchfield.listTmuxSessionsandkillTmuxSessiontoTerminalManager, exposing them via two new WebSocket RPCs (terminal.listTmuxSessions,terminal.killTmuxSession) withAuthTerminalOperateScopeenforcement.launch.kind === 'tmux'now spawnstmux attach-sessioninstead of a shell; if spawn fails, no shell fallback is attempted.TmuxSessionStatusStripin the chat view and aTmuxSessionMenuin the terminal drawer/panel so users can list, attach to, and stop tmux sessions from both web and mobile UIs.BACKGROUND_TERMINAL_INSTRUCTIONSto Claude and Codex system prompts, instructing the AI to use named detached tmux sessions for long-running background commands.TerminalSessionSnapshotorTerminalSummarymust handle the new requiredlaunchfield.📊 Macroscope summarized 5b39660. 15 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted
🗂️ Filtered Issues
No issues evaluated.