@mjasnikovs/pi-task 0.2.3 → 0.4.1

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.
@@ -15,6 +15,7 @@ import { allocateAutoId, buildAutoBody, parseDecomposeList, parseTaskList, check
15
15
  import { writeTaskFile, readTaskFile, updateTaskFrontMatter } from './task-io.js';
16
16
  import { gitCommitAll } from './auto-commit.js';
17
17
  import { runPhaseChild, USER_CANCELLED } from './child-runner.js';
18
+ import { SessionUI, registerBridgeCommand } from '../remote/bridge.js';
18
19
  import { startAutoLoader } from './widget.js';
19
20
  // Matches pi's @-file completion token (a path after @, until whitespace).
20
21
  const MENTION_RE = /(?:^|\s)@([^\s]+)/g;
@@ -55,6 +56,7 @@ export async function planAuto(ctx, cwd, feature, deps) {
55
56
  // with the model's recommended default pre-filled (Enter to accept, type to
56
57
  // override); we never auto-answer. The model emits NONE when nothing remains.
57
58
  const theme = ctx.ui.theme;
59
+ const ui = new SessionUI(ctx);
58
60
  // Inline any @file spec the user referenced so clarify/decompose reason over
59
61
  // the real content, not a one-line "Implement @file" that reads as trivial.
60
62
  const featureForModel = await expandFeatureMentions(cwd, feature);
@@ -74,7 +76,12 @@ export async function planAuto(ctx, cwd, feature, deps) {
74
76
  const title = suggested ?
75
77
  `${shownQ}\n${theme.fg('muted', 'Recommended:')}\n\n${renderInlineMarkdown(suggested, theme)}\n\n${theme.fg('muted', 'press Enter to accept')}`
76
78
  : `${shownQ}\n${theme.fg('muted', '(no recommendation — please answer)')}`;
77
- const a = await ctx.ui.input(title, plainSuggested);
79
+ const a = await ui.ask({
80
+ localTitle: title,
81
+ question: plainQ,
82
+ recommended: plainSuggested,
83
+ allowSkip: plainSuggested === undefined
84
+ });
78
85
  if (a === undefined) {
79
86
  ctx.ui.notify('/task-auto cancelled.', 'warning');
80
87
  return null;
@@ -303,15 +310,15 @@ async function handleTaskAutoCancel(_args, ctx) {
303
310
  }
304
311
  // ─── Registration ────────────────────────────────────────────────────────────
305
312
  export function registerTaskAuto(pi) {
306
- pi.registerCommand('task-auto', {
313
+ registerBridgeCommand(pi, 'task-auto', {
307
314
  description: 'Plan a feature into tasks and run them. Usage: /task-auto <feature>',
308
315
  handler: handleTaskAuto
309
316
  });
310
- pi.registerCommand('task-auto-resume', {
317
+ registerBridgeCommand(pi, 'task-auto-resume', {
311
318
  description: 'Resume the active /task-auto run.',
312
319
  handler: handleTaskAutoResume
313
320
  });
314
- pi.registerCommand('task-auto-cancel', {
321
+ registerBridgeCommand(pi, 'task-auto-cancel', {
315
322
  description: 'Stop the running /task-auto loop after the current task.',
316
323
  handler: handleTaskAutoCancel
317
324
  });
@@ -58,7 +58,12 @@ export async function runChild(cwd, tools, prompt, signal, onLine, onContextUsag
58
58
  }
59
59
  });
60
60
  return {
61
- text: result.text ?? result.stdout.trim(),
61
+ // Use `||` (not `??`) so an empty string from json-events mode falls
62
+ // back to raw stdout. Without this, a child that exits 0 but emits no
63
+ // assistant text (e.g. model API error swallowed in json mode) always
64
+ // fails with the unhelpful "X child produced no output" — the raw
65
+ // stdout/stderr that might contain the real error is discarded.
66
+ text: result.text || result.stdout.trim(),
62
67
  exitCode: result.exitCode,
63
68
  stderr: result.stderr.trim(),
64
69
  loopHit
@@ -71,7 +76,7 @@ export async function runPhaseChild(deps, name, tools, prompt) {
71
76
  throw new Error(`${name} child failed: ${r.stderr || '(no stderr)'}`);
72
77
  }
73
78
  if (r.text.trim().length === 0) {
74
- throw new Error(`${name} child produced no output`);
79
+ throw new Error(`${name} child produced no output${r.stderr ? ' — stderr: ' + r.stderr : ''}`);
75
80
  }
76
81
  return r.text;
77
82
  }
@@ -122,7 +127,7 @@ export async function runPhaseWithLoopGuard(deps, name, tools, buildPrompt) {
122
127
  throw new Error(`${name} child failed: ${r.stderr || '(no stderr)'}`);
123
128
  }
124
129
  if (r.text.trim().length === 0) {
125
- throw new Error(`${name} child produced no output`);
130
+ throw new Error(`${name} child produced no output${r.stderr ? ' — stderr: ' + r.stderr : ''}`);
126
131
  }
127
132
  return r.text;
128
133
  }
@@ -21,6 +21,7 @@ import { PHASES, postCommitPhase } from './phases.js';
21
21
  import { handleFailure } from './failure-classifier.js';
22
22
  import { PHASE_INDEX, PHASE_ORDER, allocateTaskId, ensureTasksDir, normaliseTaskId, parseFrontMatter, readSection, readTaskFile, setTaskSection, taskFilePath, tasksDir, updateTaskFrontMatter, writeTaskFile, extractSection, RESUMABLE_STATES } from './task-file.js';
23
23
  import { startWidget, WIDGET_KEY } from './widget.js';
24
+ import { publishViewer, publishNotify, registerBridgeCommand } from '../remote/bridge.js';
24
25
  import { parseVerifyBlock } from './parsers.js';
25
26
  import { formatTimings } from './timings.js';
26
27
  // ─── Module-level state ──────────────────────────────────────────────────────
@@ -341,6 +342,7 @@ async function handleTaskList(_args, ctx) {
341
342
  if (lines.length === 0)
342
343
  lines.push('(no tasks in .pi-tasks/)');
343
344
  lines.push('', 'resume: /task-resume <id> (eligible: in_progress, pending, cancelled, failed)');
345
+ publishViewer('Tasks', lines.join('\n'));
344
346
  await ctx.ui.editor('Tasks', lines.join('\n'));
345
347
  }
346
348
  async function handleTaskResume(args, ctx) {
@@ -354,6 +356,7 @@ async function handleTaskResume(args, ctx) {
354
356
  }
355
357
  catch {
356
358
  ctx.ui.notify(`${id} not found in .pi-tasks/`, 'error');
359
+ publishNotify(`${id} not found in .pi-tasks/`, 'error');
357
360
  return;
358
361
  }
359
362
  }
@@ -382,6 +385,7 @@ async function handleTaskResume(args, ctx) {
382
385
  candidates.sort((a, b) => b.mtime - a.mtime);
383
386
  if (candidates.length === 0) {
384
387
  ctx.ui.notify('No resumable tasks.', 'info');
388
+ publishNotify('No resumable tasks.', 'info');
385
389
  return;
386
390
  }
387
391
  id = candidates[0].id;
@@ -393,27 +397,29 @@ async function handleTaskResume(args, ctx) {
393
397
  async function handleTaskCancel(_args, ctx) {
394
398
  if (!activeTask) {
395
399
  ctx.ui.notify('No task is running.', 'info');
400
+ publishNotify('No task is running.', 'info');
396
401
  return;
397
402
  }
398
403
  activeTask.cancel();
399
404
  ctx.ui.notify(`Cancelling ${activeTask.taskId}…`, 'warning');
405
+ publishNotify(`Cancelling ${activeTask.taskId}…`, 'warning');
400
406
  }
401
407
  // ─── Entry point ─────────────────────────────────────────────────────────────
402
408
  export function registerTask(pi) {
403
409
  piApi = pi;
404
- pi.registerCommand('task', {
410
+ registerBridgeCommand(pi, 'task', {
405
411
  description: 'Start a new task. Usage: /task <prompt>',
406
412
  handler: handleTask
407
413
  });
408
- pi.registerCommand('task-list', {
414
+ registerBridgeCommand(pi, 'task-list', {
409
415
  description: 'List tasks in this project.',
410
416
  handler: handleTaskList
411
417
  });
412
- pi.registerCommand('task-resume', {
418
+ registerBridgeCommand(pi, 'task-resume', {
413
419
  description: 'Resume a task. Usage: /task-resume [id]',
414
420
  handler: handleTaskResume
415
421
  });
416
- pi.registerCommand('task-cancel', {
422
+ registerBridgeCommand(pi, 'task-cancel', {
417
423
  description: 'Cancel the currently running task.',
418
424
  handler: handleTaskCancel
419
425
  });
@@ -15,6 +15,7 @@ import { setTaskSection, updateTaskFrontMatter } from './task-file.js';
15
15
  import { renderInlineMarkdown, stripInlineMarkdown } from './inline-markdown.js';
16
16
  import { parseVerifyBlock, parseGrillQuestions, parseAutoAnswer, parseVerifyToolingOutput, validateSpecShape, stripSpecPreamble, deriveTitle, isCritiqueClean } from './parsers.js';
17
17
  import { runPhaseChild, runPhaseWithLoopGuard, runWithEmphasisRetry, prependHint, USER_CANCELLED } from './child-runner.js';
18
+ import { SessionUI } from '../remote/bridge.js';
18
19
  // ─── Re-export constants from their home modules ────────────────────────────
19
20
  export { MAX_GRILL_QUESTIONS };
20
21
  // ─── Tooling helpers ─────────────────────────────────────────────────────────
@@ -332,6 +333,7 @@ export async function phaseGrill(deps, ctx, widgetState, refined, research) {
332
333
  // or surfaced as a pre-filled recommendation. The model emits NONE when
333
334
  // nothing ambiguous remains. Kept in sync with /task-auto's clarify dialog.
334
335
  const theme = ctx.ui.theme;
336
+ const ui = new SessionUI(ctx);
335
337
  const out = []; // human-facing Q&A transcript (with auto-worker debug lines)
336
338
  const qa = []; // compact Q&A fed back into the next question
337
339
  // Open-ended: keep asking until the model emits NONE or the user dismisses.
@@ -361,11 +363,16 @@ export async function phaseGrill(deps, ctx, widgetState, refined, research) {
361
363
  }
362
364
  else {
363
365
  const plainSuggested = auto.suggested === undefined ? undefined : stripInlineMarkdown(auto.suggested);
364
- const title = auto.suggested ?
366
+ const localTitle = auto.suggested ?
365
367
  `${shownQ}\n${theme.fg('muted', 'Recommended:')}\n\n${renderInlineMarkdown(auto.suggested, theme)}\n\n${theme.fg('muted', 'press Enter to accept')}`
366
368
  : `${shownQ}\n${theme.fg('muted', '(no recommendation — please answer)')}`;
367
369
  widgetState.lastLine = `awaiting Q${n + 1}`;
368
- const a = await ctx.ui.input(title, plainSuggested);
370
+ const a = await ui.ask({
371
+ localTitle,
372
+ question: plainQ,
373
+ recommended: plainSuggested,
374
+ allowSkip: plainSuggested === undefined
375
+ });
369
376
  if (a === undefined)
370
377
  throw new Error(USER_CANCELLED);
371
378
  const typed = a.trim();
@@ -5,6 +5,7 @@
5
5
  * context usage, and the latest child-process line.
6
6
  */
7
7
  import { PHASE_INDEX, PHASE_ORDER } from './task-file.js';
8
+ import { publishWidget } from '../remote/bridge.js';
8
9
  // ─── Constants ───────────────────────────────────────────────────────────────
9
10
  export const WIDGET_KEY = 'pi-tasks';
10
11
  export const AUTO_WIDGET_KEY = 'pi-task-auto';
@@ -92,12 +93,15 @@ export function startWidget(ctx, getState) {
92
93
  return () => { };
93
94
  const render = () => {
94
95
  const s = getState();
96
+ const lines = s ? buildWidgetLines(s, ctx.ui.theme) : undefined;
97
+ const plain = s ? buildWidgetLines(s, undefined) : undefined; // un-themed for the wire
95
98
  try {
96
- ctx.ui.setWidget(WIDGET_KEY, s ? buildWidgetLines(s, ctx.ui.theme) : undefined);
99
+ ctx.ui.setWidget(WIDGET_KEY, lines);
97
100
  }
98
101
  catch {
99
102
  /* stale ctx */
100
103
  }
104
+ publishWidget(WIDGET_KEY, plain);
101
105
  };
102
106
  render();
103
107
  const timer = setInterval(render, WIDGET_REFRESH_MS);
@@ -129,12 +133,15 @@ export function startAutoLoader(ctx, getState) {
129
133
  return () => { };
130
134
  const render = () => {
131
135
  const s = getState();
136
+ const lines = s ? buildAutoLoaderLines(s, ctx.ui.theme) : undefined;
137
+ const plain = s ? buildAutoLoaderLines(s, undefined) : undefined;
132
138
  try {
133
- ctx.ui.setWidget(AUTO_WIDGET_KEY, s ? buildAutoLoaderLines(s, ctx.ui.theme) : undefined);
139
+ ctx.ui.setWidget(AUTO_WIDGET_KEY, lines);
134
140
  }
135
141
  catch {
136
142
  /* stale ctx */
137
143
  }
144
+ publishWidget(AUTO_WIDGET_KEY, plain);
138
145
  };
139
146
  render();
140
147
  const timer = setInterval(render, WIDGET_REFRESH_MS);
@@ -147,6 +154,7 @@ export function startAutoLoader(ctx, getState) {
147
154
  catch {
148
155
  /* stale ctx */
149
156
  }
157
+ publishWidget(AUTO_WIDGET_KEY, undefined);
150
158
  };
151
159
  }
152
160
  export function flashTerminalWidget(ctx, state, taskId, reason) {
@@ -163,8 +171,12 @@ export function flashTerminalWidget(ctx, state, taskId, reason) {
163
171
  line = theme.fg('error', `✘ ${taskId} failed${reason ? ': ' + reason : ''}`);
164
172
  clearMs = FAIL_CLEAR_MS;
165
173
  }
174
+ const plainLine = state === 'cancelled' ?
175
+ `⚠ ${taskId} cancelled`
176
+ : `✘ ${taskId} failed${reason ? ': ' + reason : ''}`;
166
177
  try {
167
178
  ctx.ui.setWidget(WIDGET_KEY, [line]);
179
+ publishWidget(WIDGET_KEY, [plainLine]);
168
180
  }
169
181
  catch {
170
182
  /* stale ctx */
@@ -172,6 +184,7 @@ export function flashTerminalWidget(ctx, state, taskId, reason) {
172
184
  setTimeout(() => {
173
185
  try {
174
186
  ctx.ui.setWidget(WIDGET_KEY, undefined);
187
+ publishWidget(WIDGET_KEY, undefined);
175
188
  }
176
189
  catch {
177
190
  /* stale ctx */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.2.3",
4
- "description": "Deterministic spec-orchestration for local models, with bundled web/docs/fetch/worker subagent tools.",
3
+ "version": "0.4.1",
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",
7
7
  "types": "./dist/index.d.ts",
@@ -17,20 +17,24 @@
17
17
  "prepublishOnly": "bun run build"
18
18
  },
19
19
  "peerDependencies": {
20
- "@earendil-works/pi-coding-agent": "0.75.5",
21
- "@earendil-works/pi-agent-core": "0.75.5",
22
- "@earendil-works/pi-tui": "0.75.5"
20
+ "@earendil-works/pi-coding-agent": "0.78.1",
21
+ "@earendil-works/pi-agent-core": "0.78.1",
22
+ "@earendil-works/pi-tui": "0.78.1"
23
23
  },
24
24
  "dependencies": {
25
25
  "@mozilla/readability": "^0.6.0",
26
26
  "@sinclair/typebox": "0.34.49",
27
27
  "jsdom": "^29.1.1",
28
- "turndown": "^7.2.4"
28
+ "qrcode": "^1.5.4",
29
+ "turndown": "^7.2.4",
30
+ "ws": "^8.18.0"
29
31
  },
30
32
  "devDependencies": {
31
- "@earendil-works/pi-coding-agent": "0.75.5",
32
- "@earendil-works/pi-agent-core": "0.75.5",
33
- "@earendil-works/pi-tui": "0.75.5",
33
+ "@earendil-works/pi-coding-agent": "0.78.1",
34
+ "@earendil-works/pi-agent-core": "0.78.1",
35
+ "@earendil-works/pi-tui": "0.78.1",
36
+ "@types/qrcode": "^1.5.5",
37
+ "@types/ws": "^8.5.14",
34
38
  "@eslint/js": "10.0.1",
35
39
  "@sinclair/typebox": "0.34.49",
36
40
  "@types/bun": "1.3.12",
@@ -48,7 +52,9 @@
48
52
  "pi-extension",
49
53
  "coding-agent",
50
54
  "task-orchestration",
51
- "local-llm"
55
+ "local-llm",
56
+ "remote",
57
+ "web"
52
58
  ],
53
59
  "repository": {
54
60
  "type": "git",