@quandev104/pi-style 0.1.3 → 0.1.4
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 +6 -0
- package/dist/extensions/pi-style.js +379 -71
- package/dist/extensions/pi-style.js.map +1 -1
- package/extension-src/pi-style/domain/status.ts +1 -1
- package/extension-src/pi-style/features/tools/boxed/bash.ts +279 -45
- package/extension-src/pi-style/features/tools/boxed/edit.ts +28 -2
- package/extension-src/pi-style/features/tools/boxed/fallback.ts +47 -4
- package/extension-src/pi-style/features/tools/boxed/quick-edit.ts +25 -4
- package/extension-src/pi-style/features/tools/boxed/session-config.ts +64 -4
- package/extension-src/pi-style/features/tools/boxed/shared.ts +41 -1
- package/extension-src/pi-style/features/tools/boxed/write.ts +17 -1
- package/extension-src/pi-style/pi/session-coordinator.ts +9 -1
- package/extension-src/pi-style/shared/box.ts +78 -21
- package/package.json +1 -1
|
@@ -33,19 +33,79 @@ export function getToolsRenderConfig(): ToolsRenderConfig {
|
|
|
33
33
|
|
|
34
34
|
// Wall-clock elapsed tracking through the renderer context state (no tool
|
|
35
35
|
// re-registration, so result.details has no execution timing).
|
|
36
|
+
//
|
|
37
|
+
// Elapsed is computed live from the recorded start on every read, so a running
|
|
38
|
+
// tool keeps growing its displayed time. The value only freezes once the
|
|
39
|
+
// execution end is recorded (terminal result), which keeps the completed footer
|
|
40
|
+
// stable across later re-renders (expand toggles, terminal resizes).
|
|
36
41
|
|
|
37
42
|
const STARTED_AT_KEY = "__piStyleStartedAt";
|
|
38
|
-
const
|
|
43
|
+
const ENDED_AT_KEY = "__piStyleEndedAt";
|
|
44
|
+
const RESULT_SEEN_KEY = "__piStyleResultSeen";
|
|
45
|
+
const TICKER_KEY = "__piStyleElapsedTicker";
|
|
39
46
|
|
|
40
47
|
export function recordExecutionStarted(state: Record<string, unknown> | undefined, executionStarted: boolean): void {
|
|
41
48
|
if (!executionStarted || !state || typeof state !== "object") return;
|
|
42
49
|
if (typeof state[STARTED_AT_KEY] !== "number") state[STARTED_AT_KEY] = performance.now();
|
|
43
50
|
}
|
|
44
51
|
|
|
52
|
+
/** Freeze the elapsed at the first terminal render (idempotent). */
|
|
53
|
+
export function recordExecutionEnded(state: Record<string, unknown> | undefined): void {
|
|
54
|
+
if (!state || typeof state !== "object") return;
|
|
55
|
+
if (typeof state[ENDED_AT_KEY] !== "number") state[ENDED_AT_KEY] = performance.now();
|
|
56
|
+
}
|
|
57
|
+
|
|
45
58
|
export function getStateElapsedMs(state: Record<string, unknown> | undefined): number | undefined {
|
|
46
59
|
if (!state || typeof state !== "object") return undefined;
|
|
47
|
-
|
|
48
|
-
|
|
60
|
+
const started = state[STARTED_AT_KEY];
|
|
61
|
+
if (typeof started !== "number") return undefined;
|
|
62
|
+
const ended = state[ENDED_AT_KEY];
|
|
63
|
+
if (typeof ended === "number") return Math.max(0, ended - started);
|
|
64
|
+
return Math.max(0, performance.now() - started);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Whether a result renderer already produced a continuation for this call. */
|
|
68
|
+
export function isResultSeen(state: Record<string, unknown> | undefined): boolean {
|
|
69
|
+
return Boolean(state && typeof state === "object" && state[RESULT_SEEN_KEY] === true);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Record that a result renderer ran for this call (streaming or final). */
|
|
73
|
+
export function markResultSeen(state: Record<string, unknown> | undefined): void {
|
|
74
|
+
if (!state || typeof state !== "object") return;
|
|
75
|
+
state[RESULT_SEEN_KEY] = true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
type TickerHandle = ReturnType<typeof setInterval>;
|
|
79
|
+
|
|
80
|
+
/** States that currently own a 1s elapsed-render interval, for session cleanup. */
|
|
81
|
+
const tickerStates = new Set<Record<string, unknown>>();
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* While a tool is running, re-render once per second so live elapsed labels
|
|
85
|
+
* tick without any output events. Idempotent per state.
|
|
86
|
+
*/
|
|
87
|
+
export function startElapsedTicker(state: Record<string, unknown> | undefined, invalidate: () => void): void {
|
|
88
|
+
if (!state || typeof state !== "object") return;
|
|
89
|
+
if (state[TICKER_KEY] !== undefined) return;
|
|
90
|
+
state[TICKER_KEY] = setInterval(() => invalidate(), 1000) as unknown as TickerHandle;
|
|
91
|
+
tickerStates.add(state);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Stop a running tool's elapsed ticker (terminal result, error, session end). */
|
|
95
|
+
export function stopElapsedTicker(state: Record<string, unknown> | undefined): void {
|
|
96
|
+
if (!state || typeof state !== "object") return;
|
|
97
|
+
const handle = state[TICKER_KEY] as TickerHandle | undefined;
|
|
98
|
+
if (handle !== undefined) clearInterval(handle);
|
|
99
|
+
delete state[TICKER_KEY];
|
|
100
|
+
tickerStates.delete(state);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Stop every elapsed ticker (session start/shutdown). */
|
|
104
|
+
export function stopAllElapsedTickers(): void {
|
|
105
|
+
for (const state of tickerStates) {
|
|
106
|
+
const handle = state[TICKER_KEY] as TickerHandle | undefined;
|
|
107
|
+
if (handle !== undefined) clearInterval(handle);
|
|
108
|
+
delete state[TICKER_KEY];
|
|
49
109
|
}
|
|
50
|
-
|
|
110
|
+
tickerStates.clear();
|
|
51
111
|
}
|
|
@@ -13,12 +13,21 @@ import {
|
|
|
13
13
|
resolveRelativePath,
|
|
14
14
|
shortenPath,
|
|
15
15
|
} from "../../../shared/box.js";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
getStateElapsedMs,
|
|
18
|
+
isResultSeen,
|
|
19
|
+
markResultSeen,
|
|
20
|
+
recordExecutionEnded,
|
|
21
|
+
recordExecutionStarted,
|
|
22
|
+
startElapsedTicker,
|
|
23
|
+
stopElapsedTicker,
|
|
24
|
+
} from "./session-config.js";
|
|
17
25
|
|
|
18
26
|
/** Renderer context delivered by Pi's ToolExecutionComponent (getRenderContext). */
|
|
19
27
|
export interface BoxedToolContext {
|
|
20
28
|
readonly args: Record<string, unknown>;
|
|
21
29
|
readonly toolCallId: string;
|
|
30
|
+
readonly invalidate: () => void;
|
|
22
31
|
readonly state: Record<string, unknown>;
|
|
23
32
|
readonly cwd: string;
|
|
24
33
|
readonly executionStarted: boolean;
|
|
@@ -88,6 +97,7 @@ export function compactCall(
|
|
|
88
97
|
isError: Boolean(options.context.isError),
|
|
89
98
|
isPartial: Boolean(options.context.isPartial),
|
|
90
99
|
isPending: pendingFlag(options.context),
|
|
100
|
+
running: Boolean(options.context.executionStarted),
|
|
91
101
|
});
|
|
92
102
|
}
|
|
93
103
|
|
|
@@ -96,6 +106,36 @@ export function noteExecutionStart(context: BoxedToolContext): void {
|
|
|
96
106
|
recordExecutionStarted(context.state, context.executionStarted);
|
|
97
107
|
}
|
|
98
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Keep running/ended execution state in sync from a call renderer pass. While
|
|
111
|
+
* the tool runs, a 1s re-render ticker keeps live elapsed labels current; once
|
|
112
|
+
* the call renders in its terminal form the elapsed freezes.
|
|
113
|
+
*/
|
|
114
|
+
export function noteBoxedCallState(context: BoxedToolContext): void {
|
|
115
|
+
if (!context.executionStarted) return;
|
|
116
|
+
if (context.isPartial) startElapsedTicker(context.state, context.invalidate);
|
|
117
|
+
else {
|
|
118
|
+
recordExecutionEnded(context.state);
|
|
119
|
+
stopElapsedTicker(context.state);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Record a result renderer pass and keep the ticker/ended state in sync.
|
|
125
|
+
* Returns whether this is the first result pass for the call, so renderers can
|
|
126
|
+
* render nothing while the pending/running call card stands alone.
|
|
127
|
+
*/
|
|
128
|
+
export function noteBoxedResultPhase(context: BoxedToolContext, isPartial: boolean): boolean {
|
|
129
|
+
const firstResultPass = !isResultSeen(context.state);
|
|
130
|
+
markResultSeen(context.state);
|
|
131
|
+
if (isPartial) startElapsedTicker(context.state, context.invalidate);
|
|
132
|
+
else {
|
|
133
|
+
recordExecutionEnded(context.state);
|
|
134
|
+
stopElapsedTicker(context.state);
|
|
135
|
+
}
|
|
136
|
+
return firstResultPass;
|
|
137
|
+
}
|
|
138
|
+
|
|
99
139
|
export function stateElapsedMs(context: BoxedToolContext): number | undefined {
|
|
100
140
|
return getStateElapsedMs(context.state);
|
|
101
141
|
}
|
|
@@ -34,6 +34,14 @@ import {
|
|
|
34
34
|
/** Right-side bottom-border hint shown when the compact preview is truncated. */
|
|
35
35
|
const WRITE_EXPAND_HINT = "Ctrl+O for more";
|
|
36
36
|
|
|
37
|
+
/** Partial-pass result: the compact call keeps its `◌ Running` card. */
|
|
38
|
+
const EMPTY_WRITE_RESULT: Component = Object.freeze({
|
|
39
|
+
invalidate() {},
|
|
40
|
+
render() {
|
|
41
|
+
return [];
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
37
45
|
type NumberedLine = { number: string; content: string };
|
|
38
46
|
|
|
39
47
|
/**
|
|
@@ -66,6 +74,7 @@ function renderWritePreviewBox(
|
|
|
66
74
|
state?: Record<string, unknown>;
|
|
67
75
|
isError: boolean;
|
|
68
76
|
isPending: boolean;
|
|
77
|
+
running?: boolean;
|
|
69
78
|
expanded: boolean;
|
|
70
79
|
},
|
|
71
80
|
): Component {
|
|
@@ -78,6 +87,7 @@ function renderWritePreviewBox(
|
|
|
78
87
|
...(options.state ? { state: options.state } : {}),
|
|
79
88
|
isError: options.isError,
|
|
80
89
|
isPending: options.isPending,
|
|
90
|
+
running: Boolean(options.running),
|
|
81
91
|
bodyLines: () => {
|
|
82
92
|
if (preview.length === 0) return [];
|
|
83
93
|
const shown = preview.slice(0, budget).map((line) => formatNumberedLine(theme, line));
|
|
@@ -107,10 +117,11 @@ export const writeTool: BoxedToolDefinition = {
|
|
|
107
117
|
state: context.state,
|
|
108
118
|
isError: Boolean(context.isError),
|
|
109
119
|
isPending: Boolean(context.isPartial),
|
|
120
|
+
running: Boolean(context.executionStarted),
|
|
110
121
|
expanded: Boolean(context.expanded),
|
|
111
122
|
});
|
|
112
123
|
},
|
|
113
|
-
result(result,
|
|
124
|
+
result(result, options, theme, context) {
|
|
114
125
|
clearFooterState(context);
|
|
115
126
|
const output = getTextOutput(result);
|
|
116
127
|
const detail = displayPath(String(context?.args?.path ?? context?.args?.file_path ?? ""), context);
|
|
@@ -124,6 +135,11 @@ export const writeTool: BoxedToolDefinition = {
|
|
|
124
135
|
});
|
|
125
136
|
}
|
|
126
137
|
|
|
138
|
+
// While the result is still streaming, don't stamp a metrics footer into
|
|
139
|
+
// the shared state: the compact call keeps its `◌ Running` card and only
|
|
140
|
+
// closes with `elapsed · words` once the tool settles.
|
|
141
|
+
if (options.isPartial) return EMPTY_WRITE_RESULT;
|
|
142
|
+
|
|
127
143
|
// Success (compact and expanded): the preview box closes with the metrics
|
|
128
144
|
// footer stored into the shared renderer state; the result adds nothing.
|
|
129
145
|
return compactFooterWithState(theme, result, context);
|
|
@@ -7,7 +7,11 @@ import { setSpecialBlockTheme } from "../features/messages/special-blocks.js";
|
|
|
7
7
|
import { resetBashTreeRegistry } from "../features/tools/boxed/bash.js";
|
|
8
8
|
import { resetBatchRegistry } from "../features/tools/boxed/batch.js";
|
|
9
9
|
import { resetGrepRegistry } from "../features/tools/boxed/grep.js";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
setToolsRenderConfig,
|
|
12
|
+
stopAllElapsedTickers,
|
|
13
|
+
type ToolsRenderConfig,
|
|
14
|
+
} from "../features/tools/boxed/session-config.js";
|
|
11
15
|
import { createCompatibilityCoordinator } from "./compatibility-coordinator.js";
|
|
12
16
|
import {
|
|
13
17
|
type CompatibilityCleanupResult,
|
|
@@ -150,6 +154,9 @@ export function createPiStyleSessionCoordinator(pi: ExtensionAPI, hooks: Compati
|
|
|
150
154
|
resetBatchRegistry();
|
|
151
155
|
resetGrepRegistry();
|
|
152
156
|
resetBashTreeRegistry();
|
|
157
|
+
// Stop any 1s elapsed re-render ticker left by a tool that was still
|
|
158
|
+
// running when the session ended.
|
|
159
|
+
stopAllElapsedTickers();
|
|
153
160
|
active = false;
|
|
154
161
|
await app.reload();
|
|
155
162
|
productGate = app.productPolicy.corePatchGate;
|
|
@@ -216,6 +223,7 @@ export function createPiStyleSessionCoordinator(pi: ExtensionAPI, hooks: Compati
|
|
|
216
223
|
resetBatchRegistry();
|
|
217
224
|
resetGrepRegistry();
|
|
218
225
|
resetBashTreeRegistry();
|
|
226
|
+
stopAllElapsedTickers();
|
|
219
227
|
app.sessionShutdown();
|
|
220
228
|
// Tier C prototype patches stay installed across session switches. Pi renders
|
|
221
229
|
// the restored chat (renderBeforeBind) AFTER session_shutdown but BEFORE the
|
|
@@ -43,7 +43,15 @@ export interface BoxedRenderOptions {
|
|
|
43
43
|
isError?: boolean;
|
|
44
44
|
isPartial?: boolean;
|
|
45
45
|
isPending?: boolean;
|
|
46
|
+
/** Execution has started but the tool is still running (title `◌` instead of `✓`). */
|
|
47
|
+
running?: boolean;
|
|
48
|
+
/** A result renderer already produced a continuation for this call, so the call
|
|
49
|
+
* leaves the box open instead of closing it with a pending label. */
|
|
50
|
+
resultSeen?: boolean;
|
|
46
51
|
pendingText?: string;
|
|
52
|
+
/** Verbatim bottom-border label for the pending/running card (overrides the
|
|
53
|
+
* `… ${pendingText}` default, e.g. a live `◌ Running · 12.4s` status). */
|
|
54
|
+
pendingLabel?: string;
|
|
47
55
|
state?: Record<string, unknown>;
|
|
48
56
|
/** Wall-clock elapsed override (used when metrics are not in result.details). */
|
|
49
57
|
elapsedMs?: number;
|
|
@@ -333,22 +341,45 @@ function formatBoxedStatusIcon(theme: BoxTheme, isError?: boolean): string {
|
|
|
333
341
|
|
|
334
342
|
/**
|
|
335
343
|
* Colored `➔ Name` prefix for tool titles (identity color). The status glyph
|
|
336
|
-
* (
|
|
344
|
+
* (✓/◌/✗) is appended separately by formatBoxedToolTitle.
|
|
337
345
|
*/
|
|
338
346
|
export function formatToolTitlePrefix(theme: BoxTheme, name: string): string {
|
|
339
347
|
return colorFromExtra(theme, "bashPromptColor", "bashMode", `➔ ${name}`);
|
|
340
348
|
}
|
|
341
349
|
|
|
342
|
-
export
|
|
350
|
+
export type BoxedTitleStatus = "running" | "pending";
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Boxed tool title: `➔ Name ✓` when settled, `➔ Name ◌` while running, plain
|
|
354
|
+
* `➔ Name` while pending, and a fully error-colored `➔ Name ✗` on failure.
|
|
355
|
+
* The ✓/◌ glyphs are never shown before the tool settles, so a card that is
|
|
356
|
+
* still executing never reads as finished.
|
|
357
|
+
*/
|
|
358
|
+
export function formatBoxedToolTitle(
|
|
359
|
+
theme: BoxTheme,
|
|
360
|
+
name: string,
|
|
361
|
+
isError?: boolean,
|
|
362
|
+
status?: BoxedTitleStatus,
|
|
363
|
+
): string {
|
|
343
364
|
// On failure the whole title turns error-colored (not just the ✗) so a failed
|
|
344
365
|
// tool reads instantly; on success the tool keeps its identity color and only
|
|
345
366
|
// the ✓ carries the success color.
|
|
346
367
|
const coloredTitle = isError
|
|
347
368
|
? theme.fg("error", `➔ ${name} ✗`)
|
|
348
|
-
:
|
|
369
|
+
: status === "running"
|
|
370
|
+
? `${formatToolTitlePrefix(theme, name)} ${theme.fg("text", "◌")}`
|
|
371
|
+
: status === "pending"
|
|
372
|
+
? formatToolTitlePrefix(theme, name)
|
|
373
|
+
: `${formatToolTitlePrefix(theme, name)} ${formatBoxedStatusIcon(theme, false)}`;
|
|
349
374
|
return typeof theme?.bold === "function" ? theme.bold(coloredTitle) : coloredTitle;
|
|
350
375
|
}
|
|
351
376
|
|
|
377
|
+
/** Live running status label for pending/running cards and streaming footers. */
|
|
378
|
+
export function formatBoxedRunningStatus(theme: BoxTheme, elapsedMs: number | undefined): string {
|
|
379
|
+
const elapsed = elapsedMs === undefined ? "" : `${theme.fg("text", ` · ${(elapsedMs / 1000).toFixed(1)}s`)}`;
|
|
380
|
+
return `${theme.fg("dim", "◌ Running")}${elapsed}`;
|
|
381
|
+
}
|
|
382
|
+
|
|
352
383
|
function boxText(theme: BoxTheme, text: string): string {
|
|
353
384
|
return `${RESET_INTENSITY}${theme.fg("borderMuted", text)}`;
|
|
354
385
|
}
|
|
@@ -571,7 +602,12 @@ export function renderBoxedToolCall(
|
|
|
571
602
|
},
|
|
572
603
|
render(width: number): string[] {
|
|
573
604
|
if (cache?.width === width) return cache.lines;
|
|
574
|
-
const title = formatBoxedToolTitle(
|
|
605
|
+
const title = formatBoxedToolTitle(
|
|
606
|
+
theme,
|
|
607
|
+
toolName,
|
|
608
|
+
options.isError,
|
|
609
|
+
options.isPending ? (options.running ? "running" : "pending") : undefined,
|
|
610
|
+
);
|
|
575
611
|
const headerLabel = options.headerDetail ? `${title} · ${options.headerDetail}` : title;
|
|
576
612
|
const renderedWidth = boxWidth(width);
|
|
577
613
|
const lines = [
|
|
@@ -579,22 +615,26 @@ export function renderBoxedToolCall(
|
|
|
579
615
|
boxBlankLine(theme, renderedWidth),
|
|
580
616
|
...detailLines.flatMap((line) => boxedWrappedLines(theme, line, renderedWidth)),
|
|
581
617
|
];
|
|
582
|
-
if (options.isPending) {
|
|
583
|
-
|
|
618
|
+
if (options.isPending && !options.resultSeen) {
|
|
619
|
+
// Pending/running card: close the box with the status label. Once a
|
|
620
|
+
// result renderer has produced a continuation, the box stays open and
|
|
621
|
+
// that continuation closes it.
|
|
622
|
+
const pendingLabel =
|
|
623
|
+
options.pendingLabel ?? theme.fg("dim", `… ${options.pendingText ?? "Waiting for output…"}`);
|
|
584
624
|
lines.push(
|
|
585
625
|
boxBlankLine(theme, renderedWidth),
|
|
586
626
|
boxLabeledBorder(
|
|
587
627
|
theme,
|
|
588
628
|
BOX_ROUND_BOTTOM_LEFT,
|
|
589
629
|
BOX_ROUND_BOTTOM_RIGHT,
|
|
590
|
-
|
|
630
|
+
pendingLabel,
|
|
591
631
|
undefined,
|
|
592
632
|
renderedWidth,
|
|
593
633
|
),
|
|
594
634
|
);
|
|
595
635
|
} else {
|
|
596
636
|
// Leave the box open with trailing breathing room; the result renderer
|
|
597
|
-
// continues it with the
|
|
637
|
+
// continues it with the result divider.
|
|
598
638
|
lines.push(boxBlankLine(theme, renderedWidth));
|
|
599
639
|
}
|
|
600
640
|
cache = { width, lines };
|
|
@@ -624,7 +664,12 @@ export function renderCompactBoxedToolCall(
|
|
|
624
664
|
invalidate() {},
|
|
625
665
|
render(width: number): string[] {
|
|
626
666
|
const renderedWidth = boxWidth(width);
|
|
627
|
-
const title = formatBoxedToolTitle(
|
|
667
|
+
const title = formatBoxedToolTitle(
|
|
668
|
+
theme,
|
|
669
|
+
toolName,
|
|
670
|
+
options.isError,
|
|
671
|
+
options.isPending ? (options.running ? "running" : "pending") : undefined,
|
|
672
|
+
);
|
|
628
673
|
const headerLabel = detailLine ? `${title} · ${detailLine}` : title;
|
|
629
674
|
const compactFooter =
|
|
630
675
|
typeof options.state?.[COMPACT_FOOTER_KEY] === "string" ? options.state[COMPACT_FOOTER_KEY] : "";
|
|
@@ -649,19 +694,23 @@ export function renderCompactBoxedToolCall(
|
|
|
649
694
|
),
|
|
650
695
|
);
|
|
651
696
|
} else if (options.isPending) {
|
|
652
|
-
const
|
|
697
|
+
const pendingLabel =
|
|
698
|
+
options.pendingLabel ??
|
|
699
|
+
(options.running
|
|
700
|
+
? formatBoxedRunningStatus(theme, undefined)
|
|
701
|
+
: theme.fg("dim", `… ${options.pendingText ?? "Waiting for output…"}`));
|
|
653
702
|
lines.push(
|
|
654
703
|
boxLabeledBorder(
|
|
655
704
|
theme,
|
|
656
705
|
BOX_ROUND_BOTTOM_LEFT,
|
|
657
706
|
BOX_ROUND_BOTTOM_RIGHT,
|
|
658
|
-
|
|
707
|
+
pendingLabel,
|
|
659
708
|
options.bottomRightLabel,
|
|
660
709
|
renderedWidth,
|
|
661
710
|
),
|
|
662
711
|
);
|
|
663
712
|
} else {
|
|
664
|
-
// No footer yet (transient, or the result opens the
|
|
713
|
+
// No footer yet (transient, or the result opens the result divider):
|
|
665
714
|
// leave the box open so the result renderer continues the same box.
|
|
666
715
|
}
|
|
667
716
|
return lines;
|
|
@@ -689,6 +738,10 @@ export function renderBoxedToolResult(
|
|
|
689
738
|
expandHint?: string;
|
|
690
739
|
isError?: boolean;
|
|
691
740
|
isPartial?: boolean;
|
|
741
|
+
/** Skip the result divider entirely (streaming continuation into an open call box). */
|
|
742
|
+
showDivider?: boolean;
|
|
743
|
+
/** Error state marker prepended to the body (default `✗ Error`). */
|
|
744
|
+
errorLabel?: string;
|
|
692
745
|
} = {},
|
|
693
746
|
): Component {
|
|
694
747
|
let cache: RenderLinesCache | null = null;
|
|
@@ -702,7 +755,7 @@ export function renderBoxedToolResult(
|
|
|
702
755
|
const renderedWidth = boxWidth(width);
|
|
703
756
|
const maxContentWidth = boxInnerWidth(renderedWidth);
|
|
704
757
|
const bodyLines = typeof body === "function" ? body(maxContentWidth) : body.render(maxContentWidth);
|
|
705
|
-
const errorPrefix = options.isError ? [theme.fg("error", "✗ Error")] : [];
|
|
758
|
+
const errorPrefix = options.isError ? [theme.fg("error", options.errorLabel ?? "✗ Error")] : [];
|
|
706
759
|
const outputLines =
|
|
707
760
|
bodyLines.length > 0
|
|
708
761
|
? [...errorPrefix, ...bodyLines]
|
|
@@ -713,14 +766,18 @@ export function renderBoxedToolResult(
|
|
|
713
766
|
? options.dividerLabel(renderedWidth)
|
|
714
767
|
: (options.dividerLabel ?? "Response");
|
|
715
768
|
const rendered = [
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
769
|
+
...(options.showDivider === false
|
|
770
|
+
? []
|
|
771
|
+
: [
|
|
772
|
+
boxLabeledBorder(
|
|
773
|
+
theme,
|
|
774
|
+
BOX_DIVIDER_LEFT,
|
|
775
|
+
BOX_DIVIDER_RIGHT,
|
|
776
|
+
theme.fg("dim", dividerText),
|
|
777
|
+
options.dividerRightLabel ? theme.fg("dim", options.dividerRightLabel) : undefined,
|
|
778
|
+
renderedWidth,
|
|
779
|
+
),
|
|
780
|
+
]),
|
|
724
781
|
boxBlankLine(theme, renderedWidth),
|
|
725
782
|
...renderBoxedOutputLines(theme, outputLines, renderedWidth, options.renderLineBudget ?? outputLines.length),
|
|
726
783
|
boxBlankLine(theme, renderedWidth),
|