practicode 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Cargo.lock CHANGED
@@ -357,7 +357,7 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
357
357
 
358
358
  [[package]]
359
359
  name = "practicode"
360
- version = "0.1.3"
360
+ version = "0.1.4"
361
361
  dependencies = [
362
362
  "anyhow",
363
363
  "crossterm 0.29.0",
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "practicode"
3
- version = "0.1.3"
3
+ version = "0.1.4"
4
4
  edition = "2024"
5
5
  description = "Local-first coding-test practice in a Rust terminal UI with optional AI help."
6
6
  readme = "README.md"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "practicode",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Local-first coding-test practice in a Rust terminal UI with optional AI help.",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/ai.rs CHANGED
@@ -15,6 +15,12 @@ use std::{
15
15
  time::Duration,
16
16
  };
17
17
 
18
+ #[derive(Clone, Debug, Default)]
19
+ pub struct ModelCatalog {
20
+ pub models: Vec<String>,
21
+ pub message: Option<String>,
22
+ }
23
+
18
24
  pub fn run_ai_prompt(root: &Path, problem: &Problem, settings: &Settings, prompt: &str) -> String {
19
25
  let solution = match ensure_submission(root, problem, settings) {
20
26
  Ok(path) => path,
@@ -102,10 +108,17 @@ pub fn provider_status(provider: &str) -> String {
102
108
  }
103
109
  }
104
110
 
105
- pub fn available_models(provider: &str) -> Vec<String> {
111
+ pub fn available_models(provider: &str) -> ModelCatalog {
106
112
  match normalize_ai_provider(provider).as_str() {
107
113
  "codex" => codex_models(),
108
- _ => Vec::new(),
114
+ "claude" => ModelCatalog {
115
+ models: Vec::new(),
116
+ message: Some(
117
+ "Claude CLI does not expose a model list; use /model <name> for a known model."
118
+ .to_string(),
119
+ ),
120
+ },
121
+ _ => ModelCatalog::default(),
109
122
  }
110
123
  }
111
124
 
@@ -139,20 +152,56 @@ pub fn read_problem_notes(root: &Path) -> Result<String> {
139
152
  }
140
153
  }
141
154
 
142
- fn codex_models() -> Vec<String> {
155
+ fn codex_models() -> ModelCatalog {
143
156
  if which("codex").is_none() {
144
- return Vec::new();
157
+ return ModelCatalog {
158
+ models: Vec::new(),
159
+ message: Some(
160
+ "Codex CLI not found; choose /provider claude or install Codex CLI.".to_string(),
161
+ ),
162
+ };
145
163
  }
164
+ if codex_daemon_path().is_none_or(|path| !path.exists()) {
165
+ return ModelCatalog {
166
+ models: Vec::new(),
167
+ message: Some("Codex app-server daemon is unavailable; install the standalone Codex app to list models, or use /model <name>.".to_string()),
168
+ };
169
+ }
170
+ let mut start = Command::new("codex");
171
+ start.args(["app-server", "daemon", "start"]);
172
+ let _ = run_capture(&mut start, "", Duration::from_secs(5));
146
173
  let mut command = Command::new("codex");
147
174
  command.args(["app-server", "proxy"]);
148
175
  let input = r#"{"id":1,"method":"model/list","params":{"limit":25}}"#;
149
176
  let Ok(run) = run_capture(&mut command, &format!("{input}\n"), Duration::from_secs(2)) else {
150
- return Vec::new();
177
+ return ModelCatalog {
178
+ models: Vec::new(),
179
+ message: Some("Could not query Codex model list.".to_string()),
180
+ };
151
181
  };
152
182
  if run.code != Some(0) {
153
- return Vec::new();
183
+ let detail = output_text(&run.stdout, &run.stderr);
184
+ return ModelCatalog {
185
+ models: Vec::new(),
186
+ message: Some(if detail.is_empty() {
187
+ "Could not query Codex model list.".to_string()
188
+ } else {
189
+ format!("Could not query Codex model list: {detail}")
190
+ }),
191
+ };
192
+ }
193
+ let models = parse_model_list(&run.stdout);
194
+ if models.is_empty() {
195
+ ModelCatalog {
196
+ models,
197
+ message: Some("Codex app-server returned no models.".to_string()),
198
+ }
199
+ } else {
200
+ ModelCatalog {
201
+ models,
202
+ message: None,
203
+ }
154
204
  }
155
- parse_model_list(&run.stdout)
156
205
  }
157
206
 
158
207
  fn parse_model_list(output: &str) -> Vec<String> {
package/src/tui.rs CHANGED
@@ -1,7 +1,7 @@
1
1
  use crate::{
2
2
  ai::{
3
- append_problem_note, available_models, provider_status, read_problem_notes, run_ai_next,
4
- run_ai_prompt,
3
+ ModelCatalog, append_problem_note, available_models, provider_status, read_problem_notes,
4
+ run_ai_next, run_ai_prompt,
5
5
  },
6
6
  core::{
7
7
  AI_PROVIDERS, AppState, HistoryItem, LANGUAGES, PROBLEM_NOTES_PATH, Problem, THEMES,
@@ -282,6 +282,7 @@ pub struct PracticodeApp {
282
282
  command_palette_cursor: usize,
283
283
  output: String,
284
284
  output_is_markdown: bool,
285
+ showing_model_status: bool,
285
286
  show_output: bool,
286
287
  focus: Focus,
287
288
  list_cursor: Option<usize>,
@@ -290,9 +291,10 @@ pub struct PracticodeApp {
290
291
  busy_frame: usize,
291
292
  task_rx: Option<Receiver<TaskResult>>,
292
293
  update_rx: Option<Receiver<UpdateCheck>>,
293
- model_rx: Option<Receiver<Vec<String>>>,
294
+ model_rx: Option<Receiver<ModelCatalog>>,
294
295
  available_models: Vec<String>,
295
296
  available_models_provider: String,
297
+ model_message: Option<String>,
296
298
  update_check: Option<UpdateCheck>,
297
299
  update_notice: Option<String>,
298
300
  should_quit: bool,
@@ -325,6 +327,7 @@ impl PracticodeApp {
325
327
  command_palette_cursor: 0,
326
328
  output: String::new(),
327
329
  output_is_markdown: false,
330
+ showing_model_status: false,
328
331
  show_output: false,
329
332
  focus: Focus::Code,
330
333
  list_cursor: None,
@@ -336,6 +339,7 @@ impl PracticodeApp {
336
339
  model_rx: None,
337
340
  available_models: Vec::new(),
338
341
  available_models_provider: String::new(),
342
+ model_message: None,
339
343
  update_check: None,
340
344
  update_notice: None,
341
345
  should_quit: false,
@@ -403,6 +407,10 @@ impl PracticodeApp {
403
407
  self.status_text()
404
408
  }
405
409
 
410
+ pub fn output_for_test(&self) -> &str {
411
+ &self.output
412
+ }
413
+
406
414
  pub fn command_suggestions_for_test(&self) -> Vec<String> {
407
415
  self.command_suggestions()
408
416
  .into_iter()
@@ -413,6 +421,13 @@ impl PracticodeApp {
413
421
  pub fn set_available_models_for_test(&mut self, models: Vec<&str>) {
414
422
  self.available_models = models.into_iter().map(str::to_string).collect();
415
423
  self.available_models_provider = self.state.settings.ai_provider.clone();
424
+ self.model_message = None;
425
+ }
426
+
427
+ pub fn set_model_message_for_test(&mut self, message: &str) {
428
+ self.available_models.clear();
429
+ self.available_models_provider = self.state.settings.ai_provider.clone();
430
+ self.model_message = Some(message.to_string());
416
431
  }
417
432
 
418
433
  pub fn pane_title_for_test(title: &str, active: bool) -> String {
@@ -910,6 +925,7 @@ impl PracticodeApp {
910
925
  self.model_rx = None;
911
926
  self.available_models.clear();
912
927
  self.available_models_provider.clear();
928
+ self.model_message = None;
913
929
  save_state(&self.root, &self.state)?;
914
930
  self.write_text_output(&format!(
915
931
  "AI provider: {}\n{}",
@@ -918,7 +934,9 @@ impl PracticodeApp {
918
934
  ));
919
935
  }
920
936
  "model" if arg.is_empty() => {
921
- self.write_text_output(&self.model_status_text());
937
+ self.start_model_check();
938
+ self.check_models();
939
+ self.write_model_status();
922
940
  }
923
941
  "model" => {
924
942
  self.state.settings.ai_model = if arg == "auto" {
@@ -927,7 +945,9 @@ impl PracticodeApp {
927
945
  arg.to_string()
928
946
  };
929
947
  save_state(&self.root, &self.state)?;
930
- self.write_text_output(&self.model_status_text());
948
+ self.start_model_check();
949
+ self.check_models();
950
+ self.write_model_status();
931
951
  }
932
952
  "hint" if arg.is_empty() => {
933
953
  self.start_ai_prompt("Give one concise hint for the current problem.")?
@@ -1195,14 +1215,21 @@ impl PracticodeApp {
1195
1215
 
1196
1216
  fn check_models(&mut self) {
1197
1217
  let models = self.model_rx.as_ref().and_then(|rx| rx.try_recv().ok());
1198
- if let Some(models) = models {
1218
+ if let Some(catalog) = models {
1199
1219
  self.model_rx = None;
1200
- self.available_models = models;
1220
+ self.available_models = catalog.models;
1221
+ self.model_message = catalog.message;
1222
+ if self.showing_model_status {
1223
+ self.output = self.model_status_text();
1224
+ self.output_is_markdown = false;
1225
+ self.show_output = true;
1226
+ }
1201
1227
  }
1202
1228
  }
1203
1229
 
1204
1230
  fn model_status_text(&self) -> String {
1205
1231
  let mut lines = vec![
1232
+ format!("AI provider: {}", self.state.settings.ai_provider),
1206
1233
  format!(
1207
1234
  "AI model: {}",
1208
1235
  if self.state.settings.ai_model == "auto" {
@@ -1213,11 +1240,15 @@ impl PracticodeApp {
1213
1240
  ),
1214
1241
  "Use /model auto to let the provider choose its default.".to_string(),
1215
1242
  ];
1216
- if self.available_models.is_empty() {
1243
+ if self.model_rx.is_some() {
1244
+ lines.push("Loading provider model list...".to_string());
1245
+ } else if self.available_models.is_empty() {
1217
1246
  lines.push(
1218
- "Provider model list is unavailable; use /model <name> for a known model."
1219
- .to_string(),
1247
+ self.model_message
1248
+ .clone()
1249
+ .unwrap_or_else(|| "Provider model list is unavailable.".to_string()),
1220
1250
  );
1251
+ lines.push("Use /model <name> for a known model.".to_string());
1221
1252
  } else {
1222
1253
  lines.push("Available models:".to_string());
1223
1254
  lines.extend(
@@ -1244,6 +1275,7 @@ impl PracticodeApp {
1244
1275
  }
1245
1276
 
1246
1277
  fn write_output(&mut self, output: &str) {
1278
+ self.showing_model_status = false;
1247
1279
  self.output = output.to_string();
1248
1280
  self.output_is_markdown = true;
1249
1281
  self.show_output = true;
@@ -1251,12 +1283,21 @@ impl PracticodeApp {
1251
1283
  }
1252
1284
 
1253
1285
  fn write_text_output(&mut self, output: &str) {
1286
+ self.showing_model_status = false;
1254
1287
  self.output = output.trim_end().to_string();
1255
1288
  self.output_is_markdown = false;
1256
1289
  self.show_output = true;
1257
1290
  self.focus = Focus::Output;
1258
1291
  }
1259
1292
 
1293
+ fn write_model_status(&mut self) {
1294
+ self.output = self.model_status_text();
1295
+ self.output_is_markdown = false;
1296
+ self.showing_model_status = true;
1297
+ self.show_output = true;
1298
+ self.focus = Focus::Output;
1299
+ }
1300
+
1260
1301
  fn show_update_notice(&mut self) {
1261
1302
  let lang = self.state.settings.ui_language.clone();
1262
1303
  if let Some(version) = &self.update_notice {