@quandev104/pi-style 0.1.2 → 0.1.3
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 +15 -0
- package/README.md +1 -2
- package/dist/extensions/pi-style.js +5510 -4865
- package/dist/extensions/pi-style.js.map +1 -1
- package/extension-src/pi-style/app/command-service.ts +2 -2
- package/extension-src/pi-style/app/index.ts +0 -1
- package/extension-src/pi-style/domain/config-authorization.ts +1 -2
- package/extension-src/pi-style/domain/config-normalization.ts +3 -3
- package/extension-src/pi-style/domain/config-types.ts +2 -2
- package/extension-src/pi-style/domain/theme.ts +3 -0
- package/extension-src/pi-style/features/messages/index.ts +2 -8
- package/extension-src/pi-style/features/tools/boxed/bash.ts +384 -0
- package/extension-src/pi-style/features/tools/boxed/batch.ts +459 -0
- package/extension-src/pi-style/features/tools/boxed/find.ts +48 -48
- package/extension-src/pi-style/features/tools/boxed/grep.ts +161 -89
- package/extension-src/pi-style/features/tools/boxed/index.ts +4 -0
- package/extension-src/pi-style/features/tools/boxed/ls.ts +39 -47
- package/extension-src/pi-style/features/tools/boxed/output-tree.ts +368 -0
- package/extension-src/pi-style/features/tools/boxed/read.ts +32 -189
- package/extension-src/pi-style/features/tools/boxed/session-config.ts +6 -0
- package/extension-src/pi-style/features/tools/boxed/write.ts +91 -49
- package/extension-src/pi-style/features/tools/index.ts +14 -0
- package/extension-src/pi-style/pi/compatibility-coordinator.ts +4 -26
- package/extension-src/pi-style/pi/compatibility-probe.ts +3 -33
- package/extension-src/pi-style/pi/compatibility-registry.ts +0 -1
- package/extension-src/pi-style/pi/config-session.ts +0 -2
- package/extension-src/pi-style/pi/index.ts +35 -1
- package/extension-src/pi-style/pi/session-coordinator.ts +39 -3
- package/extension-src/pi-style/shared/box.ts +41 -7
- package/extension-src/pi-style/shared/theme-extras.ts +0 -2
- package/package.json +1 -1
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
// Boxed write tool renderer
|
|
2
2
|
// (renderCall/renderResult only).
|
|
3
|
+
//
|
|
4
|
+
// The write call renders a compact preview box: the file path in the top
|
|
5
|
+
// border, the written content as numbered lines in the body (cat -n style),
|
|
6
|
+
// and the metrics footer in the bottom border. The footer lives in the shared
|
|
7
|
+
// renderer state — the result renderer stores it (elapsed + words), the call
|
|
8
|
+
// component reads it at paint time and closes the box. The preview is capped
|
|
9
|
+
// at the collapsed line budget with a `Ctrl+O for more` hint on the bottom
|
|
10
|
+
// border when truncated; expanded shows the expanded budget. Errors keep the
|
|
11
|
+
// plain open call box so the boxed error result never duplicates a box.
|
|
3
12
|
|
|
13
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
4
14
|
import { stripAnsi } from "../../../shared/ansi.js";
|
|
5
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
type BoxTheme,
|
|
17
|
+
boxedToolWidthKey,
|
|
18
|
+
getTextOutput,
|
|
19
|
+
renderBoxedToolResult,
|
|
20
|
+
renderCompactBoxedToolCall,
|
|
21
|
+
replaceTabs,
|
|
22
|
+
} from "../../../shared/box.js";
|
|
23
|
+
import { getToolsRenderConfig } from "./session-config.js";
|
|
6
24
|
import {
|
|
7
25
|
type BoxedToolDefinition,
|
|
8
26
|
clearFooterState,
|
|
@@ -13,39 +31,86 @@ import {
|
|
|
13
31
|
resultFooterLines,
|
|
14
32
|
} from "./shared.js";
|
|
15
33
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (!normalized) return undefined;
|
|
34
|
+
/** Right-side bottom-border hint shown when the compact preview is truncated. */
|
|
35
|
+
const WRITE_EXPAND_HINT = "Ctrl+O for more";
|
|
19
36
|
|
|
20
|
-
|
|
21
|
-
if (byteMatch) {
|
|
22
|
-
const bytes = Number(byteMatch[1]);
|
|
23
|
-
if (Number.isFinite(bytes)) {
|
|
24
|
-
return `↳ Wrote ${bytes} ${bytes === 1 ? "byte" : "bytes"}.`;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
37
|
+
type NumberedLine = { number: string; content: string };
|
|
27
38
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Numbered preview lines for the written content, `cat -n` style: every split
|
|
41
|
+
* line keeps its number (including a trailing empty line produced by a final
|
|
42
|
+
* newline), right-aligned to the widest line number.
|
|
43
|
+
*/
|
|
44
|
+
function numberedPreviewLines(content: string): NumberedLine[] {
|
|
45
|
+
const normalized = replaceTabs(String(content ?? "")).replace(/\r/g, "");
|
|
46
|
+
if (!normalized) return [];
|
|
47
|
+
const lines = normalized.split("\n");
|
|
48
|
+
const gutterWidth = Math.max(1, String(lines.length).length);
|
|
49
|
+
return lines.map((line, index) => ({
|
|
50
|
+
number: String(index + 1).padStart(gutterWidth),
|
|
51
|
+
content: line,
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** One boxed preview row: dim gutter + toolOutput content. */
|
|
56
|
+
function formatNumberedLine(theme: BoxTheme, line: NumberedLine): string {
|
|
57
|
+
return `${theme.fg("borderMuted", `${line.number} `)}${theme.fg("toolOutput", line.content)}`;
|
|
58
|
+
}
|
|
35
59
|
|
|
36
|
-
|
|
60
|
+
/** Compact write box: path header, numbered content preview, metrics footer. */
|
|
61
|
+
function renderWritePreviewBox(
|
|
62
|
+
theme: BoxTheme,
|
|
63
|
+
detailLine: string,
|
|
64
|
+
content: string,
|
|
65
|
+
options: {
|
|
66
|
+
state?: Record<string, unknown>;
|
|
67
|
+
isError: boolean;
|
|
68
|
+
isPending: boolean;
|
|
69
|
+
expanded: boolean;
|
|
70
|
+
},
|
|
71
|
+
): Component {
|
|
72
|
+
const preview = numberedPreviewLines(content);
|
|
73
|
+
const config = getToolsRenderConfig();
|
|
74
|
+
const budget = options.expanded ? config.maxExpandedLines : config.maxCollapsedLines;
|
|
75
|
+
const truncated = preview.length > budget;
|
|
76
|
+
|
|
77
|
+
return renderCompactBoxedToolCall(theme, "Write", detailLine, {
|
|
78
|
+
...(options.state ? { state: options.state } : {}),
|
|
79
|
+
isError: options.isError,
|
|
80
|
+
isPending: options.isPending,
|
|
81
|
+
bodyLines: () => {
|
|
82
|
+
if (preview.length === 0) return [];
|
|
83
|
+
const shown = preview.slice(0, budget).map((line) => formatNumberedLine(theme, line));
|
|
84
|
+
if (!truncated) return shown;
|
|
85
|
+
const omitted = preview.length - budget;
|
|
86
|
+
const note = options.expanded ? `… ${omitted} more lines omitted by render budget` : `… ${omitted} more lines`;
|
|
87
|
+
return [...shown, theme.fg("muted", note)];
|
|
88
|
+
},
|
|
89
|
+
...(options.expanded || options.isPending || !truncated ? {} : { bottomRightLabel: WRITE_EXPAND_HINT }),
|
|
90
|
+
});
|
|
37
91
|
}
|
|
38
92
|
|
|
39
93
|
export const writeTool: BoxedToolDefinition = {
|
|
40
94
|
call(args, theme, context) {
|
|
41
95
|
noteExecutionStart(context);
|
|
42
96
|
const detail = displayPath(String(args?.path ?? args?.file_path ?? ""), context);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
97
|
+
const detailLine = `${theme.fg("dim", "Path: ")}${detail}`;
|
|
98
|
+
// On error keep the plain open box: the result renderer continues it with
|
|
99
|
+
// the boxed error body, so call and result never duplicate a box.
|
|
100
|
+
if (context.isError) {
|
|
101
|
+
return compactCall(theme, "Write", detailLine, {
|
|
102
|
+
detailKey: detail,
|
|
103
|
+
context,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return renderWritePreviewBox(theme, detailLine, String(args?.content ?? ""), {
|
|
107
|
+
state: context.state,
|
|
108
|
+
isError: Boolean(context.isError),
|
|
109
|
+
isPending: Boolean(context.isPartial),
|
|
110
|
+
expanded: Boolean(context.expanded),
|
|
46
111
|
});
|
|
47
112
|
},
|
|
48
|
-
result(result,
|
|
113
|
+
result(result, _options, theme, context) {
|
|
49
114
|
clearFooterState(context);
|
|
50
115
|
const output = getTextOutput(result);
|
|
51
116
|
const detail = displayPath(String(context?.args?.path ?? context?.args?.file_path ?? ""), context);
|
|
@@ -59,31 +124,8 @@ export const writeTool: BoxedToolDefinition = {
|
|
|
59
124
|
});
|
|
60
125
|
}
|
|
61
126
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const lineCount = content ? content.split("\n").length : 0;
|
|
66
|
-
if (lineCount > 0) {
|
|
67
|
-
const summary = `↳ Wrote ${lineCount} ${lineCount === 1 ? "line" : "lines"}.`;
|
|
68
|
-
return renderBoxedToolResult(theme, () => [theme.fg("dim", summary)], {
|
|
69
|
-
widthKey,
|
|
70
|
-
footerLines: resultFooterLines(theme, result, context),
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const summary = parseWriteSummary(output);
|
|
75
|
-
if (summary) {
|
|
76
|
-
return renderBoxedToolResult(theme, () => [theme.fg("dim", summary)], {
|
|
77
|
-
widthKey,
|
|
78
|
-
footerLines: resultFooterLines(theme, result, context),
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const normalized = stripTrailingNotice(stripAnsi(output)).trim();
|
|
83
|
-
const fallback = normalized ? `↳ ${normalized}` : "↳ Wrote file.";
|
|
84
|
-
return renderBoxedToolResult(theme, () => [theme.fg("dim", fallback)], {
|
|
85
|
-
widthKey,
|
|
86
|
-
footerLines: resultFooterLines(theme, result, context),
|
|
87
|
-
});
|
|
127
|
+
// Success (compact and expanded): the preview box closes with the metrics
|
|
128
|
+
// footer stored into the shared renderer state; the result adds nothing.
|
|
129
|
+
return compactFooterWithState(theme, result, context);
|
|
88
130
|
},
|
|
89
131
|
};
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import { EMPTY_BATCH_COMPONENT } from "./boxed/batch.js";
|
|
2
3
|
import { renderBoxedToolCall, renderBoxedToolResult } from "./boxed/index.js";
|
|
3
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Batch members render zero lines. Pi's ToolExecutionComponent always adds a
|
|
7
|
+
* built-in Spacer child (one blank line) and only sets hideComponent when
|
|
8
|
+
* hasContent is false — but adding an empty renderer still marks hasContent
|
|
9
|
+
* true. Mark the instance hidden so members contribute zero lines (no stray
|
|
10
|
+
* blank margin after the batch panel). updateDisplay resets hideComponent on
|
|
11
|
+
* every pass; the wrapper re-applies it on each dispatch.
|
|
12
|
+
*/
|
|
13
|
+
function hideBatchMember(instance: object): void {
|
|
14
|
+
(instance as { hideComponent?: boolean }).hideComponent = true;
|
|
15
|
+
}
|
|
16
|
+
|
|
4
17
|
/**
|
|
5
18
|
* Neutralize the native ToolExecutionComponent status background for boxed
|
|
6
19
|
* rendering: Pi's updateDisplay sets contentBox/selfRenderContainer bgFn to
|
|
@@ -434,6 +447,7 @@ export function createToolDecorationOwner(snapshot: Partial<ToolDecorationSnapsh
|
|
|
434
447
|
);
|
|
435
448
|
})();
|
|
436
449
|
neutralizeToolContainerBackground(instance);
|
|
450
|
+
if (component === EMPTY_BATCH_COMPONENT) hideBatchMember(instance);
|
|
437
451
|
return component;
|
|
438
452
|
};
|
|
439
453
|
}
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
export interface CompatibilityCoordinator {
|
|
12
12
|
captureAuthorization(
|
|
13
13
|
coreFlag: boolean,
|
|
14
|
-
userFlag: boolean,
|
|
15
14
|
assistantFlag: boolean,
|
|
16
15
|
specialBlocksFlag: boolean,
|
|
17
16
|
toolsFlag: boolean,
|
|
@@ -33,7 +32,6 @@ export function createCompatibilityCoordinator(dispose = disposePiCompatibilityP
|
|
|
33
32
|
let authorization:
|
|
34
33
|
| {
|
|
35
34
|
core: boolean;
|
|
36
|
-
user: boolean;
|
|
37
35
|
assistant: boolean;
|
|
38
36
|
specialBlocks: boolean;
|
|
39
37
|
tools: boolean;
|
|
@@ -44,15 +42,13 @@ export function createCompatibilityCoordinator(dispose = disposePiCompatibilityP
|
|
|
44
42
|
get report() {
|
|
45
43
|
return report;
|
|
46
44
|
},
|
|
47
|
-
captureAuthorization(core,
|
|
48
|
-
authorization = { core,
|
|
45
|
+
captureAuthorization(core, assistant, specialBlocks, tools, ascii) {
|
|
46
|
+
authorization = { core, assistant, specialBlocks, tools, ascii };
|
|
49
47
|
},
|
|
50
48
|
state(config) {
|
|
51
49
|
const version = detectPiVersion();
|
|
52
50
|
const messagesConfigured =
|
|
53
|
-
config.enabled &&
|
|
54
|
-
config.messages.enabled &&
|
|
55
|
-
(config.messages.userPrefix || config.messages.assistantPrefix || config.messages.specialBlocks);
|
|
51
|
+
config.enabled && config.messages.enabled && (config.messages.assistantPrefix || config.messages.specialBlocks);
|
|
56
52
|
const toolsConfigured = config.enabled && config.tools.enabled;
|
|
57
53
|
const surface = (
|
|
58
54
|
feature: "messages" | "tools",
|
|
@@ -91,12 +87,6 @@ export function createCompatibilityCoordinator(dispose = disposePiCompatibilityP
|
|
|
91
87
|
nativeFallbacks: report?.unsupported.filter((item) => item.reason.includes("fallback")).length ?? 0,
|
|
92
88
|
piVersion: version.version ?? report?.piVersion ?? "unknown",
|
|
93
89
|
versionRange: report?.versionRange ?? ">=0.83.0 <0.84.0",
|
|
94
|
-
userMessage: surface(
|
|
95
|
-
"messages",
|
|
96
|
-
config.enabled && config.messages.enabled && config.messages.userPrefix,
|
|
97
|
-
Boolean(authorization?.user),
|
|
98
|
-
"native-user-message",
|
|
99
|
-
),
|
|
100
90
|
assistantMessage: surface(
|
|
101
91
|
"messages",
|
|
102
92
|
config.enabled && config.messages.enabled && config.messages.assistantPrefix,
|
|
@@ -116,15 +106,6 @@ export function createCompatibilityCoordinator(dispose = disposePiCompatibilityP
|
|
|
116
106
|
if (cleanupPending && !report) cleanupPending = false;
|
|
117
107
|
if (cleanupPending || !tui || !config.enabled || !authorization?.core || productDenied) return undefined;
|
|
118
108
|
const certifiedHost = detectPiVersion().version === "0.83.0";
|
|
119
|
-
const userEnabled =
|
|
120
|
-
authorization.user &&
|
|
121
|
-
isTierCAuthorized({
|
|
122
|
-
certifiedHost,
|
|
123
|
-
coreFlag: authorization.core,
|
|
124
|
-
surfaceFlag: true,
|
|
125
|
-
surface: "userMessage",
|
|
126
|
-
config,
|
|
127
|
-
});
|
|
128
109
|
const assistantEnabled =
|
|
129
110
|
authorization.assistant &&
|
|
130
111
|
isTierCAuthorized({
|
|
@@ -143,7 +124,7 @@ export function createCompatibilityCoordinator(dispose = disposePiCompatibilityP
|
|
|
143
124
|
surface: "specialBlocks",
|
|
144
125
|
config,
|
|
145
126
|
});
|
|
146
|
-
const messagesEnabled = (
|
|
127
|
+
const messagesEnabled = (assistantEnabled || specialBlocksEnabled) && config.messages.enabled;
|
|
147
128
|
const toolsEnabled =
|
|
148
129
|
authorization.tools &&
|
|
149
130
|
isTierCAuthorized({ certifiedHost, coreFlag: authorization.core, surfaceFlag: true, surface: "tools", config });
|
|
@@ -155,7 +136,6 @@ export function createCompatibilityCoordinator(dispose = disposePiCompatibilityP
|
|
|
155
136
|
messages: {
|
|
156
137
|
...config.messages,
|
|
157
138
|
enabled: messagesEnabled,
|
|
158
|
-
userPrefix: userEnabled,
|
|
159
139
|
assistantPrefix: assistantEnabled,
|
|
160
140
|
specialBlocks: messagesEnabled && config.messages.specialBlocks && specialBlocksEnabled,
|
|
161
141
|
},
|
|
@@ -165,9 +145,7 @@ export function createCompatibilityCoordinator(dispose = disposePiCompatibilityP
|
|
|
165
145
|
},
|
|
166
146
|
},
|
|
167
147
|
messageSnapshot: {
|
|
168
|
-
userPrefix: authorization.ascii ? "[user] " : "❯ ",
|
|
169
148
|
assistantPrefix: authorization.ascii ? "[assistant] " : "│ ",
|
|
170
|
-
userEnabled,
|
|
171
149
|
assistantEnabled,
|
|
172
150
|
},
|
|
173
151
|
toolSnapshot: {
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
CustomMessageComponent,
|
|
9
9
|
SkillInvocationMessageComponent,
|
|
10
10
|
ToolExecutionComponent,
|
|
11
|
-
UserMessageComponent,
|
|
12
11
|
} from "@earendil-works/pi-coding-agent";
|
|
13
12
|
import { decorateMessageRender, type MessageDecorationSnapshot } from "../features/messages/index.js";
|
|
14
13
|
import { renderSpecialMessageBlock, type SpecialBlockSubtype } from "../features/messages/special-blocks.js";
|
|
@@ -31,7 +30,6 @@ const reportStates = new WeakMap<
|
|
|
31
30
|
// code loaded before this module could spoof the same function source. We therefore
|
|
32
31
|
// fail closed on every unrecorded Pi build and never use module-load capture as trust.
|
|
33
32
|
export const TRUSTED_NATIVE_FINGERPRINTS: Readonly<Record<string, string>> = Object.freeze({
|
|
34
|
-
"native-user-message:render": "b442a17c",
|
|
35
33
|
"native-assistant-message:render": "2a39243f",
|
|
36
34
|
"native-compaction-message:updateDisplay": "f8c44e78",
|
|
37
35
|
"native-branch-message:updateDisplay": "415d57b7",
|
|
@@ -43,19 +41,6 @@ export const TRUSTED_NATIVE_FINGERPRINTS: Readonly<Record<string, string>> = Obj
|
|
|
43
41
|
|
|
44
42
|
export const CERTIFICATION_TABLE = Object.freeze({
|
|
45
43
|
"0.83.0": Object.freeze({
|
|
46
|
-
"native-user-message:render": Object.freeze({
|
|
47
|
-
feature: "messages",
|
|
48
|
-
subtype: "native-user-message",
|
|
49
|
-
target: UserMessageComponent.prototype,
|
|
50
|
-
method: "render",
|
|
51
|
-
writable: true,
|
|
52
|
-
configurable: true,
|
|
53
|
-
name: "render",
|
|
54
|
-
arity: 1,
|
|
55
|
-
fingerprint: TRUSTED_NATIVE_FINGERPRINTS["native-user-message:render"],
|
|
56
|
-
adapterId: "message-prefix-osc133-v1",
|
|
57
|
-
status: "certified" as const,
|
|
58
|
-
}),
|
|
59
44
|
"native-assistant-message:render": Object.freeze({
|
|
60
45
|
feature: "messages",
|
|
61
46
|
subtype: "native-assistant-message",
|
|
@@ -232,14 +217,6 @@ function trustedNativeIdentity(spec: TargetSpec, piVersion: string | undefined):
|
|
|
232
217
|
}
|
|
233
218
|
|
|
234
219
|
export const targetSpecs: readonly TargetSpec[] = [
|
|
235
|
-
{
|
|
236
|
-
feature: "messages",
|
|
237
|
-
subtype: "native-user-message",
|
|
238
|
-
target: UserMessageComponent.prototype,
|
|
239
|
-
method: "render",
|
|
240
|
-
adapterId: "message-prefix-osc133-v1",
|
|
241
|
-
status: "certified",
|
|
242
|
-
},
|
|
243
220
|
{
|
|
244
221
|
feature: "messages",
|
|
245
222
|
subtype: "native-assistant-message",
|
|
@@ -404,7 +381,7 @@ function shape(target: object, method: string): boolean {
|
|
|
404
381
|
export interface CompatibilityProbeOptions {
|
|
405
382
|
markers?: Set<string>;
|
|
406
383
|
config?: Readonly<{
|
|
407
|
-
messages: { enabled: boolean;
|
|
384
|
+
messages: { enabled: boolean; assistantPrefix: boolean; specialBlocks: boolean };
|
|
408
385
|
tools: { enabled: boolean; style: string; maxCollapsedLines: number; maxExpandedLines: number; dimOutput: boolean };
|
|
409
386
|
preset: string;
|
|
410
387
|
}>;
|
|
@@ -450,7 +427,6 @@ function surfaceDisabled(spec: TargetSpec, config: CompatibilityProbeOptions["co
|
|
|
450
427
|
if (!config) return false;
|
|
451
428
|
if (spec.feature === "tools") return !config.tools.enabled;
|
|
452
429
|
if (!config.messages.enabled) return true;
|
|
453
|
-
if (spec.subtype === "native-user-message") return !config.messages.userPrefix;
|
|
454
430
|
if (spec.subtype === "native-assistant-message") return !config.messages.assistantPrefix;
|
|
455
431
|
if (isSpecialBlock(spec)) return !config.messages.specialBlocks;
|
|
456
432
|
return true;
|
|
@@ -496,14 +472,8 @@ function probeSpec(options: {
|
|
|
496
472
|
args,
|
|
497
473
|
) ?? Reflect.apply(original as (...values: unknown[]) => unknown, target, args)
|
|
498
474
|
);
|
|
499
|
-
if (spec.subtype === "native-
|
|
500
|
-
return decorateMessageRender(
|
|
501
|
-
spec.subtype as "native-user-message" | "native-assistant-message",
|
|
502
|
-
original,
|
|
503
|
-
target,
|
|
504
|
-
args,
|
|
505
|
-
messageSnapshot,
|
|
506
|
-
);
|
|
475
|
+
if (spec.subtype === "native-assistant-message")
|
|
476
|
+
return decorateMessageRender(original, target, args, messageSnapshot);
|
|
507
477
|
return renderSpecialMessageBlock(spec.subtype as SpecialBlockSubtype, original, target, args);
|
|
508
478
|
},
|
|
509
479
|
});
|
|
@@ -8,7 +8,6 @@ export interface SessionFlagReader {
|
|
|
8
8
|
}
|
|
9
9
|
export interface SessionAuthorization {
|
|
10
10
|
core: boolean;
|
|
11
|
-
user: boolean;
|
|
12
11
|
assistant: boolean;
|
|
13
12
|
specialBlocks: boolean;
|
|
14
13
|
tools: boolean;
|
|
@@ -59,7 +58,6 @@ export function resolveProductGate(
|
|
|
59
58
|
export function readSessionAuthorization(pi: SessionFlagReader): SessionAuthorization {
|
|
60
59
|
return {
|
|
61
60
|
core: pi.getFlag("pi-style-core-patches") === true,
|
|
62
|
-
user: pi.getFlag("pi-style-message-user") === true,
|
|
63
61
|
assistant: pi.getFlag("pi-style-message-assistant") === true,
|
|
64
62
|
specialBlocks: pi.getFlag("pi-style-message-special-blocks") === true,
|
|
65
63
|
tools: pi.getFlag("pi-style-tools") === true,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { StatusSnapshot } from "../domain/status.js";
|
|
3
|
+
import { closeActiveBatch } from "../features/tools/boxed/batch.js";
|
|
3
4
|
import { registerPiStyleCommand } from "./commands.js";
|
|
4
5
|
import { type CompatibilityTestHooks, createPiStyleSessionCoordinator } from "./session-coordinator.js";
|
|
5
6
|
import { usageFromSession } from "./session-usage.js";
|
|
@@ -10,6 +11,24 @@ function usagePatch(ctx: ExtensionContext): StatusSnapshot {
|
|
|
10
11
|
return usage ? { usage } : {};
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Add Pi's read-only tools (grep/find/ls) to the active tool set if they are
|
|
16
|
+
* registered. Preserves any other active tools (e.g. extension tools).
|
|
17
|
+
* Only calls setActiveTools when something actually changed.
|
|
18
|
+
*/
|
|
19
|
+
function activateReadOnlyTools(pi: ExtensionAPI): void {
|
|
20
|
+
const available = new Set(pi.getAllTools().map((tool) => tool.name));
|
|
21
|
+
const active = new Set(pi.getActiveTools());
|
|
22
|
+
let changed = false;
|
|
23
|
+
for (const name of ["grep", "find", "ls"] as const) {
|
|
24
|
+
if (available.has(name) && !active.has(name)) {
|
|
25
|
+
active.add(name);
|
|
26
|
+
changed = true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (changed) pi.setActiveTools([...active]);
|
|
30
|
+
}
|
|
31
|
+
|
|
13
32
|
let compatibilityTestHooks: CompatibilityTestHooks = {};
|
|
14
33
|
export function __setCompatibilityTestHooks(hooks: CompatibilityTestHooks): () => void {
|
|
15
34
|
const previous = compatibilityTestHooks;
|
|
@@ -26,10 +45,10 @@ export default function piStyleExtension(pi: ExtensionAPI): void {
|
|
|
26
45
|
// product gate `compatibility.allowCorePatches: false` (or `enabled: false`) in config.
|
|
27
46
|
for (const [name, description] of [
|
|
28
47
|
["pi-style-core-patches", "Enable pi-style message/tool core patches"],
|
|
29
|
-
["pi-style-message-user", "Enable pi-style user message prefix"],
|
|
30
48
|
["pi-style-message-assistant", "Enable pi-style assistant message prefix"],
|
|
31
49
|
["pi-style-message-special-blocks", "Enable pi-style boxed compaction/skill/branch/custom message blocks"],
|
|
32
50
|
["pi-style-tools", "Enable pi-style tool renderer decoration"],
|
|
51
|
+
["pi-style-readonly-tools", "Enable grep/find/ls read-only tools in the active tool set"],
|
|
33
52
|
] as const)
|
|
34
53
|
pi.registerFlag(name, { type: "boolean", description, default: true });
|
|
35
54
|
// ASCII markers stay opt-in; unicode markers are the default.
|
|
@@ -37,6 +56,13 @@ export default function piStyleExtension(pi: ExtensionAPI): void {
|
|
|
37
56
|
const coordinator = createPiStyleSessionCoordinator(pi, compatibilityTestHooks);
|
|
38
57
|
registerPiStyleCommand(pi, coordinator.app);
|
|
39
58
|
pi.on("session_start", async (event, ctx) => {
|
|
59
|
+
// Pi only activates read/bash/edit/write by default; grep/find/ls are
|
|
60
|
+
// registered but inactive (kept out of the model's tool list to keep the
|
|
61
|
+
// core small). Activate them so the TUI shows them and the model can call
|
|
62
|
+
// them directly, mirroring Claude Code's glob/grep/read tool set.
|
|
63
|
+
if (pi.getFlag("pi-style-readonly-tools") === true) {
|
|
64
|
+
activateReadOnlyTools(pi);
|
|
65
|
+
}
|
|
40
66
|
await coordinator.start(event, ctx);
|
|
41
67
|
});
|
|
42
68
|
pi.on("agent_start", () => coordinator.app.runtime.current?.dismissStartup());
|
|
@@ -54,6 +80,14 @@ export default function piStyleExtension(pi: ExtensionAPI): void {
|
|
|
54
80
|
);
|
|
55
81
|
pi.on("thinking_level_select", (event) => coordinator.app.update({ thinkingLevel: event.level }, "immediate"));
|
|
56
82
|
pi.on("session_info_changed", (event) => coordinator.app.update({ sessionName: event.name }, "coalesced"));
|
|
83
|
+
pi.on("message_start", () => {
|
|
84
|
+
// A new message is a batch boundary: quiet-tool (read/ls/find) calls of the
|
|
85
|
+
// new message start a fresh batch instead of joining the previous one.
|
|
86
|
+
closeActiveBatch();
|
|
87
|
+
// grep/bash tree panels are NOT cleared here: historical panels must keep
|
|
88
|
+
// their state so Pi re-renders of previous messages (scroll/resume) stay
|
|
89
|
+
// intact. Only session boundaries reset them (session-coordinator).
|
|
90
|
+
});
|
|
57
91
|
pi.on("message_update", () => coordinator.app.update({}, "coalesced"));
|
|
58
92
|
// Usage (tokens + cost) is aggregated from finalized session entries at
|
|
59
93
|
// message/turn boundaries, mirroring Pi's native footer; per-chunk updates
|
|
@@ -2,8 +2,12 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
2
2
|
import { type KeyId, matchesKey } from "@earendil-works/pi-tui";
|
|
3
3
|
import type { ConfigFilePort } from "../app/config-storage.js";
|
|
4
4
|
import { createPiStyleApp, type PiStyleApp } from "../app/index.js";
|
|
5
|
+
import { resolveTheme } from "../domain/theme.js";
|
|
5
6
|
import { setSpecialBlockTheme } from "../features/messages/special-blocks.js";
|
|
6
|
-
import {
|
|
7
|
+
import { resetBashTreeRegistry } from "../features/tools/boxed/bash.js";
|
|
8
|
+
import { resetBatchRegistry } from "../features/tools/boxed/batch.js";
|
|
9
|
+
import { resetGrepRegistry } from "../features/tools/boxed/grep.js";
|
|
10
|
+
import { setToolsRenderConfig, type ToolsRenderConfig } from "../features/tools/boxed/session-config.js";
|
|
7
11
|
import { createCompatibilityCoordinator } from "./compatibility-coordinator.js";
|
|
8
12
|
import {
|
|
9
13
|
type CompatibilityCleanupResult,
|
|
@@ -45,6 +49,8 @@ export function createPiStyleSessionCoordinator(pi: ExtensionAPI, hooks: Compati
|
|
|
45
49
|
let active = false;
|
|
46
50
|
let tuiSession = false;
|
|
47
51
|
let terminalInputUnsubscribe: (() => void) | undefined;
|
|
52
|
+
let sessionTheme: unknown;
|
|
53
|
+
let sessionUi: import("@earendil-works/pi-coding-agent").ExtensionUIContext | undefined;
|
|
48
54
|
const source = createConfigSourceAdapter(
|
|
49
55
|
pi,
|
|
50
56
|
filePort,
|
|
@@ -68,6 +74,22 @@ export function createPiStyleSessionCoordinator(pi: ExtensionAPI, hooks: Compati
|
|
|
68
74
|
),
|
|
69
75
|
);
|
|
70
76
|
};
|
|
77
|
+
/** Render-scoped tool config: line budgets + the resolved open-tree glyph. */
|
|
78
|
+
const applyToolsRenderConfig = (config: import("../domain/config-types.js").NormalizedPiStyleConfig) => {
|
|
79
|
+
setToolsRenderConfig({
|
|
80
|
+
...config.tools,
|
|
81
|
+
batchOpenGlyph: resolveTheme(sessionTheme as never, config, process.env).glyph("batchOpen"),
|
|
82
|
+
nerdFonts: resolveTheme(sessionTheme as never, config, process.env).mode === "nerd",
|
|
83
|
+
} satisfies ToolsRenderConfig);
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Hide Pi's "Thinking..." placeholder label: an empty label renders zero
|
|
87
|
+
* lines, so the thinking block leaves no trace while content stays hidden.
|
|
88
|
+
* Passing undefined restores the default label.
|
|
89
|
+
*/
|
|
90
|
+
const applyMessagesConfig = (config: import("../domain/config-types.js").NormalizedPiStyleConfig) => {
|
|
91
|
+
sessionUi?.setHiddenThinkingLabel?.(config.messages.hideThinkingLabel ? "" : undefined);
|
|
92
|
+
};
|
|
71
93
|
const app: PiStyleApp = createPiStyleApp(
|
|
72
94
|
undefined,
|
|
73
95
|
{
|
|
@@ -79,6 +101,10 @@ export function createPiStyleSessionCoordinator(pi: ExtensionAPI, hooks: Compati
|
|
|
79
101
|
(config) => {
|
|
80
102
|
productGate = app.productPolicy.corePatchGate;
|
|
81
103
|
if (!active) return;
|
|
104
|
+
// Apply render-scoped tool config live so `/pi-style set tools.*` takes
|
|
105
|
+
// effect immediately (line budgets, dimOutput, open-tree glyph, …).
|
|
106
|
+
applyToolsRenderConfig(config);
|
|
107
|
+
applyMessagesConfig(config);
|
|
82
108
|
if (compatibility.report) {
|
|
83
109
|
const cleanup = compatibility.dispose();
|
|
84
110
|
if (!cleanup.complete) {
|
|
@@ -100,7 +126,6 @@ export function createPiStyleSessionCoordinator(pi: ExtensionAPI, hooks: Compati
|
|
|
100
126
|
authorization = readSessionAuthorization(pi);
|
|
101
127
|
compatibility.captureAuthorization(
|
|
102
128
|
authorization.core,
|
|
103
|
-
authorization.user,
|
|
104
129
|
authorization.assistant,
|
|
105
130
|
authorization.specialBlocks,
|
|
106
131
|
authorization.tools,
|
|
@@ -120,6 +145,11 @@ export function createPiStyleSessionCoordinator(pi: ExtensionAPI, hooks: Compati
|
|
|
120
145
|
tuiSession = ctx.mode === "tui";
|
|
121
146
|
app.setProjectTrusted(ctx.isProjectTrusted());
|
|
122
147
|
source.setSession(cwd, ctx.isProjectTrusted());
|
|
148
|
+
// Drop any batch state carried over from the previous session (Pi renders
|
|
149
|
+
// the restored chat between session_shutdown and the next session_start).
|
|
150
|
+
resetBatchRegistry();
|
|
151
|
+
resetGrepRegistry();
|
|
152
|
+
resetBashTreeRegistry();
|
|
123
153
|
active = false;
|
|
124
154
|
await app.reload();
|
|
125
155
|
productGate = app.productPolicy.corePatchGate;
|
|
@@ -127,7 +157,10 @@ export function createPiStyleSessionCoordinator(pi: ExtensionAPI, hooks: Compati
|
|
|
127
157
|
compatibility.install(app.config, ctx.mode === "tui", productGate);
|
|
128
158
|
// Session-scoped render configuration for the boxed tool/message surfaces.
|
|
129
159
|
// Populated once per session (never inside render).
|
|
130
|
-
|
|
160
|
+
sessionTheme = ctx.ui?.theme as never;
|
|
161
|
+
sessionUi = ctx.ui as import("@earendil-works/pi-coding-agent").ExtensionUIContext | undefined;
|
|
162
|
+
applyToolsRenderConfig(app.config);
|
|
163
|
+
applyMessagesConfig(app.config);
|
|
131
164
|
if (ctx.ui?.theme) setSpecialBlockTheme(ctx.ui.theme as never);
|
|
132
165
|
const toolDetails = collectToolDetails(pi.getActiveTools?.(), pi.getAllTools?.());
|
|
133
166
|
app.sessionStart(
|
|
@@ -180,6 +213,9 @@ export function createPiStyleSessionCoordinator(pi: ExtensionAPI, hooks: Compati
|
|
|
180
213
|
tuiSession = false;
|
|
181
214
|
terminalInputUnsubscribe?.();
|
|
182
215
|
terminalInputUnsubscribe = undefined;
|
|
216
|
+
resetBatchRegistry();
|
|
217
|
+
resetGrepRegistry();
|
|
218
|
+
resetBashTreeRegistry();
|
|
183
219
|
app.sessionShutdown();
|
|
184
220
|
// Tier C prototype patches stay installed across session switches. Pi renders
|
|
185
221
|
// the restored chat (renderBeforeBind) AFTER session_shutdown but BEFORE the
|
|
@@ -47,6 +47,14 @@ export interface BoxedRenderOptions {
|
|
|
47
47
|
state?: Record<string, unknown>;
|
|
48
48
|
/** Wall-clock elapsed override (used when metrics are not in result.details). */
|
|
49
49
|
elapsedMs?: number;
|
|
50
|
+
/** Width-dependent content lines rendered between the top border and the
|
|
51
|
+
* footer border of a compact box (replaces the blank breathing line).
|
|
52
|
+
* Each returned line is truncated to the box inner width by the renderer.
|
|
53
|
+
* Returns an empty array for no body (blank line preserved). */
|
|
54
|
+
bodyLines?: (contentWidth: number) => string[];
|
|
55
|
+
/** Right-side label embedded in the compact box bottom border before the
|
|
56
|
+
* corner (e.g. an expand hint such as `Ctrl+O for more`). */
|
|
57
|
+
bottomRightLabel?: string;
|
|
50
58
|
}
|
|
51
59
|
|
|
52
60
|
export function isExpanded(options: { expanded?: boolean } | undefined): boolean {
|
|
@@ -133,7 +141,7 @@ export function countWords(text: string): number {
|
|
|
133
141
|
return count;
|
|
134
142
|
}
|
|
135
143
|
|
|
136
|
-
function formatCompactCount(value: number): string {
|
|
144
|
+
export function formatCompactCount(value: number): string {
|
|
137
145
|
if (value < 1000) return `${Math.round(value)}`;
|
|
138
146
|
if (value < 10000) return `${(value / 1000).toFixed(1)}k`;
|
|
139
147
|
if (value < 1000000) return `${Math.round(value / 1000)}k`;
|
|
@@ -323,14 +331,21 @@ function formatBoxedStatusIcon(theme: BoxTheme, isError?: boolean): string {
|
|
|
323
331
|
return theme.fg(isError ? "error" : "success", icon);
|
|
324
332
|
}
|
|
325
333
|
|
|
334
|
+
/**
|
|
335
|
+
* Colored `➔ Name` prefix for tool titles (identity color). The status glyph
|
|
336
|
+
* (✓/✗) is appended separately by formatBoxedToolTitle.
|
|
337
|
+
*/
|
|
338
|
+
export function formatToolTitlePrefix(theme: BoxTheme, name: string): string {
|
|
339
|
+
return colorFromExtra(theme, "bashPromptColor", "bashMode", `➔ ${name}`);
|
|
340
|
+
}
|
|
341
|
+
|
|
326
342
|
export function formatBoxedToolTitle(theme: BoxTheme, name: string, isError?: boolean): string {
|
|
327
|
-
const rawTitle = `➔ ${name}`;
|
|
328
343
|
// On failure the whole title turns error-colored (not just the ✗) so a failed
|
|
329
344
|
// tool reads instantly; on success the tool keeps its identity color and only
|
|
330
345
|
// the ✓ carries the success color.
|
|
331
346
|
const coloredTitle = isError
|
|
332
|
-
? theme.fg("error",
|
|
333
|
-
: `${
|
|
347
|
+
? theme.fg("error", `➔ ${name} ✗`)
|
|
348
|
+
: `${formatToolTitlePrefix(theme, name)} ${formatBoxedStatusIcon(theme, false)}`;
|
|
334
349
|
return typeof theme?.bold === "function" ? theme.bold(coloredTitle) : coloredTitle;
|
|
335
350
|
}
|
|
336
351
|
|
|
@@ -423,6 +438,22 @@ export function boxBlankLine(theme: BoxTheme, width: number): string {
|
|
|
423
438
|
return `${boxFrameText(theme, BOX_VERTICAL)}${sidePad}${" ".repeat(contentWidth)}${sidePad}${boxFrameText(theme, BOX_VERTICAL)}`;
|
|
424
439
|
}
|
|
425
440
|
|
|
441
|
+
export function boxLineAligned(theme: BoxTheme, left: string, right: string, width: number): string {
|
|
442
|
+
const renderedWidth = boxWidth(width);
|
|
443
|
+
const contentWidth = boxInnerWidth(renderedWidth);
|
|
444
|
+
const rightWidth = safeVisibleWidth(right);
|
|
445
|
+
const sidePad = " ".repeat(BOX_SIDE_PADDING);
|
|
446
|
+
|
|
447
|
+
if (!right || rightWidth >= contentWidth) {
|
|
448
|
+
return boxLine(theme, right || left, renderedWidth);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const maxLeftWidth = Math.max(1, contentWidth - rightWidth - 1);
|
|
452
|
+
const truncatedLeft = safeTruncateToWidth(left, maxLeftWidth, "…");
|
|
453
|
+
const gap = " ".repeat(Math.max(1, contentWidth - safeVisibleWidth(truncatedLeft) - rightWidth));
|
|
454
|
+
return `${boxFrameText(theme, BOX_VERTICAL)}${sidePad}${truncatedLeft}${gap}${right}${sidePad}${boxFrameText(theme, BOX_VERTICAL)}`;
|
|
455
|
+
}
|
|
456
|
+
|
|
426
457
|
export function boxLineWithRight(theme: BoxTheme, left: string, right: string, width: number): string {
|
|
427
458
|
const renderedWidth = boxWidth(width);
|
|
428
459
|
const contentWidth = boxInnerWidth(renderedWidth);
|
|
@@ -599,9 +630,12 @@ export function renderCompactBoxedToolCall(
|
|
|
599
630
|
typeof options.state?.[COMPACT_FOOTER_KEY] === "string" ? options.state[COMPACT_FOOTER_KEY] : "";
|
|
600
631
|
const _footerIsError = Boolean(options.state?.[COMPACT_FOOTER_ERROR_KEY]);
|
|
601
632
|
const _footerIsPartial = Boolean(options.state?.[COMPACT_FOOTER_PARTIAL_KEY]);
|
|
633
|
+
const bodyLines = options.bodyLines ? options.bodyLines(boxInnerWidth(renderedWidth)) : [];
|
|
602
634
|
const lines = [
|
|
603
635
|
boxLabeledBorder(theme, BOX_ROUND_TOP_LEFT, BOX_ROUND_TOP_RIGHT, headerLabel, undefined, renderedWidth),
|
|
604
|
-
|
|
636
|
+
...(bodyLines.length > 0
|
|
637
|
+
? bodyLines.map((line) => boxLine(theme, line, renderedWidth))
|
|
638
|
+
: [boxBlankLine(theme, renderedWidth)]),
|
|
605
639
|
];
|
|
606
640
|
if (compactFooter) {
|
|
607
641
|
lines.push(
|
|
@@ -610,7 +644,7 @@ export function renderCompactBoxedToolCall(
|
|
|
610
644
|
BOX_ROUND_BOTTOM_LEFT,
|
|
611
645
|
BOX_ROUND_BOTTOM_RIGHT,
|
|
612
646
|
compactFooter,
|
|
613
|
-
|
|
647
|
+
options.bottomRightLabel,
|
|
614
648
|
renderedWidth,
|
|
615
649
|
),
|
|
616
650
|
);
|
|
@@ -622,7 +656,7 @@ export function renderCompactBoxedToolCall(
|
|
|
622
656
|
BOX_ROUND_BOTTOM_LEFT,
|
|
623
657
|
BOX_ROUND_BOTTOM_RIGHT,
|
|
624
658
|
theme.fg("dim", `… ${pendingText}`),
|
|
625
|
-
|
|
659
|
+
options.bottomRightLabel,
|
|
626
660
|
renderedWidth,
|
|
627
661
|
),
|
|
628
662
|
);
|