@quandev104/pi-style 0.1.3 → 0.1.5
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 +30 -0
- package/README.md +8 -4
- package/dist/extensions/pi-style.js +3777 -958
- package/dist/extensions/pi-style.js.map +1 -1
- package/extension-src/pi-style/app/command-service.ts +2 -0
- package/extension-src/pi-style/domain/config-normalization.ts +21 -5
- package/extension-src/pi-style/domain/config-presets.ts +1 -1
- package/extension-src/pi-style/domain/config-types.ts +7 -3
- package/extension-src/pi-style/domain/status.ts +1 -1
- package/extension-src/pi-style/domain/theme.ts +6 -1
- package/extension-src/pi-style/features/editor/index.ts +169 -27
- package/extension-src/pi-style/features/messages/index.ts +66 -0
- package/extension-src/pi-style/features/tools/bash-execution.ts +112 -0
- package/extension-src/pi-style/features/tools/boxed/bash.ts +430 -172
- package/extension-src/pi-style/features/tools/boxed/batch.ts +50 -27
- package/extension-src/pi-style/features/tools/boxed/command-shape.ts +136 -0
- 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/find.ts +2 -2
- package/extension-src/pi-style/features/tools/boxed/gh.ts +1012 -0
- package/extension-src/pi-style/features/tools/boxed/git.ts +1960 -0
- package/extension-src/pi-style/features/tools/boxed/grep.ts +2 -2
- package/extension-src/pi-style/features/tools/boxed/output-tree.ts +9 -10
- package/extension-src/pi-style/features/tools/boxed/quick-edit.ts +25 -4
- package/extension-src/pi-style/features/tools/boxed/read.ts +3 -3
- 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 +19 -2
- package/extension-src/pi-style/pi/compatibility-coordinator.ts +17 -0
- package/extension-src/pi-style/pi/compatibility-probe.ts +104 -10
- package/extension-src/pi-style/pi/compatibility-registry.ts +19 -3
- package/extension-src/pi-style/pi/index.ts +18 -1
- package/extension-src/pi-style/pi/session-coordinator.ts +33 -1
- package/extension-src/pi-style/shared/box.ts +86 -25
- package/extension-src/pi-style/shared/split-diff.ts +9 -9
- package/package.json +1 -1
- package/themes/titanium-light.json +82 -0
- package/themes/titanium.json +79 -0
- package/themes/.gitkeep +0 -0
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import type { Component } from "@earendil-works/pi-tui";
|
|
15
15
|
import { stripAnsi } from "../../../shared/ansi.js";
|
|
16
|
-
import { type BoxTheme, getTextOutput, shortenPath } from "../../../shared/box.js";
|
|
16
|
+
import { type BoxTheme, dimLine, getTextOutput, shortenPath } from "../../../shared/box.js";
|
|
17
17
|
import { safeTruncateToWidth } from "../../../shared/render-budget.js";
|
|
18
18
|
import {
|
|
19
19
|
type GrepMatch,
|
|
@@ -111,7 +111,7 @@ function renderErrorLines(theme: BoxTheme, errorText: string, width: number): st
|
|
|
111
111
|
.map((line) => line.trim())
|
|
112
112
|
.filter((line) => line.length > 0);
|
|
113
113
|
if (raw.length === 0) return [];
|
|
114
|
-
const prefix = `${TREE_INDENT}${
|
|
114
|
+
const prefix = `${TREE_INDENT}${dimLine("└─")} `;
|
|
115
115
|
const out = raw
|
|
116
116
|
.slice(0, GREP_ERROR_LINES)
|
|
117
117
|
.map((line) => safeTruncateToWidth(`${prefix}${theme.fg("error", line)}`, Math.max(1, width), "…"));
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
// as one visual family.
|
|
21
21
|
|
|
22
22
|
import type { BoxTheme } from "../../../shared/box.js";
|
|
23
|
+
import { dimLine } from "../../../shared/box.js";
|
|
23
24
|
import { safeTruncateToWidth } from "../../../shared/render-budget.js";
|
|
24
25
|
|
|
25
26
|
/** Indent for top-level tree rows; matches the quiet-tool batch panel. */
|
|
@@ -251,11 +252,11 @@ export function renderOutputTree(
|
|
|
251
252
|
const lastIndex = visible.length - 1;
|
|
252
253
|
for (let i = 0; i < visible.length; i++) {
|
|
253
254
|
const branch = i < lastIndex || more > 0 ? "├─" : "└─";
|
|
254
|
-
const line = `${indent}${
|
|
255
|
+
const line = `${indent}${dimLine(branch)} ${theme.fg(entryColor, label(visible[i] ?? ""))}`;
|
|
255
256
|
out.push(safeTruncateToWidth(line, safeWidth, "…"));
|
|
256
257
|
}
|
|
257
258
|
if (more > 0) {
|
|
258
|
-
const line = `${indent}${
|
|
259
|
+
const line = `${indent}${dimLine("└─")} ${theme.fg("dim", `… ${more} more ${pluralForm(moreUnit, more)}`)}`;
|
|
259
260
|
out.push(safeTruncateToWidth(line, safeWidth, "…"));
|
|
260
261
|
}
|
|
261
262
|
return out;
|
|
@@ -274,7 +275,7 @@ function formatMatchRow(theme: BoxTheme, match: GrepMatch): string {
|
|
|
274
275
|
// Match rows render in the output text color (not primary) so they read like
|
|
275
276
|
// the matched code; only the file nodes carry the primary color.
|
|
276
277
|
const label = theme.fg("toolOutput", `*${match.line}`);
|
|
277
|
-
const sep =
|
|
278
|
+
const sep = dimLine("│");
|
|
278
279
|
return `${label}${sep} ${theme.fg("toolOutput", match.content)}`;
|
|
279
280
|
}
|
|
280
281
|
|
|
@@ -313,7 +314,7 @@ export function renderGrepTree(
|
|
|
313
314
|
if (singleFile) {
|
|
314
315
|
budget.forEach((match, index) => {
|
|
315
316
|
const isLast = index === totalVisible - 1 && !truncated;
|
|
316
|
-
push(`${indent}${
|
|
317
|
+
push(`${indent}${dimLine(isLast ? "└─" : "├─")} ${formatMatchRow(theme, match)}`);
|
|
317
318
|
});
|
|
318
319
|
} else {
|
|
319
320
|
// Walk the budget, tracking position within each file group so the file
|
|
@@ -323,7 +324,7 @@ export function renderGrepTree(
|
|
|
323
324
|
const group = groups[gi];
|
|
324
325
|
if (!group) continue;
|
|
325
326
|
const isLastGroup = gi === groups.length - 1;
|
|
326
|
-
const trunk = isLastGroup ? " " :
|
|
327
|
+
const trunk = isLastGroup ? " " : dimLine("│");
|
|
327
328
|
|
|
328
329
|
const visibleHere: GrepMatch[] = [];
|
|
329
330
|
for (const match of group.matches) {
|
|
@@ -336,22 +337,20 @@ export function renderGrepTree(
|
|
|
336
337
|
const groupIsLastRendered = shown >= totalVisible && !truncated;
|
|
337
338
|
const fileLabel = options.withIcons ? `${fileIcon(group.file)} ${group.file}` : group.file;
|
|
338
339
|
// File nodes use the primary (accent) color, matching read/ls/find paths.
|
|
339
|
-
push(`${indent}${
|
|
340
|
+
push(`${indent}${dimLine(groupIsLastRendered ? "└─" : "├─")} ${theme.fg("accent", fileLabel)}`);
|
|
340
341
|
|
|
341
342
|
visibleHere.forEach((match, index) => {
|
|
342
343
|
const isLastInGroup = index === visibleHere.length - 1;
|
|
343
344
|
const isLastOverall = groupIsLastRendered && isLastInGroup;
|
|
344
345
|
push(
|
|
345
|
-
`${indent}${trunk}${TREE_CHILD_INDENT}${
|
|
346
|
+
`${indent}${trunk}${TREE_CHILD_INDENT}${dimLine(isLastOverall ? "└─" : "├─")} ${formatMatchRow(theme, match)}`,
|
|
346
347
|
);
|
|
347
348
|
});
|
|
348
349
|
}
|
|
349
350
|
}
|
|
350
351
|
|
|
351
352
|
if (truncated) {
|
|
352
|
-
push(
|
|
353
|
-
`${indent}${theme.fg("borderMuted", "└─")} ${theme.fg("dim", `… ${remaining} more ${pluralForm("match", remaining)}`)}`,
|
|
354
|
-
);
|
|
353
|
+
push(`${indent}${dimLine("└─")} ${theme.fg("dim", `… ${remaining} more ${pluralForm("match", remaining)}`)}`);
|
|
355
354
|
}
|
|
356
355
|
return out;
|
|
357
356
|
}
|
|
@@ -4,19 +4,35 @@ import { getLanguageFromPath } from "@earendil-works/pi-coding-agent";
|
|
|
4
4
|
import { stripAnsi } from "../../../shared/ansi.js";
|
|
5
5
|
import {
|
|
6
6
|
type BoxTheme,
|
|
7
|
-
|
|
7
|
+
formatBoxedRunningStatus,
|
|
8
8
|
getTextOutput,
|
|
9
9
|
renderBoxedToolCall,
|
|
10
10
|
renderBoxedToolResult,
|
|
11
11
|
} from "../../../shared/box.js";
|
|
12
12
|
import { formatElapsedMs, getElapsedMs } from "../../../shared/elapsed.js";
|
|
13
13
|
import { AdaptiveDiffComponent, buildSplitRows, countDiffStats } from "../../../shared/split-diff.js";
|
|
14
|
-
import { getStateElapsedMs } from "./session-config.js";
|
|
15
|
-
import {
|
|
14
|
+
import { getStateElapsedMs, isResultSeen } from "./session-config.js";
|
|
15
|
+
import {
|
|
16
|
+
type BoxedToolContext,
|
|
17
|
+
type BoxedToolDefinition,
|
|
18
|
+
displayPath,
|
|
19
|
+
noteBoxedCallState,
|
|
20
|
+
noteBoxedResultPhase,
|
|
21
|
+
noteExecutionStart,
|
|
22
|
+
stateElapsedMs,
|
|
23
|
+
} from "./shared.js";
|
|
16
24
|
|
|
17
25
|
const MAX_HIGHLIGHT_DIFF_CHARS = 12000;
|
|
18
26
|
const MAX_HIGHLIGHT_DIFF_ROWS = 120;
|
|
19
27
|
|
|
28
|
+
/** First-partial-pass result: the pending/running call card stands alone. */
|
|
29
|
+
const EMPTY_QUICK_EDIT_RESULT = Object.freeze({
|
|
30
|
+
invalidate() {},
|
|
31
|
+
render() {
|
|
32
|
+
return [];
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
20
36
|
interface QuickEditToolConfig {
|
|
21
37
|
toolLabel: string;
|
|
22
38
|
applyingLabel: string;
|
|
@@ -131,10 +147,12 @@ function renderQuickEditResult(
|
|
|
131
147
|
config: QuickEditToolConfig,
|
|
132
148
|
) {
|
|
133
149
|
if (options.isPartial) {
|
|
150
|
+
const firstResultPass = noteBoxedResultPhase(context, options.isPartial);
|
|
151
|
+
if (firstResultPass) return EMPTY_QUICK_EDIT_RESULT;
|
|
134
152
|
return renderBoxedToolResult(
|
|
135
153
|
theme,
|
|
136
154
|
() => [`${theme.fg("dim", "↳")} ${theme.fg("muted", `Applying ${config.applyingLabel}...`)}`],
|
|
137
|
-
{ isPartial: true },
|
|
155
|
+
{ showDivider: false, footerLines: [formatBoxedRunningStatus(theme, stateElapsedMs(context))], isPartial: true },
|
|
138
156
|
);
|
|
139
157
|
}
|
|
140
158
|
|
|
@@ -196,12 +214,15 @@ export function quickEditTool(config: QuickEditToolConfig): BoxedToolDefinition
|
|
|
196
214
|
return {
|
|
197
215
|
call(args, theme, context) {
|
|
198
216
|
noteExecutionStart(context);
|
|
217
|
+
noteBoxedCallState(context);
|
|
199
218
|
const detail = displayPath(String(args?.path ?? ""), context);
|
|
200
219
|
return renderBoxedToolCall(theme, config.toolLabel, [], {
|
|
201
220
|
headerDetail: detail,
|
|
202
221
|
isError: Boolean(context.isError),
|
|
203
222
|
isPartial: Boolean(context.isPartial),
|
|
204
223
|
isPending: Boolean(context.isPartial),
|
|
224
|
+
running: Boolean(context.executionStarted),
|
|
225
|
+
resultSeen: isResultSeen(context.state),
|
|
205
226
|
});
|
|
206
227
|
},
|
|
207
228
|
result(result, options, theme, context) {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Boxed read tool renderer
|
|
2
2
|
// (renderCall/renderResult only; no tool re-registration).
|
|
3
3
|
//
|
|
4
|
-
// Read calls render
|
|
5
|
-
// consecutive reads group into one panel (see
|
|
6
|
-
//
|
|
4
|
+
// Read calls render boxless: a lone read is a single inline line
|
|
5
|
+
// (`➔ Read <path>`), consecutive reads group into one tree panel (see
|
|
6
|
+
// batch.ts).
|
|
7
7
|
|
|
8
8
|
import { stripAnsi } from "../../../shared/ansi.js";
|
|
9
9
|
import { getTextOutput } from "../../../shared/box.js";
|
|
@@ -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
|
}
|
|
@@ -15,6 +15,7 @@ import { stripAnsi } from "../../../shared/ansi.js";
|
|
|
15
15
|
import {
|
|
16
16
|
type BoxTheme,
|
|
17
17
|
boxedToolWidthKey,
|
|
18
|
+
dimLine,
|
|
18
19
|
getTextOutput,
|
|
19
20
|
renderBoxedToolResult,
|
|
20
21
|
renderCompactBoxedToolCall,
|
|
@@ -34,6 +35,14 @@ import {
|
|
|
34
35
|
/** Right-side bottom-border hint shown when the compact preview is truncated. */
|
|
35
36
|
const WRITE_EXPAND_HINT = "Ctrl+O for more";
|
|
36
37
|
|
|
38
|
+
/** Partial-pass result: the compact call keeps its `◌ Running` card. */
|
|
39
|
+
const EMPTY_WRITE_RESULT: Component = Object.freeze({
|
|
40
|
+
invalidate() {},
|
|
41
|
+
render() {
|
|
42
|
+
return [];
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
37
46
|
type NumberedLine = { number: string; content: string };
|
|
38
47
|
|
|
39
48
|
/**
|
|
@@ -54,7 +63,7 @@ function numberedPreviewLines(content: string): NumberedLine[] {
|
|
|
54
63
|
|
|
55
64
|
/** One boxed preview row: dim gutter + toolOutput content. */
|
|
56
65
|
function formatNumberedLine(theme: BoxTheme, line: NumberedLine): string {
|
|
57
|
-
return `${
|
|
66
|
+
return `${dimLine(`${line.number} `)}${theme.fg("toolOutput", line.content)}`;
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
/** Compact write box: path header, numbered content preview, metrics footer. */
|
|
@@ -66,6 +75,7 @@ function renderWritePreviewBox(
|
|
|
66
75
|
state?: Record<string, unknown>;
|
|
67
76
|
isError: boolean;
|
|
68
77
|
isPending: boolean;
|
|
78
|
+
running?: boolean;
|
|
69
79
|
expanded: boolean;
|
|
70
80
|
},
|
|
71
81
|
): Component {
|
|
@@ -78,6 +88,7 @@ function renderWritePreviewBox(
|
|
|
78
88
|
...(options.state ? { state: options.state } : {}),
|
|
79
89
|
isError: options.isError,
|
|
80
90
|
isPending: options.isPending,
|
|
91
|
+
running: Boolean(options.running),
|
|
81
92
|
bodyLines: () => {
|
|
82
93
|
if (preview.length === 0) return [];
|
|
83
94
|
const shown = preview.slice(0, budget).map((line) => formatNumberedLine(theme, line));
|
|
@@ -107,10 +118,11 @@ export const writeTool: BoxedToolDefinition = {
|
|
|
107
118
|
state: context.state,
|
|
108
119
|
isError: Boolean(context.isError),
|
|
109
120
|
isPending: Boolean(context.isPartial),
|
|
121
|
+
running: Boolean(context.executionStarted),
|
|
110
122
|
expanded: Boolean(context.expanded),
|
|
111
123
|
});
|
|
112
124
|
},
|
|
113
|
-
result(result,
|
|
125
|
+
result(result, options, theme, context) {
|
|
114
126
|
clearFooterState(context);
|
|
115
127
|
const output = getTextOutput(result);
|
|
116
128
|
const detail = displayPath(String(context?.args?.path ?? context?.args?.file_path ?? ""), context);
|
|
@@ -124,6 +136,11 @@ export const writeTool: BoxedToolDefinition = {
|
|
|
124
136
|
});
|
|
125
137
|
}
|
|
126
138
|
|
|
139
|
+
// While the result is still streaming, don't stamp a metrics footer into
|
|
140
|
+
// the shared state: the compact call keeps its `◌ Running` card and only
|
|
141
|
+
// closes with `elapsed · words` once the tool settles.
|
|
142
|
+
if (options.isPartial) return EMPTY_WRITE_RESULT;
|
|
143
|
+
|
|
127
144
|
// Success (compact and expanded): the preview box closes with the metrics
|
|
128
145
|
// footer stored into the shared renderer state; the result adds nothing.
|
|
129
146
|
return compactFooterWithState(theme, result, context);
|
|
@@ -125,6 +125,21 @@ export function createCompatibilityCoordinator(dispose = disposePiCompatibilityP
|
|
|
125
125
|
config,
|
|
126
126
|
});
|
|
127
127
|
const messagesEnabled = (assistantEnabled || specialBlocksEnabled) && config.messages.enabled;
|
|
128
|
+
// The hidden-thinking collapse is an assistant-message surface patch: it needs
|
|
129
|
+
// the assistant flag and `messages.hideThinkingLabel`, independent of the
|
|
130
|
+
// assistant prefix feature.
|
|
131
|
+
const thinkingCollapseEnabled = Boolean(
|
|
132
|
+
authorization.assistant &&
|
|
133
|
+
config.messages.enabled &&
|
|
134
|
+
config.messages.hideThinkingLabel &&
|
|
135
|
+
isTierCAuthorized({
|
|
136
|
+
certifiedHost,
|
|
137
|
+
coreFlag: authorization.core,
|
|
138
|
+
surfaceFlag: true,
|
|
139
|
+
surface: "messages",
|
|
140
|
+
config,
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
128
143
|
const toolsEnabled =
|
|
129
144
|
authorization.tools &&
|
|
130
145
|
isTierCAuthorized({ certifiedHost, coreFlag: authorization.core, surfaceFlag: true, surface: "tools", config });
|
|
@@ -137,6 +152,7 @@ export function createCompatibilityCoordinator(dispose = disposePiCompatibilityP
|
|
|
137
152
|
...config.messages,
|
|
138
153
|
enabled: messagesEnabled,
|
|
139
154
|
assistantPrefix: assistantEnabled,
|
|
155
|
+
hideThinkingLabel: thinkingCollapseEnabled,
|
|
140
156
|
specialBlocks: messagesEnabled && config.messages.specialBlocks && specialBlocksEnabled,
|
|
141
157
|
},
|
|
142
158
|
tools: {
|
|
@@ -147,6 +163,7 @@ export function createCompatibilityCoordinator(dispose = disposePiCompatibilityP
|
|
|
147
163
|
messageSnapshot: {
|
|
148
164
|
assistantPrefix: authorization.ascii ? "[assistant] " : "│ ",
|
|
149
165
|
assistantEnabled,
|
|
166
|
+
collapseHiddenThinking: thinkingCollapseEnabled,
|
|
150
167
|
},
|
|
151
168
|
toolSnapshot: {
|
|
152
169
|
callMarker: authorization.ascii ? "[tool] " : "[tool] ",
|
|
@@ -3,14 +3,20 @@ import { dirname, join } from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import {
|
|
5
5
|
AssistantMessageComponent,
|
|
6
|
+
BashExecutionComponent,
|
|
6
7
|
BranchSummaryMessageComponent,
|
|
7
8
|
CompactionSummaryMessageComponent,
|
|
8
9
|
CustomMessageComponent,
|
|
9
10
|
SkillInvocationMessageComponent,
|
|
10
11
|
ToolExecutionComponent,
|
|
11
12
|
} from "@earendil-works/pi-coding-agent";
|
|
12
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
decorateMessageRender,
|
|
15
|
+
decorateMessageUpdate,
|
|
16
|
+
type MessageDecorationSnapshot,
|
|
17
|
+
} from "../features/messages/index.js";
|
|
13
18
|
import { renderSpecialMessageBlock, type SpecialBlockSubtype } from "../features/messages/special-blocks.js";
|
|
19
|
+
import { renderBashExecutionBox } from "../features/tools/bash-execution.js";
|
|
14
20
|
import { createToolDecorationOwner } from "../features/tools/index.js";
|
|
15
21
|
import {
|
|
16
22
|
type CompatibilityRecord,
|
|
@@ -31,12 +37,14 @@ const reportStates = new WeakMap<
|
|
|
31
37
|
// fail closed on every unrecorded Pi build and never use module-load capture as trust.
|
|
32
38
|
export const TRUSTED_NATIVE_FINGERPRINTS: Readonly<Record<string, string>> = Object.freeze({
|
|
33
39
|
"native-assistant-message:render": "2a39243f",
|
|
40
|
+
"native-assistant-message:updateContent": "4a2f15ff",
|
|
34
41
|
"native-compaction-message:updateDisplay": "f8c44e78",
|
|
35
42
|
"native-branch-message:updateDisplay": "415d57b7",
|
|
36
43
|
"native-skill-message:updateDisplay": "48099ea6",
|
|
37
44
|
"native-custom-message:rebuild": "76ae2e3a",
|
|
38
45
|
"tool-call-renderer:getCallRenderer": "951ea0e0",
|
|
39
46
|
"tool-result-renderer:getResultRenderer": "8a25cd71",
|
|
47
|
+
"native-bash-execution:render": "a5b5abca",
|
|
40
48
|
});
|
|
41
49
|
|
|
42
50
|
export const CERTIFICATION_TABLE = Object.freeze({
|
|
@@ -80,6 +88,19 @@ export const CERTIFICATION_TABLE = Object.freeze({
|
|
|
80
88
|
adapterId: "tool-renderer-component-v1",
|
|
81
89
|
status: "certified" as const,
|
|
82
90
|
}),
|
|
91
|
+
"native-assistant-message:updateContent": Object.freeze({
|
|
92
|
+
feature: "messages",
|
|
93
|
+
subtype: "native-assistant-message",
|
|
94
|
+
target: AssistantMessageComponent.prototype,
|
|
95
|
+
method: "updateContent",
|
|
96
|
+
writable: true,
|
|
97
|
+
configurable: true,
|
|
98
|
+
name: "updateContent",
|
|
99
|
+
arity: 1,
|
|
100
|
+
fingerprint: TRUSTED_NATIVE_FINGERPRINTS["native-assistant-message:updateContent"],
|
|
101
|
+
adapterId: "message-thinking-collapse-v1",
|
|
102
|
+
status: "certified" as const,
|
|
103
|
+
}),
|
|
83
104
|
"native-compaction-message:updateDisplay": Object.freeze({
|
|
84
105
|
feature: "messages",
|
|
85
106
|
subtype: "native-compaction-message",
|
|
@@ -120,6 +141,23 @@ export const CERTIFICATION_TABLE = Object.freeze({
|
|
|
120
141
|
adapterId: "message-block-boxed-v1",
|
|
121
142
|
status: "certified" as const,
|
|
122
143
|
}),
|
|
144
|
+
"native-bash-execution:render": Object.freeze({
|
|
145
|
+
feature: "tools",
|
|
146
|
+
subtype: "native-bash-execution",
|
|
147
|
+
target: BashExecutionComponent.prototype,
|
|
148
|
+
method: "render",
|
|
149
|
+
writable: true,
|
|
150
|
+
configurable: true,
|
|
151
|
+
// The additive render patch is certified by the class constructor identity
|
|
152
|
+
// (name/arity/source fingerprint): the class defines no own `render`, so
|
|
153
|
+
// the installed own method is the only one and the inherited Container
|
|
154
|
+
// render is the native fallback.
|
|
155
|
+
name: "BashExecutionComponent",
|
|
156
|
+
arity: 2,
|
|
157
|
+
fingerprint: TRUSTED_NATIVE_FINGERPRINTS["native-bash-execution:render"],
|
|
158
|
+
adapterId: "bash-execution-box-v1",
|
|
159
|
+
status: "certified" as const,
|
|
160
|
+
}),
|
|
123
161
|
}),
|
|
124
162
|
});
|
|
125
163
|
|
|
@@ -187,6 +225,12 @@ export interface TargetSpec {
|
|
|
187
225
|
adapterId: string | undefined;
|
|
188
226
|
status: "certified" | "native-fallback";
|
|
189
227
|
fallbackReason?: string;
|
|
228
|
+
/** "add-method" installs a new own method; the class constructor fingerprint certifies the target. */
|
|
229
|
+
kind?: "method" | "add-method";
|
|
230
|
+
/** Function name to verify (defaults to `method`; additive patches verify the class constructor name). */
|
|
231
|
+
identityName?: string;
|
|
232
|
+
/** Expected arity (defaults to the standard per-method rule; additive patches verify the constructor arity). */
|
|
233
|
+
arity?: number;
|
|
190
234
|
}
|
|
191
235
|
|
|
192
236
|
export function fingerprint(value: unknown): string | undefined {
|
|
@@ -201,15 +245,33 @@ export function fingerprint(value: unknown): string | undefined {
|
|
|
201
245
|
|
|
202
246
|
function trustedNativeIdentity(spec: TargetSpec, piVersion: string | undefined): unknown {
|
|
203
247
|
if (piVersion !== TRUSTED_PI_VERSION) return undefined;
|
|
248
|
+
if (spec.kind === "add-method") {
|
|
249
|
+
// Additive install: the prototype must not already own the method (it is
|
|
250
|
+
// inherited), the class constructor identity must match the recorded build,
|
|
251
|
+
// and the inherited method becomes the native fallback for the delegate.
|
|
252
|
+
if (Object.getOwnPropertyDescriptor(spec.target, spec.method)) return undefined;
|
|
253
|
+
const ctor = Object.getOwnPropertyDescriptor(spec.target, "constructor")?.value;
|
|
254
|
+
const key = `${spec.subtype}:${spec.method}`;
|
|
255
|
+
if (
|
|
256
|
+
typeof ctor !== "function" ||
|
|
257
|
+
ctor.name !== (spec.identityName ?? spec.method) ||
|
|
258
|
+
ctor.length !== (spec.arity ?? 0) ||
|
|
259
|
+
fingerprint(ctor) !== TRUSTED_NATIVE_FINGERPRINTS[key]
|
|
260
|
+
)
|
|
261
|
+
return undefined;
|
|
262
|
+
const inherited = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(spec.target), spec.method)?.value;
|
|
263
|
+
return typeof inherited === "function" ? inherited : undefined;
|
|
264
|
+
}
|
|
204
265
|
const descriptor = Object.getOwnPropertyDescriptor(spec.target, spec.method);
|
|
205
266
|
const value = descriptor?.value;
|
|
206
267
|
const key = `${spec.subtype}:${spec.method}`;
|
|
268
|
+
const expectedArity = spec.arity ?? (spec.method === "render" || spec.method === "updateContent" ? 1 : 0);
|
|
207
269
|
if (
|
|
208
270
|
descriptor?.writable !== true ||
|
|
209
271
|
descriptor.configurable !== true ||
|
|
210
272
|
typeof value !== "function" ||
|
|
211
|
-
value.name !== spec.method ||
|
|
212
|
-
value.length !==
|
|
273
|
+
value.name !== (spec.identityName ?? spec.method) ||
|
|
274
|
+
value.length !== expectedArity ||
|
|
213
275
|
fingerprint(value) !== TRUSTED_NATIVE_FINGERPRINTS[key]
|
|
214
276
|
)
|
|
215
277
|
return undefined;
|
|
@@ -225,6 +287,14 @@ export const targetSpecs: readonly TargetSpec[] = [
|
|
|
225
287
|
adapterId: "message-prefix-osc133-v1",
|
|
226
288
|
status: "certified",
|
|
227
289
|
},
|
|
290
|
+
{
|
|
291
|
+
feature: "messages",
|
|
292
|
+
subtype: "native-assistant-message",
|
|
293
|
+
target: AssistantMessageComponent.prototype,
|
|
294
|
+
method: "updateContent",
|
|
295
|
+
adapterId: "message-thinking-collapse-v1",
|
|
296
|
+
status: "certified",
|
|
297
|
+
},
|
|
228
298
|
{
|
|
229
299
|
feature: "messages",
|
|
230
300
|
subtype: "native-compaction-message",
|
|
@@ -273,6 +343,17 @@ export const targetSpecs: readonly TargetSpec[] = [
|
|
|
273
343
|
adapterId: "tool-renderer-component-v1",
|
|
274
344
|
status: "certified",
|
|
275
345
|
},
|
|
346
|
+
{
|
|
347
|
+
feature: "tools",
|
|
348
|
+
subtype: "native-bash-execution",
|
|
349
|
+
target: BashExecutionComponent.prototype,
|
|
350
|
+
method: "render",
|
|
351
|
+
kind: "add-method",
|
|
352
|
+
identityName: "BashExecutionComponent",
|
|
353
|
+
arity: 2,
|
|
354
|
+
adapterId: "bash-execution-box-v1",
|
|
355
|
+
status: "certified",
|
|
356
|
+
},
|
|
276
357
|
];
|
|
277
358
|
|
|
278
359
|
export interface PiVersionResolution {
|
|
@@ -373,15 +454,18 @@ function versionInRange(version: string | undefined): boolean {
|
|
|
373
454
|
return version === TRUSTED_PI_VERSION;
|
|
374
455
|
}
|
|
375
456
|
|
|
376
|
-
function shape(
|
|
377
|
-
const descriptor = Object.getOwnPropertyDescriptor(target, method);
|
|
457
|
+
function shape(spec: TargetSpec): boolean {
|
|
458
|
+
const descriptor = Object.getOwnPropertyDescriptor(spec.target, spec.method);
|
|
459
|
+
// Additive installs need an unowned slot (the method is inherited); every
|
|
460
|
+
// other patch requires the native own writable/configurable method.
|
|
461
|
+
if (spec.kind === "add-method") return descriptor === undefined;
|
|
378
462
|
return typeof descriptor?.value === "function" && descriptor.writable === true && descriptor.configurable === true;
|
|
379
463
|
}
|
|
380
464
|
|
|
381
465
|
export interface CompatibilityProbeOptions {
|
|
382
466
|
markers?: Set<string>;
|
|
383
467
|
config?: Readonly<{
|
|
384
|
-
messages: { enabled: boolean; assistantPrefix: boolean; specialBlocks: boolean };
|
|
468
|
+
messages: { enabled: boolean; assistantPrefix: boolean; specialBlocks: boolean; hideThinkingLabel: boolean };
|
|
385
469
|
tools: { enabled: boolean; style: string; maxCollapsedLines: number; maxExpandedLines: number; dimOutput: boolean };
|
|
386
470
|
preset: string;
|
|
387
471
|
}>;
|
|
@@ -418,7 +502,7 @@ function createFallbackRecord(
|
|
|
418
502
|
|
|
419
503
|
function probeDiagnostic(spec: TargetSpec, piVersion: string | undefined, identity: unknown): string {
|
|
420
504
|
if (!versionInRange(piVersion)) return "Pi version is unknown or outside the recorded 0.83.0 support build";
|
|
421
|
-
if (!shape(spec
|
|
505
|
+
if (!shape(spec)) return "target method shape is not an own writable/configurable function";
|
|
422
506
|
if (identity === undefined) return "recorded 0.83.0 native fingerprint, name, or arity did not match";
|
|
423
507
|
return "exact native identity verified; certified guarded decoration enabled";
|
|
424
508
|
}
|
|
@@ -427,7 +511,9 @@ function surfaceDisabled(spec: TargetSpec, config: CompatibilityProbeOptions["co
|
|
|
427
511
|
if (!config) return false;
|
|
428
512
|
if (spec.feature === "tools") return !config.tools.enabled;
|
|
429
513
|
if (!config.messages.enabled) return true;
|
|
430
|
-
if (spec.subtype === "native-assistant-message") return !config.messages.assistantPrefix;
|
|
514
|
+
if (spec.subtype === "native-assistant-message" && spec.method === "render") return !config.messages.assistantPrefix;
|
|
515
|
+
if (spec.subtype === "native-assistant-message" && spec.method === "updateContent")
|
|
516
|
+
return !config.messages.hideThinkingLabel;
|
|
431
517
|
if (isSpecialBlock(spec)) return !config.messages.specialBlocks;
|
|
432
518
|
return true;
|
|
433
519
|
}
|
|
@@ -456,13 +542,19 @@ function probeSpec(options: {
|
|
|
456
542
|
method: spec.method,
|
|
457
543
|
piVersion: piVersion ?? "unknown",
|
|
458
544
|
versionRange: PI_VERSION_RANGE,
|
|
459
|
-
shape: identity !== undefined && versionInRange(piVersion) && shape(spec
|
|
545
|
+
shape: identity !== undefined && versionInRange(piVersion) && shape(spec),
|
|
460
546
|
generation,
|
|
461
547
|
expectedIdentity: identity,
|
|
462
548
|
hasExpectedIdentity: true,
|
|
463
549
|
diagnostic,
|
|
550
|
+
...(spec.kind ? { kind: spec.kind } : {}),
|
|
464
551
|
delegate: (original, target, args) => {
|
|
465
552
|
markers.add(`${spec.subtype}:delegated`);
|
|
553
|
+
if (spec.subtype === "native-bash-execution")
|
|
554
|
+
return (
|
|
555
|
+
renderBashExecutionBox(target, args) ??
|
|
556
|
+
Reflect.apply(original as (...values: unknown[]) => unknown, target, args)
|
|
557
|
+
);
|
|
466
558
|
if (spec.feature === "tools")
|
|
467
559
|
return (
|
|
468
560
|
toolOwner?.decorateToolRendererSelection(
|
|
@@ -472,8 +564,10 @@ function probeSpec(options: {
|
|
|
472
564
|
args,
|
|
473
565
|
) ?? Reflect.apply(original as (...values: unknown[]) => unknown, target, args)
|
|
474
566
|
);
|
|
475
|
-
if (spec.subtype === "native-assistant-message")
|
|
567
|
+
if (spec.subtype === "native-assistant-message") {
|
|
568
|
+
if (spec.method === "updateContent") return decorateMessageUpdate(original, target, args, messageSnapshot);
|
|
476
569
|
return decorateMessageRender(original, target, args, messageSnapshot);
|
|
570
|
+
}
|
|
477
571
|
return renderSpecialMessageBlock(spec.subtype as SpecialBlockSubtype, original, target, args);
|
|
478
572
|
},
|
|
479
573
|
});
|