anveesa 0.5.2 → 0.5.3

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
@@ -60,7 +60,7 @@ dependencies = [
60
60
 
61
61
  [[package]]
62
62
  name = "anveesa"
63
- version = "0.5.2"
63
+ version = "0.5.3"
64
64
  dependencies = [
65
65
  "anyhow",
66
66
  "base64",
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "anveesa"
3
- version = "0.5.2"
3
+ version = "0.5.3"
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.5.2",
3
+ "version": "0.5.3",
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
@@ -636,7 +636,7 @@ async fn ask_streaming(
636
636
  history: &[ChatMessage],
637
637
  workspace_context: Option<&str>,
638
638
  policy: ApprovalPolicy,
639
- image: Option<ImageAttachment>,
639
+ image: Option<ImageAttachment>, // single-image path kept for REPL compatibility
640
640
  mode: RenderMode,
641
641
  ) -> Result<TurnResult> {
642
642
  let provider_name = config
@@ -652,7 +652,7 @@ async fn ask_streaming(
652
652
  system: options.system.clone(),
653
653
  workspace_context: workspace_context.map(str::to_string),
654
654
  history: history.to_vec(),
655
- image,
655
+ images: image.into_iter().collect(),
656
656
  mcp: None, // REPL path: MCP not yet wired here
657
657
  };
658
658
 
@@ -49,8 +49,8 @@ pub struct PromptRequest {
49
49
  pub system: Option<String>,
50
50
  pub workspace_context: Option<String>,
51
51
  pub history: Vec<ChatMessage>,
52
- /// Optional image grabbed from the clipboard for the current turn only.
53
- pub image: Option<ImageAttachment>,
52
+ /// Images attached to the current turn (clipboard paste or explicit attach).
53
+ pub images: Vec<ImageAttachment>,
54
54
  /// Connected MCP servers (runtime only, not part of session history).
55
55
  pub mcp: Option<std::sync::Arc<crate::mcp::McpManager>>,
56
56
  }
@@ -822,13 +822,15 @@ fn build_messages(
822
822
  messages.push(json!({ "role": role, "content": message.content }));
823
823
  }
824
824
 
825
- // Current user turn — multimodal when a clipboard image is attached.
826
- let user_content = match &request.image {
827
- Some(img) => json!([
828
- { "type": "text", "text": &request.prompt },
829
- { "type": "image_url", "image_url": { "url": format!("data:{};base64,{}", img.mime, img.data) } }
830
- ]),
831
- None => json!(&request.prompt),
825
+ // Current user turn — multimodal when images are attached.
826
+ let user_content = if request.images.is_empty() {
827
+ json!(&request.prompt)
828
+ } else {
829
+ let mut parts = vec![json!({ "type": "text", "text": &request.prompt })];
830
+ for img in &request.images {
831
+ parts.push(json!({ "type": "image_url", "image_url": { "url": format!("data:{};base64,{}", img.mime, img.data) } }));
832
+ }
833
+ json!(parts)
832
834
  };
833
835
  messages.push(json!({ "role": "user", "content": user_content }));
834
836
 
package/src/tui.rs CHANGED
@@ -98,7 +98,7 @@ pub struct App {
98
98
  input_history: Vec<String>,
99
99
  hist_idx: Option<usize>,
100
100
  hist_saved: String,
101
- pending_image: Option<ImageAttachment>,
101
+ pending_images: Vec<ImageAttachment>,
102
102
  last_image_fp: Option<String>,
103
103
  images_available: bool,
104
104
 
@@ -193,7 +193,7 @@ impl App {
193
193
  input_history,
194
194
  hist_idx: None,
195
195
  hist_saved: String::new(),
196
- pending_image: None,
196
+ pending_images: Vec::new(),
197
197
  last_image_fp: None,
198
198
  images_available,
199
199
 
@@ -324,11 +324,9 @@ async fn handle_event(app: &mut App, event: Event) -> Result<()> {
324
324
  Event::Paste(text) => {
325
325
  if app.mode != Mode::Input { return Ok(()); }
326
326
  if text.trim().is_empty() {
327
- // Empty paste = user pasted an image (terminal can't forward it as text)
328
- // Try to grab it directly from the clipboard
329
327
  if app.images_available {
330
328
  if let Some(img) = crate::grab_clipboard_image() {
331
- app.pending_image = Some(img);
329
+ app.pending_images.push(img);
332
330
  app.last_image_fp = None;
333
331
  return Ok(());
334
332
  }
@@ -338,6 +336,7 @@ async fn handle_event(app: &mut App, event: Event) -> Result<()> {
338
336
  app.input.insert_str(app.input_cursor, &normalized);
339
337
  app.input_cursor += normalized.len();
340
338
  app.hist_idx = None;
339
+ app.tab_state = None;
341
340
  }
342
341
  }
343
342
  Event::Resize(_, _) => {}
@@ -475,22 +474,25 @@ async fn handle_key(app: &mut App, KeyEvent { code, modifiers, .. }: KeyEvent) -
475
474
  delete_word_before(&mut app.input, &mut app.input_cursor);
476
475
  app.hist_idx = None;
477
476
  }
478
- // Ctrl+V universal paste: image first, then clipboard text
479
- KeyCode::Char('v') if modifiers.contains(KeyModifiers::CONTROL) => {
477
+ // Ctrl+V (all platforms) or Cmd+V (macOS) — image first, then text
478
+ KeyCode::Char('v')
479
+ if modifiers.contains(KeyModifiers::CONTROL)
480
+ || (cfg!(target_os = "macos") && modifiers.contains(KeyModifiers::SUPER)) =>
481
+ {
480
482
  if app.images_available {
481
483
  if let Some(img) = crate::grab_clipboard_image() {
482
- app.pending_image = Some(img);
484
+ app.pending_images.push(img);
483
485
  app.last_image_fp = None;
484
486
  return Ok(());
485
487
  }
486
488
  }
487
- // No image — fall back to clipboard text
488
489
  if let Some(text) = crate::read_clipboard_text() {
489
490
  if !text.is_empty() {
490
491
  let normalized = text.replace('\r', "\n");
491
492
  app.input.insert_str(app.input_cursor, &normalized);
492
493
  app.input_cursor += normalized.len();
493
494
  app.hist_idx = None;
495
+ app.tab_state = None;
494
496
  }
495
497
  }
496
498
  }
@@ -610,7 +612,7 @@ fn handle_slash_command(app: &mut App, text: &str) -> bool {
610
612
  app.streaming_buf.clear();
611
613
  app.accumulated_response.clear();
612
614
  app.usage = Usage::default();
613
- app.pending_image = None;
615
+ app.pending_images.clear();
614
616
  app.seen_paths.clear();
615
617
  app.undo_stack.clear();
616
618
  app.input.clear();
@@ -634,7 +636,7 @@ fn handle_slash_command(app: &mut App, text: &str) -> bool {
634
636
  Tab complete /command or file path (press again to cycle)\n\
635
637
  [ ] navigate between file diffs Enter expand/collapse focused diff\n\
636
638
  j/k scroll (when input empty) PageUp/Dn scroll\n\
637
- Ctrl+V paste (image or text) Ctrl+M scroll/select mode\n\
639
+ ⌘V (macOS) / Ctrl+V paste image or text (repeat to queue multiple images)\n\
638
640
  Ctrl+W delete-word Ctrl+U clear line\n\
639
641
  \n\
640
642
  Search: set BRAVE_SEARCH_API_KEY or SERPER_API_KEY for better results".into(),
@@ -874,17 +876,24 @@ async fn submit_prompt(app: &mut App, text: String) -> Result<()> {
874
876
  app.pending_prompt = text.clone();
875
877
  app.accumulated_response.clear();
876
878
 
877
- // Auto-attach clipboard image if nothing was explicitly Ctrl+V'd
878
- let image = app.pending_image.take().or_else(|| {
879
- if !app.images_available { return None; }
880
- let img = crate::grab_clipboard_image()?;
881
- let fp = crate::image_fingerprint(&img);
882
- if app.last_image_fp.as_deref() == Some(&fp) {
883
- return None; // same image as last time
879
+ // Collect explicitly pasted images; if none, auto-attach a new clipboard image once.
880
+ let images: Vec<crate::provider::ImageAttachment> = if !app.pending_images.is_empty() {
881
+ std::mem::take(&mut app.pending_images)
882
+ } else if app.images_available {
883
+ if let Some(img) = crate::grab_clipboard_image() {
884
+ let fp = crate::image_fingerprint(&img);
885
+ if app.last_image_fp.as_deref() == Some(&fp) {
886
+ vec![]
887
+ } else {
888
+ app.last_image_fp = Some(fp);
889
+ vec![img]
890
+ }
891
+ } else {
892
+ vec![]
884
893
  }
885
- app.last_image_fp = Some(fp);
886
- Some(img)
887
- });
894
+ } else {
895
+ vec![]
896
+ };
888
897
 
889
898
  app.messages.push(Msg::User { text: text.clone() });
890
899
  app.input.clear();
@@ -924,7 +933,7 @@ async fn submit_prompt(app: &mut App, text: String) -> Result<()> {
924
933
  system: options.system.clone(),
925
934
  workspace_context,
926
935
  history,
927
- image,
936
+ images,
928
937
  mcp: mcp_arc,
929
938
  };
930
939
  let result = crate::provider::ask(&config, &provider_name, request, policy, &stream_tx_inner).await;
@@ -1525,10 +1534,9 @@ fn render_input(frame: &mut Frame, area: Rect, app: &App) {
1525
1534
  return;
1526
1535
  }
1527
1536
 
1528
- if app.input.is_empty() && app.pending_image.is_none() {
1529
- // Placeholder hint
1537
+ if app.input.is_empty() && app.pending_images.is_empty() {
1530
1538
  frame.render_widget(
1531
- Paragraph::new(" ❯ Ask anything… (↑/↓ history · Ctrl+V paste image)")
1539
+ Paragraph::new(" ❯ Ask anything… (↑/↓ history · V paste image)")
1532
1540
  .style(Style::default().fg(Color::Rgb(60, 60, 80))),
1533
1541
  inner,
1534
1542
  );
@@ -1536,7 +1544,11 @@ fn render_input(frame: &mut Frame, area: Rect, app: &App) {
1536
1544
  return;
1537
1545
  }
1538
1546
 
1539
- let label = if app.pending_image.is_some() { " [📎] ❯ " } else { " ❯ " };
1547
+ let label = match app.pending_images.len() {
1548
+ 0 => " ❯ ".to_string(),
1549
+ 1 => " [📎] ❯ ".to_string(),
1550
+ n => format!(" [📎 ×{n}] ❯ "),
1551
+ };
1540
1552
  let label_w = label.chars().count();
1541
1553
  let display = format!("{label}{}", app.input);
1542
1554