anveesa 0.3.3 → 0.3.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
@@ -54,7 +54,7 @@ dependencies = [
54
54
 
55
55
  [[package]]
56
56
  name = "anveesa"
57
- version = "0.3.3"
57
+ version = "0.3.5"
58
58
  dependencies = [
59
59
  "anyhow",
60
60
  "base64",
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "anveesa"
3
- version = "0.3.3"
3
+ version = "0.3.5"
4
4
  edition = "2024"
5
5
  default-run = "anveesa"
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anveesa",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "A terminal CLI that wraps AI providers (OpenAI-compatible APIs and local CLIs) into a single unified command",
5
5
  "main": "bin/anveesa.js",
6
6
  "bin": {
package/src/lib.rs CHANGED
@@ -143,15 +143,22 @@ async fn run_interactive(options: AskOptions) -> Result<()> {
143
143
 
144
144
  loop {
145
145
  print_input_separator(is_tty, width);
146
- let line = match read_prompt_line(&label, width, &mut paste_count) {
147
- Ok(PromptRead::Line(line)) => line,
148
- Ok(PromptRead::Interrupted) => continue,
149
- Ok(PromptRead::Eof) => {
150
- println!();
151
- break;
152
- }
153
- Err(error) => return Err(error).context("failed to read interactive prompt"),
154
- };
146
+ let (line, ctrl_v_image) =
147
+ match read_prompt_line(&label, width, &mut paste_count, images_available) {
148
+ Ok(PromptRead::Line(line, img)) => (line, img),
149
+ Ok(PromptRead::Interrupted) => continue,
150
+ Ok(PromptRead::Eof) => {
151
+ println!();
152
+ break;
153
+ }
154
+ Err(error) => return Err(error).context("failed to read interactive prompt"),
155
+ };
156
+
157
+ // Ctrl+V image takes precedence over a previously pending image.
158
+ if let Some(img) = ctrl_v_image {
159
+ last_image_fp = Some(image_fingerprint(&img));
160
+ pending_image = Some(img);
161
+ }
155
162
 
156
163
  let prompt = line.trim().to_string();
157
164
  if prompt.is_empty() {
@@ -1313,7 +1320,7 @@ fn print_session_header(
1313
1320
  }
1314
1321
 
1315
1322
  enum PromptRead {
1316
- Line(String),
1323
+ Line(String, Option<ImageAttachment>),
1317
1324
  Interrupted,
1318
1325
  Eof,
1319
1326
  }
@@ -1389,6 +1396,24 @@ impl PromptBuffer {
1389
1396
  self.segments.pop();
1390
1397
  }
1391
1398
  }
1399
+
1400
+ /// Ctrl+U / Cmd+Delete — erase the entire line.
1401
+ fn clear_all(&mut self) {
1402
+ self.full.clear();
1403
+ self.display.clear();
1404
+ self.segments.clear();
1405
+ }
1406
+
1407
+ /// Ctrl+W / Option+Delete — erase the last word (whitespace-delimited).
1408
+ fn pop_word(&mut self) {
1409
+ // Trim trailing whitespace first, then remove up to the previous whitespace boundary.
1410
+ while self.full.ends_with(' ') {
1411
+ self.pop_last();
1412
+ }
1413
+ while !self.full.is_empty() && !self.full.ends_with(' ') {
1414
+ self.pop_last();
1415
+ }
1416
+ }
1392
1417
  }
1393
1418
 
1394
1419
  #[cfg(unix)]
@@ -1448,13 +1473,28 @@ impl RawPromptMode {
1448
1473
  }
1449
1474
  }
1450
1475
 
1451
- fn read_prompt_line(label: &str, width: usize, paste_count: &mut usize) -> Result<PromptRead> {
1476
+ fn read_prompt_line(
1477
+ label: &str,
1478
+ width: usize,
1479
+ paste_count: &mut usize,
1480
+ images_available: bool,
1481
+ ) -> Result<PromptRead> {
1452
1482
  let _raw_mode = RawPromptMode::enter()?;
1453
1483
  let mut input = io::stdin().lock();
1454
1484
  let mut buffer = PromptBuffer::default();
1455
1485
  let mut display_rows = 1usize;
1486
+ let mut ctrl_v_image: Option<ImageAttachment> = None;
1487
+
1488
+ // Compose the visible prompt label, optionally prefixed with an image indicator.
1489
+ let effective_label = |img: &Option<ImageAttachment>| -> String {
1490
+ if img.is_some() {
1491
+ format!("\x1b[2m[📎]\x1b[0m {label}")
1492
+ } else {
1493
+ label.to_string()
1494
+ }
1495
+ };
1456
1496
 
1457
- print!("{label}");
1497
+ print!("{}", effective_label(&ctrl_v_image));
1458
1498
  io::stdout().flush().context("failed to write prompt")?;
1459
1499
 
1460
1500
  loop {
@@ -1466,29 +1506,63 @@ fn read_prompt_line(label: &str, width: usize, paste_count: &mut usize) -> Resul
1466
1506
  match byte[0] {
1467
1507
  b'\r' | b'\n' => {
1468
1508
  println!();
1469
- return Ok(PromptRead::Line(buffer.full));
1509
+ return Ok(PromptRead::Line(buffer.full, ctrl_v_image));
1470
1510
  }
1471
1511
  3 => {
1512
+ // Ctrl+C — discard any pasted image too
1513
+ ctrl_v_image = None;
1472
1514
  println!("^C");
1473
1515
  return Ok(PromptRead::Interrupted);
1474
1516
  }
1475
1517
  4 if buffer.is_empty() => return Ok(PromptRead::Eof),
1476
1518
  8 | 127 => {
1519
+ // Backspace
1477
1520
  buffer.pop_last();
1478
- display_rows = redraw_prompt_line(label, &buffer.display, display_rows, width)?;
1521
+ let lbl = effective_label(&ctrl_v_image);
1522
+ display_rows = redraw_prompt_line(&lbl, &buffer.display, display_rows, width)?;
1523
+ }
1524
+ 21 => {
1525
+ // Ctrl+U / Cmd+Delete — erase entire line
1526
+ buffer.clear_all();
1527
+ let lbl = effective_label(&ctrl_v_image);
1528
+ display_rows = redraw_prompt_line(&lbl, &buffer.display, display_rows, width)?;
1529
+ }
1530
+ 22 if images_available => {
1531
+ // Ctrl+V — paste image from clipboard
1532
+ match grab_clipboard_image() {
1533
+ Some(img) => {
1534
+ ctrl_v_image = Some(img);
1535
+ let lbl = effective_label(&ctrl_v_image);
1536
+ display_rows =
1537
+ redraw_prompt_line(&lbl, &buffer.display, display_rows, width)?;
1538
+ }
1539
+ None => {
1540
+ // No image in clipboard — ring the bell
1541
+ print!("\x07");
1542
+ let _ = io::stdout().flush();
1543
+ }
1544
+ }
1545
+ }
1546
+ 23 => {
1547
+ // Ctrl+W / Option+Delete — erase last word
1548
+ buffer.pop_word();
1549
+ let lbl = effective_label(&ctrl_v_image);
1550
+ display_rows = redraw_prompt_line(&lbl, &buffer.display, display_rows, width)?;
1479
1551
  }
1480
1552
  0x1b => {
1481
1553
  let sequence = read_escape_sequence(&mut input)?;
1482
1554
  if sequence == b"[200~" {
1483
1555
  let paste = normalize_pasted_text(read_bracketed_paste(&mut input)?);
1484
1556
  push_paste(&mut buffer, paste, paste_count);
1485
- display_rows = redraw_prompt_line(label, &buffer.display, display_rows, width)?;
1557
+ let lbl = effective_label(&ctrl_v_image);
1558
+ display_rows = redraw_prompt_line(&lbl, &buffer.display, display_rows, width)?;
1486
1559
  }
1487
1560
  }
1488
1561
  byte if byte >= 0x20 && byte != 0x7f => {
1489
1562
  if let Some(ch) = read_utf8_char(byte, &mut input)? {
1490
1563
  buffer.push_text(ch.encode_utf8(&mut [0; 4]));
1491
- display_rows = redraw_prompt_line(label, &buffer.display, display_rows, width)?;
1564
+ let lbl = effective_label(&ctrl_v_image);
1565
+ display_rows = redraw_prompt_line(&lbl, &buffer.display, display_rows, width)?;
1492
1566
  }
1493
1567
  }
1494
1568
  _ => {}