# План: инструмент терминала — гэпы + UI управления терминалами

Контекст: см. анализ в конце обсуждения (terminal.py, terminal_manager.py, navi_code profile).
Гэпы: TUI не обрабатывает `terminal_output`/`terminal_closed`; `TerminalClosed` не
эмитируется; live-stream background-терминала обрывается после `open` (per-tool sink
закрывается); нет TUI-рендерера terminal tool_call; нет способа посмотреть/закрыть терминалы из UI.

Решение по live-выводу: **модалка + count** (не live-панель). TUI накапливает `terminal_output`
по терминалам; `/terminals` модалка показывает список + last output; статус-бар — count открытых.

## Этап 1 — Backend: session-broadcast sink + lifecycle events

**Корень гэпов 2+3:** `event_sink` сейчас per-tool-call (`current_event_sink.set` в
`_execute_tools_with_sink`, `_TOOL_DONE` после возврата `open`). Background-терминал живёт в
`terminal_manager`, его reader-tasks пишут в `output_buffer` (deque 500), но в стрим уже некому.
`TerminalClosed` не эмитируется вообще.

**Что делаем:**
- `TerminalManager`: per-session broadcast sink — `bind_session(session_id, sink)` /
  `unbind_session(session_id)`. Хранит `dict[session_id, sink]`. Reader-tasks и lifecycle-события
  пишут в session-sink (живёт пока сессия), не в per-tool sink.
- `terminal_manager.open`: reader-tasks (`_read_stream`) → `TerminalOutputDelta` в session-sink
  (если bound), не в per-tool `event_sink`. Per-tool sink остаётся только для immediate open-feedback.
- `terminal_manager._close_one`: emit `TerminalClosed(name, reason)` в session-sink.
- Новое событие `TerminalOpened` (`events.py`: dataclass + `to_wire` → `{"type":"terminal_opened",
  "terminal_name","description","pid","background"}`), emit в `open` (после старта proc).
- `orchestrator`/`container`: `bind_session(session_id, broadcast)` при старте хода/attach,
  `unbind` + `close_all` на session end / shutdown. (Точка: где session-broadcast создаётся —
  orchestrator, который шлёт WS.)

**Риск:** средний. Меняет sink-архитектуру terminal_manager. Совместимость: webclient уже
handle `terminal_output`/`terminal_closed`; `terminal_opened` — новый handler (мелочь).

## Этап 2 — Backend: REST endpoints для list/close

`/terminals` модалке нужен источник списка + закрытие. Тянуть через REST (как sessions_picker).

- `GET /sessions/{id}/terminals` → `terminal_manager.list(session_id)` (массив summary).
- `POST /sessions/{id}/terminals/{name}/close` → `terminal_manager.close(session_id, name)`.
- `api.py` (terminal client, async): `list_terminals(session_id)`, `close_terminal(session_id, name)`.

**Риск:** низкий. Прозрачные endpoint-ы над существующим terminal_manager.

## Этап 3 — TUI: `terminal` tool_call renderer (action-aware)

Сейчас `tool_call` с `tool="terminal"` → generic `ToolResultRenderer` (без структуры). `filesystem`
имеет спец-рендерер; `terminal` — нет.

- `renderers/terminal.py`: action-aware (по образцу filesystem):
  - `run`: команда + exit-code (success/error цвет) + output (capped).
  - `open`: `terminal_name` + description + PID + background-флаг.
  - `list`: таблица активных (статус-эмодзи, name, description, PID, uptime).
  - `status`: name/desc/command/PID/cwd/uptime + output tail.
  - `send_input`: echo «Sent input to <name>».
  - `close`: «Terminal <name> closed».
- Регистрация в `renderers/__init__.py` **перед** generic `ToolResultRenderer` (как filesystem/todo).
- `chat_panel._item_msg` для `tool_call` уже раскрывает meta — `terminal` renderer читает
  `args.action`/`result`/`success`/`metadata`.

**Риск:** низкий. Только рендер.

## Этап 4 — TUI: handle terminal events + count в статусе

- `ChatModel.handle_ws_event`: cases для `terminal_opened` / `terminal_output` / `terminal_closed`:
  - накапливать состояние `terminals: dict[name → {status, output_tail, pid, background, closed}]`;
  - **не** создавать chat-пузырь (как `model_info`/`todo_updated` — `return` без форварда в чат).
- `tui_app.on_ws_event`: forward в chat_model + обновлять StatusPanel count.
- `StatusPanel`: заменить `_hint` («Ctrl+P palette | /help commands») на «Terminals: N».
  Seed на attach (Этап 2 `api.list_terminals`), live-обновление из events.
- Seed count на `attach_session` через `api.list_terminals` (async, в worker — как `get_todos`).

**Риск:** низкий-средний. StatusPanel hint → count (видимый UX change, может захотеть combos
куда-то перенести — но пользователь сказал заменить).

## Этап 5 — TUI: `/terminals` команда + модалка

- `screens/terminals_picker.py` (модалка, по образцу `sessions_picker`):
  - список: `api.list_terminals(session_id)` (REST seed) + live из ChatModel-состояния
    (terminal_output обновляет output_tail, terminal_opened/closed — список).
  - строка: статус-эмодзи + name + description + PID + uptime; expand → last output tail.
  - `up/down` — навигация, `enter` — **close** выбранного (`api.close_terminal`),
    `escape` — cancel. Без подтверждения (явное действие в модалке).
- `commands/builtin.py`: `TerminalsCommand` (`/terminals`) → `app._open_terminals_picker()`.
  meta.keybind — опционально (напр. `ctrl+x t` свободен? сейчас `ctrl+x t` = toggle thinking;
  подберём свободный или без keybind).
- `tui_app._open_terminals_picker`: push screen + callback (close → `run_worker`).

**Риск:** средний. Модалка + live-обновление списка при events (refresh при terminal_output/
opened/closed, если модалка открыта).

## Порядок и контроль

Этапы 1→2 (backend) → 3→4→5 (TUI). Каждый: подтверждение подхода → реализация + тесты →
полный pytest зелёный → коммит (без Co-Authored-By).

Зависимости: Этап 4/5 зависят от 1 (events) + 2 (REST). Этап 3 (renderer) независим.

## Тесты (по этапам)

- **1:** terminal_manager bind/unbind; `terminal_opened`/`terminal_closed` emit в session-sink;
  background output стримится после возврата `open`.
- **2:** REST list/close (через test-client + mock terminal_manager).
- **3:** renderer: каждый action → структура (run exit-code, open PID, list таблица, status tail).
- **4:** ChatModel cases (накапливает, не создаёт пузырь); StatusPanel «Terminals: N» обновляется.
- **5:** модалка: список seed + close → `api.close_terminal`; live refresh.

## Out of scope

- Live-панель с реалтайм-выводом (отклонено — модалка + count).
- Подсветка terminal-output как код (минор, можно позже).
- Дедуп `_resolve_working_dir` (terminal/code_exec) — maintainability, не в этом плане.