@linimin/pi-letscook 0.1.69 → 0.1.70

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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.70
4
+
5
+ ### Changed
6
+
7
+ - added a visible `/cook startup plan` overlay while same-entry primary-agent startup synthesis is running so users no longer wait on a silent UI before Start/Cancel appears
8
+ - reused the same cancellable overlay/heartbeat pattern for `/cook` startup subprocesses so progress updates, elapsed time, and waiting state stay visible during startup-plan synthesis
9
+
3
10
  ## 0.1.69
4
11
 
5
12
  ### Changed
@@ -277,14 +277,14 @@ export function buildContextProposalAnalystPrompt(projectName: string, discussio
277
277
  return lines.join("\n");
278
278
  }
279
279
 
280
- export function contextProposalAnalystProgressLines(
280
+ function buildCookStartupProgressLines(
281
281
  activity: LiveRoleActivity,
282
282
  buildInlineRunningLines: (details: {
283
283
  role?: string;
284
284
  startedAt?: number;
285
285
  updatedAt?: number;
286
286
  currentAction?: string;
287
- toolActivity?: string[];
287
+ toolActivity?: string;
288
288
  toolRecentActivity?: string[];
289
289
  recentActivity?: string[];
290
290
  assistantSummary?: string;
@@ -294,6 +294,7 @@ export function contextProposalAnalystProgressLines(
294
294
  verifying?: string;
295
295
  stateDeltas?: string[];
296
296
  }) => string[],
297
+ footerLine: string,
297
298
  ): string[] {
298
299
  return [
299
300
  ...buildInlineRunningLines({
@@ -312,10 +313,52 @@ export function contextProposalAnalystProgressLines(
312
313
  stateDeltas: activity.stateDeltas,
313
314
  }),
314
315
  "",
315
- "This step only prepares a proposal for confirmation.",
316
+ footerLine,
316
317
  ];
317
318
  }
318
319
 
320
+ export function contextProposalAnalystProgressLines(
321
+ activity: LiveRoleActivity,
322
+ buildInlineRunningLines: (details: {
323
+ role?: string;
324
+ startedAt?: number;
325
+ updatedAt?: number;
326
+ currentAction?: string;
327
+ toolActivity?: string;
328
+ toolRecentActivity?: string[];
329
+ recentActivity?: string[];
330
+ assistantSummary?: string;
331
+ progress?: string;
332
+ rationale?: string;
333
+ nextStep?: string;
334
+ verifying?: string;
335
+ stateDeltas?: string[];
336
+ }) => string[],
337
+ ): string[] {
338
+ return buildCookStartupProgressLines(activity, buildInlineRunningLines, "This step only prepares a proposal for confirmation.");
339
+ }
340
+
341
+ export function primaryAgentHandoffProgressLines(
342
+ activity: LiveRoleActivity,
343
+ buildInlineRunningLines: (details: {
344
+ role?: string;
345
+ startedAt?: number;
346
+ updatedAt?: number;
347
+ currentAction?: string;
348
+ toolActivity?: string;
349
+ toolRecentActivity?: string[];
350
+ recentActivity?: string[];
351
+ assistantSummary?: string;
352
+ progress?: string;
353
+ rationale?: string;
354
+ nextStep?: string;
355
+ verifying?: string;
356
+ stateDeltas?: string[];
357
+ }) => string[],
358
+ ): string[] {
359
+ return buildCookStartupProgressLines(activity, buildInlineRunningLines, "This step only synthesizes the startup plan for Start/Cancel confirmation.");
360
+ }
361
+
319
362
  export function buildEvaluationRoleContextLines(
320
363
  snapshot: CompletionStateSnapshot,
321
364
  role: string,
@@ -11,7 +11,7 @@ import {
11
11
  type ContextProposal,
12
12
  type RecentDiscussionEntry,
13
13
  } from "./proposal";
14
- import { contextProposalAnalystProgressLines } from "./prompt-surfaces";
14
+ import { contextProposalAnalystProgressLines, primaryAgentHandoffProgressLines } from "./prompt-surfaces";
15
15
  import {
16
16
  applyLiveRoleEvent,
17
17
  buildInlineRunningLines,
@@ -108,7 +108,12 @@ const PRIMARY_AGENT_HANDOFF_SYSTEM_PROMPT = [
108
108
  ].join(" ");
109
109
  const PRIMARY_AGENT_HANDOFF_ROLE = "cook-primary-agent-handoff";
110
110
 
111
- class StartupAnalystOverlay extends Container {
111
+ type CookStartupOverlayOptions = {
112
+ title: string;
113
+ footer: string;
114
+ };
115
+
116
+ class CookStartupOverlay extends Container {
112
117
  private readonly border: DynamicBorder;
113
118
  private readonly title: Text;
114
119
  private readonly body: Text;
@@ -116,7 +121,10 @@ class StartupAnalystOverlay extends Container {
116
121
  private lines: string[] = [];
117
122
  onAbort?: () => void;
118
123
 
119
- constructor(private readonly theme: any) {
124
+ constructor(
125
+ private readonly theme: any,
126
+ private readonly options: CookStartupOverlayOptions,
127
+ ) {
120
128
  super();
121
129
  this.border = new DynamicBorder((s: string) => this.theme.fg("accent", s));
122
130
  this.title = new Text("", 1, 0);
@@ -136,9 +144,9 @@ class StartupAnalystOverlay extends Container {
136
144
  }
137
145
 
138
146
  private updateDisplay(): void {
139
- this.title.setText(this.theme.fg("accent", this.theme.bold("/cook proposal analyst")));
147
+ this.title.setText(this.theme.fg("accent", this.theme.bold(this.options.title)));
140
148
  this.body.setText(formatInlineRunningText(this.theme, this.lines, { primaryAssistant: true }));
141
- this.footer.setText(this.theme.fg("muted", "Esc/Ctrl+C cancel • This analysis runs before /cook writes canonical workflow state"));
149
+ this.footer.setText(this.theme.fg("muted", this.options.footer));
142
150
  }
143
151
 
144
152
  override handleInput(data: string): void {
@@ -192,12 +200,15 @@ async function runContextProposalAnalystSubprocess(params: AnalyzeContextProposa
192
200
  const args: string[] = ["--mode", "json", "-p", "--no-session", "--no-extensions", "--append-system-prompt", systemPromptTemp.filePath, "--model", modelArg, prompt];
193
201
  const invocation = getPiInvocation(args);
194
202
  const liveActivity = createLiveRoleActivity(STARTUP_ANALYST_ROLE);
203
+ liveActivity.toolActivity = undefined;
204
+ liveActivity.toolRecentActivity = [];
205
+ liveActivity.recentActivity = [];
195
206
  liveActivity.progress = "Analyzing recent discussion";
196
207
  liveActivity.currentAction = "Reading recent discussion and preparing a startup proposal";
197
208
  liveActivity.assistantSummary = liveActivity.progress;
198
209
  liveActivity.recentActivity = pushRecentActivity(liveActivity.recentActivity, `assistant: ${liveActivity.progress}`);
199
210
  const messages: RoleMessage[] = [];
200
- let overlay: StartupAnalystOverlay | undefined;
211
+ let overlay: CookStartupOverlay | undefined;
201
212
  let finishOverlay: ((value: string | undefined) => void) | undefined;
202
213
  let overlaySettled = false;
203
214
  const settleOverlay = (value: string | undefined) => {
@@ -313,7 +324,10 @@ async function runContextProposalAnalystSubprocess(params: AnalyzeContextProposa
313
324
  if (ui) {
314
325
  return await ui.custom<string | undefined>((_tui, theme, _kb, done) => {
315
326
  finishOverlay = done;
316
- overlay = new StartupAnalystOverlay(theme);
327
+ overlay = new CookStartupOverlay(theme, {
328
+ title: "/cook proposal analyst",
329
+ footer: "Esc/Ctrl+C cancel • This analysis runs before /cook writes canonical workflow state",
330
+ });
317
331
  overlay.setLines(contextProposalAnalystProgressLines(liveActivity, buildInlineRunningLines));
318
332
  run().then(settleOverlay).catch(() => settleOverlay(undefined));
319
333
  return overlay;
@@ -353,52 +367,147 @@ async function runPrimaryAgentHandoffSubprocess(params: GenerateCookHandoffWithA
353
367
  if (!modelArg) return undefined;
354
368
  const cwd = params.getCtxCwd(ctx);
355
369
  const runCwd = findCompletionRoot(cwd) ?? findRepoRoot(cwd) ?? cwd;
370
+ const rootKey = completionRootKey(undefined, cwd);
356
371
  const prompt = buildPrimaryAgentHandoffPrompt(projectName, recentEntries, params.workflowContextLines ?? []);
357
372
  const systemPromptTemp = await writeTempFile(runCwd, "pi-cook-primary-agent-handoff-", PRIMARY_AGENT_HANDOFF_SYSTEM_PROMPT);
358
373
  const args: string[] = ["--mode", "json", "-p", "--no-session", "--no-extensions", "--append-system-prompt", systemPromptTemp.filePath, "--model", modelArg, prompt];
359
374
  const invocation = getPiInvocation(args);
360
375
  const liveActivity = createLiveRoleActivity(PRIMARY_AGENT_HANDOFF_ROLE);
361
- liveActivity.progress = "Preparing primary-agent /cook handoff";
362
- liveActivity.currentAction = "Authoring explicit startup handoff from current task context";
363
- liveActivity.assistantSummary = liveActivity.progress;
364
- try {
365
- const output = await new Promise<string | undefined>((resolve) => {
366
- const proc = spawn(invocation.command, invocation.args, {
367
- cwd: runCwd,
368
- env: process.env,
369
- stdio: ["ignore", "pipe", "pipe"],
370
- shell: false,
371
- });
372
- let buffer = "";
373
- const messages: RoleMessage[] = [];
374
- const processLine = (line: string) => {
375
- if (!line.trim()) return;
376
- try {
377
- const event = JSON.parse(line) as JsonRecord;
378
- applyLiveRoleEvent(liveActivity, event, messages);
379
- } catch {
380
- // ignore malformed lines
376
+ liveActivity.toolActivity = undefined;
377
+ liveActivity.toolRecentActivity = [];
378
+ liveActivity.recentActivity = [];
379
+ liveActivity.currentAction = "Reading current task context";
380
+ liveActivity.rationale = "Loading canonical workflow context";
381
+ liveActivity.nextStep = "Synthesizing startup plan";
382
+ liveActivity.verifying = "Waiting for model response...";
383
+ let overlay: CookStartupOverlay | undefined;
384
+ let finishOverlay: ((value: string | undefined) => void) | undefined;
385
+ let overlaySettled = false;
386
+ const settleOverlay = (value: string | undefined) => {
387
+ if (overlaySettled) return;
388
+ overlaySettled = true;
389
+ finishOverlay?.(value);
390
+ };
391
+ const updateActivity = (fresh = false) => {
392
+ if (fresh) liveActivity.updatedAt = nowMs();
393
+ params.liveRoleActivityByRoot.set(rootKey, cloneLiveRoleActivity(liveActivity, { status: "running" }));
394
+ void refreshCompletionStatus({
395
+ ctx,
396
+ liveRoleActivityByRoot: params.liveRoleActivityByRoot,
397
+ completionStatusKey: params.completionStatusKey,
398
+ safeUiCall: params.safeUiCall,
399
+ getCtxCwd: params.getCtxCwd,
400
+ getCtxHasUI: params.getCtxHasUI,
401
+ getCtxUi: params.getCtxUi,
402
+ });
403
+ overlay?.setLines(primaryAgentHandoffProgressLines(liveActivity, buildInlineRunningLines));
404
+ };
405
+ const heartbeat = setInterval(() => updateActivity(false), ANALYST_HEARTBEAT_MS);
406
+ const run = async (): Promise<string | undefined> => {
407
+ try {
408
+ updateActivity(true);
409
+ const output = await new Promise<string | undefined>((resolve) => {
410
+ const proc = spawn(invocation.command, invocation.args, {
411
+ cwd: runCwd,
412
+ env: process.env,
413
+ stdio: ["ignore", "pipe", "pipe"],
414
+ shell: false,
415
+ });
416
+ let settled = false;
417
+ const resolveOnce = (value: string | undefined) => {
418
+ if (settled) return;
419
+ settled = true;
420
+ resolve(value);
421
+ };
422
+ const abort = () => {
423
+ proc.kill("SIGTERM");
424
+ resolveOnce(undefined);
425
+ };
426
+ const handleSigint = () => abort();
427
+ let buffer = "";
428
+ const messages: RoleMessage[] = [];
429
+ const processLine = (line: string) => {
430
+ if (!line.trim()) return;
431
+ try {
432
+ const event = JSON.parse(line) as JsonRecord;
433
+ if (applyLiveRoleEvent(liveActivity, event, messages)) updateActivity(true);
434
+ } catch {
435
+ // ignore malformed lines
436
+ }
437
+ };
438
+ proc.stdout.on("data", (chunk) => {
439
+ buffer += chunk.toString();
440
+ const lines = buffer.split("\n");
441
+ buffer = lines.pop() ?? "";
442
+ for (const line of lines) processLine(line);
443
+ });
444
+ proc.stderr.on("data", (_chunk) => {
445
+ // ignore handoff stderr unless the subprocess exits without assistant output
446
+ });
447
+ proc.on("close", (code) => {
448
+ process.off("SIGINT", handleSigint);
449
+ if (buffer.trim()) processLine(buffer);
450
+ resolveOnce(code === 0 ? liveActivity.lastAssistantText?.trim() || undefined : undefined);
451
+ });
452
+ proc.on("error", () => {
453
+ process.off("SIGINT", handleSigint);
454
+ resolveOnce(undefined);
455
+ });
456
+ process.once("SIGINT", handleSigint);
457
+ if (overlay) {
458
+ overlay.onAbort = () => {
459
+ process.off("SIGINT", handleSigint);
460
+ abort();
461
+ };
381
462
  }
382
- };
383
- proc.stdout.on("data", (chunk) => {
384
- buffer += chunk.toString();
385
- const lines = buffer.split("\n");
386
- buffer = lines.pop() ?? "";
387
- for (const line of lines) processLine(line);
388
463
  });
389
- proc.stderr.on("data", () => {
390
- // ignore stderr unless no assistant output arrives
464
+ params.liveRoleActivityByRoot.set(rootKey, cloneLiveRoleActivity(liveActivity, { status: output ? "ok" : "error" }));
465
+ await refreshCompletionStatus({
466
+ ctx,
467
+ liveRoleActivityByRoot: params.liveRoleActivityByRoot,
468
+ completionStatusKey: params.completionStatusKey,
469
+ safeUiCall: params.safeUiCall,
470
+ getCtxCwd: params.getCtxCwd,
471
+ getCtxHasUI: params.getCtxHasUI,
472
+ getCtxUi: params.getCtxUi,
391
473
  });
392
- proc.on("close", (code) => {
393
- if (buffer.trim()) processLine(buffer);
394
- resolve(code === 0 ? liveActivity.lastAssistantText?.trim() || undefined : undefined);
474
+ return output;
475
+ } finally {
476
+ clearInterval(heartbeat);
477
+ setTimeout(() => {
478
+ const current = params.liveRoleActivityByRoot.get(rootKey);
479
+ if (current && current.role === PRIMARY_AGENT_HANDOFF_ROLE && current.status !== "running") {
480
+ params.liveRoleActivityByRoot.delete(rootKey);
481
+ void refreshCompletionStatus({
482
+ ctx,
483
+ liveRoleActivityByRoot: params.liveRoleActivityByRoot,
484
+ completionStatusKey: params.completionStatusKey,
485
+ safeUiCall: params.safeUiCall,
486
+ getCtxCwd: params.getCtxCwd,
487
+ getCtxHasUI: params.getCtxHasUI,
488
+ getCtxUi: params.getCtxUi,
489
+ });
490
+ }
491
+ }, 10_000);
492
+ await fsp.rm(systemPromptTemp.dir, { recursive: true, force: true });
493
+ }
494
+ };
495
+ if (params.getCtxHasUI(ctx)) {
496
+ const ui = params.getCtxUi(ctx);
497
+ if (ui) {
498
+ return await ui.custom<string | undefined>((_tui, theme, _kb, done) => {
499
+ finishOverlay = done;
500
+ overlay = new CookStartupOverlay(theme, {
501
+ title: "/cook startup plan",
502
+ footer: "Esc/Ctrl+C cancel • This startup-plan synthesis runs before /cook writes canonical workflow state",
503
+ });
504
+ overlay.setLines(primaryAgentHandoffProgressLines(liveActivity, buildInlineRunningLines));
505
+ run().then(settleOverlay).catch(() => settleOverlay(undefined));
506
+ return overlay;
395
507
  });
396
- proc.on("error", () => resolve(undefined));
397
- });
398
- return output;
399
- } finally {
400
- await fsp.rm(systemPromptTemp.dir, { recursive: true, force: true });
508
+ }
401
509
  }
510
+ return await run();
402
511
  }
403
512
 
404
513
  export async function generateCookHandoffWithAgent(params: GenerateCookHandoffWithAgentParams): Promise<string | undefined> {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linimin/pi-letscook",
3
- "version": "0.1.69",
3
+ "version": "0.1.70",
4
4
  "description": "Pi package for long-running completion workflows with canonical .agent state, role-based subagents, continuity, and verification helpers.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -48,7 +48,7 @@ assertIncludes('extensions/completion/proposal.ts', 'export function serializeRe
48
48
  assertIncludes('extensions/completion/proposal.ts', 'export function extractJsonObjectFromText(');
49
49
 
50
50
  assertIncludes('extensions/completion/role-runner.ts', 'export async function analyzeContextProposalWithAgent(');
51
- assertIncludes('extensions/completion/role-runner.ts', 'class StartupAnalystOverlay extends Container');
51
+ assertIncludes('extensions/completion/role-runner.ts', 'class CookStartupOverlay extends Container');
52
52
  assertIncludes('extensions/completion/role-runner.ts', 'async function runContextProposalAnalystSubprocess(');
53
53
 
54
54
  assertIncludes('extensions/completion/prompt-surfaces.ts', 'export function buildSystemReminder(');
@@ -30,6 +30,8 @@ assertIncludes('extensions/completion/role-runner.ts', 'const transcription = ex
30
30
  assertIncludes('extensions/completion/role-runner.ts', 'env: { ...process.env, PI_COMPLETION_ROLE: params.role },');
31
31
  assertIncludes('extensions/completion/role-runner.ts', 'async function runContextProposalAnalystSubprocess(');
32
32
  assertIncludes('extensions/completion/role-runner.ts', 'export async function analyzeContextProposalWithAgent(');
33
+ assertIncludes('extensions/completion/role-runner.ts', 'class CookStartupOverlay extends Container');
34
+ assertIncludes('extensions/completion/role-runner.ts', 'overlay = new CookStartupOverlay(theme, {');
33
35
  assertIncludes('extensions/completion/index.ts', 'import { analyzeContextProposalWithAgent, generateCookHandoffWithAgent, runCompletionRole } from "./role-runner";');
34
36
  assertIncludes('extensions/completion/index.ts', 'const result = await runCompletionRole({');
35
37
  assertIncludes('extensions/completion/index.ts', 'const raw = await generateCookHandoffWithAgent({');