@quandev104/pi-style 0.1.2 → 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 +21 -0
- package/README.md +1 -2
- package/dist/extensions/pi-style.js +5919 -4966
- 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/status.ts +1 -1
- 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 +663 -45
- package/extension-src/pi-style/features/tools/boxed/batch.ts +459 -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 +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/quick-edit.ts +25 -4
- package/extension-src/pi-style/features/tools/boxed/read.ts +32 -189
- package/extension-src/pi-style/features/tools/boxed/session-config.ts +70 -4
- package/extension-src/pi-style/features/tools/boxed/shared.ts +41 -1
- package/extension-src/pi-style/features/tools/boxed/write.ts +105 -47
- 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 +47 -3
- package/extension-src/pi-style/shared/box.ts +117 -26
- package/extension-src/pi-style/shared/theme-extras.ts +0 -2
- package/package.json +1 -1
|
@@ -8,6 +8,10 @@ export interface ToolsRenderConfig {
|
|
|
8
8
|
maxExpandedLines: number;
|
|
9
9
|
dimOutput: boolean;
|
|
10
10
|
showElapsed: boolean;
|
|
11
|
+
/** Open-tree glyph for the done batch header (nerd `\u{F111}` / unicode `●`). */
|
|
12
|
+
batchOpenGlyph: string;
|
|
13
|
+
/** Nerd Font mode is active: file-type icons render in output trees. */
|
|
14
|
+
nerdFonts: boolean;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
let sessionToolsConfig: ToolsRenderConfig = {
|
|
@@ -15,6 +19,8 @@ let sessionToolsConfig: ToolsRenderConfig = {
|
|
|
15
19
|
maxExpandedLines: 50,
|
|
16
20
|
dimOutput: false,
|
|
17
21
|
showElapsed: true,
|
|
22
|
+
batchOpenGlyph: "●",
|
|
23
|
+
nerdFonts: false,
|
|
18
24
|
};
|
|
19
25
|
|
|
20
26
|
export function setToolsRenderConfig(config: Partial<ToolsRenderConfig>): void {
|
|
@@ -27,19 +33,79 @@ export function getToolsRenderConfig(): ToolsRenderConfig {
|
|
|
27
33
|
|
|
28
34
|
// Wall-clock elapsed tracking through the renderer context state (no tool
|
|
29
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).
|
|
30
41
|
|
|
31
42
|
const STARTED_AT_KEY = "__piStyleStartedAt";
|
|
32
|
-
const
|
|
43
|
+
const ENDED_AT_KEY = "__piStyleEndedAt";
|
|
44
|
+
const RESULT_SEEN_KEY = "__piStyleResultSeen";
|
|
45
|
+
const TICKER_KEY = "__piStyleElapsedTicker";
|
|
33
46
|
|
|
34
47
|
export function recordExecutionStarted(state: Record<string, unknown> | undefined, executionStarted: boolean): void {
|
|
35
48
|
if (!executionStarted || !state || typeof state !== "object") return;
|
|
36
49
|
if (typeof state[STARTED_AT_KEY] !== "number") state[STARTED_AT_KEY] = performance.now();
|
|
37
50
|
}
|
|
38
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
|
+
|
|
39
58
|
export function getStateElapsedMs(state: Record<string, unknown> | undefined): number | undefined {
|
|
40
59
|
if (!state || typeof state !== "object") return undefined;
|
|
41
|
-
|
|
42
|
-
|
|
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];
|
|
43
109
|
}
|
|
44
|
-
|
|
110
|
+
tickerStates.clear();
|
|
45
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
|
}
|
|
@@ -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,36 +31,94 @@ 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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
+
});
|
|
27
44
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
type NumberedLine = { number: string; content: string };
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Numbered preview lines for the written content, `cat -n` style: every split
|
|
49
|
+
* line keeps its number (including a trailing empty line produced by a final
|
|
50
|
+
* newline), right-aligned to the widest line number.
|
|
51
|
+
*/
|
|
52
|
+
function numberedPreviewLines(content: string): NumberedLine[] {
|
|
53
|
+
const normalized = replaceTabs(String(content ?? "")).replace(/\r/g, "");
|
|
54
|
+
if (!normalized) return [];
|
|
55
|
+
const lines = normalized.split("\n");
|
|
56
|
+
const gutterWidth = Math.max(1, String(lines.length).length);
|
|
57
|
+
return lines.map((line, index) => ({
|
|
58
|
+
number: String(index + 1).padStart(gutterWidth),
|
|
59
|
+
content: line,
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
35
62
|
|
|
36
|
-
|
|
63
|
+
/** One boxed preview row: dim gutter + toolOutput content. */
|
|
64
|
+
function formatNumberedLine(theme: BoxTheme, line: NumberedLine): string {
|
|
65
|
+
return `${theme.fg("borderMuted", `${line.number} `)}${theme.fg("toolOutput", line.content)}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Compact write box: path header, numbered content preview, metrics footer. */
|
|
69
|
+
function renderWritePreviewBox(
|
|
70
|
+
theme: BoxTheme,
|
|
71
|
+
detailLine: string,
|
|
72
|
+
content: string,
|
|
73
|
+
options: {
|
|
74
|
+
state?: Record<string, unknown>;
|
|
75
|
+
isError: boolean;
|
|
76
|
+
isPending: boolean;
|
|
77
|
+
running?: boolean;
|
|
78
|
+
expanded: boolean;
|
|
79
|
+
},
|
|
80
|
+
): Component {
|
|
81
|
+
const preview = numberedPreviewLines(content);
|
|
82
|
+
const config = getToolsRenderConfig();
|
|
83
|
+
const budget = options.expanded ? config.maxExpandedLines : config.maxCollapsedLines;
|
|
84
|
+
const truncated = preview.length > budget;
|
|
85
|
+
|
|
86
|
+
return renderCompactBoxedToolCall(theme, "Write", detailLine, {
|
|
87
|
+
...(options.state ? { state: options.state } : {}),
|
|
88
|
+
isError: options.isError,
|
|
89
|
+
isPending: options.isPending,
|
|
90
|
+
running: Boolean(options.running),
|
|
91
|
+
bodyLines: () => {
|
|
92
|
+
if (preview.length === 0) return [];
|
|
93
|
+
const shown = preview.slice(0, budget).map((line) => formatNumberedLine(theme, line));
|
|
94
|
+
if (!truncated) return shown;
|
|
95
|
+
const omitted = preview.length - budget;
|
|
96
|
+
const note = options.expanded ? `… ${omitted} more lines omitted by render budget` : `… ${omitted} more lines`;
|
|
97
|
+
return [...shown, theme.fg("muted", note)];
|
|
98
|
+
},
|
|
99
|
+
...(options.expanded || options.isPending || !truncated ? {} : { bottomRightLabel: WRITE_EXPAND_HINT }),
|
|
100
|
+
});
|
|
37
101
|
}
|
|
38
102
|
|
|
39
103
|
export const writeTool: BoxedToolDefinition = {
|
|
40
104
|
call(args, theme, context) {
|
|
41
105
|
noteExecutionStart(context);
|
|
42
106
|
const detail = displayPath(String(args?.path ?? args?.file_path ?? ""), context);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
107
|
+
const detailLine = `${theme.fg("dim", "Path: ")}${detail}`;
|
|
108
|
+
// On error keep the plain open box: the result renderer continues it with
|
|
109
|
+
// the boxed error body, so call and result never duplicate a box.
|
|
110
|
+
if (context.isError) {
|
|
111
|
+
return compactCall(theme, "Write", detailLine, {
|
|
112
|
+
detailKey: detail,
|
|
113
|
+
context,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return renderWritePreviewBox(theme, detailLine, String(args?.content ?? ""), {
|
|
117
|
+
state: context.state,
|
|
118
|
+
isError: Boolean(context.isError),
|
|
119
|
+
isPending: Boolean(context.isPartial),
|
|
120
|
+
running: Boolean(context.executionStarted),
|
|
121
|
+
expanded: Boolean(context.expanded),
|
|
46
122
|
});
|
|
47
123
|
},
|
|
48
124
|
result(result, options, theme, context) {
|
|
@@ -59,31 +135,13 @@ export const writeTool: BoxedToolDefinition = {
|
|
|
59
135
|
});
|
|
60
136
|
}
|
|
61
137
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
}
|
|
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;
|
|
81
142
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return
|
|
85
|
-
widthKey,
|
|
86
|
-
footerLines: resultFooterLines(theme, result, context),
|
|
87
|
-
});
|
|
143
|
+
// Success (compact and expanded): the preview box closes with the metrics
|
|
144
|
+
// footer stored into the shared renderer state; the result adds nothing.
|
|
145
|
+
return compactFooterWithState(theme, result, context);
|
|
88
146
|
},
|
|
89
147
|
};
|
|
@@ -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
|