Контекст: см. анализ в конце обсуждения (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 открытых.
Корень гэпов 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 (мелочь).
/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.
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 ».close: «Terminal closed».renderers/__init__.py перед generic ToolResultRenderer (как filesystem/todo).chat_panel._item_msg для tool_call уже раскрывает meta — terminal renderer читает args.action/result/success/metadata.Риск: низкий. Только рендер.
ChatModel.handle_ws_event: cases для terminal_opened / terminal_output / terminal_closed:
terminals: dict[name → {status, output_tail, pid, background, closed}];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.attach_session через api.list_terminals (async, в worker — как get_todos).Риск: низкий-средний. StatusPanel hint → count (видимый UX change, может захотеть combos куда-то перенести — но пользователь сказал заменить).
/terminals команда + модалкаscreens/terminals_picker.py (модалка, по образцу sessions_picker):
api.list_terminals(session_id) (REST seed) + live из ChatModel-состояния (terminal_output обновляет output_tail, terminal_opened/closed — список).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) независим.
terminal_opened/terminal_closed emit в session-sink; background output стримится после возврата open.api.close_terminal; live refresh._resolve_working_dir (terminal/code_exec) — maintainability, не в этом плане.