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 +1 -1
- package/Cargo.toml +1 -1
- package/package.json +1 -1
- package/src/lib.rs +2 -2
- package/src/provider/mod.rs +2 -2
- package/src/provider/openai_compatible.rs +9 -7
- package/src/tui.rs +38 -26
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
package/package.json
CHANGED
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
|
|
package/src/provider/mod.rs
CHANGED
|
@@ -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
|
-
///
|
|
53
|
-
pub
|
|
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
|
|
826
|
-
let user_content =
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
479
|
-
KeyCode::Char('v')
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
//
|
|
878
|
-
let
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
let
|
|
882
|
-
|
|
883
|
-
|
|
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
|
-
|
|
886
|
-
|
|
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
|
-
|
|
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.
|
|
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 ·
|
|
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 =
|
|
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
|
|