@hheei/pi-ext-ui 0.5.1 → 0.6.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 +1 -1
- package/dist/extension.js +13 -8
- package/dist/host-transcript-patch.d.ts +2 -0
- package/dist/host-transcript-patch.js +144 -113
- package/dist/index.d.ts +1 -1
- package/dist/message-view.d.ts +14 -0
- package/dist/message-view.js +29 -9
- package/dist/nested-calls-view.js +41 -27
- package/dist/payload-parser.js +5 -5
- package/dist/renderer.js +45 -0
- package/dist/tool-group-view.d.ts +45 -0
- package/dist/tool-group-view.js +458 -0
- package/dist/tool-view.d.ts +15 -0
- package/dist/tool-view.js +39 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ The shared view API owns fold levels, compact sections, status glyphs, copy feed
|
|
|
8
8
|
|
|
9
9
|
Background Bash and subagent tools own their display formats through ext-core `ToolPresentation` providers. This resolver consumes those providers automatically, including nested codemode calls, and adds shared folding and copy controls. Without `pi-ext-ui`, the same tools retain native Pi renderers. No direct dependency on `pi-ext-ui` is required by the tool extensions.
|
|
10
10
|
|
|
11
|
-
Activity groups are on by default for Pi **>=1.0.1** TUI sessions. Turn them off with the Tool UI setting `toolGroups.enabled` in `ext_settings.json` (the ext-core settings page). The change applies to the current session. A heading such as `▸ codemode ×2 · grep ×3` keeps one live summary row per tool beneath a single outer rail. Each thinking round and notification also keeps a summary row; notifications are not counted. Groups form while tools run and can span consecutive tool/thinking rounds. Prose, user messages, unknown components, images, and unknown tool renderers break groups. Streaming arguments, partial results, completion, and errors update the existing row.
|
|
11
|
+
Activity groups are on by default for Pi **>=1.0.1** TUI sessions. Turn them off with the Tool UI setting `toolGroups.enabled` in `ext_settings.json` (the ext-core settings page). The change applies to the current session. A heading such as `▸ codemode ×2 · grep ×3` keeps one live summary row per tool beneath a single outer rail. Each thinking round and notification also keeps a summary row; notifications are not counted. Groups form while tools run and can span consecutive tool/thinking rounds. Prose, user messages, unknown components, images, and unknown tool renderers break groups. Any MessageView transcript notice stays inside the group, so bg-bash, subagent, memory, optimizer, and codemode-guard notices do not need their own allowlist. Notification rows show their text directly. Streaming arguments, partial results, completion, and errors update the existing row.
|
|
12
12
|
|
|
13
13
|
Fullscreen: click the group heading to restore the original component layouts and interactions beneath the outer rail. Both terminal modes: Ctrl+O forces groups open; closing global expansion restores each group’s local choice. Appending members preserves that choice. Original per-tool fold choices remain intact. Groups are presentation only and are rebuilt on history navigation; session data, model context, and HTML export remain unchanged. Thinking summaries keep a label; expanding respects native thinking visibility. Each `ctx.ui.notify` call retains its full text for expansion. Notifications remain session-local, as in Pi.
|
|
14
14
|
|
package/dist/extension.js
CHANGED
|
@@ -4,13 +4,6 @@ import { createMessageView } from "./message-view.js";
|
|
|
4
4
|
import { createOptionalToolRendererFactory, registerCollapsibleToolRenderer } from "./renderer.js";
|
|
5
5
|
import { activityGroupsEnabled, createToolGroupSettings } from "./settings.js";
|
|
6
6
|
import { clearCapturedNestedCalls } from "./tracker.js";
|
|
7
|
-
function sessionId(extension) {
|
|
8
|
-
const manager = extension.sessionManager;
|
|
9
|
-
if (typeof manager !== "object" || manager === null || !("getSessionId" in manager))
|
|
10
|
-
return "";
|
|
11
|
-
const read = manager.getSessionId;
|
|
12
|
-
return typeof read === "function" ? String(read.call(manager)) : "";
|
|
13
|
-
}
|
|
14
7
|
export default function (pi) {
|
|
15
8
|
let redrawGroups;
|
|
16
9
|
pi.on("agent_settled", () => redrawGroups?.());
|
|
@@ -39,6 +32,18 @@ export default function (pi) {
|
|
|
39
32
|
}
|
|
40
33
|
};
|
|
41
34
|
const provider = createToolGroupSettings({ onEnabled: apply });
|
|
35
|
+
const onAgentStart = pi.on("agent_start", () => {
|
|
36
|
+
patch?.beginRun();
|
|
37
|
+
redrawGroups?.();
|
|
38
|
+
});
|
|
39
|
+
const onAgentEnd = pi.on("agent_end", () => {
|
|
40
|
+
patch?.endRun();
|
|
41
|
+
redrawGroups?.();
|
|
42
|
+
});
|
|
43
|
+
context.resources.add("run-clock", () => {
|
|
44
|
+
onAgentStart();
|
|
45
|
+
onAgentEnd();
|
|
46
|
+
});
|
|
42
47
|
context.resources.add("tool-group-settings", registerSettings(provider, getRuntimeSettingsRegistry(pi)));
|
|
43
48
|
context.resources.add("tool-groups", () => {
|
|
44
49
|
redrawGroups = undefined;
|
|
@@ -54,7 +59,7 @@ export default function (pi) {
|
|
|
54
59
|
context.resources.add("message-view", () => handle.dispose());
|
|
55
60
|
try {
|
|
56
61
|
const state = await provider.storage.load({
|
|
57
|
-
sessionId:
|
|
62
|
+
sessionId: context.extension.sessionManager.getSessionId(),
|
|
58
63
|
signal: context.signal,
|
|
59
64
|
});
|
|
60
65
|
if (!context.signal.aborted)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { AssistantMessageComponent, InteractiveMode, ToolExecutionComponent, VERSION, } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Container,
|
|
3
|
-
import {
|
|
2
|
+
import { Container, Spacer, stripTerminalSequences, Text, wrapTextWithAnsi, } from "@earendil-works/pi-tui";
|
|
3
|
+
import { MessageView } from "./message-view.js";
|
|
4
|
+
import { ToolGroupView } from "./tool-group-view.js";
|
|
5
|
+
import { ToolView, toSingleRow } from "./tool-view.js";
|
|
4
6
|
const SEAMS = ["getRegisteredToolDefinition", "showExtensionNotify"];
|
|
5
7
|
function record(value) {
|
|
6
8
|
return typeof value === "object" && value !== null;
|
|
@@ -26,34 +28,56 @@ function readHost(value) {
|
|
|
26
28
|
const requestRender = tui.requestRender;
|
|
27
29
|
return { chat, ui, requestRender: () => requestRender.call(tui) };
|
|
28
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
|
+
}
|
|
29
42
|
function assistantInfo(component) {
|
|
30
43
|
const message = Reflect.get(component, "lastMessage");
|
|
31
44
|
if (!record(message) || !Array.isArray(message.content)) {
|
|
32
45
|
throw new Error("Pi assistant message shape changed");
|
|
33
46
|
}
|
|
47
|
+
const hidden = Reflect.get(component, "hideThinkingBlock");
|
|
34
48
|
const calls = new Set();
|
|
35
49
|
let eligible = !["length", "aborted", "error"].includes(String(message.stopReason));
|
|
36
|
-
let thinking =
|
|
50
|
+
let thinking = "";
|
|
37
51
|
for (const block of message.content) {
|
|
38
52
|
if (!record(block))
|
|
39
53
|
throw new Error("Pi assistant content shape changed");
|
|
40
54
|
if (block.type === "toolCall" && typeof block.id === "string")
|
|
41
55
|
calls.add(block.id);
|
|
42
56
|
else if (block.type === "thinking" && typeof block.thinking === "string")
|
|
43
|
-
thinking
|
|
57
|
+
thinking += block.thinking;
|
|
44
58
|
else if (block.type !== "text" || typeof block.text !== "string" || block.text.trim() !== "")
|
|
45
59
|
eligible = false;
|
|
46
60
|
}
|
|
47
|
-
return {
|
|
61
|
+
return {
|
|
62
|
+
calls,
|
|
63
|
+
eligible,
|
|
64
|
+
thinking,
|
|
65
|
+
hidden: hidden === true,
|
|
66
|
+
usage: readUsage(message.usage),
|
|
67
|
+
};
|
|
48
68
|
}
|
|
49
69
|
function toolMember(component, calls) {
|
|
50
70
|
const id = Reflect.get(component, "toolCallId");
|
|
51
71
|
const name = Reflect.get(component, "toolName");
|
|
52
72
|
const partial = Reflect.get(component, "isPartial");
|
|
73
|
+
const complete = Reflect.get(component, "argsComplete");
|
|
53
74
|
const result = Reflect.get(component, "result");
|
|
54
75
|
const renderer = Reflect.get(component, "resultRendererComponent");
|
|
55
76
|
const callRenderer = Reflect.get(component, "callRendererComponent");
|
|
56
|
-
if (typeof id !== "string" ||
|
|
77
|
+
if (typeof id !== "string" ||
|
|
78
|
+
typeof name !== "string" ||
|
|
79
|
+
typeof partial !== "boolean" ||
|
|
80
|
+
typeof complete !== "boolean") {
|
|
57
81
|
throw new Error("Pi tool component shape changed");
|
|
58
82
|
}
|
|
59
83
|
if (!calls.has(id) || !(callRenderer instanceof ToolView))
|
|
@@ -69,115 +93,92 @@ function toolMember(component, calls) {
|
|
|
69
93
|
: callRenderer.summary;
|
|
70
94
|
if (summary === undefined)
|
|
71
95
|
return undefined;
|
|
72
|
-
|
|
96
|
+
const status = !complete
|
|
97
|
+
? "streaming"
|
|
98
|
+
: partial
|
|
99
|
+
? "running"
|
|
100
|
+
: record(result) && result.isError === true
|
|
101
|
+
? "error"
|
|
102
|
+
: "ok";
|
|
103
|
+
const children = renderer instanceof ToolView ? renderer.children : callRenderer.children;
|
|
104
|
+
const childCount = renderer instanceof ToolView ? renderer.childCount : undefined;
|
|
105
|
+
return {
|
|
106
|
+
kind: "tool",
|
|
107
|
+
component,
|
|
108
|
+
title: toSingleRow(stripTerminalSequences(name)),
|
|
109
|
+
summary,
|
|
110
|
+
status,
|
|
111
|
+
...(children === undefined || children.length === 0 ? {} : { children }),
|
|
112
|
+
...(childCount === undefined ? {} : { childCount }),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function messageView(component) {
|
|
116
|
+
if (component instanceof MessageView)
|
|
117
|
+
return component;
|
|
118
|
+
if (!(component instanceof Container))
|
|
119
|
+
return undefined;
|
|
120
|
+
for (const child of component.children)
|
|
121
|
+
if (child instanceof MessageView)
|
|
122
|
+
return child;
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
/** Shared transcript notices stay in the group. The frame is the contract, not each custom type. */
|
|
126
|
+
function noticeMember(component) {
|
|
127
|
+
const view = messageView(component);
|
|
128
|
+
if (view === undefined)
|
|
129
|
+
return undefined;
|
|
130
|
+
return {
|
|
131
|
+
kind: "notice",
|
|
132
|
+
component,
|
|
133
|
+
title: view.kind,
|
|
134
|
+
summary: view.activitySummary,
|
|
135
|
+
// A notice with no preview and no body has nothing to open; the group draws a pin only when
|
|
136
|
+
// detail exists, so an unconditional function would give it an arrow onto an empty body.
|
|
137
|
+
...(view.hasDetail ? { detail: (width) => view.detailRows(width) } : {}),
|
|
138
|
+
};
|
|
73
139
|
}
|
|
74
140
|
class NotifyView extends Container {
|
|
75
141
|
#text = new Text("", 1, 0);
|
|
76
|
-
#
|
|
142
|
+
#ui;
|
|
77
143
|
#message;
|
|
78
144
|
#type;
|
|
79
|
-
constructor(message, type,
|
|
145
|
+
constructor(message, type, ui) {
|
|
80
146
|
super();
|
|
81
|
-
this.#
|
|
147
|
+
this.#ui = ui;
|
|
82
148
|
this.#message = message;
|
|
83
149
|
this.#type = type;
|
|
84
150
|
this.addChild(new Spacer(1));
|
|
85
151
|
this.addChild(this.#text);
|
|
86
152
|
}
|
|
87
153
|
get summary() {
|
|
88
|
-
return this.#
|
|
154
|
+
return this.#ui.theme.fg(this.#type === "info" ? "dim" : this.#type, toSingleRow(this.#content));
|
|
89
155
|
}
|
|
90
|
-
get
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
class ToolGroup extends Container {
|
|
100
|
-
#header = new Text("", 0, 0);
|
|
101
|
-
#originals = new Container();
|
|
102
|
-
#context;
|
|
103
|
-
#members = [];
|
|
104
|
-
#expanded = false;
|
|
105
|
-
#renderedExpanded = false;
|
|
106
|
-
constructor(context, redraw) {
|
|
107
|
-
super();
|
|
108
|
-
this.#context = context;
|
|
109
|
-
const heading = new MouseRegion(this.#header, (event) => {
|
|
110
|
-
if (event.type !== "click" || event.button !== "left")
|
|
111
|
-
return undefined;
|
|
112
|
-
this.#expanded = this.#context.ui.getToolsExpanded() ? false : !this.#expanded;
|
|
113
|
-
redraw();
|
|
114
|
-
return { handled: true, render: true };
|
|
115
|
-
});
|
|
116
|
-
const body = {
|
|
117
|
-
render: (width) => {
|
|
118
|
-
const rows = this.#renderedExpanded
|
|
119
|
-
? renderWithoutToolRails(this.#originals, Math.max(1, width - 2))
|
|
120
|
-
: this.#members.flatMap((member) => member.summary === undefined ? [] : [member.summary]);
|
|
121
|
-
const rail = this.#context.ui.theme.fg("dim", "▎");
|
|
122
|
-
return rows.map((row) => truncateToWidth(`${rail} ${row}`, Math.max(0, width), "…"));
|
|
123
|
-
},
|
|
124
|
-
handleMouse: (event) => {
|
|
125
|
-
if (!this.#renderedExpanded || event.x < 2)
|
|
126
|
-
return undefined;
|
|
127
|
-
return this.#originals.handleMouse({
|
|
128
|
-
...event,
|
|
129
|
-
x: event.x - 2,
|
|
130
|
-
width: Math.max(1, event.width - 2),
|
|
131
|
-
});
|
|
132
|
-
},
|
|
133
|
-
invalidate() { },
|
|
156
|
+
get member() {
|
|
157
|
+
return {
|
|
158
|
+
kind: "notice",
|
|
159
|
+
component: this,
|
|
160
|
+
title: this.#type,
|
|
161
|
+
summary: this.summary,
|
|
162
|
+
detail: (width) => this.#detail(width),
|
|
134
163
|
};
|
|
135
|
-
this.children = [new Spacer(1), heading, body];
|
|
136
164
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
this.#
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
let offset = 0;
|
|
143
|
-
return {
|
|
144
|
-
render(width) {
|
|
145
|
-
const rows = component.render(width);
|
|
146
|
-
// Pi adds one leading separator per tool; the group supplies its own spacing.
|
|
147
|
-
offset = rows[0] === "" ? 1 : 0;
|
|
148
|
-
return rows.slice(offset);
|
|
149
|
-
},
|
|
150
|
-
handleMouse(event) {
|
|
151
|
-
return component.handleMouse?.({
|
|
152
|
-
...event,
|
|
153
|
-
y: event.y + offset,
|
|
154
|
-
height: event.height + offset,
|
|
155
|
-
});
|
|
156
|
-
},
|
|
157
|
-
invalidate() { },
|
|
158
|
-
};
|
|
165
|
+
#detail(width) {
|
|
166
|
+
const inner = Math.max(1, width - 2);
|
|
167
|
+
return this.#content.split("\n").flatMap((line) => {
|
|
168
|
+
const wrapped = wrapTextWithAnsi(line, inner);
|
|
169
|
+
return (wrapped.length > 0 ? wrapped : [""]).map((row) => ` ${row}`);
|
|
159
170
|
});
|
|
160
171
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
this.#
|
|
172
|
+
get #content() {
|
|
173
|
+
const prefix = this.#type === "error" ? "Error: " : this.#type === "warning" ? "Warning: " : "";
|
|
174
|
+
return `${prefix}${this.#message}`;
|
|
164
175
|
}
|
|
165
176
|
render(width) {
|
|
166
|
-
this.#
|
|
167
|
-
const theme = this.#context.ui.theme;
|
|
168
|
-
const counts = new Map();
|
|
169
|
-
for (const member of this.#members) {
|
|
170
|
-
if (member.name !== undefined)
|
|
171
|
-
counts.set(member.name, (counts.get(member.name) ?? 0) + 1);
|
|
172
|
-
}
|
|
173
|
-
const heading = [...counts]
|
|
174
|
-
.map(([name, count]) => `${theme.fg("toolTitle", theme.bold(name))} ${theme.fg("dim", `×${count}`)}`)
|
|
175
|
-
.join(theme.fg("dim", " · "));
|
|
176
|
-
this.#header.setText(truncateToWidth(`${theme.fg("dim", this.#renderedExpanded ? "▾" : "▸")} ${heading}`, Math.max(0, width), "…"));
|
|
177
|
+
this.#text.setText(this.#ui.theme.fg(this.#type === "info" ? "dim" : this.#type, this.#content));
|
|
177
178
|
return super.render(width);
|
|
178
179
|
}
|
|
179
180
|
}
|
|
180
|
-
function attach(host,
|
|
181
|
+
function attach(host, run) {
|
|
181
182
|
const chat = host.chat;
|
|
182
183
|
const original = {
|
|
183
184
|
render: chat.render,
|
|
@@ -199,13 +200,16 @@ function attach(host, context) {
|
|
|
199
200
|
const rebuild = () => {
|
|
200
201
|
const next = new Map();
|
|
201
202
|
const children = [];
|
|
203
|
+
const blocks = [];
|
|
202
204
|
let calls = new Set();
|
|
203
205
|
let members = [];
|
|
204
|
-
|
|
206
|
+
/** Closes the pending run. `ended` is false only for the run still open at the transcript tail. */
|
|
207
|
+
const flush = (ended) => {
|
|
205
208
|
const first = members[0]?.component;
|
|
206
|
-
if (first !== undefined && members.filter((member) => member.
|
|
207
|
-
const group = groups.get(first) ??
|
|
208
|
-
|
|
209
|
+
if (first !== undefined && members.filter((member) => member.kind === "tool").length > 1) {
|
|
210
|
+
const group = groups.get(first) ??
|
|
211
|
+
new ToolGroupView(host.ui, host.requestRender, run.current?.endedAt === undefined ? run.current : undefined);
|
|
212
|
+
blocks.push({ group, members, ended });
|
|
209
213
|
next.set(first, group);
|
|
210
214
|
children.push(group);
|
|
211
215
|
}
|
|
@@ -218,9 +222,13 @@ function attach(host, context) {
|
|
|
218
222
|
const info = assistantInfo(child);
|
|
219
223
|
calls = info.calls;
|
|
220
224
|
if (info.eligible) {
|
|
225
|
+
const thinking = info.thinking;
|
|
221
226
|
members.push({
|
|
227
|
+
kind: "assistant",
|
|
222
228
|
component: child,
|
|
223
|
-
...(
|
|
229
|
+
...(thinking === "" ? {} : { thinkingChars: thinking.length }),
|
|
230
|
+
...(thinking === "" || info.hidden ? {} : { thinking }),
|
|
231
|
+
...(info.usage === undefined ? {} : { usage: info.usage }),
|
|
224
232
|
});
|
|
225
233
|
continue;
|
|
226
234
|
}
|
|
@@ -233,13 +241,24 @@ function attach(host, context) {
|
|
|
233
241
|
}
|
|
234
242
|
}
|
|
235
243
|
else if (child instanceof NotifyView) {
|
|
236
|
-
members.push(
|
|
244
|
+
members.push(child.member);
|
|
237
245
|
continue;
|
|
238
246
|
}
|
|
239
|
-
|
|
247
|
+
else {
|
|
248
|
+
const continuation = noticeMember(child);
|
|
249
|
+
if (continuation !== undefined) {
|
|
250
|
+
members.push(continuation);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
flush(true);
|
|
240
255
|
children.push(child);
|
|
241
256
|
}
|
|
242
|
-
flush();
|
|
257
|
+
flush(false);
|
|
258
|
+
const frozen = run.current === undefined || run.current.endedAt !== undefined;
|
|
259
|
+
for (const block of blocks) {
|
|
260
|
+
block.group.update(block.members, frozen || block.ended);
|
|
261
|
+
}
|
|
243
262
|
groups = next;
|
|
244
263
|
projection.children = children;
|
|
245
264
|
};
|
|
@@ -254,7 +273,7 @@ function attach(host, context) {
|
|
|
254
273
|
active = false;
|
|
255
274
|
groups.clear();
|
|
256
275
|
projection.clear();
|
|
257
|
-
|
|
276
|
+
host.ui.notify(`Tool grouping disabled: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
258
277
|
return original.render.call(chat, width);
|
|
259
278
|
}
|
|
260
279
|
// Real renderer errors propagate; never hide a tool-rendering failure.
|
|
@@ -276,7 +295,7 @@ function attach(host, context) {
|
|
|
276
295
|
notify(message, type) {
|
|
277
296
|
if (!active)
|
|
278
297
|
return false;
|
|
279
|
-
chat.addChild(new NotifyView(message, type,
|
|
298
|
+
chat.addChild(new NotifyView(message, type, host.ui));
|
|
280
299
|
host.requestRender();
|
|
281
300
|
return true;
|
|
282
301
|
},
|
|
@@ -300,7 +319,7 @@ function attach(host, context) {
|
|
|
300
319
|
Object.defineProperty(chat, key, descriptor);
|
|
301
320
|
}
|
|
302
321
|
if (conflict)
|
|
303
|
-
|
|
322
|
+
host.ui.notify("Tool grouping patch changed by another extension; restart Pi to fully unload it.", "warning");
|
|
304
323
|
host.requestRender();
|
|
305
324
|
},
|
|
306
325
|
};
|
|
@@ -313,7 +332,7 @@ function connect(owner, host) {
|
|
|
313
332
|
}
|
|
314
333
|
catch (error) {
|
|
315
334
|
owner.failed.add(host.chat);
|
|
316
|
-
owner.
|
|
335
|
+
owner.ui.notify(`Tool grouping unavailable: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
317
336
|
}
|
|
318
337
|
}
|
|
319
338
|
function patchState() {
|
|
@@ -354,7 +373,7 @@ function patchState() {
|
|
|
354
373
|
if (record(instance))
|
|
355
374
|
failed.add(instance);
|
|
356
375
|
for (const owner of owners.values())
|
|
357
|
-
owner.
|
|
376
|
+
owner.ui.notify(`Tool grouping unavailable: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
358
377
|
return undefined;
|
|
359
378
|
}
|
|
360
379
|
if (record(instance) && !seen.has(instance)) {
|
|
@@ -397,7 +416,8 @@ export function installHostToolGroups(context) {
|
|
|
397
416
|
!(major > 1 || (major === 1 && (minor > 0 || (minor === 0 && patch >= 1)))))
|
|
398
417
|
throw new Error(`Tool grouping requires Pi >=1.0.1; running ${VERSION}`);
|
|
399
418
|
const state = patchState();
|
|
400
|
-
|
|
419
|
+
const ui = context.ui;
|
|
420
|
+
if (state.owners.has(ui))
|
|
401
421
|
throw new Error("Tool grouping already owns this UI runtime");
|
|
402
422
|
if (state.owners.size === 0) {
|
|
403
423
|
for (const seam of SEAMS) {
|
|
@@ -411,13 +431,15 @@ export function installHostToolGroups(context) {
|
|
|
411
431
|
value: state.wrappers[seam],
|
|
412
432
|
});
|
|
413
433
|
}
|
|
434
|
+
const run = {};
|
|
414
435
|
const owner = {
|
|
415
|
-
|
|
436
|
+
ui,
|
|
416
437
|
hosts: new Map(),
|
|
417
438
|
failed: new WeakSet(),
|
|
418
|
-
|
|
439
|
+
run,
|
|
440
|
+
attach: (host) => attach(host, run),
|
|
419
441
|
};
|
|
420
|
-
state.owners.set(
|
|
442
|
+
state.owners.set(ui, owner);
|
|
421
443
|
// Reload rebuilds history before session_start; reconnect already discovered instances.
|
|
422
444
|
for (const reference of state.hosts) {
|
|
423
445
|
const instance = reference.deref();
|
|
@@ -427,7 +449,7 @@ export function installHostToolGroups(context) {
|
|
|
427
449
|
}
|
|
428
450
|
try {
|
|
429
451
|
const host = readHost(instance);
|
|
430
|
-
if (host.ui ===
|
|
452
|
+
if (host.ui === ui)
|
|
431
453
|
connect(owner, host);
|
|
432
454
|
}
|
|
433
455
|
catch {
|
|
@@ -440,11 +462,20 @@ export function installHostToolGroups(context) {
|
|
|
440
462
|
for (const host of owner.hosts.values())
|
|
441
463
|
host.requestRender();
|
|
442
464
|
},
|
|
465
|
+
beginRun() {
|
|
466
|
+
run.current = { startedAt: performance.now() };
|
|
467
|
+
},
|
|
468
|
+
endRun() {
|
|
469
|
+
const current = run.current;
|
|
470
|
+
if (current !== undefined && current.endedAt === undefined)
|
|
471
|
+
current.endedAt = performance.now();
|
|
472
|
+
},
|
|
443
473
|
dispose() {
|
|
444
474
|
if (disposed)
|
|
445
475
|
return;
|
|
446
476
|
disposed = true;
|
|
447
|
-
|
|
477
|
+
run.current = undefined;
|
|
478
|
+
state.owners.delete(ui);
|
|
448
479
|
for (const host of owner.hosts.values())
|
|
449
480
|
host.dispose();
|
|
450
481
|
owner.hosts.clear();
|
|
@@ -455,7 +486,7 @@ export function installHostToolGroups(context) {
|
|
|
455
486
|
state.wrappers[seam])
|
|
456
487
|
Object.defineProperty(InteractiveMode.prototype, seam, state.originals[seam]);
|
|
457
488
|
else
|
|
458
|
-
|
|
489
|
+
ui.notify("Tool grouping seam changed by another extension; restart Pi to fully unload it.", "warning");
|
|
459
490
|
}
|
|
460
491
|
},
|
|
461
492
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -2,5 +2,5 @@ export { type DiffHunk, type DiffLine, type DiffSource, type DiffStats, DiffView
|
|
|
2
2
|
export { createMessageView, type MessageRows, type MessageTone, MessageView, type MessageViewHeader, type MessageViewSpec, } from "./message-view.js";
|
|
3
3
|
export { extractChildToolCopyText, NestedCallsView, type NestedCallsViewProps, } from "./nested-calls-view.js";
|
|
4
4
|
export { type CollapsibleToolRendererOptions, createCollapsibleToolRendererResolver, estimateStreamingTokens, inlinePresentation, isMcpTool, type NestedCallRecord, registerCollapsibleToolRenderer, SPECIALIZED_TOOL_RENDERERS, type SpecializedToolRendererName, type ToolRenderContext, } from "./renderer.js";
|
|
5
|
-
export { type Collapsible, createToolView, type FoldLevel, formatStatusGlyph, formatToolHeaderText, isCollapsible, type RailColorToken, type RailSection, type ToolStatusKind, ToolView, type ToolViewCopySpec, type ToolViewHeaderSpec, type ToolViewSpec, transitionFoldLevel, } from "./tool-view.js";
|
|
5
|
+
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";
|
|
6
6
|
export { type CapturedToolCall, clearCapturedNestedCalls, getCapturedNestedCalls, recordToolExecutionEnd, recordToolExecutionStart, } from "./tracker.js";
|
package/dist/message-view.d.ts
CHANGED
|
@@ -12,4 +12,18 @@ 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
|
+
/**
|
|
18
|
+
* Detail rows inside an activity group: preview and body below the summary row the group already
|
|
19
|
+
* draws, regardless of local fold. Repeating the header there would print the summary twice.
|
|
20
|
+
*/
|
|
21
|
+
detailRows(width: number): string[];
|
|
22
|
+
/** Single-row label used when this notice sits inside an activity group. */
|
|
23
|
+
get activitySummary(): string;
|
|
24
|
+
/**
|
|
25
|
+
* Whether anything would render below the label. `expandable` asks about the body only; a
|
|
26
|
+
* preview-only notice still has detail to show, so the group keys its pin on this instead.
|
|
27
|
+
*/
|
|
28
|
+
get hasDetail(): boolean;
|
|
15
29
|
}
|
package/dist/message-view.js
CHANGED
|
@@ -106,12 +106,8 @@ export class MessageView {
|
|
|
106
106
|
invalidateRows(this.#spec.body);
|
|
107
107
|
}
|
|
108
108
|
handleMouse(event) {
|
|
109
|
-
if (!this.expandable ||
|
|
110
|
-
event.type === "wheel" ||
|
|
111
|
-
event.type !== "click" ||
|
|
112
|
-
event.button !== "left") {
|
|
109
|
+
if (!this.expandable || event.type !== "click" || event.button !== "left")
|
|
113
110
|
return undefined;
|
|
114
|
-
}
|
|
115
111
|
const effectiveWidth = event.width > 0 ? event.width : this.#lastWidth;
|
|
116
112
|
const pad = Math.max(0, this.#spec.outputPad ?? 0);
|
|
117
113
|
const innerX = event.x - pad;
|
|
@@ -125,13 +121,37 @@ export class MessageView {
|
|
|
125
121
|
this.toggle();
|
|
126
122
|
return { handled: true, render: true };
|
|
127
123
|
}
|
|
128
|
-
|
|
124
|
+
/** Provider-declared notice type, used to group notices without reading their text. */
|
|
125
|
+
get kind() {
|
|
126
|
+
return this.#spec.header.kind;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Detail rows inside an activity group: preview and body below the summary row the group already
|
|
130
|
+
* draws, regardless of local fold. Repeating the header there would print the summary twice.
|
|
131
|
+
*/
|
|
132
|
+
detailRows(width) {
|
|
133
|
+
const pad = Math.max(0, this.#spec.outputPad ?? 0);
|
|
134
|
+
const safeWidth = Math.max(1, width - pad * 2);
|
|
135
|
+
const leftPad = pad > 0 ? " ".repeat(pad) : "";
|
|
136
|
+
return [...layRows(this.#spec.preview, safeWidth), ...layRows(this.#spec.body, safeWidth)].map((line) => (leftPad === "" ? line : `${leftPad}${line}`));
|
|
137
|
+
}
|
|
138
|
+
/** Single-row label used when this notice sits inside an activity group. */
|
|
139
|
+
get activitySummary() {
|
|
129
140
|
const { theme, header } = this.#spec;
|
|
130
141
|
const kind = theme.fg("dim", toSingleRow(header.kind));
|
|
131
142
|
const summaryRaw = header.summary === undefined ? "" : toSingleRow(header.summary);
|
|
132
143
|
const summary = summaryRaw === "" ? "" : theme.fg(header.tone ?? "dim", summaryRaw);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
144
|
+
return summary === "" ? kind : `${kind}${theme.fg("dim", " · ")}${summary}`;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Whether anything would render below the label. `expandable` asks about the body only; a
|
|
148
|
+
* preview-only notice still has detail to show, so the group keys its pin on this instead.
|
|
149
|
+
*/
|
|
150
|
+
get hasDetail() {
|
|
151
|
+
return hasRows(this.#spec.preview) || hasRows(this.#spec.body);
|
|
152
|
+
}
|
|
153
|
+
#header(width, expanded) {
|
|
154
|
+
const pin = this.expandable ? `${this.#spec.theme.fg("dim", expanded ? "▾" : "▸")} ` : "";
|
|
155
|
+
return truncate(`${pin}${this.activitySummary}`, width);
|
|
136
156
|
}
|
|
137
157
|
}
|