@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.
@@ -1,8 +1,8 @@
1
1
  import { AssistantMessageComponent, InteractiveMode, ToolExecutionComponent, VERSION, } from "@earendil-works/pi-coding-agent";
2
- import { Container, MouseRegion, Spacer, stripTerminalSequences, Text, } from "@earendil-works/pi-tui";
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 { renderWithoutToolRails, ToolView, toSingleRow } from "./tool-view.js";
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
- let thinking = false;
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
- thinking ||= block.thinking.trim() !== "";
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 { calls, eligible, thinking };
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" || typeof name !== "string" || typeof partial !== "boolean") {
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
- const summary = renderer instanceof ToolView
70
- ? (renderer.summary ?? callRenderer.summary)
71
- : callRenderer.summary;
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
- return { component, name: toSingleRow(stripTerminalSequences(name)), summary };
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 noticeContinuation(component) {
130
+ function noticeMember(component) {
88
131
  const view = messageView(component);
89
- return view === undefined ? undefined : { component, summary: view.activitySummary };
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
- class NotifyView extends Container {
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
- #context;
151
+ #ui;
94
152
  #message;
95
153
  #type;
96
- constructor(message, type, context) {
97
- super();
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 summary() {
105
- return this.#context.ui.theme.fg(this.#type === "info" ? "dim" : this.#type, toSingleRow(this.#content));
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
- this.#text.setText(this.#context.ui.theme.fg(this.#type === "info" ? "dim" : this.#type, this.#content));
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
- // Original chatContainer.invalidate invalidates original members exactly once.
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, context) {
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
- const flush = () => {
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
- if (first !== undefined && members.filter((member) => member.name !== undefined).length > 1) {
272
- const group = groups.get(first) ?? new ToolGroup(context, host.requestRender);
273
- group.update(members);
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.map((member) => member.component));
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
- members.push({
287
- component: child,
288
- ...(info.thinking ? { summary: context.ui.theme.fg("thinkingText", "thinking…") } : {}),
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({ component: child, summary: child.summary });
343
+ members.push(child.member);
302
344
  continue;
303
345
  }
304
346
  else {
305
- const continuation = noticeContinuation(child);
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
- groups.clear();
377
+ disposeGroups();
328
378
  projection.clear();
329
- context.ui.notify(`Tool grouping disabled: ${error instanceof Error ? error.message : String(error)}`, "warning");
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, context));
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
- groups.clear();
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
- context.ui.notify("Tool grouping patch changed by another extension; restart Pi to fully unload it.", "warning");
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.context.ui.notify(`Tool grouping unavailable: ${error instanceof Error ? error.message : String(error)}`, "warning");
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.context.ui.notify(`Tool grouping unavailable: ${error instanceof Error ? error.message : String(error)}`, "warning");
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
- if (state.owners.has(context.ui))
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
- context,
544
+ ui,
488
545
  hosts: new Map(),
489
546
  failed: new WeakSet(),
490
- attach: (host) => attach(host, context),
547
+ attach: (host) => attach(host, run),
491
548
  };
492
- state.owners.set(context.ui, owner);
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 === context.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
- state.owners.delete(context.ui);
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
- context.ui.notify("Tool grouping seam changed by another extension; restart Pi to fully unload it.", "warning");
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, NestedCallsView, type NestedCallsViewProps, } from "./nested-calls-view.js";
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";
6
- export { type CapturedToolCall, clearCapturedNestedCalls, getCapturedNestedCalls, recordToolExecutionEnd, recordToolExecutionStart, } from "./tracker.js";
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, NestedCallsView, } from "./nested-calls-view.js";
4
- export { createCollapsibleToolRendererResolver, estimateStreamingTokens, inlinePresentation, isMcpTool, registerCollapsibleToolRenderer, SPECIALIZED_TOOL_RENDERERS, } from "./renderer.js";
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 { clearCapturedNestedCalls, getCapturedNestedCalls, recordToolExecutionEnd, recordToolExecutionStart, } from "./tracker.js";
8
+ export { NestedCallArgsIndex, ToolExecutionTracker, } from "./tracker.js";
@@ -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
  }