@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.
- package/CHANGELOG.md +25 -0
- package/dist/cli.js +3795 -3760
- package/dist/types/advisor/advise-tool.d.ts +3 -0
- package/dist/types/cli/gallery-cli.d.ts +6 -0
- package/dist/types/commands/gallery.d.ts +1 -1
- package/dist/types/config/service-tier.d.ts +34 -0
- package/dist/types/config/settings-schema.d.ts +36 -33
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -0
- package/dist/types/mcp/oauth-flow.d.ts +42 -5
- package/dist/types/modes/components/custom-editor.d.ts +32 -0
- package/dist/types/modes/components/tool-execution.d.ts +5 -5
- package/dist/types/modes/controllers/event-controller.d.ts +10 -0
- package/dist/types/modes/controllers/input-controller.d.ts +2 -1
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/task/executor.d.ts +9 -1
- package/dist/types/task/parallel.d.ts +17 -1
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/utils/clipboard.d.ts +10 -0
- package/package.json +13 -13
- package/scripts/generate-legacy-pi-bundled-registry.ts +10 -0
- package/src/advisor/__tests__/advisor.test.ts +44 -0
- package/src/advisor/advise-tool.ts +33 -0
- package/src/autolearn/controller.ts +17 -2
- package/src/cli/gallery-cli.ts +31 -2
- package/src/cli/gallery-fixtures/agentic.ts +13 -4
- package/src/commands/gallery.ts +11 -3
- package/src/config/service-tier.ts +87 -0
- package/src/config/settings-schema.ts +48 -23
- package/src/eval/agent-bridge.ts +2 -0
- package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +1 -1
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +4 -4
- package/src/extensibility/plugins/legacy-pi-compat.ts +193 -5
- package/src/main.ts +1 -0
- package/src/mcp/manager.ts +12 -3
- package/src/mcp/oauth-flow.ts +121 -7
- package/src/mcp/transports/stdio.ts +5 -0
- package/src/modes/components/chat-transcript-builder.ts +31 -0
- package/src/modes/components/custom-editor.test.ts +80 -0
- package/src/modes/components/custom-editor.ts +86 -6
- package/src/modes/components/tool-execution.ts +50 -25
- package/src/modes/controllers/event-controller.ts +57 -8
- package/src/modes/controllers/input-controller.ts +213 -93
- package/src/modes/controllers/mcp-command-controller.ts +18 -2
- package/src/modes/utils/ui-helpers.ts +40 -0
- package/src/prompts/system/autolearn-nudge-autocontinue.md +5 -0
- package/src/prompts/system/autolearn-nudge.md +4 -2
- package/src/prompts/tools/todo.md +1 -1
- package/src/sdk.ts +1 -0
- package/src/session/agent-session.ts +76 -15
- package/src/session/session-history-format.ts +21 -4
- package/src/task/executor.ts +79 -2
- package/src/task/index.ts +11 -6
- package/src/task/parallel.ts +59 -7
- package/src/tools/index.ts +3 -1
- package/src/tools/irc.ts +16 -2
- package/src/tools/todo.ts +20 -10
- package/src/utils/clipboard.ts +57 -0
- package/src/utils/shell-snapshot-fn-env.sh +60 -0
- 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
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
#
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
614
|
-
//
|
|
615
|
-
if (this.#
|
|
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.#
|
|
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.#
|
|
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
|
|
672
|
-
*
|
|
673
|
-
*
|
|
674
|
-
*
|
|
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.#
|
|
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
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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();
|