@oh-my-pi/pi-coding-agent 16.1.19 → 16.1.21

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 (59) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/cli.js +3795 -3760
  3. package/dist/types/advisor/advise-tool.d.ts +3 -0
  4. package/dist/types/cli/gallery-cli.d.ts +6 -0
  5. package/dist/types/commands/gallery.d.ts +1 -1
  6. package/dist/types/config/service-tier.d.ts +34 -0
  7. package/dist/types/config/settings-schema.d.ts +36 -33
  8. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -0
  9. package/dist/types/mcp/oauth-flow.d.ts +42 -5
  10. package/dist/types/modes/components/custom-editor.d.ts +32 -0
  11. package/dist/types/modes/components/tool-execution.d.ts +5 -5
  12. package/dist/types/modes/controllers/event-controller.d.ts +10 -0
  13. package/dist/types/modes/controllers/input-controller.d.ts +2 -1
  14. package/dist/types/session/agent-session.d.ts +2 -0
  15. package/dist/types/task/executor.d.ts +9 -1
  16. package/dist/types/task/parallel.d.ts +17 -1
  17. package/dist/types/tools/index.d.ts +3 -1
  18. package/dist/types/utils/clipboard.d.ts +10 -0
  19. package/package.json +13 -13
  20. package/scripts/generate-legacy-pi-bundled-registry.ts +10 -0
  21. package/src/advisor/__tests__/advisor.test.ts +44 -0
  22. package/src/advisor/advise-tool.ts +33 -0
  23. package/src/autolearn/controller.ts +17 -2
  24. package/src/cli/gallery-cli.ts +31 -2
  25. package/src/cli/gallery-fixtures/agentic.ts +13 -4
  26. package/src/commands/gallery.ts +11 -3
  27. package/src/config/service-tier.ts +87 -0
  28. package/src/config/settings-schema.ts +48 -23
  29. package/src/eval/agent-bridge.ts +2 -0
  30. package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +1 -1
  31. package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +4 -4
  32. package/src/extensibility/plugins/legacy-pi-compat.ts +193 -5
  33. package/src/main.ts +1 -0
  34. package/src/mcp/manager.ts +12 -3
  35. package/src/mcp/oauth-flow.ts +121 -7
  36. package/src/mcp/transports/stdio.ts +5 -0
  37. package/src/modes/components/chat-transcript-builder.ts +31 -0
  38. package/src/modes/components/custom-editor.test.ts +80 -0
  39. package/src/modes/components/custom-editor.ts +86 -6
  40. package/src/modes/components/tool-execution.ts +50 -25
  41. package/src/modes/controllers/event-controller.ts +57 -8
  42. package/src/modes/controllers/input-controller.ts +213 -93
  43. package/src/modes/controllers/mcp-command-controller.ts +18 -2
  44. package/src/modes/utils/ui-helpers.ts +40 -0
  45. package/src/prompts/system/autolearn-nudge-autocontinue.md +5 -0
  46. package/src/prompts/system/autolearn-nudge.md +4 -2
  47. package/src/prompts/tools/todo.md +1 -1
  48. package/src/sdk.ts +1 -0
  49. package/src/session/agent-session.ts +76 -15
  50. package/src/session/session-history-format.ts +21 -4
  51. package/src/task/executor.ts +79 -2
  52. package/src/task/index.ts +11 -6
  53. package/src/task/parallel.ts +59 -7
  54. package/src/tools/index.ts +3 -1
  55. package/src/tools/irc.ts +16 -2
  56. package/src/tools/todo.ts +20 -10
  57. package/src/utils/clipboard.ts +57 -0
  58. package/src/utils/shell-snapshot-fn-env.sh +60 -0
  59. package/src/utils/shell-snapshot.ts +77 -20
@@ -39,7 +39,7 @@ import {
39
39
  truncateToWidth,
40
40
  } from "../../tools/render-utils";
41
41
  import { toolRenderers } from "../../tools/renderers";
42
- import { TODO_STRIKE_TOTAL_FRAMES } from "../../tools/todo";
42
+ import { TODO_STRIKE_TOTAL_FRAMES, type TodoToolDetails } from "../../tools/todo";
43
43
  import { isFramedBlockComponent, renderStatusLine, WidthAwareText } from "../../tui";
44
44
  import { sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
45
45
  import { renderDiff } from "./diff";
@@ -75,6 +75,28 @@ function stripTrailingUnbalancedRemoval(diff: string | undefined): string | unde
75
75
  return lines.slice(0, lastAddIdx + 1).join("\n");
76
76
  }
77
77
 
78
+ type DisplaceableToolName = "job" | "todo";
79
+
80
+ function isTodoToolDetails(details: unknown): details is TodoToolDetails {
81
+ return (
82
+ typeof details === "object" &&
83
+ details !== null &&
84
+ "phases" in details &&
85
+ Array.isArray((details as { phases?: unknown }).phases)
86
+ );
87
+ }
88
+
89
+ function displaceableToolName(
90
+ toolName: string,
91
+ result: { details?: unknown; isError?: boolean },
92
+ isPartial: boolean,
93
+ ): DisplaceableToolName | undefined {
94
+ if (result.isError === true) return undefined;
95
+ if (toolName === "job" && isWaitingPollDetails(result.details)) return "job";
96
+ if (toolName === "todo" && !isPartial && isTodoToolDetails(result.details)) return "todo";
97
+ return undefined;
98
+ }
99
+
78
100
  function stabilizeStreamingPreviews(previews: PerFileDiffPreview[]): PerFileDiffPreview[] {
79
101
  let changed = false;
80
102
  const next = previews.map(preview => {
@@ -234,11 +256,11 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
234
256
  // sealed the block stays in the transcript's repaintable live region so a
235
257
  // late result still repaints instead of stranding the streaming preview.
236
258
  #sealed = false;
237
- // A `job` poll result whose watched jobs are all still running. Such a
238
- // block never finalizes (stays in the transcript live region) so a
239
- // follow-up `job` call can displace it instead of stacking another
240
- // "waiting on N jobs" frame. Cleared by `seal()`.
241
- #displaceable = false;
259
+ // Tool result snapshots that may be superseded by a later same-tool call
260
+ // while still in the transcript live region. `job` uses this for repeated
261
+ // all-running polls; `todo` uses it for per-turn state snapshots so only the
262
+ // latest list remains visible.
263
+ #displaceableByToolName: DisplaceableToolName | undefined;
242
264
  // Probe into the owning transcript (absent outside the interactive
243
265
  // transcript, e.g. in tests): whether this block is still repaintable.
244
266
  #liveRegion?: TranscriptLiveRegionProbe;
@@ -427,11 +449,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
427
449
  this.#result = result;
428
450
  this.#resultVersion++;
429
451
  this.#isPartial = isPartial;
430
- // A `job` poll that found every watched job still running is transient
431
- // "still waiting" chrome; keep the block displaceable so the next `job`
432
- // call replaces it instead of stacking another waiting frame (see the
433
- // event controller's displaceable-poll bookkeeping).
434
- this.#displaceable = this.#toolName === "job" && result.isError !== true && isWaitingPollDetails(result.details);
452
+ this.#displaceableByToolName = displaceableToolName(this.#toolName, result, isPartial);
435
453
  // When tool is complete, ensure args are marked complete so spinner stops
436
454
  if (!isPartial) {
437
455
  this.#argsComplete = true;
@@ -490,10 +508,12 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
490
508
  }
491
509
 
492
510
  /**
493
- * Start or stop spinner animation based on whether this is a partial task result.
511
+ * Start or stop spinner animation for result states that visibly tick.
494
512
  */
495
513
  #updateSpinnerAnimation(): void {
496
- // Spinner for: task tool with partial result, or edit/write while args streaming
514
+ // Spinner for: task tool with partial result, edit/write while args
515
+ // streaming, or a still-running job poll. Todo snapshots stay live for
516
+ // displacement but should remain visually static.
497
517
  const isStreamingArgs = !this.#argsComplete && (isEditLikeToolName(this.#toolName) || this.#toolName === "write");
498
518
  const isBackgroundAsyncRunning =
499
519
  (this.#result?.details as { async?: { state?: string } } | undefined)?.async?.state === "running";
@@ -502,7 +522,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
502
522
  // Detached async task progress rows are static now; progress snapshots
503
523
  // still call #maybeFreezeBackgroundTask before applying so rows settle
504
524
  // once the block leaves the live region.
505
- const needsSpinner = isStreamingArgs || isPartialTask || this.isDisplaceableBlock();
525
+ const needsSpinner = isStreamingArgs || isPartialTask || this.#displaceableByToolName === "job";
506
526
  if (needsSpinner && !this.#spinnerInterval) {
507
527
  const frameCount = theme.spinnerFrames.length;
508
528
  const frame = sharedSpinnerFrame(frameCount);
@@ -610,9 +630,9 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
610
630
  isTranscriptBlockFinalized(): boolean {
611
631
  if (this.#sealed) return true;
612
632
  if (this.#result === undefined) return false;
613
- // A displaceable waiting poll stays live: its rows are kept out of
614
- // native scrollback so a follow-up `job` call can remove the block.
615
- if (this.#displaceable) return false;
633
+ // A displaceable snapshot stays live: its rows are kept out of native
634
+ // scrollback so a follow-up tool call can remove the block.
635
+ if (this.#displaceableByToolName) return false;
616
636
  if (!this.#isPartial) return true;
617
637
  // Partial result: a background async tool is accepted to freeze (the agent
618
638
  // continues while it runs and would otherwise pin an unbounded live region);
@@ -630,7 +650,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
630
650
  * committed prefix survives the remaining transitions.
631
651
  */
632
652
  isTranscriptBlockCommitStable(): boolean {
633
- if (this.#displaceable) return false;
653
+ if (this.#displaceableByToolName) return false;
634
654
  if (this.isTranscriptBlockFinalized()) return true;
635
655
  // `provisionalPendingPreview` describes only the PENDING call preview
636
656
  // (`renderCall`, before any result): the result render may re-anchor it
@@ -658,7 +678,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
658
678
  seal(): void {
659
679
  if (this.#sealed) return;
660
680
  this.#sealed = true;
661
- this.#displaceable = false;
681
+ this.#displaceableByToolName = undefined;
662
682
  // A sealed detached task is abandoned history: settle its progress rows
663
683
  // on static gray.
664
684
  this.#backgroundTaskFrozen = true;
@@ -668,14 +688,19 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
668
688
  }
669
689
 
670
690
  /**
671
- * Whether this block is a waiting `job` poll (every watched job still
672
- * running) that has not been sealed. Such a block never finalized, so none
673
- * of its rows entered native scrollback (the ticking spinner keeps the
674
- * stable-prefix ratchet at zero) and the whole block can be removed when a
675
- * follow-up `job` call supersedes it.
691
+ * Whether this block is a supersedable result snapshot that has not been
692
+ * sealed. Such a block never finalized, so none of its rows entered native
693
+ * scrollback and the whole block can be removed when a follow-up matching
694
+ * tool call supersedes it.
676
695
  */
677
696
  isDisplaceableBlock(): boolean {
678
- return this.#displaceable && !this.#sealed;
697
+ return this.#displaceableByToolName !== undefined && !this.#sealed;
698
+ }
699
+
700
+ canBeDisplacedBy(nextToolName: string | undefined): boolean {
701
+ return (
702
+ this.#displaceableByToolName !== undefined && this.#displaceableByToolName === nextToolName && !this.#sealed
703
+ );
679
704
  }
680
705
 
681
706
  /**
@@ -77,6 +77,10 @@ export class EventController {
77
77
  // one persistent poll instead of a stack of "waiting on N jobs" frames —
78
78
  // and sealed in place the moment anything else lands below it.
79
79
  #displaceablePollComponent: ToolExecutionComponent | undefined = undefined;
80
+ // Most recent successful `todo` snapshot in the active turn. It stays live
81
+ // across intervening tool output so a later `todo` update can replace the
82
+ // old full list; the turn boundary seals the final snapshot as history.
83
+ #displaceableTodoComponent: ToolExecutionComponent | undefined = undefined;
80
84
  // Most recent TTSR notification block. A new ttsr_triggered event merges its
81
85
  // rules into this block while it is still the (live-region) transcript tail.
82
86
  #lastTtsrNotification: TtsrNotificationComponent | undefined = undefined;
@@ -226,6 +230,7 @@ export class EventController {
226
230
  this.#ircExpiryTimers.clear();
227
231
  this.#liveIrcCards.clear();
228
232
  this.#displaceablePollComponent = undefined;
233
+ this.#displaceableTodoComponent = undefined;
229
234
  this.#lastTtsrNotification = undefined;
230
235
  this.#streamingReveal.stop();
231
236
  this.#toolArgsReveal.stop();
@@ -248,6 +253,7 @@ export class EventController {
248
253
  this.#readToolCallArgs.clear();
249
254
  this.#readToolCallAssistantComponents.clear();
250
255
  this.#resetReadGroup();
256
+ this.#resolveDisplaceableTodo();
251
257
  this.#lastAssistantComponent = undefined;
252
258
  // Restore the previous turn's inline error in the transcript before dropping
253
259
  // the banner, so the error stays in history once the banner is gone.
@@ -296,6 +302,7 @@ export class EventController {
296
302
 
297
303
  this.#resetReadGroup();
298
304
  this.#resolveDisplaceablePoll();
305
+ this.#resolveDisplaceableTodo();
299
306
  const wasOptimistic = this.ctx.optimisticUserMessageSignature === signature;
300
307
  const matchedLocalSubmission = this.ctx.locallySubmittedUserSignatures.delete(signature);
301
308
  const replacesOptimistic =
@@ -423,6 +430,38 @@ export class EventController {
423
430
  this.ctx.ui.requestRender();
424
431
  }
425
432
 
433
+ #resolveDisplaceableTodo(nextToolName?: string): void {
434
+ const previous = this.#displaceableTodoComponent;
435
+ if (!previous) return;
436
+ if (!previous.isDisplaceableBlock()) {
437
+ this.#displaceableTodoComponent = undefined;
438
+ return;
439
+ }
440
+ if (previous.canBeDisplacedBy(nextToolName)) {
441
+ this.#displaceableTodoComponent = undefined;
442
+ this.ctx.chatContainer.removeChild(previous);
443
+ previous.seal();
444
+ this.ctx.ui.requestRender();
445
+ return;
446
+ }
447
+ if (nextToolName !== undefined) return;
448
+ this.#displaceableTodoComponent = undefined;
449
+ previous.seal();
450
+ this.ctx.ui.requestRender();
451
+ }
452
+
453
+ /**
454
+ * Adopt a rebuilt-tail todo snapshot as the controller's tracked live
455
+ * snapshot. Used by rebuild paths (settings/extensions overlay close, focus
456
+ * attach, /resume) to preserve displacement continuity when a turn is still
457
+ * active — without this, the next same-turn `todo` update would stack
458
+ * another panel because the controller's tracker was reset before rebuild.
459
+ * Drops the candidate when it is no longer a displaceable todo.
460
+ */
461
+ inheritDisplaceableTodo(component: ToolExecutionComponent | null | undefined): void {
462
+ this.#displaceableTodoComponent = component?.canBeDisplacedBy("todo") ? component : undefined;
463
+ }
464
+
426
465
  async #handleNotice(event: Extract<AgentSessionEvent, { type: "notice" }>): Promise<void> {
427
466
  const message = event.source ? `${event.source}: ${event.message}` : event.message;
428
467
  if (event.level === "error") {
@@ -699,8 +738,8 @@ export class EventController {
699
738
 
700
739
  async #handleToolExecutionStart(event: Extract<AgentSessionEvent, { type: "tool_execution_start" }>): Promise<void> {
701
740
  this.#updateWorkingMessageFromIntent(event.intent);
741
+ this.#resolveDisplaceablePoll(event.toolName);
702
742
  if (!this.ctx.pendingTools.has(event.toolCallId)) {
703
- this.#resolveDisplaceablePoll(event.toolName);
704
743
  if (event.toolName === "read" && readArgsHaveTarget(event.args) && !readArgsTargetInternalUrl(event.args)) {
705
744
  this.#trackReadToolCall(event.toolCallId, event.args);
706
745
  const component = this.ctx.pendingTools.get(event.toolCallId);
@@ -830,13 +869,22 @@ export class EventController {
830
869
  this.ctx.pendingTools.delete(event.toolCallId);
831
870
  this.#backgroundToolCallIds.delete(event.toolCallId);
832
871
  }
833
- if (
834
- event.toolName === "job" &&
835
- component instanceof ToolExecutionComponent &&
836
- component.isDisplaceableBlock()
837
- ) {
838
- // Remember the waiting poll so the next `job` call can displace it.
839
- this.#displaceablePollComponent = component;
872
+ if (component instanceof ToolExecutionComponent && component.isDisplaceableBlock()) {
873
+ if (event.toolName === "job" && component.canBeDisplacedBy("job")) {
874
+ // Remember the waiting poll so the next `job` call can displace it.
875
+ this.#displaceablePollComponent = component;
876
+ } else if (event.toolName === "todo" && component.canBeDisplacedBy("todo")) {
877
+ // Successful todo update supersedes the prior live snapshot. A failed
878
+ // follow-up never reaches this branch (canBeDisplacedBy("todo") returns
879
+ // false for errored results), so the last-good panel stays on screen.
880
+ const previous = this.#displaceableTodoComponent;
881
+ if (previous && previous !== component && previous.isDisplaceableBlock()) {
882
+ this.#displaceableTodoComponent = undefined;
883
+ this.ctx.chatContainer.removeChild(previous);
884
+ previous.seal();
885
+ }
886
+ this.#displaceableTodoComponent = component;
887
+ }
840
888
  }
841
889
  this.ctx.ui.requestRender();
842
890
  }
@@ -918,6 +966,7 @@ export class EventController {
918
966
  // The turn is over: nothing else lands this turn, so the waiting poll is
919
967
  // final history — seal it instead of letting its spinner tick while idle.
920
968
  this.#resolveDisplaceablePoll();
969
+ this.#resolveDisplaceableTodo();
921
970
  this.#lastAssistantComponent = undefined;
922
971
  this.ctx.ui.requestRender();
923
972
  this.#scheduleIdleCompaction();