1use std::path::PathBuf;
15
16#[derive(Debug, Clone, Copy)]
17pub struct Step {
18 pub heading: &'static str,
19 pub body: &'static str,
20}
21
22pub const STEPS: &[Step] = &[
23 Step {
24 heading: "Welcome to teamctl-ui",
25 body: "A live view of your team. Roster on the left, focused agent in the middle, mailbox on the right. Press any key to advance, Esc to leave.",
26 },
27 Step {
28 heading: "Roster + state glyphs",
29 body: "Each agent shows a single-cell glyph: ● running · ✉ unread · ! approval pending · ✕ stopped · ? unknown. Tab to focus the roster, j/k to walk it.",
30 },
31 Step {
32 heading: "Detail pane",
33 body: "The selected agent's tmux session streams here. The title line shows which agent you're following; lines tail-clip to fit.",
34 },
35 Step {
36 heading: "Mailbox tabs",
37 body: "Inbox / Channel / Wire — `]` walks forward, `[` walks back, when the mailbox pane is focused. Tab itself always cycles pane focus, never tabs. Inbox is DMs to the focused agent; Wire is project-wide broadcasts.",
38 },
39 Step {
40 heading: "Approvals",
41 body: "When an agent files request_approval, a stripe appears at the top. Press `a` to open the modal, then `y` to approve or Shift-`N` to deny. j/k cycle if multiple are pending.",
42 },
43 Step {
44 heading: "Compose",
45 body: "@ DMs the focused agent; ! broadcasts to a channel (picker first). The editor is vim-style — i to insert, Esc to normal, Ctrl+Enter to send, Esc Esc to cancel.",
46 },
47 Step {
48 heading: "Layouts",
49 body: "Ctrl+W toggles Wall view (4 agents at once + scroll). Ctrl+M toggles Mailbox-first (channel-feed centric). Both fall back to Triptych on toggle.",
50 },
51 Step {
52 heading: "Splits",
53 body: "Ctrl+| / Ctrl+- split the detail pane so you can watch two agents at once. Ctrl+H/J/K/L cycles between splits, Ctrl+W q closes the focused one.",
54 },
55 Step {
56 heading: "Help + quit",
57 body: "? opens the full keymap. q quits (with confirm). t reopens this tour. You're ready.",
58 },
59];
60
61pub fn sentinel_path(team_root: &std::path::Path) -> PathBuf {
65 team_root.join("state/ui-tutorial-completed")
66}
67
68pub fn has_completed(team_root: &std::path::Path) -> bool {
69 sentinel_path(team_root).exists()
70}
71
72pub fn mark_completed(team_root: &std::path::Path) -> std::io::Result<()> {
86 let path = sentinel_path(team_root);
87 if let Some(parent) = path.parent() {
88 std::fs::create_dir_all(parent)?;
89 }
90 std::fs::write(&path, b"")
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use std::fs;
97
98 #[test]
99 fn step_count_under_ten() {
100 assert!(
103 STEPS.len() <= 10,
104 "tutorial bloated to {} steps",
105 STEPS.len()
106 );
107 }
108
109 #[test]
110 fn sentinel_round_trip_in_tempdir() {
111 let tmp = tempfile::tempdir().unwrap();
112 let root = tmp.path();
113 assert!(!has_completed(root));
114 mark_completed(root).unwrap();
115 assert!(has_completed(root));
116 let marker = sentinel_path(root);
119 let bytes = fs::read(&marker).unwrap();
120 assert!(bytes.is_empty());
121 }
122}