1	//! Picker **view** — render a `&PickerModel` to a ratatui frame.
2	//!
3	//! ratatui's double-buffered renderer removes the manual erase/redraw +
4	//! wrapped-row-counting hazard the raw-ANSI reference (`vercel-labs/skills`)
5	//! carried. Rendering is a pure function of the model + the frame area, so every
6	//! screen is asserted against a `TestBackend` `Buffer` in the unit tests (no
7	//! live terminal) — doyle's "assert the rendered buffer" gate.
8	// [impl->REQ-RUN-PICKER]
9
10	use ratatui::layout::{Constraint, Direction, Layout, Rect};
11	use ratatui::style::{Color, Modifier, Style};
12	use ratatui::text::{Line, Span};
13	use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph, Wrap};
14	use ratatui::Frame;
15
16	use super::model::{
17	    glyph, Category, ConfirmOption, EndpointRow, EpDisplay, EpStatus, PickerModel, Screen,
18	};
19
20	/// Amber — the harness-only (online, not broker-PTY-controllable) display state.
21	const AMBER: Color = Color::Rgb(255, 176, 0);
22
23	/// The colour for an endpoint's four-state display status (REQ-PICKER-1).
24	fn display_color(d: EpDisplay) -> Color {
25	    match d {
26	        EpDisplay::Offline => Color::DarkGray,
27	        EpDisplay::Online => Color::Green,
28	        EpDisplay::HarnessOnly => AMBER,
29	        EpDisplay::Controlled => Color::Blue,
30	    }
31	}
32
33	/// The pinned keybind legend (bottom row). Designed so a future tap-mode can
34	/// surface the same actions without the keys (the phone caveat).
35	const LEGEND: &str = "←→ category · ↑↓ item · enter confirm · esc back · / filter · n new · s shortcut";
36
37	/// Highlight style for the row under a cursor.
38	fn cursor_style() -> Style {
39	    Style::default()
40	        .fg(Color::Cyan)
41	        .add_modifier(Modifier::BOLD)
42	}
43
44	/// A status square span coloured by the endpoint's four-state DISPLAY status
45	/// (REQ-PICKER-1): green ■ online · gray ▢ offline · amber ■ harness-only · blue
46	/// ■ controlled. The glyph stays the online/offline ■/▢ duality; the colour
47	/// carries the four-state.
48	fn square_span(row: &EndpointRow) -> Span<'static> {
49	    let color = display_color(row.display_status());
50	    Span::styled(row.status.square().to_string(), Style::default().fg(color))
51	}
52
53	/// Render the whole picker for the current screen.
54	pub fn render(model: &PickerModel, f: &mut Frame) {
55	    let area = f.area();
56	    match model.screen {
57	        Screen::Kind => render_kind(model, f, area),
58	        Screen::CreateAdapter => render_create_adapter(model, f, area),
59	        Screen::CreateId => render_create_id(model, f, area),
60	        Screen::PickExisting => render_pick(model, f, area),
61	        Screen::Confirm => render_confirm(model, f, area),
62	        Screen::Resume => render_resume(model, f, area),
63	    }
64	}
65
66	fn titled_block(title: &str) -> Block<'_> {
67	    Block::default().borders(Borders::ALL).title(title)
68	}
69
70	/// A cursor-aware list item: prefix the cursor row with the caret glyph.
71	fn item(label: String, selected: bool) -> ListItem<'static> {
72	    if selected {
73	        ListItem::new(Line::from(vec![
74	            Span::styled(format!("{} ", glyph::CURSOR), cursor_style()),
75	            Span::styled(label, cursor_style()),
76	        ]))
77	    } else {
78	        ListItem::new(Line::from(format!("  {label}")))
79	    }
80	}
81
82	// ── Layer 1: kind ────────────────────────────────────────────────────────
83	fn render_kind(model: &PickerModel, f: &mut Frame, area: Rect) {
84	    let items = vec![
85	        item("Create new".to_string(), model.kind_cursor == 0),
86	        item("Pick existing".to_string(), model.kind_cursor == 1),
87	    ];
88	    let list = List::new(items).block(titled_block("What kind of endpoint?"));
89	    f.render_widget(list, area);
90	}
91
92	// ── Create-new: adapter tree ───────────────────────────────────────────────
93	fn render_create_adapter(model: &PickerModel, f: &mut Frame, area: Rect) {
94	    let chunks = Layout::default()
95	        .direction(Direction::Vertical)
96	        .constraints([Constraint::Min(1), Constraint::Length(1)])
97	        .split(area);
98	    let items: Vec<ListItem> = model
99	        .adapter_rows
100	        .iter()
101	        .enumerate()
102	        .map(|(i, row)| {
103	            let label = match &row.profile {
104	                Some(p) => format!("{} {}:{p}", glyph::BRANCH, row.adapter),
105	                None => row.adapter.clone(),
106	            };
107	            item(label, i == model.adapter_cursor)
108	        })
109	        .collect();
110	    let list = if items.is_empty() {
111	        List::new(vec![ListItem::new(
112	            "(no harness adapters registered — `spt adapter list`)",
113	        )])
114	    } else {
115	        List::new(items)
116	    }
117	    .block(titled_block("Choose your harness adapter for this endpoint:"));
118	    f.render_widget(list, chunks[0]);
119	    f.render_widget(legend(), chunks[1]);
120	}
121
122	// ── Create-new: id entry ────────────────────────────────────────────────────
123	fn render_create_id(model: &PickerModel, f: &mut Frame, area: Rect) {
124	    let mut lines = vec![
125	        Line::from("Enter an endpoint id. Alphanumeric + hyphen + underscore only."),
126	        Line::from(""),
127	        Line::from(vec![
128	            Span::raw("id: "),
129	            Span::styled(
130	                format!("{}_", model.id_buffer),
131	                Style::default().add_modifier(Modifier::BOLD),
132	            ),
133	        ]),
134	    ];
135	    if let Some(err) = &model.id_error {
136	        lines.push(Line::from(""));
137	        lines.push(Line::from(Span::styled(
138	            format!("invalid: {err}"),
139	            Style::default().fg(Color::Red),
140	        )));
141	    }
142	    let adapter = model
143	        .selected_adapter()
144	        .map(|a| a.address())
145	        .unwrap_or_default();
146	    let title = format!("New {adapter} endpoint");
147	    let p = Paragraph::new(lines).block(titled_block(&title));
148	    f.render_widget(p, area);
149	}
150
151	// ── Pick-existing ───────────────────────────────────────────────────────────
152	fn category_tabs(model: &PickerModel) -> Line<'static> {
153	    let mut spans = vec![Span::raw("[ ")];
154	    for (i, cat) in Category::ALL.iter().enumerate() {
155	        let label = cat.label(&model.project_name);
156	        let style = if *cat == model.category {
157	            cursor_style()
158	        } else {
159	            Style::default().fg(Color::DarkGray)
160	        };
161	        spans.push(Span::styled(label, style));
162	        if i + 1 < Category::ALL.len() {
163	            spans.push(Span::raw(" | "));
164	        }
165	    }
166	    spans.push(Span::raw(" ]"));
167	    Line::from(spans)
168	}
169
170	fn legend() -> Paragraph<'static> {
171	    Paragraph::new(Line::from(Span::styled(
172	        LEGEND,
173	        Style::default().fg(Color::DarkGray),
174	    )))
175	}
176
177	/// The bottom row: a transient flash message (e.g. shortcut written) when set,
178	/// else the pinned keybind legend.
179	fn bottom(model: &PickerModel) -> Paragraph<'static> {
180	    match &model.flash {
181	        Some(msg) => Paragraph::new(Line::from(Span::styled(
182	            msg.clone(),
183	            Style::default().fg(Color::Yellow),
184	        ))),
185	        None => legend(),
186	    }
187	}
188
189	fn render_pick(model: &PickerModel, f: &mut Frame, area: Rect) {
190	    let rows = Layout::default()
191	        .direction(Direction::Vertical)
192	        .constraints([
193	            Constraint::Length(1), // category tabs
194	            Constraint::Min(1),    // body (list | description)
195	            Constraint::Length(1), // filter / legend
196	        ])
197	        .split(area);
198	    f.render_widget(Paragraph::new(category_tabs(model)), rows[0]);
199
200	    let body = Layout::default()
201	        .direction(Direction::Horizontal)
202	        .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
203	        .split(rows[1]);
204
205	    // Left: grouped endpoint list with status squares.
206	    let vis = model.visible_indices();
207	    if vis.is_empty() {
208	        let empty = Paragraph::new("no endpoints — press n to create")
209	            .block(titled_block("Available endpoints:"));
210	        f.render_widget(empty, body[0]);
211	    } else {
212	        let mut items: Vec<ListItem> = Vec::new();
213	        let mut last_group: Option<&str> = None;
214	        // The visible-list cursor maps to the i-th visible row; track that the
215	        // group headers we interleave don't shift it (headers are non-selectable
216	        // display rows, the cursor counts only endpoint rows).
217	        for (vi, &ei) in vis.iter().enumerate() {
218	            let ep = &model.endpoints[ei];
219	            if last_group != Some(ep.group.as_str()) {
220	                items.push(ListItem::new(Span::styled(
221	                    ep.group.clone(),
222	                    Style::default().fg(Color::DarkGray).add_modifier(Modifier::DIM),
223	                )));
224	                last_group = Some(ep.group.as_str());
225	            }
226	            let selected = vi == model.item_cursor;
227	            let caret = if selected { glyph::CURSOR } else { " " };
228	            let line = Line::from(vec![
229	                Span::styled(format!("{caret} "), cursor_style()),
230	                square_span(ep),
231	                Span::raw(format!(" {}", ep.id)),
232	            ]);
233	            let li = ListItem::new(line);
234	            items.push(if selected {
235	                li.style(cursor_style())
236	            } else {
237	                li
238	            });
239	        }
240	        f.render_widget(
241	            List::new(items).block(titled_block("Available endpoints:")),
242	            body[0],
243	        );
244	    }
245
246	    // Right: the two-pane description for the highlighted endpoint.
247	    f.render_widget(description_pane(model), body[1]);
248
249	    // Bottom: the filter line (when filtering) else the legend.
250	    if model.filtering || !model.query.is_empty() {
251	        let q = Paragraph::new(Line::from(vec![
252	            Span::styled("/", Style::default().fg(Color::Cyan)),
253	            Span::raw(model.query.clone()),
254	            Span::styled("_", Style::default().add_modifier(Modifier::BOLD)),
255	        ]));
256	        f.render_widget(q, rows[2]);
257	    } else {
258	        f.render_widget(legend(), rows[2]);
259	    }
260	}
261
262	fn description_pane(model: &PickerModel) -> Paragraph<'static> {
263	    let Some(ep) = model.selected_endpoint() else {
264	        return Paragraph::new("").block(titled_block("Description"));
265	    };
266	    let history = if ep.project_history.is_empty() {
267	        "(none)".to_string()
268	    } else {
269	        ep.project_history.join(", ")
270	    };
271	    let desc = if ep.description.is_empty() {
272	        "(no description)".to_string()
273	    } else {
274	        ep.description.clone()
275	    };
276	    // The colour-coded STATUS line at the TOP, matching the list-item square
277	    // (REQ-PICKER-1).
278	    let display = ep.display_status();
279	    let lines = vec![
280	        Line::from(vec![
281	            Span::styled("status: ", Style::default().fg(Color::DarkGray)),
282	            Span::styled(
283	                display.label().to_string(),
284	                Style::default()
285	                    .fg(display_color(display))
286	                    .add_modifier(Modifier::BOLD),
287	            ),
288	        ]),
289	        Line::from(vec![
290	            Span::styled("harness: ", Style::default().fg(Color::DarkGray)),
291	            Span::raw(ep.adapter_profile.clone()),
292	        ]),
293	        Line::from(vec![
294	            Span::styled("project history: ", Style::default().fg(Color::DarkGray)),
295	            Span::raw(history),
296	        ]),
297	        Line::from(""),
298	        Line::from(desc),
299	    ];
300	    Paragraph::new(lines)
301	        .wrap(Wrap { trim: true })
302	        .block(titled_block("Description"))
303	}
304
305	// ── Confirm ─────────────────────────────────────────────────────────────────
306	fn confirm_option_label(opt: ConfirmOption) -> &'static str {
307	    match opt {
308	        ConfirmOption::Attach => "Attach now",
309	        ConfirmOption::Start => "Start now",
310	        ConfirmOption::View => "View now (read-only)",
311	        ConfirmOption::Kick => "Kick controller and take control",
312	        ConfirmOption::Resume => "Resume from history",
313	        ConfirmOption::ChangeAdapter => "Change harness adapter",
314	        ConfirmOption::Instantiate => "Instantiate locally",
315	        ConfirmOption::Fork => "Fork endpoint",
316	        ConfirmOption::Shortcut => "New/Update spt-<id> shortcut (s)",
317	    }
318	}
319
320	fn render_confirm(model: &PickerModel, f: &mut Frame, area: Rect) {
321	    let chunks = Layout::default()
322	        .direction(Direction::Vertical)
323	        .constraints([Constraint::Length(7), Constraint::Min(1), Constraint::Length(1)])
324	        .split(area);
325
326	    // The selection summary block.
327	    if let Some(ep) = model.selected_endpoint() {
328	        let status_word = match ep.status {
329	            EpStatus::Online => "online",
330	            EpStatus::Offline => "offline",
331	        };
332	        let loc = if ep.is_local {
333	            "LOCAL".to_string()
334	        } else {
335	            ep.node.clone()
336	        };
337	        let history = if ep.project_history.is_empty() {
338	            "(none)".to_string()
339	        } else {
340	            ep.project_history.join(", ")
341	        };
342	        // The W2.5 controlled-by pin (REQ-RCVIEW-1): `controlled by <node>
343	        // (+N viewing)` when a controller holds the endpoint.
344	        let control_line = match &ep.driven_by {
345	            Some(node) if ep.viewer_count > 0 => {
346	                format!("controlled by {node} (+{} viewing)", ep.viewer_count)
347	            }
348	            Some(node) => format!("controlled by {node}"),
349	            None if ep.viewer_count > 0 => format!("(+{} viewing)", ep.viewer_count),
350	            None => String::new(),
351	        };
352	        let summary = vec![
353	            Line::from(Span::styled(
354	                ep.id.clone(),
355	                Style::default().add_modifier(Modifier::BOLD),
356	            )),
357	            Line::from(vec![
358	                square_span(ep),
359	                Span::raw(format!(" {status_word} • {loc}")),
360	            ]),
361	            Line::from(control_line),
362	            Line::from(ep.adapter_profile.clone()),
363	            Line::from(format!("Project history: {history}")),
364	            Line::from(ep.description.clone()),
365	        ];
366	        f.render_widget(
367	            Paragraph::new(summary).block(titled_block("Confirm selection")),
368	            chunks[0],
369	        );
370	    }
371
372	    // The status-dependent option set.
373	    let opts = model.confirm_options();
374	    let items: Vec<ListItem> = opts
375	        .iter()
376	        .enumerate()
377	        .map(|(i, opt)| item(confirm_option_label(*opt).to_string(), i == model.confirm_cursor))
378	        .collect();
379	    f.render_widget(List::new(items).block(titled_block("Options")), chunks[1]);
380	    f.render_widget(bottom(model), chunks[2]);
381	}
382
383	// ── Resume-from-history ─────────────────────────────────────────────────────
384	fn render_resume(model: &PickerModel, f: &mut Frame, area: Rect) {
385	    let chunks = Layout::default()
386	        .direction(Direction::Vertical)
387	        .constraints([Constraint::Min(1), Constraint::Length(1)])
388	        .split(area);
389	    let rows = model.resume_rows();
390	    let items: Vec<ListItem> = if rows.is_empty() {
391	        vec![ListItem::new("(no prior sessions in the ledger)")]
392	    } else {
393	        rows.iter()
394	            .enumerate()
395	            .map(|(i, r)| item(r.title(), i == model.resume_cursor))
396	            .collect()
397	    };
398	    f.render_widget(
399	        List::new(items).block(titled_block("Resume from a prior session:")),
400	        chunks[0],
401	    );
402	    f.render_widget(legend(), chunks[1]);
403	}
404
405	#[cfg(test)]
406	mod tests {
407	    use super::*;
408	    use crate::picker::model::{AdapterInfo, EndpointRow, ResumeRow};
409	    use ratatui::backend::TestBackend;
410	    use ratatui::Terminal;
411
412	    /// Flatten a rendered backend to one big string (row-major) for `contains`
413	    /// assertions — the buffer-level gate doyle asked for.
414	    fn rendered(model: &PickerModel) -> String {
415	        let backend = TestBackend::new(100, 30);
416	        let mut terminal = Terminal::new(backend).unwrap();
417	        terminal.draw(|f| render(model, f)).unwrap();
418	        let buf = terminal.backend().buffer().clone();
419	        let mut out = String::new();
420	        for y in 0..buf.area.height {
421	            for x in 0..buf.area.width {
422	                out.push_str(buf[(x, y)].symbol());
423	            }
424	            out.push('\n');
425	        }
426	        out
427	    }
428
429	    fn ep(id: &str, status: EpStatus, is_local: bool) -> EndpointRow {
430	        EndpointRow {
431	            id: id.to_string(),
432	            group: "local".to_string(),
433	            node: "LOCAL".to_string(),
434	            status,
435	            is_local,
436	            adapter_profile: "claude-spt:fast".to_string(),
437	            project_history: vec!["spt-core".to_string()],
438	            description: "the gatekeeper".to_string(),
439	            resume_rows: vec![],
440	            driven_by: None,
441	            viewer_count: 0,
442	            controllable: None,
443	            endpoint_type: "live_agent".to_string(),
444	        }
445	    }
446
447	    // [unit->REQ-PICKER-1] the pick-existing details render the colour-coded STATUS
448	    // line for the selected endpoint, and the harness-only state is shown for an
449	    // online harness-hosted live agent (amber) — the operator-visible fix.
450	    #[test]
451	    fn pick_status_line_four_state() {
452	        // A harness-hosted live agent (online, controllable == Some(false)) → the
453	        // amber "harness-only" STATUS line (this is wall-a's case).
454	        let mut harness = ep("wall-a", EpStatus::Online, true);
455	        harness.controllable = Some(false);
456	        let mut m = PickerModel::new("spt-core".into(), vec![], vec![harness], None, None);
457	        m.screen = Screen::PickExisting;
458	        m.category = Category::Local;
459	        let s = rendered(&m);
460	        assert!(s.contains("status:"), "the STATUS line renders");
461	        assert!(
462	            s.contains("ONLINE - HARNESS ONLY"),
463	            "a harness-hosted online live agent shows the harness-only state"
464	        );
465
466	        // A spt-hosted (controllable) live agent → plain ONLINE.
467	        let mut spt = ep("hosted", EpStatus::Online, true);
468	        spt.controllable = Some(true);
469	        let mut m2 = PickerModel::new("spt-core".into(), vec![], vec![spt], None, None);
470	        m2.screen = Screen::PickExisting;
471	        m2.category = Category::Local;
472	        assert!(!rendered(&m2).contains("HARNESS ONLY"), "controllable → plain ONLINE");
473
474	        // A controlled endpoint → the blue CONTROLLED state.
475	        let mut ctl = ep("driven", EpStatus::Online, true);
476	        ctl.driven_by = Some("cafe".into());
477	        let mut m3 = PickerModel::new("spt-core".into(), vec![], vec![ctl], None, None);
478	        m3.screen = Screen::PickExisting;
479	        m3.category = Category::Local;
480	        assert!(rendered(&m3).contains("ONLINE + CONTROLLED"));
481	    }
482
483	    // [unit->REQ-RUN-PICKER] the kind screen renders both choices + the caret on
484	    // the cursor row.
485	    #[test]
486	    fn kind_buffer() {
487	        let m = PickerModel::new("spt-core".into(), vec![], vec![], None, None);
488	        let s = rendered(&m);
489	        assert!(s.contains("What kind of endpoint?"));
490	        assert!(s.contains("Create new"));
491	        assert!(s.contains("Pick existing"));
492	        assert!(s.contains(glyph::CURSOR), "the cursor caret renders");
493	    }
494
495	    // [unit->REQ-RUN-PICKER] the adapter screen renders the tree-nested profiles
496	    // under their adapter with the branch glyph.
497	    #[test]
498	    fn adapter_tree_buffer() {
499	        let rows = crate::picker::model::build_adapter_tree(&[AdapterInfo {
500	            name: "claude-spt".into(),
501	            shipped: vec!["fast".into()],
502	            local: vec![],
503	        }]);
504	        let mut m = PickerModel::new("p".into(), rows, vec![], None, None);
505	        m.screen = Screen::CreateAdapter;
506	        let s = rendered(&m);
507	        assert!(s.contains("Choose your harness adapter"));
508	        assert!(s.contains("claude-spt"));
509	        assert!(s.contains(glyph::BRANCH), "profile nests under the adapter");
510	        assert!(s.contains("claude-spt:fast"));
511	    }
512
513	    // [unit->REQ-RUN-PICKER] pick-existing renders the category tabs, the status
514	    // squares, and the two-pane description for the highlighted endpoint.
515	    #[test]
516	    fn pick_buffer_has_squares_and_description() {
517	        let eps = vec![
518	            ep("doyle", EpStatus::Online, true),
519	            ep("offline-one", EpStatus::Offline, true),
520	        ];
521	        let mut m = PickerModel::new("spt-core".into(), vec![], eps, None, None);
522	        m.screen = Screen::PickExisting;
523	        m.category = Category::Local;
524	        let s = rendered(&m);
525	        assert!(s.contains("Local node"), "category tab");
526	        assert!(s.contains(glyph::ONLINE), "online square");
527	        assert!(s.contains(glyph::OFFLINE), "offline square");
528	        assert!(s.contains("doyle"));
529	        // description pane for the highlighted (first) endpoint:
530	        assert!(s.contains("claude-spt:fast"), "adapter:profile in the pane");
531	        assert!(s.contains("spt-core"), "project history in the pane");
532	        assert!(s.contains("the gatekeeper"), "endpoint description in the pane");
533	    }
534
535	    // [unit->REQ-RUN-PICKER] an empty category renders the create hint.
536	    #[test]
537	    fn pick_empty_category_hint() {
538	        let mut m = PickerModel::new("p".into(), vec![], vec![], None, None);
539	        m.screen = Screen::PickExisting;
540	        m.category = Category::Local;
541	        let s = rendered(&m);
542	        assert!(s.contains("no endpoints — press n to create"));
543	    }
544
545	    // [unit->REQ-RUN-PICKER] the confirm screen renders the status-dependent
546	    // options; an offline+local endpoint with a ledger offers Resume.
547	    #[test]
548	    fn confirm_buffer_options() {
549	        let mut e = ep("doyle", EpStatus::Offline, true);
550	        e.resume_rows = vec![ResumeRow {
551	            session_id: "s".into(),
552	            ts: "t".into(),
553	            trigger: "boot".into(),
554	            project: String::new(),
555	        }];
556	        let mut m = PickerModel::new("p".into(), vec![], vec![e], None, None);
557	        m.screen = Screen::PickExisting;
558	        m.category = Category::Local;
559	        m.enter_pick();
560	        let s = rendered(&m);
561	        assert!(s.contains("Confirm selection"));
562	        assert!(s.contains("offline • LOCAL"));
563	        // Offline ⇒ "Start now" (bring up), not "Attach now" (REQ-PICKER-ONLINE-ACTION).
564	        assert!(s.contains("Start now"));
565	        assert!(!s.contains("Attach now"), "offline ⇒ no Attach now");
566	        assert!(s.contains("Resume from history"), "offline+local+ledger ⇒ Resume option");
567	        assert!(s.contains("Fork endpoint"));
568	    }
569
570	    // [unit->REQ-RUN-PICKER] the resume screen lists ledger sessions by title.
571	    #[test]
572	    fn resume_buffer_lists_sessions() {
573	        let mut e = ep("doyle", EpStatus::Offline, true);
574	        e.resume_rows = vec![ResumeRow {
575	            session_id: "abcdef12345".into(),
576	            ts: "2026-06-14T10:00:00Z".into(),
577	            trigger: "clear".into(),
578	            project: "spt-core".into(),
579	        }];
580	        let mut m = PickerModel::new("p".into(), vec![], vec![e], None, None);
581	        m.screen = Screen::PickExisting;
582	        m.category = Category::Local;
583	        m.enter_pick();
584	        m.enter_resume();
585	        let s = rendered(&m);
586	        assert!(s.contains("Resume from a prior session"));
587	        assert!(s.contains("spt-core @ 2026-06-14T10:00:00Z"));
588	        assert!(s.contains("12345"), "session id5 in the title");
589	    }
590	}
591

//! Picker **view** — render a `&PickerModel` to a ratatui frame.
//!
//! ratatui's double-buffered renderer removes the manual erase/redraw +
//! wrapped-row-counting hazard the raw-ANSI reference (`vercel-labs/skills`)
//! carried. Rendering is a pure function of the model + the frame area, so every
//! screen is asserted against a `TestBackend` `Buffer` in the unit tests (no
//! live terminal) — doyle's "assert the rendered buffer" gate.
// [impl->REQ-RUN-PICKER]

use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph, Wrap};
use ratatui::Frame;

use super::model::{
    glyph, Category, ConfirmOption, EndpointRow, EpDisplay, EpStatus, PickerModel, Screen,
};

/// Amber — the harness-only (online, not broker-PTY-controllable) display state.
const AMBER: Color = Color::Rgb(255, 176, 0);

/// The colour for an endpoint's four-state display status (REQ-PICKER-1).
fn display_color(d: EpDisplay) -> Color {
    match d {
        EpDisplay::Offline => Color::DarkGray,
        EpDisplay::Online => Color::Green,
        EpDisplay::HarnessOnly => AMBER,
        EpDisplay::Controlled => Color::Blue,
    }
}

/// The pinned keybind legend (bottom row). Designed so a future tap-mode can
/// surface the same actions without the keys (the phone caveat).
const LEGEND: &str = "←→ category · ↑↓ item · enter confirm · esc back · / filter · n new · s shortcut";

/// Highlight style for the row under a cursor.
fn cursor_style() -> Style {
    Style::default()
        .fg(Color::Cyan)
        .add_modifier(Modifier::BOLD)
}

/// A status square span coloured by the endpoint's four-state DISPLAY status
/// (REQ-PICKER-1): green ■ online · gray ▢ offline · amber ■ harness-only · blue
/// ■ controlled. The glyph stays the online/offline ■/▢ duality; the colour
/// carries the four-state.
fn square_span(row: &EndpointRow) -> Span<'static> {
    let color = display_color(row.display_status());
    Span::styled(row.status.square().to_string(), Style::default().fg(color))
}

/// Render the whole picker for the current screen.
pub fn render(model: &PickerModel, f: &mut Frame) {
    let area = f.area();
    match model.screen {
        Screen::Kind => render_kind(model, f, area),
        Screen::CreateAdapter => render_create_adapter(model, f, area),
        Screen::CreateId => render_create_id(model, f, area),
        Screen::PickExisting => render_pick(model, f, area),
        Screen::Confirm => render_confirm(model, f, area),
        Screen::Resume => render_resume(model, f, area),
    }
}

fn titled_block(title: &str) -> Block<'_> {
    Block::default().borders(Borders::ALL).title(title)
}

/// A cursor-aware list item: prefix the cursor row with the caret glyph.
fn item(label: String, selected: bool) -> ListItem<'static> {
    if selected {
        ListItem::new(Line::from(vec![
            Span::styled(format!("{} ", glyph::CURSOR), cursor_style()),
            Span::styled(label, cursor_style()),
        ]))
    } else {
        ListItem::new(Line::from(format!("  {label}")))
    }
}

// ── Layer 1: kind ────────────────────────────────────────────────────────
fn render_kind(model: &PickerModel, f: &mut Frame, area: Rect) {
    let items = vec![
        item("Create new".to_string(), model.kind_cursor == 0),
        item("Pick existing".to_string(), model.kind_cursor == 1),
    ];
    let list = List::new(items).block(titled_block("What kind of endpoint?"));
    f.render_widget(list, area);
}

// ── Create-new: adapter tree ───────────────────────────────────────────────
fn render_create_adapter(model: &PickerModel, f: &mut Frame, area: Rect) {
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Min(1), Constraint::Length(1)])
        .split(area);
    let items: Vec<ListItem> = model
        .adapter_rows
        .iter()
        .enumerate()
        .map(|(i, row)| {
            let label = match &row.profile {
                Some(p) => format!("{} {}:{p}", glyph::BRANCH, row.adapter),
                None => row.adapter.clone(),
            };
            item(label, i == model.adapter_cursor)
        })
        .collect();
    let list = if items.is_empty() {
        List::new(vec![ListItem::new(
            "(no harness adapters registered — `spt adapter list`)",
        )])
    } else {
        List::new(items)
    }
    .block(titled_block("Choose your harness adapter for this endpoint:"));
    f.render_widget(list, chunks[0]);
    f.render_widget(legend(), chunks[1]);
}

// ── Create-new: id entry ────────────────────────────────────────────────────
fn render_create_id(model: &PickerModel, f: &mut Frame, area: Rect) {
    let mut lines = vec![
        Line::from("Enter an endpoint id. Alphanumeric + hyphen + underscore only."),
        Line::from(""),
        Line::from(vec![
            Span::raw("id: "),
            Span::styled(
                format!("{}_", model.id_buffer),
                Style::default().add_modifier(Modifier::BOLD),
            ),
        ]),
    ];
    if let Some(err) = &model.id_error {
        lines.push(Line::from(""));
        lines.push(Line::from(Span::styled(
            format!("invalid: {err}"),
            Style::default().fg(Color::Red),
        )));
    }
    let adapter = model
        .selected_adapter()
        .map(|a| a.address())
        .unwrap_or_default();
    let title = format!("New {adapter} endpoint");
    let p = Paragraph::new(lines).block(titled_block(&title));
    f.render_widget(p, area);
}

// ── Pick-existing ───────────────────────────────────────────────────────────
fn category_tabs(model: &PickerModel) -> Line<'static> {
    let mut spans = vec![Span::raw("[ ")];
    for (i, cat) in Category::ALL.iter().enumerate() {
        let label = cat.label(&model.project_name);
        let style = if *cat == model.category {
            cursor_style()
        } else {
            Style::default().fg(Color::DarkGray)
        };
        spans.push(Span::styled(label, style));
        if i + 1 < Category::ALL.len() {
            spans.push(Span::raw(" | "));
        }
    }
    spans.push(Span::raw(" ]"));
    Line::from(spans)
}

fn legend() -> Paragraph<'static> {
    Paragraph::new(Line::from(Span::styled(
        LEGEND,
        Style::default().fg(Color::DarkGray),
    )))
}

/// The bottom row: a transient flash message (e.g. shortcut written) when set,
/// else the pinned keybind legend.
fn bottom(model: &PickerModel) -> Paragraph<'static> {
    match &model.flash {
        Some(msg) => Paragraph::new(Line::from(Span::styled(
            msg.clone(),
            Style::default().fg(Color::Yellow),
        ))),
        None => legend(),
    }
}

fn render_pick(model: &PickerModel, f: &mut Frame, area: Rect) {
    let rows = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(1), // category tabs
            Constraint::Min(1),    // body (list | description)
            Constraint::Length(1), // filter / legend
        ])
        .split(area);
    f.render_widget(Paragraph::new(category_tabs(model)), rows[0]);

    let body = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
        .split(rows[1]);

    // Left: grouped endpoint list with status squares.
    let vis = model.visible_indices();
    if vis.is_empty() {
        let empty = Paragraph::new("no endpoints — press n to create")
            .block(titled_block("Available endpoints:"));
        f.render_widget(empty, body[0]);
    } else {
        let mut items: Vec<ListItem> = Vec::new();
        let mut last_group: Option<&str> = None;
        // The visible-list cursor maps to the i-th visible row; track that the
        // group headers we interleave don't shift it (headers are non-selectable
        // display rows, the cursor counts only endpoint rows).
        for (vi, &ei) in vis.iter().enumerate() {
            let ep = &model.endpoints[ei];
            if last_group != Some(ep.group.as_str()) {
                items.push(ListItem::new(Span::styled(
                    ep.group.clone(),
                    Style::default().fg(Color::DarkGray).add_modifier(Modifier::DIM),
                )));
                last_group = Some(ep.group.as_str());
            }
            let selected = vi == model.item_cursor;
            let caret = if selected { glyph::CURSOR } else { " " };
            let line = Line::from(vec![
                Span::styled(format!("{caret} "), cursor_style()),
                square_span(ep),
                Span::raw(format!(" {}", ep.id)),
            ]);
            let li = ListItem::new(line);
            items.push(if selected {
                li.style(cursor_style())
            } else {
                li
            });
        }
        f.render_widget(
            List::new(items).block(titled_block("Available endpoints:")),
            body[0],
        );
    }

    // Right: the two-pane description for the highlighted endpoint.
    f.render_widget(description_pane(model), body[1]);

    // Bottom: the filter line (when filtering) else the legend.
    if model.filtering || !model.query.is_empty() {
        let q = Paragraph::new(Line::from(vec![
            Span::styled("/", Style::default().fg(Color::Cyan)),
            Span::raw(model.query.clone()),
            Span::styled("_", Style::default().add_modifier(Modifier::BOLD)),
        ]));
        f.render_widget(q, rows[2]);
    } else {
        f.render_widget(legend(), rows[2]);
    }
}

fn description_pane(model: &PickerModel) -> Paragraph<'static> {
    let Some(ep) = model.selected_endpoint() else {
        return Paragraph::new("").block(titled_block("Description"));
    };
    let history = if ep.project_history.is_empty() {
        "(none)".to_string()
    } else {
        ep.project_history.join(", ")
    };
    let desc = if ep.description.is_empty() {
        "(no description)".to_string()
    } else {
        ep.description.clone()
    };
    // The colour-coded STATUS line at the TOP, matching the list-item square
    // (REQ-PICKER-1).
    let display = ep.display_status();
    let lines = vec![
        Line::from(vec![
            Span::styled("status: ", Style::default().fg(Color::DarkGray)),
            Span::styled(
                display.label().to_string(),
                Style::default()
                    .fg(display_color(display))
                    .add_modifier(Modifier::BOLD),
            ),
        ]),
        Line::from(vec![
            Span::styled("harness: ", Style::default().fg(Color::DarkGray)),
            Span::raw(ep.adapter_profile.clone()),
        ]),
        Line::from(vec![
            Span::styled("project history: ", Style::default().fg(Color::DarkGray)),
            Span::raw(history),
        ]),
        Line::from(""),
        Line::from(desc),
    ];
    Paragraph::new(lines)
        .wrap(Wrap { trim: true })
        .block(titled_block("Description"))
}

// ── Confirm ─────────────────────────────────────────────────────────────────
fn confirm_option_label(opt: ConfirmOption) -> &'static str {
    match opt {
        ConfirmOption::Attach => "Attach now",
        ConfirmOption::Start => "Start now",
        ConfirmOption::View => "View now (read-only)",
        ConfirmOption::Kick => "Kick controller and take control",
        ConfirmOption::Resume => "Resume from history",
        ConfirmOption::ChangeAdapter => "Change harness adapter",
        ConfirmOption::Instantiate => "Instantiate locally",
        ConfirmOption::Fork => "Fork endpoint",
        ConfirmOption::Shortcut => "New/Update spt-<id> shortcut (s)",
    }
}

fn render_confirm(model: &PickerModel, f: &mut Frame, area: Rect) {
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Length(7), Constraint::Min(1), Constraint::Length(1)])
        .split(area);

    // The selection summary block.
    if let Some(ep) = model.selected_endpoint() {
        let status_word = match ep.status {
            EpStatus::Online => "online",
            EpStatus::Offline => "offline",
        };
        let loc = if ep.is_local {
            "LOCAL".to_string()
        } else {
            ep.node.clone()
        };
        let history = if ep.project_history.is_empty() {
            "(none)".to_string()
        } else {
            ep.project_history.join(", ")
        };
        // The W2.5 controlled-by pin (REQ-RCVIEW-1): `controlled by <node>
        // (+N viewing)` when a controller holds the endpoint.
        let control_line = match &ep.driven_by {
            Some(node) if ep.viewer_count > 0 => {
                format!("controlled by {node} (+{} viewing)", ep.viewer_count)
            }
            Some(node) => format!("controlled by {node}"),
            None if ep.viewer_count > 0 => format!("(+{} viewing)", ep.viewer_count),
            None => String::new(),
        };
        let summary = vec![
            Line::from(Span::styled(
                ep.id.clone(),
                Style::default().add_modifier(Modifier::BOLD),
            )),
            Line::from(vec![
                square_span(ep),
                Span::raw(format!(" {status_word} • {loc}")),
            ]),
            Line::from(control_line),
            Line::from(ep.adapter_profile.clone()),
            Line::from(format!("Project history: {history}")),
            Line::from(ep.description.clone()),
        ];
        f.render_widget(
            Paragraph::new(summary).block(titled_block("Confirm selection")),
            chunks[0],
        );
    }

    // The status-dependent option set.
    let opts = model.confirm_options();
    let items: Vec<ListItem> = opts
        .iter()
        .enumerate()
        .map(|(i, opt)| item(confirm_option_label(*opt).to_string(), i == model.confirm_cursor))
        .collect();
    f.render_widget(List::new(items).block(titled_block("Options")), chunks[1]);
    f.render_widget(bottom(model), chunks[2]);
}

// ── Resume-from-history ─────────────────────────────────────────────────────
fn render_resume(model: &PickerModel, f: &mut Frame, area: Rect) {
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Min(1), Constraint::Length(1)])
        .split(area);
    let rows = model.resume_rows();
    let items: Vec<ListItem> = if rows.is_empty() {
        vec![ListItem::new("(no prior sessions in the ledger)")]
    } else {
        rows.iter()
            .enumerate()
            .map(|(i, r)| item(r.title(), i == model.resume_cursor))
            .collect()
    };
    f.render_widget(
        List::new(items).block(titled_block("Resume from a prior session:")),
        chunks[0],
    );
    f.render_widget(legend(), chunks[1]);
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::picker::model::{AdapterInfo, EndpointRow, ResumeRow};
    use ratatui::backend::TestBackend;
    use ratatui::Terminal;

    /// Flatten a rendered backend to one big string (row-major) for `contains`
    /// assertions — the buffer-level gate doyle asked for.
    fn rendered(model: &PickerModel) -> String {
        let backend = TestBackend::new(100, 30);
        let mut terminal = Terminal::new(backend).unwrap();
        terminal.draw(|f| render(model, f)).unwrap();
        let buf = terminal.backend().buffer().clone();
        let mut out = String::new();
        for y in 0..buf.area.height {
            for x in 0..buf.area.width {
                out.push_str(buf[(x, y)].symbol());
            }
            out.push('\n');
        }
        out
    }

    fn ep(id: &str, status: EpStatus, is_local: bool) -> EndpointRow {
        EndpointRow {
            id: id.to_string(),
            group: "local".to_string(),
            node: "LOCAL".to_string(),
            status,
            is_local,
            adapter_profile: "claude-spt:fast".to_string(),
            project_history: vec!["spt-core".to_string()],
            description: "the gatekeeper".to_string(),
            resume_rows: vec![],
            driven_by: None,
            viewer_count: 0,
            controllable: None,
            endpoint_type: "live_agent".to_string(),
        }
    }

    // [unit->REQ-PICKER-1] the pick-existing details render the colour-coded STATUS
    // line for the selected endpoint, and the harness-only state is shown for an
    // online harness-hosted live agent (amber) — the operator-visible fix.
    #[test]
    fn pick_status_line_four_state() {
        // A harness-hosted live agent (online, controllable == Some(false)) → the
        // amber "harness-only" STATUS line (this is wall-a's case).
        let mut harness = ep("wall-a", EpStatus::Online, true);
        harness.controllable = Some(false);
        let mut m = PickerModel::new("spt-core".into(), vec![], vec![harness], None, None);
        m.screen = Screen::PickExisting;
        m.category = Category::Local;
        let s = rendered(&m);
        assert!(s.contains("status:"), "the STATUS line renders");
        assert!(
            s.contains("ONLINE - HARNESS ONLY"),
            "a harness-hosted online live agent shows the harness-only state"
        );

        // A spt-hosted (controllable) live agent → plain ONLINE.
        let mut spt = ep("hosted", EpStatus::Online, true);
        spt.controllable = Some(true);
        let mut m2 = PickerModel::new("spt-core".into(), vec![], vec![spt], None, None);
        m2.screen = Screen::PickExisting;
        m2.category = Category::Local;
        assert!(!rendered(&m2).contains("HARNESS ONLY"), "controllable → plain ONLINE");

        // A controlled endpoint → the blue CONTROLLED state.
        let mut ctl = ep("driven", EpStatus::Online, true);
        ctl.driven_by = Some("cafe".into());
        let mut m3 = PickerModel::new("spt-core".into(), vec![], vec![ctl], None, None);
        m3.screen = Screen::PickExisting;
        m3.category = Category::Local;
        assert!(rendered(&m3).contains("ONLINE + CONTROLLED"));
    }

    // [unit->REQ-RUN-PICKER] the kind screen renders both choices + the caret on
    // the cursor row.
    #[test]
    fn kind_buffer() {
        let m = PickerModel::new("spt-core".into(), vec![], vec![], None, None);
        let s = rendered(&m);
        assert!(s.contains("What kind of endpoint?"));
        assert!(s.contains("Create new"));
        assert!(s.contains("Pick existing"));
        assert!(s.contains(glyph::CURSOR), "the cursor caret renders");
    }

    // [unit->REQ-RUN-PICKER] the adapter screen renders the tree-nested profiles
    // under their adapter with the branch glyph.
    #[test]
    fn adapter_tree_buffer() {
        let rows = crate::picker::model::build_adapter_tree(&[AdapterInfo {
            name: "claude-spt".into(),
            shipped: vec!["fast".into()],
            local: vec![],
        }]);
        let mut m = PickerModel::new("p".into(), rows, vec![], None, None);
        m.screen = Screen::CreateAdapter;
        let s = rendered(&m);
        assert!(s.contains("Choose your harness adapter"));
        assert!(s.contains("claude-spt"));
        assert!(s.contains(glyph::BRANCH), "profile nests under the adapter");
        assert!(s.contains("claude-spt:fast"));
    }

    // [unit->REQ-RUN-PICKER] pick-existing renders the category tabs, the status
    // squares, and the two-pane description for the highlighted endpoint.
    #[test]
    fn pick_buffer_has_squares_and_description() {
        let eps = vec![
            ep("doyle", EpStatus::Online, true),
            ep("offline-one", EpStatus::Offline, true),
        ];
        let mut m = PickerModel::new("spt-core".into(), vec![], eps, None, None);
        m.screen = Screen::PickExisting;
        m.category = Category::Local;
        let s = rendered(&m);
        assert!(s.contains("Local node"), "category tab");
        assert!(s.contains(glyph::ONLINE), "online square");
        assert!(s.contains(glyph::OFFLINE), "offline square");
        assert!(s.contains("doyle"));
        // description pane for the highlighted (first) endpoint:
        assert!(s.contains("claude-spt:fast"), "adapter:profile in the pane");
        assert!(s.contains("spt-core"), "project history in the pane");
        assert!(s.contains("the gatekeeper"), "endpoint description in the pane");
    }

    // [unit->REQ-RUN-PICKER] an empty category renders the create hint.
    #[test]
    fn pick_empty_category_hint() {
        let mut m = PickerModel::new("p".into(), vec![], vec![], None, None);
        m.screen = Screen::PickExisting;
        m.category = Category::Local;
        let s = rendered(&m);
        assert!(s.contains("no endpoints — press n to create"));
    }

    // [unit->REQ-RUN-PICKER] the confirm screen renders the status-dependent
    // options; an offline+local endpoint with a ledger offers Resume.
    #[test]
    fn confirm_buffer_options() {
        let mut e = ep("doyle", EpStatus::Offline, true);
        e.resume_rows = vec![ResumeRow {
            session_id: "s".into(),
            ts: "t".into(),
            trigger: "boot".into(),
            project: String::new(),
        }];
        let mut m = PickerModel::new("p".into(), vec![], vec![e], None, None);
        m.screen = Screen::PickExisting;
        m.category = Category::Local;
        m.enter_pick();
        let s = rendered(&m);
        assert!(s.contains("Confirm selection"));
        assert!(s.contains("offline • LOCAL"));
        // Offline ⇒ "Start now" (bring up), not "Attach now" (REQ-PICKER-ONLINE-ACTION).
        assert!(s.contains("Start now"));
        assert!(!s.contains("Attach now"), "offline ⇒ no Attach now");
        assert!(s.contains("Resume from history"), "offline+local+ledger ⇒ Resume option");
        assert!(s.contains("Fork endpoint"));
    }

    // [unit->REQ-RUN-PICKER] the resume screen lists ledger sessions by title.
    #[test]
    fn resume_buffer_lists_sessions() {
        let mut e = ep("doyle", EpStatus::Offline, true);
        e.resume_rows = vec![ResumeRow {
            session_id: "abcdef12345".into(),
            ts: "2026-06-14T10:00:00Z".into(),
            trigger: "clear".into(),
            project: "spt-core".into(),
        }];
        let mut m = PickerModel::new("p".into(), vec![], vec![e], None, None);
        m.screen = Screen::PickExisting;
        m.category = Category::Local;
        m.enter_pick();
        m.enter_resume();
        let s = rendered(&m);
        assert!(s.contains("Resume from a prior session"));
        assert!(s.contains("spt-core @ 2026-06-14T10:00:00Z"));
        assert!(s.contains("12345"), "session id5 in the title");
    }
}


Full picture. Let me check the REQ registry for the picker reqs + their active stages before tagging.


Bash

grep -n -A6 'REQ-RUN-PICKER\|REQ-PICKER-4\|REQ-PICKER-ONLINE-ACTION\|REQ-PICKER-1\b' traceable-reqs.toml | head -80

Show picker REQ entries in registry


495:id = "REQ-RUN-PICKER"
496-title = "Interactive `spt endpoint run` picker (ratatui TUI): bare `spt endpoint run` (no --adapter/--id) enters an in-process picker (flags-present = the REQ-HOST-RUN-1 non-interactive path, untouched). Layer 1 picks kind (Create new | Pick existing). Create-new: choose a registered kind=\"harness\" adapter with its shipped+local profiles tree-nested (registry::registered / manifest.profiles / local_profile_names) → enter a charset-validated id → start. Pick-existing: category select (left/right) over [<cwd-project> | Local node | Subnet], endpoints grouped + alphabetically sorted per category, a status square per endpoint (online green ■ / offline gray ▢ — the blue \"attached\" tri-state + Kick are DEFERRED to a broker attach-presence slice, M12-W2-RULING Q1), type-to-filter (`/`, nucleo-matcher), a pinned keybind legend, and a right-half two-pane description (harness adapter:profile · best-effort project history newest→oldest from the contextstore p-<project> branches, empty-if-none · `spt endpoint description`). Confirm layer offers status-dependent options — Attach/Start/View (rc pump / cmd_endpoint_run) · Instantiate-locally (remote) · Change-harness-adapter (offline) · Fork (cmd_fork) · Resume-from-history (offline+LOCAL only; enumerate spt_store::sessions::last_k, titles `<project> @ <ts> (…id5)`, feed session_id → cmd_endpoint_run --resume). A single action enum is the source of truth so a future tap-mode (phone PTY) layers on without re-coupling to keybinds. EVERY terminal action routes through cmd_endpoint_run / existing CLI fns — no second bringup path."
497-required_stages = ["doc", "impl", "unit"]  # activated M12-W2 (no int: TUI Buffer + state-model unit asserts; live loop manual-verify; bringup path int-covered by REQ-HOST-RUN-1/REQ-RC-1)
498-[[requirements]]
499-id = "REQ-RUN-SHORTCUT"
500-title = "`<basename>-<id>` launcher shortcut generation (picker `s` keybind, M12-W2-T2.4): from any pre-start options set the picker writes/updates a `<basename>-<id>` launcher at the project root baking the current selection's non-interactive `spt endpoint run` flags (terminal actions only: adapter[:profile] + id + (create|resume) + (start|attach|view); Kick/Instantiate/Change-adapter/Fork are interactive-only, not bakeable). BASENAME IS A PARAMETER (operator rev. 2026-06-14): harness-agnostic spt-core defaults to `spt` (→ `spt-<id>`); an adapter/flow OVERRIDES it (spt-claude-code → `cc`), so spt-core NEVER bakes `cc` (a harness name) into itself. The basename must be a DISTINCT token, never bare `spt` (a `spt.cmd` would shadow the real `spt.exe` only under cmd.exe cwd-first search, silently no-op in PowerShell/Unix, and self-recurse). The script is the CURRENT OS's native form — `.cmd` on Windows (NOT `.ps1`: default PATHEXT excludes `.ps1` so a bare/ext-less name never resolves one; `.cmd` is PATHEXT-resolvable), POSIX `sh` (+chmod +x) on Unix (a single portable form can't be both). The generated header documents the invocation reality (cmd.exe bare `<name>` in the project dir / PowerShell `.\\<name>` / Unix `./<name>`; a truly-bare basename on PATH = a PATH-installed launcher, `/spt:setup`'s job). Overwrite is SENTINEL-guarded: the generator writes + checks a generated-by header marker — it overwrites its own prior output freely, but REFUSES + warns if a same-named file lacks the sentinel (never clobber a user file). Requires the additive `--create` flag on `Run{}` (the default-fresh made explicit; N-1-safe)."
501-required_stages = ["doc", "impl", "unit"]  # activated M12-W2 (no int: shortcut content + sentinel create-vs-update as unit; flag→action mapping unit)
--
531:id = "REQ-PICKER-1"
532-title = "The picker renders a FOUR-state endpoint status (extending the W2 online/offline duality): the list-item square AND a color-coded STATUS line at the top of the pick-existing right-side details both show — gray OFFLINE; green ONLINE (online + PTY-controllable spt-hosted, not controlled); amber 'ONLINE - HARNESS ONLY' (online but NOT broker-PTY-controllable = harness-hosted, no broker PTY seat — today mis-shows green); blue 'ONLINE + CONTROLLED' (online + driven_by.is_some()). Derived on EndpointRow from {offline | controllable | driven_by} with precedence offline→gray, else driven_by→blue, else !controllable→amber, else green (driven_by outranks harness-only; mutually exclusive in practice — a harness-only endpoint has no broker PTY to control). The controllable discriminator is a NEW InfoJson.controllable: Option<bool> (serde-default, N-1-safe), stamped at the establish seam — cmd_listen (harness-hosted relay, no broker PTY) → Some(false); cmd_bind live_agent (spt-hosted broker PTY) → Some(true); absent → not-controllable (amber) default (harness-hosted is the common mis-reported case; one bind self-corrects). Store-projection-only (no live daemon query — doyle ruling). (v0.10.0)"
533-required_stages = ["impl", "unit"]  # activated v0.10.0 PICKER-1: impl = InfoJson.controllable + listen/bind stamp + EpStatus 4-state + square_span/render_pick color + data.rs derive; unit = the 4-state derivation + view snapshot render)
534-[[requirements]]
535-id = "REQ-PICKER-2"
536-title = "The picker's project-history loader reads the git-backed context store, not the bare working tree: data.rs project_history_for enumerates an endpoint's projects via the BranchStore branch set (the context store keeps per-project context in git branches — contextstore::project_branch(project_id), checked out to projects/<project>/<id>/ only on-demand) instead of raw std::fs::read_dir over the empty working tree (which returned empty for ALL rows incl wall-a — the operator bug). Ordered newest→oldest by branch commit recency; degrades to empty (informational pane), never fails. (v0.10.0)"
537-required_stages = ["impl", "unit"]  # activated v0.10.0 PICKER-2: impl = data.rs project_history_for enumerates p-<project> branches (BranchStore::branches_by_recency) holding the agent's per-project file, newest→oldest; unit = branches_by_recency orders newest-committed branch first
--
543:id = "REQ-PICKER-4"
544-title = "The picker's Subnet category renders the canonical node LABEL, not bare key-hex: a subnet row's node renders as 'LABEL (keyprefix…)' (e.g. 'HFENDULEAM (bcead52b…)') per CONTEXT.md:650 + Instance.node_label, NOT the raw node key-hex (SPT_DEV:14efb80cb… — a picker-only regression because resource_projection→ResourceRow drops node_label, so data.rs subnet_rows uses the raw row.node). Thread node_label into the picker subnet path (ResourceRow gains node_label, or subnet_rows looks it up via the registry's node_labels) and REUSE the one canonical render (format!(\"{l} ({}…)\", key_prefix) — cli.rs / wansend.rs), never a re-implementation. (v0.10.0)"
545-required_stages = ["impl", "unit"]  # activated v0.10.0 PICKER-4: impl = node_label threaded onto ResourceRow in resource_projection (instance label, node_labels-map fallback) + canonical node_label_display reused by BOTH the picker and endpoint-list; unit = resource_projection_threads_node_label + node_label_display render
546-[[requirements]]
547-id = "REQ-PICKER-5"
548:title = "`spt endpoint list` (bare/subnet view) renders an ALIGNED table with canonical node labels: cmd_endpoint_list prints subnet rows with `\\t` TAB separators (cli.rs:~1651-1662) so variable-width endpoint_ids snap fields to different tab-stops → a RAGGED status column (operator screenshot: X/help statuses misaligned vs rt-*/sptc-*/wall-a); and it calls the node renderer with no label → bare key-hex for every row (SAME ResourceRow-drops-node_label root as REQ-PICKER-4). FIX: max-width per-column padding (mirror render_node_rows' pad, pad by char count not byte len — '…' is multibyte) replacing the tabs, and render the node via the shared node_label_display now that ResourceRow carries node_label (REQ-PICKER-4). Extract a pure row-formatter seam so the alignment+label is unit-testable. ALSO: the bare list is the SUBNET view (a just-run LOCAL perch is invisible cross-subnet until the next advertise tick), so emit a `--local` hint line so a freshly-run endpoint isn't perceived as lost. (v0.10.0; operator-flagged + doyle dispatch 2026-06-17)"
549-required_stages = ["impl", "unit"]  # activated v0.10.0 PICKER-5: impl = pure format_subnet_rows (per-column char-width pad + node_label_display) wired into cmd_endpoint_list, replacing the \t tabs + None-label, plus the --local hint; unit = a ragged-width row set formats aligned with LABEL (keyprefix…)
550-# --- Added 2026-06-17 (post-v0.10.0 findings backlog; operator greenlight + doyle dispatch); rule 3 register-before-satisfy ---
551-[[requirements]]
552-id = "REQ-SEND-SPT-HOSTED"
553-title = "An inbound `spt send` is DELIVERED to an spt-hosted endpoint (brought up via `spt endpoint run` → `api bind`, broker holds its PTY, NO `api listen` relay). Today cmd_bind→establish_perch (api/startup.rs ~441) writes info.json + ready marker + controllable=Some(true) but registers NO message-listener / NO address, so deliver.rs resolve_address→None→spool (deliver.rs:132-140) and the message NEVER reaches the live PTY — the endpoint reads 'online' (ready marker) yet `spt send` silently SPOOLS ('online but not deliverable' lie). Per CONTEXT:187-188 the daemon owns the PTY and delivers, manifest-configurable per activity-state (direct PTY injection / relay / HTTP). FIX: route an inbound send for an spt-hosted target through the daemon → broker InputReq → session.write_input PTY-inject (broker.rs dispatch_input/write_input ~988-1022), the same path the brain uses; the live-delivery handshake must report Sent (not Queued) and stop the spool-only fallback for a broker-hosted, PTY-resident endpoint. Detection is local: controllable==Some(true) + spt-hosted state + resolve_address==None. = the spt-core HALF of the wall-b finding (perri owns the adapter half: bind-hook fired-zero-perch + the missing endpoint-run int test). (post-v0.10.0)"
554-required_stages = ["impl", "unit", "int"]  # activated v0.11.0: impl = KIND_ENDPOINT_INPUT frame + broker dispatch_endpoint_input (endpoint→session scan + write_input, idle-direct, logged) + Brain::inject_endpoint client + cmd_send is_spt_hosted_no_relay gate + render_event_whole inject; unit = endpoint_input_frames_round_trip + spt_hosted_no_relay_detection; int = endpoint_keyed_inject_reaches_hosted_pty (real broker PTY round-trip + negative miss). FOLLOW WAVE: activity-gated routing (idle→inject / active→spool-for-poll-drain) per CONTEXT:188
--
1151:id = "REQ-PICKER-ONLINE-ACTION"
1152-title = "The `spt endpoint run` picker shows the correct action for an ALREADY-ONLINE endpoint — Attach, NOT 'Start now' (operator-raised v0.12.0 real-harness finding). Symptom: the picker offers 'Start now' for endpoints that are already online. ROOT TBD — investigate the status→action mapping (v0.10.0 PICKER-1 four-state status, picker/model.rs): is it reading live/online state correctly, or rendering stale/wedged broker state (i.e. partly a symptom of the broker wedge / status=online latch)? Fix so online → Attach. (v0.12.1)"
1153-required_stages = ["impl", "unit"]  # ACTIVATED v0.12.1 Wave 3 P2 (todlando). ROOT: confirm_options offered [Attach, Start, View] for an ONLINE free endpoint — the "Start now" (ConfirmOption::Start) made no sense for an already-running endpoint (confirm_terminal even mapped online Start→Attach, so it was a redundant, misleading label, not a wrong action). NOT a stale/wedged-state read — the EpStatus was correct; the option SET was wrong. FIX (picker/model.rs confirm_options): status-split the free branch — online ⇒ [Attach, View], offline ⇒ [Start, View]. impl = the split; unit = confirm_options_status_dependent extended (online ⇒ Attach + NO Start; offline ⇒ Start + NO bare Attach). No CLI/help change → no docs-drift
1154-[[requirements]]
1155-id = "REQ-ENDPOINT-LIST-MERGE-LOCAL"
1156-title = "`spt endpoint list` always merges this node's LOCAL (unadvertised) perches into the view; the `--local` flag is REMOVED (operator decision 2026-06-17). Rationale: `spt whoami` is a thin alias of `endpoint list` — a just-online agent running `whoami` must see its OWN perch, or it gets an omitted-self view ('chaos'). FIX: drop the `--local` flag + its `--detail` conflict test + the v0.10.0 REQ-PICKER-5 hint line (cli.rs:1678) + cmd_list_local; the bare list merges local perches into the subnet view; fix the whoami alias path accordingly. Run `cargo run -p xtask -- gen` (docs-drift, DEFAULT target). (v0.12.1)"
1157-required_stages = ["doc", "impl", "unit"]  # ACTIVATED v0.12.1 Wave 4 E1 (todlando). doc = CONTEXT.md "endpoint list always merges local perches" note. impl = cmd_endpoint_list always appends print_local_section (LOCAL roster) after SELF pin + subnet groups; the `--local` flag + its conflicts_with + cmd_list_local + the two `--local` hint lines are REMOVED; cmd_whoami calls the 2-arg cmd_endpoint_list. unit = local_section_always_renders_local_perches (pure render_local_section: an unadvertised perch — ready=false/no address — still lists; empty ⇒ header + quiet marker) + the parse test asserting `--local` is now an error. xtask gen regen docs-site/src/cli/reference.md (CLI docs-drift gate); no REQ-*/M#-W# in the clap /// help


All grounded. REQs: items 1/2/4 → extend **REQ-RUN-PICKER**; item 3 → **REQ-PICKER-4**. No new reqs (stages already active). Now src edits — I own src + breaking-test fixes, then dispatch ONE engineer for new coverage.

Starting with `model.rs`:


Edit

        // A lone --adapter pre-selects its row; a lone --id pre-fills the id and
        // jumps straight into create-new.
        let adapter_cursor = prefill_adapter
            .as_deref()
            .and_then(|a| adapter_rows.iter().position(|r| r.address() == a || r.adapter == a))
            .unwrap_or(0);
        let screen = if prefill_id.is_some() || prefill_adapter.is_some() {
            Screen::CreateAdapter
        } else {
            Screen::Kind
        };

        // A lone --adapter pre-selects its row; a lone --id pre-fills the id and
        // jumps straight into create-new.
        let adapter_cursor = prefill_adapter
            .as_deref()
            .and_then(|a| adapter_rows.iter().position(|r| r.address() == a || r.adapter == a))
            .unwrap_or(0);
        // W4 UX: a bare picker OPENS on Pick-existing (the common case — most runs
        // re-attach an existing endpoint); `n` jumps to Create-new and Esc backs to
        // the Layer-1 Kind menu (now a jump-target, not the entry). A prefill still
        // jumps straight into create-new.
        let screen = if prefill_id.is_some() || prefill_adapter.is_some() {
            Screen::CreateAdapter
        } else {
            Screen::PickExisting
        };


The file C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs has been updated successfully. (file state is current in your context — no need to Read it back)

        // A lone --adapter pre-selects its row; a lone --id pre-fills the id and
        // jumps straight into create-new.
        let adapter_cursor = prefill_adapter
            .as_deref()
            .and_then(|a| adapter_rows.iter().position(|r| r.address() == a || r.adapter == a))
            .unwrap_or(0);
        let screen = if prefill_id.is_some() || prefill_adapter.is_some() {
            Screen::CreateAdapter
        } else {
            Screen::Kind
        };

        // A lone --adapter pre-selects its row; a lone --id pre-fills the id and
        // jumps straight into create-new.
        let adapter_cursor = prefill_adapter
            .as_deref()
            .and_then(|a| adapter_rows.iter().position(|r| r.address() == a || r.adapter == a))
            .unwrap_or(0);
        // W4 UX: a bare picker OPENS on Pick-existing (the common case — most runs
        // re-attach an existing endpoint); `n` jumps to Create-new and Esc backs to
        // the Layer-1 Kind menu (now a jump-target, not the entry). A prefill still
        // jumps straight into create-new.
        let screen = if prefill_id.is_some() || prefill_adapter.is_some() {
            Screen::CreateAdapter
        } else {
            Screen::PickExisting
        };

+        // W4 UX: a bare picker OPENS on Pick-existing (the common case — most runs

+        // re-attach an existing endpoint); `n` jumps to Create-new and Esc backs to

+        // the Layer-1 Kind menu (now a jump-target, not the entry). A prefill still

+        // jumps straight into create-new.


        match opt {
            // An online endpoint's PTY is live → attach the rc pump; an offline
            // one must be brought up first. Attach = Control intent.
            ConfirmOption::Attach | ConfirmOption::Start => Some(if online {
                Outcome::Attach { id: ep.id.clone(), intent: AttachIntent::Control }
            } else {
                bringup(match opt {
                    ConfirmOption::Start => RunMode::Start,
                    _ => RunMode::Attach,
                })
            }),

        match opt {
            // An online endpoint's PTY is live → attach the rc pump; an offline
            // one is brought up then ATTACHED by default (W4 UX: Start-now lands you
            // in the live session, not detached with no stdout — the `h` keybind is
            // the headless escape, see [`start_headless_outcome`](Self::start_headless_outcome)).
            // Attach = Control intent.
            ConfirmOption::Attach | ConfirmOption::Start => Some(if online {
                Outcome::Attach { id: ep.id.clone(), intent: AttachIntent::Control }
            } else {
                bringup(RunMode::Attach)
            }),


        match opt {
            // An online endpoint's PTY is live → attach the rc pump; an offline
            // one must be brought up first. Attach = Control intent.
            ConfirmOption::Attach | ConfirmOption::Start => Some(if online {
                Outcome::Attach { id: ep.id.clone(), intent: AttachIntent::Control }
            } else {
                bringup(match opt {
                    ConfirmOption::Start => RunMode::Start,
                    _ => RunMode::Attach,
                })
            }),

        match opt {
            // An online endpoint's PTY is live → attach the rc pump; an offline
            // one is brought up then ATTACHED by default (W4 UX: Start-now lands you
            // in the live session, not detached with no stdout — the `h` keybind is
            // the headless escape, see [`start_headless_outcome`](Self::start_headless_outcome)).
            // Attach = Control intent.
            ConfirmOption::Attach | ConfirmOption::Start => Some(if online {
                Outcome::Attach { id: ep.id.clone(), intent: AttachIntent::Control }
            } else {
                bringup(RunMode::Attach)
            }),

-            // one must be brought up first. Attach = Control intent.

+            // one is brought up then ATTACHED by default (W4 UX: Start-now lands you

+            // in the live session, not detached with no stdout — the `h` keybind is

+            // the headless escape, see [`start_headless_outcome`](Self::start_headless_outcome)).

+            // Attach = Control intent.

-                    ConfirmOption::Start => RunMode::Start,

-                    _ => RunMode::Attach,

+                bringup(RunMode::Attach)

Outcome::Attach { id: ep.id.clone(), intent: AttachIntent::Viewer }


    // ── Resume sub-screen (offline+local only) ───────────────────────────
    pub fn enter_resume(&mut self) {

    /// The HEADLESS bringup (start, no attach) for the selected endpoint — the
    /// `h` escape on the Confirm screen (W4 UX). Only meaningful for an OFFLINE
    /// endpoint (an online one is already up); returns `None` otherwise so the key
    /// is a no-op. Mirrors the default attach bringup but bakes `RunMode::Start` so
    /// the session comes up detached — routes through `cmd_endpoint_run` like every
    /// other bringup (no second path).
    // [impl->REQ-RUN-PICKER]
    pub fn start_headless_outcome(&self) -> Option<Outcome> {
        let ep = self.selected_endpoint()?;
        if ep.status != EpStatus::Offline {
            return None;
        }
        Some(Outcome::Run {
            adapter: ep.adapter_profile.clone(),
            id: ep.id.clone(),
            resume: None,
            mode: RunMode::Start,
        })
    }

    // ── Resume sub-screen (offline+local only) ───────────────────────────
    pub fn enter_resume(&mut self) {


    /// The HEADLESS bringup (start, no attach) for the selected endpoint — the
    /// `h` escape on the Confirm screen (W4 UX). Only meaningful for an OFFLINE
    /// endpoint (an online one is already up); returns `None` otherwise so the key
    /// is a no-op. Mirrors the default attach bringup but bakes `RunMode::Start` so
    /// the session comes up detached — routes through `cmd_endpoint_run` like every
    /// other bringup (no second path).
    // [impl->REQ-RUN-PICKER]
    pub fn start_headless_outcome(&self) -> Option<Outcome> {
        let ep = self.selected_endpoint()?;
        if ep.status != EpStatus::Offline {
            return None;
        }
        Some(Outcome::Run {
            adapter: ep.adapter_profile.clone(),
            id: ep.id.clone(),
            resume: None,
            mode: RunMode::Start,
        })
    }

    // ── Resume sub-screen (offline+local only) ───────────────────────────
    pub fn enter_resume(&mut self) {

+    /// The HEADLESS bringup (start, no attach) for the selected endpoint — the

+    /// `h` escape on the Confirm screen (W4 UX). Only meaningful for an OFFLINE

+    /// endpoint (an online one is already up); returns `None` otherwise so the key

+    /// is a no-op. Mirrors the default attach bringup but bakes `RunMode::Start` so

+    /// the session comes up detached — routes through `cmd_endpoint_run` like every

+    /// other bringup (no second path).

+    pub fn start_headless_outcome(&self) -> Option<Outcome> {

+        let ep = self.selected_endpoint()?;

+        if ep.status != EpStatus::Offline {

+            adapter: ep.adapter_profile.clone(),


    /// Resume-from-history outcome: bring the endpoint up against the chosen
    /// prior session id (`cmd_endpoint_run --resume`).
    pub fn resume_outcome(&self) -> Option<Outcome> {
        let session_id = self.selected_resume()?.session_id.clone();
        let ep = self.selected_endpoint()?;
        Some(Outcome::Run {
            adapter: ep.adapter_profile.clone(),
            id: ep.id.clone(),
            resume: Some(session_id),
            mode: RunMode::Attach,
        })
    }

    /// Resume-from-history outcome: bring the endpoint up against the chosen
    /// prior session id (`cmd_endpoint_run --resume`). `headless` (the `h` keybind,
    /// W4 UX) brings it up detached (`RunMode::Start`); the default ATTACHES
    /// (`RunMode::Attach`) so a resume lands you back in the live session.
    // [impl->REQ-RUN-PICKER]
    pub fn resume_outcome(&self, headless: bool) -> Option<Outcome> {
        let session_id = self.selected_resume()?.session_id.clone();
        let ep = self.selected_endpoint()?;
        Some(Outcome::Run {
            adapter: ep.adapter_profile.clone(),
            id: ep.id.clone(),
            resume: Some(session_id),
            mode: if headless { RunMode::Start } else { RunMode::Attach },
        })
    }


    /// Resume-from-history outcome: bring the endpoint up against the chosen
    /// prior session id (`cmd_endpoint_run --resume`).
    pub fn resume_outcome(&self) -> Option<Outcome> {
        let session_id = self.selected_resume()?.session_id.clone();
        let ep = self.selected_endpoint()?;
        Some(Outcome::Run {
            adapter: ep.adapter_profile.clone(),
            id: ep.id.clone(),
            resume: Some(session_id),
            mode: RunMode::Attach,
        })
    }

    /// Resume-from-history outcome: bring the endpoint up against the chosen
    /// prior session id (`cmd_endpoint_run --resume`). `headless` (the `h` keybind,
    /// W4 UX) brings it up detached (`RunMode::Start`); the default ATTACHES
    /// (`RunMode::Attach`) so a resume lands you back in the live session.
    // [impl->REQ-RUN-PICKER]
    pub fn resume_outcome(&self, headless: bool) -> Option<Outcome> {
        let session_id = self.selected_resume()?.session_id.clone();
        let ep = self.selected_endpoint()?;
        Some(Outcome::Run {
            adapter: ep.adapter_profile.clone(),
            id: ep.id.clone(),
            resume: Some(session_id),
            mode: if headless { RunMode::Start } else { RunMode::Attach },
        })
    }

-    /// prior session id (`cmd_endpoint_run --resume`).

-    pub fn resume_outcome(&self) -> Option<Outcome> {

+    /// prior session id (`cmd_endpoint_run --resume`). `headless` (the `h` keybind,

+    /// W4 UX) brings it up detached (`RunMode::Start`); the default ATTACHES

+    /// (`RunMode::Attach`) so a resume lands you back in the live session.

+    pub fn resume_outcome(&self, headless: bool) -> Option<Outcome> {

+            mode: if headless { RunMode::Start } else { RunMode::Attach },


Now fix the existing model.rs tests my changes break:


    // [unit->REQ-RUN-PICKER] kind layer routes into the right branch.
    #[test]
    fn kind_routes() {
        let mut m = PickerModel::new("p".into(), vec![], vec![], None, None);
        assert_eq!(m.screen, Screen::Kind);
        m.kind_cursor = 0;

    // [unit->REQ-RUN-PICKER] a bare picker OPENS on Pick-existing (W4 UX); the Kind
    // layer is a jump-target (reached via Esc/back) that still routes into the right
    // branch.
    #[test]
    fn kind_routes() {
        let mut m = PickerModel::new("p".into(), vec![], vec![], None, None);
        assert_eq!(m.screen, Screen::PickExisting, "bare picker opens on Pick-existing");
        m.screen = Screen::Kind; // jump back to the Layer-1 menu
        m.kind_cursor = 0;


    // [unit->REQ-RUN-PICKER] kind layer routes into the right branch.
    #[test]
    fn kind_routes() {
        let mut m = PickerModel::new("p".into(), vec![], vec![], None, None);
        assert_eq!(m.screen, Screen::Kind);
        m.kind_cursor = 0;

    // [unit->REQ-RUN-PICKER] a bare picker OPENS on Pick-existing (W4 UX); the Kind
    // layer is a jump-target (reached via Esc/back) that still routes into the right
    // branch.
    #[test]
    fn kind_routes() {
        let mut m = PickerModel::new("p".into(), vec![], vec![], None, None);
        assert_eq!(m.screen, Screen::PickExisting, "bare picker opens on Pick-existing");
        m.screen = Screen::Kind; // jump back to the Layer-1 menu
        m.kind_cursor = 0;

assert_eq!(no_proj.title(), "boot @ T (…xy)");

-    // [unit->REQ-RUN-PICKER] kind layer routes into the right branch.

+    // [unit->REQ-RUN-PICKER] a bare picker OPENS on Pick-existing (W4 UX); the Kind

+    // layer is a jump-target (reached via Esc/back) that still routes into the right

-        assert_eq!(m.screen, Screen::Kind);

+        assert_eq!(m.screen, Screen::PickExisting, "bare picker opens on Pick-existing");

+        m.screen = Screen::Kind; // jump back to the Layer-1 menu


        // offline → bringup (Run) with the endpoint's adapter.
        let off = ep("cold", "g", EpStatus::Offline, true);
        let mut m2 = PickerModel::new("p".into(), vec![], vec![off], None, None);
        m2.category = Category::Local;
        m2.enter_pick();
        assert_eq!(
            m2.confirm_terminal(ConfirmOption::Start),
            Some(Outcome::Run {
                adapter: "claude-spt".into(),
                id: "cold".into(),
                resume: None,
                mode: RunMode::Start,
            })
        );
        // the re-entrant options yield no immediate outcome.
        assert_eq!(m2.confirm_terminal(ConfirmOption::Fork), None);
        assert_eq!(m2.confirm_terminal(ConfirmOption::ChangeAdapter), None);
    }

        // offline Start → bringup (Run) that ATTACHES by default (W4 UX: Start-now
        // lands you in the live session, not detached).
        let off = ep("cold", "g", EpStatus::Offline, true);
        let mut m2 = PickerModel::new("p".into(), vec![], vec![off], None, None);
        m2.category = Category::Local;
        m2.enter_pick();
        assert_eq!(
            m2.confirm_terminal(ConfirmOption::Start),
            Some(Outcome::Run {
                adapter: "claude-spt".into(),
                id: "cold".into(),
                resume: None,
                mode: RunMode::Attach,
            }),
            "offline Start now attaches by default"
        );
        // the `h` headless escape brings the SAME offline endpoint up detached.
        assert_eq!(
            m2.start_headless_outcome(),
            Some(Outcome::Run {
                adapter: "claude-spt".into(),
                id: "cold".into(),
                resume: None,
                mode: RunMode::Start,
            }),
            "h → headless (Start) bringup"
        );
        // the re-entrant options yield no immediate outcome.
        assert_eq!(m2.confirm_terminal(ConfirmOption::Fork), None);
        assert_eq!(m2.confirm_terminal(ConfirmOption::ChangeAdapter), None);

        // h is a no-op on an ONLINE endpoint (already up, nothing to start).
        let online2 = ep("hot", "g", EpStatus::Online, true);
        let mut m3 = PickerModel::new("p".into(), vec![], vec![online2], None, None);
        m3.category = Category::Local;
        m3.enter_pick();
        assert_eq!(m3.start_headless_outcome(), None, "online ⇒ h is a no-op");
    }


        // offline → bringup (Run) with the endpoint's adapter.
        let off = ep("cold", "g", EpStatus::Offline, true);
        let mut m2 = PickerModel::new("p".into(), vec![], vec![off], None, None);
        m2.category = Category::Local;
        m2.enter_pick();
        assert_eq!(
            m2.confirm_terminal(ConfirmOption::Start),
            Some(Outcome::Run {
                adapter: "claude-spt".into(),
                id: "cold".into(),
                resume: None,
                mode: RunMode::Start,
            })
        );
        // the re-entrant options yield no immediate outcome.
        assert_eq!(m2.confirm_terminal(ConfirmOption::Fork), None);
        assert_eq!(m2.confirm_terminal(ConfirmOption::ChangeAdapter), None);
    }

        // offline Start → bringup (Run) that ATTACHES by default (W4 UX: Start-now
        // lands you in the live session, not detached).
        let off = ep("cold", "g", EpStatus::Offline, true);
        let mut m2 = PickerModel::new("p".into(), vec![], vec![off], None, None);
        m2.category = Category::Local;
        m2.enter_pick();
        assert_eq!(
            m2.confirm_terminal(ConfirmOption::Start),
            Some(Outcome::Run {
                adapter: "claude-spt".into(),
                id: "cold".into(),
                resume: None,
                mode: RunMode::Attach,
            }),
            "offline Start now attaches by default"
        );
        // the `h` headless escape brings the SAME offline endpoint up detached.
        assert_eq!(
            m2.start_headless_outcome(),
            Some(Outcome::Run {
                adapter: "claude-spt".into(),
                id: "cold".into(),
                resume: None,
                mode: RunMode::Start,
            }),
            "h → headless (Start) bringup"
        );
        // the re-entrant options yield no immediate outcome.
        assert_eq!(m2.confirm_terminal(ConfirmOption::Fork), None);
        assert_eq!(m2.confirm_terminal(ConfirmOption::ChangeAdapter), None);

        // h is a no-op on an ONLINE endpoint (already up, nothing to start).
        let online2 = ep("hot", "g", EpStatus::Online, true);
        let mut m3 = PickerModel::new("p".into(), vec![], vec![online2], None, None);
        m3.category = Category::Local;
        m3.enter_pick();
        assert_eq!(m3.start_headless_outcome(), None, "online ⇒ h is a no-op");
    }

o => panic!("expected Shortcut, got {o:?}"),

-        // offline → bringup (Run) with the endpoint's adapter.

+        // offline Start → bringup (Run) that ATTACHES by default (W4 UX: Start-now

+        // lands you in the live session, not detached).

+            "offline Start now attaches by default"

+        // the `h` headless escape brings the SAME offline endpoint up detached.

+            m2.start_headless_outcome(),

+                adapter: "claude-spt".into(),

+            "h → headless (Start) bringup"

+        // h is a no-op on an ONLINE endpoint (already up, nothing to start).

+        let online2 = ep("hot", "g", EpStatus::Online, true);

+        let mut m3 = PickerModel::new("p".into(), vec![], vec![online2], None, None);

+        assert_eq!(m3.start_headless_outcome(), None, "online ⇒ h is a no-op");

// [unit->REQ-RUN-PICKER] resume picks a prior session and bakes --resume.


        m.move_resume(1);
        assert_eq!(m.selected_resume().unwrap().session_id, "new");
        assert_eq!(
            m.resume_outcome(),
            Some(Outcome::Run {
                adapter: "claude-spt".into(),
                id: "cold".into(),
                resume: Some("new".into()),
                mode: RunMode::Attach,
            })
        );
        assert!(!m.back() && m.screen == Screen::Confirm, "back → confirm");

        m.move_resume(1);
        assert_eq!(m.selected_resume().unwrap().session_id, "new");
        // Default resume ATTACHES (W4 UX); `h` resumes headless (detached).
        assert_eq!(
            m.resume_outcome(false),
            Some(Outcome::Run {
                adapter: "claude-spt".into(),
                id: "cold".into(),
                resume: Some("new".into()),
                mode: RunMode::Attach,
            })
        );
        assert_eq!(
            m.resume_outcome(true),
            Some(Outcome::Run {
                adapter: "claude-spt".into(),
                id: "cold".into(),
                resume: Some("new".into()),
                mode: RunMode::Start,
            }),
            "h → headless resume"
        );
        assert!(!m.back() && m.screen == Screen::Confirm, "back → confirm");


        m.move_resume(1);
        assert_eq!(m.selected_resume().unwrap().session_id, "new");
        assert_eq!(
            m.resume_outcome(),
            Some(Outcome::Run {
                adapter: "claude-spt".into(),
                id: "cold".into(),
                resume: Some("new".into()),
                mode: RunMode::Attach,
            })
        );
        assert!(!m.back() && m.screen == Screen::Confirm, "back → confirm");

        m.move_resume(1);
        assert_eq!(m.selected_resume().unwrap().session_id, "new");
        // Default resume ATTACHES (W4 UX); `h` resumes headless (detached).
        assert_eq!(
            m.resume_outcome(false),
            Some(Outcome::Run {
                adapter: "claude-spt".into(),
                id: "cold".into(),
                resume: Some("new".into()),
                mode: RunMode::Attach,
            })
        );
        assert_eq!(
            m.resume_outcome(true),
            Some(Outcome::Run {
                adapter: "claude-spt".into(),
                id: "cold".into(),
                resume: Some("new".into()),
                mode: RunMode::Start,
            }),
            "h → headless resume"
        );
        assert!(!m.back() && m.screen == Screen::Confirm, "back → confirm");

+        // Default resume ATTACHES (W4 UX); `h` resumes headless (detached).

+                resume: Some("new".into()),


Now wire `h` into mod.rs key handlers + fix the `resume_outcome` caller:


        Screen::Resume => match code {
            KeyCode::Up => model.move_resume(-1),
            KeyCode::Down => model.move_resume(1),
            KeyCode::Enter => return model.resume_outcome(),
            _ => {}
        },

        Screen::Resume => match code {
            KeyCode::Up => model.move_resume(-1),
            KeyCode::Down => model.move_resume(1),
            // Enter resumes + ATTACHES; `h` resumes headless (detached) — W4 UX.
            KeyCode::Enter => return model.resume_outcome(false),
            KeyCode::Char('h') => return model.resume_outcome(true),
            _ => {}
        },


The file C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\mod.rs has been updated successfully. (file state is current in your context — no need to Read it back)

        Screen::Resume => match code {
            KeyCode::Up => model.move_resume(-1),
            KeyCode::Down => model.move_resume(1),
            KeyCode::Enter => return model.resume_outcome(),
            _ => {}
        },

        Screen::Resume => match code {
            KeyCode::Up => model.move_resume(-1),
            KeyCode::Down => model.move_resume(1),
            // Enter resumes + ATTACHES; `h` resumes headless (detached) — W4 UX.
            KeyCode::Enter => return model.resume_outcome(false),
            KeyCode::Char('h') => return model.resume_outcome(true),
            _ => {}
        },

-            KeyCode::Enter => return model.resume_outcome(),

+            // Enter resumes + ATTACHES; `h` resumes headless (detached) — W4 UX.

+            KeyCode::Enter => return model.resume_outcome(false),

+            KeyCode::Char('h') => return model.resume_outcome(true),


        // The `s` keybind = build/update the spt-<id> shortcut from every
        // pre-start options set (picker spec).
        KeyCode::Char('s') => return model.confirm_terminal(ConfirmOption::Shortcut),

        // The `s` keybind = build/update the spt-<id> shortcut from every
        // pre-start options set (picker spec).
        KeyCode::Char('s') => return model.confirm_terminal(ConfirmOption::Shortcut),
        // The `h` keybind = bring an OFFLINE endpoint up HEADLESS (start, no
        // attach) — the escape from the W4 attach-by-default; a no-op online.
        KeyCode::Char('h') => return model.start_headless_outcome(),


        // The `s` keybind = build/update the spt-<id> shortcut from every
        // pre-start options set (picker spec).
        KeyCode::Char('s') => return model.confirm_terminal(ConfirmOption::Shortcut),
        // The `h` keybind = bring an OFFLINE endpoint up HEADLESS (start, no
        // attach) — the escape from the W4 attach-by-default; a no-op online.
        KeyCode::Char('h') => return model.start_headless_outcome(),

+        // The `h` keybind = bring an OFFLINE endpoint up HEADLESS (start, no

+        // attach) — the escape from the W4 attach-by-default; a no-op online.

+        KeyCode::Char('h') => return model.start_headless_outcome(),


Now item 3 — resolve `driven_by` to node NAME in data.rs. Add a pure helper + label map, thread into `local_rows`:


/// Every discoverable endpoint, across all three categories. The model
/// partitions by category; this just produces the union (local perches +
/// subnet registry rows).
// [impl->REQ-RUN-PICKER]
pub fn gather_endpoints() -> Vec<EndpointRow> {
    let local = local_rows();
    let subnet = subnet_rows();
    let mut rows = Vec::with_capacity(local.len() + subnet.len());
    rows.extend(local);
    rows.extend(subnet);
    reconcile_self_owned(&mut rows);
    rows
}

/// Every discoverable endpoint, across all three categories. The model
/// partitions by category; this just produces the union (local perches +
/// subnet registry rows).
// [impl->REQ-RUN-PICKER]
pub fn gather_endpoints() -> Vec<EndpointRow> {
    // Resolve the node-key→label map ONCE (the subnet registries) so a LOCAL row's
    // `driven_by` controller renders as a NAME, not bare key-hex (REQ-PICKER-4).
    let node_labels = node_label_map();
    let local = local_rows(&node_labels);
    let subnet = subnet_rows();
    let mut rows = Vec::with_capacity(local.len() + subnet.len());
    rows.extend(local);
    rows.extend(subnet);
    reconcile_self_owned(&mut rows);
    rows
}

/// The known node-key → human label map, gathered from this node's per-subnet
/// registry snapshots (the same `node_labels` lease `endpoint list` reads). Used
/// to render a `driven_by` controller key as its canonical `LABEL (keyprefix…)`
/// instead of bare hex (REQ-PICKER-4). Degrades to empty (→ bare `keyprefix…`,
/// still better than the full raw key) when no snapshot carries the label.
fn node_label_map() -> std::collections::BTreeMap<String, String> {
    let mut map = std::collections::BTreeMap::new();
    let subnets = spt_store::subnet::SubnetStore::load();
    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join("registry"));
    for s in &subnets.subnets {
        if let Some(reg) = regs.get(&s.name) {
            for (node, label) in reg.node_labels() {
                map.entry(node.to_string()).or_insert_with(|| label.to_string());
            }
        }
    }
    map
}

/// Render an endpoint's raw `driven_by` controller key through the ONE canonical
/// node renderer (`node_label_display`) — `LABEL (keyprefix…)` when the label is
/// known, else `keyprefix…` — so the picker's "controlled by" pin never leaks the
/// full raw key-hex (REQ-PICKER-4, the D item). Pure over its inputs (unit-tested);
/// `None` in ⇒ `None` out (no controller).
// [impl->REQ-PICKER-4]
fn driven_by_display(
    hex: Option<&str>,
    node_labels: &std::collections::BTreeMap<String, String>,
) -> Option<String> {
    hex.map(|h| {
        spt_net::net::registry::node_label_display(h, node_labels.get(h).map(|s| s.as_str()))
    })
}


The file C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\data.rs has been updated successfully. (file state is current in your context — no need to Read it back)

/// Every discoverable endpoint, across all three categories. The model
/// partitions by category; this just produces the union (local perches +
/// subnet registry rows).
// [impl->REQ-RUN-PICKER]
pub fn gather_endpoints() -> Vec<EndpointRow> {
    let local = local_rows();
    let subnet = subnet_rows();
    let mut rows = Vec::with_capacity(local.len() + subnet.len());
    rows.extend(local);
    rows.extend(subnet);
    reconcile_self_owned(&mut rows);
    rows
}

/// Every discoverable endpoint, across all three categories. The model
/// partitions by category; this just produces the union (local perches +
/// subnet registry rows).
// [impl->REQ-RUN-PICKER]
pub fn gather_endpoints() -> Vec<EndpointRow> {
    // Resolve the node-key→label map ONCE (the subnet registries) so a LOCAL row's
    // `driven_by` controller renders as a NAME, not bare key-hex (REQ-PICKER-4).
    let node_labels = node_label_map();
    let local = local_rows(&node_labels);
    let subnet = subnet_rows();
    let mut rows = Vec::with_capacity(local.len() + subnet.len());
    rows.extend(local);
    rows.extend(subnet);
    reconcile_self_owned(&mut rows);
    rows
}

/// The known node-key → human label map, gathered from this node's per-subnet
/// registry snapshots (the same `node_labels` lease `endpoint list` reads). Used
/// to render a `driven_by` controller key as its canonical `LABEL (keyprefix…)`
/// instead of bare hex (REQ-PICKER-4). Degrades to empty (→ bare `keyprefix…`,
/// still better than the full raw key) when no snapshot carries the label.
fn node_label_map() -> std::collections::BTreeMap<String, String> {
    let mut map = std::collections::BTreeMap::new();
    let subnets = spt_store::subnet::SubnetStore::load();
    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join("registry"));
    for s in &subnets.subnets {
        if let Some(reg) = regs.get(&s.name) {
            for (node, label) in reg.node_labels() {
                map.entry(node.to_string()).or_insert_with(|| label.to_string());
            }
        }
    }
    map
}

/// Render an endpoint's raw `driven_by` controller key through the ONE canonical
/// node renderer (`node_label_display`) — `LABEL (keyprefix…)` when the label is
/// known, else `keyprefix…` — so the picker's "controlled by" pin never leaks the
/// full raw key-hex (REQ-PICKER-4, the D item). Pure over its inputs (unit-tested);
/// `None` in ⇒ `None` out (no controller).
// [impl->REQ-PICKER-4]
fn driven_by_display(
    hex: Option<&str>,
    node_labels: &std::collections::BTreeMap<String, String>,
) -> Option<String> {
    hex.map(|h| {
        spt_net::net::registry::node_label_display(h, node_labels.get(h).map(|s| s.as_str()))
    })
}

+    // Resolve the node-key→label map ONCE (the subnet registries) so a LOCAL row's

+    // `driven_by` controller renders as a NAME, not bare key-hex (REQ-PICKER-4).

+    let node_labels = node_label_map();

+    let local = local_rows(&node_labels);

+/// The known node-key → human label map, gathered from this node's per-subnet

+/// registry snapshots (the same `node_labels` lease `endpoint list` reads). Used

+/// to render a `driven_by` controller key as its canonical `LABEL (keyprefix…)`

+/// instead of bare hex (REQ-PICKER-4). Degrades to empty (→ bare `keyprefix…`,

+/// still better than the full raw key) when no snapshot carries the label.

+fn node_label_map() -> std::collections::BTreeMap<String, String> {

+    let mut map = std::collections::BTreeMap::new();

+    let subnets = spt_store::subnet::SubnetStore::load();

+    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join("registry"));

+        if let Some(reg) = regs.get(&s.name) {

+            for (node, label) in reg.node_labels() {

+                map.entry(node.to_string()).or_insert_with(|| label.to_string());

+/// Render an endpoint's raw `driven_by` controller key through the ONE canonical

+/// node renderer (`node_label_display`) — `LABEL (keyprefix…)` when the label is

+/// known, else `keyprefix…` — so the picker's "controlled by" pin never leaks the

+/// full raw key-hex (REQ-PICKER-4, the D item). Pure over its inputs (unit-tested);

+/// `None` in ⇒ `None` out (no controller).

+    node_labels: &std::collections::BTreeMap<String, String>,

+        spt_net::net::registry::node_label_display(h, node_labels.get(h).map(|s| s.as_str()))

/// Reconcile the status of a self-owned endpoint that is dual-listed in both the

/// Local and Subnet categories (REQ-PICKER-3). The two listings draw from

/// independent sources — Local from the live roster (ground truth for an endpoint


/// This node's local perches (from the roster). Local rows feed both the Local
/// and Project categories (the model filters Project by project-history match).
fn local_rows() -> Vec<EndpointRow> {

/// This node's local perches (from the roster). Local rows feed both the Local
/// and Project categories (the model filters Project by project-history match).
/// `node_labels` resolves a row's `driven_by` controller key to its display name.
fn local_rows(node_labels: &std::collections::BTreeMap<String, String>) -> Vec<EndpointRow> {


/// This node's local perches (from the roster). Local rows feed both the Local
/// and Project categories (the model filters Project by project-history match).
fn local_rows() -> Vec<EndpointRow> {

/// This node's local perches (from the roster). Local rows feed both the Local
/// and Project categories (the model filters Project by project-history match).
/// `node_labels` resolves a row's `driven_by` controller key to its display name.
fn local_rows(node_labels: &std::collections::BTreeMap<String, String>) -> Vec<EndpointRow> {

+/// `node_labels` resolves a row's `driven_by` controller key to its display name.

+fn local_rows(node_labels: &std::collections::BTreeMap<String, String>) -> Vec<EndpointRow> {


                // Render the controller key as its NAME, not raw hex (REQ-PICKER-4).
                driven_by: driven_by_display(
                    rec.as_ref().and_then(|r| r.driven_by.as_deref()),
                    node_labels,
                ),


                // Render the controller key as its NAME, not raw hex (REQ-PICKER-4).
                driven_by: driven_by_display(
                    rec.as_ref().and_then(|r| r.driven_by.as_deref()),
                    node_labels,
                ),

-                driven_by: rec.as_ref().and_then(|r| r.driven_by.clone()),

+                // Render the controller key as its NAME, not raw hex (REQ-PICKER-4).

+                driven_by: driven_by_display(

+                    rec.as_ref().and_then(|r| r.driven_by.as_deref()),

// Broker-PTY-controllability + type for the 4-state display

// (REQ-PICKER-1) — read off this local perch's info.json.


Update the `driven_by` field doc in model.rs to reflect it now carries the display name for local rows:


    /// The node currently controlling this endpoint's session (its broker-owned
    /// `driven_by`), if any — drives the W2.5 status-conditional confirm options
    /// (REQ-RCVIEW-1 / REQ-KICK-1): a CONTROLLED endpoint offers View + Kick, not
    /// a plain Attach. Local rows read it off `info.json`; subnet rows leave it
    /// `None` (the wire busy-refuse + client guidance still protect a remote one).
    pub driven_by: Option<String>,

    /// The node currently controlling this endpoint's session (its broker-owned
    /// `driven_by`), if any — drives the W2.5 status-conditional confirm options
    /// (REQ-RCVIEW-1 / REQ-KICK-1): a CONTROLLED endpoint offers View + Kick, not
    /// a plain Attach. Local rows read it off `info.json` and render it through the
    /// canonical `node_label_display` so the "controlled by" pin shows the NODE NAME
    /// (`LABEL (keyprefix…)`), not raw key-hex (REQ-PICKER-4); subnet rows leave it
    /// `None` (the wire busy-refuse + client guidance still protect a remote one).
    /// Only ever read as `is_some()` (the controlled gate) or rendered verbatim.
    pub driven_by: Option<String>,