@hheei/pi-ext-ui 0.5.2 → 0.7.0
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 +2 -2
- package/dist/background.d.ts +11 -0
- package/dist/background.js +31 -0
- package/dist/extension.js +71 -7
- package/dist/host-transcript-patch.d.ts +2 -0
- package/dist/host-transcript-patch.js +258 -181
- package/dist/index.d.ts +6 -4
- package/dist/index.js +5 -3
- package/dist/message-view.d.ts +6 -0
- package/dist/message-view.js +38 -8
- package/dist/native-tools.d.ts +69 -0
- package/dist/native-tools.js +414 -0
- package/dist/nested-calls-view.d.ts +6 -2
- package/dist/nested-calls-view.js +98 -99
- package/dist/payload-parser.d.ts +10 -28
- package/dist/payload-parser.js +9 -269
- package/dist/renderer.d.ts +6 -4
- package/dist/renderer.js +280 -174
- package/dist/runtime.d.ts +13 -0
- package/dist/runtime.js +16 -0
- package/dist/settings.d.ts +3 -0
- package/dist/settings.js +18 -1
- package/dist/tool-group-view.d.ts +89 -0
- package/dist/tool-group-view.js +645 -0
- package/dist/tool-view.d.ts +33 -4
- package/dist/tool-view.js +116 -35
- package/dist/tracker.d.ts +36 -17
- package/dist/tracker.js +157 -19
- package/package.json +2 -2
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { AssistantMessageComponent, InteractiveMode, ToolExecutionComponent, VERSION, } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Container,
|
|
3
|
-
import { fitRow } from "@hheei/pi-ext-core";
|
|
2
|
+
import { Container, stripTerminalSequences, Text } from "@earendil-works/pi-tui";
|
|
4
3
|
import { MessageView } from "./message-view.js";
|
|
5
|
-
import {
|
|
4
|
+
import { memberParts, TailUnit, ToolGroupView, } from "./tool-group-view.js";
|
|
5
|
+
import { ToolView, toSingleRow } from "./tool-view.js";
|
|
6
6
|
const SEAMS = ["getRegisteredToolDefinition", "showExtensionNotify"];
|
|
7
7
|
function record(value) {
|
|
8
8
|
return typeof value === "object" && value !== null;
|
|
@@ -28,34 +28,60 @@ function readHost(value) {
|
|
|
28
28
|
const requestRender = tui.requestRender;
|
|
29
29
|
return { chat, ui, requestRender: () => requestRender.call(tui) };
|
|
30
30
|
}
|
|
31
|
+
function readUsage(value) {
|
|
32
|
+
if (!record(value))
|
|
33
|
+
return undefined;
|
|
34
|
+
const { input, output, cacheRead, cacheWrite } = value;
|
|
35
|
+
if (typeof input !== "number" ||
|
|
36
|
+
typeof output !== "number" ||
|
|
37
|
+
typeof cacheRead !== "number" ||
|
|
38
|
+
typeof cacheWrite !== "number")
|
|
39
|
+
return undefined;
|
|
40
|
+
return { input, output, cacheRead, cacheWrite };
|
|
41
|
+
}
|
|
31
42
|
function assistantInfo(component) {
|
|
32
43
|
const message = Reflect.get(component, "lastMessage");
|
|
33
44
|
if (!record(message) || !Array.isArray(message.content)) {
|
|
34
45
|
throw new Error("Pi assistant message shape changed");
|
|
35
46
|
}
|
|
47
|
+
const hidden = Reflect.get(component, "hideThinkingBlock");
|
|
36
48
|
const calls = new Set();
|
|
37
49
|
let eligible = !["length", "aborted", "error"].includes(String(message.stopReason));
|
|
38
|
-
|
|
50
|
+
const thinking = [];
|
|
39
51
|
for (const block of message.content) {
|
|
40
52
|
if (!record(block))
|
|
41
53
|
throw new Error("Pi assistant content shape changed");
|
|
42
54
|
if (block.type === "toolCall" && typeof block.id === "string")
|
|
43
55
|
calls.add(block.id);
|
|
44
|
-
else if (block.type === "thinking" && typeof block.thinking === "string")
|
|
45
|
-
|
|
56
|
+
else if (block.type === "thinking" && typeof block.thinking === "string") {
|
|
57
|
+
// Pi joins a thinking run with blank lines; the same separator keeps paragraphs readable here.
|
|
58
|
+
const text = block.thinking.trim();
|
|
59
|
+
if (text !== "")
|
|
60
|
+
thinking.push(text);
|
|
61
|
+
}
|
|
46
62
|
else if (block.type !== "text" || typeof block.text !== "string" || block.text.trim() !== "")
|
|
47
63
|
eligible = false;
|
|
48
64
|
}
|
|
49
|
-
return {
|
|
65
|
+
return {
|
|
66
|
+
calls,
|
|
67
|
+
eligible,
|
|
68
|
+
thinking: thinking.join("\n\n"),
|
|
69
|
+
hidden: hidden === true,
|
|
70
|
+
usage: readUsage(message.usage),
|
|
71
|
+
};
|
|
50
72
|
}
|
|
51
73
|
function toolMember(component, calls) {
|
|
52
74
|
const id = Reflect.get(component, "toolCallId");
|
|
53
75
|
const name = Reflect.get(component, "toolName");
|
|
54
76
|
const partial = Reflect.get(component, "isPartial");
|
|
77
|
+
const complete = Reflect.get(component, "argsComplete");
|
|
55
78
|
const result = Reflect.get(component, "result");
|
|
56
79
|
const renderer = Reflect.get(component, "resultRendererComponent");
|
|
57
80
|
const callRenderer = Reflect.get(component, "callRendererComponent");
|
|
58
|
-
if (typeof id !== "string" ||
|
|
81
|
+
if (typeof id !== "string" ||
|
|
82
|
+
typeof name !== "string" ||
|
|
83
|
+
typeof partial !== "boolean" ||
|
|
84
|
+
typeof complete !== "boolean") {
|
|
59
85
|
throw new Error("Pi tool component shape changed");
|
|
60
86
|
}
|
|
61
87
|
if (!calls.has(id) || !(callRenderer instanceof ToolView))
|
|
@@ -66,12 +92,29 @@ function toolMember(component, calls) {
|
|
|
66
92
|
!result.content.every((block) => record(block) && block.type === "text") ||
|
|
67
93
|
!(renderer instanceof ToolView)))
|
|
68
94
|
return undefined;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (summary === undefined)
|
|
95
|
+
// The visible view decides the facts: a hidden call renderer reports no summary of its own.
|
|
96
|
+
const view = renderer instanceof ToolView ? renderer : callRenderer;
|
|
97
|
+
if (view.summary === undefined)
|
|
73
98
|
return undefined;
|
|
74
|
-
|
|
99
|
+
const status = !complete
|
|
100
|
+
? "streaming"
|
|
101
|
+
: partial
|
|
102
|
+
? "running"
|
|
103
|
+
: record(result) && result.isError === true
|
|
104
|
+
? "error"
|
|
105
|
+
: "ok";
|
|
106
|
+
const children = view.children;
|
|
107
|
+
return {
|
|
108
|
+
kind: "tool",
|
|
109
|
+
component,
|
|
110
|
+
title: toSingleRow(stripTerminalSequences(name)),
|
|
111
|
+
status,
|
|
112
|
+
// The host keeps isPartial true until it stores a final result; argsComplete alone is not
|
|
113
|
+
// completion, and restored history never sets it. The result is the observed end fact.
|
|
114
|
+
completed: !partial && result !== undefined,
|
|
115
|
+
...(children === undefined || children.length === 0 ? {} : { children }),
|
|
116
|
+
...(view.childCount === undefined ? {} : { childCount: view.childCount }),
|
|
117
|
+
};
|
|
75
118
|
}
|
|
76
119
|
function messageView(component) {
|
|
77
120
|
if (component instanceof MessageView)
|
|
@@ -84,165 +127,62 @@ function messageView(component) {
|
|
|
84
127
|
return undefined;
|
|
85
128
|
}
|
|
86
129
|
/** Shared transcript notices stay in the group. The frame is the contract, not each custom type. */
|
|
87
|
-
function
|
|
130
|
+
function noticeMember(component) {
|
|
88
131
|
const view = messageView(component);
|
|
89
|
-
|
|
132
|
+
if (view === undefined)
|
|
133
|
+
return undefined;
|
|
134
|
+
return {
|
|
135
|
+
kind: "notice",
|
|
136
|
+
component,
|
|
137
|
+
title: view.kind,
|
|
138
|
+
summary: view.activitySummary,
|
|
139
|
+
// Preview is visible content; only a real body opens. A preview-only notice therefore never
|
|
140
|
+
// grows a second expansion level, and the notice lays out both blocks itself.
|
|
141
|
+
content: (width) => view.previewRows(width),
|
|
142
|
+
...(view.expandable ? { detail: (width) => view.bodyRows(width) } : {}),
|
|
143
|
+
};
|
|
90
144
|
}
|
|
91
|
-
|
|
145
|
+
/**
|
|
146
|
+
* A captured `ui.notify`. It owns its content: one indented, width-wrapped block with the notice's
|
|
147
|
+
* semantic color. The group draws these rows once, so a plain notice has no expansion of its own.
|
|
148
|
+
*/
|
|
149
|
+
class NotifyView {
|
|
92
150
|
#text = new Text("", 1, 0);
|
|
93
|
-
#
|
|
151
|
+
#ui;
|
|
94
152
|
#message;
|
|
95
153
|
#type;
|
|
96
|
-
constructor(message, type,
|
|
97
|
-
|
|
98
|
-
this.#context = context;
|
|
154
|
+
constructor(message, type, ui) {
|
|
155
|
+
this.#ui = ui;
|
|
99
156
|
this.#message = message;
|
|
100
157
|
this.#type = type;
|
|
101
|
-
this.addChild(new Spacer(1));
|
|
102
|
-
this.addChild(this.#text);
|
|
103
158
|
}
|
|
104
|
-
get
|
|
105
|
-
return
|
|
159
|
+
get member() {
|
|
160
|
+
return {
|
|
161
|
+
kind: "notice",
|
|
162
|
+
component: this,
|
|
163
|
+
title: this.#type,
|
|
164
|
+
content: (width) => this.rows(width),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/** The notice body as it appears both in the transcript and inside the activity group. */
|
|
168
|
+
rows(width) {
|
|
169
|
+
// info is the terminal's own readable text; a warning or error keeps its status token and
|
|
170
|
+
// its written prefix, so the level never depends on color alone.
|
|
171
|
+
this.#text.setText(this.#ui.theme.fg(this.#type === "info" ? "text" : this.#type, this.#content));
|
|
172
|
+
return this.#text.render(width);
|
|
106
173
|
}
|
|
107
174
|
get #content() {
|
|
108
175
|
const prefix = this.#type === "error" ? "Error: " : this.#type === "warning" ? "Warning: " : "";
|
|
109
176
|
return `${prefix}${this.#message}`;
|
|
110
177
|
}
|
|
111
178
|
render(width) {
|
|
112
|
-
|
|
113
|
-
return super.render(width);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
class ToolGroup extends Container {
|
|
117
|
-
#header = new Text("", 0, 0);
|
|
118
|
-
#originals = new Container();
|
|
119
|
-
#context;
|
|
120
|
-
#redraw;
|
|
121
|
-
#members = [];
|
|
122
|
-
#expanded = false;
|
|
123
|
-
#renderedExpanded = false;
|
|
124
|
-
#open = new Set();
|
|
125
|
-
#hits = [];
|
|
126
|
-
constructor(context, redraw) {
|
|
127
|
-
super();
|
|
128
|
-
this.#context = context;
|
|
129
|
-
this.#redraw = redraw;
|
|
130
|
-
const heading = new MouseRegion(this.#header, (event) => {
|
|
131
|
-
if (event.type !== "click" || event.button !== "left")
|
|
132
|
-
return undefined;
|
|
133
|
-
this.#expanded = this.#context.ui.getToolsExpanded() ? false : !this.#expanded;
|
|
134
|
-
if (!this.#expanded)
|
|
135
|
-
this.#open.clear();
|
|
136
|
-
redraw();
|
|
137
|
-
return { handled: true, render: true };
|
|
138
|
-
});
|
|
139
|
-
const body = {
|
|
140
|
-
render: (width) => this.#renderBody(width),
|
|
141
|
-
handleMouse: (event) => this.#hit(event),
|
|
142
|
-
invalidate() { },
|
|
143
|
-
};
|
|
144
|
-
this.children = [new Spacer(1), heading, body];
|
|
145
|
-
}
|
|
146
|
-
#renderBody(width) {
|
|
147
|
-
const inner = Math.max(1, width - 2);
|
|
148
|
-
const rail = this.#context.ui.theme.fg("dim", "▎");
|
|
149
|
-
const lines = [];
|
|
150
|
-
const hits = [];
|
|
151
|
-
this.#members.forEach((member, index) => {
|
|
152
|
-
const wrapper = this.#originals.children[index];
|
|
153
|
-
if (wrapper === undefined)
|
|
154
|
-
return;
|
|
155
|
-
const native = this.#renderedExpanded || this.#open.has(member.component);
|
|
156
|
-
const rows = native
|
|
157
|
-
? renderWithoutToolRails(wrapper, inner)
|
|
158
|
-
: member.summary === undefined
|
|
159
|
-
? []
|
|
160
|
-
: [member.summary];
|
|
161
|
-
if (rows.length === 0)
|
|
162
|
-
return;
|
|
163
|
-
hits.push({ component: member.component, wrapper, native, height: rows.length });
|
|
164
|
-
for (const row of rows) {
|
|
165
|
-
lines.push(fitRow(`${rail} ${row}`, Math.max(0, width), "…"));
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
this.#hits = hits;
|
|
169
|
-
return lines;
|
|
170
|
-
}
|
|
171
|
-
#hit(event) {
|
|
172
|
-
let y = event.y;
|
|
173
|
-
for (const hit of this.#hits) {
|
|
174
|
-
if (y >= hit.height) {
|
|
175
|
-
y -= hit.height;
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
if (!hit.native) {
|
|
179
|
-
if (event.type !== "click" || event.button !== "left")
|
|
180
|
-
return undefined;
|
|
181
|
-
this.#open.add(hit.component);
|
|
182
|
-
this.#redraw();
|
|
183
|
-
return { handled: true, render: true };
|
|
184
|
-
}
|
|
185
|
-
// The rail belongs to the group. Content, including header fold and copy, stays with the member.
|
|
186
|
-
if (event.x < 2) {
|
|
187
|
-
if (!this.#renderedExpanded && event.type === "click" && event.button === "left") {
|
|
188
|
-
this.#open.delete(hit.component);
|
|
189
|
-
this.#redraw();
|
|
190
|
-
return { handled: true, render: true };
|
|
191
|
-
}
|
|
192
|
-
return undefined;
|
|
193
|
-
}
|
|
194
|
-
return hit.wrapper.handleMouse?.({
|
|
195
|
-
...event,
|
|
196
|
-
y,
|
|
197
|
-
height: hit.height,
|
|
198
|
-
x: event.x - 2,
|
|
199
|
-
width: Math.max(1, event.width - 2),
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
return undefined;
|
|
203
|
-
}
|
|
204
|
-
update(members) {
|
|
205
|
-
this.#members = members;
|
|
206
|
-
this.#originals.children = members.map(({ component }) => {
|
|
207
|
-
let offset = 0;
|
|
208
|
-
return {
|
|
209
|
-
render(width) {
|
|
210
|
-
const rows = component.render(width);
|
|
211
|
-
// Pi adds one leading separator per tool; the group supplies its own spacing.
|
|
212
|
-
offset = rows[0] === "" ? 1 : 0;
|
|
213
|
-
return rows.slice(offset);
|
|
214
|
-
},
|
|
215
|
-
handleMouse(event) {
|
|
216
|
-
return component.handleMouse?.({
|
|
217
|
-
...event,
|
|
218
|
-
y: event.y + offset,
|
|
219
|
-
height: event.height + offset,
|
|
220
|
-
});
|
|
221
|
-
},
|
|
222
|
-
invalidate() { },
|
|
223
|
-
};
|
|
224
|
-
});
|
|
179
|
+
return ["", ...this.rows(width)];
|
|
225
180
|
}
|
|
226
181
|
invalidate() {
|
|
227
|
-
|
|
228
|
-
this.#header.invalidate();
|
|
229
|
-
}
|
|
230
|
-
render(width) {
|
|
231
|
-
this.#renderedExpanded = this.#expanded || this.#context.ui.getToolsExpanded();
|
|
232
|
-
const theme = this.#context.ui.theme;
|
|
233
|
-
const counts = new Map();
|
|
234
|
-
for (const member of this.#members) {
|
|
235
|
-
if (member.name !== undefined)
|
|
236
|
-
counts.set(member.name, (counts.get(member.name) ?? 0) + 1);
|
|
237
|
-
}
|
|
238
|
-
const heading = [...counts]
|
|
239
|
-
.map(([name, count]) => `${theme.fg("toolTitle", theme.bold(name))} ${theme.fg("dim", `×${count}`)}`)
|
|
240
|
-
.join(theme.fg("dim", " · "));
|
|
241
|
-
this.#header.setText(fitRow(`${theme.fg("dim", this.#renderedExpanded ? "▾" : "▸")} ${heading}`, Math.max(0, width), "…"));
|
|
242
|
-
return super.render(width);
|
|
182
|
+
this.#text.invalidate();
|
|
243
183
|
}
|
|
244
184
|
}
|
|
245
|
-
function attach(host,
|
|
185
|
+
function attach(host, run) {
|
|
246
186
|
const chat = host.chat;
|
|
247
187
|
const original = {
|
|
248
188
|
render: chat.render,
|
|
@@ -261,32 +201,134 @@ function attach(host, context) {
|
|
|
261
201
|
const projection = new Container();
|
|
262
202
|
let groups = new Map();
|
|
263
203
|
let active = true;
|
|
204
|
+
const disposeGroups = () => {
|
|
205
|
+
for (const group of groups.values())
|
|
206
|
+
group.dispose();
|
|
207
|
+
groups.clear();
|
|
208
|
+
};
|
|
209
|
+
/**
|
|
210
|
+
* Record first observation time for new components during a live loop. Restored history stays
|
|
211
|
+
* unknown until live content joins its block.
|
|
212
|
+
*/
|
|
213
|
+
const claim = (child) => {
|
|
214
|
+
if (run.observed.has(child))
|
|
215
|
+
return;
|
|
216
|
+
const current = run.current;
|
|
217
|
+
run.observed.set(child, current !== undefined && current.run.endedAt === undefined
|
|
218
|
+
? { at: performance.now(), run: current.run }
|
|
219
|
+
: undefined);
|
|
220
|
+
};
|
|
221
|
+
const observe = () => {
|
|
222
|
+
for (const child of chat.children)
|
|
223
|
+
claim(child);
|
|
224
|
+
};
|
|
225
|
+
/** Create or recover one clock for each live continuous block. */
|
|
226
|
+
const clockOf = (members, endedAt) => {
|
|
227
|
+
const parts = members.flatMap(memberParts);
|
|
228
|
+
let clock;
|
|
229
|
+
for (const part of parts) {
|
|
230
|
+
clock = run.clocks.get(part);
|
|
231
|
+
if (clock !== undefined)
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
if (clock === undefined) {
|
|
235
|
+
const origin = parts
|
|
236
|
+
.map((part) => run.observed.get(part))
|
|
237
|
+
.find((value) => value !== undefined);
|
|
238
|
+
if (origin === undefined)
|
|
239
|
+
return undefined;
|
|
240
|
+
clock = { startedAt: origin.at };
|
|
241
|
+
if (origin.run.endedAt !== undefined)
|
|
242
|
+
clock.endedAt = origin.run.endedAt;
|
|
243
|
+
else
|
|
244
|
+
run.activeClocks.add(clock);
|
|
245
|
+
}
|
|
246
|
+
if (endedAt !== undefined && clock.endedAt === undefined) {
|
|
247
|
+
clock.endedAt = endedAt;
|
|
248
|
+
run.activeClocks.delete(clock);
|
|
249
|
+
}
|
|
250
|
+
for (const part of parts)
|
|
251
|
+
if (run.clocks.get(part) === undefined)
|
|
252
|
+
run.clocks.set(part, clock);
|
|
253
|
+
return clock;
|
|
254
|
+
};
|
|
264
255
|
const rebuild = () => {
|
|
265
256
|
const next = new Map();
|
|
266
257
|
const children = [];
|
|
258
|
+
const blocks = [];
|
|
267
259
|
let calls = new Set();
|
|
268
260
|
let members = [];
|
|
269
|
-
|
|
261
|
+
/** Every request this pending block owns, in transcript order, whether or not it is displayed. */
|
|
262
|
+
let requests = [];
|
|
263
|
+
/**
|
|
264
|
+
* Closes the pending block. `ended` is false only for the block still open at the transcript tail:
|
|
265
|
+
* that one keeps its last visible unit outside the group, and its facts still count here.
|
|
266
|
+
*/
|
|
267
|
+
const flush = (ended, boundary) => {
|
|
270
268
|
const first = members[0]?.component;
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
269
|
+
let weight = 0;
|
|
270
|
+
for (const member of members) {
|
|
271
|
+
if (member.kind === "tool")
|
|
272
|
+
weight += 1;
|
|
273
|
+
else if (member.kind === "notice" ||
|
|
274
|
+
(member.thinking !== undefined && member.thinking !== ""))
|
|
275
|
+
weight += 0.5;
|
|
276
|
+
}
|
|
277
|
+
if (first !== undefined && weight >= 2) {
|
|
278
|
+
const group = groups.get(first) ?? new ToolGroupView(host.ui, host.requestRender);
|
|
279
|
+
const tail = ended ? undefined : members.at(-1);
|
|
280
|
+
const inside = tail === undefined ? members : members.slice(0, -1);
|
|
281
|
+
const boundaryTime = ended && boundary !== undefined
|
|
282
|
+
? (run.observed.get(boundary)?.at ?? performance.now())
|
|
283
|
+
: undefined;
|
|
284
|
+
blocks.push({
|
|
285
|
+
group,
|
|
286
|
+
members: inside,
|
|
287
|
+
tail,
|
|
288
|
+
requests,
|
|
289
|
+
clock: clockOf(members, boundaryTime),
|
|
290
|
+
});
|
|
274
291
|
next.set(first, group);
|
|
275
292
|
children.push(group);
|
|
293
|
+
if (tail !== undefined)
|
|
294
|
+
children.push(new TailUnit(tail));
|
|
276
295
|
}
|
|
277
296
|
else
|
|
278
|
-
children.push(...members.
|
|
297
|
+
children.push(...members.flatMap(memberParts));
|
|
279
298
|
members = [];
|
|
299
|
+
requests = [];
|
|
280
300
|
};
|
|
281
301
|
for (const child of chat.children) {
|
|
302
|
+
claim(child);
|
|
282
303
|
if (child instanceof AssistantMessageComponent) {
|
|
283
304
|
const info = assistantInfo(child);
|
|
284
305
|
calls = info.calls;
|
|
306
|
+
// The request belongs to the block whose work it answers, even when its text is native
|
|
307
|
+
// content the aggregate never shows.
|
|
308
|
+
requests.push(info.usage === undefined ? {} : { usage: info.usage });
|
|
285
309
|
if (info.eligible) {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
310
|
+
const thinking = info.thinking;
|
|
311
|
+
const previous = members.at(-1);
|
|
312
|
+
if (thinking !== "" && previous?.kind === "thinking") {
|
|
313
|
+
// One continuous run: consecutive thinking blocks are one item, paragraphs kept.
|
|
314
|
+
members[members.length - 1] = {
|
|
315
|
+
...previous,
|
|
316
|
+
thinking: `${previous.thinking ?? ""}\n\n${thinking}`,
|
|
317
|
+
parts: [...memberParts(previous), child],
|
|
318
|
+
...(previous.thinkingHidden === true || info.hidden ? { thinkingHidden: true } : {}),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
members.push({
|
|
323
|
+
kind: "thinking",
|
|
324
|
+
component: child,
|
|
325
|
+
// The host's hide setting is a display preference, not a content filter: the text
|
|
326
|
+
// stays available to the group's preview and expansion, and is never written back.
|
|
327
|
+
...(thinking === ""
|
|
328
|
+
? {}
|
|
329
|
+
: { thinking, ...(info.hidden ? { thinkingHidden: true } : {}) }),
|
|
330
|
+
});
|
|
331
|
+
}
|
|
290
332
|
continue;
|
|
291
333
|
}
|
|
292
334
|
}
|
|
@@ -298,20 +340,28 @@ function attach(host, context) {
|
|
|
298
340
|
}
|
|
299
341
|
}
|
|
300
342
|
else if (child instanceof NotifyView) {
|
|
301
|
-
members.push(
|
|
343
|
+
members.push(child.member);
|
|
302
344
|
continue;
|
|
303
345
|
}
|
|
304
346
|
else {
|
|
305
|
-
const continuation =
|
|
347
|
+
const continuation = noticeMember(child);
|
|
306
348
|
if (continuation !== undefined) {
|
|
307
349
|
members.push(continuation);
|
|
308
350
|
continue;
|
|
309
351
|
}
|
|
310
352
|
}
|
|
311
|
-
flush();
|
|
353
|
+
flush(true, child);
|
|
312
354
|
children.push(child);
|
|
313
355
|
}
|
|
314
|
-
flush();
|
|
356
|
+
flush(false);
|
|
357
|
+
for (const block of blocks) {
|
|
358
|
+
block.group.update(block.members, block.tail, block.requests, block.clock);
|
|
359
|
+
}
|
|
360
|
+
// A block the new projection does not reuse is gone: its hold timer must go with it.
|
|
361
|
+
for (const [key, group] of groups) {
|
|
362
|
+
if (next.get(key) !== group)
|
|
363
|
+
group.dispose();
|
|
364
|
+
}
|
|
315
365
|
groups = next;
|
|
316
366
|
projection.children = children;
|
|
317
367
|
};
|
|
@@ -324,9 +374,9 @@ function attach(host, context) {
|
|
|
324
374
|
}
|
|
325
375
|
catch (error) {
|
|
326
376
|
active = false;
|
|
327
|
-
|
|
377
|
+
disposeGroups();
|
|
328
378
|
projection.clear();
|
|
329
|
-
|
|
379
|
+
host.ui.notify(`Tool grouping disabled: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
330
380
|
return original.render.call(chat, width);
|
|
331
381
|
}
|
|
332
382
|
// Real renderer errors propagate; never hide a tool-rendering failure.
|
|
@@ -348,16 +398,17 @@ function attach(host, context) {
|
|
|
348
398
|
notify(message, type) {
|
|
349
399
|
if (!active)
|
|
350
400
|
return false;
|
|
351
|
-
chat.addChild(new NotifyView(message, type,
|
|
401
|
+
chat.addChild(new NotifyView(message, type, host.ui));
|
|
352
402
|
host.requestRender();
|
|
353
403
|
return true;
|
|
354
404
|
},
|
|
405
|
+
observe,
|
|
355
406
|
dispose() {
|
|
356
407
|
if (disposed)
|
|
357
408
|
return;
|
|
358
409
|
disposed = true;
|
|
359
410
|
active = false;
|
|
360
|
-
|
|
411
|
+
disposeGroups();
|
|
361
412
|
projection.clear();
|
|
362
413
|
let conflict = false;
|
|
363
414
|
for (const key of ["render", "handleMouse", "invalidate"]) {
|
|
@@ -372,7 +423,7 @@ function attach(host, context) {
|
|
|
372
423
|
Object.defineProperty(chat, key, descriptor);
|
|
373
424
|
}
|
|
374
425
|
if (conflict)
|
|
375
|
-
|
|
426
|
+
host.ui.notify("Tool grouping patch changed by another extension; restart Pi to fully unload it.", "warning");
|
|
376
427
|
host.requestRender();
|
|
377
428
|
},
|
|
378
429
|
};
|
|
@@ -385,7 +436,7 @@ function connect(owner, host) {
|
|
|
385
436
|
}
|
|
386
437
|
catch (error) {
|
|
387
438
|
owner.failed.add(host.chat);
|
|
388
|
-
owner.
|
|
439
|
+
owner.ui.notify(`Tool grouping unavailable: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
389
440
|
}
|
|
390
441
|
}
|
|
391
442
|
function patchState() {
|
|
@@ -426,7 +477,7 @@ function patchState() {
|
|
|
426
477
|
if (record(instance))
|
|
427
478
|
failed.add(instance);
|
|
428
479
|
for (const owner of owners.values())
|
|
429
|
-
owner.
|
|
480
|
+
owner.ui.notify(`Tool grouping unavailable: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
430
481
|
return undefined;
|
|
431
482
|
}
|
|
432
483
|
if (record(instance) && !seen.has(instance)) {
|
|
@@ -469,7 +520,8 @@ export function installHostToolGroups(context) {
|
|
|
469
520
|
!(major > 1 || (major === 1 && (minor > 0 || (minor === 0 && patch >= 1)))))
|
|
470
521
|
throw new Error(`Tool grouping requires Pi >=1.0.1; running ${VERSION}`);
|
|
471
522
|
const state = patchState();
|
|
472
|
-
|
|
523
|
+
const ui = context.ui;
|
|
524
|
+
if (state.owners.has(ui))
|
|
473
525
|
throw new Error("Tool grouping already owns this UI runtime");
|
|
474
526
|
if (state.owners.size === 0) {
|
|
475
527
|
for (const seam of SEAMS) {
|
|
@@ -483,13 +535,18 @@ export function installHostToolGroups(context) {
|
|
|
483
535
|
value: state.wrappers[seam],
|
|
484
536
|
});
|
|
485
537
|
}
|
|
538
|
+
const run = {
|
|
539
|
+
observed: new WeakMap(),
|
|
540
|
+
clocks: new WeakMap(),
|
|
541
|
+
activeClocks: new Set(),
|
|
542
|
+
};
|
|
486
543
|
const owner = {
|
|
487
|
-
|
|
544
|
+
ui,
|
|
488
545
|
hosts: new Map(),
|
|
489
546
|
failed: new WeakSet(),
|
|
490
|
-
attach: (host) => attach(host,
|
|
547
|
+
attach: (host) => attach(host, run),
|
|
491
548
|
};
|
|
492
|
-
state.owners.set(
|
|
549
|
+
state.owners.set(ui, owner);
|
|
493
550
|
// Reload rebuilds history before session_start; reconnect already discovered instances.
|
|
494
551
|
for (const reference of state.hosts) {
|
|
495
552
|
const instance = reference.deref();
|
|
@@ -499,7 +556,7 @@ export function installHostToolGroups(context) {
|
|
|
499
556
|
}
|
|
500
557
|
try {
|
|
501
558
|
const host = readHost(instance);
|
|
502
|
-
if (host.ui ===
|
|
559
|
+
if (host.ui === ui)
|
|
503
560
|
connect(owner, host);
|
|
504
561
|
}
|
|
505
562
|
catch {
|
|
@@ -512,11 +569,31 @@ export function installHostToolGroups(context) {
|
|
|
512
569
|
for (const host of owner.hosts.values())
|
|
513
570
|
host.requestRender();
|
|
514
571
|
},
|
|
572
|
+
beginRun() {
|
|
573
|
+
// History may not have rendered yet. Claim it before opening the new live run.
|
|
574
|
+
for (const host of owner.hosts.values())
|
|
575
|
+
host.observe();
|
|
576
|
+
run.current = { run: {} };
|
|
577
|
+
},
|
|
578
|
+
endRun() {
|
|
579
|
+
// Mark unseen transcript children before ending the run so later renders treat them as history.
|
|
580
|
+
for (const host of owner.hosts.values())
|
|
581
|
+
host.observe();
|
|
582
|
+
const endedAt = performance.now();
|
|
583
|
+
const current = run.current;
|
|
584
|
+
if (current !== undefined && current.run.endedAt === undefined)
|
|
585
|
+
current.run.endedAt = endedAt;
|
|
586
|
+
for (const clock of run.activeClocks)
|
|
587
|
+
clock.endedAt = endedAt;
|
|
588
|
+
run.activeClocks.clear();
|
|
589
|
+
},
|
|
515
590
|
dispose() {
|
|
516
591
|
if (disposed)
|
|
517
592
|
return;
|
|
518
593
|
disposed = true;
|
|
519
|
-
|
|
594
|
+
run.current = undefined;
|
|
595
|
+
run.activeClocks.clear();
|
|
596
|
+
state.owners.delete(ui);
|
|
520
597
|
for (const host of owner.hosts.values())
|
|
521
598
|
host.dispose();
|
|
522
599
|
owner.hosts.clear();
|
|
@@ -527,7 +604,7 @@ export function installHostToolGroups(context) {
|
|
|
527
604
|
state.wrappers[seam])
|
|
528
605
|
Object.defineProperty(InteractiveMode.prototype, seam, state.originals[seam]);
|
|
529
606
|
else
|
|
530
|
-
|
|
607
|
+
ui.notify("Tool grouping seam changed by another extension; restart Pi to fully unload it.", "warning");
|
|
531
608
|
}
|
|
532
609
|
},
|
|
533
610
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { type DiffHunk, type DiffLine, type DiffSource, type DiffStats, DiffView, type DiffViewOptions, diffStats, } from "./diff-view.js";
|
|
2
2
|
export { createMessageView, type MessageRows, type MessageTone, MessageView, type MessageViewHeader, type MessageViewSpec, } from "./message-view.js";
|
|
3
|
-
export { extractChildToolCopyText,
|
|
4
|
-
export {
|
|
5
|
-
export { type
|
|
6
|
-
export {
|
|
3
|
+
export { extractChildToolCopyText, inlinePresentation } from "./native-tools.js";
|
|
4
|
+
export { NestedCallsView, type NestedCallsViewProps, } from "./nested-calls-view.js";
|
|
5
|
+
export { type CollapsibleToolRendererOptions, createCollapsibleToolRendererResolver, estimateStreamingTokens, isMcpTool, type NestedCallRecord, registerCollapsibleToolRenderer, SPECIALIZED_TOOL_RENDERERS, type SpecializedToolRendererName, type ToolRenderContext, } from "./renderer.js";
|
|
6
|
+
export { ToolUiRuntime } from "./runtime.js";
|
|
7
|
+
export { type Collapsible, createToolView, type FoldLevel, formatStatusGlyph, formatToolHeaderText, isCollapsible, type RailColorToken, type RailSection, type ToolStatusKind, ToolView, type ToolViewChild, type ToolViewCopySpec, type ToolViewHeaderSpec, type ToolViewSpec, transitionFoldLevel, } from "./tool-view.js";
|
|
8
|
+
export { type CapturedToolCall, type NestedCallArgsFact, NestedCallArgsIndex, ToolExecutionTracker, } from "./tracker.js";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { DiffView, diffStats, } from "./diff-view.js";
|
|
2
2
|
export { createMessageView, MessageView, } from "./message-view.js";
|
|
3
|
-
export { extractChildToolCopyText,
|
|
4
|
-
export {
|
|
3
|
+
export { extractChildToolCopyText, inlinePresentation } from "./native-tools.js";
|
|
4
|
+
export { NestedCallsView, } from "./nested-calls-view.js";
|
|
5
|
+
export { createCollapsibleToolRendererResolver, estimateStreamingTokens, isMcpTool, registerCollapsibleToolRenderer, SPECIALIZED_TOOL_RENDERERS, } from "./renderer.js";
|
|
6
|
+
export { ToolUiRuntime } from "./runtime.js";
|
|
5
7
|
export { createToolView, formatStatusGlyph, formatToolHeaderText, isCollapsible, ToolView, transitionFoldLevel, } from "./tool-view.js";
|
|
6
|
-
export {
|
|
8
|
+
export { NestedCallArgsIndex, ToolExecutionTracker, } from "./tracker.js";
|
package/dist/message-view.d.ts
CHANGED
|
@@ -12,6 +12,12 @@ export declare class MessageView implements Component {
|
|
|
12
12
|
render(width: number): string[];
|
|
13
13
|
invalidate(): void;
|
|
14
14
|
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined;
|
|
15
|
+
/** Provider-declared notice type, used to group notices without reading their text. */
|
|
16
|
+
get kind(): string;
|
|
17
|
+
/** Preview rows inside an activity group: visible content below the summary, folded or not. */
|
|
18
|
+
previewRows(width: number): string[];
|
|
19
|
+
/** Body rows inside an activity group: what opening this notice reveals. */
|
|
20
|
+
bodyRows(width: number): string[];
|
|
15
21
|
/** Single-row label used when this notice sits inside an activity group. */
|
|
16
22
|
get activitySummary(): string;
|
|
17
23
|
}
|