practicode 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Cargo.lock CHANGED
@@ -357,7 +357,7 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
357
357
 
358
358
  [[package]]
359
359
  name = "practicode"
360
- version = "0.1.4"
360
+ version = "0.1.5"
361
361
  dependencies = [
362
362
  "anyhow",
363
363
  "crossterm 0.29.0",
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "practicode"
3
- version = "0.1.4"
3
+ version = "0.1.5"
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
@@ -63,6 +63,7 @@ Submissions are saved as you type under `submissions/<problem-id>/solution.<ext>
63
63
  | Command | Action |
64
64
  | --- | --- |
65
65
  | `/run` | Judge the current submission |
66
+ | `/code` | Return to the code editor |
66
67
  | `/next` | Open the next local problem, or ask AI to create one |
67
68
  | `/next easy string problem` | Ask AI for a custom next problem |
68
69
  | `/prev` | Go back through problem history |
@@ -78,7 +79,6 @@ Submissions are saved as you type under `submissions/<problem-id>/solution.<ext>
78
79
  | `/lang python` | Set code language: `python`, `ts`, `java`, `rust` |
79
80
  | `/ui en` | Set UI language: `en`, `ko`, `ja`, `zh`, `es` |
80
81
  | `/theme dark` | Set theme: `dark` or `light` |
81
- | `/source ai` | Prefer AI for next-problem generation |
82
82
  | `/update` | Show update instructions when a newer version is available |
83
83
  | `/exit` | Quit |
84
84
 
@@ -106,7 +106,6 @@ Claude Code is also supported:
106
106
  ```text
107
107
  /provider claude
108
108
  /model sonnet
109
- /source ai
110
109
  ```
111
110
 
112
111
  Generated problems and submissions stay local:
@@ -15,7 +15,7 @@
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 code | / command | ? help",
18
+ "hint_output": "Esc/click edit | / command | ? help",
19
19
  "hint_code": "Esc then / command",
20
20
  "hint_idle": "/ command | ? help",
21
21
  "help_title": "Help",
@@ -23,6 +23,7 @@
23
23
  "keys": "Keys",
24
24
  "debug_prints": "Debug prints",
25
25
  "cmd_run": "Judge the current submission",
26
+ "cmd_code": "Return to the code editor",
26
27
  "cmd_edit": "Return to the code editor",
27
28
  "cmd_next": "Open the next problem",
28
29
  "cmd_prev": "Open the previous problem",
@@ -50,5 +51,7 @@
50
51
  "update_check_disabled": "Update check is disabled.",
51
52
  "update_check_failed": "Could not check for updates.",
52
53
  "generating_next": "Generating next problem",
53
- "already_busy": "Already busy."
54
+ "already_busy": "Already busy.",
55
+ "run_pass_next": "Next: /next",
56
+ "run_fail_next": "Fix: press Esc or click this pane to edit, then /run"
54
57
  }
@@ -15,7 +15,7 @@
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 codigo | / comando | ? ayuda",
18
+ "hint_output": "Esc/clic editar | / comando | ? ayuda",
19
19
  "hint_code": "Esc y luego / comando",
20
20
  "hint_idle": "/ comando | ? ayuda",
21
21
  "help_title": "Ayuda",
@@ -23,6 +23,7 @@
23
23
  "keys": "Teclas",
24
24
  "debug_prints": "Salida de depuracion",
25
25
  "cmd_run": "Evalua la solucion actual",
26
+ "cmd_code": "Volver al editor de codigo",
26
27
  "cmd_edit": "Volver al editor de codigo",
27
28
  "cmd_next": "Abrir el siguiente problema",
28
29
  "cmd_prev": "Abrir el problema anterior",
@@ -50,5 +51,7 @@
50
51
  "update_check_disabled": "La comprobacion de actualizaciones esta desactivada.",
51
52
  "update_check_failed": "No se pudo comprobar actualizaciones.",
52
53
  "generating_next": "Generando el siguiente problema",
53
- "already_busy": "Ya hay una tarea en curso."
54
+ "already_busy": "Ya hay una tarea en curso.",
55
+ "run_pass_next": "Siguiente: /next",
56
+ "run_fail_next": "Corrige: pulsa Esc o haz clic en este panel para editar, luego /run"
54
57
  }
@@ -15,7 +15,7 @@
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/クリック 編集 | / コマンド | ? ヘルプ",
19
19
  "hint_code": "Esc の後 / コマンド",
20
20
  "hint_idle": "/ コマンド | ? ヘルプ",
21
21
  "help_title": "ヘルプ",
@@ -23,6 +23,7 @@
23
23
  "keys": "キー",
24
24
  "debug_prints": "デバッグ出力",
25
25
  "cmd_run": "現在の提出を判定",
26
+ "cmd_code": "コードエディタに戻る",
26
27
  "cmd_edit": "コードエディタに戻る",
27
28
  "cmd_next": "次の問題を開く",
28
29
  "cmd_prev": "前の問題を開く",
@@ -50,5 +51,7 @@
50
51
  "update_check_disabled": "更新確認は無効です。",
51
52
  "update_check_failed": "更新を確認できませんでした。",
52
53
  "generating_next": "次の問題を生成中",
53
- "already_busy": "すでに処理中です。"
54
+ "already_busy": "すでに処理中です。",
55
+ "run_pass_next": "次: /next",
56
+ "run_fail_next": "修正: Esc またはこのペインをクリックして編集し、/run"
54
57
  }
@@ -15,7 +15,7 @@
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/클릭 편집 | / 명령 | ? 도움말",
19
19
  "hint_code": "Esc 후 / 명령",
20
20
  "hint_idle": "/ 명령 | ? 도움말",
21
21
  "help_title": "도움말",
@@ -23,6 +23,7 @@
23
23
  "keys": "키",
24
24
  "debug_prints": "디버그 출력",
25
25
  "cmd_run": "현재 제출을 채점",
26
+ "cmd_code": "코드 편집기로 돌아가기",
26
27
  "cmd_edit": "코드 편집기로 돌아가기",
27
28
  "cmd_next": "다음 문제 열기",
28
29
  "cmd_prev": "이전 문제 열기",
@@ -50,5 +51,7 @@
50
51
  "update_check_disabled": "업데이트 확인이 꺼져 있습니다.",
51
52
  "update_check_failed": "업데이트를 확인할 수 없습니다.",
52
53
  "generating_next": "다음 문제 생성 중",
53
- "already_busy": "이미 작업 중입니다."
54
+ "already_busy": "이미 작업 중입니다.",
55
+ "run_pass_next": "다음: /next",
56
+ "run_fail_next": "수정: Esc를 누르거나 이 창을 클릭해 편집한 뒤 /run"
54
57
  }
@@ -15,7 +15,7 @@
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/点击 编辑 | / 命令 | ? 帮助",
19
19
  "hint_code": "Esc 后输入 / 命令",
20
20
  "hint_idle": "/ 命令 | ? 帮助",
21
21
  "help_title": "帮助",
@@ -23,6 +23,7 @@
23
23
  "keys": "按键",
24
24
  "debug_prints": "调试输出",
25
25
  "cmd_run": "评测当前提交",
26
+ "cmd_code": "回到代码编辑器",
26
27
  "cmd_edit": "回到代码编辑器",
27
28
  "cmd_next": "打开下一题",
28
29
  "cmd_prev": "打开上一题",
@@ -50,5 +51,7 @@
50
51
  "update_check_disabled": "更新检查已禁用。",
51
52
  "update_check_failed": "无法检查更新。",
52
53
  "generating_next": "正在生成下一题",
53
- "already_busy": "已有任务正在运行。"
54
+ "already_busy": "已有任务正在运行。",
55
+ "run_pass_next": "下一步: /next",
56
+ "run_fail_next": "修复: 按 Esc 或点击此面板编辑,然后 /run"
54
57
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "practicode",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Local-first coding-test practice in a Rust terminal UI with optional AI help.",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/core.rs CHANGED
@@ -601,8 +601,8 @@ pub fn judge(root: &Path, problem: &Problem, settings: &Settings) -> JudgeResult
601
601
  let run = match run_capture(&mut process, &case.input, Duration::from_secs(5)) {
602
602
  Ok(run) => run,
603
603
  Err(error) => {
604
- lines.push(format!("case {}: FAIL", index + 1));
605
- lines.push(error.to_string());
604
+ lines.push(format!("Case {}: FAIL", index + 1));
605
+ push_labeled_block(&mut lines, "Error", &error.to_string());
606
606
  break;
607
607
  }
608
608
  };
@@ -610,28 +610,20 @@ pub fn judge(root: &Path, problem: &Problem, settings: &Settings) -> JudgeResult
610
610
  let expected = case.output.trim();
611
611
  if !run.timed_out && run.code == Some(0) && got == expected {
612
612
  passed += 1;
613
- lines.push(format!("case {}: PASS", index + 1));
613
+ lines.push(format!("Case {}: PASS", index + 1));
614
614
  if !run.stderr.trim().is_empty() {
615
- lines.push("stderr:".to_string());
616
- lines.push(run.stderr.trim_end().to_string());
615
+ push_labeled_block(&mut lines, "Stderr", run.stderr.trim_end());
617
616
  }
618
617
  } else {
619
- lines.push(format!("case {}: FAIL", index + 1));
618
+ lines.push(format!("Case {}: FAIL", index + 1));
620
619
  if run.timed_out {
621
- lines.push("timeout: 5s".to_string());
620
+ push_labeled_block(&mut lines, "Error", "timeout: 5s");
622
621
  }
623
- lines.push(format!("input: {:?}", case.input));
624
- lines.push(format!("expected: {:?}", expected));
625
- lines.push(format!("got: {:?}", got));
626
- lines.push("stdout:".to_string());
627
- lines.push(if run.stdout.trim_end().is_empty() {
628
- "<empty>".to_string()
629
- } else {
630
- run.stdout.trim_end().to_string()
631
- });
622
+ push_labeled_block(&mut lines, "Input", case.input.trim_end());
623
+ push_labeled_block(&mut lines, "Expected", expected);
624
+ push_labeled_block(&mut lines, "Got", run.stdout.trim_end());
632
625
  if !run.stderr.trim().is_empty() {
633
- lines.push("stderr:".to_string());
634
- lines.push(run.stderr.trim_end().to_string());
626
+ push_labeled_block(&mut lines, "Stderr", run.stderr.trim_end());
635
627
  }
636
628
  break;
637
629
  }
@@ -645,6 +637,16 @@ pub fn judge(root: &Path, problem: &Problem, settings: &Settings) -> JudgeResult
645
637
  }
646
638
  }
647
639
 
640
+ fn push_labeled_block(lines: &mut Vec<String>, label: &str, body: &str) {
641
+ lines.push(String::new());
642
+ lines.push(label.to_string());
643
+ if body.is_empty() {
644
+ lines.push(" <empty>".to_string());
645
+ } else {
646
+ lines.extend(body.lines().map(|line| format!(" {line}")));
647
+ }
648
+ }
649
+
648
650
  pub fn command_for(root: &Path, path: &Path, language: &str) -> Result<Option<CommandSpec>> {
649
651
  match language {
650
652
  "python" => Ok(which("python3")
package/src/lib.rs CHANGED
@@ -1,5 +1,9 @@
1
1
  use anyhow::{Context, Result};
2
- use crossterm::{cursor::SetCursorStyle, execute};
2
+ use crossterm::{
3
+ cursor::SetCursorStyle,
4
+ event::{DisableMouseCapture, EnableMouseCapture},
5
+ execute,
6
+ };
3
7
  use std::{env, io::stdout};
4
8
 
5
9
  pub mod ai;
@@ -25,9 +29,13 @@ pub fn run_cli() -> Result<()> {
25
29
 
26
30
  let mut app = tui::PracticodeApp::new(root)?;
27
31
  let mut terminal = ratatui::init();
28
- let _ = execute!(stdout(), SetCursorStyle::SteadyBar);
32
+ let _ = execute!(stdout(), SetCursorStyle::SteadyBar, EnableMouseCapture);
29
33
  let result = app.run(&mut terminal);
30
34
  ratatui::restore();
31
- let _ = execute!(stdout(), SetCursorStyle::DefaultUserShape);
35
+ let _ = execute!(
36
+ stdout(),
37
+ SetCursorStyle::DefaultUserShape,
38
+ DisableMouseCapture
39
+ );
32
40
  result
33
41
  }
package/src/tui.rs CHANGED
@@ -16,7 +16,10 @@ use crate::{
16
16
  update::{CURRENT_VERSION, UpdateCheck, check_latest_version},
17
17
  };
18
18
  use anyhow::Result;
19
- use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
19
+ use crossterm::event::{
20
+ self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent,
21
+ MouseEventKind,
22
+ };
20
23
  use ratatui::{
21
24
  DefaultTerminal, Frame,
22
25
  layout::{Constraint, Direction, Layout, Position, Rect},
@@ -59,9 +62,9 @@ const COMMAND_HINTS: &[CommandHint] = &[
59
62
  help: true,
60
63
  },
61
64
  CommandHint {
62
- insert: "edit",
63
- display: "/edit",
64
- desc_key: "cmd_edit",
65
+ insert: "code",
66
+ display: "/code",
67
+ desc_key: "cmd_code",
65
68
  keep_open: false,
66
69
  help: true,
67
70
  },
@@ -226,20 +229,6 @@ const COMMAND_HINTS: &[CommandHint] = &[
226
229
  keep_open: false,
227
230
  help: false,
228
231
  },
229
- CommandHint {
230
- insert: "source local",
231
- display: "/source local",
232
- desc_key: "cmd_source",
233
- keep_open: false,
234
- help: false,
235
- },
236
- CommandHint {
237
- insert: "source ai",
238
- display: "/source ai",
239
- desc_key: "cmd_source",
240
- keep_open: false,
241
- help: false,
242
- },
243
232
  CommandHint {
244
233
  insert: "update",
245
234
  display: "/update",
@@ -297,6 +286,9 @@ pub struct PracticodeApp {
297
286
  model_message: Option<String>,
298
287
  update_check: Option<UpdateCheck>,
299
288
  update_notice: Option<String>,
289
+ code_area: Rect,
290
+ output_area: Rect,
291
+ command_area: Rect,
300
292
  should_quit: bool,
301
293
  }
302
294
 
@@ -342,6 +334,9 @@ impl PracticodeApp {
342
334
  model_message: None,
343
335
  update_check: None,
344
336
  update_notice: None,
337
+ code_area: Rect::default(),
338
+ output_area: Rect::default(),
339
+ command_area: Rect::default(),
345
340
  should_quit: false,
346
341
  };
347
342
  app.load_code_editor()?;
@@ -357,11 +352,12 @@ impl PracticodeApp {
357
352
  self.check_update();
358
353
  self.start_model_check();
359
354
  self.check_models();
360
- if event::poll(Duration::from_millis(100))?
361
- && let Event::Key(key) = event::read()?
362
- && key.kind != KeyEventKind::Release
363
- {
364
- self.handle_key(key)?;
355
+ if event::poll(Duration::from_millis(100))? {
356
+ match event::read()? {
357
+ Event::Key(key) if key.kind != KeyEventKind::Release => self.handle_key(key)?,
358
+ Event::Mouse(mouse) => self.handle_mouse(mouse)?,
359
+ _ => {}
360
+ }
365
361
  }
366
362
  if !self.busy_label.is_empty() {
367
363
  self.busy_frame = (self.busy_frame + 1) % 16;
@@ -395,6 +391,16 @@ impl PracticodeApp {
395
391
  self.handle_key(key)
396
392
  }
397
393
 
394
+ pub fn handle_mouse_for_test(&mut self, mouse: MouseEvent) -> Result<()> {
395
+ self.handle_mouse(mouse)
396
+ }
397
+
398
+ pub fn set_pane_areas_for_test(&mut self, code: Rect, output: Rect, command: Rect) {
399
+ self.code_area = code;
400
+ self.output_area = output;
401
+ self.command_area = command;
402
+ }
403
+
398
404
  pub fn busy_label(&self) -> &str {
399
405
  &self.busy_label
400
406
  }
@@ -448,6 +454,9 @@ impl PracticodeApp {
448
454
  .direction(Direction::Horizontal)
449
455
  .constraints([Constraint::Percentage(58), Constraint::Percentage(42)])
450
456
  .split(vertical[0]);
457
+ self.code_area = body[1];
458
+ self.output_area = body[1];
459
+ self.command_area = vertical[2];
451
460
 
452
461
  let problem = Paragraph::new(self.problem_text())
453
462
  .block(Self::block(
@@ -459,13 +468,7 @@ impl PracticodeApp {
459
468
  frame.render_widget(problem, body[0]);
460
469
 
461
470
  if self.show_output {
462
- let text = if !self.busy_label.is_empty() {
463
- format!("{}{}", self.busy_body, self.busy_dots())
464
- } else if self.output_is_markdown {
465
- render_markdown_plain(&self.output)
466
- } else {
467
- self.output.clone()
468
- };
471
+ let text = self.output_text();
469
472
  let output = Paragraph::new(text)
470
473
  .block(Self::block(
471
474
  ui_text(&self.state.settings.ui_language, "output"),
@@ -518,6 +521,79 @@ impl PracticodeApp {
518
521
  self.set_terminal_cursor(frame, body[1], vertical[2]);
519
522
  }
520
523
 
524
+ fn output_text(&self) -> Text<'static> {
525
+ let light = self.state.settings.theme == "light";
526
+ let title_style = if light {
527
+ Style::default()
528
+ .fg(Color::Blue)
529
+ .add_modifier(Modifier::BOLD)
530
+ } else {
531
+ Style::default()
532
+ .fg(Color::Yellow)
533
+ .add_modifier(Modifier::BOLD)
534
+ };
535
+ let label_style = if light {
536
+ Style::default()
537
+ .fg(Color::Magenta)
538
+ .add_modifier(Modifier::BOLD)
539
+ } else {
540
+ Style::default()
541
+ .fg(Color::Cyan)
542
+ .add_modifier(Modifier::BOLD)
543
+ };
544
+ let body_style = if light {
545
+ Style::default().fg(Color::Black)
546
+ } else {
547
+ Style::default().fg(Color::Rgb(229, 231, 235))
548
+ };
549
+ let code_style = if light {
550
+ Style::default()
551
+ .fg(Color::Black)
552
+ .bg(Color::Rgb(229, 231, 235))
553
+ } else {
554
+ Style::default()
555
+ .fg(Color::Rgb(243, 244, 246))
556
+ .bg(Color::Rgb(31, 41, 55))
557
+ };
558
+ if !self.busy_label.is_empty() {
559
+ return Text::from(Line::from(Span::styled(
560
+ format!("{}{}", self.busy_body, self.busy_dots()),
561
+ title_style,
562
+ )));
563
+ }
564
+ let output = if self.output_is_markdown {
565
+ render_markdown_plain(&self.output)
566
+ } else {
567
+ self.output.clone()
568
+ };
569
+ let mut lines = Vec::new();
570
+ for line in output.lines() {
571
+ if line.is_empty() {
572
+ lines.push(Line::default());
573
+ } else if line.starts_with("PASS ")
574
+ || line.starts_with("FAIL ")
575
+ || line.starts_with("Case ")
576
+ || line.starts_with("Next:")
577
+ || line.starts_with("Fix:")
578
+ {
579
+ lines.push(Line::from(Span::styled(line.to_string(), title_style)));
580
+ } else if matches!(
581
+ line,
582
+ "Input" | "Expected" | "Got" | "Stdout" | "Stderr" | "Compile" | "Error"
583
+ ) {
584
+ lines.push(Line::from(Span::styled(line.to_string(), label_style)));
585
+ } else if line.starts_with(" ") {
586
+ lines.push(Line::from(vec![
587
+ Span::raw(" "),
588
+ Span::styled(line.trim_start().to_string(), code_style),
589
+ ]));
590
+ } else {
591
+ lines.push(Line::from(Span::styled(line.to_string(), body_style)));
592
+ }
593
+ }
594
+ Text::from(lines)
595
+ }
596
+
521
597
  fn problem_text(&self) -> Text<'static> {
522
598
  let lang = normalize_ui_language(&self.state.settings.ui_language);
523
599
  let light = self.state.settings.theme == "light";
@@ -732,6 +808,19 @@ impl PracticodeApp {
732
808
  }
733
809
  }
734
810
 
811
+ fn handle_mouse(&mut self, mouse: MouseEvent) -> Result<()> {
812
+ if !matches!(mouse.kind, MouseEventKind::Down(MouseButton::Left)) {
813
+ return Ok(());
814
+ }
815
+ let position = Position::new(mouse.column, mouse.row);
816
+ if self.command_area.contains(position) {
817
+ self.focus_command();
818
+ } else if self.code_area.contains(position) || self.output_area.contains(position) {
819
+ self.action_edit()?;
820
+ }
821
+ Ok(())
822
+ }
823
+
735
824
  fn handle_command_key(&mut self, key: KeyEvent) -> Result<()> {
736
825
  match key.code {
737
826
  KeyCode::Esc => {
@@ -887,7 +976,7 @@ impl PracticodeApp {
887
976
  }
888
977
  match command {
889
978
  "run" | "r" => self.action_run()?,
890
- "edit" | "e" => self.action_edit()?,
979
+ "code" | "edit" | "e" => self.action_edit()?,
891
980
  "next" | "n" => self.action_next(arg)?,
892
981
  "prev" | "previous" | "p" => self.action_previous()?,
893
982
  "giveup" | "give" | "g" => self.action_give_up()?,
@@ -900,12 +989,12 @@ impl PracticodeApp {
900
989
  "theme" if arg.is_empty() => self.action_toggle_theme()?,
901
990
  "theme" if THEMES.contains(&arg) => self.set_theme(arg)?,
902
991
  "source" | "next-source" if arg.is_empty() => {
903
- self.write_text_output(&format!("Next source: {}", self.next_source_label()));
992
+ self.write_text_output(&self.next_source_help());
904
993
  }
905
994
  "source" | "next-source" if matches!(arg, "bank" | "local" | "ai") => {
906
995
  self.state.settings.next_source = normalize_next_source(arg);
907
996
  save_state(&self.root, &self.state)?;
908
- self.write_text_output(&format!("Next source: {}", self.next_source_label()));
997
+ self.write_text_output(&self.next_source_help());
909
998
  }
910
999
  "ai-next-command" if !arg.is_empty() => {
911
1000
  self.state.settings.ai_next_command = arg.to_string();
@@ -982,9 +1071,9 @@ impl PracticodeApp {
982
1071
  result.total_cases
983
1072
  );
984
1073
  let next_step = if result.passed {
985
- "Next: /next"
1074
+ ui_text(&self.state.settings.ui_language, "run_pass_next")
986
1075
  } else {
987
- "Fix code, then /run"
1076
+ ui_text(&self.state.settings.ui_language, "run_fail_next")
988
1077
  };
989
1078
  self.write_text_output(&format!("{headline}\n{}\n\n{next_step}", result.output));
990
1079
  Ok(())
@@ -1655,11 +1744,11 @@ impl PracticodeApp {
1655
1744
  )
1656
1745
  }
1657
1746
 
1658
- fn next_source_label(&self) -> &'static str {
1747
+ fn next_source_help(&self) -> String {
1659
1748
  if self.state.settings.next_source == "ai" {
1660
- "ai"
1749
+ "Next behavior: plain /next asks AI every time. Use /source local to use local problems first.".to_string()
1661
1750
  } else {
1662
- "local"
1751
+ "Next behavior: /next uses local problems first and asks AI when it needs a new one. Use /next <request> to ask AI directly.".to_string()
1663
1752
  }
1664
1753
  }
1665
1754