@oh-my-pi/pi-tui 18.8.6 → 18.8.7
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 -16
- package/README.md +7 -5
- package/dist/types/chat/assistant-message.d.ts +1 -0
- package/dist/types/chat/compaction-summary-message.d.ts +1 -0
- package/dist/types/chat/user-message.d.ts +2 -0
- package/dist/types/components/box.d.ts +1 -0
- package/dist/types/components/disclosure.d.ts +1 -0
- package/dist/types/components/image.d.ts +1 -0
- package/dist/types/components/key-value-list.d.ts +1 -0
- package/dist/types/components/layout/row.d.ts +1 -0
- package/dist/types/components/layout/stack.d.ts +1 -0
- package/dist/types/components/markdown.d.ts +1 -0
- package/dist/types/components/scroll-view.d.ts +1 -0
- package/dist/types/components/section.d.ts +1 -0
- package/dist/types/components/table.d.ts +1 -0
- package/dist/types/components/text.d.ts +1 -0
- package/dist/types/components/truncated-text.d.ts +1 -0
- package/dist/types/native/blobs.d.ts +3 -1
- package/dist/types/overlays/session-selector.d.ts +1 -1
- package/dist/types/render/output-pane.d.ts +1 -0
- package/dist/types/render/render-utils.d.ts +1 -0
- package/dist/types/render/tool-card.d.ts +6 -0
- package/dist/types/render/width-aware-text.d.ts +2 -0
- package/dist/types/terminal.d.ts +10 -2
- package/dist/types/tools/write.d.ts +1 -1
- package/dist/types/tools/yield.d.ts +22 -0
- package/dist/types/tui.d.ts +11 -0
- package/package.json +9 -9
- package/src/chat/advisor-message.ts +11 -0
- package/src/chat/assistant-message.ts +6 -0
- package/src/chat/compaction-summary-message.ts +8 -0
- package/src/chat/read-tool-group.ts +6 -4
- package/src/chat/skill-message.ts +6 -0
- package/src/chat/table-chart.ts +17 -20
- package/src/chat/tool-execution.ts +18 -0
- package/src/chat/user-message.ts +14 -0
- package/src/chrome/transcript-container.ts +32 -1
- package/src/components/box.ts +5 -0
- package/src/components/disclosure.ts +8 -0
- package/src/components/image.ts +7 -5
- package/src/components/key-value-list.ts +4 -0
- package/src/components/layout/row.ts +7 -0
- package/src/components/layout/stack.ts +7 -0
- package/src/components/markdown.ts +8 -0
- package/src/components/scroll-view.ts +5 -0
- package/src/components/section.ts +5 -0
- package/src/components/table.ts +4 -0
- package/src/components/text.ts +7 -0
- package/src/components/truncated-text.ts +5 -0
- package/src/native/backend.ts +16 -15
- package/src/native/blobs.ts +43 -10
- package/src/native/reconcile.ts +4 -0
- package/src/overlays/session-selector.ts +29 -19
- package/src/overlays/snapcompact-shape-preview.ts +2 -3
- package/src/prompt/attachment-chips.ts +13 -10
- package/src/render/output-pane.ts +6 -0
- package/src/render/render-utils.ts +11 -0
- package/src/render/tool-card.ts +15 -0
- package/src/render/width-aware-text.ts +5 -0
- package/src/terminal.ts +24 -3
- package/src/tools/bash.ts +10 -11
- package/src/tools/edit.ts +142 -120
- package/src/tools/fetch.ts +5 -6
- package/src/tools/index.ts +2 -0
- package/src/tools/mcp.ts +15 -2
- package/src/tools/read.ts +7 -5
- package/src/tools/write.ts +70 -56
- package/src/tools/yield.ts +225 -0
- package/src/tui.ts +25 -0
|
@@ -1128,6 +1128,9 @@ export function createCachedComponent(
|
|
|
1128
1128
|
invalidate() {
|
|
1129
1129
|
cached = undefined;
|
|
1130
1130
|
},
|
|
1131
|
+
releaseRenderCaches() {
|
|
1132
|
+
cached = undefined;
|
|
1133
|
+
},
|
|
1131
1134
|
};
|
|
1132
1135
|
}
|
|
1133
1136
|
|
|
@@ -1160,6 +1163,14 @@ export function invalidateRenderedStringCache(cache: RenderedStringCache): void
|
|
|
1160
1163
|
cache.theme = null;
|
|
1161
1164
|
}
|
|
1162
1165
|
|
|
1166
|
+
export function releaseRenderedStringCache(cache: RenderedStringCache): void {
|
|
1167
|
+
cache.theme = null;
|
|
1168
|
+
cache.expanded = false;
|
|
1169
|
+
cache.salt = "";
|
|
1170
|
+
cache.content = "";
|
|
1171
|
+
cache.value = "";
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1163
1174
|
/** Reuse a rendered string while its theme, expansion, content, and salt match. */
|
|
1164
1175
|
export function cachedRenderedString(
|
|
1165
1176
|
cache: RenderedStringCache | undefined,
|
package/src/render/tool-card.ts
CHANGED
|
@@ -69,6 +69,11 @@ export interface ToolCardOptions {
|
|
|
69
69
|
paddingY?: number;
|
|
70
70
|
ignoreTight?: boolean;
|
|
71
71
|
onInvalidate?: () => void;
|
|
72
|
+
/**
|
|
73
|
+
* Drop builder-owned memos the next build recomputes. Called by
|
|
74
|
+
* {@link ToolCard.releaseRenderCaches}, which never calls `onInvalidate`.
|
|
75
|
+
*/
|
|
76
|
+
onReleaseRenderCaches?: () => void;
|
|
72
77
|
onDispose?: () => void;
|
|
73
78
|
}
|
|
74
79
|
|
|
@@ -381,6 +386,16 @@ export class ToolCard implements Component {
|
|
|
381
386
|
this.#options.onInvalidate?.();
|
|
382
387
|
}
|
|
383
388
|
|
|
389
|
+
releaseRenderCaches(): void {
|
|
390
|
+
this.#block.invalidate();
|
|
391
|
+
this.#lastBlockOptions = undefined;
|
|
392
|
+
this.#plainKey = "";
|
|
393
|
+
this.#plainText.setText("");
|
|
394
|
+
this.#plainText.releaseRenderCaches();
|
|
395
|
+
for (const child of this.#renderedChildren) child.releaseRenderCaches?.();
|
|
396
|
+
this.#options.onReleaseRenderCaches?.();
|
|
397
|
+
}
|
|
398
|
+
|
|
384
399
|
dispose(): void {
|
|
385
400
|
if (this.#disposed) return;
|
|
386
401
|
this.#disposed = true;
|
|
@@ -51,6 +51,11 @@ export class WidthAwareText implements Component {
|
|
|
51
51
|
this.#inner.invalidate();
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/** The formatted text stays: the inner `Text` renders from it, so dropping it would free nothing. */
|
|
55
|
+
releaseRenderCaches(): void {
|
|
56
|
+
this.#inner.releaseRenderCaches();
|
|
57
|
+
}
|
|
58
|
+
|
|
54
59
|
/**
|
|
55
60
|
* Re-run the formatter on the next render because its inputs changed.
|
|
56
61
|
* Unlike {@link invalidate}, the inner `Text` keeps its wrap cache, so a
|
package/src/terminal.ts
CHANGED
|
@@ -501,7 +501,12 @@ export const CURSOR_SHAPE_CODES: Record<CursorShape, number> = {
|
|
|
501
501
|
bar: 6,
|
|
502
502
|
};
|
|
503
503
|
export interface Terminal {
|
|
504
|
-
|
|
504
|
+
/**
|
|
505
|
+
* Start the terminal with input, resize, and host-disconnect handlers.
|
|
506
|
+
* Unless input is deferred, this sends capability probes; a terminal that
|
|
507
|
+
* cannot parse one may leave concealed bytes on the cursor row, so the
|
|
508
|
+
* caller repaints after starting.
|
|
509
|
+
*/
|
|
505
510
|
start(
|
|
506
511
|
onInput: (data: string) => void,
|
|
507
512
|
onResize: () => void,
|
|
@@ -513,8 +518,10 @@ export interface Terminal {
|
|
|
513
518
|
* Take ownership of stdin after a `deferInput` start: enable raw mode,
|
|
514
519
|
* attach input handlers, and run the capability probes start() skipped.
|
|
515
520
|
* Bytes the user typed in cooked mode meanwhile are replayed through
|
|
516
|
-
* `onInput`. No-op when input was never deferred.
|
|
517
|
-
*
|
|
521
|
+
* `onInput`. No-op when input was never deferred. Like {@link start}, the
|
|
522
|
+
* probes may leave concealed bytes on the cursor row, so the caller
|
|
523
|
+
* repaints afterwards. Optional so custom Terminals built against older
|
|
524
|
+
* pi-tui versions keep working.
|
|
518
525
|
*/
|
|
519
526
|
enableInput?(): void;
|
|
520
527
|
|
|
@@ -707,6 +714,17 @@ function parseOsc99KeyValues(section: string): Map<string, string> {
|
|
|
707
714
|
}
|
|
708
715
|
return values;
|
|
709
716
|
}
|
|
717
|
+
/**
|
|
718
|
+
* Brackets the startup capability probes. A terminal that cannot parse a probe
|
|
719
|
+
* prints it as text at the cursor (Apple Terminal drops `ESC _`/`ESC \` and
|
|
720
|
+
* prints APC payloads, and abandons DECRQM at its `$`, printing the final `p`).
|
|
721
|
+
* DECSC, autowrap off, and SGR 8 (conceal) keep that text invisible and on the
|
|
722
|
+
* cursor row, so it can neither wrap nor scroll the screen; DECRC puts the
|
|
723
|
+
* cursor back. The screen owner repaints the row afterwards (see
|
|
724
|
+
* {@link Terminal.start} / {@link Terminal.enableInput}).
|
|
725
|
+
*/
|
|
726
|
+
const PROBE_GUARD_BEGIN = "\x1b7\x1b[?7l\x1b[8m";
|
|
727
|
+
const PROBE_GUARD_END = "\x1b[28m\x1b[?7h\x1b8";
|
|
710
728
|
const XTERM_SCROLL_TO_BOTTOM_MODES = [1010, 1011] as const;
|
|
711
729
|
type Osc11QueryRoute = "direct" | "tmux";
|
|
712
730
|
const TMUX_OSC11_CACHE_REFRESH_DELAY_MS = 100;
|
|
@@ -1099,6 +1117,8 @@ export class ProcessTerminal implements Terminal {
|
|
|
1099
1117
|
// events that lose modifier information. Must run after setRawMode(true)
|
|
1100
1118
|
// since that resets console mode flags.
|
|
1101
1119
|
this.#enableWindowsVTInput();
|
|
1120
|
+
// Every response-eliciting probe goes out inside the leak guard.
|
|
1121
|
+
this.#safeWrite(PROBE_GUARD_BEGIN);
|
|
1102
1122
|
// Query and enable Kitty keyboard protocol
|
|
1103
1123
|
// The query handler intercepts input temporarily, then installs the user's handler
|
|
1104
1124
|
// See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
|
|
@@ -1161,6 +1181,7 @@ export class ProcessTerminal implements Terminal {
|
|
|
1161
1181
|
for (const mode of XTERM_SCROLL_TO_BOTTOM_MODES) {
|
|
1162
1182
|
this.#queryPrivateMode(mode);
|
|
1163
1183
|
}
|
|
1184
|
+
this.#safeWrite(PROBE_GUARD_END);
|
|
1164
1185
|
}
|
|
1165
1186
|
|
|
1166
1187
|
/**
|
package/src/tools/bash.ts
CHANGED
|
@@ -494,6 +494,15 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
494
494
|
let cachedIsPartial: boolean | undefined;
|
|
495
495
|
let cachedPreviewWindow: number | undefined;
|
|
496
496
|
let cachedSnapshot: ToolCardSnapshot | undefined;
|
|
497
|
+
const dropSnapshot = () => {
|
|
498
|
+
cachedSnapshot = undefined;
|
|
499
|
+
cachedWidth = undefined;
|
|
500
|
+
cachedPreviewLines = undefined;
|
|
501
|
+
cachedExpanded = undefined;
|
|
502
|
+
cachedRawOutput = undefined;
|
|
503
|
+
cachedIsPartial = undefined;
|
|
504
|
+
cachedPreviewWindow = undefined;
|
|
505
|
+
};
|
|
497
506
|
|
|
498
507
|
return framedToolCard(
|
|
499
508
|
uiTheme,
|
|
@@ -596,17 +605,7 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
596
605
|
cachedSnapshot = snapshot;
|
|
597
606
|
return snapshot;
|
|
598
607
|
},
|
|
599
|
-
{
|
|
600
|
-
onInvalidate: () => {
|
|
601
|
-
cachedSnapshot = undefined;
|
|
602
|
-
cachedWidth = undefined;
|
|
603
|
-
cachedPreviewLines = undefined;
|
|
604
|
-
cachedExpanded = undefined;
|
|
605
|
-
cachedRawOutput = undefined;
|
|
606
|
-
cachedIsPartial = undefined;
|
|
607
|
-
cachedPreviewWindow = undefined;
|
|
608
|
-
},
|
|
609
|
-
},
|
|
608
|
+
{ onInvalidate: dropSnapshot, onReleaseRenderCaches: dropSnapshot },
|
|
610
609
|
);
|
|
611
610
|
},
|
|
612
611
|
describeCall(args: TArgs, options: RenderResultOptions): NativeToolView {
|
package/src/tools/edit.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
invalidateRenderedStringCache,
|
|
37
37
|
PREVIEW_LIMITS,
|
|
38
38
|
previewWindowRows,
|
|
39
|
+
releaseRenderedStringCache,
|
|
39
40
|
type RenderedStringCache,
|
|
40
41
|
replaceTabs,
|
|
41
42
|
shortenPath,
|
|
@@ -1110,41 +1111,49 @@ export const editToolRenderer = {
|
|
|
1110
1111
|
return renderInlineEditRow(uiTheme, { op, rename, rawPath, pending: true });
|
|
1111
1112
|
}
|
|
1112
1113
|
const callPreviewCaches: RenderedStringCache[] = [];
|
|
1113
|
-
return framedToolCard(
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1114
|
+
return framedToolCard(
|
|
1115
|
+
uiTheme,
|
|
1116
|
+
({ width }) => {
|
|
1117
|
+
// No status icon on the head row: it's the head of the framed block,
|
|
1118
|
+
// and native-scrollback commits are prefix-only — an animated glyph
|
|
1119
|
+
// would pin the commit boundary at the top, and the pending hourglass
|
|
1120
|
+
// just adds noise. The liveness cue rides the trailing "(preview)" /
|
|
1121
|
+
// "(streaming)" line instead.
|
|
1122
|
+
const header = renderEditHeader(width, uiTheme, {
|
|
1123
|
+
op,
|
|
1124
|
+
rawPath,
|
|
1125
|
+
rename,
|
|
1126
|
+
extraSuffix: fileCount > 1 ? uiTheme.fg("dim", ` (+${fileCount - 1} more)`) : undefined,
|
|
1127
|
+
});
|
|
1128
|
+
let body = getCallPreview(
|
|
1129
|
+
editArgs,
|
|
1130
|
+
rawPath,
|
|
1131
|
+
width,
|
|
1132
|
+
uiTheme,
|
|
1133
|
+
renderContext,
|
|
1134
|
+
options.expanded,
|
|
1135
|
+
options?.spinnerFrame,
|
|
1136
|
+
callPreviewCaches,
|
|
1137
|
+
);
|
|
1138
|
+
if (applyPatchError) {
|
|
1139
|
+
body += `\n${uiTheme.fg("error", truncateToWidth(replaceTabs(applyPatchError), Math.max(1, width - 2)))}`;
|
|
1140
|
+
}
|
|
1141
|
+
const bodyLines = body ? body.split("\n") : [];
|
|
1142
|
+
while (bodyLines.length > 0 && bodyLines[0].trim() === "") bodyLines.shift();
|
|
1143
|
+
return {
|
|
1144
|
+
header,
|
|
1145
|
+
sections: bodyLines.length > 0 ? [{ content: bodyLines }] : [],
|
|
1146
|
+
phase: applyPatchError ? "error" : "pending",
|
|
1147
|
+
borderColor: applyPatchError ? "error" : "borderMuted",
|
|
1148
|
+
contentPaddingLeft: 0,
|
|
1149
|
+
};
|
|
1150
|
+
},
|
|
1151
|
+
{
|
|
1152
|
+
onReleaseRenderCaches: () => {
|
|
1153
|
+
for (const cache of callPreviewCaches) releaseRenderedStringCache(cache);
|
|
1154
|
+
},
|
|
1155
|
+
},
|
|
1156
|
+
);
|
|
1148
1157
|
},
|
|
1149
1158
|
|
|
1150
1159
|
describeCall(
|
|
@@ -1452,74 +1461,54 @@ function renderSingleFileResult(
|
|
|
1452
1461
|
const renderedDiffCache = createRenderedStringCache();
|
|
1453
1462
|
const statsSuffixCache = createRenderedStringCache();
|
|
1454
1463
|
|
|
1455
|
-
return framedToolCard(
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
(editDiffPreview && "firstChangedLine" in editDiffPreview ? editDiffPreview.firstChangedLine : undefined) ||
|
|
1472
|
-
(details && !isError ? details.firstChangedLine : undefined);
|
|
1473
|
-
const linkPath = details && "path" in details ? details.path : undefined;
|
|
1474
|
-
|
|
1475
|
-
// Change stats ride inline on the header bar next to the path.
|
|
1476
|
-
const previewDiff = editDiffPreview && !("error" in editDiffPreview) ? editDiffPreview.diff : undefined;
|
|
1477
|
-
const headerDiff = isError ? undefined : details?.diff || previewDiff;
|
|
1478
|
-
const statsSuffix = headerDiff
|
|
1479
|
-
? cachedRenderedString(statsSuffixCache, uiTheme, false, "", headerDiff, () =>
|
|
1480
|
-
formatDiffStatsSuffix(headerDiff, uiTheme),
|
|
1481
|
-
)
|
|
1482
|
-
: "";
|
|
1483
|
-
const header = renderEditHeader(width, uiTheme, {
|
|
1484
|
-
icon: isError ? "error" : "success",
|
|
1485
|
-
iconOverride: !isError && !options.isPartial ? uiTheme.styledSymbol("tool.edit", "accent") : undefined,
|
|
1486
|
-
op,
|
|
1487
|
-
rawPath,
|
|
1488
|
-
rename,
|
|
1489
|
-
firstChangedLine,
|
|
1490
|
-
linkPath,
|
|
1491
|
-
statsSuffix,
|
|
1492
|
-
});
|
|
1493
|
-
const innerWidth = Math.max(1, width - 2);
|
|
1494
|
-
|
|
1495
|
-
let body = "";
|
|
1496
|
-
if (isError) {
|
|
1497
|
-
if (errorText) body = uiTheme.fg("error", replaceTabs(errorText));
|
|
1498
|
-
} else if (details?.diff) {
|
|
1499
|
-
body = renderDiffSection(
|
|
1500
|
-
details.diff,
|
|
1501
|
-
rawPath,
|
|
1502
|
-
expanded,
|
|
1503
|
-
innerWidth,
|
|
1504
|
-
uiTheme,
|
|
1505
|
-
renderDiffFn,
|
|
1506
|
-
renderedDiffCache,
|
|
1507
|
-
diffSectionCache,
|
|
1508
|
-
);
|
|
1509
|
-
} else if (details) {
|
|
1510
|
-
// Authoritative result with no textual diff: a delete, a move-only
|
|
1511
|
-
// rename, or a genuine no-op. The header already names the op
|
|
1512
|
-
// (Delete / `src → dst`); only a true no-op needs an explanatory
|
|
1513
|
-
// body so an empty card isn't mistaken for a stalled edit.
|
|
1514
|
-
if (op !== "delete" && op !== "create" && !rename) {
|
|
1515
|
-
const noChangePath = linkPath ? shortenPath(linkPath) : rawPath ? shortenPath(rawPath) : "";
|
|
1516
|
-
body = uiTheme.fg("dim", `No changes were made${noChangePath ? ` to ${noChangePath}` : ""}.`);
|
|
1464
|
+
return framedToolCard(
|
|
1465
|
+
uiTheme,
|
|
1466
|
+
({ width }) => {
|
|
1467
|
+
const { expanded, renderContext } = options;
|
|
1468
|
+
// A finalized result is authoritative: its `details` describe exactly
|
|
1469
|
+
// what happened. The shared streaming `editDiffPreview` is a call-phase
|
|
1470
|
+
// artifact (in a batch it reflects only the first file), so consulting it
|
|
1471
|
+
// for an empty-diff delete/move/no-op result mislabels the card. Fall
|
|
1472
|
+
// back to the preview only when no details exist yet.
|
|
1473
|
+
const editDiffPreview = details ? undefined : renderContext?.editDiffPreview;
|
|
1474
|
+
const renderDiffFn = renderContext?.renderDiff ?? renderFallbackDiff;
|
|
1475
|
+
|
|
1476
|
+
if (diffSectionRenderDiffFn !== renderDiffFn) {
|
|
1477
|
+
diffSectionRenderDiffFn = renderDiffFn;
|
|
1478
|
+
invalidateRenderedStringCache(diffSectionCache);
|
|
1479
|
+
invalidateRenderedStringCache(renderedDiffCache);
|
|
1517
1480
|
}
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1481
|
+
const firstChangedLine =
|
|
1482
|
+
(editDiffPreview && "firstChangedLine" in editDiffPreview ? editDiffPreview.firstChangedLine : undefined) ||
|
|
1483
|
+
(details && !isError ? details.firstChangedLine : undefined);
|
|
1484
|
+
const linkPath = details && "path" in details ? details.path : undefined;
|
|
1485
|
+
|
|
1486
|
+
// Change stats ride inline on the header bar next to the path.
|
|
1487
|
+
const previewDiff = editDiffPreview && !("error" in editDiffPreview) ? editDiffPreview.diff : undefined;
|
|
1488
|
+
const headerDiff = isError ? undefined : details?.diff || previewDiff;
|
|
1489
|
+
const statsSuffix = headerDiff
|
|
1490
|
+
? cachedRenderedString(statsSuffixCache, uiTheme, false, "", headerDiff, () =>
|
|
1491
|
+
formatDiffStatsSuffix(headerDiff, uiTheme),
|
|
1492
|
+
)
|
|
1493
|
+
: "";
|
|
1494
|
+
const header = renderEditHeader(width, uiTheme, {
|
|
1495
|
+
icon: isError ? "error" : "success",
|
|
1496
|
+
iconOverride: !isError && !options.isPartial ? uiTheme.styledSymbol("tool.edit", "accent") : undefined,
|
|
1497
|
+
op,
|
|
1498
|
+
rawPath,
|
|
1499
|
+
rename,
|
|
1500
|
+
firstChangedLine,
|
|
1501
|
+
linkPath,
|
|
1502
|
+
statsSuffix,
|
|
1503
|
+
});
|
|
1504
|
+
const innerWidth = Math.max(1, width - 2);
|
|
1505
|
+
|
|
1506
|
+
let body = "";
|
|
1507
|
+
if (isError) {
|
|
1508
|
+
if (errorText) body = uiTheme.fg("error", replaceTabs(errorText));
|
|
1509
|
+
} else if (details?.diff) {
|
|
1521
1510
|
body = renderDiffSection(
|
|
1522
|
-
|
|
1511
|
+
details.diff,
|
|
1523
1512
|
rawPath,
|
|
1524
1513
|
expanded,
|
|
1525
1514
|
innerWidth,
|
|
@@ -1528,27 +1517,56 @@ function renderSingleFileResult(
|
|
|
1528
1517
|
renderedDiffCache,
|
|
1529
1518
|
diffSectionCache,
|
|
1530
1519
|
);
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1520
|
+
} else if (details) {
|
|
1521
|
+
// Authoritative result with no textual diff: a delete, a move-only
|
|
1522
|
+
// rename, or a genuine no-op. The header already names the op
|
|
1523
|
+
// (Delete / `src → dst`); only a true no-op needs an explanatory
|
|
1524
|
+
// body so an empty card isn't mistaken for a stalled edit.
|
|
1525
|
+
if (op !== "delete" && op !== "create" && !rename) {
|
|
1526
|
+
const noChangePath = linkPath ? shortenPath(linkPath) : rawPath ? shortenPath(rawPath) : "";
|
|
1527
|
+
body = uiTheme.fg("dim", `No changes were made${noChangePath ? ` to ${noChangePath}` : ""}.`);
|
|
1528
|
+
}
|
|
1529
|
+
} else if (editDiffPreview) {
|
|
1530
|
+
if ("error" in editDiffPreview) body = uiTheme.fg("error", replaceTabs(editDiffPreview.error));
|
|
1531
|
+
else if (editDiffPreview.diff)
|
|
1532
|
+
body = renderDiffSection(
|
|
1533
|
+
editDiffPreview.diff,
|
|
1534
|
+
rawPath,
|
|
1535
|
+
expanded,
|
|
1536
|
+
innerWidth,
|
|
1537
|
+
uiTheme,
|
|
1538
|
+
renderDiffFn,
|
|
1539
|
+
renderedDiffCache,
|
|
1540
|
+
diffSectionCache,
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
if (details?.diagnostics) {
|
|
1544
|
+
body += formatDiagnostics(details.diagnostics, expanded, uiTheme, (fp: string) =>
|
|
1545
|
+
uiTheme.getLangIcon(getLanguageFromPath(fp)),
|
|
1546
|
+
);
|
|
1547
|
+
}
|
|
1537
1548
|
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1549
|
+
// Diff lines self-wrap with a continuation gutter; pre-wrap to the frame's
|
|
1550
|
+
// inner width so renderOutputBlock's generic wrap is a no-op. Edit frames
|
|
1551
|
+
// use a flush left border because code-frame gutters already provide padding.
|
|
1552
|
+
const bodyLines =
|
|
1553
|
+
body.length > 0 ? body.split("\n").flatMap(line => wrapEditRendererLine(line, innerWidth)) : [];
|
|
1554
|
+
while (bodyLines.length > 0 && bodyLines[0].trim() === "") bodyLines.shift();
|
|
1543
1555
|
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1556
|
+
return {
|
|
1557
|
+
header,
|
|
1558
|
+
sections: bodyLines.length > 0 ? [{ content: bodyLines }] : [],
|
|
1559
|
+
phase: isError ? "error" : options.isPartial ? "partial" : "success",
|
|
1560
|
+
borderColor: isError ? "error" : "borderMuted",
|
|
1561
|
+
contentPaddingLeft: 0,
|
|
1562
|
+
};
|
|
1563
|
+
},
|
|
1564
|
+
{
|
|
1565
|
+
// Only the width-keyed diff section goes; the highlighted diff and stats suffix do not depend on width, and
|
|
1566
|
+
// re-highlighting every committed card on each resize replay costs seconds.
|
|
1567
|
+
onReleaseRenderCaches: () => releaseRenderedStringCache(diffSectionCache),
|
|
1568
|
+
},
|
|
1569
|
+
);
|
|
1552
1570
|
}
|
|
1553
1571
|
|
|
1554
1572
|
function renderMultiFileResult(
|
|
@@ -1603,5 +1621,9 @@ function renderMultiFileResult(
|
|
|
1603
1621
|
cached = undefined;
|
|
1604
1622
|
for (const c of fileComponents) c.invalidate?.();
|
|
1605
1623
|
},
|
|
1624
|
+
releaseRenderCaches() {
|
|
1625
|
+
cached = undefined;
|
|
1626
|
+
for (const c of fileComponents) c.releaseRenderCaches?.();
|
|
1627
|
+
},
|
|
1606
1628
|
};
|
|
1607
1629
|
}
|
package/src/tools/fetch.ts
CHANGED
|
@@ -282,6 +282,10 @@ export function renderReadUrlResult(
|
|
|
282
282
|
|
|
283
283
|
let lastExpanded: boolean | undefined;
|
|
284
284
|
let contentPreviewLines: string[] | undefined;
|
|
285
|
+
const dropPreview = () => {
|
|
286
|
+
lastExpanded = undefined;
|
|
287
|
+
contentPreviewLines = undefined;
|
|
288
|
+
};
|
|
285
289
|
return framedToolCard(
|
|
286
290
|
uiTheme,
|
|
287
291
|
() => {
|
|
@@ -315,11 +319,6 @@ export function renderReadUrlResult(
|
|
|
315
319
|
applyBg: false,
|
|
316
320
|
};
|
|
317
321
|
},
|
|
318
|
-
{
|
|
319
|
-
onInvalidate: () => {
|
|
320
|
-
lastExpanded = undefined;
|
|
321
|
-
contentPreviewLines = undefined;
|
|
322
|
-
},
|
|
323
|
-
},
|
|
322
|
+
{ onInvalidate: dropPreview, onReleaseRenderCaches: dropPreview },
|
|
324
323
|
);
|
|
325
324
|
}
|
package/src/tools/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { todoToolRenderer } from "./todo";
|
|
|
27
27
|
import { createVibeToolRenderer } from "./vibe";
|
|
28
28
|
import { webSearchToolRenderer } from "./web-search";
|
|
29
29
|
import { writeToolRenderer } from "./write";
|
|
30
|
+
import { yieldToolRenderer } from "./yield";
|
|
30
31
|
import { setXdevRendererLookup } from "./xdev";
|
|
31
32
|
|
|
32
33
|
export * from "./renderer";
|
|
@@ -67,6 +68,7 @@ export const toolRenderers: Record<string, ToolRenderer> = {
|
|
|
67
68
|
vibe_kill: createVibeToolRenderer("kill"),
|
|
68
69
|
vibe_list: createVibeToolRenderer("list"),
|
|
69
70
|
write: writeToolRenderer,
|
|
71
|
+
yield: yieldToolRenderer,
|
|
70
72
|
};
|
|
71
73
|
|
|
72
74
|
// Wire the xd:// render delegation without the xdev module importing this registry.
|
package/src/tools/mcp.ts
CHANGED
|
@@ -121,7 +121,15 @@ function renderMarkdownMCPResult(
|
|
|
121
121
|
applyBg: false,
|
|
122
122
|
};
|
|
123
123
|
},
|
|
124
|
-
{
|
|
124
|
+
{
|
|
125
|
+
paddingX: 0,
|
|
126
|
+
paddingY: 0,
|
|
127
|
+
onInvalidate: () => (bodyMemo = undefined),
|
|
128
|
+
onReleaseRenderCaches: () => {
|
|
129
|
+
bodyMemo = undefined;
|
|
130
|
+
markdown.releaseRenderCaches();
|
|
131
|
+
},
|
|
132
|
+
},
|
|
125
133
|
);
|
|
126
134
|
}
|
|
127
135
|
|
|
@@ -208,7 +216,12 @@ export function renderMCPResult(
|
|
|
208
216
|
}
|
|
209
217
|
return { status, phase, body: bodyMemo.lines, applyBg: false };
|
|
210
218
|
},
|
|
211
|
-
{
|
|
219
|
+
{
|
|
220
|
+
paddingX: 0,
|
|
221
|
+
paddingY: 0,
|
|
222
|
+
onInvalidate: () => (bodyMemo = undefined),
|
|
223
|
+
onReleaseRenderCaches: () => (bodyMemo = undefined),
|
|
224
|
+
},
|
|
212
225
|
);
|
|
213
226
|
|
|
214
227
|
function buildMcpResultBody(contentWidth: number): string[] {
|
package/src/tools/read.ts
CHANGED
|
@@ -600,6 +600,11 @@ export const readToolRenderer = {
|
|
|
600
600
|
let cachedWidth: number | undefined;
|
|
601
601
|
let cachedExpanded: boolean | undefined;
|
|
602
602
|
let cachedLines: string[] | undefined;
|
|
603
|
+
const dropCache = () => {
|
|
604
|
+
cachedWidth = undefined;
|
|
605
|
+
cachedExpanded = undefined;
|
|
606
|
+
cachedLines = undefined;
|
|
607
|
+
};
|
|
603
608
|
return markFramedBlockComponent({
|
|
604
609
|
render: (width: number) => {
|
|
605
610
|
const expanded = options.expanded;
|
|
@@ -634,11 +639,8 @@ export const readToolRenderer = {
|
|
|
634
639
|
cachedExpanded = expanded;
|
|
635
640
|
return cachedLines;
|
|
636
641
|
},
|
|
637
|
-
invalidate:
|
|
638
|
-
|
|
639
|
-
cachedExpanded = undefined;
|
|
640
|
-
cachedLines = undefined;
|
|
641
|
-
},
|
|
642
|
+
invalidate: dropCache,
|
|
643
|
+
releaseRenderCaches: dropCache,
|
|
642
644
|
});
|
|
643
645
|
},
|
|
644
646
|
describeCall(args: ReadRenderArgs): NativeToolView {
|