@oh-my-pi/pi-coding-agent 15.11.0 → 15.11.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.
- package/CHANGELOG.md +30 -1
- package/dist/cli.js +73 -67
- package/dist/types/capability/mcp.d.ts +1 -0
- package/dist/types/config/settings-schema.d.ts +13 -4
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/oauth-flow.d.ts +6 -1
- package/dist/types/mcp/transports/stdio.d.ts +1 -0
- package/dist/types/mcp/types.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -0
- package/dist/types/modes/components/mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/settings-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +3 -0
- package/dist/types/modes/components/transcript-container.d.ts +3 -2
- package/dist/types/modes/controllers/tool-args-reveal.d.ts +43 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/task/index.d.ts +3 -3
- package/dist/types/tools/render-utils.d.ts +22 -0
- package/package.json +11 -11
- package/src/capability/mcp.ts +1 -0
- package/src/cli/gallery-cli.ts +5 -4
- package/src/config/mcp-schema.json +4 -0
- package/src/config/settings-schema.ts +15 -4
- package/src/edit/renderer.ts +96 -46
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +6 -1
- package/src/internal-urls/docs-index.generated.ts +4 -4
- package/src/mcp/manager.ts +3 -0
- package/src/mcp/oauth-discovery.ts +27 -2
- package/src/mcp/oauth-flow.ts +47 -1
- package/src/mcp/transports/stdio.ts +3 -0
- package/src/mcp/types.ts +2 -0
- package/src/modes/components/assistant-message.ts +15 -0
- package/src/modes/components/btw-panel.ts +5 -1
- package/src/modes/components/mcp-add-wizard.ts +13 -0
- package/src/modes/components/settings-selector.ts +2 -0
- package/src/modes/components/status-line/component.ts +22 -12
- package/src/modes/components/status-line/types.ts +3 -0
- package/src/modes/components/transcript-container.ts +99 -18
- package/src/modes/components/tree-selector.ts +6 -1
- package/src/modes/controllers/event-controller.ts +28 -4
- package/src/modes/controllers/mcp-command-controller.ts +34 -2
- package/src/modes/controllers/selector-controller.ts +4 -0
- package/src/modes/controllers/tool-args-reveal.ts +174 -0
- package/src/modes/interactive-mode.ts +9 -2
- package/src/modes/theme/theme.ts +6 -0
- package/src/prompts/tools/task.md +7 -2
- package/src/session/agent-session.ts +25 -4
- package/src/task/index.ts +15 -10
- package/src/task/render.ts +10 -4
- package/src/tools/render-utils.ts +56 -0
- package/src/tools/write.ts +65 -47
- package/src/web/search/providers/anthropic.ts +29 -4
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type Component,
|
|
3
|
+
Container,
|
|
4
|
+
type NativeScrollbackCommittedRows,
|
|
5
|
+
type NativeScrollbackLiveRegion,
|
|
6
|
+
type RenderStablePrefix,
|
|
7
|
+
} from "@oh-my-pi/pi-tui";
|
|
2
8
|
|
|
3
9
|
const kSnapshot = Symbol("transcript.liveDiffSnapshot");
|
|
4
10
|
|
|
5
11
|
/**
|
|
6
|
-
* Per-block
|
|
7
|
-
* derived append-only state.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
12
|
+
* Per-block render cache: the block's previous stripped contribution plus the
|
|
13
|
+
* derived append-only state. Still-live blocks use it as input to
|
|
14
|
+
* {@link deriveLiveCommitState}; finalized blocks wholly inside already
|
|
15
|
+
* committed native scrollback can replay it without calling render().
|
|
10
16
|
*/
|
|
11
17
|
interface LiveDiffSnapshot {
|
|
12
18
|
width: number;
|
|
@@ -47,6 +53,16 @@ interface SnapshotCarrier {
|
|
|
47
53
|
*/
|
|
48
54
|
interface FinalizableBlock {
|
|
49
55
|
isTranscriptBlockFinalized?(): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Monotonic content version for blocks that can still mutate *after*
|
|
58
|
+
* reporting finalized (e.g. `AssistantMessageComponent`: the inline error
|
|
59
|
+
* restored at the next turn's `agent_start`, late tool-result images). The
|
|
60
|
+
* committed-scrollback render bypass only replays a block's previous rows
|
|
61
|
+
* when the version is unchanged; without this signal a post-finalize
|
|
62
|
+
* mutation would stay invisible until a global invalidation. Blocks that
|
|
63
|
+
* never mutate post-finalize simply omit the method.
|
|
64
|
+
*/
|
|
65
|
+
getTranscriptBlockVersion?(): number;
|
|
50
66
|
}
|
|
51
67
|
|
|
52
68
|
function isBlockFinalized(child: Component): boolean {
|
|
@@ -54,6 +70,11 @@ function isBlockFinalized(child: Component): boolean {
|
|
|
54
70
|
return fn ? fn.call(child) : true;
|
|
55
71
|
}
|
|
56
72
|
|
|
73
|
+
function getBlockVersion(child: Component): number | undefined {
|
|
74
|
+
const fn = (child as Component & FinalizableBlock).getTranscriptBlockVersion;
|
|
75
|
+
return fn ? fn.call(child) : undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
57
78
|
// A "plain blank" row is empty or whitespace-only with no ANSI bytes. It marks
|
|
58
79
|
// separation padding (a `Spacer`, or a no-background `paddingY` row) as opposed
|
|
59
80
|
// to a background-colored padding row, whose escape sequences contain `\S` and
|
|
@@ -87,11 +108,16 @@ interface BlockSegment {
|
|
|
87
108
|
rawRef: readonly string[];
|
|
88
109
|
contribution: readonly string[];
|
|
89
110
|
width: number;
|
|
111
|
+
generation: number;
|
|
90
112
|
/** Frame row of this block's first emitted row (the separator when present). */
|
|
91
113
|
startRow: number;
|
|
92
114
|
/** Rows emitted: separator + contribution (0 for empty contributions). */
|
|
93
115
|
rowCount: number;
|
|
94
116
|
sep: number;
|
|
117
|
+
/** Whether the block reported finalized when this segment was rendered. */
|
|
118
|
+
finalized: boolean;
|
|
119
|
+
/** Block version observed when this segment was rendered (see {@link FinalizableBlock}). */
|
|
120
|
+
version: number | undefined;
|
|
95
121
|
}
|
|
96
122
|
|
|
97
123
|
const EMPTY_SEGMENTS: BlockSegment[] = [];
|
|
@@ -369,7 +395,10 @@ function deriveLiveCommitState(
|
|
|
369
395
|
* through {@link RenderStablePrefix} so the engine can skip marker scanning,
|
|
370
396
|
* line preparation, and the committed-prefix audit for those rows.
|
|
371
397
|
*/
|
|
372
|
-
export class TranscriptContainer
|
|
398
|
+
export class TranscriptContainer
|
|
399
|
+
extends Container
|
|
400
|
+
implements NativeScrollbackLiveRegion, NativeScrollbackCommittedRows, RenderStablePrefix
|
|
401
|
+
{
|
|
373
402
|
// Bumped to retire every block's diff snapshot at once (theme change /
|
|
374
403
|
// clear); a snapshot is only honored when its stored generation matches.
|
|
375
404
|
#generation = 0;
|
|
@@ -390,6 +419,10 @@ export class TranscriptContainer extends Container implements NativeScrollbackLi
|
|
|
390
419
|
#lines: string[] = [];
|
|
391
420
|
#segments: BlockSegment[] = EMPTY_SEGMENTS;
|
|
392
421
|
#renderWidth = -1;
|
|
422
|
+
// Local rows already committed to native scrollback by the previous frame.
|
|
423
|
+
// Finalized blocks wholly before this boundary are immutable on-screen history;
|
|
424
|
+
// their previous contribution can be replayed without calling render().
|
|
425
|
+
#committedRows = 0;
|
|
393
426
|
// Stable-prefix floor accumulated across renders since the last
|
|
394
427
|
// getRenderStablePrefixRows() read (see RenderStablePrefix: reading
|
|
395
428
|
// consumes the report and re-bases the baseline). Out-of-band renders
|
|
@@ -407,6 +440,10 @@ export class TranscriptContainer extends Container implements NativeScrollbackLi
|
|
|
407
440
|
super.clear();
|
|
408
441
|
}
|
|
409
442
|
|
|
443
|
+
setNativeScrollbackCommittedRows(rows: number): void {
|
|
444
|
+
this.#committedRows = Number.isFinite(rows) ? Math.max(0, Math.trunc(rows)) : 0;
|
|
445
|
+
}
|
|
446
|
+
|
|
410
447
|
getRenderStablePrefixRows(): number {
|
|
411
448
|
const value = Math.min(this.#stableRowsFloor, this.#lines.length);
|
|
412
449
|
this.#stableRowsFloor = this.#lines.length;
|
|
@@ -497,21 +534,43 @@ export class TranscriptContainer extends Container implements NativeScrollbackLi
|
|
|
497
534
|
|
|
498
535
|
// This child's contribution: its current render with plain-blank
|
|
499
536
|
// top/bottom edges stripped (the container owns inter-block gaps).
|
|
500
|
-
//
|
|
501
|
-
//
|
|
502
|
-
//
|
|
503
|
-
//
|
|
504
|
-
//
|
|
537
|
+
// Finalized blocks wholly inside committed native scrollback can reuse
|
|
538
|
+
// their previous contribution without calling render(): those rows are
|
|
539
|
+
// immutable terminal history for the current width/generation. Blocks
|
|
540
|
+
// outside committed history still render normally so late results,
|
|
541
|
+
// post-finalize re-layouts, and expand toggles remain visible.
|
|
505
542
|
const previousSnapshot = child[kSnapshot];
|
|
506
|
-
const raw = child.render(width);
|
|
507
543
|
const previous = previousSegments[i];
|
|
508
|
-
const
|
|
544
|
+
const finalized = isBlockFinalized(child);
|
|
545
|
+
const version = getBlockVersion(child);
|
|
546
|
+
const committedReusable =
|
|
509
547
|
previous !== undefined &&
|
|
510
548
|
previous.component === child &&
|
|
511
|
-
previous.
|
|
512
|
-
previous.
|
|
549
|
+
previous.width === width &&
|
|
550
|
+
previous.generation === this.#generation &&
|
|
551
|
+
previous.startRow === row &&
|
|
552
|
+
previous.startRow + previous.rowCount <= this.#committedRows &&
|
|
553
|
+
finalized &&
|
|
554
|
+
// Only replay bytes that were themselves produced by a finalized
|
|
555
|
+
// render: a block finalizing between frames may have changed content
|
|
556
|
+
// while its rows were already committed via the append-only live
|
|
557
|
+
// path, so the first post-transition frame must render. Defense in
|
|
558
|
+
// depth on the transcript side — the TUI commit policy should keep
|
|
559
|
+
// that window closed, but the safety must not live there alone.
|
|
560
|
+
previous.finalized &&
|
|
561
|
+
// Post-finalize mutations (inline error restore, late tool images)
|
|
562
|
+
// bump the block version; a mismatch forces a real render so the
|
|
563
|
+
// committed-prefix audit can observe and re-anchor the change.
|
|
564
|
+
previous.version === version;
|
|
565
|
+
const raw = committedReusable ? previous.rawRef : child.render(width);
|
|
566
|
+
const reusable =
|
|
567
|
+
committedReusable ||
|
|
568
|
+
(previous !== undefined &&
|
|
569
|
+
previous.component === child &&
|
|
570
|
+
previous.rawRef === raw &&
|
|
571
|
+
previous.width === width &&
|
|
572
|
+
previous.generation === this.#generation);
|
|
513
573
|
const contribution = reusable ? previous.contribution : stripPlainBlankEdges(raw);
|
|
514
|
-
const finalized = isBlockFinalized(child);
|
|
515
574
|
let liveCommitState: LiveCommitState | undefined;
|
|
516
575
|
if (i >= liveStartIndex && !finalized) {
|
|
517
576
|
liveCommitState = deriveLiveCommitState(previousSnapshot, contribution, width, this.#generation);
|
|
@@ -540,7 +599,18 @@ export class TranscriptContainer extends Container implements NativeScrollbackLi
|
|
|
540
599
|
lines.length = row;
|
|
541
600
|
}
|
|
542
601
|
if (chainStable) stableRows = row;
|
|
543
|
-
segments[i] = {
|
|
602
|
+
segments[i] = {
|
|
603
|
+
component: child,
|
|
604
|
+
rawRef: raw,
|
|
605
|
+
contribution,
|
|
606
|
+
width,
|
|
607
|
+
generation: this.#generation,
|
|
608
|
+
startRow: row,
|
|
609
|
+
rowCount: 0,
|
|
610
|
+
sep: 0,
|
|
611
|
+
finalized,
|
|
612
|
+
version,
|
|
613
|
+
};
|
|
544
614
|
continue;
|
|
545
615
|
}
|
|
546
616
|
|
|
@@ -584,7 +654,18 @@ export class TranscriptContainer extends Container implements NativeScrollbackLi
|
|
|
584
654
|
if (!(finalized && safeLength >= contribution.length)) commitSafeOpen = false;
|
|
585
655
|
}
|
|
586
656
|
|
|
587
|
-
segments[i] = {
|
|
657
|
+
segments[i] = {
|
|
658
|
+
component: child,
|
|
659
|
+
rawRef: raw,
|
|
660
|
+
contribution,
|
|
661
|
+
width,
|
|
662
|
+
generation: this.#generation,
|
|
663
|
+
startRow: row,
|
|
664
|
+
rowCount,
|
|
665
|
+
sep,
|
|
666
|
+
finalized,
|
|
667
|
+
version,
|
|
668
|
+
};
|
|
588
669
|
row += rowCount;
|
|
589
670
|
}
|
|
590
671
|
// Trailing shrink: blocks removed from the tail leave stale rows behind
|
|
@@ -518,6 +518,7 @@ class TreeList implements Component {
|
|
|
518
518
|
const renderedIndent = Math.min(displayIndent, maxIndentLevels);
|
|
519
519
|
const scrollOffset = displayIndent - renderedIndent;
|
|
520
520
|
const connectorPositionDisplay = hasConnector ? renderedIndent - 1 : -1;
|
|
521
|
+
const chainGutter = !hasConnector ? flatNode.gutters[flatNode.gutters.length - 1] : undefined;
|
|
521
522
|
|
|
522
523
|
// Build prefix char by char, placing gutters and connector at their positions
|
|
523
524
|
const totalChars = renderedIndent * 3;
|
|
@@ -530,8 +531,12 @@ class TreeList implements Component {
|
|
|
530
531
|
// Check if there's a gutter at this level (translated to original tree depth)
|
|
531
532
|
const gutter = flatNode.gutters.find(g => g.position === originalLevel);
|
|
532
533
|
if (gutter) {
|
|
534
|
+
// Chain rows (no connector of their own) extend only their
|
|
535
|
+
// nearest connector gutter so the flattened conversation flow
|
|
536
|
+
// stays anchored without reviving unrelated `└─` ancestors (#2298).
|
|
537
|
+
const showVertical = gutter.show || gutter === chainGutter;
|
|
533
538
|
if (posInLevel === 0) {
|
|
534
|
-
prefixChars.push(
|
|
539
|
+
prefixChars.push(showVertical ? theme.tree.vertical : " ");
|
|
535
540
|
} else {
|
|
536
541
|
prefixChars.push(" ");
|
|
537
542
|
}
|
|
@@ -21,6 +21,7 @@ import { isSilentAbort, readPendingDisplayTag, resolveAbortLabel } from "../../s
|
|
|
21
21
|
import type { ResolveToolDetails } from "../../tools/resolve";
|
|
22
22
|
import { interruptHint } from "../shared";
|
|
23
23
|
import { StreamingRevealController } from "./streaming-reveal";
|
|
24
|
+
import { ToolArgsRevealController } from "./tool-args-reveal";
|
|
24
25
|
|
|
25
26
|
type AgentSessionEventKind = AgentSessionEvent["type"];
|
|
26
27
|
|
|
@@ -86,6 +87,7 @@ export class EventController {
|
|
|
86
87
|
// rules into this block while it is still the (live-region) transcript tail.
|
|
87
88
|
#lastTtsrNotification: TtsrNotificationComponent | undefined = undefined;
|
|
88
89
|
#streamingReveal: StreamingRevealController;
|
|
90
|
+
#toolArgsReveal: ToolArgsRevealController;
|
|
89
91
|
#handlers: AgentSessionEventHandlers;
|
|
90
92
|
|
|
91
93
|
constructor(private ctx: InteractiveModeContext) {
|
|
@@ -94,6 +96,10 @@ export class EventController {
|
|
|
94
96
|
getHideThinkingBlock: () => this.ctx.hideThinkingBlock,
|
|
95
97
|
requestRender: () => this.ctx.ui.requestRender(),
|
|
96
98
|
});
|
|
99
|
+
this.#toolArgsReveal = new ToolArgsRevealController({
|
|
100
|
+
getSmoothStreaming: () => this.ctx.settings.get("display.smoothStreaming"),
|
|
101
|
+
requestRender: () => this.ctx.ui.requestRender(),
|
|
102
|
+
});
|
|
97
103
|
this.#handlers = {
|
|
98
104
|
agent_start: e => this.#handleAgentStart(e),
|
|
99
105
|
agent_end: e => this.#handleAgentEnd(e),
|
|
@@ -127,6 +133,7 @@ export class EventController {
|
|
|
127
133
|
|
|
128
134
|
dispose(): void {
|
|
129
135
|
this.#streamingReveal.stop();
|
|
136
|
+
this.#toolArgsReveal.stop();
|
|
130
137
|
this.#cancelIdleCompaction();
|
|
131
138
|
for (const timer of this.#ircExpiryTimers.values()) {
|
|
132
139
|
clearTimeout(timer);
|
|
@@ -492,10 +499,23 @@ export class EventController {
|
|
|
492
499
|
|
|
493
500
|
// Preserve the raw partial JSON for renderers that need to surface fields before the JSON object closes.
|
|
494
501
|
// Bash uses this to show inline env assignments during streaming instead of popping them in at completion.
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
502
|
+
// While the JSON is still open, ToolArgsRevealController paces the
|
|
503
|
+
// reveal (write/edit/bash previews grow smoothly when a slow provider
|
|
504
|
+
// delivers large batches); once it closes, the final args render
|
|
505
|
+
// as-is — mirroring how assistant text snaps at message_end.
|
|
506
|
+
const partialJson = "partialJson" in content ? content.partialJson : undefined;
|
|
507
|
+
let renderArgs: Record<string, unknown>;
|
|
508
|
+
if (typeof partialJson === "string") {
|
|
509
|
+
renderArgs = this.#toolArgsReveal.setTarget(
|
|
510
|
+
content.id,
|
|
511
|
+
partialJson,
|
|
512
|
+
content.customWireName !== undefined,
|
|
513
|
+
content.arguments,
|
|
514
|
+
);
|
|
515
|
+
} else {
|
|
516
|
+
this.#toolArgsReveal.finish(content.id);
|
|
517
|
+
renderArgs = content.arguments;
|
|
518
|
+
}
|
|
499
519
|
if (!this.ctx.pendingTools.has(content.id)) {
|
|
500
520
|
this.#resolveDisplaceablePoll(content.name);
|
|
501
521
|
this.#resetReadGroup();
|
|
@@ -517,10 +537,12 @@ export class EventController {
|
|
|
517
537
|
component.setExpanded(this.ctx.toolOutputExpanded);
|
|
518
538
|
this.ctx.chatContainer.addChild(component);
|
|
519
539
|
this.ctx.pendingTools.set(content.id, component);
|
|
540
|
+
this.#toolArgsReveal.bind(content.id, component);
|
|
520
541
|
} else {
|
|
521
542
|
const component = this.ctx.pendingTools.get(content.id);
|
|
522
543
|
if (component) {
|
|
523
544
|
component.updateArgs(renderArgs, content.id);
|
|
545
|
+
this.#toolArgsReveal.bind(content.id, component);
|
|
524
546
|
}
|
|
525
547
|
}
|
|
526
548
|
}
|
|
@@ -555,6 +577,7 @@ export class EventController {
|
|
|
555
577
|
if (this.ctx.streamingComponent && event.message.role === "assistant") {
|
|
556
578
|
this.ctx.streamingMessage = event.message;
|
|
557
579
|
this.#streamingReveal.stop();
|
|
580
|
+
this.#toolArgsReveal.flushAll();
|
|
558
581
|
let errorMessage: string | undefined;
|
|
559
582
|
const aborted = this.ctx.streamingMessage.stopReason === "aborted";
|
|
560
583
|
const silentlyAborted = aborted && isSilentAbort(this.ctx.streamingMessage.errorMessage);
|
|
@@ -772,6 +795,7 @@ export class EventController {
|
|
|
772
795
|
async #handleAgentEnd(_event: Extract<AgentSessionEvent, { type: "agent_end" }>): Promise<void> {
|
|
773
796
|
this.#agentTurnActive = false;
|
|
774
797
|
this.#streamingReveal.stop();
|
|
798
|
+
this.#toolArgsReveal.flushAll();
|
|
775
799
|
if (this.ctx.loadingAnimation) {
|
|
776
800
|
this.ctx.loadingAnimation.stop();
|
|
777
801
|
this.ctx.loadingAnimation = undefined;
|
|
@@ -127,6 +127,7 @@ interface OAuthFlowResult {
|
|
|
127
127
|
credentialId: string;
|
|
128
128
|
clientId?: string;
|
|
129
129
|
clientSecret?: string;
|
|
130
|
+
resource?: string;
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
type MCPAddScope = "user" | "project";
|
|
@@ -490,6 +491,7 @@ export class MCPCommandController {
|
|
|
490
491
|
|
|
491
492
|
try {
|
|
492
493
|
const oauthClientSecret = finalConfig.oauth?.clientSecret ?? "";
|
|
494
|
+
const oauthResource = oauth.resource ?? finalConfig.url;
|
|
493
495
|
const oauthResult = await this.#handleOAuthFlow(
|
|
494
496
|
oauth.authorizationUrl,
|
|
495
497
|
oauth.tokenUrl,
|
|
@@ -499,15 +501,18 @@ export class MCPCommandController {
|
|
|
499
501
|
finalConfig.oauth?.callbackPort,
|
|
500
502
|
finalConfig.oauth?.callbackPath,
|
|
501
503
|
finalConfig.oauth?.redirectUri,
|
|
504
|
+
oauthResource,
|
|
502
505
|
);
|
|
503
506
|
const persistedClientId = oauthResult.clientId ?? oauth.clientId ?? finalConfig.oauth?.clientId;
|
|
504
507
|
const persistedClientSecret = oauthResult.clientSecret ?? finalConfig.oauth?.clientSecret;
|
|
508
|
+
const persistedResource = oauthResult.resource ?? oauthResource;
|
|
505
509
|
finalConfig = {
|
|
506
510
|
...finalConfig,
|
|
507
511
|
auth: {
|
|
508
512
|
type: "oauth",
|
|
509
513
|
credentialId: oauthResult.credentialId,
|
|
510
514
|
tokenUrl: oauth.tokenUrl,
|
|
515
|
+
resource: persistedResource,
|
|
511
516
|
clientId: persistedClientId,
|
|
512
517
|
clientSecret: persistedClientSecret,
|
|
513
518
|
},
|
|
@@ -548,8 +553,25 @@ export class MCPCommandController {
|
|
|
548
553
|
done();
|
|
549
554
|
this.#handleWizardCancel();
|
|
550
555
|
},
|
|
551
|
-
async (
|
|
552
|
-
|
|
556
|
+
async (
|
|
557
|
+
authUrl: string,
|
|
558
|
+
tokenUrl: string,
|
|
559
|
+
clientId: string,
|
|
560
|
+
clientSecret: string,
|
|
561
|
+
scopes: string,
|
|
562
|
+
resource?: string,
|
|
563
|
+
) => {
|
|
564
|
+
return await this.#handleOAuthFlow(
|
|
565
|
+
authUrl,
|
|
566
|
+
tokenUrl,
|
|
567
|
+
clientId,
|
|
568
|
+
clientSecret,
|
|
569
|
+
scopes,
|
|
570
|
+
undefined,
|
|
571
|
+
undefined,
|
|
572
|
+
undefined,
|
|
573
|
+
resource,
|
|
574
|
+
);
|
|
553
575
|
},
|
|
554
576
|
async (config: MCPServerConfig) => {
|
|
555
577
|
return await this.#handleTestConnection(config);
|
|
@@ -579,6 +601,7 @@ export class MCPCommandController {
|
|
|
579
601
|
callbackPort?: number,
|
|
580
602
|
callbackPath?: string,
|
|
581
603
|
redirectUri?: string,
|
|
604
|
+
resource?: string,
|
|
582
605
|
): Promise<OAuthFlowResult> {
|
|
583
606
|
const authStorage = this.ctx.session.modelRegistry.authStorage;
|
|
584
607
|
let parsedAuthUrl: URL;
|
|
@@ -617,6 +640,7 @@ export class MCPCommandController {
|
|
|
617
640
|
redirectUri,
|
|
618
641
|
callbackPort,
|
|
619
642
|
callbackPath,
|
|
643
|
+
resource,
|
|
620
644
|
},
|
|
621
645
|
{
|
|
622
646
|
onAuth: (info: { url: string; instructions?: string }) => {
|
|
@@ -704,6 +728,7 @@ export class MCPCommandController {
|
|
|
704
728
|
credentialId,
|
|
705
729
|
clientId: flow.resolvedClientId,
|
|
706
730
|
clientSecret: flow.registeredClientSecret,
|
|
731
|
+
resource: flow.resource,
|
|
707
732
|
};
|
|
708
733
|
} catch (error) {
|
|
709
734
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
@@ -804,6 +829,7 @@ export class MCPCommandController {
|
|
|
804
829
|
tokenUrl: string;
|
|
805
830
|
clientId?: string;
|
|
806
831
|
scopes?: string;
|
|
832
|
+
resource?: string;
|
|
807
833
|
}> {
|
|
808
834
|
// First test if server actually needs auth by connecting without OAuth
|
|
809
835
|
let connectionSucceeded = false;
|
|
@@ -1415,6 +1441,9 @@ export class MCPCommandController {
|
|
|
1415
1441
|
|
|
1416
1442
|
this.#showMessage(["", theme.fg("muted", `Reauthorizing "${name}"...`), ""].join("\n"));
|
|
1417
1443
|
|
|
1444
|
+
const oauthResource =
|
|
1445
|
+
oauth.resource ?? currentAuth?.resource ?? ("url" in baseConfig ? baseConfig.url : undefined);
|
|
1446
|
+
|
|
1418
1447
|
const oauthResult = await this.#handleOAuthFlow(
|
|
1419
1448
|
oauth.authorizationUrl,
|
|
1420
1449
|
oauth.tokenUrl,
|
|
@@ -1424,10 +1453,12 @@ export class MCPCommandController {
|
|
|
1424
1453
|
found.config.oauth?.callbackPort,
|
|
1425
1454
|
found.config.oauth?.callbackPath,
|
|
1426
1455
|
found.config.oauth?.redirectUri,
|
|
1456
|
+
oauthResource,
|
|
1427
1457
|
);
|
|
1428
1458
|
|
|
1429
1459
|
const persistedClientId = oauthResult.clientId ?? oauth.clientId ?? found.config.oauth?.clientId;
|
|
1430
1460
|
const persistedClientSecret = oauthResult.clientSecret ?? (oauthClientSecret || undefined);
|
|
1461
|
+
const persistedResource = oauthResult.resource ?? oauthResource;
|
|
1431
1462
|
|
|
1432
1463
|
const updated: MCPServerConfig = {
|
|
1433
1464
|
...baseConfig,
|
|
@@ -1435,6 +1466,7 @@ export class MCPCommandController {
|
|
|
1435
1466
|
type: "oauth",
|
|
1436
1467
|
credentialId: oauthResult.credentialId,
|
|
1437
1468
|
tokenUrl: oauth.tokenUrl,
|
|
1469
|
+
resource: persistedResource,
|
|
1438
1470
|
clientId: persistedClientId,
|
|
1439
1471
|
clientSecret: persistedClientSecret,
|
|
1440
1472
|
},
|
|
@@ -120,6 +120,7 @@ export class SelectorController {
|
|
|
120
120
|
separator: settings.get("statusLine.separator"),
|
|
121
121
|
showHookStatus: settings.get("statusLine.showHookStatus"),
|
|
122
122
|
sessionAccent: settings.get("statusLine.sessionAccent"),
|
|
123
|
+
transparent: settings.get("statusLine.transparent"),
|
|
123
124
|
...previewSettings,
|
|
124
125
|
});
|
|
125
126
|
this.ctx.updateEditorTopBorder();
|
|
@@ -147,6 +148,7 @@ export class SelectorController {
|
|
|
147
148
|
separator: settings.get("statusLine.separator"),
|
|
148
149
|
showHookStatus: settings.get("statusLine.showHookStatus"),
|
|
149
150
|
sessionAccent: settings.get("statusLine.sessionAccent"),
|
|
151
|
+
transparent: settings.get("statusLine.transparent"),
|
|
150
152
|
});
|
|
151
153
|
this.ctx.updateEditorTopBorder();
|
|
152
154
|
this.ctx.ui.requestRender();
|
|
@@ -351,6 +353,7 @@ export class SelectorController {
|
|
|
351
353
|
case "statusLineShowHooks":
|
|
352
354
|
case "statusLine.showHookStatus":
|
|
353
355
|
case "statusLine.sessionAccent":
|
|
356
|
+
case "statusLine.transparent":
|
|
354
357
|
case "statusLineSegments":
|
|
355
358
|
case "statusLineModelThinking":
|
|
356
359
|
case "statusLinePathAbbreviate":
|
|
@@ -369,6 +372,7 @@ export class SelectorController {
|
|
|
369
372
|
separator: settings.get("statusLine.separator"),
|
|
370
373
|
showHookStatus: settings.get("statusLine.showHookStatus"),
|
|
371
374
|
sessionAccent: settings.get("statusLine.sessionAccent"),
|
|
375
|
+
transparent: settings.get("statusLine.transparent"),
|
|
372
376
|
segmentOptions: settings.get("statusLine.segmentOptions"),
|
|
373
377
|
};
|
|
374
378
|
this.ctx.statusLine.updateSettings(statusLineSettings);
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { parseStreamingJson } from "@oh-my-pi/pi-ai/utils/json-parse";
|
|
2
|
+
import { nextStep, STREAMING_REVEAL_FRAME_MS } from "./streaming-reveal";
|
|
3
|
+
|
|
4
|
+
/** Minimal component surface the reveal pushes frames into. */
|
|
5
|
+
type ToolArgsRevealComponent = {
|
|
6
|
+
updateArgs(args: unknown, toolCallId?: string): void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type ToolArgsRevealControllerOptions = {
|
|
10
|
+
getSmoothStreaming(): boolean;
|
|
11
|
+
requestRender(): void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type RevealEntry = {
|
|
15
|
+
component: ToolArgsRevealComponent | undefined;
|
|
16
|
+
/** Latest raw streamed argument text (JSON for function tools, raw text for custom tools). */
|
|
17
|
+
target: string;
|
|
18
|
+
/** Revealed UTF-16 code units of `target`. */
|
|
19
|
+
revealed: number;
|
|
20
|
+
/** Custom-tool raw input: display args are `{ input: prefix }`, never parsed as JSON. */
|
|
21
|
+
rawInput: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Clamp a slice end into `text`, never splitting a surrogate pair: a prefix
|
|
25
|
+
* ending on a high surrogate would feed a lone surrogate into the parsed
|
|
26
|
+
* preview args (providers decode UTF-8 incrementally, so the raw stream
|
|
27
|
+
* itself never contains one). */
|
|
28
|
+
function clampSliceEnd(text: string, end: number): number {
|
|
29
|
+
if (end <= 0) return 0;
|
|
30
|
+
if (end >= text.length) return text.length;
|
|
31
|
+
const code = text.charCodeAt(end - 1);
|
|
32
|
+
return code >= 0xd800 && code <= 0xdbff ? end + 1 : end;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Display args for a revealed raw-stream prefix. Function-tool prefixes are
|
|
36
|
+
* re-parsed with the same streaming-tolerant parser providers use, so every
|
|
37
|
+
* frame is a state the provider itself could have produced; custom tools
|
|
38
|
+
* mirror the provider's `{ input }` shape. `__partialJson` carries the
|
|
39
|
+
* matching raw prefix for renderers that read it directly (bash env preview,
|
|
40
|
+
* edit strategies). */
|
|
41
|
+
function buildDisplayArgs(prefix: string, rawInput: boolean): Record<string, unknown> {
|
|
42
|
+
const base: Record<string, unknown> = rawInput ? { input: prefix } : parseStreamingJson(prefix);
|
|
43
|
+
return { ...base, __partialJson: prefix };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Paces streamed tool-call arguments the same way StreamingRevealController
|
|
48
|
+
* paces assistant text: providers that deliver `partialJson` in large batches
|
|
49
|
+
* (or throttle their partial parses) would otherwise make write/edit/bash
|
|
50
|
+
* streaming previews jump in chunks. Each pending tool call reveals its raw
|
|
51
|
+
* argument stream at the shared 30fps cadence with the same adaptive
|
|
52
|
+
* catch-up step, re-parsing the revealed prefix per frame.
|
|
53
|
+
*
|
|
54
|
+
* Reveal units are UTF-16 code units of the raw stream, not graphemes —
|
|
55
|
+
* the prefix goes through a JSON parser rather than straight to the screen,
|
|
56
|
+
* so only surrogate-pair integrity matters (see {@link clampSliceEnd}).
|
|
57
|
+
*/
|
|
58
|
+
export class ToolArgsRevealController {
|
|
59
|
+
readonly #getSmoothStreaming: () => boolean;
|
|
60
|
+
readonly #requestRender: () => void;
|
|
61
|
+
readonly #entries = new Map<string, RevealEntry>();
|
|
62
|
+
#timer: NodeJS.Timeout | undefined;
|
|
63
|
+
|
|
64
|
+
constructor(options: ToolArgsRevealControllerOptions) {
|
|
65
|
+
this.#getSmoothStreaming = options.getSmoothStreaming;
|
|
66
|
+
this.#requestRender = options.requestRender;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Record the latest streamed argument text for a tool call and return the
|
|
71
|
+
* args to render right now. With smoothing disabled the full target passes
|
|
72
|
+
* through in the caller's legacy shape (`{ ...args, __partialJson }`).
|
|
73
|
+
*/
|
|
74
|
+
setTarget(
|
|
75
|
+
id: string,
|
|
76
|
+
partialJson: string,
|
|
77
|
+
rawInput: boolean,
|
|
78
|
+
fullArgs: Record<string, unknown>,
|
|
79
|
+
): Record<string, unknown> {
|
|
80
|
+
if (!this.#getSmoothStreaming()) {
|
|
81
|
+
// Toggle may flip mid-call: drop any live entry so ticks stop.
|
|
82
|
+
this.#entries.delete(id);
|
|
83
|
+
return { ...fullArgs, __partialJson: partialJson };
|
|
84
|
+
}
|
|
85
|
+
let entry = this.#entries.get(id);
|
|
86
|
+
if (!entry) {
|
|
87
|
+
entry = { component: undefined, target: partialJson, revealed: 0, rawInput };
|
|
88
|
+
this.#entries.set(id, entry);
|
|
89
|
+
} else {
|
|
90
|
+
// Streams only append; a non-prefix target means a rewind — snap into range.
|
|
91
|
+
if (!partialJson.startsWith(entry.target)) {
|
|
92
|
+
entry.revealed = Math.min(entry.revealed, partialJson.length);
|
|
93
|
+
}
|
|
94
|
+
entry.target = partialJson;
|
|
95
|
+
}
|
|
96
|
+
entry.revealed = clampSliceEnd(entry.target, entry.revealed);
|
|
97
|
+
this.#syncTimer();
|
|
98
|
+
return buildDisplayArgs(entry.target.slice(0, entry.revealed), entry.rawInput);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Attach the component future ticks push frames into. */
|
|
102
|
+
bind(id: string, component: ToolArgsRevealComponent): void {
|
|
103
|
+
const entry = this.#entries.get(id);
|
|
104
|
+
if (entry) entry.component = component;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Final arguments arrived (the JSON closed): drop the reveal so the
|
|
108
|
+
* caller's final-args render wins immediately, mirroring how assistant
|
|
109
|
+
* text snaps to the full message at message_end. */
|
|
110
|
+
finish(id: string): void {
|
|
111
|
+
this.#entries.delete(id);
|
|
112
|
+
if (this.#entries.size === 0) this.#stopTimer();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Snap every live entry to its full received stream and clear. Used at
|
|
116
|
+
* message_end (abort/error mid-stream) so sealed components freeze showing
|
|
117
|
+
* everything that arrived rather than a mid-reveal prefix. */
|
|
118
|
+
flushAll(): void {
|
|
119
|
+
for (const [id, entry] of this.#entries) {
|
|
120
|
+
if (entry.component && entry.revealed < entry.target.length) {
|
|
121
|
+
entry.component.updateArgs(buildDisplayArgs(entry.target, entry.rawInput), id);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
this.#entries.clear();
|
|
125
|
+
this.#stopTimer();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Clear without pushing (teardown). */
|
|
129
|
+
stop(): void {
|
|
130
|
+
this.#entries.clear();
|
|
131
|
+
this.#stopTimer();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#syncTimer(): void {
|
|
135
|
+
for (const entry of this.#entries.values()) {
|
|
136
|
+
if (entry.revealed < entry.target.length) {
|
|
137
|
+
this.#startTimer();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
this.#stopTimer();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
#startTimer(): void {
|
|
145
|
+
if (this.#timer) return;
|
|
146
|
+
this.#timer = setInterval(() => {
|
|
147
|
+
this.#tick();
|
|
148
|
+
}, STREAMING_REVEAL_FRAME_MS);
|
|
149
|
+
this.#timer.unref?.();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#stopTimer(): void {
|
|
153
|
+
if (!this.#timer) return;
|
|
154
|
+
clearInterval(this.#timer);
|
|
155
|
+
this.#timer = undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
#tick(): void {
|
|
159
|
+
let advanced = false;
|
|
160
|
+
for (const [id, entry] of this.#entries) {
|
|
161
|
+
const backlog = entry.target.length - entry.revealed;
|
|
162
|
+
if (backlog <= 0 || !entry.component) continue;
|
|
163
|
+
entry.revealed = clampSliceEnd(entry.target, entry.revealed + nextStep(backlog));
|
|
164
|
+
entry.component.updateArgs(buildDisplayArgs(entry.target.slice(0, entry.revealed), entry.rawInput), id);
|
|
165
|
+
advanced = true;
|
|
166
|
+
}
|
|
167
|
+
if (advanced) {
|
|
168
|
+
this.#requestRender();
|
|
169
|
+
} else {
|
|
170
|
+
// Every entry caught up (or unbound); setTarget restarts on growth.
|
|
171
|
+
this.#stopTimer();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -496,8 +496,12 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
496
496
|
}
|
|
497
497
|
|
|
498
498
|
playWelcomeIntro(): void {
|
|
499
|
-
this.#welcomeComponent
|
|
499
|
+
const welcome = this.#welcomeComponent;
|
|
500
|
+
// Component-scoped: the intro only mutates the welcome box's own rows,
|
|
501
|
+
// so a resumed long transcript is not re-walked per animation frame.
|
|
502
|
+
welcome?.playIntro(() => this.ui.requestComponentRender(welcome));
|
|
500
503
|
}
|
|
504
|
+
|
|
501
505
|
async init(options: InteractiveModeInitOptions = {}): Promise<void> {
|
|
502
506
|
if (this.isInitialized) return;
|
|
503
507
|
|
|
@@ -1050,6 +1054,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1050
1054
|
separator: settings.get("statusLine.separator"),
|
|
1051
1055
|
showHookStatus: settings.get("statusLine.showHookStatus"),
|
|
1052
1056
|
sessionAccent: settings.get("statusLine.sessionAccent"),
|
|
1057
|
+
transparent: settings.get("statusLine.transparent"),
|
|
1053
1058
|
segmentOptions: settings.get("statusLine.segmentOptions"),
|
|
1054
1059
|
});
|
|
1055
1060
|
}
|
|
@@ -3036,7 +3041,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3036
3041
|
this.#voiceAnimationInterval = setInterval(() => {
|
|
3037
3042
|
this.#voiceHue = (this.#voiceHue + 8) % 360;
|
|
3038
3043
|
this.#updateMicIcon();
|
|
3039
|
-
|
|
3044
|
+
// Component-scoped: the hue sweep only recolors the editor's cursor
|
|
3045
|
+
// glyph, so the transcript subtree is reused per animation frame.
|
|
3046
|
+
this.ui.requestComponentRender(this.editor);
|
|
3040
3047
|
}, 60);
|
|
3041
3048
|
}
|
|
3042
3049
|
|