From 0ab9d46b634b4e3bde31f321b18b22fe692794c0 Mon Sep 17 00:00:00 2001 From: xintaofei Date: Sat, 28 Mar 2026 17:13:29 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=AE=9E=E6=97=B6=E5=93=8D?= =?UTF-8?q?=E5=BA=94=E6=97=B6=E8=A7=A3=E6=9E=90=E7=BC=96=E8=BE=91=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E7=9A=84=E4=BB=A3=E7=A0=81=E8=A1=8C=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/acp/connection.rs | 66 +++++++++++++++++-- src-tauri/src/parsers/mod.rs | 4 +- .../message/content-parts-renderer.tsx | 15 ++++- src/lib/session-files.ts | 10 ++- 4 files changed, 84 insertions(+), 11 deletions(-) diff --git a/src-tauri/src/acp/connection.rs b/src-tauri/src/acp/connection.rs index 7155ac1..da30f58 100644 --- a/src-tauri/src/acp/connection.rs +++ b/src-tauri/src/acp/connection.rs @@ -689,7 +689,7 @@ async fn run_connection( notif.update, SessionUpdate::AvailableCommandsUpdate(_) ) { - emit_conversation_update(&cid, &h, notif.update); + emit_conversation_update(&cid, &h, notif.update, None); } Ok(()) }) @@ -1428,10 +1428,11 @@ async fn run_conversation_loop<'a>( Ok(SessionMessage::SessionMessage(dispatch)) => { let cid = conn_id.to_string(); let h = handle.clone(); + let cwd_opt = Some(cwd); let _ = MatchDispatch::new(dispatch) .if_notification( async |notif: SessionNotification| { - emit_conversation_update(&cid, &h, notif.update); + emit_conversation_update(&cid, &h, notif.update, cwd_opt); Ok(()) }, ) @@ -1511,6 +1512,7 @@ async fn run_conversation_loop<'a>( let h = handle.clone(); let runtime = terminal_runtime.clone(); let session_id = sid.clone(); + let cwd_opt = Some(cwd); if let Err(e) = MatchDispatch::new(dispatch) .if_notification( async |notif: SessionNotification| { @@ -1518,7 +1520,7 @@ async fn run_conversation_loop<'a>( ¬if.update, &mut tracked_terminal_tool_calls, ); - emit_conversation_update(&cid, &h, notif.update); + emit_conversation_update(&cid, &h, notif.update, cwd_opt); if should_poll_now { poll_tracked_terminal_tool_calls( runtime.as_ref(), @@ -1885,6 +1887,57 @@ fn serialize_tool_call_content(content: &[ToolCallContent]) -> Option { } } +/// Resolve line numbers for live tool call input. +/// +/// - For apply_patch with bare `@@`: resolve line numbers in place. +/// - For canonical edit JSON (file_path + old_string + new_string): +/// read the file, find where old_string starts, and inject `_start_line` +/// into the JSON so the frontend can generate a diff with real line numbers. +fn resolve_live_tool_input(text: &str, cwd: Option<&str>) -> String { + // Apply-patch format: resolve bare @@ in input + if text.contains("@@\n") || text.contains("@@\r\n") { + if let Some(resolved) = crate::parsers::resolve_patch_text(text, cwd) { + return resolved; + } + } + + // Canonical edit JSON: inject _start_line + if let Ok(mut parsed) = serde_json::from_str::(text) { + let file_path = parsed + .get("file_path") + .or_else(|| parsed.get("path")) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + let old_string = parsed + .get("old_string") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + if let (Some(fp), Some(old_str)) = (file_path, old_string) { + if let Some(start_line) = find_string_start_line(&fp, &old_str, cwd) { + parsed + .as_object_mut() + .unwrap() + .insert("_start_line".to_string(), serde_json::json!(start_line)); + return parsed.to_string(); + } + } + } + + text.to_string() +} + +/// Find the 1-based start line of `needle` in the file at `path`. +fn find_string_start_line(path: &str, needle: &str, cwd: Option<&str>) -> Option { + if needle.is_empty() { + return None; + } + let file_lines = crate::parsers::load_file_lines(path, cwd)?; + let file_content = file_lines.join("\n"); + let byte_offset = file_content.find(needle)?; + Some(file_content[..byte_offset].matches('\n').count() as u64 + 1) +} + fn json_value_to_text(val: &Option) -> Option { match val { Some(serde_json::Value::String(text)) => Some(text.clone()), @@ -1929,6 +1982,7 @@ fn emit_conversation_update( connection_id: &str, app_handle: &tauri::AppHandle, update: SessionUpdate, + cwd: Option<&str>, ) { match update { SessionUpdate::UserMessageChunk(_) => { @@ -1969,7 +2023,8 @@ fn emit_conversation_update( } SessionUpdate::ToolCall(tc) => { let content = serialize_tool_call_content(&tc.content); - let raw_input = json_value_to_text(&tc.raw_input); + let raw_input = json_value_to_text(&tc.raw_input) + .map(|text| resolve_live_tool_input(&text, cwd)); let raw_output = json_value_to_text(&tc.raw_output); crate::web::event_bridge::emit_event( app_handle, @@ -1992,7 +2047,8 @@ fn emit_conversation_update( .content .as_deref() .and_then(serialize_tool_call_content); - let raw_input = json_value_to_text(&tcu.fields.raw_input); + let raw_input = json_value_to_text(&tcu.fields.raw_input) + .map(|text| resolve_live_tool_input(&text, cwd)); let raw_output = json_value_to_text(&tcu.fields.raw_output); crate::web::event_bridge::emit_event( app_handle, diff --git a/src-tauri/src/parsers/mod.rs b/src-tauri/src/parsers/mod.rs index 1a38f36..6b0dac5 100644 --- a/src-tauri/src/parsers/mod.rs +++ b/src-tauri/src/parsers/mod.rs @@ -437,7 +437,7 @@ pub fn resolve_patch_line_numbers(turns: &mut [MessageTurn], cwd: Option<&str>) } /// Resolve a single patch text, replacing bare `@@` with `@@ -N,M +N,M @@`. -fn resolve_patch_text(patch: &str, cwd: Option<&str>) -> Option { +pub fn resolve_patch_text(patch: &str, cwd: Option<&str>) -> Option { let mut output = String::with_capacity(patch.len() + 256); let mut current_file_path: Option = None; let mut file_lines: Option> = None; @@ -499,7 +499,7 @@ fn resolve_patch_text(patch: &str, cwd: Option<&str>) -> Option { } /// Load file lines from disk, trying both absolute path and cwd-relative. -fn load_file_lines(path: &str, cwd: Option<&str>) -> Option> { +pub fn load_file_lines(path: &str, cwd: Option<&str>) -> Option> { use std::fs; use std::path::Path; diff --git a/src/components/message/content-parts-renderer.tsx b/src/components/message/content-parts-renderer.tsx index 267242b..f96908c 100644 --- a/src/components/message/content-parts-renderer.tsx +++ b/src/components/message/content-parts-renderer.tsx @@ -1163,12 +1163,21 @@ function EditToolInput({ input }: { input: Record }) { const filePath = str(input, "file_path") const oldString = str(input, "old_string") ?? "" const newString = str(input, "new_string") ?? "" + const startLine = num(input, "_start_line") const diffCode = useMemo(() => { - return ( - generateUnifiedDiff(oldString, newString, filePath ?? undefined) ?? "" + const diff = generateUnifiedDiff( + oldString, + newString, + filePath ?? undefined ) - }, [oldString, newString, filePath]) + if (!diff || !startLine || startLine <= 1) return diff ?? "" + // Replace line numbers in hunk headers with real start line + return diff.replace( + /^@@ -(\d+),(\d+) \+(\d+),(\d+) @@/gm, + (_, _o, oc, _n, nc) => `@@ -${startLine},${oc} +${startLine},${nc} @@` + ) + }, [oldString, newString, filePath, startLine]) return diffCode ? : null } diff --git a/src/lib/session-files.ts b/src/lib/session-files.ts index 52a2bf6..13afff6 100644 --- a/src/lib/session-files.ts +++ b/src/lib/session-files.ts @@ -896,7 +896,15 @@ function buildDiffChunk( const newStr = typeof parsed.new_string === "string" ? parsed.new_string : "" - return buildUnifiedDiff(filePath, oldStr, newStr) + const diff = buildUnifiedDiff(filePath, oldStr, newStr) + if (!diff) return null + const startLine = + typeof parsed._start_line === "number" ? parsed._start_line : 0 + if (startLine <= 1) return diff + return diff.replace( + /^@@ -(\d+),(\d+) \+(\d+),(\d+) @@/gm, + (_, _o, oc, _n, nc) => `@@ -${startLine},${oc} +${startLine},${nc} @@` + ) } if (op === "write") {