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 +1 -1
- package/Cargo.toml +1 -1
- package/package.json +1 -1
- package/src/ai.rs +56 -7
- package/src/tui.rs +51 -10
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
package/package.json
CHANGED
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) ->
|
|
111
|
+
pub fn available_models(provider: &str) -> ModelCatalog {
|
|
106
112
|
match normalize_ai_provider(provider).as_str() {
|
|
107
113
|
"codex" => codex_models(),
|
|
108
|
-
|
|
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() ->
|
|
155
|
+
fn codex_models() -> ModelCatalog {
|
|
143
156
|
if which("codex").is_none() {
|
|
144
|
-
return
|
|
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
|
|
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
|
-
|
|
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,
|
|
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<
|
|
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.
|
|
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.
|
|
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(
|
|
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.
|
|
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
|
-
|
|
1219
|
-
.
|
|
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 {
|