Skip to content

Commit c697a46

Browse files
lukemarsdenclaude
andcommitted
fix(acp_thread): format Qwen Code shell output nicely for Helix sync
When syncing tool call output to Helix via external_websocket_sync, Qwen Code shell output was being shown as raw JSON. The fix is in ToolCall::to_markdown which now: 1. First tries to format raw_output using format_shell_output 2. If that works, shows a markdown table with Command, Directory, Exit (code/signal), and the output in a code block 3. Falls back to content-based rendering if raw_output isn't present or isn't in the expected format Fallback behavior: - If output has "output" field but not in Qwen Code format: show as code block - If no "output" field: show content as-is (existing behavior) Note: This formatting is specific to Qwen Code's shell tool output format. The built-in zed-agent uses ACP's terminal protocol which has different rendering. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 3dd6a83 commit c697a46

1 file changed

Lines changed: 144 additions & 8 deletions

File tree

crates/acp_thread/src/acp_thread.rs

Lines changed: 144 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,17 @@ impl ToolCall {
345345
self.label.read(cx).source(),
346346
self.status
347347
);
348+
349+
// Try to format raw_output nicely (e.g., Qwen Code shell output)
350+
if let Some(raw_output) = &self.raw_output {
351+
if let Some(formatted) = format_shell_output(raw_output) {
352+
markdown.push_str(&formatted);
353+
markdown.push_str("\n\n");
354+
return markdown;
355+
}
356+
}
357+
358+
// Fallback to content-based rendering
348359
for content in &self.content {
349360
markdown.push_str(content.to_markdown(cx).as_str());
350361
markdown.push_str("\n\n");
@@ -2377,14 +2388,139 @@ fn markdown_for_raw_output(
23772388
cx,
23782389
)
23792390
})),
2380-
value => Some(cx.new(|cx| {
2381-
Markdown::new(
2382-
format!("```json\n{}\n```", value).into(),
2383-
Some(language_registry.clone()),
2384-
None,
2385-
cx,
2386-
)
2387-
})),
2391+
value => {
2392+
// Try to format shell output nicely
2393+
if let Some(formatted) = format_shell_output(value) {
2394+
Some(cx.new(|cx| {
2395+
Markdown::new(
2396+
formatted.into(),
2397+
Some(language_registry.clone()),
2398+
None,
2399+
cx,
2400+
)
2401+
}))
2402+
} else {
2403+
Some(cx.new(|cx| {
2404+
Markdown::new(
2405+
format!("```json\n{}\n```", value).into(),
2406+
Some(language_registry.clone()),
2407+
None,
2408+
cx,
2409+
)
2410+
}))
2411+
}
2412+
}
2413+
}
2414+
}
2415+
2416+
/// Format shell command output with a metadata table and output code block.
2417+
/// Extracts Command, Directory, Exit Code into a table, and Output into a code block.
2418+
/// Returns None if the output doesn't match the expected shell output format.
2419+
fn format_shell_output(output: &serde_json::Value) -> Option<String> {
2420+
// Shell output comes as {"output":"Command: cd /path\nDirectory: ...\nOutput: ...\nExit Code: 0"}
2421+
let obj = output.as_object()?;
2422+
let output_str = obj.get("output")?.as_str()?;
2423+
2424+
// Parse all the shell output fields
2425+
let field_markers = [
2426+
"Command:",
2427+
"Directory:",
2428+
"Output:",
2429+
"Error:",
2430+
"Exit Code:",
2431+
"Signal:",
2432+
"Background PIDs:",
2433+
"Process Group PGID:",
2434+
];
2435+
2436+
let mut command: Option<String> = None;
2437+
let mut directory: Option<String> = None;
2438+
let mut exit_code: Option<String> = None;
2439+
let mut signal: Option<String> = None;
2440+
let mut cmd_output = Vec::new();
2441+
let mut in_output_field = false;
2442+
2443+
for line in output_str.lines() {
2444+
// Check if this line starts a new field
2445+
let starts_new_field = field_markers.iter().any(|m| line.starts_with(m));
2446+
2447+
if line.starts_with("Command:") {
2448+
in_output_field = false;
2449+
command = Some(line.strip_prefix("Command:").unwrap_or("").trim().to_string());
2450+
} else if line.starts_with("Directory:") {
2451+
in_output_field = false;
2452+
directory = Some(line.strip_prefix("Directory:").unwrap_or("").trim().to_string());
2453+
} else if line.starts_with("Exit Code:") {
2454+
in_output_field = false;
2455+
exit_code = Some(line.strip_prefix("Exit Code:").unwrap_or("").trim().to_string());
2456+
} else if line.starts_with("Signal:") {
2457+
in_output_field = false;
2458+
signal = Some(line.strip_prefix("Signal:").unwrap_or("").trim().to_string());
2459+
} else if line.starts_with("Output:") {
2460+
in_output_field = true;
2461+
let value = line.strip_prefix("Output:").unwrap_or("").trim();
2462+
if !value.is_empty() {
2463+
cmd_output.push(value.to_string());
2464+
}
2465+
} else if starts_new_field {
2466+
in_output_field = false;
2467+
} else if in_output_field {
2468+
cmd_output.push(line.to_string());
2469+
}
2470+
}
2471+
2472+
let full_output = cmd_output.join("\n");
2473+
let has_output = !full_output.is_empty() && full_output != "(empty)";
2474+
2475+
// Build the formatted result with a markdown table for metadata
2476+
let mut result = String::new();
2477+
2478+
// Build table rows
2479+
let mut table_rows = Vec::new();
2480+
if let Some(cmd) = &command {
2481+
table_rows.push(format!("| Command | `{}` |", cmd));
2482+
}
2483+
if let Some(dir) = &directory {
2484+
table_rows.push(format!("| Directory | `{}` |", dir));
2485+
}
2486+
// Combine exit code and signal into one field
2487+
match (&exit_code, &signal) {
2488+
(Some(code), Some(sig)) => {
2489+
table_rows.push(format!("| Exit | {} (signal: {}) |", code, sig));
2490+
}
2491+
(Some(code), None) => {
2492+
table_rows.push(format!("| Exit | {} |", code));
2493+
}
2494+
(None, Some(sig)) => {
2495+
table_rows.push(format!("| Exit | signal: {} |", sig));
2496+
}
2497+
(None, None) => {}
2498+
}
2499+
2500+
// Add table if we have metadata
2501+
if !table_rows.is_empty() {
2502+
result.push_str("| | |\n|---|---|\n");
2503+
result.push_str(&table_rows.join("\n"));
2504+
}
2505+
2506+
// Add output code block
2507+
if has_output {
2508+
if !result.is_empty() {
2509+
result.push_str("\n\n");
2510+
}
2511+
result.push_str(&format!("```\n{}\n```", full_output));
2512+
}
2513+
2514+
if result.is_empty() {
2515+
// Fallback: if we have an "output" field but couldn't parse it in the
2516+
// expected format, just show the raw output in a code block
2517+
if !output_str.is_empty() {
2518+
Some(format!("```\n{}\n```", output_str))
2519+
} else {
2520+
None
2521+
}
2522+
} else {
2523+
Some(result)
23882524
}
23892525
}
23902526

0 commit comments

Comments
 (0)