@mjasnikovs/pi-task 0.39.2 → 0.39.3

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.
@@ -115,8 +115,6 @@ export function buildWidgetData(s) {
115
115
  }
116
116
  // ─── Widget lifecycle ────────────────────────────────────────────────────────
117
117
  export function startWidget(ctx, getState) {
118
- if (!ctx.hasUI)
119
- return () => { };
120
118
  // pi's `ctx.ui` getter calls `runner.assertActive()`, which THROWS once the
121
119
  // ctx goes stale (/reload, session replacement) — so the theme read belongs
122
120
  // INSIDE the guard, not one line above it. render() runs from a timer, and a
@@ -128,7 +126,12 @@ export function startWidget(ctx, getState) {
128
126
  if (stale)
129
127
  return;
130
128
  const s = getState();
131
- const plain = s ? buildWidgetLines(s, undefined) : undefined; // un-themed for the wire; needs no ctx
129
+ // The wire half FIRST, and unguarded: it needs no ctx, so gating it on a
130
+ // local terminal that may not exist would leave a headless host's browser
131
+ // with no phase, no progress and no failure flash.
132
+ setTaskWidget(s ? buildWidgetLines(s, undefined) : undefined, s ? buildWidgetData(s) : null);
133
+ if (!ctx.hasUI)
134
+ return;
132
135
  try {
133
136
  const lines = s ? buildWidgetLines(s, ctx.ui.theme) : undefined;
134
137
  ctx.ui.setWidget(WIDGET_KEY, lines);
@@ -136,9 +139,7 @@ export function startWidget(ctx, getState) {
136
139
  catch {
137
140
  stale = true;
138
141
  clearInterval(timer);
139
- return;
140
142
  }
141
- setTaskWidget(plain, s ? buildWidgetData(s) : null);
142
143
  };
143
144
  // The timer is created BEFORE the first render so `timer` is always bound
144
145
  // when render's catch reaches for it. Calling render() first instead would
@@ -208,16 +209,15 @@ export function buildAutoLoaderData(s) {
208
209
  * (returns a no-op disposer) when there's no UI.
209
210
  */
210
211
  export function startAutoLoader(ctx, getState) {
211
- if (!ctx.hasUI)
212
- return () => { };
213
- // Same staleness hazard as startWidget: the theme read must sit inside the
214
- // guard, and a stale ctx stops the timer rather than throwing every tick.
212
+ // Same staleness hazard as startWidget, and the same wire-half-first order.
215
213
  let stale = false;
216
214
  const render = () => {
217
215
  if (stale)
218
216
  return;
219
217
  const s = getState();
220
- const plain = s ? buildAutoLoaderLines(s, undefined) : undefined; // needs no ctx
218
+ setTaskWidget(s ? buildAutoLoaderLines(s, undefined) : undefined, s ? buildAutoLoaderData(s) : null);
219
+ if (!ctx.hasUI)
220
+ return;
221
221
  try {
222
222
  const lines = s ? buildAutoLoaderLines(s, ctx.ui.theme) : undefined;
223
223
  ctx.ui.setWidget(AUTO_WIDGET_KEY, lines);
@@ -225,9 +225,7 @@ export function startAutoLoader(ctx, getState) {
225
225
  catch {
226
226
  stale = true;
227
227
  clearInterval(timer);
228
- return;
229
228
  }
230
- setTaskWidget(plain, s ? buildAutoLoaderData(s) : null);
231
229
  };
232
230
  // The timer is created BEFORE the first render so `timer` is always bound
233
231
  // when render's catch reaches for it. Calling render() first instead would
@@ -277,40 +275,33 @@ export function buildImplData(s) {
277
275
  return d;
278
276
  }
279
277
  export function flashTerminalWidget(ctx, state, taskId, reason) {
280
- if (!ctx.hasUI)
281
- return;
282
- // Same staleness hazard as the render timers: bail rather than throw.
283
- let theme;
284
- try {
285
- theme = ctx.ui.theme;
286
- }
287
- catch {
288
- return;
289
- }
290
- let line;
291
- let clearMs;
292
- if (state === 'cancelled') {
293
- line = theme.fg('warning', `⚠ ${taskId} cancelled`);
294
- clearMs = NOTIFY_CLEAR_MS;
295
- }
296
- else {
297
- line = theme.fg('error', `✘ ${taskId} failed${reason ? ': ' + reason : ''}`);
298
- clearMs = FAIL_CLEAR_MS;
299
- }
300
278
  const plainLine = state === 'cancelled' ?
301
279
  `⚠ ${taskId} cancelled`
302
280
  : `✘ ${taskId} failed${reason ? ': ' + reason : ''}`;
303
- try {
304
- ctx.ui.setWidget(WIDGET_KEY, [line]);
305
- setTaskWidget([plainLine]);
306
- }
307
- catch {
308
- /* stale ctx */
281
+ const clearMs = state === 'cancelled' ? NOTIFY_CLEAR_MS : FAIL_CLEAR_MS;
282
+ setTaskWidget([plainLine]);
283
+ // The wire half is already done, so a missing or stale local ctx no longer
284
+ // costs the browser the flash as well. Same staleness hazard as the render
285
+ // timers: bail rather than throw.
286
+ if (ctx.hasUI) {
287
+ try {
288
+ const theme = ctx.ui.theme;
289
+ const tint = state === 'cancelled' ? 'warning' : 'error';
290
+ ctx.ui.setWidget(WIDGET_KEY, [theme.fg(tint, plainLine)]);
291
+ }
292
+ catch {
293
+ /* stale ctx */
294
+ }
309
295
  }
310
296
  setTimeout(() => {
297
+ // Wire first, for the same reason as the flash above: a ctx that went
298
+ // stale in the meantime would otherwise leave the browser showing the
299
+ // flash for the rest of the session.
300
+ setTaskWidget(undefined);
301
+ if (!ctx.hasUI)
302
+ return;
311
303
  try {
312
304
  ctx.ui.setWidget(WIDGET_KEY, undefined);
313
- setTaskWidget(undefined);
314
305
  }
315
306
  catch {
316
307
  /* stale ctx */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.39.2",
3
+ "version": "0.39.3",
4
4
  "description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",