@mjasnikovs/pi-task 0.13.4 → 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.
- package/dist/remote/bridge.js +5 -4
- package/dist/remote/broadcast.d.ts +0 -1
- package/dist/remote/broadcast.js +0 -7
- package/dist/remote/events.js +5 -5
- package/dist/remote/server.js +0 -16
- package/dist/remote/ui.js +13 -5
- package/dist/task/auto-orchestrator.js +32 -6
- package/dist/task/parsers.js +2 -3
- package/dist/task/phases.js +11 -0
- package/package.json +1 -1
package/dist/remote/bridge.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { broadcast as wsBroadcast
|
|
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 (
|
|
61
|
-
|
|
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;
|
package/dist/remote/broadcast.js
CHANGED
package/dist/remote/events.js
CHANGED
|
@@ -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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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') {
|
package/dist/remote/server.js
CHANGED
|
@@ -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
|
});
|
package/dist/remote/ui.js
CHANGED
|
@@ -202,6 +202,10 @@ export function html(wsUrl) {
|
|
|
202
202
|
font-family: inherit; line-height: 1.5; resize: vertical; margin-bottom: 4px; }
|
|
203
203
|
#prompt-card .row { display: flex; gap: 8px; margin-top: 12px; align-items: stretch;
|
|
204
204
|
flex-wrap: wrap; }
|
|
205
|
+
/* Recommendation answers can be long sentences, so stack them as a readable list. */
|
|
206
|
+
#prompt-card .row.stacked { flex-direction: column; align-items: stretch; }
|
|
207
|
+
#prompt-card .row.stacked button { flex: none; text-align: left; }
|
|
208
|
+
#prompt-card .row.stacked button.cancel { align-self: center; text-align: center; }
|
|
205
209
|
#prompt-card button { padding: 11px 16px; border-radius: 8px; border: none; cursor: pointer;
|
|
206
210
|
font-family: inherit; font-size: 14px; font-weight: 600; transition: filter .15s ease; }
|
|
207
211
|
#prompt-card button:hover { filter: brightness(1.08); }
|
|
@@ -772,7 +776,8 @@ export function html(wsUrl) {
|
|
|
772
776
|
return btn;
|
|
773
777
|
}
|
|
774
778
|
|
|
775
|
-
function renderButtons(buttons) {
|
|
779
|
+
function renderButtons(buttons, stacked) {
|
|
780
|
+
promptButtons.className = stacked ? 'row stacked' : 'row';
|
|
776
781
|
promptButtons.innerHTML = '';
|
|
777
782
|
for (let i = 0; i < buttons.length; i++) promptButtons.appendChild(buttons[i]);
|
|
778
783
|
promptButtons.appendChild(makeCancelBtn());
|
|
@@ -800,11 +805,14 @@ export function html(wsUrl) {
|
|
|
800
805
|
promptRec.style.display = 'none';
|
|
801
806
|
buttons.push(makeBtn(activeRecommended, 'primary', function () { answer(activeRecommended); }));
|
|
802
807
|
buttons.push(makeBtn(activeRecommended2, 'secondary', function () { answer(activeRecommended2); }));
|
|
803
|
-
|
|
804
|
-
//
|
|
805
|
-
|
|
806
|
-
|
|
808
|
+
buttons.push(makeBtn('✎ Manual answer', 'secondary', function () { showManualEntry(); }));
|
|
809
|
+
// Answer buttons hold full sentences — stack them so long text stays readable.
|
|
810
|
+
renderButtons(buttons, true);
|
|
811
|
+
return;
|
|
807
812
|
}
|
|
813
|
+
// Single recommendation: show it in the green panel.
|
|
814
|
+
promptRec.style.display = 'block';
|
|
815
|
+
buttons.push(makeBtn('✓ Accept', 'primary', function () { answer(activeRecommended); }));
|
|
808
816
|
buttons.push(makeBtn('✎ Manual answer', 'secondary', function () { showManualEntry(); }));
|
|
809
817
|
renderButtons(buttons);
|
|
810
818
|
}
|
|
@@ -12,7 +12,7 @@ import { parseClarifyList, deriveTitle } from './parsers.js';
|
|
|
12
12
|
import { renderInlineMarkdown, stripInlineMarkdown } from './inline-markdown.js';
|
|
13
13
|
import { AUTO_CLARIFY_PROMPT, AUTO_DECOMPOSE_PROMPT } from './auto-prompts.js';
|
|
14
14
|
import { allocateAutoId, buildAutoBody, parseDecomposeList, parseTaskList, checkOffTask, stampTaskInProgress, findResumableAuto } from './auto-io.js';
|
|
15
|
-
import { writeTaskFile, readTaskFile, updateTaskFrontMatter } from './task-io.js';
|
|
15
|
+
import { writeTaskFile, readTaskFile, updateTaskFrontMatter, taskFilePath } from './task-io.js';
|
|
16
16
|
import { gitCommitAll } from './auto-commit.js';
|
|
17
17
|
import { runPhaseChild, USER_CANCELLED } from './child-runner.js';
|
|
18
18
|
import { SessionUI, registerBridgeCommand } from '../remote/bridge.js';
|
|
@@ -217,12 +217,25 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
217
217
|
active.ui.notify(`${id}: task ${next.index + 1}/${entries.length} — ${next.title}`, 'info');
|
|
218
218
|
// If this entry already has a stamped inner id, it was started in a
|
|
219
219
|
// previous (interrupted) run — resume it from its saved phase rather
|
|
220
|
-
// than spawning a fresh task.
|
|
221
|
-
//
|
|
222
|
-
//
|
|
220
|
+
// than spawning a fresh task. But the stamped inner file can be gone
|
|
221
|
+
// (deleted, or never written because allocation was interrupted), and
|
|
222
|
+
// resuming a missing file throws ENOENT deep in the runner — which used
|
|
223
|
+
// to take pi down. So verify the file exists and otherwise fall back to
|
|
224
|
+
// a fresh start. Either way an unstamped/restarted entry is (re)stamped
|
|
225
|
+
// the moment its inner id exists, keeping the next interruption
|
|
226
|
+
// resumable. This mirrors /task-resume's continue-don't-restart.
|
|
227
|
+
let resumeId = next.producedId;
|
|
228
|
+
if (resumeId) {
|
|
229
|
+
try {
|
|
230
|
+
await fsp.access(taskFilePath(cwd, resumeId));
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
resumeId = undefined;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
223
236
|
const res = await deps.runTask(active, cwd, next.title, {
|
|
224
|
-
resumeId
|
|
225
|
-
onStart:
|
|
237
|
+
resumeId,
|
|
238
|
+
onStart: resumeId ? undefined : (innerId => stampTaskInProgress(cwd, id, next.index, innerId, next.title))
|
|
226
239
|
});
|
|
227
240
|
active = res.ctx ?? active;
|
|
228
241
|
if (res.sessionCancelled) {
|
|
@@ -251,6 +264,19 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
251
264
|
}
|
|
252
265
|
}
|
|
253
266
|
}
|
|
267
|
+
catch (err) {
|
|
268
|
+
// Safety net: no failure inside the loop may propagate out of runAutoLoop,
|
|
269
|
+
// because the resume handler doesn't wrap this call and an unhandled
|
|
270
|
+
// rejection crashes pi outright. Convert it into a failed run + notify,
|
|
271
|
+
// mirroring the in-loop per-task failure path.
|
|
272
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
273
|
+
if (msg === USER_CANCELLED) {
|
|
274
|
+
active.ui.notify(`${id} cancelled — resume with /task-auto-resume.`, 'warning');
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
await updateTaskFrontMatter(cwd, id, { state: 'failed' }).catch(() => { });
|
|
278
|
+
active.ui.notify(`${id} stopped: ${msg} — fix and run /task-auto-resume.`, 'error');
|
|
279
|
+
}
|
|
254
280
|
finally {
|
|
255
281
|
cancelRequested = false;
|
|
256
282
|
}
|
package/dist/task/parsers.js
CHANGED
|
@@ -233,7 +233,6 @@ export function validateSpecShape(spec) {
|
|
|
233
233
|
// ─── Title derivation ────────────────────────────────────────────────────────
|
|
234
234
|
export function deriveTitle(refined) {
|
|
235
235
|
const stripBold = (s) => s.replace(/^\*+|\*+$/g, '').trim();
|
|
236
|
-
const truncate = (s) => (s.length > 119 ? s.slice(0, 119) + '…' : s);
|
|
237
236
|
const lines = refined.split('\n');
|
|
238
237
|
for (let i = 0; i < lines.length; i++) {
|
|
239
238
|
const stripped = stripBold(lines[i].trim().replace(/^#+\s+/, ''));
|
|
@@ -245,7 +244,7 @@ export function deriveTitle(refined) {
|
|
|
245
244
|
const headerCheck = stripBold(line.replace(/^#+\s+/, ''));
|
|
246
245
|
if (/^(CONSTRAINTS|KNOWN-UNKNOWNS)\s*:?\s*$/i.test(headerCheck))
|
|
247
246
|
break;
|
|
248
|
-
return
|
|
247
|
+
return line;
|
|
249
248
|
}
|
|
250
249
|
break;
|
|
251
250
|
}
|
|
@@ -257,7 +256,7 @@ export function deriveTitle(refined) {
|
|
|
257
256
|
line = stripBold(line.replace(/^#+\s+/, '')).replace(/^GOAL\s*:?\s*/i, '');
|
|
258
257
|
if (line.length === 0)
|
|
259
258
|
continue;
|
|
260
|
-
return
|
|
259
|
+
return line;
|
|
261
260
|
}
|
|
262
261
|
return '(untitled)';
|
|
263
262
|
}
|
package/dist/task/phases.js
CHANGED
|
@@ -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.
|
|
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",
|