practicode 0.1.2 → 0.1.3
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 -4
- package/assets/i18n/en.json +4 -0
- package/assets/i18n/es.json +4 -0
- package/assets/i18n/ja.json +4 -0
- package/assets/i18n/ko.json +4 -0
- package/assets/i18n/zh.json +4 -0
- package/docs/CONTRIBUTING.md +65 -25
- package/docs/MAINTAINING.md +67 -0
- package/package.json +1 -1
- package/src/ai.rs +57 -0
- package/src/tui.rs +322 -38
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
package/README.md
CHANGED
|
@@ -69,9 +69,10 @@ Submissions are saved as you type under `submissions/<problem-id>/solution.<ext>
|
|
|
69
69
|
| `/list` | Browse problems with `up/down` or `j/k`, open with `Enter` |
|
|
70
70
|
| `/open 2` | Open by number, id, or slug |
|
|
71
71
|
| `/giveup` | Show the reference answer |
|
|
72
|
-
| `/
|
|
72
|
+
| `/hint` | Ask the selected AI for a concise hint |
|
|
73
|
+
| `/hint explain my bug` | Ask the selected AI about the current problem and submission |
|
|
73
74
|
| `/provider codex` | Set AI provider and show local CLI/daemon status |
|
|
74
|
-
| `/model auto` |
|
|
75
|
+
| `/model auto` | Use the provider default model for `/hint` and AI-backed `/next` |
|
|
75
76
|
| `/note prefer hashmap practice` | Append a standing note for future problem generation |
|
|
76
77
|
| `/notes` | Show your local next-problem notes |
|
|
77
78
|
| `/lang python` | Set code language: `python`, `ts`, `java`, `rust` |
|
|
@@ -133,9 +134,11 @@ cargo install --force practicode
|
|
|
133
134
|
|
|
134
135
|
`/run` executes your local submission as a normal process. practicode runs it from `.practicode/build/<problem-id>/run`, but this is not an OS sandbox. Only run code you trust.
|
|
135
136
|
|
|
136
|
-
##
|
|
137
|
+
## Contributing
|
|
137
138
|
|
|
138
|
-
|
|
139
|
+
External contributions use the fork and pull request flow in [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md).
|
|
140
|
+
|
|
141
|
+
Maintainer-only review and release notes live in [docs/MAINTAINING.md](docs/MAINTAINING.md).
|
|
139
142
|
|
|
140
143
|
## License
|
|
141
144
|
|
package/assets/i18n/en.json
CHANGED
|
@@ -29,9 +29,13 @@
|
|
|
29
29
|
"cmd_list": "Browse problems",
|
|
30
30
|
"cmd_open": "Open by number, id, or slug",
|
|
31
31
|
"cmd_giveup": "Show the reference answer",
|
|
32
|
+
"cmd_hint": "Ask for a hint about the current problem",
|
|
32
33
|
"cmd_ai": "Ask AI about the current problem and code",
|
|
33
34
|
"cmd_provider": "Set AI provider",
|
|
34
35
|
"cmd_model": "Set AI model",
|
|
36
|
+
"cmd_model_auto": "Use provider default model",
|
|
37
|
+
"cmd_model_available": "Use an available provider model",
|
|
38
|
+
"cmd_model_custom": "Type a model name",
|
|
35
39
|
"cmd_note": "Add a next-problem note",
|
|
36
40
|
"cmd_notes": "Show saved notes",
|
|
37
41
|
"cmd_lang": "Set code language",
|
package/assets/i18n/es.json
CHANGED
|
@@ -29,9 +29,13 @@
|
|
|
29
29
|
"cmd_list": "Abrir la lista de problemas",
|
|
30
30
|
"cmd_open": "Abrir por numero, id o slug",
|
|
31
31
|
"cmd_giveup": "Mostrar la respuesta de referencia",
|
|
32
|
+
"cmd_hint": "Pedir una pista para el problema actual",
|
|
32
33
|
"cmd_ai": "Preguntar a AI sobre el problema y codigo actuales",
|
|
33
34
|
"cmd_provider": "Configurar AI provider",
|
|
34
35
|
"cmd_model": "Configurar AI model",
|
|
36
|
+
"cmd_model_auto": "Usar el modelo predeterminado del provider",
|
|
37
|
+
"cmd_model_available": "Usar un modelo disponible del provider",
|
|
38
|
+
"cmd_model_custom": "Escribir un nombre de modelo",
|
|
35
39
|
"cmd_note": "Agregar nota para generar problemas",
|
|
36
40
|
"cmd_notes": "Ver notas guardadas",
|
|
37
41
|
"cmd_lang": "Configurar lenguaje de codigo",
|
package/assets/i18n/ja.json
CHANGED
|
@@ -29,9 +29,13 @@
|
|
|
29
29
|
"cmd_list": "問題一覧を開く",
|
|
30
30
|
"cmd_open": "番号、id、slug で問題を開く",
|
|
31
31
|
"cmd_giveup": "解答を見る",
|
|
32
|
+
"cmd_hint": "現在の問題のヒントを依頼",
|
|
32
33
|
"cmd_ai": "現在の問題とコードについて AI に質問",
|
|
33
34
|
"cmd_provider": "AI provider を設定",
|
|
34
35
|
"cmd_model": "AI model を設定",
|
|
36
|
+
"cmd_model_auto": "provider の既定モデルを使用",
|
|
37
|
+
"cmd_model_available": "利用可能な provider モデルを使用",
|
|
38
|
+
"cmd_model_custom": "モデル名を直接入力",
|
|
35
39
|
"cmd_note": "次の問題生成メモを追加",
|
|
36
40
|
"cmd_notes": "保存済みメモを見る",
|
|
37
41
|
"cmd_lang": "コード言語を設定",
|
package/assets/i18n/ko.json
CHANGED
|
@@ -29,9 +29,13 @@
|
|
|
29
29
|
"cmd_list": "문제 목록 열기",
|
|
30
30
|
"cmd_open": "번호, id, slug로 문제 열기",
|
|
31
31
|
"cmd_giveup": "정답 보기",
|
|
32
|
+
"cmd_hint": "현재 문제 힌트 요청",
|
|
32
33
|
"cmd_ai": "현재 문제와 코드에 대해 AI에게 질문",
|
|
33
34
|
"cmd_provider": "AI provider 설정",
|
|
34
35
|
"cmd_model": "AI model 설정",
|
|
36
|
+
"cmd_model_auto": "provider 기본 모델 사용",
|
|
37
|
+
"cmd_model_available": "사용 가능한 provider 모델 선택",
|
|
38
|
+
"cmd_model_custom": "모델 이름 직접 입력",
|
|
35
39
|
"cmd_note": "다음 문제 생성 메모 추가",
|
|
36
40
|
"cmd_notes": "저장된 메모 보기",
|
|
37
41
|
"cmd_lang": "코드 언어 설정",
|
package/assets/i18n/zh.json
CHANGED
|
@@ -29,9 +29,13 @@
|
|
|
29
29
|
"cmd_list": "打开题目列表",
|
|
30
30
|
"cmd_open": "按编号、id 或 slug 打开题目",
|
|
31
31
|
"cmd_giveup": "显示参考答案",
|
|
32
|
+
"cmd_hint": "请求当前题目的提示",
|
|
32
33
|
"cmd_ai": "向 AI 询问当前题目和代码",
|
|
33
34
|
"cmd_provider": "设置 AI provider",
|
|
34
35
|
"cmd_model": "设置 AI model",
|
|
36
|
+
"cmd_model_auto": "使用 provider 默认模型",
|
|
37
|
+
"cmd_model_available": "使用可用的 provider 模型",
|
|
38
|
+
"cmd_model_custom": "输入模型名称",
|
|
35
39
|
"cmd_note": "添加下次出题备注",
|
|
36
40
|
"cmd_notes": "查看保存的备注",
|
|
37
41
|
"cmd_lang": "设置代码语言",
|
package/docs/CONTRIBUTING.md
CHANGED
|
@@ -1,14 +1,54 @@
|
|
|
1
1
|
# Contributing
|
|
2
2
|
|
|
3
|
-
This
|
|
3
|
+
Thanks for helping improve practicode. This guide is for contributors opening issues or pull requests.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Maintainer-only review and release steps live in [MAINTAINING.md](MAINTAINING.md).
|
|
6
|
+
|
|
7
|
+
## Before You Start
|
|
8
|
+
|
|
9
|
+
- Search existing issues and pull requests first.
|
|
10
|
+
- Small bug fixes, docs fixes, tests, and localization updates can go straight to a pull request.
|
|
11
|
+
- For larger UI, AI-generation, storage, or packaging changes, open an issue first so the scope is clear.
|
|
12
|
+
- Do not commit local practice data from `.practicode/`, `problems/`, or `submissions/`.
|
|
13
|
+
- Do not include secrets, tokens, private prompts, or generated answer keys in docs or examples.
|
|
14
|
+
|
|
15
|
+
## Fork And Pull Request Flow
|
|
16
|
+
|
|
17
|
+
1. Fork `baba9811/practicode` on GitHub.
|
|
18
|
+
2. Clone your fork and add the original repo as `upstream`.
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
git clone https://github.com/<your-user>/practicode.git
|
|
22
|
+
cd practicode
|
|
23
|
+
git remote add upstream https://github.com/baba9811/practicode.git
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
3. Create a focused branch from the latest `main`.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
git fetch upstream
|
|
30
|
+
git checkout -b fix-short-name upstream/main
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
4. Make one focused change.
|
|
34
|
+
5. Run the smallest checks that cover your change.
|
|
35
|
+
6. Push your branch and open a pull request into `baba9811/practicode:main`.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
git push origin fix-short-name
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
When opening the pull request, include what changed, how you checked it, and screenshots for visible TUI changes.
|
|
42
|
+
|
|
43
|
+
## Local Setup
|
|
44
|
+
|
|
45
|
+
Prerequisites:
|
|
6
46
|
|
|
7
47
|
- Rust stable with Cargo, rustfmt, and clippy.
|
|
8
48
|
- Node.js 18+ for the npm wrapper and package checks.
|
|
9
49
|
- Optional local runtimes for judging: Python, Node.js, JDK, and Rust.
|
|
10
50
|
|
|
11
|
-
|
|
51
|
+
Common commands:
|
|
12
52
|
|
|
13
53
|
```bash
|
|
14
54
|
cargo run --
|
|
@@ -23,7 +63,7 @@ Full local check:
|
|
|
23
63
|
make test
|
|
24
64
|
```
|
|
25
65
|
|
|
26
|
-
|
|
66
|
+
## Project Map
|
|
27
67
|
|
|
28
68
|
| Path | Role |
|
|
29
69
|
| --- | --- |
|
|
@@ -35,6 +75,16 @@ The source is split by boring responsibility:
|
|
|
35
75
|
| `src/process.rs` | Process execution helpers |
|
|
36
76
|
| `tests/` | Integration tests split by module |
|
|
37
77
|
|
|
78
|
+
## Change Guidelines
|
|
79
|
+
|
|
80
|
+
- Keep pull requests small and reviewable.
|
|
81
|
+
- Reuse existing helpers and patterns before adding new code.
|
|
82
|
+
- Prefer the Rust standard library. Add crates only when they remove real complexity.
|
|
83
|
+
- Put UI strings in [assets/i18n](../assets/i18n), not inline in Rust code.
|
|
84
|
+
- Keep English localization complete first; other locales can be partial because the runtime falls back per key to English.
|
|
85
|
+
- Keep the root [README](../README.md) focused on users.
|
|
86
|
+
- Use relative links for repo-local docs and assets.
|
|
87
|
+
|
|
38
88
|
## Problem Authoring
|
|
39
89
|
|
|
40
90
|
AI generation reads [problem-authoring-notes.md](problem-authoring-notes.md) every time it creates a problem.
|
|
@@ -49,32 +99,22 @@ Local generated data stays ignored by git:
|
|
|
49
99
|
| `problems/` | Generated problem markdown/index files |
|
|
50
100
|
| `submissions/` | Local answer files |
|
|
51
101
|
|
|
52
|
-
##
|
|
53
|
-
|
|
54
|
-
`main` runs CI only. Releases are tag-based and publish to crates.io and npm through GitHub Actions.
|
|
55
|
-
|
|
56
|
-
```bash
|
|
57
|
-
make release VERSION=0.1.1
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
The release script checks versions, runs tests, creates the version commit and tag, and pushes `main` plus the tag. Do not print or commit tokens; GitHub Actions uses repository secrets.
|
|
61
|
-
|
|
62
|
-
## Documentation
|
|
63
|
-
|
|
64
|
-
Keep the root [README](../README.md) focused on users. Put contributor workflow, implementation notes, release notes, and design references here or in nearby `docs/` files.
|
|
65
|
-
|
|
66
|
-
Use relative links for repo-local docs and assets. The terminal screenshot is stored at [assets/practicode-terminal.svg](../assets/practicode-terminal.svg).
|
|
67
|
-
|
|
68
|
-
## Localization
|
|
102
|
+
## Pull Request Checklist
|
|
69
103
|
|
|
70
|
-
|
|
104
|
+
- The change is focused and explained.
|
|
105
|
+
- Relevant checks were run, or the PR says why they were not.
|
|
106
|
+
- Visible TUI changes include a screenshot or short terminal description.
|
|
107
|
+
- User-facing text is in `assets/i18n/*.json`.
|
|
108
|
+
- Local generated data, secrets, tokens, and answer keys are not committed.
|
|
71
109
|
|
|
72
|
-
##
|
|
110
|
+
## References
|
|
73
111
|
|
|
112
|
+
- GitHub contributing guide: https://docs.github.com/en/get-started/exploring-projects-on-github/contributing-to-open-source
|
|
113
|
+
- GitHub contributing guidelines: https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/setting-guidelines-for-repository-contributors
|
|
114
|
+
- GitHub pull requests: https://docs.github.com/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests
|
|
115
|
+
- Open Source Guides community notes: https://opensource.guide/building-community/
|
|
74
116
|
- WAI-ARIA combobox keyboard interaction: https://www.w3.org/WAI/ARIA/apg/patterns/combobox/
|
|
75
117
|
- Command Line Interface Guidelines: https://clig.dev/
|
|
76
|
-
- GitHub README guidance: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-readmes
|
|
77
|
-
- GitHub relative links and images: https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#relative-links
|
|
78
118
|
- Ratatui terminal UI library: https://ratatui.rs/
|
|
79
119
|
- Crossterm terminal backend/events: https://github.com/crossterm-rs/crossterm
|
|
80
120
|
- Kattis problem package format: https://www.kattis.com/problem-package-format/
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Maintaining
|
|
2
|
+
|
|
3
|
+
This guide is for maintainers with commit, tag, or publishing responsibility.
|
|
4
|
+
|
|
5
|
+
Contributor-facing workflow lives in [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
6
|
+
|
|
7
|
+
## Triage
|
|
8
|
+
|
|
9
|
+
- Keep issues actionable: expected behavior, actual behavior, reproduction steps, OS, terminal, and install method.
|
|
10
|
+
- Use small scopes. Ask broad proposals to become one issue or pull request per behavior change.
|
|
11
|
+
- Label approachable work as `good first issue` or `help wanted` when the fix is clear.
|
|
12
|
+
- Close duplicates with a link to the canonical issue.
|
|
13
|
+
|
|
14
|
+
## Pull Request Review
|
|
15
|
+
|
|
16
|
+
- Review the diff file by file.
|
|
17
|
+
- Check correctness, local-first behavior, terminal UX, docs, and tests before style.
|
|
18
|
+
- Prefer focused pull requests. Ask contributors to split unrelated changes.
|
|
19
|
+
- Require checks or a clear reason checks were not run.
|
|
20
|
+
- For visible TUI changes, ask for a screenshot or a short terminal description.
|
|
21
|
+
- Merge only after CI passes and the branch is up to date enough to avoid obvious conflicts.
|
|
22
|
+
|
|
23
|
+
## Release
|
|
24
|
+
|
|
25
|
+
`main` runs CI only. Releases are tag-based and publish to crates.io and npm through GitHub Actions.
|
|
26
|
+
|
|
27
|
+
Preflight:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
git checkout main
|
|
31
|
+
git pull --ff-only origin main
|
|
32
|
+
make test
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Release:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
make release VERSION=0.1.3
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The release script checks versions, runs tests, creates the version commit and tag, and pushes `main` plus the tag.
|
|
42
|
+
|
|
43
|
+
Verify publication:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
gh run list --limit 5
|
|
47
|
+
npm view practicode version
|
|
48
|
+
cargo search practicode --limit 1
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Do not print or commit tokens. Local `.env` and `.npmrc` are ignored; GitHub Actions uses `NPM_TOKEN` and `CRATES_TOKEN` repository secrets.
|
|
52
|
+
|
|
53
|
+
## Documentation Ownership
|
|
54
|
+
|
|
55
|
+
| File | Audience |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| [../README.md](../README.md) | Users installing and running practicode |
|
|
58
|
+
| [CONTRIBUTING.md](CONTRIBUTING.md) | External contributors opening issues or pull requests |
|
|
59
|
+
| [MAINTAINING.md](MAINTAINING.md) | Maintainers reviewing, triaging, and releasing |
|
|
60
|
+
| [problem-authoring-notes.md](problem-authoring-notes.md) | AI/local problem generation rules |
|
|
61
|
+
|
|
62
|
+
## Maintainer References
|
|
63
|
+
|
|
64
|
+
- GitHub reviewing pull requests: https://docs.github.com/articles/reviewing-proposed-changes-in-a-pull-request
|
|
65
|
+
- GitHub helping reviewers: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/getting-started/helping-others-review-your-changes
|
|
66
|
+
- GitHub healthy contributions: https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions
|
|
67
|
+
- Git contributing through forks: https://git-scm.com/book/en/v2/GitHub-Contributing-to-a-Project
|
package/package.json
CHANGED
package/src/ai.rs
CHANGED
|
@@ -102,6 +102,13 @@ pub fn provider_status(provider: &str) -> String {
|
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
pub fn available_models(provider: &str) -> Vec<String> {
|
|
106
|
+
match normalize_ai_provider(provider).as_str() {
|
|
107
|
+
"codex" => codex_models(),
|
|
108
|
+
_ => Vec::new(),
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
105
112
|
pub fn default_ai_next_prompt(request: &str) -> String {
|
|
106
113
|
format!(
|
|
107
114
|
"Read AGENTS.md, docs/problem-authoring-notes.md if present, .practicode/problem_notes.md if present, problems/INDEX.md if present, .practicode/problem_bank.json if present, and .practicode/problem-state.json. Create exactly one new non-duplicate coding practice problem. The built-in 001-hello-world already exists, so do not duplicate it. User request: {}. Make the smallest valid edits: update .practicode/problem_bank.json, one problem directory, problems/INDEX.md, and .practicode/problem-state.json. Do not include the answer in the problem statement.",
|
|
@@ -132,6 +139,44 @@ pub fn read_problem_notes(root: &Path) -> Result<String> {
|
|
|
132
139
|
}
|
|
133
140
|
}
|
|
134
141
|
|
|
142
|
+
fn codex_models() -> Vec<String> {
|
|
143
|
+
if which("codex").is_none() {
|
|
144
|
+
return Vec::new();
|
|
145
|
+
}
|
|
146
|
+
let mut command = Command::new("codex");
|
|
147
|
+
command.args(["app-server", "proxy"]);
|
|
148
|
+
let input = r#"{"id":1,"method":"model/list","params":{"limit":25}}"#;
|
|
149
|
+
let Ok(run) = run_capture(&mut command, &format!("{input}\n"), Duration::from_secs(2)) else {
|
|
150
|
+
return Vec::new();
|
|
151
|
+
};
|
|
152
|
+
if run.code != Some(0) {
|
|
153
|
+
return Vec::new();
|
|
154
|
+
}
|
|
155
|
+
parse_model_list(&run.stdout)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fn parse_model_list(output: &str) -> Vec<String> {
|
|
159
|
+
output
|
|
160
|
+
.lines()
|
|
161
|
+
.filter_map(|line| serde_json::from_str::<serde_json::Value>(line).ok())
|
|
162
|
+
.flat_map(|value| {
|
|
163
|
+
value
|
|
164
|
+
.pointer("/result/data")
|
|
165
|
+
.or_else(|| value.get("data"))
|
|
166
|
+
.and_then(|data| data.as_array())
|
|
167
|
+
.cloned()
|
|
168
|
+
.unwrap_or_default()
|
|
169
|
+
})
|
|
170
|
+
.filter_map(|model| {
|
|
171
|
+
model
|
|
172
|
+
.get("model")
|
|
173
|
+
.or_else(|| model.get("id"))
|
|
174
|
+
.and_then(|value| value.as_str())
|
|
175
|
+
.map(str::to_string)
|
|
176
|
+
})
|
|
177
|
+
.collect()
|
|
178
|
+
}
|
|
179
|
+
|
|
135
180
|
fn run_codex_prompt(root: &Path, settings: &Settings, prompt: &str) -> String {
|
|
136
181
|
let output_path = unique_temp_path("practicode-last-message", "txt");
|
|
137
182
|
let mut command = Command::new("codex");
|
|
@@ -243,3 +288,15 @@ fn output_text(stdout: &str, stderr: &str) -> String {
|
|
|
243
288
|
.collect::<Vec<_>>()
|
|
244
289
|
.join("\n")
|
|
245
290
|
}
|
|
291
|
+
|
|
292
|
+
#[cfg(test)]
|
|
293
|
+
mod tests {
|
|
294
|
+
use super::parse_model_list;
|
|
295
|
+
|
|
296
|
+
#[test]
|
|
297
|
+
fn parses_codex_model_list_response() {
|
|
298
|
+
let output =
|
|
299
|
+
r#"{"id":1,"result":{"data":[{"model":"gpt-test","displayName":"GPT Test"}]}}"#;
|
|
300
|
+
assert_eq!(parse_model_list(output), vec!["gpt-test"]);
|
|
301
|
+
}
|
|
302
|
+
}
|
package/src/tui.rs
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
use crate::{
|
|
2
|
-
ai::{
|
|
2
|
+
ai::{
|
|
3
|
+
append_problem_note, available_models, provider_status, read_problem_notes, run_ai_next,
|
|
4
|
+
run_ai_prompt,
|
|
5
|
+
},
|
|
3
6
|
core::{
|
|
4
7
|
AI_PROVIDERS, AppState, HistoryItem, LANGUAGES, PROBLEM_NOTES_PATH, Problem, THEMES,
|
|
5
8
|
UI_LANGUAGES, ensure_problem_files, ensure_submission, ext_for, give_up, judge, load_bank,
|
|
6
9
|
load_state, localized, next_problem, normalize_ai_provider, normalize_language,
|
|
7
10
|
normalize_next_source, normalize_ui_language, previous_problem, problem_by_id, record_pass,
|
|
8
|
-
|
|
11
|
+
save_state, template_for, ui_text,
|
|
9
12
|
},
|
|
10
13
|
text::{
|
|
11
14
|
byte_index, char_len, compose_hangul_jamo, display_width, prefix, render_markdown_plain,
|
|
@@ -18,6 +21,7 @@ use ratatui::{
|
|
|
18
21
|
DefaultTerminal, Frame,
|
|
19
22
|
layout::{Constraint, Direction, Layout, Position, Rect},
|
|
20
23
|
style::{Color, Modifier, Style},
|
|
24
|
+
text::{Line, Span, Text},
|
|
21
25
|
widgets::{Block, Borders, Clear, Paragraph, Wrap},
|
|
22
26
|
};
|
|
23
27
|
use std::{
|
|
@@ -38,6 +42,14 @@ struct CommandHint {
|
|
|
38
42
|
help: bool,
|
|
39
43
|
}
|
|
40
44
|
|
|
45
|
+
#[derive(Clone)]
|
|
46
|
+
struct CommandChoice {
|
|
47
|
+
insert: String,
|
|
48
|
+
display: String,
|
|
49
|
+
desc_key: &'static str,
|
|
50
|
+
keep_open: bool,
|
|
51
|
+
}
|
|
52
|
+
|
|
41
53
|
const COMMAND_HINTS: &[CommandHint] = &[
|
|
42
54
|
CommandHint {
|
|
43
55
|
insert: "run",
|
|
@@ -89,9 +101,9 @@ const COMMAND_HINTS: &[CommandHint] = &[
|
|
|
89
101
|
help: true,
|
|
90
102
|
},
|
|
91
103
|
CommandHint {
|
|
92
|
-
insert: "
|
|
93
|
-
display: "/
|
|
94
|
-
desc_key: "
|
|
104
|
+
insert: "hint ",
|
|
105
|
+
display: "/hint <request>",
|
|
106
|
+
desc_key: "cmd_hint",
|
|
95
107
|
keep_open: true,
|
|
96
108
|
help: true,
|
|
97
109
|
},
|
|
@@ -112,14 +124,14 @@ const COMMAND_HINTS: &[CommandHint] = &[
|
|
|
112
124
|
CommandHint {
|
|
113
125
|
insert: "model auto",
|
|
114
126
|
display: "/model auto",
|
|
115
|
-
desc_key: "
|
|
127
|
+
desc_key: "cmd_model_auto",
|
|
116
128
|
keep_open: false,
|
|
117
129
|
help: true,
|
|
118
130
|
},
|
|
119
131
|
CommandHint {
|
|
120
132
|
insert: "model ",
|
|
121
133
|
display: "/model <name>",
|
|
122
|
-
desc_key: "
|
|
134
|
+
desc_key: "cmd_model_custom",
|
|
123
135
|
keep_open: true,
|
|
124
136
|
help: false,
|
|
125
137
|
},
|
|
@@ -278,6 +290,9 @@ pub struct PracticodeApp {
|
|
|
278
290
|
busy_frame: usize,
|
|
279
291
|
task_rx: Option<Receiver<TaskResult>>,
|
|
280
292
|
update_rx: Option<Receiver<UpdateCheck>>,
|
|
293
|
+
model_rx: Option<Receiver<Vec<String>>>,
|
|
294
|
+
available_models: Vec<String>,
|
|
295
|
+
available_models_provider: String,
|
|
281
296
|
update_check: Option<UpdateCheck>,
|
|
282
297
|
update_notice: Option<String>,
|
|
283
298
|
should_quit: bool,
|
|
@@ -318,6 +333,9 @@ impl PracticodeApp {
|
|
|
318
333
|
busy_frame: 0,
|
|
319
334
|
task_rx: None,
|
|
320
335
|
update_rx: None,
|
|
336
|
+
model_rx: None,
|
|
337
|
+
available_models: Vec::new(),
|
|
338
|
+
available_models_provider: String::new(),
|
|
321
339
|
update_check: None,
|
|
322
340
|
update_notice: None,
|
|
323
341
|
should_quit: false,
|
|
@@ -328,10 +346,13 @@ impl PracticodeApp {
|
|
|
328
346
|
|
|
329
347
|
pub fn run(&mut self, terminal: &mut DefaultTerminal) -> Result<()> {
|
|
330
348
|
self.start_update_check();
|
|
349
|
+
self.start_model_check();
|
|
331
350
|
while !self.should_quit {
|
|
332
351
|
terminal.draw(|frame| self.draw(frame))?;
|
|
333
352
|
self.check_task();
|
|
334
353
|
self.check_update();
|
|
354
|
+
self.start_model_check();
|
|
355
|
+
self.check_models();
|
|
335
356
|
if event::poll(Duration::from_millis(100))?
|
|
336
357
|
&& let Event::Key(key) = event::read()?
|
|
337
358
|
&& key.kind != KeyEventKind::Release
|
|
@@ -382,6 +403,22 @@ impl PracticodeApp {
|
|
|
382
403
|
self.status_text()
|
|
383
404
|
}
|
|
384
405
|
|
|
406
|
+
pub fn command_suggestions_for_test(&self) -> Vec<String> {
|
|
407
|
+
self.command_suggestions()
|
|
408
|
+
.into_iter()
|
|
409
|
+
.map(|choice| choice.display)
|
|
410
|
+
.collect()
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
pub fn set_available_models_for_test(&mut self, models: Vec<&str>) {
|
|
414
|
+
self.available_models = models.into_iter().map(str::to_string).collect();
|
|
415
|
+
self.available_models_provider = self.state.settings.ai_provider.clone();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
pub fn pane_title_for_test(title: &str, active: bool) -> String {
|
|
419
|
+
Self::pane_title(title, active)
|
|
420
|
+
}
|
|
421
|
+
|
|
385
422
|
fn draw(&mut self, frame: &mut Frame) {
|
|
386
423
|
let size = frame.area();
|
|
387
424
|
let vertical = Layout::default()
|
|
@@ -397,15 +434,13 @@ impl PracticodeApp {
|
|
|
397
434
|
.constraints([Constraint::Percentage(58), Constraint::Percentage(42)])
|
|
398
435
|
.split(vertical[0]);
|
|
399
436
|
|
|
400
|
-
let problem = Paragraph::new(
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
))
|
|
408
|
-
.wrap(Wrap { trim: false });
|
|
437
|
+
let problem = Paragraph::new(self.problem_text())
|
|
438
|
+
.block(Self::block(
|
|
439
|
+
ui_text(&self.state.settings.ui_language, "problem"),
|
|
440
|
+
self.state.settings.theme == "light",
|
|
441
|
+
false,
|
|
442
|
+
))
|
|
443
|
+
.wrap(Wrap { trim: false });
|
|
409
444
|
frame.render_widget(problem, body[0]);
|
|
410
445
|
|
|
411
446
|
if self.show_output {
|
|
@@ -420,6 +455,7 @@ impl PracticodeApp {
|
|
|
420
455
|
.block(Self::block(
|
|
421
456
|
ui_text(&self.state.settings.ui_language, "output"),
|
|
422
457
|
self.state.settings.theme == "light",
|
|
458
|
+
self.focus != Focus::Command,
|
|
423
459
|
))
|
|
424
460
|
.wrap(Wrap { trim: false });
|
|
425
461
|
frame.render_widget(output, body[1]);
|
|
@@ -428,8 +464,11 @@ impl PracticodeApp {
|
|
|
428
464
|
.editor
|
|
429
465
|
.visible_text(body[1].height.saturating_sub(2) as usize);
|
|
430
466
|
let title = format!("solution.{}", ext_for(&self.state.settings.language));
|
|
431
|
-
let code = Paragraph::new(code)
|
|
432
|
-
|
|
467
|
+
let code = Paragraph::new(code).block(Self::block(
|
|
468
|
+
&title,
|
|
469
|
+
self.state.settings.theme == "light",
|
|
470
|
+
self.focus == Focus::Code,
|
|
471
|
+
));
|
|
433
472
|
frame.render_widget(code, body[1]);
|
|
434
473
|
}
|
|
435
474
|
|
|
@@ -456,6 +495,7 @@ impl PracticodeApp {
|
|
|
456
495
|
.block(Self::block(
|
|
457
496
|
ui_text(&self.state.settings.ui_language, "command"),
|
|
458
497
|
self.state.settings.theme == "light",
|
|
498
|
+
self.focus == Focus::Command,
|
|
459
499
|
))
|
|
460
500
|
.wrap(Wrap { trim: false });
|
|
461
501
|
frame.render_widget(command, vertical[2]);
|
|
@@ -463,12 +503,147 @@ impl PracticodeApp {
|
|
|
463
503
|
self.set_terminal_cursor(frame, body[1], vertical[2]);
|
|
464
504
|
}
|
|
465
505
|
|
|
506
|
+
fn problem_text(&self) -> Text<'static> {
|
|
507
|
+
let lang = normalize_ui_language(&self.state.settings.ui_language);
|
|
508
|
+
let light = self.state.settings.theme == "light";
|
|
509
|
+
let title_style = if light {
|
|
510
|
+
Style::default()
|
|
511
|
+
.fg(Color::Blue)
|
|
512
|
+
.add_modifier(Modifier::BOLD)
|
|
513
|
+
} else {
|
|
514
|
+
Style::default()
|
|
515
|
+
.fg(Color::Yellow)
|
|
516
|
+
.add_modifier(Modifier::BOLD)
|
|
517
|
+
};
|
|
518
|
+
let section_style = if light {
|
|
519
|
+
Style::default()
|
|
520
|
+
.fg(Color::Magenta)
|
|
521
|
+
.add_modifier(Modifier::BOLD)
|
|
522
|
+
} else {
|
|
523
|
+
Style::default()
|
|
524
|
+
.fg(Color::Cyan)
|
|
525
|
+
.add_modifier(Modifier::BOLD)
|
|
526
|
+
};
|
|
527
|
+
let body_style = if light {
|
|
528
|
+
Style::default().fg(Color::Black)
|
|
529
|
+
} else {
|
|
530
|
+
Style::default().fg(Color::Rgb(229, 231, 235))
|
|
531
|
+
};
|
|
532
|
+
let meta_style = if light {
|
|
533
|
+
Style::default().fg(Color::Rgb(75, 85, 99))
|
|
534
|
+
} else {
|
|
535
|
+
Style::default().fg(Color::Rgb(156, 163, 175))
|
|
536
|
+
};
|
|
537
|
+
let code_style = if light {
|
|
538
|
+
Style::default()
|
|
539
|
+
.fg(Color::Black)
|
|
540
|
+
.bg(Color::Rgb(229, 231, 235))
|
|
541
|
+
} else {
|
|
542
|
+
Style::default()
|
|
543
|
+
.fg(Color::Rgb(243, 244, 246))
|
|
544
|
+
.bg(Color::Rgb(31, 41, 55))
|
|
545
|
+
};
|
|
546
|
+
let number = self
|
|
547
|
+
.problem
|
|
548
|
+
.id
|
|
549
|
+
.split_once('-')
|
|
550
|
+
.map(|(number, _)| number)
|
|
551
|
+
.unwrap_or(&self.problem.id);
|
|
552
|
+
let mut lines = vec![
|
|
553
|
+
Line::from(Span::styled(
|
|
554
|
+
format!("{number}. {}", localized(&self.problem.title, &lang)),
|
|
555
|
+
title_style,
|
|
556
|
+
)),
|
|
557
|
+
Line::from(Span::styled(
|
|
558
|
+
format!(
|
|
559
|
+
"{}: {} {}: {}",
|
|
560
|
+
ui_text(&lang, "difficulty"),
|
|
561
|
+
self.problem.difficulty,
|
|
562
|
+
ui_text(&lang, "topics"),
|
|
563
|
+
self.problem.topics.join(", ")
|
|
564
|
+
),
|
|
565
|
+
meta_style,
|
|
566
|
+
)),
|
|
567
|
+
];
|
|
568
|
+
lines.push(Line::default());
|
|
569
|
+
for line in localized(&self.problem.statement, &lang).trim_end().lines() {
|
|
570
|
+
lines.push(Line::from(Span::styled(line.to_string(), body_style)));
|
|
571
|
+
}
|
|
572
|
+
Self::push_problem_section(
|
|
573
|
+
&mut lines,
|
|
574
|
+
ui_text(&lang, "input"),
|
|
575
|
+
&localized(&self.problem.input, &lang),
|
|
576
|
+
section_style,
|
|
577
|
+
body_style,
|
|
578
|
+
);
|
|
579
|
+
Self::push_problem_section(
|
|
580
|
+
&mut lines,
|
|
581
|
+
ui_text(&lang, "output"),
|
|
582
|
+
&localized(&self.problem.output, &lang),
|
|
583
|
+
section_style,
|
|
584
|
+
body_style,
|
|
585
|
+
);
|
|
586
|
+
lines.push(Line::default());
|
|
587
|
+
lines.push(Line::from(Span::styled(
|
|
588
|
+
ui_text(&lang, "examples").to_string(),
|
|
589
|
+
section_style,
|
|
590
|
+
)));
|
|
591
|
+
for (index, case) in self.problem.examples.iter().enumerate() {
|
|
592
|
+
lines.push(Line::from(Span::styled(
|
|
593
|
+
format!(" {} {}", ui_text(&lang, "example"), index + 1),
|
|
594
|
+
meta_style.add_modifier(Modifier::BOLD),
|
|
595
|
+
)));
|
|
596
|
+
lines.push(Line::from(Span::styled(
|
|
597
|
+
format!(" {}", ui_text(&lang, "input")),
|
|
598
|
+
meta_style,
|
|
599
|
+
)));
|
|
600
|
+
Self::push_code_lines(&mut lines, &case.input, code_style);
|
|
601
|
+
lines.push(Line::from(Span::styled(
|
|
602
|
+
format!(" {}", ui_text(&lang, "output")),
|
|
603
|
+
meta_style,
|
|
604
|
+
)));
|
|
605
|
+
Self::push_code_lines(&mut lines, &case.output, code_style);
|
|
606
|
+
}
|
|
607
|
+
Text::from(lines)
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
fn push_problem_section(
|
|
611
|
+
lines: &mut Vec<Line<'static>>,
|
|
612
|
+
title: &str,
|
|
613
|
+
body: &str,
|
|
614
|
+
section_style: Style,
|
|
615
|
+
body_style: Style,
|
|
616
|
+
) {
|
|
617
|
+
lines.push(Line::default());
|
|
618
|
+
lines.push(Line::from(Span::styled(title.to_string(), section_style)));
|
|
619
|
+
for line in body.trim_end().lines() {
|
|
620
|
+
lines.push(Line::from(Span::styled(format!(" {line}"), body_style)));
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
fn push_code_lines(lines: &mut Vec<Line<'static>>, body: &str, code_style: Style) {
|
|
625
|
+
let body = body.trim_end();
|
|
626
|
+
if body.is_empty() {
|
|
627
|
+
lines.push(Line::from(vec![
|
|
628
|
+
Span::raw(" "),
|
|
629
|
+
Span::styled("<empty>".to_string(), code_style),
|
|
630
|
+
]));
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
for line in body.lines() {
|
|
634
|
+
lines.push(Line::from(vec![
|
|
635
|
+
Span::raw(" "),
|
|
636
|
+
Span::styled(line.to_string(), code_style),
|
|
637
|
+
]));
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
466
641
|
fn draw_command_palette(&self, frame: &mut Frame, command_area: Rect) {
|
|
467
642
|
let suggestions = self.command_suggestions();
|
|
468
643
|
if suggestions.is_empty() || command_area.y < 3 {
|
|
469
644
|
return;
|
|
470
645
|
}
|
|
471
|
-
let height = ((suggestions.len() + 3) as u16).min(
|
|
646
|
+
let height = ((suggestions.len() + 3) as u16).min(14).min(command_area.y);
|
|
472
647
|
let area = Rect::new(
|
|
473
648
|
command_area.x,
|
|
474
649
|
command_area.y - height,
|
|
@@ -476,10 +651,13 @@ impl PracticodeApp {
|
|
|
476
651
|
height,
|
|
477
652
|
);
|
|
478
653
|
let selected = self.command_palette_cursor.min(suggestions.len() - 1);
|
|
654
|
+
let visible = height.saturating_sub(2) as usize;
|
|
655
|
+
let start = selected.saturating_sub(visible.saturating_sub(1));
|
|
479
656
|
let mut lines = suggestions
|
|
480
657
|
.iter()
|
|
481
658
|
.enumerate()
|
|
482
|
-
.
|
|
659
|
+
.skip(start)
|
|
660
|
+
.take(visible)
|
|
483
661
|
.map(|(index, hint)| {
|
|
484
662
|
let marker = if index == selected { ">" } else { " " };
|
|
485
663
|
format!(
|
|
@@ -495,20 +673,40 @@ impl PracticodeApp {
|
|
|
495
673
|
Paragraph::new(lines.join("\n")).block(Self::block(
|
|
496
674
|
ui_text(&self.state.settings.ui_language, "commands"),
|
|
497
675
|
self.state.settings.theme == "light",
|
|
676
|
+
true,
|
|
498
677
|
)),
|
|
499
678
|
area,
|
|
500
679
|
);
|
|
501
680
|
}
|
|
502
681
|
|
|
503
|
-
fn block(title: &str, light: bool) -> Block<'
|
|
682
|
+
fn block(title: &str, light: bool, active: bool) -> Block<'static> {
|
|
683
|
+
let border = if active {
|
|
684
|
+
if light {
|
|
685
|
+
Style::default()
|
|
686
|
+
.fg(Color::Magenta)
|
|
687
|
+
.add_modifier(Modifier::BOLD)
|
|
688
|
+
} else {
|
|
689
|
+
Style::default()
|
|
690
|
+
.fg(Color::Yellow)
|
|
691
|
+
.add_modifier(Modifier::BOLD)
|
|
692
|
+
}
|
|
693
|
+
} else if light {
|
|
694
|
+
Style::default().fg(Color::Blue)
|
|
695
|
+
} else {
|
|
696
|
+
Style::default().fg(Color::Cyan)
|
|
697
|
+
};
|
|
504
698
|
Block::default()
|
|
505
699
|
.borders(Borders::ALL)
|
|
506
|
-
.title(title)
|
|
507
|
-
.border_style(
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
700
|
+
.title(Self::pane_title(title, active))
|
|
701
|
+
.border_style(border)
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
fn pane_title(title: &str, active: bool) -> String {
|
|
705
|
+
if active {
|
|
706
|
+
format!("> {title}")
|
|
707
|
+
} else {
|
|
708
|
+
title.to_string()
|
|
709
|
+
}
|
|
512
710
|
}
|
|
513
711
|
|
|
514
712
|
fn handle_key(&mut self, key: KeyEvent) -> Result<()> {
|
|
@@ -709,6 +907,9 @@ impl PracticodeApp {
|
|
|
709
907
|
}
|
|
710
908
|
"provider" | "ai-provider" if AI_PROVIDERS.contains(&arg) => {
|
|
711
909
|
self.state.settings.ai_provider = normalize_ai_provider(arg);
|
|
910
|
+
self.model_rx = None;
|
|
911
|
+
self.available_models.clear();
|
|
912
|
+
self.available_models_provider.clear();
|
|
712
913
|
save_state(&self.root, &self.state)?;
|
|
713
914
|
self.write_text_output(&format!(
|
|
714
915
|
"AI provider: {}\n{}",
|
|
@@ -717,14 +918,21 @@ impl PracticodeApp {
|
|
|
717
918
|
));
|
|
718
919
|
}
|
|
719
920
|
"model" if arg.is_empty() => {
|
|
720
|
-
self.write_text_output(&
|
|
921
|
+
self.write_text_output(&self.model_status_text());
|
|
721
922
|
}
|
|
722
923
|
"model" => {
|
|
723
|
-
self.state.settings.ai_model = arg
|
|
924
|
+
self.state.settings.ai_model = if arg == "auto" {
|
|
925
|
+
"auto".to_string()
|
|
926
|
+
} else {
|
|
927
|
+
arg.to_string()
|
|
928
|
+
};
|
|
724
929
|
save_state(&self.root, &self.state)?;
|
|
725
|
-
self.write_text_output(&
|
|
930
|
+
self.write_text_output(&self.model_status_text());
|
|
726
931
|
}
|
|
727
|
-
"
|
|
932
|
+
"hint" if arg.is_empty() => {
|
|
933
|
+
self.start_ai_prompt("Give one concise hint for the current problem.")?
|
|
934
|
+
}
|
|
935
|
+
"hint" | "ask" | "ai" if !arg.is_empty() => self.start_ai_prompt(arg)?,
|
|
728
936
|
"note" if !arg.is_empty() => self.append_note(arg)?,
|
|
729
937
|
"note" | "notes" => self.show_notes()?,
|
|
730
938
|
"update" => self.show_update_notice(),
|
|
@@ -971,6 +1179,56 @@ impl PracticodeApp {
|
|
|
971
1179
|
self.update_rx = Some(rx);
|
|
972
1180
|
}
|
|
973
1181
|
|
|
1182
|
+
fn start_model_check(&mut self) {
|
|
1183
|
+
let provider = self.state.settings.ai_provider.clone();
|
|
1184
|
+
if self.model_rx.is_some() || self.available_models_provider == provider {
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
let query_provider = provider.clone();
|
|
1188
|
+
let (tx, rx) = mpsc::channel();
|
|
1189
|
+
thread::spawn(move || {
|
|
1190
|
+
let _ = tx.send(available_models(&query_provider));
|
|
1191
|
+
});
|
|
1192
|
+
self.available_models_provider = provider;
|
|
1193
|
+
self.model_rx = Some(rx);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
fn check_models(&mut self) {
|
|
1197
|
+
let models = self.model_rx.as_ref().and_then(|rx| rx.try_recv().ok());
|
|
1198
|
+
if let Some(models) = models {
|
|
1199
|
+
self.model_rx = None;
|
|
1200
|
+
self.available_models = models;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
fn model_status_text(&self) -> String {
|
|
1205
|
+
let mut lines = vec![
|
|
1206
|
+
format!(
|
|
1207
|
+
"AI model: {}",
|
|
1208
|
+
if self.state.settings.ai_model == "auto" {
|
|
1209
|
+
"auto (provider default)"
|
|
1210
|
+
} else {
|
|
1211
|
+
self.state.settings.ai_model.as_str()
|
|
1212
|
+
}
|
|
1213
|
+
),
|
|
1214
|
+
"Use /model auto to let the provider choose its default.".to_string(),
|
|
1215
|
+
];
|
|
1216
|
+
if self.available_models.is_empty() {
|
|
1217
|
+
lines.push(
|
|
1218
|
+
"Provider model list is unavailable; use /model <name> for a known model."
|
|
1219
|
+
.to_string(),
|
|
1220
|
+
);
|
|
1221
|
+
} else {
|
|
1222
|
+
lines.push("Available models:".to_string());
|
|
1223
|
+
lines.extend(
|
|
1224
|
+
self.available_models
|
|
1225
|
+
.iter()
|
|
1226
|
+
.map(|model| format!("- /model {model}")),
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
lines.join("\n")
|
|
1230
|
+
}
|
|
1231
|
+
|
|
974
1232
|
fn start_busy(&mut self, label: &str, body: &str) {
|
|
975
1233
|
self.busy_label = label.to_string();
|
|
976
1234
|
self.busy_body = body.to_string();
|
|
@@ -1064,7 +1322,7 @@ impl PracticodeApp {
|
|
|
1064
1322
|
self.normalize_command_input();
|
|
1065
1323
|
}
|
|
1066
1324
|
|
|
1067
|
-
fn command_suggestions(&self) -> Vec
|
|
1325
|
+
fn command_suggestions(&self) -> Vec<CommandChoice> {
|
|
1068
1326
|
if self.focus != Focus::Command {
|
|
1069
1327
|
return Vec::new();
|
|
1070
1328
|
}
|
|
@@ -1072,13 +1330,39 @@ impl PracticodeApp {
|
|
|
1072
1330
|
return Vec::new();
|
|
1073
1331
|
};
|
|
1074
1332
|
let query = query.to_lowercase();
|
|
1075
|
-
|
|
1076
|
-
.
|
|
1333
|
+
self.command_choices()
|
|
1334
|
+
.into_iter()
|
|
1077
1335
|
.filter(|hint| hint.insert.starts_with(query.trim_start()))
|
|
1078
|
-
.take(7)
|
|
1079
1336
|
.collect()
|
|
1080
1337
|
}
|
|
1081
1338
|
|
|
1339
|
+
fn command_choices(&self) -> Vec<CommandChoice> {
|
|
1340
|
+
let mut choices = Vec::new();
|
|
1341
|
+
for hint in COMMAND_HINTS {
|
|
1342
|
+
if hint.insert == "model " {
|
|
1343
|
+
for model in self
|
|
1344
|
+
.available_models
|
|
1345
|
+
.iter()
|
|
1346
|
+
.filter(|model| *model != "auto")
|
|
1347
|
+
{
|
|
1348
|
+
choices.push(CommandChoice {
|
|
1349
|
+
insert: format!("model {model}"),
|
|
1350
|
+
display: format!("/model {model}"),
|
|
1351
|
+
desc_key: "cmd_model_available",
|
|
1352
|
+
keep_open: false,
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
choices.push(CommandChoice {
|
|
1357
|
+
insert: hint.insert.to_string(),
|
|
1358
|
+
display: hint.display.to_string(),
|
|
1359
|
+
desc_key: hint.desc_key,
|
|
1360
|
+
keep_open: hint.keep_open,
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
choices
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1082
1366
|
fn move_command_palette(&mut self, delta: isize) {
|
|
1083
1367
|
let len = self.command_suggestions().len();
|
|
1084
1368
|
if len == 0 {
|
|
@@ -1093,19 +1377,19 @@ impl PracticodeApp {
|
|
|
1093
1377
|
if suggestions.is_empty() {
|
|
1094
1378
|
return Ok(false);
|
|
1095
1379
|
}
|
|
1096
|
-
let hint = suggestions[self.command_palette_cursor.min(suggestions.len() - 1)];
|
|
1380
|
+
let hint = &suggestions[self.command_palette_cursor.min(suggestions.len() - 1)];
|
|
1097
1381
|
if hint.keep_open {
|
|
1098
1382
|
self.command = format!("/{}", hint.insert);
|
|
1099
1383
|
self.command_cursor = char_len(&self.command);
|
|
1100
1384
|
self.command_palette_cursor = 0;
|
|
1101
1385
|
return Ok(true);
|
|
1102
1386
|
}
|
|
1103
|
-
let value = hint.insert;
|
|
1387
|
+
let value = hint.insert.clone();
|
|
1104
1388
|
self.command.clear();
|
|
1105
1389
|
self.command_cursor = 0;
|
|
1106
1390
|
self.command_palette_cursor = 0;
|
|
1107
1391
|
self.focus = Focus::None;
|
|
1108
|
-
self.submit_command(value)?;
|
|
1392
|
+
self.submit_command(&value)?;
|
|
1109
1393
|
Ok(true)
|
|
1110
1394
|
}
|
|
1111
1395
|
|