pi-minimalist 0.2.0 → 0.2.2
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/README.md +3 -3
- package/index.ts +2 -9
- package/package.json +1 -1
- package/src/bridge.ts +76 -55
- package/src/components.ts +97 -32
- package/src/config-file.ts +7 -83
- package/src/config-ui.ts +0 -6
- package/src/config.ts +11 -33
- package/src/core-patch.ts +167 -178
- package/src/row.ts +44 -28
- package/src/run-grouping.ts +98 -52
- package/src/thinking.ts +5 -8
- package/src/tool-renderer.ts +21 -10
- package/src/tools.ts +57 -42
package/src/config.ts
CHANGED
|
@@ -7,11 +7,7 @@
|
|
|
7
7
|
* are readable on their own.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type {
|
|
11
|
-
|
|
12
|
-
// ThemeBg is not exported publicly, so recover it from Theme.bg's own signature
|
|
13
|
-
// rather than importing a deep dist path (which is not a package export).
|
|
14
|
-
type ThemeBg = Parameters<Theme["bg"]>[0];
|
|
10
|
+
import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
15
11
|
|
|
16
12
|
/** Top-level key inside Pi's settings.json. */
|
|
17
13
|
export const SETTINGS_KEY = "minimalist";
|
|
@@ -41,6 +37,9 @@ export type Glyphs = {
|
|
|
41
37
|
|
|
42
38
|
export type GlyphStyle = "unicode" | "ascii";
|
|
43
39
|
|
|
40
|
+
/** Marker on the header of an opened run. Not user-configurable. */
|
|
41
|
+
export const OPEN_MARKER: Record<GlyphStyle, string> = { unicode: "▾", ascii: "v" };
|
|
42
|
+
|
|
44
43
|
export const GLYPH_PRESETS: Record<GlyphStyle, Glyphs> = {
|
|
45
44
|
unicode: { done: "✓", failed: "✗", running: "•", queued: "›", gutter: "▌", timer: "⏱", count: "×" },
|
|
46
45
|
// Every ASCII glyph is exactly one column wide, so rows stay aligned in fonts
|
|
@@ -56,9 +55,8 @@ export const GLYPH_PRESETS: Record<GlyphStyle, Glyphs> = {
|
|
|
56
55
|
* Which Pi theme token paints each part of a row.
|
|
57
56
|
*
|
|
58
57
|
* Overridable, but the defaults are deliberate: the action word is a LABEL, not
|
|
59
|
-
* a warning, so it uses `success` green even for mutating tools
|
|
60
|
-
*
|
|
61
|
-
* prose".
|
|
58
|
+
* a warning, so it uses `success` green even for mutating tools (orange would
|
|
59
|
+
* collide with "highlighted prose").
|
|
62
60
|
*/
|
|
63
61
|
export type Tokens = {
|
|
64
62
|
label: ThemeColor;
|
|
@@ -66,7 +64,6 @@ export type Tokens = {
|
|
|
66
64
|
thinking: ThemeColor;
|
|
67
65
|
error: ThemeColor;
|
|
68
66
|
outputGutter: ThemeColor;
|
|
69
|
-
activeBg: ThemeBg;
|
|
70
67
|
};
|
|
71
68
|
|
|
72
69
|
export const DEFAULT_TOKENS: Tokens = {
|
|
@@ -75,7 +72,6 @@ export const DEFAULT_TOKENS: Tokens = {
|
|
|
75
72
|
thinking: "thinkingText",
|
|
76
73
|
error: "error",
|
|
77
74
|
outputGutter: "borderMuted",
|
|
78
|
-
activeBg: "toolPendingBg",
|
|
79
75
|
};
|
|
80
76
|
|
|
81
77
|
// ---------------------------------------------------------------------------
|
|
@@ -113,8 +109,6 @@ export type BasicSettings = {
|
|
|
113
109
|
groupToolRuns: boolean;
|
|
114
110
|
/** Replace all but the latest assistant prose block with one summary row. */
|
|
115
111
|
foldIntermediateActivity: boolean;
|
|
116
|
-
/** Keep commentary visible until OpenAI starts its final-answer message. */
|
|
117
|
-
foldActivityOnFinalAnswer: boolean;
|
|
118
112
|
/** What replaces the activity hidden before the latest prose block. */
|
|
119
113
|
activitySummary: ActivitySummary;
|
|
120
114
|
/** Draw the left gutter bar. */
|
|
@@ -137,12 +131,6 @@ export type AdvancedSettings = {
|
|
|
137
131
|
excludeTools: string[];
|
|
138
132
|
glyphs: Partial<Glyphs>;
|
|
139
133
|
tokens: Partial<Tokens>;
|
|
140
|
-
/**
|
|
141
|
-
* Sanity cap on detail text length. NOT a display limit — rows truncate at the
|
|
142
|
-
* real viewport width. This only stops a multi-megabyte heredoc from being
|
|
143
|
-
* whitespace-collapsed, colored and measured on every repaint.
|
|
144
|
-
*/
|
|
145
|
-
maxDetailChars: number;
|
|
146
134
|
};
|
|
147
135
|
|
|
148
136
|
export type Settings = BasicSettings & AdvancedSettings;
|
|
@@ -152,7 +140,6 @@ export const DEFAULT_BASIC: BasicSettings = {
|
|
|
152
140
|
compactToolRows: true,
|
|
153
141
|
groupToolRuns: true,
|
|
154
142
|
foldIntermediateActivity: false,
|
|
155
|
-
foldActivityOnFinalAnswer: false,
|
|
156
143
|
activitySummary: "elapsed",
|
|
157
144
|
gutter: true,
|
|
158
145
|
timer: true,
|
|
@@ -175,13 +162,11 @@ export const PRESETS: Record<Exclude<Preset, "custom">, Record<LookKey, boolean>
|
|
|
175
162
|
|
|
176
163
|
export const DEFAULT_ADVANCED: AdvancedSettings = {
|
|
177
164
|
// `subagent` ships a renderer showing run id, state and a ctrl+o hint that one
|
|
178
|
-
// line cannot carry. Excluded by NAME rather than by
|
|
179
|
-
// have its own renderer?" rule, which silently
|
|
180
|
-
// and made the behavior impossible to discover from the UI.
|
|
165
|
+
// line cannot carry. Excluded by NAME rather than by a "does this tool
|
|
166
|
+
// have its own renderer?" rule, which would silently exempt every third-party tool.
|
|
181
167
|
excludeTools: ["subagent"],
|
|
182
168
|
glyphs: {},
|
|
183
169
|
tokens: {},
|
|
184
|
-
maxDetailChars: 4000,
|
|
185
170
|
};
|
|
186
171
|
|
|
187
172
|
export const DEFAULTS: Settings = { ...DEFAULT_BASIC, ...DEFAULT_ADVANCED };
|
|
@@ -274,17 +259,10 @@ export class Config {
|
|
|
274
259
|
}
|
|
275
260
|
|
|
276
261
|
/**
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
* The exclusion list is the ONLY exemption, so the master switch is checked
|
|
280
|
-
* alongside it wherever claiming happens.
|
|
262
|
+
* True when this tool should be drawn as a compact row. The exclusion list is
|
|
263
|
+
* the ONLY exemption, so the master switch is checked alongside it.
|
|
281
264
|
*/
|
|
282
|
-
isExcluded(toolName: string): boolean {
|
|
283
|
-
return this.view.excludeTools.includes(toolName);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/** True when this tool should be drawn as a compact row. */
|
|
287
265
|
compacts(toolName: string): boolean {
|
|
288
|
-
return this.view.compactToolRows && !this.
|
|
266
|
+
return this.view.compactToolRows && !this.view.excludeTools.includes(toolName);
|
|
289
267
|
}
|
|
290
268
|
}
|
package/src/core-patch.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* RUNTIME core patches
|
|
2
|
+
* RUNTIME core patches: wrappers on Pi's real component prototypes.
|
|
3
3
|
*
|
|
4
|
-
* WHY THIS WORKS
|
|
5
|
-
*
|
|
4
|
+
* WHY THIS WORKS
|
|
5
|
+
* --------------
|
|
6
6
|
* Pi's bundled CLI loads extensions through jiti with
|
|
7
7
|
* `virtualModules: VIRTUAL_MODULES` (see core/extensions/loader.ts; the bundle
|
|
8
8
|
* sets `isBundledNode = true`). Those virtual modules are the bundle's OWN live
|
|
@@ -10,32 +10,26 @@
|
|
|
10
10
|
* `@earendil-works/pi-coding-agent`, it receives the very same class objects the
|
|
11
11
|
* running TUI instantiates — not a second copy from `dist/`.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* Every seam is a `prototype` method on a public export, so wrapping at load time
|
|
14
|
+
* leaves nothing on disk modified (a Pi upgrade cannot silently revert it) and
|
|
15
|
+
* the wrappers are ordinary TypeScript, unit-testable against the real
|
|
16
|
+
* components (see test/core-patch.test.ts).
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* components (see test/core-patch.test.ts).
|
|
22
|
-
*
|
|
23
|
-
* Each wrapper reads its behavior from the process-global bridge slots at CALL
|
|
24
|
-
* time (see bridge.ts), so `/reload` swaps behavior without re-wrapping — and
|
|
25
|
-
* an unloaded extension degrades to Pi's native rendering.
|
|
18
|
+
* Each wrapper reads its behavior from the process-global `Bridge` at CALL time
|
|
19
|
+
* (see bridge.ts), so `/reload` swaps behavior without re-wrapping — and an
|
|
20
|
+
* unloaded extension degrades to Pi's native rendering.
|
|
26
21
|
*/
|
|
27
22
|
|
|
28
23
|
import type { Component } from "@earendil-works/pi-tui";
|
|
29
|
-
import {
|
|
30
|
-
import { FoldableProse } from "./components.ts";
|
|
31
|
-
import type {
|
|
32
|
-
import type {
|
|
24
|
+
import { readBridge as bridge, type Bridge } from "./bridge.ts";
|
|
25
|
+
import { EmptyComponent, FoldableProse } from "./components.ts";
|
|
26
|
+
import type { ThemeLike } from "./row.ts";
|
|
27
|
+
import type { NativeResultRenderer, ToolRenderer } from "./tool-renderer.ts";
|
|
33
28
|
|
|
34
29
|
type Globals = Record<symbol, unknown>;
|
|
35
30
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
31
|
+
const EMPTY = new EmptyComponent();
|
|
32
|
+
const barrier = (id: string) => bridge()?.observeBarrier(id);
|
|
39
33
|
|
|
40
34
|
/** Applied-once marker per prototype, so /reload never double-wraps. */
|
|
41
35
|
const PATCHED = Symbol.for("pi.minimalist.corePatched");
|
|
@@ -59,20 +53,6 @@ function once(target: object, name: string, apply: () => void): void {
|
|
|
59
53
|
// each patch function keeps its precise internal shape for the body below.
|
|
60
54
|
// ---------------------------------------------------------------------------
|
|
61
55
|
|
|
62
|
-
type ToolRendererBridge = {
|
|
63
|
-
renderShell?: "self" | "default";
|
|
64
|
-
handles?: (name: string) => boolean;
|
|
65
|
-
renderCall?: (name: string, args: unknown, theme: unknown, context: unknown) => Component;
|
|
66
|
-
renderResult?: (
|
|
67
|
-
name: string,
|
|
68
|
-
result: unknown,
|
|
69
|
-
options: unknown,
|
|
70
|
-
theme: unknown,
|
|
71
|
-
context: unknown,
|
|
72
|
-
nativeRenderer?: unknown,
|
|
73
|
-
) => Component | undefined;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
56
|
type ToolExecutionProto = {
|
|
77
57
|
toolName: string;
|
|
78
58
|
toolCallId: string;
|
|
@@ -94,16 +74,12 @@ type ToolExecutionProto = {
|
|
|
94
74
|
};
|
|
95
75
|
|
|
96
76
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
77
|
+
* Give the compact renderer priority and normalize the self-shell spacer when a
|
|
78
|
+
* tool row hosts the one folded-activity summary.
|
|
99
79
|
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* `hasRendererDefinition()` and the two getters now always answer for a handled
|
|
104
|
-
* tool, core never reaches its verbose `formatToolExecution()` branch, and
|
|
105
|
-
* late-registered tools with `toolDefinition === undefined` are covered by the
|
|
106
|
-
* same condition.
|
|
80
|
+
* `hasRendererDefinition()` and the two getters always answer for a handled tool,
|
|
81
|
+
* so core never reaches its verbose `formatToolExecution()` branch, and
|
|
82
|
+
* late-registered tools with `toolDefinition === undefined` are covered too.
|
|
107
83
|
*
|
|
108
84
|
* The native tool definitions are NOT touched: this is render-only, so built-ins
|
|
109
85
|
* keep their builtin source ownership and pi-subagents keeps exposing
|
|
@@ -117,19 +93,15 @@ export function patchToolExecution(proto: ToolExecutionProto): void {
|
|
|
117
93
|
/**
|
|
118
94
|
* "Ours" when the bridge claims this tool name — and ONLY then.
|
|
119
95
|
*
|
|
120
|
-
* `handles()` is
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* fallback here. Under the old whitelist it was load-bearing. With a blacklist
|
|
126
|
-
* it silently OVERRODE the user: an excluded tool with no renderer, and every
|
|
127
|
-
* tool when `compactToolRows` was off, got compacted anyway.
|
|
96
|
+
* `handles()` is the single authority (blacklist plus the master switch), so it
|
|
97
|
+
* covers rendererless MCP/third-party tools and late-registered tools whose
|
|
98
|
+
* definition is missing from the UI lookup entirely. Do NOT also claim "any
|
|
99
|
+
* tool with no renderer of its own": that overrides the user's exclusions and
|
|
100
|
+
* the master switch.
|
|
128
101
|
*/
|
|
129
|
-
function claims(this: ToolExecutionProto):
|
|
130
|
-
const renderer = bridge
|
|
131
|
-
|
|
132
|
-
return renderer.handles?.(this.toolName) ? renderer : undefined;
|
|
102
|
+
function claims(this: ToolExecutionProto): ToolRenderer | undefined {
|
|
103
|
+
const renderer = bridge()?.toolRenderer;
|
|
104
|
+
return renderer?.handles(this.toolName) ? renderer : undefined;
|
|
133
105
|
}
|
|
134
106
|
|
|
135
107
|
once(proto, "toolExecution", () => {
|
|
@@ -138,6 +110,8 @@ export function patchToolExecution(proto: ToolExecutionProto): void {
|
|
|
138
110
|
const ownership = new WeakMap<ToolExecutionProto, boolean>();
|
|
139
111
|
function syncShell(this: ToolExecutionProto): boolean {
|
|
140
112
|
const claimed = claims.call(this) !== undefined;
|
|
113
|
+
// A card we do not draw is still visible: let it cut folded runs.
|
|
114
|
+
if (!claimed && this.toolCallId) barrier(this.toolCallId);
|
|
141
115
|
const shell = this.hasRendererDefinition()
|
|
142
116
|
? this.getRenderShell() === "self" ? this.selfRenderContainer : this.contentBox
|
|
143
117
|
: this.contentTextRegion;
|
|
@@ -167,17 +141,17 @@ export function patchToolExecution(proto: ToolExecutionProto): void {
|
|
|
167
141
|
|
|
168
142
|
proto.getCallRenderer = function () {
|
|
169
143
|
const renderer = claims.call(this);
|
|
170
|
-
if (!renderer
|
|
144
|
+
if (!renderer) return this.toolDefinition?.renderCall;
|
|
171
145
|
return (args: unknown, theme: unknown, context: unknown) =>
|
|
172
|
-
renderer.renderCall
|
|
146
|
+
renderer.renderCall(this.toolName, args, theme, context);
|
|
173
147
|
};
|
|
174
148
|
|
|
175
149
|
proto.getResultRenderer = function () {
|
|
176
150
|
const renderer = claims.call(this);
|
|
177
|
-
if (!renderer
|
|
178
|
-
const nativeRenderer = this.toolDefinition?.renderResult;
|
|
151
|
+
if (!renderer) return this.toolDefinition?.renderResult;
|
|
152
|
+
const nativeRenderer = this.toolDefinition?.renderResult as NativeResultRenderer | undefined;
|
|
179
153
|
return (result: unknown, options: unknown, theme: unknown, context: unknown) => {
|
|
180
|
-
const component = renderer.renderResult
|
|
154
|
+
const component = renderer.renderResult(this.toolName, result, options, theme, context, nativeRenderer);
|
|
181
155
|
// `undefined` means "show Pi's own text output". Core's RESULT-RENDERER
|
|
182
156
|
// path (unlike its fallback path) would wrap undefined in a MouseRegion
|
|
183
157
|
// and crash at render, so resolve the fallback here instead.
|
|
@@ -203,7 +177,7 @@ export function patchToolExecution(proto: ToolExecutionProto): void {
|
|
|
203
177
|
// Rebuild once on transition, never on every render (or during a renderer).
|
|
204
178
|
if (syncShell.call(this)) nativeUpdateDisplay.call(this);
|
|
205
179
|
const lines = nativeRender.call(this, width);
|
|
206
|
-
const ownsSummary = bridge
|
|
180
|
+
const ownsSummary = bridge()?.activitySummaryRow(this.toolCallId);
|
|
207
181
|
// The summary must have the same one-line separation regardless of which
|
|
208
182
|
// tool/assistant component happened to become its host.
|
|
209
183
|
return ownsSummary ? withOneLeadingBlank(lines) : lines;
|
|
@@ -211,34 +185,40 @@ export function patchToolExecution(proto: ToolExecutionProto): void {
|
|
|
211
185
|
});
|
|
212
186
|
}
|
|
213
187
|
|
|
214
|
-
/**
|
|
215
|
-
|
|
188
|
+
/**
|
|
189
|
+
* A user message is visible but never observed otherwise, so without this a
|
|
190
|
+
* folded run could jump over it. It is a barrier: it only cuts runs.
|
|
191
|
+
*/
|
|
192
|
+
export function patchUserMessage(proto: { rebuild(): void }): void {
|
|
193
|
+
once(proto, "userMessage", () => {
|
|
194
|
+
const nativeRebuild = proto.rebuild;
|
|
195
|
+
// Keyed on the text: Pi builds NEW components for the same messages on every
|
|
196
|
+
// chat rebuild, and a per-component id would append a fresh barrier each time.
|
|
197
|
+
proto.rebuild = function (this: { rebuild(): void; text: string }) {
|
|
198
|
+
nativeRebuild.call(this);
|
|
199
|
+
barrier("user:" + this.text);
|
|
200
|
+
};
|
|
201
|
+
});
|
|
202
|
+
}
|
|
216
203
|
|
|
217
204
|
// ---------------------------------------------------------------------------
|
|
218
|
-
//
|
|
205
|
+
// Assistant messages: thinking, prose, summaries
|
|
219
206
|
// ---------------------------------------------------------------------------
|
|
220
207
|
|
|
221
|
-
type ThinkingBridge = (
|
|
222
|
-
text: string,
|
|
223
|
-
theme: unknown,
|
|
224
|
-
pad: number,
|
|
225
|
-
streaming?: boolean,
|
|
226
|
-
owner?: object,
|
|
227
|
-
runIndex?: number,
|
|
228
|
-
) => Component;
|
|
229
|
-
|
|
230
208
|
type AssistantProto = {
|
|
231
209
|
contentContainer: { children: Component[] };
|
|
232
210
|
hideThinkingBlock: boolean;
|
|
233
211
|
thinkingVisibilityOverrides: Map<number, boolean>;
|
|
234
212
|
markdownTheme: Record<string, unknown>;
|
|
235
|
-
outputPad: number;
|
|
236
213
|
isStreaming: boolean;
|
|
237
214
|
lastMessage?: any;
|
|
238
215
|
updateContent(message: any, isStreaming?: boolean): void;
|
|
239
216
|
render(width: number): string[];
|
|
217
|
+
handleMouse(event: MouseLike): unknown;
|
|
240
218
|
};
|
|
241
219
|
|
|
220
|
+
type MouseLike = { y: number; height: number };
|
|
221
|
+
|
|
242
222
|
/** One thinking run: consecutive `thinking` blocks, exactly as core groups them. */
|
|
243
223
|
function thinkingRuns(message: any): string[][] {
|
|
244
224
|
const runs: string[][] = [];
|
|
@@ -258,8 +238,7 @@ function thinkingRuns(message: any): string[][] {
|
|
|
258
238
|
|
|
259
239
|
/**
|
|
260
240
|
* Core wraps every thinking run — and ONLY a thinking run — in a MouseRegion, so
|
|
261
|
-
* the Nth MouseRegion in contentContainer is the Nth thinking run.
|
|
262
|
-
* seam this patch uses instead of the old bundle rewrite.
|
|
241
|
+
* the Nth MouseRegion in contentContainer is the Nth thinking run.
|
|
263
242
|
*
|
|
264
243
|
* Recognized STRUCTURALLY, not by `instanceof` or `constructor.name`, because
|
|
265
244
|
* neither is reliable across Pi's loading modes:
|
|
@@ -274,15 +253,10 @@ function isMouseRegion(value: any): boolean {
|
|
|
274
253
|
return typeof value?.onMouse === "function" && value.child !== undefined;
|
|
275
254
|
}
|
|
276
255
|
|
|
277
|
-
function thinkingRegions(children: Component[]): any[] {
|
|
278
|
-
return children.filter(isMouseRegion);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
256
|
/**
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
* Replaces five bundle rewrites inside `updateContent` with one wrapper:
|
|
257
|
+
* Collapsed preview, streaming expansion, all-purple expanded Markdown, the
|
|
258
|
+
* run-grouping chronology/spacer hooks, and the summary spacing and mouse
|
|
259
|
+
* correction in `render` / `handleMouse`. `updateContent` does:
|
|
286
260
|
*
|
|
287
261
|
* - active thinking: a streaming block stays COLLAPSED by default (its compact
|
|
288
262
|
* preview already shows the latest text). `keepActiveThinkingExpanded` forces
|
|
@@ -303,14 +277,19 @@ export function patchAssistantMessage(proto: AssistantProto): void {
|
|
|
303
277
|
once(proto, "assistantMessage", () => {
|
|
304
278
|
const nativeUpdateContent = proto.updateContent;
|
|
305
279
|
const nativeRender = proto.render;
|
|
280
|
+
const nativeHandleMouse = proto.handleMouse;
|
|
306
281
|
const compactState = new WeakMap<AssistantProto, boolean>();
|
|
282
|
+
// withOneLeadingBlank() changes the lines AFTER Container.render() recorded
|
|
283
|
+
// its mouse layout, so Pi's y (relative to the lines we returned) would
|
|
284
|
+
// miss the summary by that shift. Remember it to translate clicks back.
|
|
285
|
+
const mouseShift = new WeakMap<AssistantProto, { shift: number; height: number }>();
|
|
307
286
|
|
|
308
287
|
proto.updateContent = function (this: AssistantProto, message: any, isStreaming = this.isStreaming) {
|
|
309
288
|
const savedHide = this.hideThinkingBlock;
|
|
310
289
|
const savedOverrides = this.thinkingVisibilityOverrides;
|
|
311
290
|
// Default: a streaming block stays collapsed to its compact preview, which
|
|
312
291
|
// already shows the newest text. Opt in to force it open instead.
|
|
313
|
-
const forceOpen = isStreaming && bridge
|
|
292
|
+
const forceOpen = isStreaming && bridge()?.keepActiveThinkingExpanded() === true;
|
|
314
293
|
if (forceOpen) {
|
|
315
294
|
this.hideThinkingBlock = false;
|
|
316
295
|
this.thinkingVisibilityOverrides = new Map();
|
|
@@ -334,41 +313,43 @@ export function patchAssistantMessage(proto: AssistantProto): void {
|
|
|
334
313
|
this.updateContent(this.lastMessage, this.isStreaming);
|
|
335
314
|
}
|
|
336
315
|
const lines = nativeRender.call(this, width);
|
|
337
|
-
const view = bridge
|
|
316
|
+
const view = bridge()?.activityMessageView(this);
|
|
338
317
|
if (view === "hidden") return [];
|
|
339
|
-
|
|
318
|
+
if (view !== "summary") {
|
|
319
|
+
mouseShift.delete(this);
|
|
320
|
+
return lines;
|
|
321
|
+
}
|
|
322
|
+
mouseShift.set(this, { shift: lines.findIndex((line) => !visiblyBlank(line)) - 1, height: lines.length });
|
|
323
|
+
return withOneLeadingBlank(lines);
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
proto.handleMouse = function (this: AssistantProto, event: MouseLike) {
|
|
327
|
+
const moved = mouseShift.get(this);
|
|
328
|
+
return nativeHandleMouse.call(this, moved ? { ...event, y: event.y + moved.shift, height: moved.height } : event);
|
|
340
329
|
};
|
|
341
330
|
});
|
|
342
331
|
}
|
|
343
332
|
|
|
344
333
|
function compactThinking(): boolean {
|
|
345
|
-
return bridge
|
|
334
|
+
return bridge()?.compactThinking() === true;
|
|
346
335
|
}
|
|
347
336
|
|
|
337
|
+
type Decorating = {
|
|
338
|
+
bridge: Bridge;
|
|
339
|
+
theme: ThemeLike | undefined;
|
|
340
|
+
component: AssistantProto;
|
|
341
|
+
message: any;
|
|
342
|
+
isStreaming: boolean;
|
|
343
|
+
compact: boolean;
|
|
344
|
+
};
|
|
345
|
+
|
|
348
346
|
function decorateThinking(component: AssistantProto, message: any, isStreaming: boolean, compact: boolean): void {
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
);
|
|
353
|
-
const observeThinking = bridge<(owner: object, run: number, streaming: boolean, hidden: boolean) => string>(
|
|
354
|
-
"observeThinking",
|
|
355
|
-
);
|
|
356
|
-
const observeProse = bridge<(
|
|
357
|
-
owner: object,
|
|
358
|
-
contentIndex: number,
|
|
359
|
-
signal: {
|
|
360
|
-
phase?: "commentary" | "final_answer";
|
|
361
|
-
stopReason?: string;
|
|
362
|
-
streaming?: boolean;
|
|
363
|
-
timestamp?: number;
|
|
364
|
-
},
|
|
365
|
-
) => string>("observeProse");
|
|
366
|
-
const proseView = bridge<(id: string, theme: ThemeLike) => Row | null | undefined>("proseView");
|
|
367
|
-
const messageSpacer = bridge<(owner: object) => boolean>("messageSpacer");
|
|
368
|
-
const theme = liveTheme();
|
|
347
|
+
const b = bridge();
|
|
348
|
+
if (!b) return;
|
|
349
|
+
const ctx: Decorating = { bridge: b, theme: liveTheme(), component, message, isStreaming, compact };
|
|
369
350
|
|
|
370
351
|
const children = component.contentContainer.children;
|
|
371
|
-
const regions =
|
|
352
|
+
const regions: any[] = children.filter(isMouseRegion);
|
|
372
353
|
const runs = thinkingRuns(message);
|
|
373
354
|
|
|
374
355
|
// Direct Markdown children correspond one-for-one with non-empty text blocks;
|
|
@@ -384,73 +365,88 @@ function decorateThinking(component: AssistantProto, message: any, isStreaming:
|
|
|
384
365
|
for (let i = 0; i < (message?.content?.length ?? 0); i++) {
|
|
385
366
|
const content = message.content[i];
|
|
386
367
|
if (content?.type === "text" && String(content.text ?? "").trim()) {
|
|
387
|
-
|
|
388
|
-
const id = observeProse?.(component, i, {
|
|
389
|
-
phase,
|
|
390
|
-
stopReason: message.stopReason,
|
|
391
|
-
streaming: isStreaming,
|
|
392
|
-
timestamp: message.timestamp,
|
|
393
|
-
});
|
|
394
|
-
const target = proseChildren[prose++];
|
|
395
|
-
if (id && target && proseView && theme) {
|
|
396
|
-
children[target.index] = new FoldableProse(target.child, () => proseView(id, theme as ThemeLike));
|
|
397
|
-
}
|
|
368
|
+
wrapProse(ctx, i, proseChildren[prose++]);
|
|
398
369
|
continue;
|
|
399
370
|
}
|
|
400
371
|
if (content?.type !== "thinking") continue;
|
|
401
372
|
while (i + 1 < message.content.length && message.content[i + 1]?.type === "thinking") i++;
|
|
402
373
|
const blocks = runs[run];
|
|
403
374
|
if (!blocks) continue;
|
|
404
|
-
const
|
|
375
|
+
const region = regions[run];
|
|
405
376
|
const runIndex = run++;
|
|
406
|
-
if (
|
|
407
|
-
|
|
408
|
-
const inner = entry.child as any;
|
|
409
|
-
// Collapsed runs are a Text (the hidden label); expanded runs are a Markdown.
|
|
410
|
-
// Only Markdown carries a `theme` field, which is also the field the
|
|
411
|
-
// all-purple recolor needs, so one check serves both branches.
|
|
412
|
-
const hidden = inner?.theme === undefined;
|
|
413
|
-
// Pass `hidden` THROUGH, never `!hidden`. bridge.ts negates it into
|
|
414
|
-
// `expanded` itself, so negating here too inverted all run folding: expanded
|
|
415
|
-
// thinking became foldable (swallowing whole runs of tool rows into one
|
|
416
|
-
// summary) and collapsed thinking stopped folding entirely.
|
|
417
|
-
const id = observeThinking?.(component, runIndex, isStreaming, hidden);
|
|
418
|
-
|
|
419
|
-
if (!compact) {
|
|
420
|
-
// Native thinking still participates in the separately enabled activity
|
|
421
|
-
// fold, just like native prose. Otherwise keep Pi's component untouched.
|
|
422
|
-
if (id && proseView && theme) {
|
|
423
|
-
entry.child = new FoldableProse(inner, () => proseView(id, theme as ThemeLike));
|
|
424
|
-
}
|
|
425
|
-
continue;
|
|
426
|
-
}
|
|
377
|
+
if (region) wrapThinkingRun(ctx, region, blocks, runIndex);
|
|
378
|
+
}
|
|
427
379
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
380
|
+
wrapSpacer(ctx, children);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Wrap a host in the folding component for entry `id`. */
|
|
384
|
+
function foldable({ bridge: b, theme }: Decorating, inner: Component, id: string): Component {
|
|
385
|
+
return new FoldableProse(
|
|
386
|
+
inner,
|
|
387
|
+
() => b.proseView(id, theme!),
|
|
388
|
+
() => b.proseHeader(id, theme!),
|
|
389
|
+
(onHeader) => b.click(id, onHeader),
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Observe one prose block and, when a theme is live, let it fold. */
|
|
394
|
+
function wrapProse(ctx: Decorating, contentIndex: number, target: { child: any; index: number } | undefined): void {
|
|
395
|
+
const { bridge: b, component, message, isStreaming, theme } = ctx;
|
|
396
|
+
const id = b.observeProse(component, contentIndex, {
|
|
397
|
+
stopReason: message.stopReason,
|
|
398
|
+
streaming: isStreaming,
|
|
399
|
+
timestamp: message.timestamp,
|
|
400
|
+
});
|
|
401
|
+
if (id && target && theme) component.contentContainer.children[target.index] = foldable(ctx, target.child, id);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Observe one thinking run (a MouseRegion) and replace what it shows. */
|
|
405
|
+
function wrapThinkingRun(ctx: Decorating, region: any, blocks: string[], runIndex: number): void {
|
|
406
|
+
const { bridge: b, component, message, isStreaming, theme, compact } = ctx;
|
|
407
|
+
const inner = region.child as any;
|
|
408
|
+
// Collapsed runs are a Text (the hidden label); expanded runs are a Markdown.
|
|
409
|
+
// Only Markdown carries a `theme` field, which is also the field the
|
|
410
|
+
// all-purple recolor needs, so one check serves both branches.
|
|
411
|
+
const hidden = inner?.theme === undefined;
|
|
412
|
+
// Pass `hidden` THROUGH, never `!hidden`. The bridge negates it into
|
|
413
|
+
// `expanded` itself, so negating here too inverted all run folding: expanded
|
|
414
|
+
// thinking became foldable (swallowing whole runs of tool rows into one
|
|
415
|
+
// summary) and collapsed thinking stopped folding entirely.
|
|
416
|
+
const id = b.observeThinking(component, runIndex, isStreaming, hidden, message.timestamp);
|
|
417
|
+
|
|
418
|
+
if (!compact) {
|
|
419
|
+
// Native thinking still participates in the separately enabled activity
|
|
420
|
+
// fold, just like native prose. Otherwise keep Pi's component untouched.
|
|
421
|
+
if (id && theme) region.child = foldable(ctx, inner, id);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (!theme) return;
|
|
425
|
+
|
|
426
|
+
if (hidden) {
|
|
427
|
+
// MouseRegion.child is `private` in TS only; reassigning keeps the
|
|
428
|
+
// existing click handler (and therefore the expand/collapse toggle).
|
|
429
|
+
region.child = b.thinkingPreview(blocks.join("\n"), theme, isStreaming, component, runIndex);
|
|
430
|
+
} else {
|
|
431
|
+
// Markdown reads its (TS-private) theme at render time, and this runs
|
|
432
|
+
// before the first render, so recoloring in place needs no reconstruction.
|
|
433
|
+
inner.theme = b.thinkingMarkdownTheme(component.markdownTheme, theme);
|
|
434
|
+
// Host the header like native thinking does, so an opened run's header
|
|
435
|
+
// can sit above this row too.
|
|
436
|
+
if (id) region.child = foldable(ctx, inner, id);
|
|
445
437
|
}
|
|
438
|
+
}
|
|
446
439
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
440
|
+
/**
|
|
441
|
+
* Leading spacer: hidden only when every row of this message is hidden.
|
|
442
|
+
* Identified by what it DOES (renders exactly one blank line) rather than by
|
|
443
|
+
* class, for the same cross-loading-mode reason as isMouseRegion.
|
|
444
|
+
*/
|
|
445
|
+
function wrapSpacer({ bridge: b, component }: Decorating, children: Component[]): void {
|
|
450
446
|
const first = children[0] as any;
|
|
451
|
-
if (
|
|
447
|
+
if (first && typeof first.setLines === "function") {
|
|
452
448
|
children[0] = {
|
|
453
|
-
render: () => (messageSpacer(component) === false ? [] : [""]),
|
|
449
|
+
render: () => (b.messageSpacer(component) === false ? [] : [""]),
|
|
454
450
|
invalidate: () => {},
|
|
455
451
|
};
|
|
456
452
|
}
|
|
@@ -473,19 +469,9 @@ function visiblyBlank(line: string): boolean {
|
|
|
473
469
|
.trim() === "";
|
|
474
470
|
}
|
|
475
471
|
|
|
476
|
-
function textPhase(signature: unknown): "commentary" | "final_answer" | undefined {
|
|
477
|
-
if (typeof signature !== "string" || !signature.startsWith("{")) return undefined;
|
|
478
|
-
try {
|
|
479
|
-
const phase = JSON.parse(signature).phase;
|
|
480
|
-
return phase === "commentary" || phase === "final_answer" ? phase : undefined;
|
|
481
|
-
} catch {
|
|
482
|
-
return undefined;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
472
|
/** Pi's live Theme instance, shared through globalThis by its theme module. */
|
|
487
|
-
function liveTheme():
|
|
488
|
-
return (globalThis as Globals)[Symbol.for("@earendil-works/pi-coding-agent:theme")];
|
|
473
|
+
function liveTheme(): ThemeLike | undefined {
|
|
474
|
+
return (globalThis as Globals)[Symbol.for("@earendil-works/pi-coding-agent:theme")] as ThemeLike | undefined;
|
|
489
475
|
}
|
|
490
476
|
|
|
491
477
|
/**
|
|
@@ -499,6 +485,7 @@ function liveTheme(): unknown {
|
|
|
499
485
|
export function patchCore(core: {
|
|
500
486
|
ToolExecutionComponent?: new (...args: never[]) => unknown;
|
|
501
487
|
AssistantMessageComponent?: new (...args: never[]) => unknown;
|
|
488
|
+
UserMessageComponent?: new (...args: never[]) => unknown;
|
|
502
489
|
}): void {
|
|
503
490
|
// Pi's `private` members make these classes structurally unassignable to the
|
|
504
491
|
// interfaces above, so cross the boundary once, here, explicitly.
|
|
@@ -507,4 +494,6 @@ export function patchCore(core: {
|
|
|
507
494
|
const assistant = proto(core.AssistantMessageComponent);
|
|
508
495
|
if (tool) patchToolExecution(tool);
|
|
509
496
|
if (assistant) patchAssistantMessage(assistant);
|
|
497
|
+
const user = proto(core.UserMessageComponent);
|
|
498
|
+
if (user) patchUserMessage(user);
|
|
510
499
|
}
|