@hheei/pi-ext-ui 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/extension.js +39 -14
- package/dist/host-transcript-patch.js +94 -22
- package/dist/message-view.d.ts +2 -0
- package/dist/message-view.js +7 -4
- package/dist/settings.d.ts +6 -0
- package/dist/settings.js +36 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,8 +8,8 @@ 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
|
-
|
|
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
|
|
|
15
|
-
|
|
15
|
+
Grouping uses two centralized, version-checked host prototype hooks: tool lookup and extension notifications. Thinking uses the existing assistant message, with no extra hook. Unsupported hosts or incompatible layouts report a warning and retain normal rendering. Cleanup restores the original methods; if another extension replaces the same methods, restart Pi to unload the conflicting patch. For workspace development use `pnpm run pi-dev`: running the bundled CLI beside physical host devDependencies can resolve a second host class, unlike the published installation.
|
package/dist/extension.js
CHANGED
|
@@ -1,33 +1,43 @@
|
|
|
1
|
-
import { messageViewPoint, registerExtensionHook, registerExtensionLifecycle, toolRendererPoint, } from "@hheei/pi-ext-core";
|
|
1
|
+
import { getRuntimeSettingsRegistry, messageViewPoint, registerExtensionHook, registerExtensionLifecycle, registerSettings, toolRendererPoint, } from "@hheei/pi-ext-core";
|
|
2
2
|
import { installHostToolGroups } from "./host-transcript-patch.js";
|
|
3
3
|
import { createMessageView } from "./message-view.js";
|
|
4
4
|
import { createOptionalToolRendererFactory, registerCollapsibleToolRenderer } from "./renderer.js";
|
|
5
|
+
import { activityGroupsEnabled, createToolGroupSettings } from "./settings.js";
|
|
5
6
|
import { clearCapturedNestedCalls } from "./tracker.js";
|
|
6
7
|
export default function (pi) {
|
|
7
|
-
pi.registerFlag("ui-tool-groups", {
|
|
8
|
-
type: "boolean",
|
|
9
|
-
default: false,
|
|
10
|
-
description: "Group tool summaries, thinking and notifications (Pi >=1.0.1 runtime patch)",
|
|
11
|
-
});
|
|
12
8
|
let redrawGroups;
|
|
13
9
|
pi.on("agent_settled", () => redrawGroups?.());
|
|
14
10
|
registerCollapsibleToolRenderer(pi);
|
|
15
11
|
registerExtensionLifecycle(pi, {
|
|
16
12
|
key: "@hheei/pi-ext-ui",
|
|
17
|
-
start(context) {
|
|
18
|
-
|
|
13
|
+
async start(context) {
|
|
14
|
+
let patch;
|
|
15
|
+
const apply = (enabled) => {
|
|
16
|
+
if (context.extension.mode !== "tui")
|
|
17
|
+
return;
|
|
18
|
+
if (!enabled) {
|
|
19
|
+
redrawGroups = undefined;
|
|
20
|
+
patch?.dispose();
|
|
21
|
+
patch = undefined;
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (patch !== undefined)
|
|
25
|
+
return;
|
|
19
26
|
try {
|
|
20
|
-
|
|
27
|
+
patch = installHostToolGroups(context.extension);
|
|
21
28
|
redrawGroups = patch.requestRender;
|
|
22
|
-
context.resources.add("tool-groups", () => {
|
|
23
|
-
redrawGroups = undefined;
|
|
24
|
-
patch.dispose();
|
|
25
|
-
});
|
|
26
29
|
}
|
|
27
30
|
catch (error) {
|
|
28
31
|
context.extension.ui.notify(error instanceof Error ? error.message : String(error), "warning");
|
|
29
32
|
}
|
|
30
|
-
}
|
|
33
|
+
};
|
|
34
|
+
const provider = createToolGroupSettings({ onEnabled: apply });
|
|
35
|
+
context.resources.add("tool-group-settings", registerSettings(provider, getRuntimeSettingsRegistry(pi)));
|
|
36
|
+
context.resources.add("tool-groups", () => {
|
|
37
|
+
redrawGroups = undefined;
|
|
38
|
+
patch?.dispose();
|
|
39
|
+
patch = undefined;
|
|
40
|
+
});
|
|
31
41
|
context.resources.add("tracker-cleanup", () => {
|
|
32
42
|
clearCapturedNestedCalls();
|
|
33
43
|
});
|
|
@@ -35,6 +45,21 @@ export default function (pi) {
|
|
|
35
45
|
context.resources.add("tool-renderer", () => toolRenderers.dispose());
|
|
36
46
|
const handle = registerExtensionHook(context.pi, messageViewPoint, createMessageView);
|
|
37
47
|
context.resources.add("message-view", () => handle.dispose());
|
|
48
|
+
try {
|
|
49
|
+
const state = await provider.storage.load({
|
|
50
|
+
sessionId: context.extension.sessionManager.getSessionId(),
|
|
51
|
+
signal: context.signal,
|
|
52
|
+
});
|
|
53
|
+
if (!context.signal.aborted)
|
|
54
|
+
apply(activityGroupsEnabled(state));
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (!context.signal.aborted) {
|
|
58
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
59
|
+
context.extension.ui.notify(`Tool activity groups settings unreadable: ${detail}`, "warning");
|
|
60
|
+
apply(true);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
38
63
|
},
|
|
39
64
|
});
|
|
40
65
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { AssistantMessageComponent, InteractiveMode, ToolExecutionComponent, VERSION, } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Container, MouseRegion, Spacer, stripTerminalSequences, Text,
|
|
2
|
+
import { Container, MouseRegion, Spacer, stripTerminalSequences, Text, } from "@earendil-works/pi-tui";
|
|
3
|
+
import { fitRow } from "@hheei/pi-ext-core";
|
|
4
|
+
import { MessageView } from "./message-view.js";
|
|
3
5
|
import { renderWithoutToolRails, ToolView, toSingleRow } from "./tool-view.js";
|
|
4
6
|
const SEAMS = ["getRegisteredToolDefinition", "showExtensionNotify"];
|
|
5
7
|
function record(value) {
|
|
@@ -71,6 +73,21 @@ function toolMember(component, calls) {
|
|
|
71
73
|
return undefined;
|
|
72
74
|
return { component, name: toSingleRow(stripTerminalSequences(name)), summary };
|
|
73
75
|
}
|
|
76
|
+
function messageView(component) {
|
|
77
|
+
if (component instanceof MessageView)
|
|
78
|
+
return component;
|
|
79
|
+
if (!(component instanceof Container))
|
|
80
|
+
return undefined;
|
|
81
|
+
for (const child of component.children)
|
|
82
|
+
if (child instanceof MessageView)
|
|
83
|
+
return child;
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
/** Shared transcript notices stay in the group. The frame is the contract, not each custom type. */
|
|
87
|
+
function noticeContinuation(component) {
|
|
88
|
+
const view = messageView(component);
|
|
89
|
+
return view === undefined ? undefined : { component, summary: view.activitySummary };
|
|
90
|
+
}
|
|
74
91
|
class NotifyView extends Container {
|
|
75
92
|
#text = new Text("", 1, 0);
|
|
76
93
|
#context;
|
|
@@ -85,7 +102,7 @@ class NotifyView extends Container {
|
|
|
85
102
|
this.addChild(this.#text);
|
|
86
103
|
}
|
|
87
104
|
get summary() {
|
|
88
|
-
return this.#context.ui.theme.fg(this.#type === "info" ? "dim" : this.#type, toSingleRow(
|
|
105
|
+
return this.#context.ui.theme.fg(this.#type === "info" ? "dim" : this.#type, toSingleRow(this.#content));
|
|
89
106
|
}
|
|
90
107
|
get #content() {
|
|
91
108
|
const prefix = this.#type === "error" ? "Error: " : this.#type === "warning" ? "Warning: " : "";
|
|
@@ -100,45 +117,93 @@ class ToolGroup extends Container {
|
|
|
100
117
|
#header = new Text("", 0, 0);
|
|
101
118
|
#originals = new Container();
|
|
102
119
|
#context;
|
|
120
|
+
#redraw;
|
|
103
121
|
#members = [];
|
|
104
122
|
#expanded = false;
|
|
105
123
|
#renderedExpanded = false;
|
|
124
|
+
#open = new Set();
|
|
125
|
+
#hits = [];
|
|
106
126
|
constructor(context, redraw) {
|
|
107
127
|
super();
|
|
108
128
|
this.#context = context;
|
|
129
|
+
this.#redraw = redraw;
|
|
109
130
|
const heading = new MouseRegion(this.#header, (event) => {
|
|
110
131
|
if (event.type !== "click" || event.button !== "left")
|
|
111
132
|
return undefined;
|
|
112
133
|
this.#expanded = this.#context.ui.getToolsExpanded() ? false : !this.#expanded;
|
|
134
|
+
if (!this.#expanded)
|
|
135
|
+
this.#open.clear();
|
|
113
136
|
redraw();
|
|
114
137
|
return { handled: true, render: true };
|
|
115
138
|
});
|
|
116
139
|
const body = {
|
|
117
|
-
render: (width) =>
|
|
118
|
-
|
|
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
|
-
},
|
|
140
|
+
render: (width) => this.#renderBody(width),
|
|
141
|
+
handleMouse: (event) => this.#hit(event),
|
|
133
142
|
invalidate() { },
|
|
134
143
|
};
|
|
135
144
|
this.children = [new Spacer(1), heading, body];
|
|
136
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
|
+
}
|
|
137
204
|
update(members) {
|
|
138
205
|
this.#members = members;
|
|
139
|
-
this.#originals.children = members.map(({ component
|
|
140
|
-
if (name === undefined)
|
|
141
|
-
return component;
|
|
206
|
+
this.#originals.children = members.map(({ component }) => {
|
|
142
207
|
let offset = 0;
|
|
143
208
|
return {
|
|
144
209
|
render(width) {
|
|
@@ -173,7 +238,7 @@ class ToolGroup extends Container {
|
|
|
173
238
|
const heading = [...counts]
|
|
174
239
|
.map(([name, count]) => `${theme.fg("toolTitle", theme.bold(name))} ${theme.fg("dim", `×${count}`)}`)
|
|
175
240
|
.join(theme.fg("dim", " · "));
|
|
176
|
-
this.#header.setText(
|
|
241
|
+
this.#header.setText(fitRow(`${theme.fg("dim", this.#renderedExpanded ? "▾" : "▸")} ${heading}`, Math.max(0, width), "…"));
|
|
177
242
|
return super.render(width);
|
|
178
243
|
}
|
|
179
244
|
}
|
|
@@ -236,6 +301,13 @@ function attach(host, context) {
|
|
|
236
301
|
members.push({ component: child, summary: child.summary });
|
|
237
302
|
continue;
|
|
238
303
|
}
|
|
304
|
+
else {
|
|
305
|
+
const continuation = noticeContinuation(child);
|
|
306
|
+
if (continuation !== undefined) {
|
|
307
|
+
members.push(continuation);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
239
311
|
flush();
|
|
240
312
|
children.push(child);
|
|
241
313
|
}
|
package/dist/message-view.d.ts
CHANGED
|
@@ -12,4 +12,6 @@ export declare class MessageView implements Component {
|
|
|
12
12
|
render(width: number): string[];
|
|
13
13
|
invalidate(): void;
|
|
14
14
|
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined;
|
|
15
|
+
/** Single-row label used when this notice sits inside an activity group. */
|
|
16
|
+
get activitySummary(): string;
|
|
15
17
|
}
|
package/dist/message-view.js
CHANGED
|
@@ -125,13 +125,16 @@ export class MessageView {
|
|
|
125
125
|
this.toggle();
|
|
126
126
|
return { handled: true, render: true };
|
|
127
127
|
}
|
|
128
|
-
|
|
128
|
+
/** Single-row label used when this notice sits inside an activity group. */
|
|
129
|
+
get activitySummary() {
|
|
129
130
|
const { theme, header } = this.#spec;
|
|
130
131
|
const kind = theme.fg("dim", toSingleRow(header.kind));
|
|
131
132
|
const summaryRaw = header.summary === undefined ? "" : toSingleRow(header.summary);
|
|
132
133
|
const summary = summaryRaw === "" ? "" : theme.fg(header.tone ?? "dim", summaryRaw);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
return summary === "" ? kind : `${kind}${theme.fg("dim", " · ")}${summary}`;
|
|
135
|
+
}
|
|
136
|
+
#header(width, expanded) {
|
|
137
|
+
const pin = this.expandable ? `${this.#spec.theme.fg("dim", expanded ? "▾" : "▸")} ` : "";
|
|
138
|
+
return truncate(`${pin}${this.activitySummary}`, width);
|
|
136
139
|
}
|
|
137
140
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type SettingsProvider, type SettingsState } from "@hheei/pi-ext-core";
|
|
2
|
+
/** Missing or non-boolean values stay on; only an explicit false disables groups. */
|
|
3
|
+
export declare function activityGroupsEnabled(state: SettingsState | undefined): boolean;
|
|
4
|
+
export declare function createToolGroupSettings(options: {
|
|
5
|
+
readonly onEnabled: (enabled: boolean) => void;
|
|
6
|
+
}): SettingsProvider;
|
package/dist/settings.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createJsonSettingsStorage, } from "@hheei/pi-ext-core";
|
|
2
|
+
const GROUP = "toolGroups";
|
|
3
|
+
const FIELD = "enabled";
|
|
4
|
+
/** Missing or non-boolean values stay on; only an explicit false disables groups. */
|
|
5
|
+
export function activityGroupsEnabled(state) {
|
|
6
|
+
const value = state?.[GROUP]?.[FIELD];
|
|
7
|
+
return value !== false;
|
|
8
|
+
}
|
|
9
|
+
export function createToolGroupSettings(options) {
|
|
10
|
+
return {
|
|
11
|
+
id: "pi-ext-ui",
|
|
12
|
+
title: "Tool UI",
|
|
13
|
+
origin: "@hheei/pi-ext-ui",
|
|
14
|
+
groups: [
|
|
15
|
+
{
|
|
16
|
+
id: GROUP,
|
|
17
|
+
title: "Tool activity",
|
|
18
|
+
fields: [
|
|
19
|
+
{
|
|
20
|
+
id: FIELD,
|
|
21
|
+
label: "Activity groups",
|
|
22
|
+
type: "boolean",
|
|
23
|
+
defaultValue: true,
|
|
24
|
+
description: "Group consecutive tool, thinking, and notification rows in the TUI transcript.",
|
|
25
|
+
parse: (draft) => draft === "true",
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
storage: createJsonSettingsStorage({ group: GROUP }),
|
|
31
|
+
onChange(change) {
|
|
32
|
+
if (change.groupId === GROUP && change.fieldId === FIELD)
|
|
33
|
+
options.onEnabled(change.value === true);
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|