Skip to content

Commit 12438e0

Browse files
committed
cli updates
Signed-off-by: Yujong Lee <yujonglee.dev@gmail.com>
1 parent 4203a63 commit 12438e0

File tree

29 files changed

+532
-457
lines changed

29 files changed

+532
-457
lines changed

apps/cli/src/cli.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,10 @@ pub enum Commands {
8989
provider: Option<ConnectProvider>,
9090
},
9191
/// Browse past sessions
92-
Sessions,
92+
Sessions {
93+
#[command(subcommand)]
94+
command: Option<SessionsCommands>,
95+
},
9396
/// Show configured providers and settings
9497
Status,
9598
/// Authenticate with char.com
@@ -123,6 +126,15 @@ pub enum Commands {
123126
},
124127
}
125128

129+
#[derive(Subcommand)]
130+
pub enum SessionsCommands {
131+
/// View a specific session
132+
View {
133+
#[arg(long)]
134+
id: String,
135+
},
136+
}
137+
126138
#[derive(Subcommand)]
127139
pub enum ChatCommands {
128140
/// Resume an existing chat session

apps/cli/src/commands/chat/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ impl Screen for ChatScreen {
7474
let effects = self.app.dispatch(Action::Paste(pasted));
7575
self.apply_effects(effects)
7676
}
77-
TuiEvent::Draw => ScreenControl::Continue,
77+
TuiEvent::Draw | TuiEvent::Resize => ScreenControl::Continue,
7878
}
7979
}
8080

apps/cli/src/commands/connect/app.rs

Lines changed: 77 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,23 @@ use url::Url;
55
use crate::cli::{ConnectProvider, ConnectionType};
66

77
use super::action::Action;
8-
use super::effect::Effect;
8+
use super::effect::{Effect, SaveData};
99
use super::providers::{LLM_PROVIDERS, STT_PROVIDERS};
1010

1111
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1212
pub(crate) enum Step {
13-
SelectType,
1413
SelectProvider,
1514
InputBaseUrl,
1615
InputApiKey,
1716
Done,
1817
}
1918

19+
#[derive(Clone, Copy, Debug)]
20+
pub(crate) enum ListEntry {
21+
Header(ConnectionType),
22+
Provider(ConnectionType, ConnectProvider),
23+
}
24+
2025
pub(crate) struct App {
2126
step: Step,
2227
connection_type: Option<ConnectionType>,
@@ -40,7 +45,7 @@ impl App {
4045
api_key: Option<String>,
4146
) -> (Self, Vec<Effect>) {
4247
let mut app = Self {
43-
step: Step::SelectType,
48+
step: Step::SelectProvider,
4449
connection_type,
4550
provider,
4651
base_url,
@@ -100,23 +105,36 @@ impl App {
100105
&mut self.list_state
101106
}
102107

103-
pub fn provider_list(&self) -> &'static [ConnectProvider] {
108+
pub fn flat_entries(&self) -> Vec<ListEntry> {
104109
match self.connection_type {
105-
Some(ConnectionType::Stt) => STT_PROVIDERS,
106-
Some(ConnectionType::Llm) => LLM_PROVIDERS,
107-
None => &[],
110+
Some(ConnectionType::Llm) => LLM_PROVIDERS
111+
.iter()
112+
.map(|&p| ListEntry::Provider(ConnectionType::Llm, p))
113+
.collect(),
114+
Some(ConnectionType::Stt) => STT_PROVIDERS
115+
.iter()
116+
.map(|&p| ListEntry::Provider(ConnectionType::Stt, p))
117+
.collect(),
118+
None => {
119+
let mut entries = Vec::new();
120+
entries.push(ListEntry::Header(ConnectionType::Llm));
121+
for &p in LLM_PROVIDERS {
122+
entries.push(ListEntry::Provider(ConnectionType::Llm, p));
123+
}
124+
entries.push(ListEntry::Header(ConnectionType::Stt));
125+
for &p in STT_PROVIDERS {
126+
entries.push(ListEntry::Provider(ConnectionType::Stt, p));
127+
}
128+
entries
129+
}
108130
}
109131
}
110132

111133
pub fn breadcrumb(&self) -> String {
112-
let mut parts = Vec::new();
113-
if let Some(ct) = self.connection_type {
114-
parts.push(ct.to_string());
115-
}
116-
if let Some(p) = self.provider {
117-
parts.push(p.to_string());
134+
match self.provider {
135+
Some(p) => p.to_string(),
136+
None => String::new(),
118137
}
119-
parts.join(" > ")
120138
}
121139

122140
fn handle_key(&mut self, key: KeyEvent) -> Vec<Effect> {
@@ -127,7 +145,7 @@ impl App {
127145
}
128146

129147
match self.step {
130-
Step::SelectType | Step::SelectProvider => self.handle_list_key(key),
148+
Step::SelectProvider => self.handle_list_key(key),
131149
Step::InputBaseUrl | Step::InputApiKey => self.handle_input_key(key),
132150
Step::Done => Vec::new(),
133151
}
@@ -151,27 +169,36 @@ impl App {
151169
fn handle_list_key(&mut self, key: KeyEvent) -> Vec<Effect> {
152170
match key.code {
153171
KeyCode::Up | KeyCode::Char('k') => {
154-
self.list_state.select_previous();
172+
self.list_navigate(-1);
155173
Vec::new()
156174
}
157175
KeyCode::Down | KeyCode::Char('j') => {
158-
self.list_state.select_next();
176+
self.list_navigate(1);
159177
Vec::new()
160178
}
161179
KeyCode::Enter => {
162180
self.confirm_list_selection();
163-
self.step = match self.step {
164-
Step::SelectType => Step::SelectProvider,
165-
Step::SelectProvider => Step::InputBaseUrl,
166-
_ => unreachable!(),
167-
};
181+
self.step = Step::InputBaseUrl;
168182
self.advance()
169183
}
170184
KeyCode::Char('q') => vec![Effect::Exit],
171185
_ => Vec::new(),
172186
}
173187
}
174188

189+
fn list_navigate(&mut self, direction: isize) {
190+
let entries = self.flat_entries();
191+
let current = self.list_state.selected().unwrap_or(0);
192+
let mut next = current as isize + direction;
193+
while next >= 0 && (next as usize) < entries.len() {
194+
if matches!(entries[next as usize], ListEntry::Provider(..)) {
195+
self.list_state.select(Some(next as usize));
196+
return;
197+
}
198+
next += direction;
199+
}
200+
}
201+
175202
fn handle_input_key(&mut self, key: KeyEvent) -> Vec<Effect> {
176203
match key.code {
177204
KeyCode::Enter => {
@@ -228,21 +255,10 @@ impl App {
228255

229256
fn confirm_list_selection(&mut self) {
230257
let idx = self.list_state.selected().unwrap_or(0);
231-
match self.step {
232-
Step::SelectType => {
233-
self.connection_type = Some(if idx == 0 {
234-
ConnectionType::Stt
235-
} else {
236-
ConnectionType::Llm
237-
});
238-
}
239-
Step::SelectProvider => {
240-
let providers = self.provider_list();
241-
if idx < providers.len() {
242-
self.provider = Some(providers[idx]);
243-
}
244-
}
245-
_ => {}
258+
let entries = self.flat_entries();
259+
if let Some(ListEntry::Provider(ct, provider)) = entries.get(idx) {
260+
self.connection_type = Some(*ct);
261+
self.provider = Some(*provider);
246262
}
247263
}
248264

@@ -268,27 +284,28 @@ impl App {
268284
Ok(())
269285
}
270286

287+
fn first_selectable_index(&self) -> usize {
288+
self.flat_entries()
289+
.iter()
290+
.position(|e| matches!(e, ListEntry::Provider(..)))
291+
.unwrap_or(0)
292+
}
293+
271294
fn advance(&mut self) -> Vec<Effect> {
272295
loop {
273296
match self.step {
274-
Step::SelectType => {
275-
if self.connection_type.is_some() {
276-
self.step = Step::SelectProvider;
277-
continue;
278-
}
279-
self.list_state = ListState::default().with_selected(Some(0));
280-
return Vec::new();
281-
}
282297
Step::SelectProvider => {
283298
if let Some(provider) = self.provider {
284-
let ct = self.connection_type.unwrap();
285-
if provider.valid_for(ct) {
286-
self.step = Step::InputBaseUrl;
287-
continue;
299+
if let Some(ct) = self.connection_type {
300+
if provider.valid_for(ct) {
301+
self.step = Step::InputBaseUrl;
302+
continue;
303+
}
288304
}
289305
self.provider = None;
290306
}
291-
self.list_state = ListState::default().with_selected(Some(0));
307+
let first = self.first_selectable_index();
308+
self.list_state = ListState::default().with_selected(Some(first));
292309
return Vec::new();
293310
}
294311
Step::InputBaseUrl => {
@@ -322,12 +339,12 @@ impl App {
322339
return Vec::new();
323340
}
324341
Step::Done => {
325-
return vec![Effect::Save {
342+
return vec![Effect::Save(SaveData {
326343
connection_type: self.connection_type.unwrap(),
327344
provider: self.provider.unwrap(),
328345
base_url: self.base_url.clone(),
329346
api_key: self.api_key.clone(),
330-
}];
347+
})];
331348
}
332349
}
333350
}
@@ -357,13 +374,13 @@ mod tests {
357374
Some("key123".to_string()),
358375
);
359376
assert_eq!(app.step(), Step::Done);
360-
assert!(matches!(effects.as_slice(), [Effect::Save { .. }]));
377+
assert!(matches!(effects.as_slice(), [Effect::Save(_)]));
361378
}
362379

363380
#[test]
364-
fn no_args_starts_at_select_type() {
381+
fn no_args_starts_at_select_provider() {
365382
let (app, effects) = App::new(None, None, None, None);
366-
assert_eq!(app.step(), Step::SelectType);
383+
assert_eq!(app.step(), Step::SelectProvider);
367384
assert!(effects.is_empty());
368385
}
369386

@@ -399,13 +416,15 @@ mod tests {
399416
}
400417

401418
#[test]
402-
fn select_type_then_advance() {
419+
fn select_provider_from_flat_list() {
403420
let (mut app, _) = App::new(None, None, None, None);
404-
assert_eq!(app.step(), Step::SelectType);
421+
assert_eq!(app.step(), Step::SelectProvider);
422+
// First selectable entry is the first LLM provider (index 1, after the header)
423+
assert_eq!(app.list_state_mut().selected(), Some(1));
405424

406425
let effects = app.dispatch(Action::Key(KeyEvent::from(KeyCode::Enter)));
407426
assert!(effects.is_empty());
408-
assert_eq!(app.step(), Step::SelectProvider);
427+
assert_eq!(app.step(), Step::InputBaseUrl);
409428
}
410429

411430
#[test]
Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
use crate::cli::{ConnectProvider, ConnectionType};
22

3+
pub(crate) struct SaveData {
4+
pub connection_type: ConnectionType,
5+
pub provider: ConnectProvider,
6+
pub base_url: Option<String>,
7+
pub api_key: Option<String>,
8+
}
9+
310
pub(crate) enum Effect {
4-
Save {
5-
connection_type: ConnectionType,
6-
provider: ConnectProvider,
7-
base_url: Option<String>,
8-
api_key: Option<String>,
9-
},
11+
Save(SaveData),
1012
Exit,
1113
}

0 commit comments

Comments
 (0)