pi-extension-utils 0.7.7 → 0.7.9
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/src/pane/overlay.js +11 -0
- package/dist/src/reminders/host.js +17 -1
- package/dist/src/widgets/client.js +5 -2
- package/dist/src/widgets/host.js +19 -4
- package/docs/widgets.md +7 -1
- package/package.json +5 -5
package/dist/src/pane/overlay.js
CHANGED
|
@@ -223,8 +223,19 @@ export function paneOverlay(options) {
|
|
|
223
223
|
primaryState.cursor = selectableIndex;
|
|
224
224
|
primaryState.scrollOffset = selectableIndex;
|
|
225
225
|
};
|
|
226
|
+
let previousPrimaryKeys = [];
|
|
226
227
|
const computeSelectionFromRows = (primaryRows, bodyHeight) => {
|
|
227
228
|
applyInitialSelection(primaryRows);
|
|
229
|
+
// The cursor indexes the previous snapshot. Resolve its stable identity
|
|
230
|
+
// before interpreting that index against refreshed or reordered rows.
|
|
231
|
+
if (options.primary.mode === "cursor" && options.primary.selectionKey) {
|
|
232
|
+
const keys = primaryRows.map((row, index) => isSeparatorRow(row) ? undefined : selectionKeyFor(row, index));
|
|
233
|
+
const previousKey = previousPrimaryKeys[primaryState.cursor];
|
|
234
|
+
const nextIndex = previousKey === undefined ? -1 : keys.indexOf(previousKey);
|
|
235
|
+
if (nextIndex >= 0)
|
|
236
|
+
primaryState.cursor = nextIndex;
|
|
237
|
+
previousPrimaryKeys = keys;
|
|
238
|
+
}
|
|
228
239
|
const primaryMode = options.primary.mode ?? "scroll";
|
|
229
240
|
let selectedIndex = 0;
|
|
230
241
|
if (primaryMode === "cursor") {
|
|
@@ -256,17 +256,33 @@ function padToWidth(line, width) {
|
|
|
256
256
|
}
|
|
257
257
|
const SYSTEM_REMINDER_MIN_BODY_WIDTH = 76;
|
|
258
258
|
const SYSTEM_REMINDER_BODY_WIDTH_RATIO = 0.9;
|
|
259
|
+
/**
|
|
260
|
+
* Bordered transcript box for one reminder message.
|
|
261
|
+
*
|
|
262
|
+
* Content and details are fixed for the component's lifetime, so the laid-out lines depend only on
|
|
263
|
+
* width and theme colors. Lines are cached for the last width because the TUI renders every
|
|
264
|
+
* transcript component on each frame and large reminders are expensive to re-wrap. `invalidate()`
|
|
265
|
+
* drops the cache so theme changes take effect on the next render.
|
|
266
|
+
*/
|
|
259
267
|
class SystemReminderComponent {
|
|
260
268
|
content;
|
|
261
269
|
details;
|
|
262
270
|
theme;
|
|
271
|
+
cache;
|
|
263
272
|
constructor(content, details, theme) {
|
|
264
273
|
this.content = content;
|
|
265
274
|
this.details = details;
|
|
266
275
|
this.theme = theme;
|
|
267
276
|
}
|
|
268
|
-
invalidate() {
|
|
277
|
+
invalidate() {
|
|
278
|
+
this.cache = undefined;
|
|
279
|
+
}
|
|
269
280
|
render(width) {
|
|
281
|
+
if (this.cache?.width !== width)
|
|
282
|
+
this.cache = { width, lines: this.layout(width) };
|
|
283
|
+
return this.cache.lines;
|
|
284
|
+
}
|
|
285
|
+
layout(width) {
|
|
270
286
|
if (width < 3)
|
|
271
287
|
return [truncateToWidth("System Reminder", width)];
|
|
272
288
|
const availableBodyWidth = width - 2;
|
|
@@ -70,11 +70,14 @@ export function connectWidgetCoordinator(pi, opts) {
|
|
|
70
70
|
mount.disposed = false;
|
|
71
71
|
mount.tui?.requestRender?.();
|
|
72
72
|
}
|
|
73
|
+
function fallbackKey(record) {
|
|
74
|
+
return `pi-extension-utils-fallback:${JSON.stringify([clientId, record.placement, record.key])}`;
|
|
75
|
+
}
|
|
73
76
|
function clearFallback(record) {
|
|
74
|
-
opts.ctx.ui.setWidget(record
|
|
77
|
+
opts.ctx.ui.setWidget(fallbackKey(record), undefined, { placement: record.placement });
|
|
75
78
|
}
|
|
76
79
|
function restoreFallback(record) {
|
|
77
|
-
opts.ctx.ui.setWidget(record
|
|
80
|
+
opts.ctx.ui.setWidget(fallbackKey(record), record.fallbackFactory, { placement: record.placement });
|
|
78
81
|
}
|
|
79
82
|
function attach() {
|
|
80
83
|
if (disposed || coordinated)
|
package/dist/src/widgets/host.js
CHANGED
|
@@ -114,7 +114,7 @@ function createHostComponent(tui, theme, records, hidden) {
|
|
|
114
114
|
// whole agent: render failures drop that widget until its client
|
|
115
115
|
// re-registers. Records are replaced wholesale on every re-registration
|
|
116
116
|
// and remount, so a dropped component cannot linger past the handshake.
|
|
117
|
-
|
|
117
|
+
let healthy = [];
|
|
118
118
|
if (!hidden) {
|
|
119
119
|
for (const record of records) {
|
|
120
120
|
try {
|
|
@@ -128,19 +128,26 @@ function createHostComponent(tui, theme, records, hidden) {
|
|
|
128
128
|
return {
|
|
129
129
|
render(width) {
|
|
130
130
|
const lines = [];
|
|
131
|
+
const failed = new Set();
|
|
131
132
|
for (const component of [...healthy]) {
|
|
133
|
+
if (failed.has(component))
|
|
134
|
+
continue;
|
|
132
135
|
try {
|
|
133
136
|
lines.push(...component.render(width));
|
|
134
137
|
}
|
|
135
138
|
catch (error) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
+
failed.add(component);
|
|
140
|
+
healthy = healthy.filter((child) => child !== component);
|
|
141
|
+
disposeChild(component);
|
|
139
142
|
console.warn(`pi-extension-utils: widget render threw, dropping it until re-registration`, error instanceof Error ? error.message : error);
|
|
140
143
|
}
|
|
141
144
|
}
|
|
142
145
|
return lines;
|
|
143
146
|
},
|
|
147
|
+
dispose() {
|
|
148
|
+
for (const component of new Set(healthy.splice(0)))
|
|
149
|
+
disposeChild(component);
|
|
150
|
+
},
|
|
144
151
|
invalidate() {
|
|
145
152
|
for (const component of healthy) {
|
|
146
153
|
try {
|
|
@@ -153,6 +160,14 @@ function createHostComponent(tui, theme, records, hidden) {
|
|
|
153
160
|
},
|
|
154
161
|
};
|
|
155
162
|
}
|
|
163
|
+
function disposeChild(component) {
|
|
164
|
+
try {
|
|
165
|
+
component.dispose?.();
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
console.warn("pi-extension-utils: widget dispose threw", error instanceof Error ? error.message : error);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
156
171
|
function hostKey(placement) {
|
|
157
172
|
return `${HOST_WIDGET_PREFIX}-${placement}`;
|
|
158
173
|
}
|
package/docs/widgets.md
CHANGED
|
@@ -48,9 +48,15 @@ When any client holds a fullscreen lease, coordinated widgets are hidden and res
|
|
|
48
48
|
await client.ui.fullscreen((tui, theme, keybindings, done) => new MyComponent(tui, theme, done));
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
+
## Component ownership
|
|
52
|
+
|
|
53
|
+
A widget factory transfers ownership of its returned component to the mount. Pi disposes the old mount before calling a replacement factory. Return a fresh component on each mount; do not reuse a disposed component or share one across placements or independent mounts.
|
|
54
|
+
|
|
55
|
+
The host remounts a placement when its registrations or fullscreen visibility change. It disposes each owned child once on replacement, removal, hiding, or Pi UI teardown. A child whose render throws is detached and disposed immediately; a throwing disposer does not prevent sibling cleanup. If a factory throws before returning a component, it must clean up any resources it already created.
|
|
56
|
+
|
|
51
57
|
## Fallback
|
|
52
58
|
|
|
53
|
-
If the host is not ready yet, each logical widget registers one stable proxy through the extension's own `ctx.ui.setWidget`. Later `widgets.set` calls replace the proxy's delegated component and request a redraw without re-registering the raw widget, preserving cross-extension insertion order on Pi versions that delete before setting. Set and fullscreen-acquire activity also retry the hello handshake without a polling timer. When the host announces readiness, the client clears the fallback widget and re-registers through the coordinator.
|
|
59
|
+
If the host is not ready yet, each logical widget registers one stable proxy through the extension's own `ctx.ui.setWidget`. Its internal raw key includes the client ID, placement, and logical key, so separate clients and placements can use the same logical key. Later `widgets.set` calls replace the proxy's delegated component and request a redraw without re-registering the raw widget, preserving cross-extension insertion order on Pi versions that delete before setting. Set and fullscreen-acquire activity also retry the hello handshake without a polling timer. When the host announces readiness, the client clears the fallback widget and re-registers through the coordinator.
|
|
54
60
|
|
|
55
61
|
## Example
|
|
56
62
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-extension-utils",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.9",
|
|
4
4
|
"description": "Shared Pi extension utilities for coordinated widgets, fullscreen leases, and logging.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/src/index.js",
|
|
@@ -43,10 +43,10 @@
|
|
|
43
43
|
"@earendil-works/pi-tui": "*"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@earendil-works/pi-agent-core": "
|
|
47
|
-
"@earendil-works/pi-ai": "
|
|
48
|
-
"@earendil-works/pi-coding-agent": "
|
|
49
|
-
"@earendil-works/pi-tui": "
|
|
46
|
+
"@earendil-works/pi-agent-core": "0.87.1",
|
|
47
|
+
"@earendil-works/pi-ai": "0.87.1",
|
|
48
|
+
"@earendil-works/pi-coding-agent": "0.87.1",
|
|
49
|
+
"@earendil-works/pi-tui": "0.87.1",
|
|
50
50
|
"@types/node": "^24.0.0",
|
|
51
51
|
"typescript": "^6.0.3"
|
|
52
52
|
},
|