支持实时响应时解析编辑工具的代码行号
This commit is contained in:
@@ -689,7 +689,7 @@ async fn run_connection(
|
|||||||
notif.update,
|
notif.update,
|
||||||
SessionUpdate::AvailableCommandsUpdate(_)
|
SessionUpdate::AvailableCommandsUpdate(_)
|
||||||
) {
|
) {
|
||||||
emit_conversation_update(&cid, &h, notif.update);
|
emit_conversation_update(&cid, &h, notif.update, None);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
@@ -1428,10 +1428,11 @@ async fn run_conversation_loop<'a>(
|
|||||||
Ok(SessionMessage::SessionMessage(dispatch)) => {
|
Ok(SessionMessage::SessionMessage(dispatch)) => {
|
||||||
let cid = conn_id.to_string();
|
let cid = conn_id.to_string();
|
||||||
let h = handle.clone();
|
let h = handle.clone();
|
||||||
|
let cwd_opt = Some(cwd);
|
||||||
let _ = MatchDispatch::new(dispatch)
|
let _ = MatchDispatch::new(dispatch)
|
||||||
.if_notification(
|
.if_notification(
|
||||||
async |notif: SessionNotification| {
|
async |notif: SessionNotification| {
|
||||||
emit_conversation_update(&cid, &h, notif.update);
|
emit_conversation_update(&cid, &h, notif.update, cwd_opt);
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -1511,6 +1512,7 @@ async fn run_conversation_loop<'a>(
|
|||||||
let h = handle.clone();
|
let h = handle.clone();
|
||||||
let runtime = terminal_runtime.clone();
|
let runtime = terminal_runtime.clone();
|
||||||
let session_id = sid.clone();
|
let session_id = sid.clone();
|
||||||
|
let cwd_opt = Some(cwd);
|
||||||
if let Err(e) = MatchDispatch::new(dispatch)
|
if let Err(e) = MatchDispatch::new(dispatch)
|
||||||
.if_notification(
|
.if_notification(
|
||||||
async |notif: SessionNotification| {
|
async |notif: SessionNotification| {
|
||||||
@@ -1518,7 +1520,7 @@ async fn run_conversation_loop<'a>(
|
|||||||
¬if.update,
|
¬if.update,
|
||||||
&mut tracked_terminal_tool_calls,
|
&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 {
|
if should_poll_now {
|
||||||
poll_tracked_terminal_tool_calls(
|
poll_tracked_terminal_tool_calls(
|
||||||
runtime.as_ref(),
|
runtime.as_ref(),
|
||||||
@@ -1885,6 +1887,57 @@ fn serialize_tool_call_content(content: &[ToolCallContent]) -> Option<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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::<serde_json::Value>(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<u64> {
|
||||||
|
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<serde_json::Value>) -> Option<String> {
|
fn json_value_to_text(val: &Option<serde_json::Value>) -> Option<String> {
|
||||||
match val {
|
match val {
|
||||||
Some(serde_json::Value::String(text)) => Some(text.clone()),
|
Some(serde_json::Value::String(text)) => Some(text.clone()),
|
||||||
@@ -1929,6 +1982,7 @@ fn emit_conversation_update(
|
|||||||
connection_id: &str,
|
connection_id: &str,
|
||||||
app_handle: &tauri::AppHandle,
|
app_handle: &tauri::AppHandle,
|
||||||
update: SessionUpdate,
|
update: SessionUpdate,
|
||||||
|
cwd: Option<&str>,
|
||||||
) {
|
) {
|
||||||
match update {
|
match update {
|
||||||
SessionUpdate::UserMessageChunk(_) => {
|
SessionUpdate::UserMessageChunk(_) => {
|
||||||
@@ -1969,7 +2023,8 @@ fn emit_conversation_update(
|
|||||||
}
|
}
|
||||||
SessionUpdate::ToolCall(tc) => {
|
SessionUpdate::ToolCall(tc) => {
|
||||||
let content = serialize_tool_call_content(&tc.content);
|
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);
|
let raw_output = json_value_to_text(&tc.raw_output);
|
||||||
crate::web::event_bridge::emit_event(
|
crate::web::event_bridge::emit_event(
|
||||||
app_handle,
|
app_handle,
|
||||||
@@ -1992,7 +2047,8 @@ fn emit_conversation_update(
|
|||||||
.content
|
.content
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.and_then(serialize_tool_call_content);
|
.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);
|
let raw_output = json_value_to_text(&tcu.fields.raw_output);
|
||||||
crate::web::event_bridge::emit_event(
|
crate::web::event_bridge::emit_event(
|
||||||
app_handle,
|
app_handle,
|
||||||
|
|||||||
@@ -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 @@`.
|
/// Resolve a single patch text, replacing bare `@@` with `@@ -N,M +N,M @@`.
|
||||||
fn resolve_patch_text(patch: &str, cwd: Option<&str>) -> Option<String> {
|
pub fn resolve_patch_text(patch: &str, cwd: Option<&str>) -> Option<String> {
|
||||||
let mut output = String::with_capacity(patch.len() + 256);
|
let mut output = String::with_capacity(patch.len() + 256);
|
||||||
let mut current_file_path: Option<String> = None;
|
let mut current_file_path: Option<String> = None;
|
||||||
let mut file_lines: Option<Vec<String>> = None;
|
let mut file_lines: Option<Vec<String>> = None;
|
||||||
@@ -499,7 +499,7 @@ fn resolve_patch_text(patch: &str, cwd: Option<&str>) -> Option<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Load file lines from disk, trying both absolute path and cwd-relative.
|
/// Load file lines from disk, trying both absolute path and cwd-relative.
|
||||||
fn load_file_lines(path: &str, cwd: Option<&str>) -> Option<Vec<String>> {
|
pub fn load_file_lines(path: &str, cwd: Option<&str>) -> Option<Vec<String>> {
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
|||||||
@@ -1163,12 +1163,21 @@ function EditToolInput({ input }: { input: Record<string, unknown> }) {
|
|||||||
const filePath = str(input, "file_path")
|
const filePath = str(input, "file_path")
|
||||||
const oldString = str(input, "old_string") ?? ""
|
const oldString = str(input, "old_string") ?? ""
|
||||||
const newString = str(input, "new_string") ?? ""
|
const newString = str(input, "new_string") ?? ""
|
||||||
|
const startLine = num(input, "_start_line")
|
||||||
|
|
||||||
const diffCode = useMemo(() => {
|
const diffCode = useMemo(() => {
|
||||||
return (
|
const diff = generateUnifiedDiff(
|
||||||
generateUnifiedDiff(oldString, newString, filePath ?? undefined) ?? ""
|
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 ? <UnifiedDiffPreview diffText={diffCode} /> : null
|
return diffCode ? <UnifiedDiffPreview diffText={diffCode} /> : null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -896,7 +896,15 @@ function buildDiffChunk(
|
|||||||
const newStr =
|
const newStr =
|
||||||
typeof parsed.new_string === "string" ? parsed.new_string : ""
|
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") {
|
if (op === "write") {
|
||||||
|
|||||||
Reference in New Issue
Block a user