Skip to main content

pi/modes/interactive/
startup.rs

1//! Startup surfaces: loaded resources summary, diagnostics, first-time setup,
2//! shortcut overlay, and release-notes/changelog.
3//!
4//! Ports `renderLoadedResources`, `IdleStatus`, `FirstTimeSetupComponent`,
5//! the `/hotkeys` overlay, and the `/changelog` markdown box. Pure data →
6//! pi-tui components.
7
8use pi_tui::component::Component;
9use pi_tui::components::{Markdown, Spacer, Text};
10
11use super::state::{LoadedResource, ShortcutHint, StartupDiagnostics};
12use super::theme::{self, MarkdownOptions, MarkdownTheme, ResolvedTheme, ThemeColor};
13
14// ---------------------------------------------------------------------------
15// Loaded resources
16// ---------------------------------------------------------------------------
17
18/// Build the loaded-resources summary (one line per skill/prompt/theme/context).
19#[must_use]
20pub fn build_resources(resources: &[LoadedResource], th: &ResolvedTheme) -> Box<dyn Component> {
21    if resources.is_empty() {
22        return Box::new(Spacer::new(0));
23    }
24    let mut stack = super::messages::ColumnStack::new();
25    for r in resources {
26        let line = format!(
27            "{} {}",
28            th.fg(ThemeColor::Accent, &format!("[{}]", r.kind)),
29            th.fg(ThemeColor::Muted, &r.label),
30        );
31        stack.push(Box::new(Text::with_padding(line, 1, 0)));
32    }
33    Box::new(stack)
34}
35
36// ---------------------------------------------------------------------------
37// Startup diagnostics
38// ---------------------------------------------------------------------------
39
40/// Build the startup diagnostics block (warnings/errors from resource load).
41#[must_use]
42pub fn build_diagnostics(
43    diagnostics: &StartupDiagnostics,
44    th: &ResolvedTheme,
45) -> Box<dyn Component> {
46    if diagnostics.entries.is_empty() {
47        return Box::new(Spacer::new(0));
48    }
49    let mut stack = super::messages::ColumnStack::new();
50    for d in &diagnostics.entries {
51        let (color, glyph) = match d.severity {
52            super::state::DiagnosticSeverity::Warning => (ThemeColor::Warning, "⚠"),
53            super::state::DiagnosticSeverity::Error => (ThemeColor::Error, "✗"),
54        };
55        let line = format!(
56            "{} {}{}",
57            th.fg(color, glyph),
58            th.fg(ThemeColor::Dim, &format!("{}: ", d.source)),
59            d.message,
60        );
61        stack.push(Box::new(Text::with_padding(line, 1, 0)));
62    }
63    Box::new(stack)
64}
65
66// ---------------------------------------------------------------------------
67// First-time setup
68// ---------------------------------------------------------------------------
69
70/// Build the first-time-setup wizard component (theme + analytics steps).
71#[must_use]
72pub fn build_first_time_setup(
73    step: usize,
74    md_theme: MarkdownTheme,
75    th: &ResolvedTheme,
76) -> Box<dyn Component> {
77    let mut stack = super::messages::ColumnStack::new();
78    stack.push(Box::new(Text::with_padding(
79        theme::bold(&th.fg(ThemeColor::Accent, "Welcome to pi")),
80        1,
81        0,
82    )));
83    let body = match step {
84        0 => "Choose a theme: **dark** or **light**?\nUse `/theme` later to change.",
85        1 => "Enable anonymous usage analytics?\n(you can change this in settings)",
86        _ => "Setup complete. Type a message to begin.",
87    };
88    stack.push(Box::new(Markdown::new(
89        body,
90        1,
91        0,
92        md_theme,
93        theme::default_text_style(),
94        MarkdownOptions::default(),
95    )));
96    Box::new(stack)
97}
98
99// ---------------------------------------------------------------------------
100// Shortcut overlay
101// ---------------------------------------------------------------------------
102
103/// Build the shortcut/help overlay component from hint rows.
104#[must_use]
105pub fn build_shortcut_overlay(
106    hints: &[ShortcutHint],
107    extension_hints: &[ShortcutHint],
108    th: &ResolvedTheme,
109) -> Box<dyn Component> {
110    let mut stack = super::messages::ColumnStack::new();
111    stack.push(Box::new(Text::with_padding(
112        theme::bold(&th.fg(ThemeColor::Accent, "Keyboard shortcuts")),
113        1,
114        0,
115    )));
116    push_shortcut_hints(&mut stack, hints, th);
117    if !extension_hints.is_empty() {
118        stack.push(Box::new(Spacer::new(1)));
119        stack.push(Box::new(Text::with_padding(
120            theme::bold(&th.fg(ThemeColor::Accent, "Extensions")),
121            1,
122            0,
123        )));
124        push_shortcut_hints(&mut stack, extension_hints, th);
125    }
126    Box::new(stack)
127}
128
129fn push_shortcut_hints(
130    stack: &mut super::messages::ColumnStack,
131    hints: &[ShortcutHint],
132    th: &ResolvedTheme,
133) {
134    for hint in hints {
135        let line = format!(
136            "  {}  {}",
137            th.fg(ThemeColor::Accent, &hint.key),
138            th.fg(ThemeColor::Muted, &hint.action),
139        );
140        stack.push(Box::new(Text::with_padding(line, 0, 0)));
141    }
142}
143
144/// Default shortcut hint rows (ports the reference keybinding table).
145#[must_use]
146pub fn default_shortcut_hints() -> Vec<ShortcutHint> {
147    use super::state::ShortcutHint;
148    [
149        ("Esc", "Interrupt / abort"),
150        ("Ctrl+C", "Clear editor (×2 to exit)"),
151        ("Ctrl+D", "Exit"),
152        ("Shift+Tab", "Cycle thinking"),
153        ("Ctrl+L", "Model selector"),
154        ("Ctrl+P", "Cycle models"),
155        ("Ctrl+O", "Toggle tool output"),
156        ("Ctrl+T", "Toggle thinking"),
157        ("Ctrl+G", "External editor"),
158        ("Ctrl+X", "Copy last assistant"),
159        ("Alt+Enter", "Queue follow-up"),
160        ("Alt+Up", "Restore follow-up"),
161        ("Ctrl+V", "Paste image/text"),
162        ("/help", "Slash commands"),
163        ("? , /hotkeys", "This overlay"),
164    ]
165    .into_iter()
166    .map(|(key, action)| ShortcutHint {
167        key: key.to_owned(),
168        action: action.to_owned(),
169    })
170    .collect()
171}
172
173// ---------------------------------------------------------------------------
174// Release notes / changelog
175// ---------------------------------------------------------------------------
176
177/// Build the changelog/release-notes component from markdown source.
178#[must_use]
179pub fn build_changelog(
180    markdown: &str,
181    md_theme: MarkdownTheme,
182    _th: &ResolvedTheme,
183) -> Box<dyn Component> {
184    let mut stack = super::messages::ColumnStack::new();
185    stack.push(Box::new(Markdown::new(
186        markdown,
187        1,
188        0,
189        md_theme,
190        theme::default_text_style(),
191        MarkdownOptions::default(),
192    )));
193    Box::new(stack)
194}