@oh-my-pi/pi-coding-agent 16.3.5 → 16.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/cli.js +3388 -3374
- package/dist/types/edit/file-snapshot-store.d.ts +4 -2
- package/dist/types/edit/renderer.d.ts +0 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -0
- package/dist/types/extensibility/shared-events.d.ts +8 -1
- package/dist/types/modes/components/assistant-message.d.ts +15 -10
- package/dist/types/modes/components/bash-execution.d.ts +6 -0
- package/dist/types/modes/components/eval-execution.d.ts +6 -0
- package/dist/types/modes/components/tool-execution.d.ts +3 -13
- package/dist/types/modes/components/transcript-container.d.ts +26 -28
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +15 -6
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/tools/bash.d.ts +0 -2
- package/dist/types/tools/browser/tab-supervisor.d.ts +10 -0
- package/dist/types/tools/eval-render.d.ts +0 -2
- package/dist/types/tools/renderers.d.ts +0 -20
- package/dist/types/tools/ssh.d.ts +0 -2
- package/dist/types/tools/write.d.ts +1 -1
- package/package.json +12 -12
- package/src/edit/file-snapshot-store.ts +5 -2
- package/src/edit/renderer.ts +0 -5
- package/src/extensibility/custom-tools/types.ts +2 -0
- package/src/extensibility/shared-events.ts +9 -1
- package/src/modes/components/assistant-message.ts +134 -82
- package/src/modes/components/bash-execution.ts +9 -0
- package/src/modes/components/chat-transcript-builder.ts +8 -4
- package/src/modes/components/eval-execution.ts +9 -0
- package/src/modes/components/tool-execution.ts +4 -50
- package/src/modes/components/transcript-container.ts +82 -432
- package/src/modes/components/tree-selector.ts +9 -3
- package/src/modes/controllers/command-controller.ts +0 -3
- package/src/modes/controllers/event-controller.ts +74 -14
- package/src/modes/controllers/extension-ui-controller.ts +0 -1
- package/src/modes/controllers/input-controller.ts +4 -10
- package/src/modes/interactive-mode.ts +12 -8
- package/src/modes/utils/transcript-render-helpers.ts +40 -13
- package/src/modes/utils/ui-helpers.ts +12 -7
- package/src/sdk.ts +1 -0
- package/src/session/agent-session.ts +148 -1
- package/src/session/session-context.ts +7 -0
- package/src/tools/bash.ts +0 -4
- package/src/tools/browser/tab-supervisor.ts +47 -3
- package/src/tools/eval-render.ts +0 -20
- package/src/tools/read.ts +63 -20
- package/src/tools/renderers.ts +0 -20
- package/src/tools/ssh.ts +0 -16
- package/src/tools/write.ts +13 -6
|
@@ -48,6 +48,16 @@ export interface PendingRun {
|
|
|
48
48
|
session: ToolSession;
|
|
49
49
|
signal?: AbortSignal;
|
|
50
50
|
toolCalls: Map<string, AbortController>;
|
|
51
|
+
/**
|
|
52
|
+
* Fires when `releaseTab` closes the tab out from under an in-flight run
|
|
53
|
+
* (sibling `browser close --all`, session-scoped reap, etc.). Composed
|
|
54
|
+
* into the cmux run's signal so `wait(...)`, cmux socket calls, and the
|
|
55
|
+
* facade proxies unwind promptly instead of blocking to the run's
|
|
56
|
+
* timeout. `pending.reject` still fires first so the awaiting caller
|
|
57
|
+
* sees the tab-close error immediately; `closeAc` propagates the
|
|
58
|
+
* cancellation into the still-running `runCmuxCode` body (issue #4499).
|
|
59
|
+
*/
|
|
60
|
+
closeAc?: AbortController;
|
|
51
61
|
}
|
|
52
62
|
|
|
53
63
|
interface TabSessionBase<TBrowser extends BrowserHandle = BrowserHandle> {
|
|
@@ -380,23 +390,47 @@ async function runInTabWithSnapshot(
|
|
|
380
390
|
if (tab.pending.size > 0) throw new ToolError(`Tab ${JSON.stringify(name)} is busy`);
|
|
381
391
|
const id = Snowflake.next();
|
|
382
392
|
const { promise, resolve, reject } = Promise.withResolvers<RunResultOk>();
|
|
393
|
+
// `releaseTab` calls `pending.reject(closeError)` when the tab dies
|
|
394
|
+
// out from under an in-flight run (sibling `browser close --all`,
|
|
395
|
+
// session-scoped reap, etc.). Both backends below MUST end up awaiting
|
|
396
|
+
// this same `promise` so:
|
|
397
|
+
// 1. The caller sees `Tab ... was closed` immediately instead of
|
|
398
|
+
// blocking to the run's timeout, and
|
|
399
|
+
// 2. `reject(...)` always has an attached handler — a zero-consumer
|
|
400
|
+
// rejection would fire `unhandledRejection` and the CLI's
|
|
401
|
+
// top-level handler would tear the whole session down, killing
|
|
402
|
+
// every other tab and subagent sharing the process (issue #4499).
|
|
403
|
+
// The cmux branch also composes `closeAc.signal` into the run's abort
|
|
404
|
+
// signal so `wait(...)`, cmux socket calls, and the facade proxies
|
|
405
|
+
// unwind promptly when the tab is closed — otherwise a `wait(60_000)`
|
|
406
|
+
// with no in-flight socket request would keep `runCmuxCode` blocked
|
|
407
|
+
// until timeout even after the tab is gone.
|
|
408
|
+
const closeAc = new AbortController();
|
|
383
409
|
const pending: PendingRun = {
|
|
384
410
|
resolve,
|
|
385
411
|
reject,
|
|
386
412
|
session: opts.session ?? ({} as ToolSession),
|
|
387
413
|
signal: opts.signal,
|
|
388
414
|
toolCalls: new Map(),
|
|
415
|
+
closeAc,
|
|
389
416
|
};
|
|
390
417
|
tab.pending.set(id, pending);
|
|
391
418
|
if (tab.backend === "cmux") {
|
|
419
|
+
const runSignal = opts.signal ? AbortSignal.any([opts.signal, closeAc.signal]) : closeAc.signal;
|
|
392
420
|
try {
|
|
393
|
-
|
|
421
|
+
// `runCmuxCode.then(resolve, reject)` publishes the run's real
|
|
422
|
+
// outcome to `promise`, but `releaseTab` may have already
|
|
423
|
+
// rejected it — `Promise.withResolvers` settles on the first
|
|
424
|
+
// call and later resolve/reject are no-ops, so the tab-close
|
|
425
|
+
// error still wins the race.
|
|
426
|
+
runCmuxCode(tab.cmuxTab, {
|
|
394
427
|
code: opts.code,
|
|
395
428
|
timeoutMs: opts.timeoutMs,
|
|
396
|
-
signal:
|
|
429
|
+
signal: runSignal,
|
|
397
430
|
session: pending.session,
|
|
398
431
|
snapshot,
|
|
399
|
-
});
|
|
432
|
+
}).then(resolve, reject);
|
|
433
|
+
return await promise;
|
|
400
434
|
} finally {
|
|
401
435
|
tab.pending.delete(id);
|
|
402
436
|
}
|
|
@@ -460,6 +494,16 @@ export async function releaseTab(name: string, opts: ReleaseTabOptions = {}): Pr
|
|
|
460
494
|
} catch {}
|
|
461
495
|
}
|
|
462
496
|
for (const ctrl of pending.toolCalls.values()) ctrl.abort(closeError);
|
|
497
|
+
// Propagate the closure into the cmux run's abort signal so
|
|
498
|
+
// `wait(...)`, in-flight cmux socket calls, and the facade proxies
|
|
499
|
+
// unwind promptly. Firing this BEFORE `pending.reject` means
|
|
500
|
+
// `runCmuxCode` finishes with `ToolAbortError` and its `.then(reject)`
|
|
501
|
+
// is a no-op — `promise` still settles with the tab-close error via
|
|
502
|
+
// the `reject` call below. Without it, a run that isn't currently
|
|
503
|
+
// making a socket request (e.g. `await wait(60_000)`) would keep
|
|
504
|
+
// `runCmuxCode` blocked until timeout even after `pending.reject`
|
|
505
|
+
// unblocked the caller (issue #4499 review feedback).
|
|
506
|
+
pending.closeAc?.abort(closeError);
|
|
463
507
|
pending.reject(closeError);
|
|
464
508
|
}
|
|
465
509
|
tab.pending.clear();
|
package/src/tools/eval-render.ts
CHANGED
|
@@ -773,24 +773,4 @@ export const evalToolRenderer = {
|
|
|
773
773
|
|
|
774
774
|
mergeCallAndResult: true,
|
|
775
775
|
inline: true,
|
|
776
|
-
// Collapsed pending preview shows tail-window code cells; the result render
|
|
777
|
-
// interleaves each cell's output under its code, re-laying-out every row
|
|
778
|
-
// below the first cell. Expanded output is top-anchored enough for the
|
|
779
|
-
// transcript to commit its settled prefix.
|
|
780
|
-
provisionalPendingPreview: "collapsed",
|
|
781
|
-
// Partial-result chrome is NOT byte-stable: `renderAgentProgressEvents`
|
|
782
|
-
// inserts/removes each subagent's current-tool line as it starts/stops a
|
|
783
|
-
// tool, and ticks status icon/stats/duration on already-rendered rows,
|
|
784
|
-
// while `options.isPartial` holds for the whole eval() cell (agent
|
|
785
|
-
// progress ticks never carry an `async` completed/failed state, so
|
|
786
|
-
// `event-controller.ts` keeps `isPartial: true` throughout). If this
|
|
787
|
-
// block were commit-stable during that churn, `deriveLiveCommitState`'s
|
|
788
|
-
// stable-prefix ratchet would promote agent rows that keep mutating (a
|
|
789
|
-
// "slow ticker", see transcript-container.ts) into native scrollback,
|
|
790
|
-
// and the tui resync's "duplication, never loss" contract would
|
|
791
|
-
// repeatedly re-show the frame tail under a heavy concurrent
|
|
792
|
-
// `agent()`/`parallel()` fan-out — the overlapping/duplicated tree rows
|
|
793
|
-
// seen when many subagents run at once. Once the cell settles
|
|
794
|
-
// (`isPartial === false`) the block is commit-stable again.
|
|
795
|
-
provisionalPartialResult: true,
|
|
796
776
|
};
|
package/src/tools/read.ts
CHANGED
|
@@ -334,6 +334,24 @@ function contiguousLineNumbers(startLine: number, count: number): number[] {
|
|
|
334
334
|
return lines;
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
+
function lineNumbersFromSpans(spans: readonly { startLine: number; endLine: number }[]): number[] {
|
|
338
|
+
const lines: number[] = [];
|
|
339
|
+
for (const span of spans) {
|
|
340
|
+
for (let line = span.startLine; line <= span.endLine; line++) lines.push(line);
|
|
341
|
+
}
|
|
342
|
+
return lines;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function recordInMemorySeenLines(
|
|
346
|
+
session: ToolSession,
|
|
347
|
+
absolutePath: string | undefined,
|
|
348
|
+
fullText: string,
|
|
349
|
+
seenLines: readonly number[] | undefined,
|
|
350
|
+
): void {
|
|
351
|
+
if (!absolutePath || !path.isAbsolute(absolutePath) || !seenLines || seenLines.length === 0) return;
|
|
352
|
+
getFileSnapshotStore(session).record(canonicalSnapshotKey(absolutePath), normalizeToLF(fullText), seenLines);
|
|
353
|
+
}
|
|
354
|
+
|
|
337
355
|
function lineNumbersFromEntries(entries: readonly LineEntry[]): number[] {
|
|
338
356
|
const lines: number[] = [];
|
|
339
357
|
for (const entry of entries) {
|
|
@@ -1319,6 +1337,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1319
1337
|
: undefined;
|
|
1320
1338
|
let emittedHashlineHeader = false;
|
|
1321
1339
|
let seenLines: number[] | undefined;
|
|
1340
|
+
let rawSeenLines: number[] | undefined;
|
|
1322
1341
|
const formatText = (content: string, startNum: number): string => {
|
|
1323
1342
|
const lineCount = countTextLines(content);
|
|
1324
1343
|
details.displayContent = {
|
|
@@ -1382,10 +1401,12 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1382
1401
|
} else if (truncation.truncated) {
|
|
1383
1402
|
const outputLines = truncation.outputLines ?? countTextLines(truncation.content);
|
|
1384
1403
|
const endLineDisplay = startLineDisplay + Math.max(0, outputLines - 1);
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1404
|
+
if (options.raw === true) {
|
|
1405
|
+
rawSeenLines = contiguousLineNumbers(startLineDisplay, outputLines);
|
|
1406
|
+
outputText = formatText(truncation.content, startLineDisplay);
|
|
1407
|
+
} else {
|
|
1408
|
+
outputText = formatLineEntries(buildLineEntries(endLineDisplay), startLineDisplay);
|
|
1409
|
+
}
|
|
1389
1410
|
details.truncation = truncation;
|
|
1390
1411
|
truncationInfo = {
|
|
1391
1412
|
result: truncation,
|
|
@@ -1395,21 +1416,28 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1395
1416
|
const remaining = allLines.length - (startLine + userLimitedLines);
|
|
1396
1417
|
const nextOffset = startLine + userLimitedLines + 1;
|
|
1397
1418
|
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1419
|
+
if (options.raw === true) {
|
|
1420
|
+
rawSeenLines = contiguousLineNumbers(startLineDisplay, userLimitedLines);
|
|
1421
|
+
outputText = formatText(selectedContent, startLineDisplay);
|
|
1422
|
+
} else {
|
|
1423
|
+
outputText = formatLineEntries(buildLineEntries(endLine), startLineDisplay);
|
|
1424
|
+
}
|
|
1402
1425
|
outputText += `\n\n[${remaining} more lines in ${options.entityLabel}. Use :${nextOffset} to continue]`;
|
|
1403
1426
|
} else {
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1427
|
+
if (options.raw === true) {
|
|
1428
|
+
rawSeenLines = contiguousLineNumbers(startLineDisplay, endLine - startLine);
|
|
1429
|
+
outputText = formatText(truncation.content, startLineDisplay);
|
|
1430
|
+
} else {
|
|
1431
|
+
outputText = formatLineEntries(buildLineEntries(endLine), startLineDisplay);
|
|
1432
|
+
}
|
|
1408
1433
|
}
|
|
1409
1434
|
|
|
1410
1435
|
if (hashContext?.tag && options.sourcePath && seenLines) {
|
|
1411
1436
|
recordSeenLines(this.session, options.sourcePath, hashContext.tag, seenLines);
|
|
1412
1437
|
}
|
|
1438
|
+
if (options.raw === true && options.sourcePath && options.immutable !== true && rawSeenLines) {
|
|
1439
|
+
recordInMemorySeenLines(this.session, options.sourcePath, text, rawSeenLines);
|
|
1440
|
+
}
|
|
1413
1441
|
resultBuilder.text(outputText);
|
|
1414
1442
|
if (truncationInfo) {
|
|
1415
1443
|
resultBuilder.truncation(truncationInfo.result, truncationInfo.options);
|
|
@@ -1503,6 +1531,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1503
1531
|
if (hashContext?.tag && options.sourcePath && seenLines) {
|
|
1504
1532
|
recordSeenLines(this.session, options.sourcePath, hashContext.tag, seenLines);
|
|
1505
1533
|
}
|
|
1534
|
+
if (options.raw === true && options.sourcePath && options.immutable !== true && visibleSpans.length > 0) {
|
|
1535
|
+
recordInMemorySeenLines(this.session, options.sourcePath, text, lineNumbersFromSpans(visibleSpans));
|
|
1536
|
+
}
|
|
1506
1537
|
resultBuilder.text(finalText);
|
|
1507
1538
|
return resultBuilder.done();
|
|
1508
1539
|
}
|
|
@@ -1615,14 +1646,16 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1615
1646
|
}
|
|
1616
1647
|
if (cloned) displayLines = cloned;
|
|
1617
1648
|
}
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1649
|
+
if (displayLines.length > 0) {
|
|
1650
|
+
const endLine = range.startLine + displayLines.length - 1;
|
|
1651
|
+
visibleSpans.push({ startLine: range.startLine, endLine });
|
|
1652
|
+
for (let i = 0; i < displayLines.length; i++) {
|
|
1653
|
+
displayLineByNumber.set(range.startLine + i, displayLines[i] ?? "");
|
|
1654
|
+
}
|
|
1655
|
+
if (!fullLines || rawSelector) {
|
|
1656
|
+
const blockText = displayLines.join("\n");
|
|
1657
|
+
blocks.push(formatTextWithMode(blockText, range.startLine, shouldAddHashLines, shouldAddLineNumbers));
|
|
1658
|
+
}
|
|
1626
1659
|
}
|
|
1627
1660
|
}
|
|
1628
1661
|
|
|
@@ -1662,6 +1695,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1662
1695
|
recordSeenLinesFromBody(this.session, absolutePath, tag, outputText, clippedLines);
|
|
1663
1696
|
outputText = `${formatReadHashlineHeader(formatPathRelativeToCwd(absolutePath, this.session.cwd), tag)}\n${outputText}`;
|
|
1664
1697
|
}
|
|
1698
|
+
} else if (rawSelector && visibleSpans.length > 0) {
|
|
1699
|
+
const rawSeenLines = lineNumbersFromSpans(visibleSpans);
|
|
1700
|
+
if (rawSeenLines.length > 0) await recordFileSnapshot(this.session, absolutePath, rawSeenLines);
|
|
1665
1701
|
}
|
|
1666
1702
|
if (notices.length > 0) {
|
|
1667
1703
|
outputText = outputText ? `${outputText}\n${notices.join("\n")}` : notices.join("\n");
|
|
@@ -2669,6 +2705,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2669
2705
|
if (hashContext?.tag) {
|
|
2670
2706
|
recordSeenLinesFromBody(this.session, absolutePath, hashContext.tag, outputText, clippedLines);
|
|
2671
2707
|
}
|
|
2708
|
+
if (rawSelector && !firstLineExceedsLimit && collectedLines.length > 0) {
|
|
2709
|
+
await recordFileSnapshot(
|
|
2710
|
+
this.session,
|
|
2711
|
+
absolutePath,
|
|
2712
|
+
contiguousLineNumbers(startLineDisplay, collectedLines.length),
|
|
2713
|
+
);
|
|
2714
|
+
}
|
|
2672
2715
|
|
|
2673
2716
|
if (capturedDisplayContent) {
|
|
2674
2717
|
details.displayContent = capturedDisplayContent;
|
package/src/tools/renderers.ts
CHANGED
|
@@ -43,26 +43,6 @@ export type ToolRenderer = {
|
|
|
43
43
|
mergeCallAndResult?: boolean;
|
|
44
44
|
/** Render without background box, inline in the response flow */
|
|
45
45
|
inline?: boolean;
|
|
46
|
-
/**
|
|
47
|
-
* Whether pending-call rows are provisional: useful on screen while a tool is
|
|
48
|
-
* streaming, but not durable transcript history. `true` means every pending
|
|
49
|
-
* shape is provisional. `"collapsed"` means only the collapsed pending shape
|
|
50
|
-
* is provisional; expanded rendering is top-anchored/append-shaped enough to
|
|
51
|
-
* let the transcript commit its settled prefix. Absent = the pending preview
|
|
52
|
-
* streams rows the result render preserves.
|
|
53
|
-
*/
|
|
54
|
-
provisionalPendingPreview?: boolean | "collapsed";
|
|
55
|
-
/**
|
|
56
|
-
* Whether the partial-result render is provisional: chrome rows (header
|
|
57
|
-
* glyph, frame state) that change between `options.isPartial === true` and
|
|
58
|
-
* the final result render. When `true`, the block is treated as
|
|
59
|
-
* commit-unstable while a partial result is in flight, so the
|
|
60
|
-
* stable-prefix ratchet in `deriveLiveCommitState` cannot promote the
|
|
61
|
-
* partial chrome to native scrollback only to have the final render strand
|
|
62
|
-
* it above the settled frame. Absent = the partial render is byte-stable
|
|
63
|
-
* with the final render and may commit like any settled stream.
|
|
64
|
-
*/
|
|
65
|
-
provisionalPartialResult?: boolean;
|
|
66
46
|
/**
|
|
67
47
|
* Whether the renderer's pending-call path visibly consumes
|
|
68
48
|
* `options.spinnerFrame`. Used to avoid scheduling repaint ticks for live
|
package/src/tools/ssh.ts
CHANGED
|
@@ -386,22 +386,6 @@ export const sshToolRenderer = {
|
|
|
386
386
|
});
|
|
387
387
|
},
|
|
388
388
|
mergeCallAndResult: true,
|
|
389
|
-
// Pending call preview can re-anchor wholesale when the final result inserts
|
|
390
|
-
// the `Output` section, so no pending SSH rows may commit to native
|
|
391
|
-
// scrollback — even when expanded. The expanded pending shape was previously
|
|
392
|
-
// allowed to commit, which left two visible shapes in native scrollback once
|
|
393
|
-
// the result settled: a stale `⏳ SSH: [host]` header above the final frame,
|
|
394
|
-
// and the pending `╰──╯` footer reused in-place as the new `├── Output ──┤`
|
|
395
|
-
// separator with a fresh footer pushed below it.
|
|
396
|
-
provisionalPendingPreview: true,
|
|
397
|
-
// Partial-result chrome (pending icon and frame state) differs from the
|
|
398
|
-
// final SSH glyph/state, so the block stays commit-unstable while
|
|
399
|
-
// `options.isPartial` holds. Without this, a long-running SSH command's
|
|
400
|
-
// stable pending header would be promoted by the stable-prefix ratchet and
|
|
401
|
-
// committed to native scrollback, then the final render's SSH glyph would
|
|
402
|
-
// land below and strand a duplicate pending header above the final frame
|
|
403
|
-
// ([#3177](https://github.com/can1357/oh-my-pi/issues/3177)).
|
|
404
|
-
provisionalPartialResult: true,
|
|
405
389
|
// Streamed args can initially render the SSH placeholder (`⏳ SSH: […]` /
|
|
406
390
|
// `$ …`), then the first partial result inserts the `Output` section and
|
|
407
391
|
// re-anchors the frame. Force a full repaint at that seam so placeholder rows
|
package/src/tools/write.ts
CHANGED
|
@@ -974,7 +974,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
974
974
|
interface WriteRenderArgs {
|
|
975
975
|
path?: string;
|
|
976
976
|
file_path?: string;
|
|
977
|
-
content?:
|
|
977
|
+
content?: unknown;
|
|
978
978
|
}
|
|
979
979
|
|
|
980
980
|
const WRITE_PREVIEW_LINES = 6;
|
|
@@ -990,8 +990,14 @@ function formatLineCountSuffix(lineCount: number, uiTheme: Theme): string {
|
|
|
990
990
|
return uiTheme.fg("dim", ` · ${lineCount} line${lineCount === 1 ? "" : "s"}`);
|
|
991
991
|
}
|
|
992
992
|
|
|
993
|
-
function normalizeDisplayText(text:
|
|
994
|
-
|
|
993
|
+
function normalizeDisplayText(text: unknown): string {
|
|
994
|
+
let displayText = "";
|
|
995
|
+
if (typeof text === "string") {
|
|
996
|
+
displayText = text;
|
|
997
|
+
} else if (text !== undefined && text !== null) {
|
|
998
|
+
displayText = String(text);
|
|
999
|
+
}
|
|
1000
|
+
return displayText.replace(/\r/g, "");
|
|
995
1001
|
}
|
|
996
1002
|
|
|
997
1003
|
/**
|
|
@@ -1098,11 +1104,12 @@ export const writeToolRenderer = {
|
|
|
1098
1104
|
},
|
|
1099
1105
|
uiTheme,
|
|
1100
1106
|
);
|
|
1107
|
+
const content = normalizeDisplayText(args.content);
|
|
1101
1108
|
const streamingCache = createRenderedStringCache();
|
|
1102
1109
|
return framedBlock(uiTheme, width => {
|
|
1103
|
-
const body =
|
|
1110
|
+
const body = content
|
|
1104
1111
|
? formatStreamingContent(
|
|
1105
|
-
|
|
1112
|
+
content,
|
|
1106
1113
|
Boolean(options?.expanded),
|
|
1107
1114
|
lang,
|
|
1108
1115
|
uiTheme,
|
|
@@ -1130,7 +1137,7 @@ export const writeToolRenderer = {
|
|
|
1130
1137
|
): Component {
|
|
1131
1138
|
const rawPath = args?.file_path || args?.path || "";
|
|
1132
1139
|
const filePath = shortenPath(rawPath);
|
|
1133
|
-
const fileContent = args?.content
|
|
1140
|
+
const fileContent = normalizeDisplayText(args?.content);
|
|
1134
1141
|
const lang = getLanguageFromPath(rawPath);
|
|
1135
1142
|
const langIcon = uiTheme.fg("muted", uiTheme.getLangIcon(lang));
|
|
1136
1143
|
// The header shows the cwd-relative path but links to the absolute path the
|