1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
//! Setup state for a session, and the header copy that follows from it.
//!
//! One fact decides how a running session presents itself: whether the active
//! provider has usable credentials. The session header and the statusline badge
//! read the same [`SetupState`], so a login or a logout changes them together
//! instead of letting them drift apart.
//!
//! [`SetupEntry`] is the separate question of whether this launch opens the
//! full-screen setup at all, and at which step. See [`super::setup_screen`].
use ratatui::{style::Style, text::Span};
use super::theme::Theme;
/// How much weight a header hint carries.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum HintTone {
/// Reference material the user can read at leisure.
Reference,
/// The one step that unblocks the session.
NextStep,
}
impl HintTone {
fn style(self) -> Style {
match self {
Self::Reference => Theme::dim(),
Self::NextStep => Theme::accent(),
}
}
}
/// One line of the header hint block.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct Hint {
pub(super) text: &'static str,
pub(super) tone: HintTone,
}
impl Hint {
const fn reference(text: &'static str) -> Self {
Self {
text,
tone: HintTone::Reference,
}
}
const fn next_step(text: &'static str) -> Self {
Self {
text,
tone: HintTone::NextStep,
}
}
pub(super) fn style(self) -> Style {
self.tone.style()
}
}
/// Hints for a session that can already run a turn.
const READY_HINTS: &[Hint] = &[
Hint::reference(" shift+tab Cycle reasoning level"),
Hint::reference(" ctrl+c Clear the composer"),
Hint::reference(" / Show available commands"),
Hint::reference(" ! Run a shell command"),
];
/// Hints for a session with no usable credentials. Login leads, because every
/// other action fails until it succeeds.
const SIGNED_OUT_HINTS: &[Hint] = &[
Hint::next_step(" /login Sign in to a provider"),
Hint::reference(" /model Choose a model"),
Hint::reference(" / Show available commands"),
];
/// The one headline the session header can carry. A first launch says its
/// welcome on the setup screen, so the header never repeats it.
const SIGNED_OUT_HEADLINE: &str = " Not signed in. Rho needs a provider before it can answer.";
/// Which step a launch opens the first-run setup screen at.
///
/// A real first launch asks for [`SetupEntry::Auto`]. The named steps exist so
/// the flow can be reviewed on a machine that is already configured, where
/// `Auto` would skip sign-in because models are already available.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum SetupEntry {
/// Whichever step can do something.
Auto,
/// Sign-in, even when the session could already list models.
SignIn,
/// Model choice, even when no login has happened on this machine.
ChooseModel,
}
/// Where a session sits in provider setup.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct SetupState {
/// The active provider resolved to usable credentials.
pub(super) signed_in: bool,
}
impl Default for SetupState {
/// A session that can run a turn, which is what most sessions are.
fn default() -> Self {
Self { signed_in: true }
}
}
impl SetupState {
/// Copy above the hint block, shown only when the session cannot run a
/// turn. A session that works needs no announcement.
pub(super) fn headline(self) -> Option<Span<'static>> {
(!self.signed_in).then(|| Span::styled(SIGNED_OUT_HEADLINE, Theme::warning()))
}
pub(super) fn hints(self) -> &'static [Hint] {
if self.signed_in {
READY_HINTS
} else {
SIGNED_OUT_HINTS
}
}
}
impl super::App {
/// The session's live setup state. Login and logout clear or set
/// `auth_unavailable`, so every read reflects the current credentials.
pub(super) fn setup_state(&self) -> SetupState {
SetupState {
signed_in: self.info.services.auth_unavailable.is_none(),
}
}
/// A prompt submitted with no credentials opens the login picker instead of
/// failing a turn, and the composer holds the prompt so one enter sends it
/// once a provider is live.
///
/// The prompt is written back rather than left alone, because a template or
/// skill command clears the composer while it expands. Writing back the
/// resolved text keeps that work instead of dropping it, and it is the text
/// the turn would have sent.
pub(super) fn offer_login_instead_of_turn(
&mut self,
turn: super::TurnPrompt,
) -> anyhow::Result<()> {
self.input_ui.set_text(turn.display);
self.input_ui.set_cursor(self.input_ui.char_len());
// Open the picker first; it sets its own chrome status, then replace
// the toast with the held-prompt notice the user needs to see.
self.open_login_picker();
self.notify_status("not signed in yet; your prompt is still in the composer");
Ok(())
}
/// Point a freshly signed-in user back at the prompt they were holding.
pub(super) fn announce_held_prompt_after_login(&mut self) {
if self.setup_state().signed_in && !self.input_ui.text().trim().is_empty() {
self.notify_status("press enter to send the prompt you were holding");
}
}
}
#[cfg(test)]
#[path = "first_run_tests.rs"]
mod tests;