anveesa 0.7.0 → 0.7.2
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/bin/anveesa +12 -0
- package/install.js +92 -0
- package/package.json +22 -35
- package/Cargo.lock +0 -2250
- package/Cargo.toml +0 -30
- package/LICENSE +0 -21
- package/bin/anveesa.js +0 -50
- package/scripts/install.js +0 -203
- package/src/cli.rs +0 -126
- package/src/config.rs +0 -743
- package/src/display.rs +0 -774
- package/src/image.rs +0 -340
- package/src/lib.rs +0 -710
- package/src/main.rs +0 -4
- package/src/mcp.rs +0 -243
- package/src/prompt.rs +0 -620
- package/src/provider/command.rs +0 -306
- package/src/provider/mod.rs +0 -210
- package/src/provider/openai_compatible.rs +0 -1527
- package/src/session.rs +0 -501
- package/src/tools.rs +0 -2359
- package/src/tools_scenarios.rs +0 -1693
- package/src/tui/commands.rs +0 -404
- package/src/tui/format.rs +0 -300
- package/src/tui/input.rs +0 -153
- package/src/tui/render.rs +0 -574
- package/src/tui/stream.rs +0 -336
- package/src/tui.rs +0 -641
- package/src/web.rs +0 -179
- package/src/web_ui.html +0 -213
- package/src/workspace.rs +0 -210
package/src/display.rs
DELETED
|
@@ -1,774 +0,0 @@
|
|
|
1
|
-
use std::{
|
|
2
|
-
io::{self, IsTerminal, Write},
|
|
3
|
-
path::Path,
|
|
4
|
-
time::{Duration, Instant},
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
use tokio::sync::mpsc;
|
|
8
|
-
|
|
9
|
-
use crate::{
|
|
10
|
-
provider::{ApprovalDecision, DiffKind, StreamEvent, ToolConfirmPreview, Usage},
|
|
11
|
-
session::format_session_age,
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
use crate::RenderMode;
|
|
15
|
-
|
|
16
|
-
pub async fn render_stream(
|
|
17
|
-
mut rx: mpsc::UnboundedReceiver<StreamEvent>,
|
|
18
|
-
mode: RenderMode,
|
|
19
|
-
started: Instant,
|
|
20
|
-
) {
|
|
21
|
-
let spinner = io::stderr().is_terminal();
|
|
22
|
-
let mut frame = 0usize;
|
|
23
|
-
// True only when the 2-line spinner is currently painted on screen.
|
|
24
|
-
// Used by clear_spinner to avoid wiping lines that belong to the response text.
|
|
25
|
-
let mut spinner_active = false;
|
|
26
|
-
let mut first_token = true;
|
|
27
|
-
let mut produced = false;
|
|
28
|
-
let mut line_open = false;
|
|
29
|
-
let mut usage: Option<Usage> = None;
|
|
30
|
-
let mut plan_tasks: Vec<String> = vec![];
|
|
31
|
-
let mut plan_done: Vec<bool> = vec![];
|
|
32
|
-
let mut status_message = "Waiting for response".to_string();
|
|
33
|
-
|
|
34
|
-
static TIPS: &[&str] = &[
|
|
35
|
-
"/clear reset context",
|
|
36
|
-
"/attach clipboard image",
|
|
37
|
-
"/exit leave session",
|
|
38
|
-
"--yes auto-approve edits",
|
|
39
|
-
];
|
|
40
|
-
|
|
41
|
-
loop {
|
|
42
|
-
tokio::select! {
|
|
43
|
-
maybe = rx.recv() => match maybe {
|
|
44
|
-
Some(StreamEvent::Status { message }) => {
|
|
45
|
-
clear_spinner(spinner, spinner_active);
|
|
46
|
-
spinner_active = false;
|
|
47
|
-
if line_open {
|
|
48
|
-
println!();
|
|
49
|
-
line_open = false;
|
|
50
|
-
}
|
|
51
|
-
status_message = message;
|
|
52
|
-
print_status(&status_message, spinner);
|
|
53
|
-
first_token = true;
|
|
54
|
-
frame = 0;
|
|
55
|
-
}
|
|
56
|
-
Some(StreamEvent::Token(text)) => {
|
|
57
|
-
if first_token {
|
|
58
|
-
clear_spinner(spinner, spinner_active);
|
|
59
|
-
spinner_active = false;
|
|
60
|
-
if matches!(mode, RenderMode::Interactive) {
|
|
61
|
-
print_assistant_header(started);
|
|
62
|
-
}
|
|
63
|
-
first_token = false;
|
|
64
|
-
}
|
|
65
|
-
produced = true;
|
|
66
|
-
line_open = true;
|
|
67
|
-
print!("{text}");
|
|
68
|
-
let _ = io::stdout().flush();
|
|
69
|
-
}
|
|
70
|
-
Some(StreamEvent::Usage(value)) => usage = Some(value),
|
|
71
|
-
Some(StreamEvent::ToolCall { summary }) => {
|
|
72
|
-
clear_spinner(spinner, spinner_active);
|
|
73
|
-
spinner_active = false;
|
|
74
|
-
if line_open {
|
|
75
|
-
println!();
|
|
76
|
-
line_open = false;
|
|
77
|
-
}
|
|
78
|
-
status_message = format!("Running {summary}");
|
|
79
|
-
print_tool_call(&summary, spinner);
|
|
80
|
-
first_token = true;
|
|
81
|
-
frame = 0;
|
|
82
|
-
}
|
|
83
|
-
Some(StreamEvent::ToolResult { summary, ok, elapsed_ms, error }) => {
|
|
84
|
-
clear_spinner(spinner, spinner_active);
|
|
85
|
-
spinner_active = false;
|
|
86
|
-
if line_open {
|
|
87
|
-
println!();
|
|
88
|
-
line_open = false;
|
|
89
|
-
}
|
|
90
|
-
print_tool_result(&summary, ok, elapsed_ms, error.as_deref(), spinner);
|
|
91
|
-
status_message = if ok {
|
|
92
|
-
"Continuing".to_string()
|
|
93
|
-
} else {
|
|
94
|
-
"Handling tool error".to_string()
|
|
95
|
-
};
|
|
96
|
-
first_token = true;
|
|
97
|
-
frame = 0;
|
|
98
|
-
}
|
|
99
|
-
Some(StreamEvent::Confirm { preview, reply }) => {
|
|
100
|
-
clear_spinner(spinner, spinner_active);
|
|
101
|
-
spinner_active = false;
|
|
102
|
-
if line_open {
|
|
103
|
-
println!();
|
|
104
|
-
line_open = false;
|
|
105
|
-
}
|
|
106
|
-
let decision = tokio::task::block_in_place(|| {
|
|
107
|
-
show_confirm_preview(&preview, spinner);
|
|
108
|
-
prompt_confirm_decision(spinner)
|
|
109
|
-
});
|
|
110
|
-
match decision {
|
|
111
|
-
ApprovalDecision::AllowOnce => {
|
|
112
|
-
print_status("Applying action", spinner);
|
|
113
|
-
status_message = "Applying action".to_string();
|
|
114
|
-
}
|
|
115
|
-
ApprovalDecision::AllowForTurn => {
|
|
116
|
-
print_status("Applying action (all approved for this turn)", spinner);
|
|
117
|
-
status_message = "Applying action".to_string();
|
|
118
|
-
}
|
|
119
|
-
ApprovalDecision::Deny => {
|
|
120
|
-
print_status("Action declined", spinner);
|
|
121
|
-
status_message = "Continuing".to_string();
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
let _ = reply.send(decision);
|
|
125
|
-
// Re-arm the spinner for the next API round.
|
|
126
|
-
first_token = true;
|
|
127
|
-
frame = 0;
|
|
128
|
-
}
|
|
129
|
-
Some(StreamEvent::FileOp { verb, path, added, removed, preview, truncated }) => {
|
|
130
|
-
clear_spinner(spinner, spinner_active);
|
|
131
|
-
spinner_active = false;
|
|
132
|
-
if line_open {
|
|
133
|
-
println!();
|
|
134
|
-
line_open = false;
|
|
135
|
-
}
|
|
136
|
-
print_file_op(&verb, &path, added, removed, &preview, truncated, spinner);
|
|
137
|
-
// Re-arm the spinner for the next API round.
|
|
138
|
-
first_token = true;
|
|
139
|
-
frame = 0;
|
|
140
|
-
}
|
|
141
|
-
Some(StreamEvent::PlanSet { tasks }) => {
|
|
142
|
-
clear_spinner(spinner, spinner_active);
|
|
143
|
-
spinner_active = false;
|
|
144
|
-
if line_open {
|
|
145
|
-
println!();
|
|
146
|
-
line_open = false;
|
|
147
|
-
}
|
|
148
|
-
plan_done = vec![false; tasks.len()];
|
|
149
|
-
plan_tasks = tasks;
|
|
150
|
-
print_plan_list(&plan_tasks, &plan_done, spinner);
|
|
151
|
-
first_token = true;
|
|
152
|
-
frame = 0;
|
|
153
|
-
}
|
|
154
|
-
Some(StreamEvent::PlanTaskDone { index }) => {
|
|
155
|
-
clear_spinner(spinner, spinner_active);
|
|
156
|
-
spinner_active = false;
|
|
157
|
-
if line_open {
|
|
158
|
-
println!();
|
|
159
|
-
line_open = false;
|
|
160
|
-
}
|
|
161
|
-
if index < plan_done.len() {
|
|
162
|
-
plan_done[index] = true;
|
|
163
|
-
}
|
|
164
|
-
print_plan_list(&plan_tasks, &plan_done, spinner);
|
|
165
|
-
first_token = true;
|
|
166
|
-
frame = 0;
|
|
167
|
-
}
|
|
168
|
-
Some(StreamEvent::Thinking(_)) => {} // thinking blocks shown in TUI only
|
|
169
|
-
None => break,
|
|
170
|
-
},
|
|
171
|
-
// 100 ms tick
|
|
172
|
-
_ = tokio::time::sleep(Duration::from_millis(100)), if first_token && spinner => {
|
|
173
|
-
let elapsed = started.elapsed().as_secs_f32();
|
|
174
|
-
let time_str = format_elapsed(elapsed);
|
|
175
|
-
// Dots cycle: "" → "." → ".." → "…" (every 3 frames ≈ 300 ms)
|
|
176
|
-
let dots = ["", ".", "..", "…"][frame % 4];
|
|
177
|
-
// Tip rotates every 40 frames (~4 s)
|
|
178
|
-
let tip = TIPS[(frame / 40) % TIPS.len()];
|
|
179
|
-
let status = truncate_for_status(&status_message, 76);
|
|
180
|
-
|
|
181
|
-
if !spinner_active {
|
|
182
|
-
// First paint — just print 2 lines (no overwrite needed).
|
|
183
|
-
eprint!(
|
|
184
|
-
"\x1b[1;32m+\x1b[0m {status}{dots} \x1b[2m({time_str})\x1b[0m\n \x1b[90m└\x1b[0m \x1b[2m{tip}\x1b[0m"
|
|
185
|
-
);
|
|
186
|
-
spinner_active = true;
|
|
187
|
-
} else {
|
|
188
|
-
// Overwrite: move up 1 line, clear both lines, reprint.
|
|
189
|
-
eprint!(
|
|
190
|
-
"\r\x1b[2K\x1b[1A\x1b[2K\r\x1b[1;32m+\x1b[0m {status}{dots} \x1b[2m({time_str})\x1b[0m\n \x1b[90m└\x1b[0m \x1b[2m{tip}\x1b[0m"
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
let _ = io::stderr().flush();
|
|
194
|
-
frame += 1;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if produced && line_open {
|
|
200
|
-
println!();
|
|
201
|
-
} else {
|
|
202
|
-
clear_spinner(spinner, spinner_active);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if spinner
|
|
206
|
-
&& let Some(usage) = usage
|
|
207
|
-
&& usage.total_tokens > 0
|
|
208
|
-
{
|
|
209
|
-
if usage.cache_read_tokens > 0 || usage.cache_write_tokens > 0 {
|
|
210
|
-
eprintln!(
|
|
211
|
-
"\x1b[2m {} in · {} out · {} total (cache: {} hit · {} write)\x1b[0m",
|
|
212
|
-
usage.prompt_tokens,
|
|
213
|
-
usage.completion_tokens,
|
|
214
|
-
usage.total_tokens,
|
|
215
|
-
usage.cache_read_tokens,
|
|
216
|
-
usage.cache_write_tokens,
|
|
217
|
-
);
|
|
218
|
-
} else {
|
|
219
|
-
eprintln!(
|
|
220
|
-
"\x1b[2m {} in · {} out · {} total\x1b[0m",
|
|
221
|
-
usage.prompt_tokens, usage.completion_tokens, usage.total_tokens
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
pub fn print_tool_call(summary: &str, is_tty: bool) {
|
|
228
|
-
if is_tty {
|
|
229
|
-
eprintln!("\x1b[90m └─ {summary}\x1b[0m");
|
|
230
|
-
} else {
|
|
231
|
-
eprintln!("tool: {summary}");
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
pub fn print_status(message: &str, is_tty: bool) {
|
|
236
|
-
if is_tty {
|
|
237
|
-
eprintln!("\x1b[90m · {message}\x1b[0m");
|
|
238
|
-
} else {
|
|
239
|
-
eprintln!("status: {message}");
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
pub fn print_tool_result(summary: &str, ok: bool, elapsed_ms: u128, error: Option<&str>, is_tty: bool) {
|
|
244
|
-
let elapsed = format_duration_ms(elapsed_ms);
|
|
245
|
-
if is_tty {
|
|
246
|
-
if ok {
|
|
247
|
-
eprintln!("\x1b[1;32m ✓\x1b[0m \x1b[90m{summary} completed in {elapsed}\x1b[0m");
|
|
248
|
-
} else if let Some(error) = error {
|
|
249
|
-
eprintln!("\x1b[1;31m ✗\x1b[0m \x1b[90m{summary} failed in {elapsed}: {error}\x1b[0m");
|
|
250
|
-
} else {
|
|
251
|
-
eprintln!("\x1b[1;31m ✗\x1b[0m \x1b[90m{summary} failed in {elapsed}\x1b[0m");
|
|
252
|
-
}
|
|
253
|
-
} else if ok {
|
|
254
|
-
eprintln!("tool ok: {summary} ({elapsed})");
|
|
255
|
-
} else if let Some(error) = error {
|
|
256
|
-
eprintln!("tool failed: {summary} ({elapsed}): {error}");
|
|
257
|
-
} else {
|
|
258
|
-
eprintln!("tool failed: {summary} ({elapsed})");
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
pub fn print_file_op(
|
|
263
|
-
verb: &str,
|
|
264
|
-
path: &str,
|
|
265
|
-
added: usize,
|
|
266
|
-
removed: usize,
|
|
267
|
-
preview: &[crate::provider::DiffLine],
|
|
268
|
-
truncated: bool,
|
|
269
|
-
is_tty: bool,
|
|
270
|
-
) {
|
|
271
|
-
if !is_tty {
|
|
272
|
-
println!("{verb}({path}): +{added} -{removed}");
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Shorten path relative to cwd when possible
|
|
277
|
-
let display_path = std::env::current_dir()
|
|
278
|
-
.ok()
|
|
279
|
-
.and_then(|cwd| {
|
|
280
|
-
let abs = std::path::Path::new(path);
|
|
281
|
-
abs.strip_prefix(&cwd).ok().map(|r| r.display().to_string())
|
|
282
|
-
})
|
|
283
|
-
.unwrap_or_else(|| path.to_string());
|
|
284
|
-
|
|
285
|
-
// Header: ● Update(src/lib.rs)
|
|
286
|
-
println!("\n\x1b[1;32m●\x1b[0m \x1b[1;32m{verb}\x1b[0m\x1b[2m({display_path})\x1b[0m");
|
|
287
|
-
|
|
288
|
-
// Summary: └ Added N lines, removed M lines
|
|
289
|
-
let summary = match (added, removed) {
|
|
290
|
-
(a, 0) if a == 0 => String::new(),
|
|
291
|
-
(a, 0) => format!("Added {} {}", a, if a == 1 { "line" } else { "lines" }),
|
|
292
|
-
(0, r) => format!("Removed {} {}", r, if r == 1 { "line" } else { "lines" }),
|
|
293
|
-
(a, r) => format!(
|
|
294
|
-
"Added {} {}, removed {} {}",
|
|
295
|
-
a,
|
|
296
|
-
if a == 1 { "line" } else { "lines" },
|
|
297
|
-
r,
|
|
298
|
-
if r == 1 { "line" } else { "lines" }
|
|
299
|
-
),
|
|
300
|
-
};
|
|
301
|
-
if !summary.is_empty() {
|
|
302
|
-
println!("\x1b[90m └\x1b[0m \x1b[2m{summary}\x1b[0m");
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Diff lines with colored backgrounds
|
|
306
|
-
for dl in preview {
|
|
307
|
-
let (bg, fg, prefix) = match dl.kind {
|
|
308
|
-
DiffKind::Add => ("\x1b[48;5;22m", "\x1b[92m", "+"),
|
|
309
|
-
DiffKind::Remove => ("\x1b[48;5;52m", "\x1b[91m", "-"),
|
|
310
|
-
};
|
|
311
|
-
// \x1b[K fills the remainder of the line with the current background colour
|
|
312
|
-
println!(
|
|
313
|
-
"{bg}\x1b[90m {:4} {fg}{prefix} {}\x1b[K\x1b[0m",
|
|
314
|
-
dl.line_no, dl.text
|
|
315
|
-
);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if truncated {
|
|
319
|
-
println!("\x1b[90m … (preview truncated)\x1b[0m");
|
|
320
|
-
}
|
|
321
|
-
println!();
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
pub fn print_plan_list(tasks: &[String], done: &[bool], is_tty: bool) {
|
|
325
|
-
eprintln!();
|
|
326
|
-
for (i, task) in tasks.iter().enumerate() {
|
|
327
|
-
let is_done = done.get(i).copied().unwrap_or(false);
|
|
328
|
-
if is_tty {
|
|
329
|
-
if is_done {
|
|
330
|
-
eprintln!("\x1b[1;32m[✓]\x1b[0m \x1b[2m{task}\x1b[0m");
|
|
331
|
-
} else {
|
|
332
|
-
eprintln!("\x1b[90m[ ]\x1b[0m {task}");
|
|
333
|
-
}
|
|
334
|
-
} else {
|
|
335
|
-
eprintln!("[{}] {task}", if is_done { "✓" } else { " " });
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
eprintln!();
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
pub fn print_assistant_header(started: Instant) {
|
|
342
|
-
let secs = started.elapsed().as_secs_f32();
|
|
343
|
-
println!();
|
|
344
|
-
if io::stdout().is_terminal() {
|
|
345
|
-
println!("\x1b[1;32m❯\x1b[0m \x1b[2m{secs:.1}s\x1b[0m");
|
|
346
|
-
} else {
|
|
347
|
-
println!("({secs:.1}s)");
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
pub fn clear_spinner(enabled: bool, active: bool) {
|
|
352
|
-
if !enabled || !active {
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
// Clear the tip line, move up, clear the status line, return to column 0.
|
|
356
|
-
eprint!("\r\x1b[2K\x1b[1A\x1b[2K\r");
|
|
357
|
-
let _ = io::stderr().flush();
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
pub fn format_elapsed(secs: f32) -> String {
|
|
361
|
-
let s = secs as u64;
|
|
362
|
-
if s >= 60 {
|
|
363
|
-
format!("{}m {}s", s / 60, s % 60)
|
|
364
|
-
} else {
|
|
365
|
-
format!("{s}s")
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
pub fn format_duration_ms(ms: u128) -> String {
|
|
370
|
-
if ms >= 1000 {
|
|
371
|
-
format!("{:.1}s", ms as f64 / 1000.0)
|
|
372
|
-
} else {
|
|
373
|
-
format!("{ms}ms")
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
pub fn truncate_for_status(value: &str, max_chars: usize) -> String {
|
|
378
|
-
let mut chars = value.chars();
|
|
379
|
-
let mut output = String::new();
|
|
380
|
-
for _ in 0..max_chars {
|
|
381
|
-
let Some(ch) = chars.next() else {
|
|
382
|
-
return output;
|
|
383
|
-
};
|
|
384
|
-
output.push(ch);
|
|
385
|
-
}
|
|
386
|
-
if chars.next().is_some() {
|
|
387
|
-
output.push('…');
|
|
388
|
-
}
|
|
389
|
-
output
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
pub fn show_confirm_preview(preview: &ToolConfirmPreview, is_tty: bool) {
|
|
393
|
-
match preview {
|
|
394
|
-
ToolConfirmPreview::FileOp {
|
|
395
|
-
verb,
|
|
396
|
-
path,
|
|
397
|
-
added,
|
|
398
|
-
removed,
|
|
399
|
-
diff,
|
|
400
|
-
truncated,
|
|
401
|
-
} => {
|
|
402
|
-
eprint_file_op(verb, path, *added, *removed, diff, *truncated, is_tty);
|
|
403
|
-
}
|
|
404
|
-
ToolConfirmPreview::CreateDir { path } => {
|
|
405
|
-
if is_tty {
|
|
406
|
-
eprintln!(
|
|
407
|
-
"\n\x1b[1;32m●\x1b[0m \x1b[1;32mCreate dir\x1b[0m\x1b[2m({path})\x1b[0m\n"
|
|
408
|
-
);
|
|
409
|
-
} else {
|
|
410
|
-
eprintln!("Create dir: {path}");
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
ToolConfirmPreview::Generic { summary } => {
|
|
414
|
-
if is_tty {
|
|
415
|
-
eprintln!("\n\x1b[1;32m●\x1b[0m \x1b[1;32m{summary}\x1b[0m\n");
|
|
416
|
-
} else {
|
|
417
|
-
eprintln!("{summary}");
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
/// Like `print_file_op` but writes to stderr — used for pre-approval previews so
|
|
424
|
-
/// the diff, the spinner clear, and the approval prompt all share the same stream.
|
|
425
|
-
pub fn eprint_file_op(
|
|
426
|
-
verb: &str,
|
|
427
|
-
path: &str,
|
|
428
|
-
added: usize,
|
|
429
|
-
removed: usize,
|
|
430
|
-
diff: &[crate::provider::DiffLine],
|
|
431
|
-
truncated: bool,
|
|
432
|
-
is_tty: bool,
|
|
433
|
-
) {
|
|
434
|
-
if !is_tty {
|
|
435
|
-
eprintln!("{verb}({path}): +{added} -{removed}");
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
let display_path = std::env::current_dir()
|
|
440
|
-
.ok()
|
|
441
|
-
.and_then(|cwd| {
|
|
442
|
-
std::path::Path::new(path)
|
|
443
|
-
.strip_prefix(&cwd)
|
|
444
|
-
.ok()
|
|
445
|
-
.map(|r| r.display().to_string())
|
|
446
|
-
})
|
|
447
|
-
.unwrap_or_else(|| path.to_string());
|
|
448
|
-
|
|
449
|
-
eprintln!("\n\x1b[1;32m●\x1b[0m \x1b[1;32m{verb}\x1b[0m\x1b[2m({display_path})\x1b[0m");
|
|
450
|
-
|
|
451
|
-
let summary = match (added, removed) {
|
|
452
|
-
(0, 0) => String::new(),
|
|
453
|
-
(a, 0) => format!("Added {} {}", a, if a == 1 { "line" } else { "lines" }),
|
|
454
|
-
(0, r) => format!("Removed {} {}", r, if r == 1 { "line" } else { "lines" }),
|
|
455
|
-
(a, r) => format!(
|
|
456
|
-
"Added {} {}, removed {} {}",
|
|
457
|
-
a,
|
|
458
|
-
if a == 1 { "line" } else { "lines" },
|
|
459
|
-
r,
|
|
460
|
-
if r == 1 { "line" } else { "lines" }
|
|
461
|
-
),
|
|
462
|
-
};
|
|
463
|
-
if !summary.is_empty() {
|
|
464
|
-
eprintln!("\x1b[90m └\x1b[0m \x1b[2m{summary}\x1b[0m");
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
for dl in diff {
|
|
468
|
-
let (bg, fg, prefix) = match dl.kind {
|
|
469
|
-
DiffKind::Add => ("\x1b[48;5;22m", "\x1b[92m", "+"),
|
|
470
|
-
DiffKind::Remove => ("\x1b[48;5;52m", "\x1b[91m", "-"),
|
|
471
|
-
};
|
|
472
|
-
eprintln!(
|
|
473
|
-
"{bg}\x1b[90m {:4} {fg}{prefix} {}\x1b[K\x1b[0m",
|
|
474
|
-
dl.line_no, dl.text
|
|
475
|
-
);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
if truncated {
|
|
479
|
-
eprintln!("\x1b[90m … (preview truncated)\x1b[0m");
|
|
480
|
-
}
|
|
481
|
-
eprintln!();
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
pub fn prompt_confirm_decision(is_tty: bool) -> ApprovalDecision {
|
|
485
|
-
let mut err = io::stderr();
|
|
486
|
-
if is_tty {
|
|
487
|
-
let _ = write!(
|
|
488
|
-
err,
|
|
489
|
-
"\x1b[1;32m❯\x1b[0m Apply? \x1b[2m[y]es / [a]ll this turn / [N]o\x1b[0m "
|
|
490
|
-
);
|
|
491
|
-
} else {
|
|
492
|
-
let _ = write!(err, "Apply? [y]es/[a]ll this turn/[N]o ");
|
|
493
|
-
}
|
|
494
|
-
let _ = err.flush();
|
|
495
|
-
|
|
496
|
-
let mut answer = String::new();
|
|
497
|
-
if io::stdin().read_line(&mut answer).is_err() {
|
|
498
|
-
return ApprovalDecision::Deny;
|
|
499
|
-
}
|
|
500
|
-
match answer.trim().to_lowercase().as_str() {
|
|
501
|
-
"y" | "yes" => ApprovalDecision::AllowOnce,
|
|
502
|
-
"a" | "all" => ApprovalDecision::AllowForTurn,
|
|
503
|
-
_ => ApprovalDecision::Deny,
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
pub fn print_status_inline(
|
|
508
|
-
is_tty: bool,
|
|
509
|
-
provider: &str,
|
|
510
|
-
model: Option<&str>,
|
|
511
|
-
cwd: &std::path::Path,
|
|
512
|
-
turns: usize,
|
|
513
|
-
usage: &Usage,
|
|
514
|
-
) {
|
|
515
|
-
let model_display = model.unwrap_or("(default)");
|
|
516
|
-
let short_cwd = std::env::var("HOME")
|
|
517
|
-
.map(|h| cwd.display().to_string().replacen(&h, "~", 1))
|
|
518
|
-
.unwrap_or_else(|_| cwd.display().to_string());
|
|
519
|
-
|
|
520
|
-
if !is_tty {
|
|
521
|
-
println!("provider: {provider} model: {model_display}");
|
|
522
|
-
println!("cwd: {short_cwd}");
|
|
523
|
-
println!("turns: {turns}");
|
|
524
|
-
if usage.total_tokens > 0 {
|
|
525
|
-
println!(
|
|
526
|
-
"tokens: {} in / {} out / {} total",
|
|
527
|
-
usage.prompt_tokens, usage.completion_tokens, usage.total_tokens
|
|
528
|
-
);
|
|
529
|
-
}
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
println!();
|
|
534
|
-
println!("\x1b[90m ──────────────────────────────────────\x1b[0m");
|
|
535
|
-
println!(
|
|
536
|
-
" \x1b[2mprovider\x1b[0m \x1b[1m{provider}\x1b[0m \x1b[2m·\x1b[0m \x1b[1m{model_display}\x1b[0m"
|
|
537
|
-
);
|
|
538
|
-
println!(" \x1b[2mcwd \x1b[0m \x1b[2m{short_cwd}\x1b[0m");
|
|
539
|
-
println!(" \x1b[2mturns \x1b[0m {turns}");
|
|
540
|
-
if usage.total_tokens > 0 {
|
|
541
|
-
println!(
|
|
542
|
-
" \x1b[2mtokens \x1b[0m {} in · {} out · {} total",
|
|
543
|
-
usage.prompt_tokens, usage.completion_tokens, usage.total_tokens
|
|
544
|
-
);
|
|
545
|
-
if usage.cache_read_tokens > 0 || usage.cache_write_tokens > 0 {
|
|
546
|
-
println!(
|
|
547
|
-
" \x1b[2mcache \x1b[0m {} read · {} write",
|
|
548
|
-
usage.cache_read_tokens, usage.cache_write_tokens
|
|
549
|
-
);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
println!("\x1b[90m ──────────────────────────────────────\x1b[0m");
|
|
553
|
-
println!();
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
pub fn print_help_inline(is_tty: bool) {
|
|
557
|
-
if !is_tty {
|
|
558
|
-
println!("commands: /clear, /export [path], /session, /attach [path], /exit, /quit, /help");
|
|
559
|
-
println!("keys: ↑/↓ history ←/→ cursor Home/End Ctrl+W delete-word Ctrl+U clear-line");
|
|
560
|
-
println!("images: Ctrl+V to paste clipboard image, or copy then send to auto-attach");
|
|
561
|
-
return;
|
|
562
|
-
}
|
|
563
|
-
println!();
|
|
564
|
-
println!("\x1b[2m Commands\x1b[0m");
|
|
565
|
-
println!("\x1b[90m ──────────────────────────────────────\x1b[0m");
|
|
566
|
-
println!(" \x1b[1;32m/status\x1b[0m provider, model, turns, token usage");
|
|
567
|
-
println!(" \x1b[1;32m/session\x1b[0m show session file, age, and turn count");
|
|
568
|
-
println!(" \x1b[1;32m/export\x1b[0m \x1b[2m[path]\x1b[0m save conversation to a markdown file");
|
|
569
|
-
println!(" \x1b[1;32m/model\x1b[0m \x1b[2m[name]\x1b[0m switch or show current model");
|
|
570
|
-
println!(" \x1b[1;32m/provider\x1b[0m \x1b[2m[name]\x1b[0m switch or show current provider");
|
|
571
|
-
println!(" \x1b[1;32m/clear\x1b[0m reset conversation and delete saved session");
|
|
572
|
-
println!(" \x1b[1;32m/attach\x1b[0m \x1b[2m[path]\x1b[0m attach image from file or clipboard");
|
|
573
|
-
println!(" \x1b[1;32m/exit\x1b[0m, \x1b[1;32m/quit\x1b[0m leave the session");
|
|
574
|
-
println!(" \x1b[1;32m/help\x1b[0m show this message");
|
|
575
|
-
println!();
|
|
576
|
-
println!("\x1b[2m Keyboard\x1b[0m");
|
|
577
|
-
println!("\x1b[90m ──────────────────────────────────────\x1b[0m");
|
|
578
|
-
println!(" \x1b[2m↑ / ↓\x1b[0m recall previous / next prompt");
|
|
579
|
-
println!(" \x1b[2m← / →\x1b[0m move cursor left / right");
|
|
580
|
-
println!(" \x1b[2mHome / End\x1b[0m jump to start / end of line");
|
|
581
|
-
println!(" \x1b[2mCtrl+W\x1b[0m delete word before cursor");
|
|
582
|
-
println!(" \x1b[2mCtrl+U\x1b[0m clear entire line \x1b[2m(also Cmd+Delete)\x1b[0m");
|
|
583
|
-
println!(" \x1b[2mCtrl+V\x1b[0m paste image from clipboard");
|
|
584
|
-
println!();
|
|
585
|
-
println!("\x1b[2m Images\x1b[0m");
|
|
586
|
-
println!("\x1b[90m ──────────────────────────────────────\x1b[0m");
|
|
587
|
-
println!(" \x1b[2mCtrl+V\x1b[0m to paste a clipboard image inline (shows \x1b[2m[📎]\x1b[0m indicator).");
|
|
588
|
-
println!(" Or Cmd+C an image and send any message — it attaches automatically.");
|
|
589
|
-
println!(" Or use \x1b[1;32m/attach\x1b[0m \x1b[2mpath/to/file.png\x1b[0m for a specific file.");
|
|
590
|
-
println!(" For broadest clipboard support: \x1b[2mbrew install pngpaste\x1b[0m");
|
|
591
|
-
println!();
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
pub fn print_session_info(is_tty: bool, path: Option<&Path>, turns: usize, saved_at: Option<u64>) {
|
|
595
|
-
let Some(path) = path else {
|
|
596
|
-
if is_tty {
|
|
597
|
-
eprintln!("\x1b[2m no session path available\x1b[0m");
|
|
598
|
-
} else {
|
|
599
|
-
println!("no session path available");
|
|
600
|
-
}
|
|
601
|
-
return;
|
|
602
|
-
};
|
|
603
|
-
|
|
604
|
-
let short_path = std::env::var("HOME")
|
|
605
|
-
.map(|h| path.display().to_string().replacen(&h, "~", 1))
|
|
606
|
-
.unwrap_or_else(|_| path.display().to_string());
|
|
607
|
-
|
|
608
|
-
if !is_tty {
|
|
609
|
-
println!("session: {short_path}");
|
|
610
|
-
println!("turns: {turns}");
|
|
611
|
-
if let Some(ts) = saved_at {
|
|
612
|
-
println!("saved: {}", format_session_age(Some(ts)));
|
|
613
|
-
}
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
println!();
|
|
618
|
-
println!("\x1b[90m ──────────────────────────────────────\x1b[0m");
|
|
619
|
-
println!(" \x1b[2mfile \x1b[0m \x1b[2m{short_path}\x1b[0m");
|
|
620
|
-
println!(" \x1b[2mturns \x1b[0m {turns}");
|
|
621
|
-
if let Some(ts) = saved_at {
|
|
622
|
-
println!(" \x1b[2msaved \x1b[0m {}", format_session_age(Some(ts)));
|
|
623
|
-
} else {
|
|
624
|
-
println!(" \x1b[2msaved \x1b[0m not yet");
|
|
625
|
-
}
|
|
626
|
-
println!("\x1b[90m ──────────────────────────────────────\x1b[0m");
|
|
627
|
-
println!();
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
pub fn print_session_header(
|
|
631
|
-
provider: &str,
|
|
632
|
-
model: &str,
|
|
633
|
-
turns: usize,
|
|
634
|
-
resumed: bool,
|
|
635
|
-
saved_at: Option<u64>,
|
|
636
|
-
) {
|
|
637
|
-
let is_tty = io::stdout().is_terminal();
|
|
638
|
-
let version = env!("CARGO_PKG_VERSION");
|
|
639
|
-
|
|
640
|
-
if !is_tty {
|
|
641
|
-
let tag = if resumed {
|
|
642
|
-
format!(" (resumed · {turns} turns · {})", format_session_age(saved_at))
|
|
643
|
-
} else {
|
|
644
|
-
String::new()
|
|
645
|
-
};
|
|
646
|
-
println!("anveesa v{version}{tag} | {provider} · {model}");
|
|
647
|
-
return;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
let width = term_width().clamp(50, 220);
|
|
651
|
-
|
|
652
|
-
let cwd = std::env::current_dir()
|
|
653
|
-
.ok()
|
|
654
|
-
.map(|p| {
|
|
655
|
-
let s = p.to_string_lossy().into_owned();
|
|
656
|
-
std::env::var("HOME")
|
|
657
|
-
.map(|h| s.replacen(&h, "~", 1))
|
|
658
|
-
.unwrap_or(s)
|
|
659
|
-
})
|
|
660
|
-
.unwrap_or_else(|| "~".to_string());
|
|
661
|
-
|
|
662
|
-
fn trunc_to(s: &str, max: usize) -> String {
|
|
663
|
-
let v: Vec<char> = s.chars().collect();
|
|
664
|
-
if v.len() <= max {
|
|
665
|
-
return s.to_string();
|
|
666
|
-
}
|
|
667
|
-
let mut r: String = v[..max.saturating_sub(1)].iter().collect();
|
|
668
|
-
r.push('…');
|
|
669
|
-
r
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
let greeting = if resumed {
|
|
673
|
-
format!(" · Resumed ({turns} turns · {})", format_session_age(saved_at))
|
|
674
|
-
} else {
|
|
675
|
-
String::new()
|
|
676
|
-
};
|
|
677
|
-
let title = format!(" anveesa v{version}{greeting} ");
|
|
678
|
-
let title_len = title.chars().count();
|
|
679
|
-
let right_dashes = width.saturating_sub(2 + title_len);
|
|
680
|
-
println!(
|
|
681
|
-
"\x1b[90m──\x1b[0m\x1b[1;32m{title}\x1b[0m\x1b[90m{}\x1b[0m",
|
|
682
|
-
"─".repeat(right_dashes)
|
|
683
|
-
);
|
|
684
|
-
|
|
685
|
-
let info = trunc_to(&format!(" {provider} · {model} · {cwd}"), width);
|
|
686
|
-
println!("\x1b[2m{info}\x1b[0m");
|
|
687
|
-
|
|
688
|
-
println!("\x1b[2m /help for commands\x1b[0m");
|
|
689
|
-
println!();
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
pub fn print_input_separator(is_tty: bool, width: usize) {
|
|
693
|
-
let line = "─".repeat(width);
|
|
694
|
-
if is_tty {
|
|
695
|
-
println!("\x1b[90m{line}\x1b[0m");
|
|
696
|
-
} else {
|
|
697
|
-
println!("{line}");
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
pub fn prompt_label(is_tty: bool) -> String {
|
|
702
|
-
if is_tty {
|
|
703
|
-
"\x1b[1;32m❯\x1b[0m ".to_string()
|
|
704
|
-
} else {
|
|
705
|
-
"> ".to_string()
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
pub fn term_width() -> usize {
|
|
710
|
-
std::env::var("COLUMNS")
|
|
711
|
-
.ok()
|
|
712
|
-
.and_then(|s| s.parse().ok())
|
|
713
|
-
.filter(|&n: &usize| n > 0)
|
|
714
|
-
.unwrap_or_else(|| {
|
|
715
|
-
std::process::Command::new("tput")
|
|
716
|
-
.arg("cols")
|
|
717
|
-
.output()
|
|
718
|
-
.ok()
|
|
719
|
-
.and_then(|o| String::from_utf8(o.stdout).ok())
|
|
720
|
-
.and_then(|s| s.trim().parse().ok())
|
|
721
|
-
.filter(|&n: &usize| n > 0)
|
|
722
|
-
.unwrap_or(90)
|
|
723
|
-
})
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
#[cfg(test)]
|
|
727
|
-
mod tests {
|
|
728
|
-
use super::*;
|
|
729
|
-
|
|
730
|
-
#[test]
|
|
731
|
-
fn format_elapsed_sub_minute() {
|
|
732
|
-
assert_eq!(format_elapsed(0.0), "0s");
|
|
733
|
-
assert_eq!(format_elapsed(45.9), "45s");
|
|
734
|
-
assert_eq!(format_elapsed(59.9), "59s");
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
#[test]
|
|
738
|
-
fn format_elapsed_minutes() {
|
|
739
|
-
assert_eq!(format_elapsed(60.0), "1m 0s");
|
|
740
|
-
assert_eq!(format_elapsed(90.0), "1m 30s");
|
|
741
|
-
assert_eq!(format_elapsed(125.0), "2m 5s");
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
#[test]
|
|
745
|
-
fn format_duration_millis() {
|
|
746
|
-
assert_eq!(format_duration_ms(0), "0ms");
|
|
747
|
-
assert_eq!(format_duration_ms(500), "500ms");
|
|
748
|
-
assert_eq!(format_duration_ms(999), "999ms");
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
#[test]
|
|
752
|
-
fn format_duration_seconds() {
|
|
753
|
-
assert_eq!(format_duration_ms(1000), "1.0s");
|
|
754
|
-
assert_eq!(format_duration_ms(1500), "1.5s");
|
|
755
|
-
assert_eq!(format_duration_ms(2000), "2.0s");
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
#[test]
|
|
759
|
-
fn truncate_for_status_short() {
|
|
760
|
-
assert_eq!(truncate_for_status("hello", 10), "hello");
|
|
761
|
-
assert_eq!(truncate_for_status("", 5), "");
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
#[test]
|
|
765
|
-
fn truncate_for_status_exact_limit() {
|
|
766
|
-
assert_eq!(truncate_for_status("hello", 5), "hello");
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
#[test]
|
|
770
|
-
fn truncate_for_status_over_limit() {
|
|
771
|
-
assert_eq!(truncate_for_status("abcdef", 5), "abcde…");
|
|
772
|
-
assert_eq!(truncate_for_status("hello world", 5), "hello…");
|
|
773
|
-
}
|
|
774
|
-
}
|