@oh-my-pi/pi-coding-agent 16.1.20 → 16.1.22

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.
Files changed (38) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/cli.js +2284 -2272
  3. package/dist/types/advisor/advise-tool.d.ts +3 -0
  4. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -0
  5. package/dist/types/mcp/oauth-flow.d.ts +42 -5
  6. package/dist/types/mcp/transports/stdio.test.d.ts +1 -0
  7. package/dist/types/modes/components/custom-editor.d.ts +32 -0
  8. package/dist/types/modes/components/tool-execution.d.ts +5 -5
  9. package/dist/types/modes/controllers/event-controller.d.ts +10 -0
  10. package/dist/types/modes/controllers/input-controller.d.ts +2 -1
  11. package/dist/types/utils/clipboard.d.ts +10 -0
  12. package/package.json +12 -12
  13. package/scripts/generate-legacy-pi-bundled-registry.ts +10 -0
  14. package/src/advisor/__tests__/advisor.test.ts +44 -0
  15. package/src/advisor/advise-tool.ts +33 -0
  16. package/src/autolearn/controller.ts +17 -2
  17. package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +1 -1
  18. package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +4 -4
  19. package/src/extensibility/plugins/legacy-pi-compat.ts +193 -5
  20. package/src/mcp/manager.ts +12 -3
  21. package/src/mcp/oauth-discovery.ts +48 -1
  22. package/src/mcp/oauth-flow.ts +121 -7
  23. package/src/mcp/transports/stdio.test.ts +28 -0
  24. package/src/mcp/transports/stdio.ts +5 -2
  25. package/src/modes/components/chat-transcript-builder.ts +31 -0
  26. package/src/modes/components/custom-editor.test.ts +80 -0
  27. package/src/modes/components/custom-editor.ts +86 -6
  28. package/src/modes/components/tool-execution.ts +50 -25
  29. package/src/modes/controllers/event-controller.ts +57 -8
  30. package/src/modes/controllers/input-controller.ts +70 -27
  31. package/src/modes/controllers/mcp-command-controller.ts +18 -2
  32. package/src/modes/utils/ui-helpers.ts +40 -0
  33. package/src/prompts/system/autolearn-nudge-autocontinue.md +5 -0
  34. package/src/prompts/system/autolearn-nudge.md +4 -2
  35. package/src/prompts/tools/todo.md +1 -1
  36. package/src/session/agent-session.ts +4 -0
  37. package/src/tools/todo.ts +20 -10
  38. package/src/utils/clipboard.ts +57 -0
@@ -496,6 +496,7 @@ export class MCPCommandController {
496
496
 
497
497
  try {
498
498
  const oauthResource = oauth.resource ?? finalConfig.url;
499
+ const oauthResourceIsFallback = !oauth.resource;
499
500
  const oauthResult = await this.#handleOAuthFlow(
500
501
  oauth.authorizationUrl,
501
502
  oauth.tokenUrl,
@@ -509,11 +510,13 @@ export class MCPCommandController {
509
510
  prompt: finalConfig.oauth?.prompt,
510
511
  serverUrl: finalConfig.url,
511
512
  resource: oauthResource,
513
+ stripSameOriginResource: oauthResourceIsFallback,
512
514
  },
513
515
  );
514
516
  finalConfig = this.#persistOAuthResult(finalConfig, oauthResult, {
515
517
  tokenUrl: oauth.tokenUrl,
516
518
  resource: oauthResource,
519
+ stripSameOriginResource: oauthResourceIsFallback,
517
520
  clientId: oauth.clientId,
518
521
  userClientSecret: finalConfig.oauth?.clientSecret,
519
522
  });
@@ -583,6 +586,7 @@ export class MCPCommandController {
583
586
  prompt?: string;
584
587
  serverUrl?: string;
585
588
  resource?: string;
589
+ stripSameOriginResource?: boolean;
586
590
  },
587
591
  ): Promise<OAuthFlowResult> {
588
592
  const authStorage = this.ctx.session.modelRegistry.authStorage;
@@ -624,6 +628,7 @@ export class MCPCommandController {
624
628
  callbackPort: opts?.callbackPort,
625
629
  callbackPath: opts?.callbackPath,
626
630
  resource: opts?.resource,
631
+ stripSameOriginResource: opts?.stripSameOriginResource,
627
632
  },
628
633
  {
629
634
  onAuth: (info: { url: string; instructions?: string }) => {
@@ -711,6 +716,7 @@ export class MCPCommandController {
711
716
  clientId: flow.resolvedClientId ?? resolvedClientId,
712
717
  clientSecret: flow.registeredClientSecret ?? resolvedClientSecret,
713
718
  resource: flow.resource,
719
+ authorizationUrl: flow.authorizationUrl,
714
720
  };
715
721
 
716
722
  await authStorage.set(credentialId, oauthCredential);
@@ -751,10 +757,17 @@ export class MCPCommandController {
751
757
  #persistOAuthResult(
752
758
  config: MCPServerConfig,
753
759
  result: OAuthFlowResult,
754
- opts: { tokenUrl: string; resource?: string; clientId?: string; userClientSecret?: string },
760
+ opts: {
761
+ tokenUrl: string;
762
+ resource?: string;
763
+ stripSameOriginResource?: boolean;
764
+ clientId?: string;
765
+ userClientSecret?: string;
766
+ },
755
767
  ): MCPServerConfig {
756
768
  const clientId = result.clientId ?? opts.clientId ?? config.oauth?.clientId;
757
- const resource = result.resource ?? opts.resource ?? config.auth?.resource;
769
+ const resource =
770
+ result.resource ?? (opts.stripSameOriginResource ? undefined : opts.resource) ?? config.auth?.resource;
758
771
  return {
759
772
  ...config,
760
773
  auth: {
@@ -1558,6 +1571,7 @@ export class MCPCommandController {
1558
1571
  const currentAuthResource = currentAuth?.resource ? expandEnvVarsDeep(currentAuth.resource) : undefined;
1559
1572
  const oauthResource =
1560
1573
  oauth.resource ?? currentAuthResource ?? ("url" in runtimeBaseConfig ? runtimeBaseConfig.url : undefined);
1574
+ const oauthResourceIsFallback = !oauth.resource && !currentAuthResource;
1561
1575
 
1562
1576
  const oauthResult = await this.#handleOAuthFlow(
1563
1577
  oauth.authorizationUrl,
@@ -1572,6 +1586,7 @@ export class MCPCommandController {
1572
1586
  prompt: found.config.oauth?.prompt,
1573
1587
  serverUrl,
1574
1588
  resource: oauthResource,
1589
+ stripSameOriginResource: oauthResourceIsFallback,
1575
1590
  },
1576
1591
  );
1577
1592
 
@@ -1592,6 +1607,7 @@ export class MCPCommandController {
1592
1607
  clientId: oauth.clientId,
1593
1608
  userClientSecret,
1594
1609
  resource: oauthResource,
1610
+ stripSameOriginResource: oauthResourceIsFallback,
1595
1611
  });
1596
1612
  await updateMCPServer(found.filePath, name, updated);
1597
1613
  }
@@ -317,6 +317,24 @@ export class UiHelpers {
317
317
  // updateResult armed.
318
318
  previous.seal();
319
319
  };
320
+ let todoSnapshot: ToolExecutionComponent | null = null;
321
+ const resolveTodoSnapshot = (nextToolName?: string) => {
322
+ const previous = todoSnapshot;
323
+ if (!previous) return;
324
+ if (!previous.isDisplaceableBlock()) {
325
+ todoSnapshot = null;
326
+ return;
327
+ }
328
+ if (previous.canBeDisplacedBy(nextToolName)) {
329
+ todoSnapshot = null;
330
+ this.ctx.chatContainer.removeChild(previous);
331
+ previous.seal();
332
+ return;
333
+ }
334
+ if (nextToolName !== undefined) return;
335
+ todoSnapshot = null;
336
+ previous.seal();
337
+ };
320
338
  const messages = sessionContext.messages;
321
339
  const count = messages.length;
322
340
  for (let i = 0; i < count; i++) {
@@ -477,11 +495,22 @@ export class UiHelpers {
477
495
  component.isDisplaceableBlock()
478
496
  ) {
479
497
  waitingPoll = component;
498
+ } else if (
499
+ message.toolName === "todo" &&
500
+ component instanceof ToolExecutionComponent &&
501
+ component.canBeDisplacedBy("todo")
502
+ ) {
503
+ // A successful todo result supersedes the prior live snapshot. Failed
504
+ // follow-ups return false from canBeDisplacedBy("todo"), so the
505
+ // last-good panel stays on screen.
506
+ resolveTodoSnapshot("todo");
507
+ todoSnapshot = component;
480
508
  }
481
509
  }
482
510
  } else {
483
511
  // A user prompt closes the displacement window, same as the live path.
484
512
  if (message.role === "user") resolveWaitingPoll();
513
+ if (message.role === "user") resolveTodoSnapshot();
485
514
  // All other messages use standard rendering
486
515
  this.ctx.addMessageToChat(message, options);
487
516
  }
@@ -495,6 +524,17 @@ export class UiHelpers {
495
524
  // A trailing waiting poll is final history on rebuild; seal it so it
496
525
  // freezes (and its spinner timer stops) like every other block.
497
526
  resolveWaitingPoll();
527
+ // A trailing todo snapshot is live state, not history: when the rebuild
528
+ // runs mid-turn (settings overlay close, focus attach during streaming),
529
+ // hand it back to the controller so a follow-up `todo` update keeps
530
+ // displacing instead of stacking. Idle rebuilds (resume / compaction)
531
+ // fall through to the seal path so the snapshot freezes as history.
532
+ if (todoSnapshot && this.ctx.session?.isStreaming) {
533
+ this.ctx.eventController?.inheritDisplaceableTodo(todoSnapshot);
534
+ todoSnapshot = null;
535
+ } else {
536
+ resolveTodoSnapshot();
537
+ }
498
538
 
499
539
  this.ctx.pendingTools.clear();
500
540
  this.ctx.ui.requestRender();
@@ -0,0 +1,5 @@
1
+ Automated capture turn — not a user reply. The user has not yet responded to your previous turn. Do not treat this prompt as their answer, as approval to continue, or as acceptance of any pending action; only the user can do that.
2
+
3
+ If your previous turn produced anything reusable, capture it now: a repeatable procedure becomes a managed skill (`manage_skill`); a durable fact, convention, or user preference is worth remembering (`learn`, when memory is enabled). Only capture what will genuinely help next time. If nothing is worth keeping, do nothing.
4
+
5
+ Then stop. Do not run any other tools, do not resume prior work, do not answer your own pending questions, and do not produce a continuation reply. Yield and wait for the user's next prompt.
@@ -1,3 +1,5 @@
1
- Before you finish: if this turn produced anything reusable, capture it now with your learning tools — a repeatable procedure becomes a managed skill (`manage_skill`), and a durable fact or convention is worth remembering (`learn`, when memory is enabled).
1
+ Hidden auto-learn reminder (not a user request). If your previous turn produced anything reusable, capture it now with your learning tools — a repeatable procedure becomes a managed skill (`manage_skill`), and a durable fact, convention, or user preference is worth remembering (`learn`, when memory is enabled).
2
2
 
3
- Only capture what will genuinely help next time. If nothing this turn is worth keeping, do nothing.
3
+ Only capture what will genuinely help next time. If nothing is worth keeping, do nothing.
4
+
5
+ This reminder is appended to the user's real message; answer that message normally — the capture is in addition to, not a replacement for, the work the user just asked for.
@@ -1,6 +1,6 @@
1
1
  **Tasks referenced by verbatim content string, NEVER an auto-generated ID — no "task-1"/"task-N" exists. Pass the content text in the `task` field.**
2
2
 
3
- Next pending task auto-promotes to `in_progress` on each completion.
3
+ On each completion the earliest still-open task (in phase order) auto-promotes to `in_progress`. Completing tasks out of phase order can move this pointer **back** to an earlier phase — that is expected; completed tasks are never reverted.
4
4
 
5
5
  ## Operations
6
6
 
@@ -1164,6 +1164,7 @@ export class AgentSession {
1164
1164
  #advisorEnabled = false;
1165
1165
  /** The advisor's own agent, retained so `/dump advisor` can serialize its transcript. Undefined when no advisor is active. */
1166
1166
  #advisorAgent?: Agent;
1167
+ #advisorAdviseTool?: AdviseTool;
1167
1168
  #advisorReadOnlyTools?: AgentTool[];
1168
1169
  #advisorWatchdogPrompt?: string;
1169
1170
  #advisorYieldQueueUnsubscribe?: () => void;
@@ -1798,6 +1799,7 @@ export class AgentSession {
1798
1799
  this.#advisorAgentUnsubscribe?.();
1799
1800
  this.#advisorAgentUnsubscribe = undefined;
1800
1801
  this.#advisorRuntime?.reset();
1802
+ this.#advisorAdviseTool?.resetDeliveredNotes();
1801
1803
  this.#attachAdvisorRecorderFeed();
1802
1804
  this.#advisorPrimaryTurnsCompleted = 0;
1803
1805
  this.#advisorInterruptImmuneTurnStart = undefined;
@@ -1878,6 +1880,7 @@ export class AgentSession {
1878
1880
  };
1879
1881
 
1880
1882
  const adviseTool = new AdviseTool(enqueueAdvice);
1883
+ this.#advisorAdviseTool = adviseTool;
1881
1884
  const advisorReadOnlyTools = this.#advisorReadOnlyTools ?? [];
1882
1885
 
1883
1886
  const appendOnlyContext = new AppendOnlyContextManager();
@@ -2004,6 +2007,7 @@ export class AgentSession {
2004
2007
  if (this.#advisorAgent) {
2005
2008
  this.#advisorAgent = undefined;
2006
2009
  }
2010
+ this.#advisorAdviseTool = undefined;
2007
2011
  this.#advisorYieldQueueUnsubscribe?.();
2008
2012
  this.#advisorYieldQueueUnsubscribe = undefined;
2009
2013
  }
package/src/tools/todo.ts CHANGED
@@ -534,21 +534,31 @@ function formatSummary(phases: TodoPhase[], errors: string[], readOnly = false):
534
534
  lines.push(` - ${task.content} [${task.status}] (${task.phase})`);
535
535
  }
536
536
  }
537
+ // Closed = completed + abandoned, mirroring the per-phase `done` count.
538
+ const closedAll = tasks.filter(task => task.status === "completed" || task.status === "abandoned").length;
539
+ // The active phase is the EARLIEST one still holding open work, so the
540
+ // in-progress pointer can sit in a phase whose successors already have
541
+ // completed tasks. Detect that "worked ahead" case to explain the
542
+ // otherwise-surprising backward pointer instead of letting it read as a
543
+ // completed task reverting to pending.
544
+ const workedAhead = phases.some(
545
+ (phase, idx) =>
546
+ idx > currentIdx && phase.tasks.some(task => task.status === "completed" || task.status === "abandoned"),
547
+ );
548
+ lines.push(`Overall: ${closedAll}/${tasks.length} done, ${remainingTasks.length} open.`);
537
549
  lines.push(
538
- `Phase ${currentIdx + 1}/${phases.length} "${current.name}" ${done}/${current.tasks.length} tasks complete`,
550
+ `Active phase ${currentIdx + 1}/${phases.length} "${current.name}" (${done}/${current.tasks.length})${
551
+ workedAhead
552
+ ? " — earliest phase with open tasks; the in-progress pointer auto-advances to the earliest open task on each completion, so it can sit behind out-of-order work (nothing was un-completed)."
553
+ : "."
554
+ }`,
539
555
  );
540
556
  for (const phase of phases) {
541
557
  lines.push(` ${phase.name}:`);
542
558
  for (const task of phase.tasks) {
543
- const sym =
544
- task.status === "completed"
545
- ? "✓"
546
- : task.status === "in_progress"
547
- ? "→"
548
- : task.status === "abandoned"
549
- ? "✗"
550
- : "○";
551
- lines.push(` ${sym} ${task.content}`);
559
+ const checkbox = task.status === "completed" ? "[X]" : "[ ]";
560
+ const tag = task.status === "in_progress" ? " (in progress)" : task.status === "abandoned" ? " (dropped)" : "";
561
+ lines.push(` - ${checkbox} ${task.content}${tag}`);
552
562
  }
553
563
  }
554
564
  return lines.join("\n");
@@ -11,6 +11,63 @@ function isWsl(): boolean {
11
11
  return process.platform === "linux" && Boolean(process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP);
12
12
  }
13
13
 
14
+ // AppleScript that returns the POSIX paths of every file URL currently on the
15
+ // macOS pasteboard, one path per line. `pbpaste(1)` only surfaces plain text,
16
+ // EPS, or RTF, so a Finder `Cmd+C` (which puts only a `public.file-url`
17
+ // representation on the pasteboard) makes `pbpaste` empty. AppleScript's
18
+ // `«class furl»` coercion reaches the file-URL representation directly and
19
+ // works for both single-file and multi-file selections. The `try` blocks
20
+ // suppress the `-1700` "can't make … into type" error AppleScript raises when
21
+ // the clipboard holds no file URLs, so the script's exit status only reflects
22
+ // `osascript` itself.
23
+ const MAC_FILE_URL_SCRIPT = [
24
+ "on run",
25
+ '\tset output to ""',
26
+ "\ttry",
27
+ "\t\tset theClip to the clipboard as «class furl»",
28
+ "\t\tif class of theClip is list then",
29
+ "\t\t\trepeat with anItem in theClip",
30
+ "\t\t\t\ttry",
31
+ "\t\t\t\t\tset output to output & POSIX path of anItem & linefeed",
32
+ "\t\t\t\tend try",
33
+ "\t\t\tend repeat",
34
+ "\t\telse",
35
+ "\t\t\ttry",
36
+ "\t\t\t\tset output to POSIX path of theClip & linefeed",
37
+ "\t\t\tend try",
38
+ "\t\tend if",
39
+ "\tend try",
40
+ "\treturn output",
41
+ "end run",
42
+ ].join("\n");
43
+
44
+ /**
45
+ * Read file paths from the macOS pasteboard's `public.file-url` representation.
46
+ *
47
+ * Used to reach the Finder `Cmd+C` pasteboard (which exposes only file URLs,
48
+ * no plain text or raw image bytes) so an image-file clipboard can be attached
49
+ * via {@link handleImagePathPaste} instead of falling through to "Clipboard is
50
+ * empty". Returns an empty array on non-darwin platforms, when AppleScript is
51
+ * unavailable, or when the pasteboard holds no file URLs.
52
+ */
53
+ export async function readMacFileUrlsFromClipboard(): Promise<string[]> {
54
+ if (process.platform !== "darwin") return [];
55
+ try {
56
+ const stdout = execSync("osascript -", {
57
+ input: MAC_FILE_URL_SCRIPT,
58
+ encoding: "utf8",
59
+ timeout: 2000,
60
+ }).toString();
61
+ return stdout
62
+ .split(/\r?\n/)
63
+ .map(line => line.trim())
64
+ .filter(line => line.length > 0);
65
+ } catch (error) {
66
+ logger.warn("clipboard: failed to read macOS file URLs", { error: String(error) });
67
+ return [];
68
+ }
69
+ }
70
+
14
71
  /**
15
72
  * Copy text to the system clipboard.
16
73
  *