use std::path::PathBuf;
#[derive(Debug, Clone, Copy)]
pub struct Step {
pub heading: &'static str,
pub body: &'static str,
}
pub const STEPS: &[Step] = &[
Step {
heading: "Welcome to teamctl-ui",
body: "A live view of your team. Agents sidebar on the left, with Detail above Mailbox on the right. Press any key to advance, Esc to leave.",
},
Step {
heading: "Agents + state glyphs",
body: "Each agent shows a single-cell glyph: ● running · ✉ unread · ! approval pending · ✕ stopped · ? unknown. Tab to focus the Agents column, j/k to walk it.",
},
Step {
heading: "Detail pane",
body: "The selected agent's tmux session streams here. The title line shows which agent you're following; lines tail-clip to fit.",
},
Step {
heading: "Mailbox tabs",
body: "Inbox / Sent / 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; Sent is everything that agent has emitted (DMs, telegram replies, channel posts, wire broadcasts); Wire is project-wide broadcasts.",
},
Step {
heading: "Approvals",
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.",
},
Step {
heading: "Compose",
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.",
},
Step {
heading: "Layouts",
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.",
},
Step {
heading: "Splits",
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.",
},
Step {
heading: "Help + quit",
body: "? opens the full keymap. q quits (with confirm). t reopens this tour. You're ready.",
},
];
pub fn sentinel_path(team_root: &std::path::Path) -> PathBuf {
team_root.join("state/ui-tutorial-completed")
}
pub fn has_completed(team_root: &std::path::Path) -> bool {
sentinel_path(team_root).exists()
}
pub fn mark_completed(team_root: &std::path::Path) -> std::io::Result<()> {
let path = sentinel_path(team_root);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&path, b"")
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn step_count_under_ten() {
assert!(
STEPS.len() <= 10,
"tutorial bloated to {} steps",
STEPS.len()
);
}
#[test]
fn sentinel_round_trip_in_tempdir() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
assert!(!has_completed(root));
mark_completed(root).unwrap();
assert!(has_completed(root));
let marker = sentinel_path(root);
let bytes = fs::read(&marker).unwrap();
assert!(bytes.is_empty());
}
}