practicode 0.1.9 → 0.1.11
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/README.md +7 -2
- package/assets/i18n/en.json +4 -1
- package/assets/i18n/es.json +4 -1
- package/assets/i18n/ja.json +4 -1
- package/assets/i18n/ko.json +4 -1
- package/assets/i18n/zh.json +4 -1
- package/docs/ARCHITECTURE.md +20 -4
- package/package.json +1 -1
- package/src/ai.rs +103 -15
- package/src/core/bank.rs +148 -0
- package/src/core/judge.rs +205 -0
- package/src/core/language.rs +110 -0
- package/src/core/model.rs +171 -0
- package/src/core/problem_files.rs +92 -0
- package/src/core/progress.rs +97 -0
- package/src/core/render.rs +119 -0
- package/src/core/state.rs +81 -0
- package/src/core.rs +18 -983
- package/src/tui/actions.rs +377 -0
- package/src/tui/command_handlers.rs +138 -0
- package/src/tui/command_input.rs +120 -0
- package/src/tui/commands.rs +56 -0
- package/src/tui/events.rs +225 -0
- package/src/tui/problem_list.rs +163 -0
- package/src/tui/settings_panel.rs +158 -59
- package/src/tui/status.rs +109 -0
- package/src/tui/tasks.rs +303 -0
- package/src/tui/view.rs +395 -0
- package/src/tui.rs +19 -1613
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
package/README.md
CHANGED
|
@@ -111,6 +111,9 @@ Submissions are saved as you type under `submissions/<problem-id>/solution.<ext>
|
|
|
111
111
|
| `/generate-ui ko, en` | Limit generated problem text languages, or use `all` |
|
|
112
112
|
| `/provider codex` | Set AI provider and show local CLI/daemon status |
|
|
113
113
|
| `/model auto` | Use the provider default model for `/hint` and AI-backed `/next` |
|
|
114
|
+
| `/effort auto` | Use the provider default effort, or set `low`, `medium`, `high`, `xhigh`; Claude also supports `max` |
|
|
115
|
+
| `/note` | Edit problem-generation notes used by AI-backed `/next` and `/generate` |
|
|
116
|
+
| `/notes` | Show saved problem-generation notes |
|
|
114
117
|
| `/language python` | Set code language: `python`, `ts`, `java`, `rust` |
|
|
115
118
|
| `/ui en` | Set UI language: `en`, `ko`, `ja`, `zh`, `es` |
|
|
116
119
|
| `/theme dark` | Set theme: `dark` or `light` |
|
|
@@ -121,9 +124,9 @@ Older command names such as `/prev`, `/list`, `/giveup`, and `/lang` still work
|
|
|
121
124
|
|
|
122
125
|
The default UI language is English. Switch it any time with `/ui ko`, `/ui ja`, `/ui zh`, or `/ui es`.
|
|
123
126
|
|
|
124
|
-
Your user profile is saved in `.practicode/problem-state.json`. It keeps UI language, code language, theme, preferred difficulty, preferred topics, topics to avoid,
|
|
127
|
+
Your user profile is saved in `.practicode/problem-state.json`. It keeps UI language, code language, theme, preferred difficulty, preferred topics, topics to avoid, generation language scope, and AI provider/model/effort. `auto` difficulty follows gradual progression; a fixed difficulty asks local selection and AI generation to prefer that level.
|
|
125
128
|
|
|
126
|
-
Inside `/profile`, use `up/down` to move and `Space` or `Enter` to cycle common settings
|
|
129
|
+
Inside `/profile`, use `up/down` to move and `Space` or `Enter` to cycle common settings, AI provider/model/effort, or generated answer/UI languages. The notes row opens an editor for `.practicode/problem_notes.md`. Use slash commands for free-form lists such as `/topics arrays, strings`.
|
|
127
130
|
|
|
128
131
|
## Problem Flow
|
|
129
132
|
|
|
@@ -144,6 +147,7 @@ Codex is the default provider:
|
|
|
144
147
|
```text
|
|
145
148
|
/provider codex
|
|
146
149
|
/model auto
|
|
150
|
+
/effort auto
|
|
147
151
|
```
|
|
148
152
|
|
|
149
153
|
Claude Code is also supported:
|
|
@@ -151,6 +155,7 @@ Claude Code is also supported:
|
|
|
151
155
|
```text
|
|
152
156
|
/provider claude
|
|
153
157
|
/model sonnet
|
|
158
|
+
/effort high
|
|
154
159
|
```
|
|
155
160
|
|
|
156
161
|
Generated problems and submissions stay local:
|
package/assets/i18n/en.json
CHANGED
|
@@ -47,7 +47,10 @@
|
|
|
47
47
|
"cmd_model_auto": "Use provider default model",
|
|
48
48
|
"cmd_model_available": "Use an available provider model",
|
|
49
49
|
"cmd_model_custom": "Type a model name",
|
|
50
|
-
"
|
|
50
|
+
"cmd_effort": "Set AI effort",
|
|
51
|
+
"cmd_effort_auto": "Use provider default effort",
|
|
52
|
+
"cmd_effort_max": "Use Claude max effort",
|
|
53
|
+
"cmd_note": "Edit problem-generation notes",
|
|
51
54
|
"cmd_notes": "Show saved notes",
|
|
52
55
|
"cmd_lang": "Set code language",
|
|
53
56
|
"cmd_ui": "Set UI language",
|
package/assets/i18n/es.json
CHANGED
|
@@ -47,7 +47,10 @@
|
|
|
47
47
|
"cmd_model_auto": "Usar el modelo predeterminado del provider",
|
|
48
48
|
"cmd_model_available": "Usar un modelo disponible del provider",
|
|
49
49
|
"cmd_model_custom": "Escribir un nombre de modelo",
|
|
50
|
-
"
|
|
50
|
+
"cmd_effort": "Configurar AI effort",
|
|
51
|
+
"cmd_effort_auto": "Usar el effort predeterminado del provider",
|
|
52
|
+
"cmd_effort_max": "Usar Claude max effort",
|
|
53
|
+
"cmd_note": "Editar notas para generar problemas",
|
|
51
54
|
"cmd_notes": "Ver notas guardadas",
|
|
52
55
|
"cmd_lang": "Configurar lenguaje de codigo",
|
|
53
56
|
"cmd_ui": "Configurar idioma de UI",
|
package/assets/i18n/ja.json
CHANGED
|
@@ -47,7 +47,10 @@
|
|
|
47
47
|
"cmd_model_auto": "provider の既定モデルを使用",
|
|
48
48
|
"cmd_model_available": "利用可能な provider モデルを使用",
|
|
49
49
|
"cmd_model_custom": "モデル名を直接入力",
|
|
50
|
-
"
|
|
50
|
+
"cmd_effort": "AI effort を設定",
|
|
51
|
+
"cmd_effort_auto": "provider の既定 effort を使用",
|
|
52
|
+
"cmd_effort_max": "Claude max effort を使用",
|
|
53
|
+
"cmd_note": "問題生成メモを編集",
|
|
51
54
|
"cmd_notes": "保存済みメモを見る",
|
|
52
55
|
"cmd_lang": "コード言語を設定",
|
|
53
56
|
"cmd_ui": "UI 言語を設定",
|
package/assets/i18n/ko.json
CHANGED
|
@@ -47,7 +47,10 @@
|
|
|
47
47
|
"cmd_model_auto": "provider 기본 모델 사용",
|
|
48
48
|
"cmd_model_available": "사용 가능한 provider 모델 선택",
|
|
49
49
|
"cmd_model_custom": "모델 이름 직접 입력",
|
|
50
|
-
"
|
|
50
|
+
"cmd_effort": "AI effort 설정",
|
|
51
|
+
"cmd_effort_auto": "provider 기본 effort 사용",
|
|
52
|
+
"cmd_effort_max": "Claude max effort 사용",
|
|
53
|
+
"cmd_note": "문제 생성 메모 편집",
|
|
51
54
|
"cmd_notes": "저장된 메모 보기",
|
|
52
55
|
"cmd_lang": "코드 언어 설정",
|
|
53
56
|
"cmd_ui": "UI 언어 설정",
|
package/assets/i18n/zh.json
CHANGED
|
@@ -47,7 +47,10 @@
|
|
|
47
47
|
"cmd_model_auto": "使用 provider 默认模型",
|
|
48
48
|
"cmd_model_available": "使用可用的 provider 模型",
|
|
49
49
|
"cmd_model_custom": "输入模型名称",
|
|
50
|
-
"
|
|
50
|
+
"cmd_effort": "设置 AI effort",
|
|
51
|
+
"cmd_effort_auto": "使用 provider 默认 effort",
|
|
52
|
+
"cmd_effort_max": "使用 Claude max effort",
|
|
53
|
+
"cmd_note": "编辑出题备注",
|
|
51
54
|
"cmd_notes": "查看保存的备注",
|
|
52
55
|
"cmd_lang": "设置代码语言",
|
|
53
56
|
"cmd_ui": "设置 UI 语言",
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -4,9 +4,25 @@ Practicode is local-first: user data stays under `.practicode/`, `problems/`, an
|
|
|
4
4
|
|
|
5
5
|
## Source Layout
|
|
6
6
|
|
|
7
|
-
- `src/core.rs`
|
|
8
|
-
- `src/core/
|
|
9
|
-
- `src/
|
|
7
|
+
- `src/core.rs` is the public core facade. Keep new domain logic in nested `src/core/` modules.
|
|
8
|
+
- `src/core/model.rs` owns persisted/user-facing data shapes and core constants.
|
|
9
|
+
- `src/core/bank.rs` owns local problem-bank loading, saving, starter data, and bank validation.
|
|
10
|
+
- `src/core/state.rs` owns state loading, saving, and settings normalization.
|
|
11
|
+
- `src/core/language.rs` owns language/provider normalization, templates, and extension mapping.
|
|
12
|
+
- `src/core/render.rs` owns plain/markdown problem rendering.
|
|
13
|
+
- `src/core/judge.rs` owns submission file creation, runtime commands, compilation, and judging.
|
|
14
|
+
- `src/core/progress.rs` owns give-up/next/previous/pass history transitions.
|
|
15
|
+
- `src/core/problem_files.rs` owns generated problem README/index file writes.
|
|
16
|
+
- `src/core/profile.rs` owns user-profile defaults and normalization helpers.
|
|
17
|
+
- `src/tui.rs` owns the `PracticodeApp` state shell, construction, run loop, and test accessors. Keep new TUI behavior in nested `src/tui/` modules.
|
|
18
|
+
- `src/tui/actions.rs` owns user actions such as run, next, generate, language/theme/profile changes.
|
|
19
|
+
- `src/tui/command_handlers.rs` owns slash-command routing.
|
|
20
|
+
- `src/tui/command_input.rs` owns command palette input, completion, and Hangul composition.
|
|
21
|
+
- `src/tui/events.rs` owns keyboard/mouse event routing.
|
|
22
|
+
- `src/tui/tasks.rs` owns background AI/update/model tasks and output writing helpers.
|
|
23
|
+
- `src/tui/view.rs` owns Ratatui drawing, pane styling, mouse-capture toggles, and cursor placement.
|
|
24
|
+
- `src/tui/problem_list.rs` owns problem-list rendering and navigation.
|
|
25
|
+
- `src/tui/status.rs` owns status-line text, busy-game text, mode hints, and help text.
|
|
10
26
|
- `src/tui/commands.rs` owns the command palette catalog.
|
|
11
27
|
- `src/tui/editor.rs` owns the in-terminal code editor state.
|
|
12
28
|
- `src/tui/problem_view.rs` owns problem-statement rendering.
|
|
@@ -17,7 +33,7 @@ Practicode is local-first: user data stays under `.practicode/`, `problems/`, an
|
|
|
17
33
|
|
|
18
34
|
## Extension Rules
|
|
19
35
|
|
|
20
|
-
- Add domain logic under the owning module first; keep `tui.rs` as
|
|
36
|
+
- Add domain logic under the owning nested module first; keep `core.rs` and `tui.rs` as facades/shells, not catch-alls.
|
|
21
37
|
- Add user-visible commands in `src/tui/commands.rs`, then route behavior in `PracticodeApp::handle_command`.
|
|
22
38
|
- Add persisted user profile settings to `Settings`, normalize them in `normalize_settings`, and cover old-state compatibility with tests.
|
|
23
39
|
- Keep provider-specific behavior in `src/ai.rs`; TUI should ask for status or start tasks, not know provider internals.
|
package/package.json
CHANGED
package/src/ai.rs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
use crate::{
|
|
2
2
|
core::{
|
|
3
|
-
AppState, LANGUAGES, PROBLEM_NOTES_PATH, Problem, Settings,
|
|
4
|
-
ensure_submission, normalize_ai_provider, render_problem,
|
|
3
|
+
AppState, CLAUDE_AI_EFFORTS, LANGUAGES, PROBLEM_NOTES_PATH, Problem, Settings,
|
|
4
|
+
UI_LANGUAGES, ensure_submission, normalize_ai_provider, render_problem,
|
|
5
5
|
},
|
|
6
6
|
process::{run_capture, sh_quote, shell_process, unique_temp_path, which},
|
|
7
7
|
};
|
|
@@ -21,6 +21,10 @@ pub struct ModelCatalog {
|
|
|
21
21
|
pub message: Option<String>,
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
const CODEX_MODEL_FALLBACKS: &[&str] =
|
|
25
|
+
&["gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex-spark"];
|
|
26
|
+
const CLAUDE_MODEL_FALLBACKS: &[&str] = &["sonnet", "opus", "fable", "claude-fable-5"];
|
|
27
|
+
|
|
24
28
|
pub fn run_ai_prompt(root: &Path, problem: &Problem, settings: &Settings, prompt: &str) -> String {
|
|
25
29
|
let solution = match ensure_submission(root, problem, settings) {
|
|
26
30
|
Ok(path) => path,
|
|
@@ -59,7 +63,8 @@ pub fn run_ai_next(root: &Path, state: &AppState, force: bool, request: &str) ->
|
|
|
59
63
|
.current_dir(root)
|
|
60
64
|
.env("PRACTICODE_NEXT_REQUEST", request)
|
|
61
65
|
.env("PRACTICODE_AI_PROVIDER", &provider)
|
|
62
|
-
.env("PRACTICODE_AI_MODEL", &state.settings.ai_model)
|
|
66
|
+
.env("PRACTICODE_AI_MODEL", &state.settings.ai_model)
|
|
67
|
+
.env("PRACTICODE_AI_EFFORT", &state.settings.ai_effort);
|
|
63
68
|
match run_capture(&mut process, "", Duration::from_secs(900)) {
|
|
64
69
|
Ok(run) if run.code == Some(0) => {
|
|
65
70
|
let output = output_text(&run.stdout, &run.stderr);
|
|
@@ -91,7 +96,8 @@ pub fn run_ai_generate(root: &Path, state: &AppState, request: &str) -> String {
|
|
|
91
96
|
.env("PRACTICODE_NEXT_REQUEST", request)
|
|
92
97
|
.env("PRACTICODE_GENERATE_BACKGROUND", "1")
|
|
93
98
|
.env("PRACTICODE_AI_PROVIDER", &provider)
|
|
94
|
-
.env("PRACTICODE_AI_MODEL", &state.settings.ai_model)
|
|
99
|
+
.env("PRACTICODE_AI_MODEL", &state.settings.ai_model)
|
|
100
|
+
.env("PRACTICODE_AI_EFFORT", &state.settings.ai_effort);
|
|
95
101
|
match run_capture(&mut process, "", Duration::from_secs(900)) {
|
|
96
102
|
Ok(run) if run.code == Some(0) => {
|
|
97
103
|
let output = output_text(&run.stdout, &run.stderr);
|
|
@@ -151,11 +157,15 @@ pub fn available_models(provider: &str) -> ModelCatalog {
|
|
|
151
157
|
match normalize_ai_provider(provider).as_str() {
|
|
152
158
|
"codex" => codex_models(),
|
|
153
159
|
"claude" => ModelCatalog {
|
|
154
|
-
models:
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
160
|
+
models: CLAUDE_MODEL_FALLBACKS
|
|
161
|
+
.iter()
|
|
162
|
+
.map(|model| (*model).to_string())
|
|
163
|
+
.collect(),
|
|
164
|
+
message: Some(format!(
|
|
165
|
+
"Bundled Claude presets from Claude Code {} --help. Efforts: {}.",
|
|
166
|
+
claude_version().unwrap_or_else(|| "current".to_string()),
|
|
167
|
+
CLAUDE_AI_EFFORTS.join(", ")
|
|
168
|
+
)),
|
|
159
169
|
},
|
|
160
170
|
_ => ModelCatalog::default(),
|
|
161
171
|
}
|
|
@@ -231,7 +241,7 @@ fn codex_models() -> ModelCatalog {
|
|
|
231
241
|
}
|
|
232
242
|
if codex_daemon_path().is_none_or(|path| !path.exists()) {
|
|
233
243
|
return ModelCatalog {
|
|
234
|
-
models:
|
|
244
|
+
models: codex_cached_models(),
|
|
235
245
|
message: Some("Codex app-server daemon is unavailable; install the standalone Codex app to list models, or use /model <name>.".to_string()),
|
|
236
246
|
};
|
|
237
247
|
}
|
|
@@ -243,18 +253,22 @@ fn codex_models() -> ModelCatalog {
|
|
|
243
253
|
let input = r#"{"id":1,"method":"model/list","params":{"limit":25}}"#;
|
|
244
254
|
let Ok(run) = run_capture(&mut command, &format!("{input}\n"), Duration::from_secs(2)) else {
|
|
245
255
|
return ModelCatalog {
|
|
246
|
-
models:
|
|
247
|
-
message: Some(
|
|
256
|
+
models: codex_cached_models(),
|
|
257
|
+
message: Some(
|
|
258
|
+
"Could not query Codex model list; using bundled/cache model presets.".to_string(),
|
|
259
|
+
),
|
|
248
260
|
};
|
|
249
261
|
};
|
|
250
262
|
if run.code != Some(0) {
|
|
251
263
|
let detail = output_text(&run.stdout, &run.stderr);
|
|
252
264
|
return ModelCatalog {
|
|
253
|
-
models:
|
|
265
|
+
models: codex_cached_models(),
|
|
254
266
|
message: Some(if detail.is_empty() {
|
|
255
|
-
"Could not query Codex model list.".to_string()
|
|
267
|
+
"Could not query Codex model list; using bundled/cache model presets.".to_string()
|
|
256
268
|
} else {
|
|
257
|
-
format!(
|
|
269
|
+
format!(
|
|
270
|
+
"Could not query Codex model list; using bundled/cache model presets: {detail}"
|
|
271
|
+
)
|
|
258
272
|
}),
|
|
259
273
|
};
|
|
260
274
|
}
|
|
@@ -272,6 +286,51 @@ fn codex_models() -> ModelCatalog {
|
|
|
272
286
|
}
|
|
273
287
|
}
|
|
274
288
|
|
|
289
|
+
fn codex_cached_models() -> Vec<String> {
|
|
290
|
+
let mut models = env::var_os("HOME")
|
|
291
|
+
.and_then(|home| {
|
|
292
|
+
fs::read_to_string(PathBuf::from(home).join(".codex/models_cache.json")).ok()
|
|
293
|
+
})
|
|
294
|
+
.and_then(|text| serde_json::from_str::<serde_json::Value>(&text).ok())
|
|
295
|
+
.and_then(|value| {
|
|
296
|
+
value
|
|
297
|
+
.get("models")
|
|
298
|
+
.and_then(|models| models.as_array())
|
|
299
|
+
.cloned()
|
|
300
|
+
})
|
|
301
|
+
.unwrap_or_default()
|
|
302
|
+
.into_iter()
|
|
303
|
+
.filter_map(|model| {
|
|
304
|
+
model
|
|
305
|
+
.get("slug")
|
|
306
|
+
.and_then(|value| value.as_str())
|
|
307
|
+
.map(str::to_string)
|
|
308
|
+
})
|
|
309
|
+
.collect::<Vec<_>>();
|
|
310
|
+
if models.is_empty() {
|
|
311
|
+
models = CODEX_MODEL_FALLBACKS
|
|
312
|
+
.iter()
|
|
313
|
+
.map(|model| (*model).to_string())
|
|
314
|
+
.collect();
|
|
315
|
+
}
|
|
316
|
+
models
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
fn claude_version() -> Option<String> {
|
|
320
|
+
which("claude").and_then(|_| {
|
|
321
|
+
let mut command = Command::new("claude");
|
|
322
|
+
command.arg("--version");
|
|
323
|
+
run_capture(&mut command, "", Duration::from_secs(2))
|
|
324
|
+
.ok()
|
|
325
|
+
.and_then(|run| {
|
|
326
|
+
output_text(&run.stdout, &run.stderr)
|
|
327
|
+
.lines()
|
|
328
|
+
.next()
|
|
329
|
+
.map(str::to_string)
|
|
330
|
+
})
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
|
|
275
334
|
fn parse_model_list(output: &str) -> Vec<String> {
|
|
276
335
|
output
|
|
277
336
|
.lines()
|
|
@@ -309,6 +368,7 @@ fn run_codex_prompt(root: &Path, settings: &Settings, prompt: &str) -> String {
|
|
|
309
368
|
if let Some(model) = settings.model_arg() {
|
|
310
369
|
command.args(["--model", model]);
|
|
311
370
|
}
|
|
371
|
+
add_codex_effort_args(&mut command, settings);
|
|
312
372
|
command.args(["-o", &output_path.display().to_string(), prompt]);
|
|
313
373
|
let result = run_capture(&mut command, "", Duration::from_secs(600));
|
|
314
374
|
let last_message = fs::read_to_string(&output_path).unwrap_or_default();
|
|
@@ -341,6 +401,9 @@ fn run_claude_prompt(root: &Path, settings: &Settings, prompt: &str) -> String {
|
|
|
341
401
|
if let Some(model) = settings.model_arg() {
|
|
342
402
|
command.args(["--model", model]);
|
|
343
403
|
}
|
|
404
|
+
if let Some(effort) = settings.effort_arg() {
|
|
405
|
+
command.args(["--effort", effort]);
|
|
406
|
+
}
|
|
344
407
|
command.args(["-p", prompt]);
|
|
345
408
|
match run_capture(&mut command, "", Duration::from_secs(600)) {
|
|
346
409
|
Ok(run) if run.code == Some(0) => {
|
|
@@ -371,6 +434,7 @@ fn default_codex_next_command(root: &Path, settings: &Settings, request: &str) -
|
|
|
371
434
|
if let Some(model) = settings.model_arg() {
|
|
372
435
|
exec.push_str(&format!(" --model {}", sh_quote(model)));
|
|
373
436
|
}
|
|
437
|
+
push_codex_effort_arg(&mut exec, settings);
|
|
374
438
|
exec.push(' ');
|
|
375
439
|
exec.push_str(&sh_quote(&default_ai_next_prompt_with_settings(
|
|
376
440
|
settings, request,
|
|
@@ -387,6 +451,7 @@ fn default_codex_generate_command(root: &Path, settings: &Settings, request: &st
|
|
|
387
451
|
if let Some(model) = settings.model_arg() {
|
|
388
452
|
exec.push_str(&format!(" --model {}", sh_quote(model)));
|
|
389
453
|
}
|
|
454
|
+
push_codex_effort_arg(&mut exec, settings);
|
|
390
455
|
exec.push(' ');
|
|
391
456
|
exec.push_str(&sh_quote(&default_ai_generate_prompt_with_settings(
|
|
392
457
|
settings, request,
|
|
@@ -407,6 +472,9 @@ fn default_claude_next_command(root: &Path, settings: &Settings, request: &str)
|
|
|
407
472
|
if let Some(model) = settings.model_arg() {
|
|
408
473
|
claude.push_str(&format!(" --model {}", sh_quote(model)));
|
|
409
474
|
}
|
|
475
|
+
if let Some(effort) = settings.effort_arg() {
|
|
476
|
+
claude.push_str(&format!(" --effort {}", sh_quote(effort)));
|
|
477
|
+
}
|
|
410
478
|
claude.push_str(" -p ");
|
|
411
479
|
claude.push_str(&sh_quote(&default_ai_next_prompt_with_settings(
|
|
412
480
|
settings, request,
|
|
@@ -423,6 +491,9 @@ fn default_claude_generate_command(root: &Path, settings: &Settings, request: &s
|
|
|
423
491
|
if let Some(model) = settings.model_arg() {
|
|
424
492
|
claude.push_str(&format!(" --model {}", sh_quote(model)));
|
|
425
493
|
}
|
|
494
|
+
if let Some(effort) = settings.effort_arg() {
|
|
495
|
+
claude.push_str(&format!(" --effort {}", sh_quote(effort)));
|
|
496
|
+
}
|
|
426
497
|
claude.push_str(" -p ");
|
|
427
498
|
claude.push_str(&sh_quote(&default_ai_generate_prompt_with_settings(
|
|
428
499
|
settings, request,
|
|
@@ -442,6 +513,23 @@ fn output_text(stdout: &str, stderr: &str) -> String {
|
|
|
442
513
|
.join("\n")
|
|
443
514
|
}
|
|
444
515
|
|
|
516
|
+
fn add_codex_effort_args(command: &mut Command, settings: &Settings) {
|
|
517
|
+
if let Some(effort) = settings.effort_arg() {
|
|
518
|
+
let effort = if effort == "max" { "xhigh" } else { effort };
|
|
519
|
+
command.args(["-c", &format!("model_reasoning_effort=\"{effort}\"")]);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
fn push_codex_effort_arg(command: &mut String, settings: &Settings) {
|
|
524
|
+
if let Some(effort) = settings.effort_arg() {
|
|
525
|
+
let effort = if effort == "max" { "xhigh" } else { effort };
|
|
526
|
+
command.push_str(&format!(
|
|
527
|
+
" -c {}",
|
|
528
|
+
sh_quote(&format!("model_reasoning_effort=\"{effort}\""))
|
|
529
|
+
));
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
445
533
|
fn list_or_none(values: &[String]) -> String {
|
|
446
534
|
if values.is_empty() {
|
|
447
535
|
"(none)".to_string()
|
package/src/core/bank.rs
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
|
|
3
|
+
pub fn starter_problem() -> Problem {
|
|
4
|
+
Problem {
|
|
5
|
+
id: "001-hello-world".to_string(),
|
|
6
|
+
slug: "hello-world".to_string(),
|
|
7
|
+
difficulty: "easy".to_string(),
|
|
8
|
+
topics: vec!["io".to_string()],
|
|
9
|
+
title: localized_map(&[
|
|
10
|
+
("en", "Hello World"),
|
|
11
|
+
("ko", "Hello World"),
|
|
12
|
+
("ja", "Hello World"),
|
|
13
|
+
("zh", "Hello World"),
|
|
14
|
+
("es", "Hello World"),
|
|
15
|
+
]),
|
|
16
|
+
statement: localized_map(&[
|
|
17
|
+
("en", "Print exactly `Hello, World!` to stdout."),
|
|
18
|
+
("ko", "표준 출력으로 정확히 `Hello, World!`를 출력하세요."),
|
|
19
|
+
("ja", "標準出力に正確に `Hello, World!` を出力してください。"),
|
|
20
|
+
("zh", "向标准输出准确打印 `Hello, World!`。"),
|
|
21
|
+
("es", "Imprime exactamente `Hello, World!` en stdout."),
|
|
22
|
+
]),
|
|
23
|
+
input: localized_map(&[
|
|
24
|
+
("en", "No input."),
|
|
25
|
+
("ko", "입력은 없습니다."),
|
|
26
|
+
("ja", "入力はありません。"),
|
|
27
|
+
("zh", "没有输入。"),
|
|
28
|
+
("es", "No hay entrada."),
|
|
29
|
+
]),
|
|
30
|
+
output: localized_map(&[
|
|
31
|
+
("en", "One line: `Hello, World!`"),
|
|
32
|
+
("ko", "`Hello, World!` 한 줄"),
|
|
33
|
+
("ja", "1行: `Hello, World!`"),
|
|
34
|
+
("zh", "一行: `Hello, World!`"),
|
|
35
|
+
("es", "Una linea: `Hello, World!`"),
|
|
36
|
+
]),
|
|
37
|
+
examples: vec![IoCase {
|
|
38
|
+
input: String::new(),
|
|
39
|
+
output: "Hello, World!\n".to_string(),
|
|
40
|
+
}],
|
|
41
|
+
cases: vec![IoCase {
|
|
42
|
+
input: String::new(),
|
|
43
|
+
output: "Hello, World!\n".to_string(),
|
|
44
|
+
}],
|
|
45
|
+
answers: HashMap::from([
|
|
46
|
+
("python".to_string(), "print('Hello, World!')\n".to_string()),
|
|
47
|
+
(
|
|
48
|
+
"ts".to_string(),
|
|
49
|
+
"console.log('Hello, World!');\n".to_string(),
|
|
50
|
+
),
|
|
51
|
+
(
|
|
52
|
+
"java".to_string(),
|
|
53
|
+
"class Solution {\n public static void main(String[] args) {\n System.out.println(\"Hello, World!\");\n }\n}\n".to_string(),
|
|
54
|
+
),
|
|
55
|
+
(
|
|
56
|
+
"rust".to_string(),
|
|
57
|
+
"fn main() {\n println!(\"Hello, World!\");\n}\n".to_string(),
|
|
58
|
+
),
|
|
59
|
+
]),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
pub fn map2(k1: &str, v1: &str, k2: &str, v2: &str) -> HashMap<String, String> {
|
|
64
|
+
HashMap::from([
|
|
65
|
+
(k1.to_string(), v1.to_string()),
|
|
66
|
+
(k2.to_string(), v2.to_string()),
|
|
67
|
+
])
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
pub fn localized_map(entries: &[(&str, &str)]) -> HashMap<String, String> {
|
|
71
|
+
entries
|
|
72
|
+
.iter()
|
|
73
|
+
.map(|(key, value)| ((*key).to_string(), (*value).to_string()))
|
|
74
|
+
.collect()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
pub fn load_bank(root: &Path) -> Result<Vec<Problem>> {
|
|
78
|
+
let path = root.join(BANK_PATH);
|
|
79
|
+
if path.exists() {
|
|
80
|
+
let text = fs::read_to_string(&path).with_context(|| format!("read {}", path.display()))?;
|
|
81
|
+
let bank: Vec<Problem> =
|
|
82
|
+
serde_json::from_str(&text).with_context(|| format!("parse {}", path.display()))?;
|
|
83
|
+
validate_bank(&bank, &path)?;
|
|
84
|
+
Ok(bank)
|
|
85
|
+
} else {
|
|
86
|
+
Ok(vec![starter_problem()])
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
pub fn save_bank(root: &Path, bank: &[Problem]) -> Result<()> {
|
|
91
|
+
let path = root.join(BANK_PATH);
|
|
92
|
+
validate_bank(bank, &path)?;
|
|
93
|
+
if let Some(parent) = path.parent() {
|
|
94
|
+
fs::create_dir_all(parent)?;
|
|
95
|
+
}
|
|
96
|
+
fs::write(&path, serde_json::to_string_pretty(bank)? + "\n")?;
|
|
97
|
+
Ok(())
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fn validate_bank(bank: &[Problem], path: &Path) -> Result<()> {
|
|
101
|
+
if bank.is_empty() {
|
|
102
|
+
bail!("{} must contain at least one problem", path.display());
|
|
103
|
+
}
|
|
104
|
+
for problem in bank {
|
|
105
|
+
if !is_safe_name(&problem.id) {
|
|
106
|
+
bail!("{} has invalid problem id {:?}", path.display(), problem.id);
|
|
107
|
+
}
|
|
108
|
+
if !is_safe_name(&problem.slug) {
|
|
109
|
+
bail!(
|
|
110
|
+
"{} has invalid slug {:?} for {}",
|
|
111
|
+
path.display(),
|
|
112
|
+
problem.slug,
|
|
113
|
+
problem.id
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
if problem.cases.is_empty() {
|
|
117
|
+
bail!(
|
|
118
|
+
"{} problem {} has no judge cases",
|
|
119
|
+
path.display(),
|
|
120
|
+
problem.id
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
if problem.answers.is_empty() {
|
|
124
|
+
bail!(
|
|
125
|
+
"{} problem {} must contain at least one answer",
|
|
126
|
+
path.display(),
|
|
127
|
+
problem.id
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
for language in problem.answers.keys() {
|
|
131
|
+
if !LANGUAGES.contains(&language.as_str()) {
|
|
132
|
+
bail!(
|
|
133
|
+
"{} problem {} has unsupported answer language {language}",
|
|
134
|
+
path.display(),
|
|
135
|
+
problem.id,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
Ok(())
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
fn is_safe_name(value: &str) -> bool {
|
|
144
|
+
!value.is_empty()
|
|
145
|
+
&& value
|
|
146
|
+
.chars()
|
|
147
|
+
.all(|char| char.is_ascii_alphanumeric() || matches!(char, '-' | '_'))
|
|
148
|
+
}
|