Ctrl K
ComunidadGuías…

feature-map/terminals.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Terminals

A session can show a live terminal next to or instead of the chat. There are two
kinds that look alike but are separate surfaces: the agent terminal, which runs
the native harness behind the Chat/Terminal switcher, and user shells, which a
user opens from the workspace rail. Both attach to a terminal on the session's
host, survive view switches, reconnect after a dropped connection, and must
reattach when the session moves to another host.

## Sub-features

- `agent-terminal-view`: the Chat/Terminal switcher shows the harness terminal
  and returns to chat with the terminal's content kept. States: starting up
  (no Resume action), automatically restarting on an online host, stopped
  (Resume offered after recovery fails or when the host is offline), connected.
  Automatic recovery runs once when opening Terminal. Leaving it open does not
  revive an idle-reaped pane. Failed recovery stays manual across view switches.
- `user-shell`: open a shell, type into it, and close it.
- `scrollback`: scroll back with the mouse wheel (including programs that track
  the mouse), by touch on phones, and by keyboard.
- `reconnect`: after the terminal connection drops, the terminal shows that it
  is reconnecting and then reattaches without user action.
- `host-switch-reattach`: after "Switch host…", both the terminal strip and the
  agent terminal attach to the new host's terminal, not the old one.
- `direct-attach`: when the runner is on the same machine the terminal connects
  directly, and falls back to the relay when the direct route is unreachable.
- `tmux-outage`: terminals stay usable through temporary tmux health-check
  failures and still notice a real exit afterwards.
- `hidden-terminal`: in chat view, the background terminal never draws over the
  chat or composer.
- `dialog-routing`: when the harness is waiting on a dialog in its terminal, chat
  says so and routes the user to the terminal. When Claude Code still reports a
  running turn but its input box has been hidden for 15 seconds (for example a
  dialog opened while a reply streams), chat says something in the terminal may
  need input, and the sidebar row moves to Needs attention with a "May need input"
  tag, until the input box returns. `!` shell mode, the ctrl+r history search
  and a draft quoting a dialog's title keep the input box in use, so they do
  not count as hidden. A runner connected to an older server sends no alert.
  Sending a Claude chat message or changing its settings returns an open
  detailed transcript view to its input box with Ctrl+O before typing.
- `split-panes`: behind `splittable_terminal_view_ui`, split the Terminal view
  into shell panes, resize them (each side stays within 25–75%), and close them.

## How to get to it (user POV)

**Agent terminal** (sessions whose harness has a terminal):

Native subagents that share a parent's terminal show their work in chat and have
no Chat/Terminal switcher. Open the parent session to use its terminal. Children
with their own terminal retain the switcher.

- Use the Chat/Terminal switcher in the session header.
- Press ⌘/Ctrl+Alt+\ to toggle between Chat and Terminal view. The chord is
  off while a rail-opened shell owns the main view.
- On a phone, use the view mode choice in the header menu.
- Choose Resume when the terminal shows as stopped.
- On desktop with `grid_sessions_display_ui` enabled, use each grid cell's header switch.
  The shortcut targets the active cell; shells remain in the shared Workspace.
- On desktop with `splittable_terminal_view_ui` enabled, use the header's "Split
  right" / "Split down" buttons in Terminal view and pick a shell type. Drag a
  divider (or focus it and use the arrow keys along its axis) to resize. "Close
  pane" asks for confirmation, then kills the focused shell; the agent pane
  cannot be closed. Panes are saved per session in this browser and show only
  for desktop owners outside grid cells. ⌘/Ctrl+Alt+Shift+\ splits right and
  ⌘/Ctrl+Alt+- splits down with the default shell, skipping the picker; the
  chords work only while those buttons show. ⌘/Ctrl+Alt+Shift+] / [ focus the
  next / previous pane. All of them work with focus inside a terminal.

**User shells:**

- Open a new shell from the workspace rail on desktop, or from the header menu
  on a phone.
- Press Control+` (on every platform) to open a shell of the remembered default
  type, also from a focused terminal. The chord is inert while the host is
  offline, the session is stranded, or a shell is still being created.
- In the desktop app, Control+` or File → New Terminal does the same, also
  while an embedded browser tab has focus. The menu item is disabled whenever
  the chord would be inert.
- Select an open shell from the rail's shell list; close it from its tab.
  In the Electron desktop app, Cmd/Ctrl+W also closes the selected visible
  workspace tab, using the same terminal confirmation. A read-only viewer's
  terminal stays open. Cmd/Ctrl+Shift+W closes the window.

**After the session moves:** open the host badge, choose "Switch host…", pick
another host, then look at both the terminal strip and the agent terminal.
Sandbox-backed sessions, including Databricks Sandbox and Arclet, cannot switch
hosts. Their "Switch host…" item is disabled; hover or focus it for the
unsupported explanation.

**Local terminal:** a native harness launched from the CLI runs in the user's
own terminal; see [native harnesses](./native-harnesses.md).

## Driving it with the repro environment

Preconditions: a running instance (`verify-env start`, then `verify-env
doctor`) and the built web UI. The mock instance has Claude and Codex terminals;
other harnesses need their real CLIs. Run each test through the instance:

```sh
verify-env run -- python -m pytest <test> --ui-skip-build --video=on \
  --output="$VERIFY_EVIDENCE/terminals"
```

- **`agent-terminal-view`:**
  `tests/browser_ui/chat/test_terminal_auto_resume.py` covers automatic recovery
  through the desktop switch, keyboard shortcut, mobile menu and grid switch,
  plus failed attempts followed by manual Resume, including a cached closed
  terminal that stays mounted during recovery. It uses the built SPA with
  mocked host, retry and terminal transports; run with
  `--browser-ui-skip-build --reruns 0 --video=on`.
  Grid cells: `tests/browser_ui/chat/test_session_grid.py` covers two real xterm
  surfaces against mocked terminal WebSockets, independent input, per-cell
  switches and keeping connections across view changes. Run without a server
  using `--browser-ui-skip-build --reruns 0 --video=on`.
  `tests/e2e_ui/shells/test_new_shell.py::test_starting_terminal_view_shows_loading_without_resume`,
  `tests/e2e_ui/shells/test_new_shell.py::test_stopped_terminal_view_keeps_resume`.
  The stopped state with a selectable empty terminal (own environment):
  `tests/e2e_ui/shells/test_new_shell.py::test_empty_terminal_view_remains_selectable_and_resumable`
  kills the session's runner, so it fails under `verify-env run`; run it with
  plain `uv run pytest`.
  Native child ownership and live output (opt-in, own environment):
  `tests/e2e/test_codex_native_child_visibility_e2e.py`.
  This uses real Codex credentials and holds a child command open while checking
  the parent terminal and child chat; see the prerequisites in
  `tests/harnesses/codex_native/forwarder/README.md`.
- **`user-shell`:**
  `tests/e2e_ui/shells/test_new_shell.py::test_new_shell_launches_and_opens`,
  `tests/e2e_ui/shells/test_new_shell.py::test_new_shell_accepts_typed_command`.
  A program's terminal queries get exactly one reply:
  `tests/e2e_ui/shells/test_new_shell.py::test_shell_program_gets_one_reply_to_a_cursor_position_query`.
  Closing a shell has no e2e coverage: close its tab, confirm, and expect the
  tab to disappear.
- **`agent-terminal-view`, `user-shell`, `split-panes`, keyboard:** no e2e
  coverage; component tests `web/src/hooks/useViewModeToggleHotkey.test.tsx`,
  `web/src/hooks/useNewShellHotkey.test.tsx`,
  `web/src/hooks/useTerminalSplitHotkey.test.tsx`,
  `web/src/hooks/usePaneCycleHotkey.test.tsx`,
  `web/src/shell/TerminalSplitControls.test.tsx` and
  `web/src/shell/MainTerminalView.test.tsx` (`pnpm --dir web test`). Manually,
  in a Claude or Codex session with the composer focused: press ⌘/Ctrl+Alt+\
  twice and expect Terminal view, then Chat view; press Control+` and
  expect a new shell tab to open and take focus. With `splittable_terminal_view_ui`,
  in Terminal view with the agent terminal focused, press ⌘/Ctrl+Alt+Shift+\
  and expect a default shell pane on the right that takes typing; press
  ⌘/Ctrl+Alt+Shift+] and expect typing to reach the agent pane again.
- **`scrollback`, wheel:**
  `tests/e2e_ui/shells/test_new_shell.py::test_shell_wheel_scroll_reaches_mouse_tracking_program`
- **`scrollback`, touch:**
  `tests/e2e_ui/mobile/test_terminal_touch_scroll.py::test_terminal_touch_swipe_scrolls_back`
- **`scrollback`, keyboard:** no e2e coverage. In a Codex terminal with more
  output than fits, press Page Up and expect older output; then check that the
  Codex terminal's other keys still behave as before.
- **`reconnect`:**
  `tests/e2e_ui/shells/test_terminal_bridge_reconnect.py::test_embedded_terminal_reconnects_after_transport_close`
- **`host-switch-reattach`:**
  `tests/e2e_ui/sessions/test_host_switch_reattaches_terminal.py::test_switching_host_reattaches_the_web_terminal`
  covers the terminal strip. The agent terminal has only web unit coverage:
  after the switch, open Terminal view and expect the new host's terminal.
- **`direct-attach`:**
  `tests/e2e_ui/shells/test_terminal_direct_attach.py::test_terminal_upgrades_to_loopback_attach_when_runner_is_local`,
  `tests/e2e_ui/shells/test_terminal_direct_attach.py::test_terminal_falls_back_to_relay_when_loopback_is_unreachable`
- **`tmux-outage`:**
  `tests/e2e_ui/shells/test_antigravity_tmux_recovery.py::test_antigravity_survives_probe_outage_then_detects_exit`
  (opt-in; Antigravity only).
  `tests/harnesses/claude_native/test_claude_native_overdrawn_prompt.py::test_pane_liveness_reads_the_requested_pane`
  checks both shared WebSocket probes against real split tmux panes, including
  an exited neighbor. This is a process check without a browser or vendor CLI.
- **Launch failure and cancellation (plain `uv run pytest`, own tmux):**
  `tests/terminals/test_launch_failure_cleanup.py` checks a missing tmux
  executable at spawn and cancellation after a real pane starts, including
  during the initial liveness check. Each must release its private directory
  and tmux server before registry ownership; a caller cancelled during that
  cleanup still gets a completed close first. This registry is shared by native
  agent terminals, user shells, and `sys_terminal_launch`; no vendor model or
  browser is used in these process checks.
  `tests/e2e/test_terminal_launch_retry_cleanup_e2e.py` also drives the runner's
  resource facade through three failed launches and a successful retry for
  Claude/Codex terminal roles and an auxiliary shell. The successful real pane
  must accept input, and final cleanup must release every allocated instance.
  `tests/runner/test_native_terminal_start_error_payload.py::test_host_setup_error_surfaces_its_remedy`
  (plain `uv run pytest`) checks that a host without a usable tmux shows how
  to fix it, not a runner-log pointer.
- **`hidden-terminal`:**
  `tests/e2e_ui/chat/test_hidden_terminal_scrollbar.py::test_hidden_terminal_scrollbar_never_paints_over_foreground`
- **`dialog-routing`:**
  `tests/e2e_ui/chat/test_blocked_dialog_terminal_routing.py::test_chat_blocked_on_terminal_dialog_routes_user_to_terminal`
  injects the blocked event; it does not run a vendor CLI. The hidden-input-box
  hint is produced by
  `tests/e2e/test_claude_native_hidden_prompt_e2e.py::test_real_claude_dialog_opened_mid_reply_points_chat_at_terminal`
  (real Claude Code and tmux against a slow local Messages API; needs `claude`
  on `PATH`), with timing cases in `tests/runner/test_resource_registry.py`
  (`-k 'hidden_input_box or input_box_probe or queued_alert_delivery or
  input_box_in_another_mode'`), the pane matrix in
  `tests/harnesses/claude_native/test_claude_native_bridge.py`
  (`-k input_box_shown`), and the labels in `web/src/pages/ChatPage.test.ts` and
  `web/src/components/SessionStateBadge.test.tsx`.
  `tests/harnesses/claude_native/test_claude_native_copy_mode.py::test_message_leaves_detailed_transcript_before_pasting`
  checks a web send through a real tmux pane with a synthetic Claude composer.
  To verify with Claude Code, open Terminal, press Ctrl+O to show the detailed
  transcript, then send a follow-up from Chat. It should appear once and the
  terminal should return to its input box. Repeat with a model or effort change.
  For Claude's OSC
  working/blocked status with an attached or closed viewer and the flag-off
  fallback, use the [native working-status recipe](./native-harnesses.md#driving-it-with-the-repro-environment).

For the desktop Close shortcut, open a shell tab in an ordinary session,
focus its terminal, and press Cmd/Ctrl+W. Confirm any terminal-close prompt;
only that tab should close. Cancel the prompt to keep the tab and session.
Repeat as a read-only viewer: the shortcut should leave the terminal open.
Cmd/Ctrl+Shift+W should close the window. Native accelerator delivery requires
an Electron run; component tests cover the selection and confirmation paths.

## Gotchas

- The terminal strip and the agent terminal behind the Chat/Terminal switcher
  are different surfaces. A reattach or reconnect fix must be checked on both.
- A host switch updates the host badge immediately. The badge changing is not
  proof the terminal reattached; look for the new host's terminal content.
- The ⌘/Ctrl+Alt chords use ⌘ on macOS and Ctrl elsewhere, and they match the
  physical key, so AltGr+T on an international layout must not open a shell.
- Key bindings added for scrollback can change how full-screen harness terminals
  behave. After a scrollback change, drive the harness terminal itself, not only
  a plain shell.
- Terminal output is drawn on a canvas, so page text searches do not see it.
  Use the tests' approach (the harness's own transcript or program input)
  instead of reading the page.
- "Starting up" and "stopped" look similar. Starting up offers no Resume action;
  stopped does. Wait past startup before calling a terminal stopped. A
  cloud-sandbox session stays in Starting up until its sandbox is ready, which
  can take minutes.
- Tests that need a harness-specific or opt-in environment skip quietly without
  it. Check that the test ran, not only that the suite passed.