pi-extension-utils 0.7.7 → 0.7.8

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.
@@ -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.key, undefined, { placement: record.placement });
77
+ opts.ctx.ui.setWidget(fallbackKey(record), undefined, { placement: record.placement });
75
78
  }
76
79
  function restoreFallback(record) {
77
- opts.ctx.ui.setWidget(record.key, record.fallbackFactory, { placement: record.placement });
80
+ opts.ctx.ui.setWidget(fallbackKey(record), record.fallbackFactory, { placement: record.placement });
78
81
  }
79
82
  function attach() {
80
83
  if (disposed || coordinated)
@@ -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
- const healthy = [];
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
- const failedIndex = healthy.indexOf(component);
137
- if (failedIndex >= 0)
138
- healthy.splice(failedIndex, 1);
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.7",
3
+ "version": "0.7.8",
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": "^0.75.4",
47
- "@earendil-works/pi-ai": "^0.75.4",
48
- "@earendil-works/pi-coding-agent": "^0.75.4",
49
- "@earendil-works/pi-tui": "^0.79.3",
46
+ "@earendil-works/pi-agent-core": "^0.84.4",
47
+ "@earendil-works/pi-ai": "^0.84.4",
48
+ "@earendil-works/pi-coding-agent": "^0.84.4",
49
+ "@earendil-works/pi-tui": "^0.84.4",
50
50
  "@types/node": "^24.0.0",
51
51
  "typescript": "^6.0.3"
52
52
  },