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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
//! Vertical layout + rendering for the coordinator TUI skeleton.
//!
//! Why: the spec (DOC-13 §3) pins a Claude-Code-like shape — input box on
//! TOP, the live session list in the MIDDLE (controller bullet always row 0),
//! and a status/key bar at the BOTTOM. Keeping the frame composition here,
//! separate from state and key dispatch, isolates the only terminal-touching
//! render path and keeps each file under the SLOC cap.
//! What: [`render`] draws the three regions for one frame; the small helpers
//! ([`input_line`], [`status_bar_text`]) build the text the layout shows and are
//! reused by tests. The session list is built from the pure row-builders in
//! [`super::rows`], so the *content* is unit-tested without a terminal.
//! Test: text helpers are unit-tested in `super::tests`; the full `render` is
//! exercised by launching the TUI (terminal glue).
use ;
use MIN_VISIBLE_ROWS;
use ;
use CoordinatorState;
/// The prompt prefix shown in the input box.
///
/// Why: the spec's mockup shows a `coordinator ›` prompt; a constant keeps the
/// prompt single-sourced between the renderer and its test.
/// What: the literal `coordinator › `.
/// Test: `input_line_shows_prompt_and_cursor`.
pub const INPUT_PROMPT: &str = "coordinator › ";
/// The bottom status/key-hint bar text.
///
/// Why: the spec's status bar lists the slash commands + global keys; the
/// skeleton shows the (stubbed) command names plus the keys Child #1 implements.
/// What: a one-line hint string drawn reversed at the bottom.
/// Test: `status_bar_lists_keys`.
pub const STATUS_BAR_HINT: &str = "/new /sessions /send /stop /resume /kill /attach /help · ↵ send · ↑↓/jk select · PgUp/PgDn scroll · q quit";
/// The fixed height (in rows, incl. the two border rows) of the output pane.
///
/// Why: the output log needs a stable region that does not starve the session
/// list on a short terminal; a fixed height keeps the layout predictable while
/// the pane shows the most recent log lines (the tail).
/// What: 8 rows — 6 visible log lines plus the top/bottom borders.
/// Test: covered indirectly by `output_tail_keeps_most_recent`.
pub const OUTPUT_PANE_HEIGHT: u16 = 8;
/// The synthetic right-column status shown for the controller bullet.
///
/// Why: the controller has no daemon summary; its right column is a synthetic
/// readiness string (§3.3). A constant keeps the skeleton value in one place.
/// What: the literal `idle · 0 delegations · ready`.
/// Test: `controller_status_is_synthetic`.
pub const CONTROLLER_STATUS: &str = "idle · 0 delegations · ready";
/// Build the input-box line: prompt + buffer + a cursor glyph.
///
/// Why: kept pure so a test can assert the rendered prompt and trailing cursor
/// without a terminal frame.
/// What: returns `coordinator › <buffer>_` (the trailing `_` is the cursor).
/// Test: `input_line_shows_prompt_and_cursor`.
/// Build the bottom status-bar text.
///
/// Why: a single helper keeps the bar text consistent and testable.
/// What: returns [`STATUS_BAR_HINT`] (Child #1 has no transient last-action
/// note; later children may fold one in here).
/// Test: `status_bar_lists_keys`.
/// The status-bar indicator shown when the daemon answered its last probe.
///
/// Why: Child #2 polls the daemon live; the operator needs an at-a-glance
/// signal that the list is fresh. A constant keeps the reachable glyph + label
/// single-sourced between the renderer and its test.
/// What: the literal `daemon ●`.
/// Test: `daemon_indicator_reflects_reachability`.
pub const DAEMON_REACHABLE: &str = "daemon ●";
/// The status-bar indicator shown when the daemon is unreachable.
///
/// Why: the counterpart to [`DAEMON_REACHABLE`] — a distinct glyph so a
/// daemon-down state reads differently from a healthy one, mirroring the cleared
/// (empty) session list `coord_poll_daemon` produces on failure.
/// What: the literal `daemon ✗`.
/// Test: `daemon_indicator_reflects_reachability`.
pub const DAEMON_UNREACHABLE: &str = "daemon ✗";
/// Pick the daemon-reachability indicator for the current state.
///
/// Why: keeps the reachable/unreachable branch out of [`render`] and gives the
/// indicator a pure, testable seam.
/// What: returns [`DAEMON_REACHABLE`] when `state.daemon_reachable`, else
/// [`DAEMON_UNREACHABLE`].
/// Test: `daemon_indicator_reflects_reachability`.
/// The status-bar hint shown when the daemon reports the catalog is stale (HR-3).
///
/// Why: DOC-17 §HR-3 requires a non-intrusive "update available" signal plus a
/// hint at the rebuild action; naming the literal here single-sources it between
/// the renderer and its test. The trailing command names the apply path so the
/// operator knows HOW to act on the offer.
/// What: the literal `catalog: ↑updates available (tm catalog apply)`.
/// Test: `catalog_indicator_reflects_staleness`.
pub const CATALOG_STALE_HINT: &str = "catalog: ↑updates available (tm catalog apply)";
/// The catalog-staleness hint for the current state, if any.
///
/// Why: the status bar only shows the rebuild offer when there IS an update, and
/// only when the daemon is reachable (a down daemon's flag is meaningless). A
/// pure helper keeps that branch out of [`render`] and testable without a
/// terminal.
/// What: returns `Some(`[`CATALOG_STALE_HINT`]`)` when the daemon is reachable
/// AND `catalog_stale`, else `None`.
/// Test: `catalog_indicator_reflects_staleness`.
/// The most recent `rows` lines of the output log (its tail), cloned for render.
///
/// Why: the output pane is fixed-height, so it shows the newest lines (a true
/// scrollback view would need its own scroll state — out of scope for Phase 1C).
/// A pure tail-slicer keeps the "keep the most recent N" rule testable without a
/// terminal.
/// What: returns a clone of the last `rows` lines of `state.output` (all of them
/// when the log is shorter than `rows`), oldest-first so they render top-to-bottom.
/// Test: `output_tail_keeps_most_recent`.
/// Build the unified, numbered session-list items: controller row 0, then
/// numbered sessions (1, 2, 3, …).
///
/// Why: STUI-1 (#1278) requires a NUMBERED list whose selected session expands
/// to the two-column `[id] │ [summary]` form. Building the `ListItem`s here (from
/// the pure row-builders) keeps `render` short and the row content testable; the
/// row highlight itself is applied by the `List` widget's `highlight_style` in
/// [`render`] (driven by `state.nav`), so this only owns the per-row TEXT.
/// What: returns one [`ListItem`] per row — the controller at index 0 (its right
/// column the synthetic status), then each session prefixed with its 1-based
/// list number via [`numbered_session_row`], with the selected session expanded
/// to `N. [id] │ [summary]` via [`active_row_two_col`].
/// Test: row *content* is covered by the `super::rows` tests; ordering here is
/// exercised by launching the TUI.
/// Build the controller row, honouring whether it is the active selection.
///
/// Why: keeps the selected/unselected branch out of [`session_list_items`].
/// What: delegates to [`controller_bullet_row`] with the synthetic status and
/// the current selection state.
/// Test: covered by `controller_bullet_row` tests + `controller_status_is_synthetic`.
/// Draw the coordinator TUI frame: input (top), list (middle), status (bottom).
///
/// Why: the single entry point the event loop calls each tick; composing the
/// vertical layout in one place matches the dashboard's `render` convention.
/// STUI-1 makes the list a STATEFUL, scrolling widget — it takes `&mut state` so
/// it can hand `state.nav`'s wrapped [`ListState`](ratatui::widgets::ListState)
/// to `render_stateful_widget`, which both reads the preserved selection/offset
/// and nudges the offset to keep the selection visible within the live viewport.
/// What: a vertical [`Layout`] — a 3-row input box, a flexing session list whose
/// middle region is floored at [`MIN_VISIBLE_ROWS`] rows plus the two border
/// rows, and a 1-row status bar; the session list pins the controller at row 0,
/// numbers the sessions, highlights the selected row via `highlight_style`, and
/// expands the selected session to two columns.
/// Test: the text/content helpers are unit-tested; this terminal-touching path
/// is exercised by launching the TUI.