From 44ea247cf82acf34e46da899eed00fba3d648a06 Mon Sep 17 00:00:00 2001 From: can1357 Date: Sun, 26 Apr 2026 04:32:20 +0200 Subject: [PATCH] fix(coding-agent/tools): corrected json-tree inline arg truncation logic - Updated inline argument formatting in formatArgsInline to truncate key/value pairs by width and show ellipsis when values overflow. - Fixed JSON-tree output to hide harness-internal INTENT_FIELD and __partialJson keys from top-level tool argument rendering. - Fixed multiline scalar rendering and empty-structure display paths in renderJsonTreeLines after tree-prefix traversal changes. --- packages/coding-agent/CHANGELOG.md | 5 +- packages/coding-agent/src/mcp/render.ts | 3 +- .../src/modes/components/tool-execution.ts | 5 +- packages/coding-agent/src/tools/json-tree.ts | 226 +++++++++--------- 4 files changed, 119 insertions(+), 120 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index ddd695722..2da8fa4a8 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,7 +1,6 @@ # Changelog ## [Unreleased] - ### Breaking Changes - Renamed atom edit operations from `before` and `after` to `pre` and `post`, so existing `atom` payloads using the old operation keys must be updated @@ -20,6 +19,9 @@ ### Changed +- Changed JSON tree output to truncate inline argument pairs by available width and add an ellipsis when values no longer fit in the display +- Changed JSON tree rendering to hide harness-internal `intent` and `__partialJson` fields from top-level tool output +- Simplified the `grep` tool schema by requiring `path`, folding glob and type filtering into path globs, auto-detecting multiline patterns, removing model-controlled context and limit options, and renaming result skipping to `skip`. - Changed atom edit request format to use a shared `loc` selector, including range (`"160sr-9ab"`) and boundary (`"^"`, `"$"`) forms instead of per-operation anchor fields - Changed atom edit payload fields so `set`, `pre`, and `post` now require line-array values and `sub` now takes a `[find, replace]` tuple, with boundary deletion now expressed as `set: []` - Changed edit diff wrapping to preserve the active line-prefix separator (`|` or `│`) while keeping continuation lines aligned by line-number width @@ -40,6 +42,7 @@ ### Fixed +- Fixed JSON tree rendering for tool arguments by excluding injected internal keys from displayed root records - Printed assistant `errorMessage` text in print mode output to stderr so message-level errors are visible during non-interactive runs - Displayed assistant `errorMessage` text in the assistant message component for completed tool responses with non-terminal stop reasons - Fixed atom input handling to ignore null optional verb fields so entries with `pre`, `set`, `post`, or `sub` set to `null` remain valid diff --git a/packages/coding-agent/src/mcp/render.ts b/packages/coding-agent/src/mcp/render.ts index d4344fb62..bbc81a55c 100644 --- a/packages/coding-agent/src/mcp/render.ts +++ b/packages/coding-agent/src/mcp/render.ts @@ -17,7 +17,6 @@ import { JSON_TREE_SCALAR_LEN_COLLAPSED, JSON_TREE_SCALAR_LEN_EXPANDED, renderJsonTreeLines, - stripInternalArgs, } from "../tools/json-tree"; import { formatExpandHint, truncateToWidth } from "../tools/render-utils"; import { renderStatusLine } from "../tui"; @@ -59,7 +58,7 @@ export function renderMCPResult( const maxDepth = JSON_TREE_MAX_DEPTH_EXPANDED; const maxLines = JSON_TREE_MAX_LINES_EXPANDED; const tree = renderJsonTreeLines( - stripInternalArgs(args), + args, theme, maxDepth, maxLines, diff --git a/packages/coding-agent/src/modes/components/tool-execution.ts b/packages/coding-agent/src/modes/components/tool-execution.ts index 3368bfd3d..b6ef5a29d 100644 --- a/packages/coding-agent/src/modes/components/tool-execution.ts +++ b/packages/coding-agent/src/modes/components/tool-execution.ts @@ -27,7 +27,6 @@ import { JSON_TREE_SCALAR_LEN_COLLAPSED, JSON_TREE_SCALAR_LEN_EXPANDED, renderJsonTreeLines, - stripInternalArgs, } from "../../tools/json-tree"; import { PYTHON_DEFAULT_PREVIEW_LINES } from "../../tools/python"; import { formatExpandHint, replaceTabs, resolveImageOptions, truncateToWidth } from "../../tools/render-utils"; @@ -743,9 +742,7 @@ export class ToolExecutionComponent extends Container { lines.push(""); lines.push(theme.fg("dim", "Args")); const tree = renderJsonTreeLines( - this.#args && typeof this.#args === "object" && !Array.isArray(this.#args) - ? stripInternalArgs(this.#args as Record) - : this.#args, + this.#args, theme, JSON_TREE_MAX_DEPTH_EXPANDED, JSON_TREE_MAX_LINES_EXPANDED, diff --git a/packages/coding-agent/src/tools/json-tree.ts b/packages/coding-agent/src/tools/json-tree.ts index 8476a8311..f83ada48a 100644 --- a/packages/coding-agent/src/tools/json-tree.ts +++ b/packages/coding-agent/src/tools/json-tree.ts @@ -13,17 +13,17 @@ export const JSON_TREE_MAX_LINES_EXPANDED = 200; export const JSON_TREE_SCALAR_LEN_COLLAPSED = 60; export const JSON_TREE_SCALAR_LEN_EXPANDED = 2000; -/** Keys injected by the harness that should not be displayed to users */ -const HIDDEN_ARG_KEYS = new Set([INTENT_FIELD, "__partialJson"]); +const HIDDEN_ARG_KEYS = { [INTENT_FIELD]: 1, __partialJson: 1 }; -/** Strip harness-internal keys from tool args for display */ -export function stripInternalArgs(args: Record): Record { - const result: Record = {}; - for (const [key, value] of Object.entries(args)) { - if (!HIDDEN_ARG_KEYS.has(key)) result[key] = value; - } - return result; +const ARGS_INLINE_PAIR_SEP = ", "; +const ARGS_INLINE_PAIR_SEP_WIDTH = Bun.stringWidth(ARGS_INLINE_PAIR_SEP); +const ARGS_INLINE_MORE = "…"; +const ARGS_INLINE_MORE_WIDTH = Bun.stringWidth(ARGS_INLINE_MORE); + +function isRecord(value: unknown): value is Record { + return !!value && typeof value === "object" && !Array.isArray(value); } + /** * Format a scalar value for inline display. */ @@ -49,40 +49,35 @@ export function formatScalar(value: unknown, maxLen: number): string { * Format args inline for collapsed view. */ export function formatArgsInline(args: Record, maxWidth: number): string { - const entries = Object.entries(args).filter(([k]) => !HIDDEN_ARG_KEYS.has(k)); - if (entries.length === 0) return ""; - - // Single arg: show key=value - if (entries.length === 1) { - const [key, value] = entries[0]; - return `${key}=${formatScalar(value, maxWidth - key.length - 1)}`; - } - - // Multiple args: show key=value, key=value... - const pairs: string[] = []; - let totalLen = 0; - - for (const [key, value] of entries) { - const valueStr = formatScalar(value, 24); - const pairStr = `${key}=${valueStr}`; - const addLen = pairs.length > 0 ? pairStr.length + 2 : pairStr.length; - - if (totalLen + addLen > maxWidth && pairs.length > 0) { - pairs.push("…"); - break; + let result = ""; + let width = 0; + for (const key in args) { + if (key in HIDDEN_ARG_KEYS) continue; + const value = args[key]; + const sep = width > 0 ? ARGS_INLINE_PAIR_SEP : ""; + const sepW = width > 0 ? ARGS_INLINE_PAIR_SEP_WIDTH : 0; + const current = width + sepW; + const cap = maxWidth - current - ARGS_INLINE_MORE_WIDTH; + if (cap <= 0) { + return `${result}${ARGS_INLINE_MORE}`; } - - pairs.push(pairStr); - totalLen += addLen; + const valueMaxLen = Math.min(maxWidth - current, 24); + const valueStr = formatScalar(value, valueMaxLen); + const piece = `${key}=${valueStr}`; + const pieceW = Bun.stringWidth(piece); + if (pieceW > cap) { + return `${result}${sep}${truncateToWidth(piece, cap)}`; + } + result += sep + piece; + width = current + pieceW; } - - return pairs.join(", "); + return result; } /** * Build tree prefix for nested rendering. */ -function buildTreePrefix(ancestors: boolean[], theme: Theme): string { +function buildTreePrefix(theme: Theme, ancestors: readonly boolean[]): string { return ancestors.map(hasNext => (hasNext ? `${theme.tree.vertical} ` : " ")).join(""); } @@ -119,109 +114,114 @@ export function renderJsonTreeLines( } const connector = isLast ? theme.tree.last : theme.tree.branch; - const prefix = `${buildTreePrefix(ancestors, theme)}${theme.fg("dim", connector)} `; + const prefix = `${buildTreePrefix(theme, ancestors)}${theme.fg("dim", connector)} `; - // Handle scalars - if (val === null || val === undefined || typeof val !== "object") { - const label = key ? theme.fg("muted", key) : theme.fg("muted", "value"); + ancestors.push(!isLast); + try { + // Handle scalars + if (val === null || val === undefined || typeof val !== "object") { + const label = key ? theme.fg("muted", key) : theme.fg("muted", "value"); - // Special handling for multiline strings - if (typeof val === "string" && val.includes("\n")) { - const strLines = val.split("\n"); - const maxStrLines = Math.min(strLines.length, Math.max(1, maxLines - lines.length - 1)); - const continuePrefix = buildTreePrefix([...ancestors, !isLast], theme); + // Special handling for multiline strings + if (typeof val === "string" && val.includes("\n")) { + const strLines = val.split("\n"); + const maxStrLines = Math.min(strLines.length, Math.max(1, maxLines - lines.length - 1)); + const continuePrefix = buildTreePrefix(theme, ancestors); - // First line with label - const firstLine = truncateToWidth(strLines[0], maxScalarLen); - pushLine(`${prefix}${iconScalar} ${label}: ${theme.fg("dim", `"${firstLine}`)}`); + // First line with label + const firstLine = truncateToWidth(strLines[0], maxScalarLen); + pushLine(`${prefix}${iconScalar} ${label}: ${theme.fg("dim", `"${firstLine}`)}`); - // Subsequent lines indented - for (let i = 1; i < maxStrLines; i++) { + // Subsequent lines indented + for (let i = 1; i < maxStrLines; i++) { + if (lines.length >= maxLines) { + truncated = true; + break; + } + const line = truncateToWidth(strLines[i], maxScalarLen); + pushLine(`${continuePrefix} ${theme.fg("dim", ` ${line}`)}`); + } + + // Show truncation and closing quote + if (strLines.length > maxStrLines) { + truncated = true; + pushLine( + `${continuePrefix} ${theme.fg("dim", ` …(${strLines.length - maxStrLines} more lines)"`)}`, + ); + } else { + // Add closing quote to last line - need to modify the last pushed line + const lastIdx = lines.length - 1; + lines[lastIdx] = `${lines[lastIdx]}${theme.fg("dim", '"')}`; + } + return; + } + + const scalar = formatScalar(val, maxScalarLen); + pushLine(`${prefix}${iconScalar} ${label}: ${theme.fg("dim", scalar)}`); + return; + } + + // Handle arrays + if (Array.isArray(val)) { + const header = key ? theme.fg("muted", key) : theme.fg("muted", "array"); + pushLine(`${prefix}${iconArray} ${header}`); + if (val.length === 0) { + pushLine( + `${buildTreePrefix(theme, ancestors)}${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", "[]")}`, + ); + return; + } + if (depth >= maxDepth) { + pushLine( + `${buildTreePrefix(theme, ancestors)}${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", "…")}`, + ); + return; + } + for (let i = 0; i < val.length; i++) { + renderNode(val[i], `[${i}]`, ancestors, i === val.length - 1, depth + 1); if (lines.length >= maxLines) { truncated = true; - break; + return; } - const line = truncateToWidth(strLines[i], maxScalarLen); - pushLine(`${continuePrefix} ${theme.fg("dim", ` ${line}`)}`); - } - - // Show truncation and closing quote - if (strLines.length > maxStrLines) { - truncated = true; - pushLine(`${continuePrefix} ${theme.fg("dim", ` …(${strLines.length - maxStrLines} more lines)"`)}`); - } else { - // Add closing quote to last line - need to modify the last pushed line - const lastIdx = lines.length - 1; - lines[lastIdx] = `${lines[lastIdx]}${theme.fg("dim", '"')}`; } return; } - const scalar = formatScalar(val, maxScalarLen); - pushLine(`${prefix}${iconScalar} ${label}: ${theme.fg("dim", scalar)}`); - return; - } + // Handle objects + if (!isRecord(val)) return; - // Handle arrays - if (Array.isArray(val)) { - const header = key ? theme.fg("muted", key) : theme.fg("muted", "array"); - pushLine(`${prefix}${iconArray} ${header}`); - if (val.length === 0) { - pushLine( - `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", "[]")}`, - ); - return; - } + const header = key ? theme.fg("muted", key) : theme.fg("muted", "object"); + pushLine(`${prefix}${iconObject} ${header}`); if (depth >= maxDepth) { + pushLine(`${buildTreePrefix(theme, ancestors)}${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", "…")}`); + return; + } + const keys = Object.keys(val); + if (keys.length === 0) { pushLine( - `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", "…")}`, + `${buildTreePrefix(theme, ancestors)}${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", "{}")}`, ); return; } - const nextAncestors = [...ancestors, !isLast]; - for (let i = 0; i < val.length; i++) { - renderNode(val[i], `[${i}]`, nextAncestors, i === val.length - 1, depth + 1); + for (let i = 0; i < keys.length; i++) { + const childKey = keys[i]; + const child = val[childKey]; + renderNode(child, childKey, ancestors, i === keys.length - 1, depth + 1); if (lines.length >= maxLines) { truncated = true; return; } } - return; - } - - // Handle objects - const header = key ? theme.fg("muted", key) : theme.fg("muted", "object"); - pushLine(`${prefix}${iconObject} ${header}`); - const entries = Object.entries(val as Record); - if (entries.length === 0) { - pushLine( - `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", "{}")}`, - ); - return; - } - if (depth >= maxDepth) { - pushLine( - `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", "…")}`, - ); - return; - } - const nextAncestors = [...ancestors, !isLast]; - for (let i = 0; i < entries.length; i++) { - const [childKey, child] = entries[i]; - renderNode(child, childKey, nextAncestors, i === entries.length - 1, depth + 1); - if (lines.length >= maxLines) { - truncated = true; - return; - } + } finally { + ancestors.pop(); } }; // Render root level - if (value && typeof value === "object" && !Array.isArray(value)) { - const entries = Object.entries(value as Record); - for (let i = 0; i < entries.length; i++) { - const [childKey, child] = entries[i]; - renderNode(child, childKey, [], i === entries.length - 1, 1); + if (isRecord(value)) { + for (const key in value) { + if (key in HIDDEN_ARG_KEYS) continue; + renderNode(value[key], key, [], true, 1); if (lines.length >= maxLines) { truncated = true; break;