@mjasnikovs/pi-task 0.15.2 → 0.16.0
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/task/impl-widget.d.ts +40 -0
- package/dist/task/impl-widget.js +131 -0
- package/dist/task/orchestrator.d.ts +6 -1
- package/dist/task/orchestrator.js +28 -3
- package/dist/task/widget.d.ts +10 -0
- package/dist/task/widget.js +15 -0
- package/package.json +1 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Implementation-turn status widget.
|
|
3
|
+
*
|
|
4
|
+
* The phase widget (widget.ts) is disposed at spec-handoff, so the host agent's
|
|
5
|
+
* implementation turn — the longest, most visible part of a run — otherwise shows
|
|
6
|
+
* only pi's bare "⠸ Working…" indicator. This module keeps the SAME rich status
|
|
7
|
+
* block alive across that turn (task id · implementing/elapsed/context bar · ↳ last
|
|
8
|
+
* tool), driven by the host's own live `ctx.getContextUsage()`.
|
|
9
|
+
*
|
|
10
|
+
* It is event-driven rather than poll-wrapped because the two delivery paths differ:
|
|
11
|
+
*
|
|
12
|
+
* • /task (fire-and-forget): the command returns right after `sendUserMessage`,
|
|
13
|
+
* so the host runs the impl turn AFTER our code is gone — only an `agent_start`
|
|
14
|
+
* handler can pick it up. Armed one-shot: `agent_end` disarms it.
|
|
15
|
+
* • /task-auto (awaited): the caller blocks across `waitForIdle` plus any
|
|
16
|
+
* compaction-resume / steer turns. Armed sticky: each `agent_start` re-shows the
|
|
17
|
+
* widget, `agent_end` only hides it between turns, and the caller disarms once
|
|
18
|
+
* the whole implementation phase has settled.
|
|
19
|
+
*
|
|
20
|
+
* Only one task runs at a time (single active task), so a single module-level slot
|
|
21
|
+
* is sufficient.
|
|
22
|
+
*/
|
|
23
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
24
|
+
export interface ImplWidgetMeta {
|
|
25
|
+
taskId: string;
|
|
26
|
+
title: string;
|
|
27
|
+
label?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Arm the implementation widget just before the spec is handed off. `oneShot`
|
|
31
|
+
* true (fire-and-forget /task) lets `agent_end` disarm it after the single turn;
|
|
32
|
+
* false (awaited /task-auto) keeps it armed until `disarmImplWidget` is called.
|
|
33
|
+
*/
|
|
34
|
+
export declare function armImplWidget(meta: ImplWidgetMeta, opts: {
|
|
35
|
+
oneShot: boolean;
|
|
36
|
+
}): void;
|
|
37
|
+
/** Tear down the widget and clear the armed slot (sticky/awaited path). */
|
|
38
|
+
export declare function disarmImplWidget(): void;
|
|
39
|
+
/** Wire the agent-lifecycle handlers that drive the widget. Call once at setup. */
|
|
40
|
+
export declare function setupImplWidget(pi: ExtensionAPI): void;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Implementation-turn status widget.
|
|
3
|
+
*
|
|
4
|
+
* The phase widget (widget.ts) is disposed at spec-handoff, so the host agent's
|
|
5
|
+
* implementation turn — the longest, most visible part of a run — otherwise shows
|
|
6
|
+
* only pi's bare "⠸ Working…" indicator. This module keeps the SAME rich status
|
|
7
|
+
* block alive across that turn (task id · implementing/elapsed/context bar · ↳ last
|
|
8
|
+
* tool), driven by the host's own live `ctx.getContextUsage()`.
|
|
9
|
+
*
|
|
10
|
+
* It is event-driven rather than poll-wrapped because the two delivery paths differ:
|
|
11
|
+
*
|
|
12
|
+
* • /task (fire-and-forget): the command returns right after `sendUserMessage`,
|
|
13
|
+
* so the host runs the impl turn AFTER our code is gone — only an `agent_start`
|
|
14
|
+
* handler can pick it up. Armed one-shot: `agent_end` disarms it.
|
|
15
|
+
* • /task-auto (awaited): the caller blocks across `waitForIdle` plus any
|
|
16
|
+
* compaction-resume / steer turns. Armed sticky: each `agent_start` re-shows the
|
|
17
|
+
* widget, `agent_end` only hides it between turns, and the caller disarms once
|
|
18
|
+
* the whole implementation phase has settled.
|
|
19
|
+
*
|
|
20
|
+
* Only one task runs at a time (single active task), so a single module-level slot
|
|
21
|
+
* is sufficient.
|
|
22
|
+
*/
|
|
23
|
+
import { WIDGET_KEY, WIDGET_REFRESH_MS, buildImplLines } from './widget.js';
|
|
24
|
+
import { setTaskWidget } from '../remote/session-state.js';
|
|
25
|
+
let armed = null;
|
|
26
|
+
let lastLine;
|
|
27
|
+
let timer = null;
|
|
28
|
+
let activeCtx = null;
|
|
29
|
+
/** Map the host's live context usage to the widget snapshot, or undefined when the
|
|
30
|
+
* token count is unknown (e.g. right after compaction, before the next response). */
|
|
31
|
+
function snapshot(ctx) {
|
|
32
|
+
const u = ctx.getContextUsage?.();
|
|
33
|
+
if (!u || u.tokens == null)
|
|
34
|
+
return undefined;
|
|
35
|
+
return { tokens: u.tokens, contextWindow: u.contextWindow, percent: u.percent ?? 0 };
|
|
36
|
+
}
|
|
37
|
+
function render() {
|
|
38
|
+
if (!armed || !activeCtx)
|
|
39
|
+
return;
|
|
40
|
+
const ctx = activeCtx;
|
|
41
|
+
const state = {
|
|
42
|
+
taskId: armed.meta.taskId,
|
|
43
|
+
title: armed.meta.title,
|
|
44
|
+
label: armed.meta.label,
|
|
45
|
+
startedAt: armed.startedAt,
|
|
46
|
+
lastLine,
|
|
47
|
+
contextUsage: snapshot(ctx)
|
|
48
|
+
};
|
|
49
|
+
try {
|
|
50
|
+
ctx.ui.setWidget(WIDGET_KEY, buildImplLines(state, ctx.ui.theme));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
/* stale ctx */
|
|
54
|
+
}
|
|
55
|
+
setTaskWidget(buildImplLines(state, undefined)); // un-themed for the wire
|
|
56
|
+
}
|
|
57
|
+
function startTimer() {
|
|
58
|
+
if (timer)
|
|
59
|
+
return;
|
|
60
|
+
render();
|
|
61
|
+
timer = setInterval(render, WIDGET_REFRESH_MS);
|
|
62
|
+
timer.unref?.();
|
|
63
|
+
}
|
|
64
|
+
function stopTimer() {
|
|
65
|
+
if (timer) {
|
|
66
|
+
clearInterval(timer);
|
|
67
|
+
timer = null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function clearWidget() {
|
|
71
|
+
try {
|
|
72
|
+
activeCtx?.ui.setWidget(WIDGET_KEY, undefined);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
/* stale ctx */
|
|
76
|
+
}
|
|
77
|
+
setTaskWidget(undefined);
|
|
78
|
+
}
|
|
79
|
+
/** One-line summary of a host tool call for the `↳` trailer (best-effort). */
|
|
80
|
+
function formatToolLine(toolName, args) {
|
|
81
|
+
const a = (args ?? {});
|
|
82
|
+
const field = a.path ?? a.file_path ?? a.command ?? a.pattern ?? a.prompt ?? a.query ?? a.url;
|
|
83
|
+
const detail = typeof field === 'string' ? ' ' + field.replace(/\s+/g, ' ').trim() : '';
|
|
84
|
+
return `${toolName}${detail}`;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Arm the implementation widget just before the spec is handed off. `oneShot`
|
|
88
|
+
* true (fire-and-forget /task) lets `agent_end` disarm it after the single turn;
|
|
89
|
+
* false (awaited /task-auto) keeps it armed until `disarmImplWidget` is called.
|
|
90
|
+
*/
|
|
91
|
+
export function armImplWidget(meta, opts) {
|
|
92
|
+
armed = { meta, oneShot: opts.oneShot, startedAt: Date.now() };
|
|
93
|
+
lastLine = undefined;
|
|
94
|
+
}
|
|
95
|
+
/** Tear down the widget and clear the armed slot (sticky/awaited path). */
|
|
96
|
+
export function disarmImplWidget() {
|
|
97
|
+
stopTimer();
|
|
98
|
+
clearWidget();
|
|
99
|
+
armed = null;
|
|
100
|
+
activeCtx = null;
|
|
101
|
+
lastLine = undefined;
|
|
102
|
+
}
|
|
103
|
+
/** Wire the agent-lifecycle handlers that drive the widget. Call once at setup. */
|
|
104
|
+
export function setupImplWidget(pi) {
|
|
105
|
+
pi.on('agent_start', (_event, ctx) => {
|
|
106
|
+
if (!armed)
|
|
107
|
+
return;
|
|
108
|
+
activeCtx = ctx;
|
|
109
|
+
lastLine = undefined;
|
|
110
|
+
startTimer();
|
|
111
|
+
});
|
|
112
|
+
pi.on('tool_execution_start', (event, _ctx) => {
|
|
113
|
+
if (!armed)
|
|
114
|
+
return;
|
|
115
|
+
lastLine = formatToolLine(event.toolName, event.args);
|
|
116
|
+
});
|
|
117
|
+
pi.on('agent_end', (_event, _ctx) => {
|
|
118
|
+
if (!armed)
|
|
119
|
+
return;
|
|
120
|
+
stopTimer();
|
|
121
|
+
clearWidget();
|
|
122
|
+
// Fire-and-forget /task: the single impl turn is over, so disarm. Awaited
|
|
123
|
+
// /task-auto leaves the slot armed so the next compaction-resume / steer
|
|
124
|
+
// turn re-shows it; the caller disarms when the phase truly settles.
|
|
125
|
+
if (armed.oneShot) {
|
|
126
|
+
armed = null;
|
|
127
|
+
activeCtx = null;
|
|
128
|
+
lastLine = undefined;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
@@ -28,6 +28,11 @@ export declare class TaskRunner {
|
|
|
28
28
|
private readonly _onStart;
|
|
29
29
|
private readonly _planContext;
|
|
30
30
|
private readonly _fixInstruction;
|
|
31
|
+
/** True when the caller awaits the implementation turn (waitForImplementation):
|
|
32
|
+
* the impl widget stays armed across the whole impl phase (incl. compaction /
|
|
33
|
+
* steer turns) and is disarmed here. False for fire-and-forget /task, where the
|
|
34
|
+
* widget is armed one-shot and its own agent_end disarms it. */
|
|
35
|
+
private readonly _implAwaited;
|
|
31
36
|
private readonly _abort;
|
|
32
37
|
private readonly _startedAt;
|
|
33
38
|
private readonly _widgetState;
|
|
@@ -43,7 +48,7 @@ export declare class TaskRunner {
|
|
|
43
48
|
*/
|
|
44
49
|
private readonly _timings;
|
|
45
50
|
private _currentPhaseChildren;
|
|
46
|
-
constructor(ctx: ExtensionCommandContext, cwd: string, rawPrompt: string, resumeId?: string, sendSpec?: (spec: string) => Promise<void>, spawnFn?: SpawnFn, onStart?: (taskId: string) => void | Promise<void>, planContext?: string, fixInstruction?: string);
|
|
51
|
+
constructor(ctx: ExtensionCommandContext, cwd: string, rawPrompt: string, resumeId?: string, sendSpec?: (spec: string) => Promise<void>, spawnFn?: SpawnFn, onStart?: (taskId: string) => void | Promise<void>, planContext?: string, fixInstruction?: string, implAwaited?: boolean);
|
|
47
52
|
get taskId(): string;
|
|
48
53
|
get signal(): AbortSignal;
|
|
49
54
|
/** Return the current widget state, or null if not started. */
|
|
@@ -23,6 +23,7 @@ import { PHASE_INDEX, PHASE_ORDER, RESUMABLE_STATES } from './task-types.js';
|
|
|
23
23
|
import { normaliseTaskId, parseFrontMatter, extractSection } from './task-parsers.js';
|
|
24
24
|
import { allocateTaskId, ensureTasksDir, readSection, readTaskFile, setTaskSection, taskFilePath, tasksDir, updateTaskFrontMatter, writeTaskFile } from './task-io.js';
|
|
25
25
|
import { startWidget } from './widget.js';
|
|
26
|
+
import { armImplWidget, disarmImplWidget, setupImplWidget } from './impl-widget.js';
|
|
26
27
|
import { publishViewer, publishNotify, registerBridgeCommand, getBridge } from '../remote/bridge.js';
|
|
27
28
|
import { pushNotify } from '../remote/push.js';
|
|
28
29
|
import { parseVerifyBlock } from './spec-validation.js';
|
|
@@ -54,6 +55,11 @@ export class TaskRunner {
|
|
|
54
55
|
_onStart;
|
|
55
56
|
_planContext;
|
|
56
57
|
_fixInstruction;
|
|
58
|
+
/** True when the caller awaits the implementation turn (waitForImplementation):
|
|
59
|
+
* the impl widget stays armed across the whole impl phase (incl. compaction /
|
|
60
|
+
* steer turns) and is disarmed here. False for fire-and-forget /task, where the
|
|
61
|
+
* widget is armed one-shot and its own agent_end disarms it. */
|
|
62
|
+
_implAwaited;
|
|
57
63
|
_abort = new AbortController();
|
|
58
64
|
_startedAt;
|
|
59
65
|
_widgetState;
|
|
@@ -69,7 +75,7 @@ export class TaskRunner {
|
|
|
69
75
|
*/
|
|
70
76
|
_timings = [];
|
|
71
77
|
_currentPhaseChildren = null;
|
|
72
|
-
constructor(ctx, cwd, rawPrompt, resumeId, sendSpec, spawnFn, onStart, planContext, fixInstruction) {
|
|
78
|
+
constructor(ctx, cwd, rawPrompt, resumeId, sendSpec, spawnFn, onStart, planContext, fixInstruction, implAwaited) {
|
|
73
79
|
this._ctx = ctx;
|
|
74
80
|
this._cwd = cwd;
|
|
75
81
|
this._rawPrompt = rawPrompt;
|
|
@@ -78,6 +84,7 @@ export class TaskRunner {
|
|
|
78
84
|
this._onStart = onStart;
|
|
79
85
|
this._planContext = planContext;
|
|
80
86
|
this._fixInstruction = fixInstruction;
|
|
87
|
+
this._implAwaited = implAwaited ?? false;
|
|
81
88
|
this._startedAt = Date.now();
|
|
82
89
|
// We'll populate id/title/phase lazily in run().
|
|
83
90
|
// Placeholder — real values set in run().
|
|
@@ -265,13 +272,30 @@ export class TaskRunner {
|
|
|
265
272
|
}
|
|
266
273
|
async _deliverSpec(ctx) {
|
|
267
274
|
const spec = this._specForDelivery();
|
|
275
|
+
// Keep the rich status block alive across the implementation turn (the phase
|
|
276
|
+
// widget was disposed at handoff). Awaited (/task-auto) stays armed across all
|
|
277
|
+
// sub-turns and is disarmed here; fire-and-forget (/task) arms one-shot and its
|
|
278
|
+
// own agent_end disarms it after the single turn.
|
|
279
|
+
const meta = {
|
|
280
|
+
taskId: this._widgetState.taskId,
|
|
281
|
+
title: this._widgetState.title,
|
|
282
|
+
label: this._widgetState.label
|
|
283
|
+
};
|
|
268
284
|
if (this._sendSpec) {
|
|
269
|
-
|
|
285
|
+
armImplWidget(meta, { oneShot: !this._implAwaited });
|
|
286
|
+
try {
|
|
287
|
+
await this._sendSpec(spec);
|
|
288
|
+
}
|
|
289
|
+
finally {
|
|
290
|
+
if (this._implAwaited)
|
|
291
|
+
disarmImplWidget();
|
|
292
|
+
}
|
|
270
293
|
return;
|
|
271
294
|
}
|
|
272
295
|
if (!piApi) {
|
|
273
296
|
throw new Error('extension not initialised (no ExtensionAPI captured)');
|
|
274
297
|
}
|
|
298
|
+
armImplWidget(meta, { oneShot: true });
|
|
275
299
|
if (ctx.isIdle()) {
|
|
276
300
|
piApi.sendUserMessage(spec);
|
|
277
301
|
}
|
|
@@ -494,7 +518,7 @@ export async function runSingleTask(ctx, cwd, rawPrompt, opts = {}) {
|
|
|
494
518
|
if (!interrupted)
|
|
495
519
|
implError = implementationError(newCtx);
|
|
496
520
|
}
|
|
497
|
-
}, opts.spawnFn, opts.onStart, opts.planContext, opts.fixInstruction);
|
|
521
|
+
}, opts.spawnFn, opts.onStart, opts.planContext, opts.fixInstruction, opts.waitForImplementation);
|
|
498
522
|
await runner.run();
|
|
499
523
|
taskId = runner.taskId;
|
|
500
524
|
}
|
|
@@ -643,6 +667,7 @@ async function handleTaskCancel(_args, ctx) {
|
|
|
643
667
|
// ─── Entry point ─────────────────────────────────────────────────────────────
|
|
644
668
|
export function registerTask(pi) {
|
|
645
669
|
piApi = pi;
|
|
670
|
+
setupImplWidget(pi);
|
|
646
671
|
registerBridgeCommand(pi, 'task', {
|
|
647
672
|
description: 'Start a new task. Usage: /task <prompt>',
|
|
648
673
|
handler: handleTask
|
package/dist/task/widget.d.ts
CHANGED
|
@@ -63,4 +63,14 @@ export declare function buildAutoLoaderLines(s: AutoLoaderState, theme?: WidgetT
|
|
|
63
63
|
* (returns a no-op disposer) when there's no UI.
|
|
64
64
|
*/
|
|
65
65
|
export declare function startAutoLoader(ctx: ExtensionCommandContext, getState: () => AutoLoaderState | null): () => void;
|
|
66
|
+
export interface ImplState {
|
|
67
|
+
taskId: string;
|
|
68
|
+
title: string;
|
|
69
|
+
/** Short display label compressed from `title`; falls back to a truncation when absent. */
|
|
70
|
+
label?: string;
|
|
71
|
+
startedAt: number;
|
|
72
|
+
lastLine?: string;
|
|
73
|
+
contextUsage?: ContextSnapshot;
|
|
74
|
+
}
|
|
75
|
+
export declare function buildImplLines(s: ImplState, theme?: WidgetTheme): string[];
|
|
66
76
|
export declare function flashTerminalWidget(ctx: ExtensionCommandContext, state: Exclude<TaskState, 'pending' | 'in_progress' | 'completed'>, taskId: string, reason: string | undefined): void;
|
package/dist/task/widget.js
CHANGED
|
@@ -170,6 +170,21 @@ export function startAutoLoader(ctx, getState) {
|
|
|
170
170
|
setTaskWidget(undefined);
|
|
171
171
|
};
|
|
172
172
|
}
|
|
173
|
+
export function buildImplLines(s, theme) {
|
|
174
|
+
const elapsed = formatDuration(Date.now() - s.startedAt);
|
|
175
|
+
const head = `${s.taskId} · ${titleForDisplay(s)}`;
|
|
176
|
+
let detail = `implementing · ${elapsed}`;
|
|
177
|
+
if (s.contextUsage) {
|
|
178
|
+
const ctxDetail = formatContextDetail(s.contextUsage, theme);
|
|
179
|
+
if (ctxDetail)
|
|
180
|
+
detail += ` · ${ctxDetail}`;
|
|
181
|
+
}
|
|
182
|
+
const lines = [head, detail];
|
|
183
|
+
const trailer = lastLineTrailer(s.lastLine, theme);
|
|
184
|
+
if (trailer)
|
|
185
|
+
lines.push(trailer);
|
|
186
|
+
return lines;
|
|
187
|
+
}
|
|
173
188
|
export function flashTerminalWidget(ctx, state, taskId, reason) {
|
|
174
189
|
if (!ctx.hasUI)
|
|
175
190
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
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",
|