Skip to content

feat(terminals): add observable tmux sessions - #6475

Closed
arekhalpern wants to merge 1 commit into
pingdotgg:mainfrom
arekhalpern:feat/observable-tmux-sessions
Closed

arekhalpern wants to merge 1 commit into
pingdotgg:mainfrom
arekhalpern:feat/observable-tmux-sessions

Conversation

@arekhalpern

@arekhalpern arekhalpern commented Aug 13, 2026 •

Copy link
Copy Markdown

What Changed

T3 Code now exposes intentional long-running work through the existing terminal experience:

  • Codex and Claude receive guidance to place servers, watchers, and long builds in named detached tmux sessions.
  • The server discovers, attaches to, and stops exact tmux sessions through typed terminal RPCs.
  • Web and desktop show a compact running-task tab above the composer and open discovered sessions in the terminal drawer.
  • The drawer lists attached sessions in its right-hand rail and provides a confirmed Stop session action.
  • Mobile can discover and attach to tmux sessions from its terminal menu.

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

Before Running sessions
Composer without a background-session indicator Composer showing two background tasks

Terminal drawer showing attached tmux sessions in the right-hand rail

Interaction video: open all running sessions from the composer tab

Verification

  • 88 focused contract, terminal manager, and authorization tests passed.
  • Contracts, client-runtime, server, and web typechecks passed.
  • Targeted lint and diff validation passed.
  • Integrated web pass covered two-session discovery, composer status, drawer attachment, live output, session switching, and session termination refresh.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for UI changes
  • I included a video for the interaction change

Built with GPT-5.6-Sol through the Codex harness in T3 Code.


Note

Medium Risk
Touches terminal lifecycle, host tmux execution, 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: TerminalOpenInput gains optional launch (shell vs tmux + session name). The terminal manager discovers sessions via tmux list-sessions, attaches with tmux attach-session (no shell fallback on spawn failure), and kills exact sessions via tmux kill-session. Snapshots/summaries carry launch and tmux-specific labels. New RPCs terminal.listTmuxSessions and terminal.killTmuxSession are 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 exit to 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

  • Extends the terminal contract with TerminalLaunch (shell vs tmux), TmuxSessionDiscovery, and kill/list schemas; snapshots and summaries now include a required launch field.
  • Adds listTmuxSessions and killTmuxSession to TerminalManager, exposing them via two new WebSocket RPCs (terminal.listTmuxSessions, terminal.killTmuxSession) with AuthTerminalOperateScope enforcement.
  • Opening a terminal with launch.kind === 'tmux' now spawns tmux attach-session instead of a shell; if spawn fails, no shell fallback is attempted.
  • Adds a TmuxSessionStatusStrip in the chat view and a TmuxSessionMenu in the terminal drawer/panel so users can list, attach to, and stop tmux sessions from both web and mobile UIs.
  • Appends BACKGROUND_TERMINAL_INSTRUCTIONS to Claude and Codex system prompts, instructing the AI to use named detached tmux sessions for long-running background commands.
  • Risk: existing clients consuming TerminalSessionSnapshot or TerminalSummary must handle the new required launch field.
📊 Macroscope summarized 5b39660. 15 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 13, 2026
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9b6ea5c0-bf11-4f6e-9779-971d36f244f5

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

timeoutBehavior: "timedOutResult",
})
.pipe(Effect.option);
return Option.isSome(versionResult) &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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) &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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(() => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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: [] },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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"`.

Comment on lines +946 to +958
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]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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: {} });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5b39660. Configure here.

@macroscopeapp

macroscopeapp Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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.

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

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant