@hheei/pi-ext-ui 0.5.2 → 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/dist/extension.js +12 -0
- package/dist/host-transcript-patch.d.ts +2 -0
- package/dist/host-transcript-patch.js +123 -164
- package/dist/index.d.ts +1 -1
- package/dist/message-view.d.ts +12 -0
- package/dist/message-view.js +22 -5
- 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
|
@@ -67,6 +67,7 @@ export class NestedCallsView {
|
|
|
67
67
|
#props;
|
|
68
68
|
#inMemoryLevels = new Map();
|
|
69
69
|
#copiedIndex = undefined;
|
|
70
|
+
#failedIndex = undefined;
|
|
70
71
|
#copiedTimer = undefined;
|
|
71
72
|
#lineMapping = [];
|
|
72
73
|
#renderCache;
|
|
@@ -96,14 +97,11 @@ export class NestedCallsView {
|
|
|
96
97
|
typeof copiedState.until === "number") {
|
|
97
98
|
const remaining = copiedState.until - Date.now();
|
|
98
99
|
if (remaining > 0) {
|
|
99
|
-
|
|
100
|
+
const failed = copiedState.failed === true;
|
|
101
|
+
this.#copiedIndex = failed ? undefined : copiedState.index;
|
|
102
|
+
this.#failedIndex = failed ? copiedState.index : undefined;
|
|
100
103
|
this.#copiedTimer = setTimeout(() => {
|
|
101
|
-
this.#
|
|
102
|
-
this.#renderCache = undefined;
|
|
103
|
-
const s = this.#props.stateHolder;
|
|
104
|
-
if (s !== undefined) {
|
|
105
|
-
delete s[NESTED_CALLS_COPIED_KEY];
|
|
106
|
-
}
|
|
104
|
+
this.#clearCopyFeedback();
|
|
107
105
|
this.#props.invalidate?.();
|
|
108
106
|
}, remaining);
|
|
109
107
|
this.#copiedTimer.unref?.();
|
|
@@ -162,9 +160,9 @@ export class NestedCallsView {
|
|
|
162
160
|
*/
|
|
163
161
|
#callContext(call) {
|
|
164
162
|
const { context } = this.#props;
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
163
|
+
// A captured call carries its own id. A persisted record has none, so it falls back to the
|
|
164
|
+
// parent's id — the only identity the host offers, and never a child's guess.
|
|
165
|
+
const toolCallId = "id" in call ? call.id : context.toolCallId;
|
|
168
166
|
return {
|
|
169
167
|
...context,
|
|
170
168
|
lastComponent: undefined,
|
|
@@ -187,39 +185,54 @@ export class NestedCallsView {
|
|
|
187
185
|
if (!text)
|
|
188
186
|
return;
|
|
189
187
|
const writeClip = this.#props.copyToClipboard ?? copyToSystemClipboard;
|
|
190
|
-
void writeClip(text).catch(() => { });
|
|
191
188
|
const durationMs = 1500;
|
|
189
|
+
// Feedback starts when the write settles: a rejected clipboard write is not a copy.
|
|
190
|
+
void writeClip(text).then(() => this.#showCopyFeedback(callIndex, durationMs, false), () => this.#showCopyFeedback(callIndex, durationMs, true));
|
|
191
|
+
}
|
|
192
|
+
/** One feedback window on one row: ✓ when the text reached the clipboard, ✗ when it did not. */
|
|
193
|
+
#showCopyFeedback(callIndex, durationMs, failed) {
|
|
192
194
|
const until = Date.now() + durationMs;
|
|
193
|
-
this.#copiedIndex = callIndex;
|
|
195
|
+
this.#copiedIndex = failed ? undefined : callIndex;
|
|
196
|
+
this.#failedIndex = failed ? callIndex : undefined;
|
|
194
197
|
this.#renderCache = undefined;
|
|
195
198
|
const state = this.#props.stateHolder;
|
|
196
199
|
if (state !== undefined) {
|
|
197
200
|
state[NESTED_CALLS_COPIED_KEY] = {
|
|
198
201
|
index: callIndex,
|
|
199
202
|
until,
|
|
203
|
+
...(failed ? { failed: true } : {}),
|
|
200
204
|
};
|
|
201
205
|
}
|
|
202
206
|
if (this.#copiedTimer !== undefined) {
|
|
203
207
|
clearTimeout(this.#copiedTimer);
|
|
204
208
|
}
|
|
205
209
|
this.#copiedTimer = setTimeout(() => {
|
|
206
|
-
this.#
|
|
207
|
-
this.#renderCache = undefined;
|
|
208
|
-
const s = this.#props.stateHolder;
|
|
209
|
-
if (s !== undefined) {
|
|
210
|
-
delete s[NESTED_CALLS_COPIED_KEY];
|
|
211
|
-
}
|
|
210
|
+
this.#clearCopyFeedback();
|
|
212
211
|
this.#props.invalidate?.();
|
|
213
212
|
}, durationMs);
|
|
214
213
|
this.#copiedTimer.unref?.();
|
|
215
214
|
this.#props.invalidate?.();
|
|
216
215
|
}
|
|
216
|
+
/** Drops the copy marker, its timer, and the shared record it left behind. */
|
|
217
|
+
#clearCopyFeedback() {
|
|
218
|
+
if (this.#copiedTimer !== undefined) {
|
|
219
|
+
clearTimeout(this.#copiedTimer);
|
|
220
|
+
this.#copiedTimer = undefined;
|
|
221
|
+
}
|
|
222
|
+
this.#copiedIndex = undefined;
|
|
223
|
+
this.#failedIndex = undefined;
|
|
224
|
+
this.#renderCache = undefined;
|
|
225
|
+
const state = this.#props.stateHolder;
|
|
226
|
+
if (state !== undefined)
|
|
227
|
+
delete state[NESTED_CALLS_COPIED_KEY];
|
|
228
|
+
}
|
|
217
229
|
render(width) {
|
|
218
230
|
const safeWidth = Math.max(1, width);
|
|
219
231
|
const levelsKey = this.#getLevelsKey();
|
|
220
232
|
if (this.#renderCache !== undefined &&
|
|
221
233
|
this.#renderCache.width === safeWidth &&
|
|
222
234
|
this.#renderCache.copiedIndex === this.#copiedIndex &&
|
|
235
|
+
this.#renderCache.failedIndex === this.#failedIndex &&
|
|
223
236
|
this.#renderCache.levelsKey === levelsKey) {
|
|
224
237
|
this.#lineMapping = this.#renderCache.lineMapping;
|
|
225
238
|
return this.#renderCache.lines;
|
|
@@ -254,7 +267,11 @@ export class NestedCallsView {
|
|
|
254
267
|
summary: [childSummary, duration].filter(Boolean).join(" · "),
|
|
255
268
|
}, theme);
|
|
256
269
|
const isCopied = this.#copiedIndex === i;
|
|
257
|
-
const copyIcon =
|
|
270
|
+
const copyIcon = this.#failedIndex === i
|
|
271
|
+
? theme.fg("error", "✗")
|
|
272
|
+
: isCopied
|
|
273
|
+
? theme.fg("success", "✓")
|
|
274
|
+
: theme.fg("dim", "");
|
|
258
275
|
const copySlotWidth = 2 + rightMargin;
|
|
259
276
|
const maxLeft = Math.max(1, width - copySlotWidth);
|
|
260
277
|
const truncatedLeft = truncate(leftContent, maxLeft);
|
|
@@ -290,6 +307,7 @@ export class NestedCallsView {
|
|
|
290
307
|
this.#renderCache = {
|
|
291
308
|
width: safeWidth,
|
|
292
309
|
copiedIndex: this.#copiedIndex,
|
|
310
|
+
failedIndex: this.#failedIndex,
|
|
293
311
|
levelsKey,
|
|
294
312
|
lines,
|
|
295
313
|
lineMapping: this.#lineMapping,
|
|
@@ -390,17 +408,13 @@ export class NestedCallsView {
|
|
|
390
408
|
return { handled: true };
|
|
391
409
|
}
|
|
392
410
|
dispose() {
|
|
393
|
-
|
|
394
|
-
clearTimeout(this.#copiedTimer);
|
|
395
|
-
this.#copiedTimer = undefined;
|
|
396
|
-
}
|
|
411
|
+
this.#clearCopyFeedback();
|
|
397
412
|
this.#renderCache = undefined;
|
|
398
413
|
}
|
|
399
414
|
collapseAll() {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
}
|
|
415
|
+
// The marker points at a row that is no longer on screen; leaving it behind would bring a
|
|
416
|
+
// permanent checkmark back when the same view expands again.
|
|
417
|
+
this.#clearCopyFeedback();
|
|
404
418
|
this.#inMemoryLevels.clear();
|
|
405
419
|
const state = this.#props.stateHolder;
|
|
406
420
|
if (state !== undefined) {
|
package/dist/payload-parser.js
CHANGED
|
@@ -166,16 +166,16 @@ export function extractSummaryArg(toolName, args) {
|
|
|
166
166
|
return firstLine(parsed.code);
|
|
167
167
|
case "bg_bash_get":
|
|
168
168
|
case "bg_bash_stop":
|
|
169
|
-
case "
|
|
170
|
-
case "
|
|
171
|
-
case "agent_stop":
|
|
169
|
+
case "agents_get":
|
|
170
|
+
case "agents_stop":
|
|
172
171
|
return typeof parsed.id === "string" ? parsed.id.trim() : "";
|
|
173
|
-
case "
|
|
172
|
+
case "agents_delegate": {
|
|
174
173
|
const agent = typeof parsed.agent === "string" ? parsed.agent.trim() : "";
|
|
174
|
+
const id = typeof parsed.id === "string" ? parsed.id.trim() : "";
|
|
175
175
|
const task = firstLine(parsed.task);
|
|
176
176
|
if (agent && task)
|
|
177
177
|
return `${agent}: ${task}`;
|
|
178
|
-
return agent || task;
|
|
178
|
+
return agent || id || task;
|
|
179
179
|
}
|
|
180
180
|
case "tool_search":
|
|
181
181
|
return typeof parsed.query === "string" ? parsed.query.trim() : "";
|
package/dist/renderer.js
CHANGED
|
@@ -79,6 +79,47 @@ function resolveToolCallSummary(params) {
|
|
|
79
79
|
// Tier 2: Contractual Fallback for built-in tools
|
|
80
80
|
return extractSummaryArg(toolName, args);
|
|
81
81
|
}
|
|
82
|
+
function isCapturedCall(call) {
|
|
83
|
+
return "args" in call || "result" in call;
|
|
84
|
+
}
|
|
85
|
+
/** Nested facts for a consumer that lists children outside the container's own body. */
|
|
86
|
+
function nestedChildren(calls) {
|
|
87
|
+
return calls.map((call) => {
|
|
88
|
+
if (isCapturedCall(call)) {
|
|
89
|
+
const isError = call.isError === true;
|
|
90
|
+
const inline = inlinePresentation(call.toolName, call.args, call.result?.details, isError);
|
|
91
|
+
return {
|
|
92
|
+
title: call.toolName,
|
|
93
|
+
...(inline === undefined ? {} : { summary: inline.summary }),
|
|
94
|
+
// The tracker records a call when it starts, so a missing result means it is still
|
|
95
|
+
// running, not that it succeeded.
|
|
96
|
+
status: isError
|
|
97
|
+
? "error"
|
|
98
|
+
: call.result === undefined
|
|
99
|
+
? "running"
|
|
100
|
+
: "ok",
|
|
101
|
+
...(call.durationMs === undefined ? {} : { durationMs: call.durationMs }),
|
|
102
|
+
...(call.result === undefined
|
|
103
|
+
? {}
|
|
104
|
+
: { detail: extractResultText(call.toolName, call.result).outputText }),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
title: call.toolName,
|
|
109
|
+
...(call.summary === undefined ? {} : { summary: call.summary }),
|
|
110
|
+
// Same precedence as formatNestedCallRow: a cancelled call is a warning, not a success.
|
|
111
|
+
status: call.status === "running"
|
|
112
|
+
? "running"
|
|
113
|
+
: call.isWarning === true
|
|
114
|
+
? "warn"
|
|
115
|
+
: call.isError === true || call.status === "error"
|
|
116
|
+
? "error"
|
|
117
|
+
: "ok",
|
|
118
|
+
...(call.durationMs === undefined ? {} : { durationMs: call.durationMs }),
|
|
119
|
+
...(call.error === undefined ? {} : { detail: call.error }),
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
}
|
|
82
123
|
function formatArgsPreview(args) {
|
|
83
124
|
if (args === undefined || args === null)
|
|
84
125
|
return "";
|
|
@@ -256,6 +297,7 @@ function renderGenericResult(params) {
|
|
|
256
297
|
invalidate: () => context.invalidate(),
|
|
257
298
|
})
|
|
258
299
|
: undefined;
|
|
300
|
+
const children = nestedChildren(childCalls);
|
|
259
301
|
const collapsedRows = codemodeNestedView !== undefined
|
|
260
302
|
? codemodeNestedView
|
|
261
303
|
: isMetaTool && nestedCalls.length > 0
|
|
@@ -366,6 +408,7 @@ function renderGenericResult(params) {
|
|
|
366
408
|
},
|
|
367
409
|
railToken: toolRailToken,
|
|
368
410
|
collapsedRows,
|
|
411
|
+
...(children.length === 0 ? {} : { children }),
|
|
369
412
|
sections,
|
|
370
413
|
copy,
|
|
371
414
|
outputPad: context.outputPad,
|
|
@@ -534,6 +577,8 @@ function renderOwnedPresentation(params) {
|
|
|
534
577
|
.map((line) => theme.fg("error", stripTerminalSequences(line)))
|
|
535
578
|
: undefined;
|
|
536
579
|
})(),
|
|
580
|
+
...(presentation.children === undefined ? {} : { children: presentation.children }),
|
|
581
|
+
...(presentation.childCount === undefined ? {} : { childCount: presentation.childCount }),
|
|
537
582
|
sections,
|
|
538
583
|
copy,
|
|
539
584
|
outputPad: context.outputPad,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type Component, Container } from "@earendil-works/pi-tui";
|
|
3
|
+
import { type ToolStatusKind, type ToolViewChild } from "./tool-view.js";
|
|
4
|
+
export interface Usage {
|
|
5
|
+
readonly input: number;
|
|
6
|
+
readonly output: number;
|
|
7
|
+
readonly cacheRead: number;
|
|
8
|
+
readonly cacheWrite: number;
|
|
9
|
+
}
|
|
10
|
+
export type MemberKind = "assistant" | "tool" | "notice";
|
|
11
|
+
export type Category = "tools" | "thinking" | "notices";
|
|
12
|
+
/** One transcript entry a block owns, reduced to the facts the collapsed frame draws. */
|
|
13
|
+
export interface Member {
|
|
14
|
+
readonly kind: MemberKind;
|
|
15
|
+
readonly component: Component;
|
|
16
|
+
readonly title?: string;
|
|
17
|
+
readonly summary?: string;
|
|
18
|
+
readonly status?: ToolStatusKind;
|
|
19
|
+
readonly children?: readonly ToolViewChild[];
|
|
20
|
+
/** Nested calls the member made, when `children` is only what fits. */
|
|
21
|
+
readonly childCount?: number;
|
|
22
|
+
readonly thinking?: string;
|
|
23
|
+
readonly thinkingChars?: number;
|
|
24
|
+
readonly usage?: Usage;
|
|
25
|
+
readonly detail?: (width: number) => string[];
|
|
26
|
+
}
|
|
27
|
+
/** One agent loop. Blocks created during the run share its origin; ending it freezes them. */
|
|
28
|
+
export interface ToolGroupRun {
|
|
29
|
+
readonly startedAt: number;
|
|
30
|
+
endedAt?: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The collapsed frame of one activity block.
|
|
34
|
+
*
|
|
35
|
+
* The frame owns only layout and hit testing: tool rows keep their own component so their glyph,
|
|
36
|
+
* colors, copy action, and fold level stay the ones the transcript already draws. Anything this
|
|
37
|
+
* view synthesized itself would drift from those rows.
|
|
38
|
+
*/
|
|
39
|
+
export declare class ToolGroupView extends Container {
|
|
40
|
+
#private;
|
|
41
|
+
constructor(ui: ExtensionUIContext, redraw: () => void, run: ToolGroupRun | undefined);
|
|
42
|
+
update(members: Member[], settled: boolean): void;
|
|
43
|
+
invalidate(): void;
|
|
44
|
+
render(width: number): string[];
|
|
45
|
+
}
|