practicode 0.1.7 → 0.1.9

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
@@ -453,7 +453,7 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
453
453
 
454
454
  [[package]]
455
455
  name = "practicode"
456
- version = "0.1.7"
456
+ version = "0.1.9"
457
457
  dependencies = [
458
458
  "anyhow",
459
459
  "crossterm",
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "practicode"
3
- version = "0.1.7"
3
+ version = "0.1.9"
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/README.md CHANGED
@@ -4,7 +4,10 @@
4
4
  ![Ratatui](https://img.shields.io/badge/Ratatui-TUI-00B4D8)
5
5
  ![Local first](https://img.shields.io/badge/local--first-practice-14B8A6)
6
6
  ![AI ready](https://img.shields.io/badge/AI-Codex%20%2B%20Claude-111827)
7
+ ![crates.io](https://img.shields.io/crates/v/practicode?logo=rust)
8
+ ![npm](https://img.shields.io/npm/v/practicode?logo=npm)
7
9
  ![CI](https://github.com/baba9811/practicode/actions/workflows/ci.yml/badge.svg)
10
+ [Socket.dev package health](https://socket.dev/npm/package/practicode)
8
11
 
9
12
  ![practicode terminal UI](assets/practicode-terminal.svg)
10
13
 
@@ -34,6 +37,8 @@ npm install -g practicode
34
37
  practicode
35
38
  ```
36
39
 
40
+ The npm package has a `postinstall` step that runs `cargo build --release --locked` from the package root so the Rust TUI binary is ready. Set `PRACTICODE_SKIP_BUILD=1` to skip that install-time build; the `practicode` launcher will try the same locked Cargo build on first run if the binary is missing.
41
+
37
42
  ### Cargo
38
43
 
39
44
  ```bash
@@ -60,9 +65,10 @@ practicode --help
60
65
 
61
66
  ## Daily Loop
62
67
 
63
- The code editor starts focused.
68
+ On first run, practicode opens a setup panel. After that, the code editor starts focused.
64
69
 
65
70
  ```text
71
+ first run: review /profile setup
66
72
  write code
67
73
  Esc, then /
68
74
  choose /run
@@ -71,6 +77,8 @@ choose /next when it passes
71
77
 
72
78
  Typing `/` outside the editor opens the command palette. Use `up/down` to move, `Enter` to run or complete the selected command, and `Esc` to cancel. Press `?` for in-app help or `Ctrl+C` to quit.
73
79
 
80
+ Hints, failed cases, answers, and user profile panels are copy-friendly: drag in the output pane to use your terminal's normal text selection/copy behavior. When the code editor is visible in the right pane, mouse focus is enabled for that editor. Use `Esc`, `e`, or `/code` to return from output to code.
81
+
74
82
  Submissions are saved as you type under `submissions/<problem-id>/solution.<ext>`.
75
83
 
76
84
  ## CLI Flags
@@ -88,17 +96,19 @@ Submissions are saved as you type under `submissions/<problem-id>/solution.<ext>
88
96
  | `/run` | Judge the current submission |
89
97
  | `/code` | Return to the code editor |
90
98
  | `/next` | Open the next unsolved problem, or ask AI only when none remain |
91
- | `/generate easy string problem` | Ask AI to create a new problem now |
99
+ | `/generate easy string problem` | Ask AI to create a new problem in the background |
92
100
  | `/back` | Go back through problem history |
93
101
  | `/problems` | Browse problems with `up/down` or `j/k`, open with `Enter` |
94
102
  | `/open 2` | Open by number, id, or slug |
95
103
  | `/answer` | Show the reference answer |
96
104
  | `/hint` | Ask the selected AI for a concise hint |
97
105
  | `/hint explain my bug` | Ask the selected AI about the current problem and submission |
98
- | `/profile` | Show your current practice profile |
106
+ | `/profile` | Show your current user profile |
99
107
  | `/difficulty auto` | Set difficulty preference: `auto`, `easy`, `medium`, `hard` |
100
108
  | `/topics arrays, strings` | Set preferred topics for future problems |
101
109
  | `/avoid dp, graph` | Set topics to avoid in future problems |
110
+ | `/generate-languages python, rust` | Limit generated answer languages, or use `all` |
111
+ | `/generate-ui ko, en` | Limit generated problem text languages, or use `all` |
102
112
  | `/provider codex` | Set AI provider and show local CLI/daemon status |
103
113
  | `/model auto` | Use the provider default model for `/hint` and AI-backed `/next` |
104
114
  | `/language python` | Set code language: `python`, `ts`, `java`, `rust` |
@@ -111,13 +121,17 @@ Older command names such as `/prev`, `/list`, `/giveup`, and `/lang` still work
111
121
 
112
122
  The default UI language is English. Switch it any time with `/ui ko`, `/ui ja`, `/ui zh`, or `/ui es`.
113
123
 
114
- Your practice profile is saved in `.practicode/problem-state.json`. It keeps UI language, code language, theme, preferred difficulty, preferred topics, and topics to avoid. `auto` difficulty follows gradual progression; a fixed difficulty asks local selection and AI generation to prefer that level.
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, and generation language scope. `auto` difficulty follows gradual progression; a fixed difficulty asks local selection and AI generation to prefer that level.
125
+
126
+ Inside `/profile`, use `up/down` to move and `Space` or `Enter` to cycle common settings or enable/disable generated answer/UI languages. Use slash commands for free-form lists such as `/topics arrays, strings`.
115
127
 
116
128
  ## Problem Flow
117
129
 
118
130
  `/next` is local-first: it opens the next unsolved local problem before generating anything. When no unsolved problem remains, it asks the selected AI provider to create one.
119
131
 
120
- Use `/generate <request>` when you explicitly want to create a new problem now.
132
+ Use `/generate <request>` when you explicitly want to create a new problem in the background while you keep solving the current one. The generated problem stays local and `/next` will pick it up later.
133
+
134
+ If `/next` has to generate because no local problem remains, it runs in the foreground. Editing and commands are paused so state cannot change halfway through that AI task. Press `Space` for the warmup timing drill, or `q`/`Ctrl+C` to quit.
121
135
 
122
136
  ```text
123
137
  /generate a slightly harder string problem
@@ -164,18 +178,24 @@ cargo install --force practicode
164
178
 
165
179
  - `/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.
166
180
  - `/hint` sends the current problem and submission to the selected AI provider CLI.
167
- - AI-backed `/next` can run a custom shell command from `settings.ai_next_command`; save only commands you trust.
181
+ - AI-backed `/next` and `/generate` can run a custom shell command from `settings.ai_next_command`; save only commands you trust.
182
+ - npm installs run the package `postinstall` script described above. It only invokes Cargo with the checked-in lockfile from this package root; it does not read local `.env`/`.npmrc` files or contact the configured AI provider.
183
+ - npm releases are published from GitHub Actions with registry signatures and provenance enabled in `package.json`. The release workflow is also prepared for npm Trusted Publishing/OIDC; maintainers should prefer that over long-lived publish tokens when the package setting is enabled on npm.
168
184
  - Local `.env`, `.npmrc`, `.practicode/`, `problems/`, and `submissions/` are ignored by git. Do not commit tokens, private prompts, or answer keys.
169
185
 
170
186
  ## Development Checks
171
187
 
172
188
  ```bash
189
+ cargo fmt --check
173
190
  cargo test
191
+ cargo clippy --all-targets --all-features -- -D warnings
174
192
  cargo run -- --smoke
175
- cargo audit
193
+ cargo audit --deny warnings
194
+ npm pack --dry-run
195
+ npm run smoke
176
196
  ```
177
197
 
178
- This repo has no npm dependencies or lockfile today, so `npm audit` and `pnpm audit` are not applicable until a matching lockfile is added.
198
+ CI runs the Rust audit gate with `cargo audit --deny warnings`; release publishing stops before crates.io/npm publish if that audit fails. This repo has no npm dependencies or lockfile today, so `npm audit` and `pnpm audit` are not applicable until a matching lockfile is added.
179
199
 
180
200
  ## Contributing
181
201
 
package/SECURITY.md ADDED
@@ -0,0 +1,11 @@
1
+ # Security Policy
2
+
3
+ ## Reporting
4
+
5
+ Report vulnerabilities through GitHub Security Advisories for this repository when available. If that is not available, open a minimal public issue asking for a private security contact and avoid posting exploit details.
6
+
7
+ Do not include tokens, private prompts, `.env`, `.npmrc`, `.practicode/`, `problems/`, or `submissions/` contents in public reports.
8
+
9
+ ## Scope
10
+
11
+ Security-sensitive areas include npm install scripts, release publishing, command execution, local judging, AI provider prompts, update checks, and local user data handling.
@@ -15,9 +15,12 @@
15
15
  "palette_hint": "up/down select | Enter run | Esc cancel",
16
16
  "hint_command": "Enter submit | Esc cancel",
17
17
  "hint_list": "up/down move | Enter open | Esc close",
18
- "hint_output": "Esc/click edit | / command | ? help",
18
+ "hint_output": "Esc/e edit | drag select to copy | / command",
19
+ "hint_settings": "up/down move | Space/Enter toggle | Esc close",
19
20
  "hint_code": "Esc then / command",
20
21
  "hint_idle": "/ command | ? help",
22
+ "hint_busy": "Working | q quit",
23
+ "hint_busy_next": "Space warmup | q quit",
21
24
  "help_title": "Help",
22
25
  "daily_loop": "Daily loop",
23
26
  "keys": "Keys",
@@ -26,17 +29,19 @@
26
29
  "cmd_code": "Return to the code editor",
27
30
  "cmd_edit": "Return to the code editor",
28
31
  "cmd_next": "Open the next problem",
29
- "cmd_generate": "Generate a new problem now",
32
+ "cmd_generate": "Generate a new problem in the background",
30
33
  "cmd_prev": "Open the previous problem",
31
34
  "cmd_list": "Browse problems",
32
35
  "cmd_open": "Open by number, id, or slug",
33
36
  "cmd_giveup": "Show the reference answer",
34
37
  "cmd_hint": "Ask for a hint about the current problem",
35
38
  "cmd_ai": "Ask AI about the current problem and code",
36
- "cmd_profile": "Show practice profile",
39
+ "cmd_profile": "Show user profile",
37
40
  "cmd_difficulty": "Set preferred difficulty",
38
41
  "cmd_topics": "Set preferred topics",
39
42
  "cmd_avoid": "Set topics to avoid",
43
+ "cmd_generate_languages": "Set answer languages for generated problems",
44
+ "cmd_generate_ui": "Set UI languages for generated problems",
40
45
  "cmd_provider": "Set AI provider",
41
46
  "cmd_model": "Set AI model",
42
47
  "cmd_model_auto": "Use provider default model",
@@ -57,6 +62,10 @@
57
62
  "update_check_failed": "Could not check for updates.",
58
63
  "generating_next": "Generating next problem",
59
64
  "already_busy": "Already busy.",
65
+ "busy_warmup": "Warmup: press Space when * hits the center.",
66
+ "busy_commands_paused": "Commands are paused until generation finishes.",
67
+ "hits": "Hits",
68
+ "misses": "Misses",
60
69
  "run_pass_next": "Next: /next",
61
- "run_fail_next": "Fix: press Esc or click this pane to edit, then /run"
70
+ "run_fail_next": "Fix: press Esc or e to edit, then /run"
62
71
  }
@@ -15,9 +15,12 @@
15
15
  "palette_hint": "arriba/abajo elegir | Enter ejecutar | Esc cancelar",
16
16
  "hint_command": "Enter ejecutar | Esc cancelar",
17
17
  "hint_list": "arriba/abajo mover | Enter abrir | Esc cerrar",
18
- "hint_output": "Esc/clic editar | / comando | ? ayuda",
18
+ "hint_output": "Esc/e editar | arrastra para copiar | / comando",
19
+ "hint_settings": "arriba/abajo mover | Space/Enter alternar | Esc cerrar",
19
20
  "hint_code": "Esc y luego / comando",
20
21
  "hint_idle": "/ comando | ? ayuda",
22
+ "hint_busy": "Trabajando | q salir",
23
+ "hint_busy_next": "Space calentamiento | q salir",
21
24
  "help_title": "Ayuda",
22
25
  "daily_loop": "Flujo diario",
23
26
  "keys": "Teclas",
@@ -26,17 +29,19 @@
26
29
  "cmd_code": "Volver al editor de codigo",
27
30
  "cmd_edit": "Volver al editor de codigo",
28
31
  "cmd_next": "Abrir el siguiente problema",
29
- "cmd_generate": "Generar un problema nuevo ahora",
32
+ "cmd_generate": "Generar un problema nuevo en segundo plano",
30
33
  "cmd_prev": "Abrir el problema anterior",
31
34
  "cmd_list": "Abrir la lista de problemas",
32
35
  "cmd_open": "Abrir por numero, id o slug",
33
36
  "cmd_giveup": "Mostrar la respuesta de referencia",
34
37
  "cmd_hint": "Pedir una pista para el problema actual",
35
38
  "cmd_ai": "Preguntar a AI sobre el problema y codigo actuales",
36
- "cmd_profile": "Mostrar perfil de practica",
39
+ "cmd_profile": "Mostrar perfil de usuario",
37
40
  "cmd_difficulty": "Configurar dificultad preferida",
38
41
  "cmd_topics": "Configurar temas preferidos",
39
42
  "cmd_avoid": "Configurar temas a evitar",
43
+ "cmd_generate_languages": "Configurar lenguajes de respuesta generados",
44
+ "cmd_generate_ui": "Configurar idiomas de UI generados",
40
45
  "cmd_provider": "Configurar AI provider",
41
46
  "cmd_model": "Configurar AI model",
42
47
  "cmd_model_auto": "Usar el modelo predeterminado del provider",
@@ -57,6 +62,10 @@
57
62
  "update_check_failed": "No se pudo comprobar actualizaciones.",
58
63
  "generating_next": "Generando el siguiente problema",
59
64
  "already_busy": "Ya hay una tarea en curso.",
65
+ "busy_warmup": "Calentamiento: pulsa Space cuando * llegue al centro.",
66
+ "busy_commands_paused": "Los comandos quedan pausados hasta que termine la generacion.",
67
+ "hits": "Aciertos",
68
+ "misses": "Fallos",
60
69
  "run_pass_next": "Siguiente: /next",
61
- "run_fail_next": "Corrige: pulsa Esc o haz clic en este panel para editar, luego /run"
70
+ "run_fail_next": "Corrige: pulsa Esc o e para editar, luego /run"
62
71
  }
@@ -15,9 +15,12 @@
15
15
  "palette_hint": "上下 選択 | Enter 実行 | Esc キャンセル",
16
16
  "hint_command": "Enter 実行 | Esc キャンセル",
17
17
  "hint_list": "上下 移動 | Enter 開く | Esc 閉じる",
18
- "hint_output": "Esc/クリック 編集 | / コマンド | ? ヘルプ",
18
+ "hint_output": "Esc/e 編集 | ドラッグ選択でコピー | / コマンド",
19
+ "hint_settings": "上下 移動 | Space/Enter 切替 | Esc 閉じる",
19
20
  "hint_code": "Esc の後 / コマンド",
20
21
  "hint_idle": "/ コマンド | ? ヘルプ",
22
+ "hint_busy": "処理中 | q 終了",
23
+ "hint_busy_next": "Space ウォームアップ | q 終了",
21
24
  "help_title": "ヘルプ",
22
25
  "daily_loop": "基本フロー",
23
26
  "keys": "キー",
@@ -26,17 +29,19 @@
26
29
  "cmd_code": "コードエディタに戻る",
27
30
  "cmd_edit": "コードエディタに戻る",
28
31
  "cmd_next": "次の問題を開く",
29
- "cmd_generate": "新しい問題を今すぐ生成",
32
+ "cmd_generate": "新しい問題をバックグラウンド生成",
30
33
  "cmd_prev": "前の問題を開く",
31
34
  "cmd_list": "問題一覧を開く",
32
35
  "cmd_open": "番号、id、slug で問題を開く",
33
36
  "cmd_giveup": "解答を見る",
34
37
  "cmd_hint": "現在の問題のヒントを依頼",
35
38
  "cmd_ai": "現在の問題とコードについて AI に質問",
36
- "cmd_profile": "練習プロファイルを表示",
39
+ "cmd_profile": "ユーザープロファイルを表示",
37
40
  "cmd_difficulty": "希望難易度を設定",
38
41
  "cmd_topics": "希望トピックを設定",
39
42
  "cmd_avoid": "避けるトピックを設定",
43
+ "cmd_generate_languages": "生成問題の解答言語を設定",
44
+ "cmd_generate_ui": "生成問題の UI 言語を設定",
40
45
  "cmd_provider": "AI provider を設定",
41
46
  "cmd_model": "AI model を設定",
42
47
  "cmd_model_auto": "provider の既定モデルを使用",
@@ -57,6 +62,10 @@
57
62
  "update_check_failed": "更新を確認できませんでした。",
58
63
  "generating_next": "次の問題を生成中",
59
64
  "already_busy": "すでに処理中です。",
65
+ "busy_warmup": "ウォームアップ: * が中央に来たら Space。",
66
+ "busy_commands_paused": "生成が終わるまでコマンドは一時停止します。",
67
+ "hits": "成功",
68
+ "misses": "ミス",
60
69
  "run_pass_next": "次: /next",
61
- "run_fail_next": "修正: Esc またはこのペインをクリックして編集し、/run"
70
+ "run_fail_next": "修正: Esc または e で編集し、/run"
62
71
  }
@@ -15,9 +15,12 @@
15
15
  "palette_hint": "위/아래 선택 | Enter 실행 | Esc 취소",
16
16
  "hint_command": "Enter 실행 | Esc 취소",
17
17
  "hint_list": "위/아래 이동 | Enter 열기 | Esc 닫기",
18
- "hint_output": "Esc/클릭 편집 | / 명령 | ? 도움말",
18
+ "hint_output": "Esc/e 편집 | 드래그 선택으로 복사 | / 명령",
19
+ "hint_settings": "위/아래 이동 | Space/Enter 토글 | Esc 닫기",
19
20
  "hint_code": "Esc 후 / 명령",
20
21
  "hint_idle": "/ 명령 | ? 도움말",
22
+ "hint_busy": "작업 중 | q 종료",
23
+ "hint_busy_next": "Space 워밍업 | q 종료",
21
24
  "help_title": "도움말",
22
25
  "daily_loop": "기본 흐름",
23
26
  "keys": "키",
@@ -26,17 +29,19 @@
26
29
  "cmd_code": "코드 편집기로 돌아가기",
27
30
  "cmd_edit": "코드 편집기로 돌아가기",
28
31
  "cmd_next": "다음 문제 열기",
29
- "cmd_generate": "새 문제를 바로 생성",
32
+ "cmd_generate": "새 문제를 백그라운드에서 생성",
30
33
  "cmd_prev": "이전 문제 열기",
31
34
  "cmd_list": "문제 목록 열기",
32
35
  "cmd_open": "번호, id, slug로 문제 열기",
33
36
  "cmd_giveup": "정답 보기",
34
37
  "cmd_hint": "현재 문제 힌트 요청",
35
38
  "cmd_ai": "현재 문제와 코드에 대해 AI에게 질문",
36
- "cmd_profile": "연습 프로파일 보기",
39
+ "cmd_profile": "사용자 프로필 보기",
37
40
  "cmd_difficulty": "선호 난이도 설정",
38
41
  "cmd_topics": "선호 주제 설정",
39
42
  "cmd_avoid": "피할 주제 설정",
43
+ "cmd_generate_languages": "생성 문제의 정답 언어 설정",
44
+ "cmd_generate_ui": "생성 문제의 UI 언어 설정",
40
45
  "cmd_provider": "AI provider 설정",
41
46
  "cmd_model": "AI model 설정",
42
47
  "cmd_model_auto": "provider 기본 모델 사용",
@@ -57,6 +62,10 @@
57
62
  "update_check_failed": "업데이트를 확인할 수 없습니다.",
58
63
  "generating_next": "다음 문제 생성 중",
59
64
  "already_busy": "이미 작업 중입니다.",
65
+ "busy_warmup": "워밍업: *가 가운데에 왔을 때 Space를 누르세요.",
66
+ "busy_commands_paused": "생성이 끝날 때까지 명령은 잠시 멈춥니다.",
67
+ "hits": "성공",
68
+ "misses": "실패",
60
69
  "run_pass_next": "다음: /next",
61
- "run_fail_next": "수정: Esc 누르거나 창을 클릭해 편집한 뒤 /run"
70
+ "run_fail_next": "수정: Esc 또는 e로 편집한 뒤 /run"
62
71
  }
@@ -15,9 +15,12 @@
15
15
  "palette_hint": "上下 选择 | Enter 执行 | Esc 取消",
16
16
  "hint_command": "Enter 执行 | Esc 取消",
17
17
  "hint_list": "上下 移动 | Enter 打开 | Esc 关闭",
18
- "hint_output": "Esc/点击 编辑 | / 命令 | ? 帮助",
18
+ "hint_output": "Esc/e 编辑 | 拖拽选择复制 | / 命令",
19
+ "hint_settings": "上下移动 | Space/Enter 切换 | Esc 关闭",
19
20
  "hint_code": "Esc 后输入 / 命令",
20
21
  "hint_idle": "/ 命令 | ? 帮助",
22
+ "hint_busy": "处理中 | q 退出",
23
+ "hint_busy_next": "Space 热身 | q 退出",
21
24
  "help_title": "帮助",
22
25
  "daily_loop": "日常流程",
23
26
  "keys": "按键",
@@ -26,17 +29,19 @@
26
29
  "cmd_code": "回到代码编辑器",
27
30
  "cmd_edit": "回到代码编辑器",
28
31
  "cmd_next": "打开下一题",
29
- "cmd_generate": "立即生成新题",
32
+ "cmd_generate": "后台生成新题",
30
33
  "cmd_prev": "打开上一题",
31
34
  "cmd_list": "打开题目列表",
32
35
  "cmd_open": "按编号、id 或 slug 打开题目",
33
36
  "cmd_giveup": "显示参考答案",
34
37
  "cmd_hint": "请求当前题目的提示",
35
38
  "cmd_ai": "向 AI 询问当前题目和代码",
36
- "cmd_profile": "显示练习配置",
39
+ "cmd_profile": "显示用户配置",
37
40
  "cmd_difficulty": "设置偏好难度",
38
41
  "cmd_topics": "设置偏好主题",
39
42
  "cmd_avoid": "设置要避开的主题",
43
+ "cmd_generate_languages": "设置生成题目的答案语言",
44
+ "cmd_generate_ui": "设置生成题目的 UI 语言",
40
45
  "cmd_provider": "设置 AI provider",
41
46
  "cmd_model": "设置 AI model",
42
47
  "cmd_model_auto": "使用 provider 默认模型",
@@ -57,6 +62,10 @@
57
62
  "update_check_failed": "无法检查更新。",
58
63
  "generating_next": "正在生成下一题",
59
64
  "already_busy": "已有任务正在运行。",
65
+ "busy_warmup": "热身:当 * 到达中间时按 Space。",
66
+ "busy_commands_paused": "生成完成前命令会暂停。",
67
+ "hits": "命中",
68
+ "misses": "失误",
60
69
  "run_pass_next": "下一步: /next",
61
- "run_fail_next": "修复: 按 Esc 或点击此面板编辑,然后 /run"
70
+ "run_fail_next": "修复: 按 Esc e 编辑,然后 /run"
62
71
  }
@@ -5,19 +5,24 @@ Practicode is local-first: user data stays under `.practicode/`, `problems/`, an
5
5
  ## Source Layout
6
6
 
7
7
  - `src/core.rs` owns problem data, state loading/saving, judging, and file generation.
8
- - `src/core/profile.rs` owns practice-profile defaults and normalization.
9
- - `src/tui.rs` owns Ratatui rendering and interaction flow.
8
+ - `src/core/profile.rs` owns user-profile defaults and normalization.
9
+ - `src/tui.rs` owns the Ratatui app shell, event routing, and workflow orchestration.
10
10
  - `src/tui/commands.rs` owns the command palette catalog.
11
- - `src/ai.rs` owns provider commands, daemon/model checks, and AI prompts.
11
+ - `src/tui/editor.rs` owns the in-terminal code editor state.
12
+ - `src/tui/problem_view.rs` owns problem-statement rendering.
13
+ - `src/tui/settings_panel.rs` owns `/profile` setup-panel rendering and keyboard toggles.
14
+ - `src/ai.rs` owns provider commands, daemon/model checks, and AI prompts for foreground `/next` generation and background `/generate` prefetch.
12
15
  - `src/update.rs` owns update checks.
13
16
  - `src/text.rs` owns terminal text editing and markdown/plain rendering helpers.
14
17
 
15
18
  ## Extension Rules
16
19
 
17
- - Add domain logic under the owning module first; keep `tui.rs` as orchestration and rendering.
20
+ - Add domain logic under the owning module first; keep `tui.rs` as orchestration, not a catch-all.
18
21
  - Add user-visible commands in `src/tui/commands.rs`, then route behavior in `PracticodeApp::handle_command`.
19
- - Add persisted profile settings to `Settings`, normalize them in `normalize_settings`, and cover old-state compatibility with tests.
22
+ - Add persisted user profile settings to `Settings`, normalize them in `normalize_settings`, and cover old-state compatibility with tests.
20
23
  - Keep provider-specific behavior in `src/ai.rs`; TUI should ask for status or start tasks, not know provider internals.
24
+ - Keep foreground and background generation flows separate: `/next` may block when no local problem exists, while `/generate` must preserve the current problem and user profile state.
25
+ - Keep output panes copy-friendly. Mouse capture should be enabled for the visible code editor, but disabled while output, hints, answers, lists, or settings panels are shown so terminal drag selection keeps working.
21
26
  - Keep local user data backwards-compatible. Missing fields should default cleanly.
22
27
 
23
28
  ## Release
@@ -45,11 +45,16 @@ Verify publication:
45
45
  ```bash
46
46
  gh run list --limit 5
47
47
  npm view practicode version
48
+ npm view practicode dist.signatures dist.attestations --json
48
49
  cargo search practicode --limit 1
49
50
  ```
50
51
 
51
52
  Do not print or commit tokens. Local `.env` and `.npmrc` are ignored; GitHub Actions uses `NPM_TOKEN` and `CRATES_TOKEN` repository secrets.
52
53
 
54
+ For npm supply-chain posture, keep `publishConfig.provenance` enabled and keep the release job's `id-token: write` permission. When the npm package's Trusted Publisher setting is configured for this repository and `.github/workflows/release.yml`, remove the long-lived `NPM_TOKEN` dependency from the npm publish steps and disallow token publishing in the npm package settings.
55
+
56
+ Socket.dev indexes the npm package page at <https://socket.dev/npm/package/practicode>. It may lag behind npm immediately after a release; verify npm first with `npm view practicode version`, then re-check Socket after indexing catches up. If Socket flags the npm `postinstall` script, confirm it still only runs the locked Cargo build documented in the README.
57
+
53
58
  ## Documentation Ownership
54
59
 
55
60
  | File | Audience |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "practicode",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Local-first coding-test practice in a Rust terminal UI with optional AI help.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -8,6 +8,9 @@
8
8
  "url": "git+https://github.com/baba9811/practicode.git"
9
9
  },
10
10
  "homepage": "https://github.com/baba9811/practicode#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/baba9811/practicode/issues"
13
+ },
11
14
  "bin": {
12
15
  "practicode": "bin/practicode.js"
13
16
  },
@@ -27,6 +30,7 @@
27
30
  "Cargo.toml",
28
31
  "LICENSE",
29
32
  "README.md",
33
+ "SECURITY.md",
30
34
  "THIRD_PARTY_LICENSES.md"
31
35
  ],
32
36
  "keywords": [
@@ -41,6 +45,7 @@
41
45
  "node": ">=18"
42
46
  },
43
47
  "publishConfig": {
44
- "access": "public"
48
+ "access": "public",
49
+ "provenance": true
45
50
  }
46
51
  }
package/src/ai.rs CHANGED
@@ -1,7 +1,7 @@
1
1
  use crate::{
2
2
  core::{
3
- AppState, PROBLEM_NOTES_PATH, Problem, Settings, ensure_submission, normalize_ai_provider,
4
- render_problem,
3
+ AppState, LANGUAGES, PROBLEM_NOTES_PATH, Problem, Settings, UI_LANGUAGES,
4
+ ensure_submission, normalize_ai_provider, render_problem,
5
5
  },
6
6
  process::{run_capture, sh_quote, shell_process, unique_temp_path, which},
7
7
  };
@@ -78,6 +78,38 @@ pub fn run_ai_next(root: &Path, state: &AppState, force: bool, request: &str) ->
78
78
  }
79
79
  }
80
80
 
81
+ pub fn run_ai_generate(root: &Path, state: &AppState, request: &str) -> String {
82
+ let provider = normalize_ai_provider(&state.settings.ai_provider);
83
+ let command = if state.settings.next_ai_command().trim().is_empty() {
84
+ default_ai_generate_command(root, &state.settings, request)
85
+ } else {
86
+ state.settings.next_ai_command().to_string()
87
+ };
88
+ let mut process = shell_process(&command);
89
+ process
90
+ .current_dir(root)
91
+ .env("PRACTICODE_NEXT_REQUEST", request)
92
+ .env("PRACTICODE_GENERATE_BACKGROUND", "1")
93
+ .env("PRACTICODE_AI_PROVIDER", &provider)
94
+ .env("PRACTICODE_AI_MODEL", &state.settings.ai_model);
95
+ match run_capture(&mut process, "", Duration::from_secs(900)) {
96
+ Ok(run) if run.code == Some(0) => {
97
+ let output = output_text(&run.stdout, &run.stderr);
98
+ format!("{provider} background generation finished\n{output}")
99
+ .trim()
100
+ .to_string()
101
+ }
102
+ Ok(run) => {
103
+ let output = output_text(&run.stdout, &run.stderr);
104
+ format!(
105
+ "{provider} background generation failed ({})\n{output}",
106
+ run.code.unwrap_or(-1)
107
+ )
108
+ }
109
+ Err(error) => format!("{provider} background generation failed\n{error}"),
110
+ }
111
+ }
112
+
81
113
  pub fn default_ai_next_command(root: &Path, settings: &Settings, request: &str) -> String {
82
114
  match normalize_ai_provider(&settings.ai_provider).as_str() {
83
115
  "claude" => default_claude_next_command(root, settings, request),
@@ -85,6 +117,13 @@ pub fn default_ai_next_command(root: &Path, settings: &Settings, request: &str)
85
117
  }
86
118
  }
87
119
 
120
+ pub fn default_ai_generate_command(root: &Path, settings: &Settings, request: &str) -> String {
121
+ match normalize_ai_provider(&settings.ai_provider).as_str() {
122
+ "claude" => default_claude_generate_command(root, settings, request),
123
+ _ => default_codex_generate_command(root, settings, request),
124
+ }
125
+ }
126
+
88
127
  pub fn provider_status(provider: &str) -> String {
89
128
  match normalize_ai_provider(provider).as_str() {
90
129
  "claude" => {
@@ -128,7 +167,7 @@ pub fn default_ai_next_prompt(request: &str) -> String {
128
167
 
129
168
  pub fn default_ai_next_prompt_with_settings(settings: &Settings, request: &str) -> String {
130
169
  format!(
131
- "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: {}. Practice profile: difficulty preference: {}; preferred topics: {}; avoid topics: {}; code language: {}; UI language: {}. Treat difficulty auto as gradual progression from state; otherwise prefer the requested difficulty unless the direct user request conflicts. 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.",
170
+ "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: {}. User profile: difficulty preference: {}; preferred topics: {}; avoid topics: {}; code language: {}; UI language: {}; generated answer languages: {}; generated UI languages: {}. Treat difficulty auto as gradual progression from state; otherwise prefer the requested difficulty unless the direct user request conflicts. 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
171
  if request.is_empty() {
133
172
  "(none)"
134
173
  } else {
@@ -138,7 +177,27 @@ pub fn default_ai_next_prompt_with_settings(settings: &Settings, request: &str)
138
177
  list_or_none(&settings.topics),
139
178
  list_or_none(&settings.avoid_topics),
140
179
  settings.language,
141
- settings.ui_language
180
+ settings.ui_language,
181
+ list_or_all(&settings.generate_languages, LANGUAGES),
182
+ list_or_all(&settings.generate_ui_languages, UI_LANGUAGES)
183
+ )
184
+ }
185
+
186
+ pub fn default_ai_generate_prompt_with_settings(settings: &Settings, request: &str) -> String {
187
+ format!(
188
+ "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 for later use. The built-in 001-hello-world already exists, so do not duplicate it. User request: {}. User profile: difficulty preference: {}; preferred topics: {}; avoid topics: {}; current code language: {}; current UI language: {}; generated answer languages: {}; generated UI languages: {}. Treat difficulty auto as gradual progression from state; otherwise prefer the requested difficulty unless the direct user request conflicts. Make the smallest valid edits: update .practicode/problem_bank.json, one problem directory, and problems/INDEX.md. Preserve .practicode/problem-state.json current_problem, history, solved, and settings; do not switch the current problem. Do not include the answer in the problem statement.",
189
+ if request.is_empty() {
190
+ "(none)"
191
+ } else {
192
+ request
193
+ },
194
+ settings.difficulty,
195
+ list_or_none(&settings.topics),
196
+ list_or_none(&settings.avoid_topics),
197
+ settings.language,
198
+ settings.ui_language,
199
+ list_or_all(&settings.generate_languages, LANGUAGES),
200
+ list_or_all(&settings.generate_ui_languages, UI_LANGUAGES)
142
201
  )
143
202
  }
144
203
 
@@ -319,6 +378,22 @@ fn default_codex_next_command(root: &Path, settings: &Settings, request: &str) -
319
378
  format!("{start}; {exec}")
320
379
  }
321
380
 
381
+ fn default_codex_generate_command(root: &Path, settings: &Settings, request: &str) -> String {
382
+ let start = "if [ -x \"$HOME/.codex/packages/standalone/current/codex\" ]; then codex app-server daemon start >/dev/null 2>&1 || true; fi";
383
+ let mut exec = format!(
384
+ "codex exec --ephemeral --cd {} --sandbox workspace-write",
385
+ sh_quote(&root.display().to_string())
386
+ );
387
+ if let Some(model) = settings.model_arg() {
388
+ exec.push_str(&format!(" --model {}", sh_quote(model)));
389
+ }
390
+ exec.push(' ');
391
+ exec.push_str(&sh_quote(&default_ai_generate_prompt_with_settings(
392
+ settings, request,
393
+ )));
394
+ format!("{start}; {exec}")
395
+ }
396
+
322
397
  fn codex_daemon_path() -> Option<PathBuf> {
323
398
  env::var_os("HOME").map(|home| {
324
399
  PathBuf::from(home)
@@ -343,6 +418,22 @@ fn default_claude_next_command(root: &Path, settings: &Settings, request: &str)
343
418
  )
344
419
  }
345
420
 
421
+ fn default_claude_generate_command(root: &Path, settings: &Settings, request: &str) -> String {
422
+ let mut claude = "claude --permission-mode acceptEdits".to_string();
423
+ if let Some(model) = settings.model_arg() {
424
+ claude.push_str(&format!(" --model {}", sh_quote(model)));
425
+ }
426
+ claude.push_str(" -p ");
427
+ claude.push_str(&sh_quote(&default_ai_generate_prompt_with_settings(
428
+ settings, request,
429
+ )));
430
+ format!(
431
+ "claude daemon status >/dev/null 2>&1 || true; cd {}; {}",
432
+ sh_quote(&root.display().to_string()),
433
+ claude
434
+ )
435
+ }
436
+
346
437
  fn output_text(stdout: &str, stderr: &str) -> String {
347
438
  [stdout.trim(), stderr.trim()]
348
439
  .into_iter()
@@ -359,6 +450,14 @@ fn list_or_none(values: &[String]) -> String {
359
450
  }
360
451
  }
361
452
 
453
+ fn list_or_all(values: &[String], all: &[&str]) -> String {
454
+ if values.is_empty() {
455
+ all.join(", ")
456
+ } else {
457
+ values.join(", ")
458
+ }
459
+ }
460
+
362
461
  #[cfg(test)]
363
462
  mod tests {
364
463
  use super::parse_model_list;