diff --git a/src-tauri/src/acp/connection.rs b/src-tauri/src/acp/connection.rs index 1af295d..20114b3 100644 --- a/src-tauri/src/acp/connection.rs +++ b/src-tauri/src/acp/connection.rs @@ -886,32 +886,65 @@ async fn handle_permission_request( let mut tool_call_value = serde_json::to_value(&req.tool_call).unwrap_or_default(); - // Resolve line numbers in rawInput for edit tool permission requests + // Inject _start_line into rawInput object for edit tool permission requests if let Some(obj) = tool_call_value.as_object_mut() { - let raw_input_key = if obj.contains_key("rawInput") { - Some("rawInput") - } else if obj.contains_key("raw_input") { - Some("raw_input") - } else { - None - }; - if let Some(key) = raw_input_key { - if let Some(input_val) = obj.get(key).cloned() { - let input_text = match &input_val { - serde_json::Value::String(s) => Some(s.clone()), - v if !v.is_null() => Some(v.to_string()), - _ => None, - }; - if let Some(text) = input_text { - let resolved = resolve_live_tool_input(&text, Some(cwd)); - if resolved != text { - obj.insert( - key.to_string(), - serde_json::Value::String(resolved), + let key = ["rawInput", "raw_input"] + .into_iter() + .find(|k| obj.contains_key(*k)); + if let Some(key) = key { + // If rawInput is an object with file_path + old_string, inject _start_line directly + if let Some(input_obj) = obj.get_mut(key).and_then(|v| v.as_object_mut()) { + let file_path = input_obj + .get("file_path") + .or_else(|| input_obj.get("path")) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + let old_string = input_obj + .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(sl) = find_string_start_line(&fp, &old_str, Some(cwd)) { + input_obj.insert( + "_start_line".to_string(), + serde_json::json!(sl), ); } } } + // If rawInput is a string, parse it and try to inject _start_line or resolve @@ + else if let Some(serde_json::Value::String(text)) = obj.get(key).cloned() { + // Try canonical edit JSON: parse, inject _start_line, write back as object + if let Ok(mut parsed) = serde_json::from_str::(&text) { + if let Some(input_obj) = parsed.as_object_mut() { + let fp = input_obj + .get("file_path") + .or_else(|| input_obj.get("path")) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + let old_str = input_obj + .get("old_string") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + if let (Some(fp), Some(old_str)) = (fp, old_str) { + if let Some(sl) = find_string_start_line(&fp, &old_str, Some(cwd)) { + input_obj.insert( + "_start_line".to_string(), + serde_json::json!(sl), + ); + // Write back as object so frontend asObject() works directly + obj.insert(key.to_string(), parsed); + } + } + } + } + // Apply_patch format: resolve bare @@ + else if text.contains("@@\n") || text.contains("@@\r\n") { + if let Some(resolved) = crate::parsers::resolve_patch_text(&text, Some(cwd)) { + obj.insert(key.to_string(), serde_json::Value::String(resolved)); + } + } + } } } diff --git a/src/lib/permission-request.ts b/src/lib/permission-request.ts index bc302b0..186b976 100644 --- a/src/lib/permission-request.ts +++ b/src/lib/permission-request.ts @@ -12,6 +12,7 @@ export interface PermissionFileChange { oldText: string newText: string unifiedDiff?: string + startLine?: number } export interface PermissionPlanEntry { @@ -111,7 +112,8 @@ function buildCompactDiffFromTexts( path: string, oldText: string, newText: string, - contextLines: number = 2 + contextLines: number = 2, + startLine: number = 1 ): string | null { const oldLines = splitNormalizedLines(oldText) const newLines = splitNormalizedLines(newText) @@ -145,7 +147,7 @@ function buildCompactDiffFromTexts( Math.min(oldLines.length, oldLines.length - suffix + contextLines) ) - const oldStart = Math.max(1, prefix - before.length + 1) + const oldStart = Math.max(1, startLine + prefix - before.length) const oldCount = before.length + removed.length + after.length const newCount = before.length + added.length + after.length @@ -197,7 +199,13 @@ function buildDiffPreviewFromChanges( typeof change.unifiedDiff === "string" && change.unifiedDiff.trim().length > 0 ? change.unifiedDiff.trim() - : buildCompactDiffFromTexts(change.path, change.oldText, change.newText) + : buildCompactDiffFromTexts( + change.path, + change.oldText, + change.newText, + 2, + change.startLine ?? 1 + ) if (!block) continue for (const line of block.split("\n")) { @@ -358,11 +366,18 @@ function parseChangeRecord( pickString(record, ["unifiedDiff", "unified_diff", "diff", "patch"]) ?? undefined + const rawStartLine = record._start_line ?? record.start_line + const startLine = + typeof rawStartLine === "number" && rawStartLine > 0 + ? rawStartLine + : undefined + return { path: normalizedPath, oldText, newText, unifiedDiff, + startLine, } } @@ -410,11 +425,13 @@ function extractRawInputFileChanges( ]) ?? "" if (oldText || newText || changes.length === 0) { + const rawSl = rawInputObj._start_line ?? rawInputObj.start_line changes.push({ path: directPath, oldText, newText, unifiedDiff: undefined, + startLine: typeof rawSl === "number" && rawSl > 0 ? rawSl : undefined, }) } } @@ -503,7 +520,8 @@ function mergeFileChanges( const oldText = prev.oldText || change.oldText const newText = prev.newText || change.newText const unifiedDiff = prev.unifiedDiff || change.unifiedDiff - merged.set(path, { path, oldText, newText, unifiedDiff }) + const startLine = prev.startLine ?? change.startLine + merged.set(path, { path, oldText, newText, unifiedDiff, startLine }) } return Array.from(merged.values()) }