1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
//! P5-4 (COMPOSABLE-HARNESS-DESIGN.md §2 module 30 `tui`; §1.9 recorded
//! deviation; §2.1 `tools.question`/`permissions.approvals`(ask-UI) →
//! `tui`|`server`): the full-screen interactive TUI, AND — because §2.1
//! names it as the interactive surface three EARLIER phases explicitly
//! deferred here — the home for the three handlers that close those
//! deferred chains:
//!
//! 1. **P5-1's approval ask-UI** — [`handlers::TuiApprovalHandler`]
//! implements `crate::permissions::PermissionsApprovalHandler`.
//! 2. **P5-2's elicitation prompt** (+ OAuth device-code display) —
//! [`handlers::TuiElicitationHandler`] implements
//! `crate::mcp::McpElicitationHandler`;
//! [`bridge::PendingOAuthDisplay`] carries the device-code modal push.
//! 3. **P5-3's parent-answerable child approvals** —
//! [`handlers::TuiChildApprovalHandler`], installed via
//! `crate::agent::Agent::set_child_approval_handler_factory` (P5-4's own
//! new seam — see that method's doc comment for why this can only ever
//! grant what the rule engine already routed to a prompt, never
//! escalate past a `Deny`).
//!
//! ## Two layers (why this crate only has ONE of them)
//!
//! A real terminal can't be driven headlessly in a unit test — so this
//! module is split in two, and only the TESTABLE half lives here:
//!
//! - **The view-model core** ([`state`], [`key`], [`keymap`], [`theme`],
//! [`history`], [`bridge`], [`handlers`]) — a pure `handle_key(KeyEvent)
//! -> Vec<Action>` / `apply(Action)` state machine plus the three
//! interactive handlers above. Zero terminal-library dependency (no
//! `ratatui`/`crossterm` in `crates/harness`'s `Cargo.toml` at all — see
//! [`key::KeyEvent`]'s doc comment) — every interaction in this crate is
//! unit-testable by constructing a [`key::KeyEvent`] by hand.
//! - **The render + event-loop layer** lives in `crates/cli/src/tui/` (a
//! binary-crate concern: `ratatui` + `crossterm`, an alternate-screen
//! terminal, real keyboard/paste input). It translates real
//! `crossterm::event::KeyEvent`s into [`key::KeyEvent`], drives
//! [`state::TuiState`], and renders the result — see that module's own
//! doc comment for the render loop and its `TestBackend` smoke test.
//!
//! ## Activation
//!
//! `Config::tui_enabled` (`capabilities.tui.enabled`, default `false`) is
//! the master gate — `crates/cli`'s `chat()` runs the pre-P5-4 rustyline
//! REPL loop byte-for-byte when it's off, or when stdin/stdout/stderr
//! aren't all a real tty (`--print`, a piped/non-interactive invocation,
//! or any headless test harness) — see [`should_activate`].
//!
//! ## Shippable-complete vs honestly-staged
//!
//! - **Shippable-complete:** the view-model core (composer editing,
//! scrollback, streaming accumulation, the three interactive-handler
//! modals, theme toggle, configurable global keybindings, cross-session
//! Ctrl+R prompt-history search, BASIC vim emulation), the render/
//! event-loop layer, image-paste passthrough — a placeholder token +
//! [`state::TuiState::pending_images`], drained by `crates/cli`'s
//! render loop via [`state::TuiState::take_pending_images`] and routed
//! into the turn as real multimodal content (`Agent::send_with_images`)
//! — external-`$EDITOR` invocation.
//! - **Honestly staged, not half-built:**
//! - FULL vim emulation (registers, visual mode, `.`-repeat, counts,
//! `dd`/`yy`/`p` as real two-keystroke commands rather than the
//! single-keystroke approximation
//! [`state::TuiState::handle_key`]'s vim-normal-mode match
//! implements) — see [`state::VimMode`]'s doc comment.
//! - OAuth device-code display (`bridge::TuiBridge::oauth_sender` /
//! [`bridge::PendingOAuthDisplay`] / `Action::ShowOAuthModal`): the
//! plumbing is complete and tested end-to-end (push → modal → render
//! → dismiss), but `crates/cli` has no call site that ever SENDS into
//! it — `attach_mcp`'s OAuth handling (`resolve_oauth_header`) only
//! ever consults an ALREADY-stored, possibly-refreshed token; a fresh
//! device-code flow (`mcp_oauth::run_device_flow`) only ever runs via
//! the separate `supercode mcp login <name>` subcommand, which never
//! activates the TUI. So a TUI session currently never has a moment
//! where a device code needs displaying at all — wiring one in would
//! mean teaching `attach_mcp` to trigger a live, session-startup-
//! blocking device-code flow when no cached token exists, a real
//! interactive-flow design decision (timeout? cancel-and-continue
//! without the server? non-interactive callers?) out of scope for
//! this module's own render-loop-and-handlers job. Kept (not removed)
//! as the ready seam for whichever future unit makes that call —
//! zero cost while unused, the same "installing a handler alone
//! changes nothing" posture every other optional seam in this crate
//! has. Cited as a follow-up for this module, not a broken partial
//! implementation — same posture as the vim item above.
pub use ;
pub use ;
pub use PromptHistory;
pub use ;
pub use ;
pub use ;
pub use Theme;
/// Whether the TUI should activate for this process: `config.tui_enabled`
/// AND stdin/stdout/stderr are all a real terminal. `crates/cli`'s `chat()`
/// is the sole call site that matters for the "default-off / non-tty /
/// `--print` = byte-identical REPL" contract (P5-4's build brief) — a
/// piped/non-interactive invocation (virtually every CI/test run,
/// regardless of whether a parity preset sets `capabilities.tui.enabled =
/// true`) always falls through to the pre-P5-4 REPL because THIS check
/// fails, not because the config bit is off.
///
/// Takes the three tty booleans as explicit parameters (rather than
/// calling `std::io::IsTerminal` itself) so this stays testable without a
/// real terminal — `crates/cli` passes
/// `io::stdin().is_terminal()`/`io::stdout().is_terminal()`/
/// `io::stderr().is_terminal()` at the real call site.