@mjasnikovs/pi-task 0.13.5 → 0.13.6

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.
@@ -1,4 +1,4 @@
1
- import { broadcast as wsBroadcast, hasConnectedClients } from './broadcast.js';
1
+ import { broadcast as wsBroadcast } from './broadcast.js';
2
2
  import { pushNotify } from './push.js';
3
3
  import { setPrompt, clearPrompt } from './session-state.js';
4
4
  const g = globalThis;
@@ -56,9 +56,10 @@ export class SessionUI {
56
56
  allowSkip: spec.allowSkip
57
57
  };
58
58
  setPrompt(prompt);
59
- // Reaches a backgrounded/suspended phone, which the in-page UI can't.
60
- if (!hasConnectedClients())
61
- void pushNotify('pi needs your input', spec.question, 'pi-prompt').catch(() => { });
59
+ // Reaches a backgrounded/suspended phone, which the in-page UI can't. The
60
+ // service worker drops the banner if a window is visible+focused (sw.ts),
61
+ // so we always push and let delivery-time visibility decide.
62
+ void pushNotify('pi needs your input', spec.question, 'pi-prompt').catch(() => { });
62
63
  // Local: resolves to a value/undefined, or undefined on abort. Swallow
63
64
  // the rejection some implementations throw on abort so it never leaks.
64
65
  const local = this.ctx.hasUI ?
@@ -3,4 +3,3 @@ export declare function addClient(ws: WebSocket): void;
3
3
  export declare function removeClient(ws: WebSocket): void;
4
4
  export declare function broadcast(msg: unknown): void;
5
5
  export declare function sendTo(ws: WebSocket, msg: unknown): void;
6
- export declare function hasConnectedClients(): boolean;
@@ -22,10 +22,3 @@ export function sendTo(ws, msg) {
22
22
  ws.send(JSON.stringify(msg));
23
23
  }
24
24
  }
25
- export function hasConnectedClients() {
26
- for (const ws of clients) {
27
- if (ws.readyState === ws.OPEN)
28
- return true;
29
- }
30
- return false;
31
- }
@@ -1,7 +1,6 @@
1
1
  import { setAgentIdle } from './state.js';
2
2
  import { pushNotify } from './push.js';
3
3
  import { publishNotify } from './bridge.js';
4
- import { hasConnectedClients } from './broadcast.js';
5
4
  import { agentStart, appendText, textEnd, startTool, updateTool, endTool, agentEnd, addUserTurn, addError, addSystemNote } from './session-state.js';
6
5
  /** Mirror pi agent events into the authoritative SessionState. Each handler
7
6
  * drives a mutator, which updates the snapshot AND broadcasts the live delta. */
@@ -23,8 +22,10 @@ export function setupEvents(pi) {
23
22
  if (errorMessage || ae.reason === 'error') {
24
23
  const message = errorMessage || 'Request failed';
25
24
  addError(message);
26
- if (!hasConnectedClients())
27
- void pushNotify('Agent error', message, 'pi-error').catch(() => { });
25
+ // Always push; the service worker suppresses the banner when a
26
+ // window is actually visible+focused (sw.ts), which is the only
27
+ // reliable foreground signal — an open WebSocket is not one.
28
+ void pushNotify('Agent error', message, 'pi-error').catch(() => { });
28
29
  }
29
30
  }
30
31
  });
@@ -43,8 +44,7 @@ export function setupEvents(pi) {
43
44
  pi.on('agent_end', (_event, ctx) => {
44
45
  setAgentIdle(true);
45
46
  agentEnd(ctx.getContextUsage());
46
- if (!hasConnectedClients())
47
- void pushNotify('Task finished', '', 'pi-end').catch(() => { });
47
+ void pushNotify('Task finished', '', 'pi-end').catch(() => { });
48
48
  });
49
49
  pi.on('input', (event, _ctx) => {
50
50
  if (event.source === 'interactive' && typeof event.text === 'string') {
@@ -121,21 +121,6 @@ export async function startServer(onMessage, getHtml) {
121
121
  handle.onFirstConnect = null;
122
122
  // One authoritative snapshot — the client replaces its whole view with it.
123
123
  sendTo(ws, snapshot());
124
- // Heartbeat: ping every 30 s; terminate if the client doesn't respond.
125
- // This catches phones that close/sleep without sending a TCP FIN, so the
126
- // server's client-set stays accurate and push notifications fire correctly.
127
- let alive = true;
128
- const heartbeat = setInterval(() => {
129
- if (!alive) {
130
- ws.terminate();
131
- return;
132
- }
133
- alive = false;
134
- ws.ping();
135
- }, 10_000);
136
- ws.on('pong', () => {
137
- alive = true;
138
- });
139
124
  ws.on('message', data => {
140
125
  let msg;
141
126
  try {
@@ -157,7 +142,6 @@ export async function startServer(onMessage, getHtml) {
157
142
  onMessage(msg.text);
158
143
  });
159
144
  ws.on('close', () => {
160
- clearInterval(heartbeat);
161
145
  removeClient(ws);
162
146
  });
163
147
  });
@@ -397,12 +397,23 @@ export async function phaseGrill(deps, ctx, widgetState, refined, research) {
397
397
  if (a === undefined)
398
398
  throw new Error(USER_CANCELLED);
399
399
  const typed = a.trim();
400
+ // Two-option mode labels the choices "A:"/"B:", so a user (local TUI
401
+ // or remote "Manual answer") naturally types the bare letter to pick.
402
+ // Map it back to the option's full text — storing the literal "A"
403
+ // leaves the next grill-gen call a dangling reference it can't decode.
404
+ const twoOption = plainSuggested !== undefined && plainAlt !== undefined;
400
405
  if (typed.length === 0 && plainSuggested) {
401
406
  answer = plainSuggested;
402
407
  }
403
408
  else if (typed.length === 0) {
404
409
  answer = '(skipped)';
405
410
  }
411
+ else if (twoOption && /^a[.)]?$/i.test(typed)) {
412
+ answer = plainSuggested;
413
+ }
414
+ else if (twoOption && /^b[.)]?$/i.test(typed)) {
415
+ answer = plainAlt;
416
+ }
406
417
  else {
407
418
  answer = typed;
408
419
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.13.5",
3
+ "version": "0.13.6",
4
4
  "description": "Deterministic spec-orchestration for local models, with a bundled real-time remote web view and web/docs/fetch/worker subagent tools.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",