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.
This commit is contained in:
can1357
2026-04-26 04:32:27 +02:00
parent 2a3edbc39a
commit 44ea247cf8
4 changed files with 119 additions and 120 deletions
+4 -1
View File
@@ -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
+1 -2
View File
@@ -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,
@@ -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<string, unknown>)
: this.#args,
this.#args,
theme,
JSON_TREE_MAX_DEPTH_EXPANDED,
JSON_TREE_MAX_LINES_EXPANDED,
+113 -113
View File
@@ -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<string, unknown>): Record<string, unknown> {
const result: Record<string, unknown> = {};
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<string, unknown> {
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<string, unknown>, 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<string, unknown>);
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<string, unknown>);
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;