Skip to content

Commit 3fa1d39

Browse files
committed
feat: Complete initial REPL implementation with integrated ratatui and crossterm; update TASK.md to reflect progress and add notes on built-in commands
1 parent d039bff commit 3fa1d39

5 files changed

Lines changed: 417 additions & 59 deletions

File tree

TASK.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,15 @@ default_model = "llama-3.3-70b-versatile"
9999

100100
**Goal:** *Basic REPL* PRD: `ratatui` + `crossterm`, log berpikir, perintah built-in awal.
101101

102-
- [ ] Integrasi `ratatui` + `crossterm`
103-
- [ ] Render *thinking log* + output streaming (sesuai gaya *Terminal UX* PRD)
104-
- [ ] Riwayat input + state sesi in-memory
105-
- [ ] Perintah built-in minimal: `/cost`, `/memory`, `/doctor` (sesuai tabel *Built-in Commands* PRD)
102+
- [x] Integrasi `ratatui` + `crossterm`
103+
- [x] Render *thinking log* + output streaming (sesuai gaya *Terminal UX* PRD)
104+
- [x] Riwayat input + state sesi in-memory
105+
- [x] Perintah built-in minimal: `/cost`, `/memory`, `/doctor` (sesuai tabel *Built-in Commands* PRD)
106106

107107
**Definition of Done:** REPL bisa sesi percakapan singkat dengan log dan tiga perintah di atas.
108108

109+
**Catatan:** `/cost` dan `/memory` berupa stub/jelasan tier sesuai PRD (pelacakan biaya nyata di Sprint 14+; persistensi memori di Sprint 6–7). `/doctor` memakai `doctor::report_lines` yang sama dengan subcommand `cantrik doctor`.
110+
109111
---
110112

111113
## Sprint 5 — Codebase intelligence: AST & indexing (Phase 1)

crates/cantrik-cli/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,6 @@ path = "src/main.rs"
1212
cantrik-core = { path = "../cantrik-core" }
1313
clap.workspace = true
1414
clap_complete.workspace = true
15+
crossterm = "0.29.0"
16+
ratatui = { version = "0.30.0", features = ["crossterm"] }
1517
tokio.workspace = true

crates/cantrik-cli/src/commands/doctor.rs

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,20 @@ use std::process::ExitCode;
44
use cantrik_core::config::{load_merged_config, resolve_config_paths};
55
use cantrik_core::llm::{ProviderKind, load_providers_toml, providers_toml_path, resolve_api_key};
66

7-
pub(crate) fn run(cwd: &Path) -> ExitCode {
7+
/// Lines for `cantrik doctor` and REPL `/doctor` (same content, no secrets).
8+
pub(crate) fn report_lines(cwd: &Path) -> Vec<String> {
9+
let mut lines = Vec::new();
810
let paths = resolve_config_paths(cwd);
911
let providers_path = providers_toml_path();
1012

11-
println!("doctor: Cantrik {}", env!("CARGO_PKG_VERSION"));
12-
println!(" global config : {}", paths.global.display());
13-
println!(" project config: {}", paths.project.display());
14-
println!(" providers.toml: {}", providers_path.display());
13+
lines.push(format!("doctor: Cantrik {}", env!("CARGO_PKG_VERSION")));
14+
lines.push(format!(" global config : {}", paths.global.display()));
15+
lines.push(format!(" project config: {}", paths.project.display()));
16+
lines.push(format!(" providers.toml: {}", providers_path.display()));
1517

1618
match load_providers_toml(&providers_path) {
1719
Ok(prov) => {
18-
println!(" providers load: OK");
20+
lines.push(" providers load: OK".to_string());
1921
for kind in ProviderKind::ALL {
2022
let status = match kind {
2123
ProviderKind::Ollama => "local (no API key required)",
@@ -39,29 +41,45 @@ pub(crate) fn run(cwd: &Path) -> ExitCode {
3941
}
4042
}
4143
};
42-
println!(" - {}: {status}", kind.as_str());
44+
lines.push(format!(" - {}: {status}", kind.as_str()));
4345
}
4446
}
45-
Err(e) => println!(" providers: {e}"),
47+
Err(e) => lines.push(format!(" providers: {e}")),
4648
}
4749

4850
match load_merged_config(cwd) {
4951
Ok(config) => {
50-
println!(" config load: OK");
52+
lines.push(" config load: OK".to_string());
5153
if let Some(lang) = config.ui.language.as_deref() {
52-
println!(" ui.language : {lang}");
54+
lines.push(format!(" ui.language : {lang}"));
5355
}
5456
if let Some(p) = config.llm.provider.as_deref() {
55-
println!(" llm.provider : {p}");
57+
lines.push(format!(" llm.provider : {p}"));
5658
}
5759
if let Some(m) = config.llm.model.as_deref() {
58-
println!(" llm.model : {m}");
60+
lines.push(format!(" llm.model : {m}"));
5961
}
60-
ExitCode::SUCCESS
6162
}
62-
Err(error) => {
63-
eprintln!(" config load: FAILED — {error}");
64-
ExitCode::from(1)
63+
Err(error) => lines.push(format!(" config load: FAILED — {error}")),
64+
}
65+
66+
lines
67+
}
68+
69+
pub(crate) fn run(cwd: &Path) -> ExitCode {
70+
let lines = report_lines(cwd);
71+
let mut config_failed = false;
72+
for line in &lines {
73+
if line.contains("config load: FAILED") {
74+
config_failed = true;
75+
eprintln!("{line}");
76+
} else {
77+
println!("{line}");
6578
}
6679
}
80+
if config_failed {
81+
ExitCode::from(1)
82+
} else {
83+
ExitCode::SUCCESS
84+
}
6785
}

crates/cantrik-cli/src/lib.rs

Lines changed: 19 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
33
mod cli;
44
mod commands;
5+
mod repl;
56

6-
use std::io::{self, BufRead, IsTerminal, Read, Write};
7+
use std::io::{self, IsTerminal, Read};
78
use std::path::Path;
89
use std::process::ExitCode;
910

@@ -115,7 +116,7 @@ pub async fn run() -> ExitCode {
115116
}
116117

117118
if io::stdin().is_terminal() {
118-
repl_placeholder().await
119+
repl_run(cwd).await
119120
} else {
120121
stdin_pipe_ask(&cwd).await
121122
}
@@ -174,47 +175,26 @@ async fn stdin_pipe_ask(cwd: &Path) -> ExitCode {
174175
commands::ask::run(&config, &text).await
175176
}
176177

177-
async fn repl_placeholder() -> ExitCode {
178-
let result = tokio::task::spawn_blocking(repl_sync).await;
179-
match result {
180-
Ok(code) => code,
181-
Err(e) => {
182-
eprintln!("REPL task failed: {e}");
183-
ExitCode::FAILURE
184-
}
185-
}
186-
}
187-
188-
fn repl_sync() -> ExitCode {
189-
println!(
190-
"Cantrik REPL (placeholder). Type 'exit' or 'quit', or Ctrl+D to leave. Full TUI arrives in Sprint 4."
191-
);
192-
let stdin = io::stdin();
193-
let mut line = String::new();
194-
loop {
195-
line.clear();
196-
print!("cantrik> ");
197-
let _ = io::stdout().flush();
198-
let n = match stdin.lock().read_line(&mut line) {
199-
Ok(n) => n,
200-
Err(error) => {
201-
eprintln!("failed to read line: {error}");
202-
return ExitCode::FAILURE;
203-
}
204-
};
205-
if n == 0 {
206-
println!();
207-
break;
178+
async fn repl_run(cwd: std::path::PathBuf) -> ExitCode {
179+
let config = match load_merged_config(&cwd) {
180+
Ok(config) => config,
181+
Err(error) => {
182+
eprintln!("failed to load config: {error}");
183+
return ExitCode::FAILURE;
208184
}
209-
let trimmed = line.trim();
210-
if trimmed.eq_ignore_ascii_case("exit") || trimmed.eq_ignore_ascii_case("quit") {
211-
break;
185+
};
186+
let handle = tokio::runtime::Handle::current();
187+
match tokio::task::spawn_blocking(move || repl::run_sync(cwd, config, handle)).await {
188+
Ok(Ok(())) => ExitCode::SUCCESS,
189+
Ok(Err(error)) => {
190+
eprintln!("REPL exited with error: {error}");
191+
ExitCode::FAILURE
212192
}
213-
if !trimmed.is_empty() {
214-
println!("(REPL placeholder) you said: {trimmed}");
193+
Err(join_error) => {
194+
eprintln!("REPL task failed: {join_error}");
195+
ExitCode::FAILURE
215196
}
216197
}
217-
ExitCode::SUCCESS
218198
}
219199

220200
#[cfg(test)]

0 commit comments

Comments
 (0)