@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
package/src/modes/theme/theme.ts
CHANGED
|
@@ -108,6 +108,7 @@ export type SymbolKey =
|
|
|
108
108
|
| "icon.time"
|
|
109
109
|
| "icon.pi"
|
|
110
110
|
| "icon.agents"
|
|
111
|
+
| "icon.job"
|
|
111
112
|
| "icon.cache"
|
|
112
113
|
| "icon.input"
|
|
113
114
|
| "icon.output"
|
|
@@ -304,6 +305,7 @@ const UNICODE_SYMBOLS: SymbolMap = {
|
|
|
304
305
|
"icon.time": "⏱",
|
|
305
306
|
"icon.pi": "π",
|
|
306
307
|
"icon.agents": "👥",
|
|
308
|
+
"icon.job": "⚙",
|
|
307
309
|
"icon.cache": "💾",
|
|
308
310
|
"icon.input": "⤵",
|
|
309
311
|
"icon.output": "⤴",
|
|
@@ -567,6 +569,8 @@ const NERD_SYMBOLS: SymbolMap = {
|
|
|
567
569
|
"icon.pi": "\ue22c",
|
|
568
570
|
// pick: | alt:
|
|
569
571
|
"icon.agents": "\uf0c0",
|
|
572
|
+
// pick: (nf-fa-gear) | alt: ⚙
|
|
573
|
+
"icon.job": "\uf013",
|
|
570
574
|
// pick: | alt:
|
|
571
575
|
"icon.cache": "\uf1c0",
|
|
572
576
|
// pick: | alt: →
|
|
@@ -796,6 +800,7 @@ const ASCII_SYMBOLS: SymbolMap = {
|
|
|
796
800
|
"icon.time": "t:",
|
|
797
801
|
"icon.pi": "pi",
|
|
798
802
|
"icon.agents": "AG",
|
|
803
|
+
"icon.job": "bg",
|
|
799
804
|
"icon.cache": "cache",
|
|
800
805
|
"icon.input": "in:",
|
|
801
806
|
"icon.output": "out:",
|
|
@@ -1678,6 +1683,7 @@ export class Theme {
|
|
|
1678
1683
|
time: this.#symbols["icon.time"],
|
|
1679
1684
|
pi: this.#symbols["icon.pi"],
|
|
1680
1685
|
agents: this.#symbols["icon.agents"],
|
|
1686
|
+
job: this.#symbols["icon.job"],
|
|
1681
1687
|
cache: this.#symbols["icon.cache"],
|
|
1682
1688
|
input: this.#symbols["icon.input"],
|
|
1683
1689
|
output: this.#symbols["icon.output"],
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
{{#if batchEnabled}}Spawns subagents to work in the background — one per `tasks[]` item; a single spawn is a one-item batch.{{else}}Spawns ONE subagent per call to work in the background.{{/if}}
|
|
1
|
+
{{#if asyncEnabled}}{{#if batchEnabled}}Spawns subagents to work in the background — one per `tasks[]` item; a single spawn is a one-item batch.{{else}}Spawns ONE subagent per call to work in the background.{{/if}}
|
|
2
2
|
|
|
3
3
|
- Spawning is non-blocking: the call returns immediately with the agent id{{#if batchEnabled}}s{{/if}} and job id{{#if batchEnabled}}s{{/if}}; each result is delivered automatically when that agent yields.
|
|
4
4
|
- Parallelism = {{#if batchEnabled}}`tasks[]` items in one call, and/or multiple `task` calls in one assistant message{{else}}multiple `task` calls in one assistant message{{/if}}. Concurrency is bounded at {{MAX_CONCURRENCY}} running subagents per session.
|
|
5
5
|
- If genuinely blocked on a result, wait with `job poll`; otherwise keep working. `job cancel` terminates a task and **cannot carry a message** — only for stalled/abandoned work.
|
|
6
|
+
{{else}}{{#if batchEnabled}}Runs subagents synchronously — one per `tasks[]` item; a single spawn is a one-item batch.{{else}}Runs ONE subagent synchronously per call.{{/if}}
|
|
7
|
+
|
|
8
|
+
- Spawning is blocking: the call returns only after the agent{{#if batchEnabled}}s{{/if}} finish; results arrive inline.
|
|
9
|
+
- Parallelism = {{#if batchEnabled}}`tasks[]` items in one call, and/or multiple `task` calls in one assistant message{{else}}multiple `task` calls in one assistant message{{/if}}. Concurrency is bounded at {{MAX_CONCURRENCY}} running subagents per session.
|
|
10
|
+
{{/if}}
|
|
6
11
|
{{#if ircEnabled}}
|
|
7
|
-
- Coordinate with
|
|
12
|
+
- Coordinate with agents via `irc` using their ids. Agents reach you and their siblings live the same way.
|
|
8
13
|
{{/if}}
|
|
9
14
|
|
|
10
15
|
<lifecycle>
|
|
@@ -546,6 +546,7 @@ interface ActiveRetryFallbackState {
|
|
|
546
546
|
originalSelector: string;
|
|
547
547
|
originalThinkingLevel: ConfiguredThinkingLevel | undefined;
|
|
548
548
|
lastAppliedFallbackThinkingLevel: ConfiguredThinkingLevel | undefined;
|
|
549
|
+
pinned: boolean;
|
|
549
550
|
}
|
|
550
551
|
|
|
551
552
|
function parseRetryFallbackSelector(selector: string): RetryFallbackSelector | undefined {
|
|
@@ -8257,10 +8258,18 @@ export class AgentSession {
|
|
|
8257
8258
|
const contextWindow = this.model?.contextWindow ?? 0;
|
|
8258
8259
|
if (isContextOverflow(message, contextWindow)) return false;
|
|
8259
8260
|
|
|
8261
|
+
if (this.#isClassifierRefusal(message)) return true;
|
|
8262
|
+
|
|
8260
8263
|
const err = message.errorMessage;
|
|
8261
8264
|
return this.#isTransientErrorMessage(err) || isUsageLimitError(err);
|
|
8262
8265
|
}
|
|
8263
8266
|
|
|
8267
|
+
#isClassifierRefusal(message: AssistantMessage): boolean {
|
|
8268
|
+
if (message.stopReason !== "error") return false;
|
|
8269
|
+
const stopType = message.stopDetails?.type;
|
|
8270
|
+
return stopType === "refusal" || stopType === "sensitive";
|
|
8271
|
+
}
|
|
8272
|
+
|
|
8264
8273
|
#isTransientErrorMessage(errorMessage: string): boolean {
|
|
8265
8274
|
return (
|
|
8266
8275
|
this.#isTransientEnvelopeErrorMessage(errorMessage) || this.#isTransientTransportErrorMessage(errorMessage)
|
|
@@ -8404,6 +8413,7 @@ export class AgentSession {
|
|
|
8404
8413
|
role: string,
|
|
8405
8414
|
selector: RetryFallbackSelector,
|
|
8406
8415
|
currentSelector: string,
|
|
8416
|
+
options?: { pinFallback?: boolean },
|
|
8407
8417
|
): Promise<void> {
|
|
8408
8418
|
const candidate = this.#modelRegistry.find(selector.provider, selector.id);
|
|
8409
8419
|
if (!candidate) {
|
|
@@ -8429,9 +8439,11 @@ export class AgentSession {
|
|
|
8429
8439
|
originalSelector: currentSelector,
|
|
8430
8440
|
originalThinkingLevel: currentThinkingLevel,
|
|
8431
8441
|
lastAppliedFallbackThinkingLevel: nextThinkingLevel,
|
|
8442
|
+
pinned: options?.pinFallback === true,
|
|
8432
8443
|
};
|
|
8433
8444
|
} else {
|
|
8434
8445
|
this.#activeRetryFallback.lastAppliedFallbackThinkingLevel = nextThinkingLevel;
|
|
8446
|
+
this.#activeRetryFallback.pinned = this.#activeRetryFallback.pinned || options?.pinFallback === true;
|
|
8435
8447
|
}
|
|
8436
8448
|
await this.#emitSessionEvent({
|
|
8437
8449
|
type: "retry_fallback_applied",
|
|
@@ -8441,7 +8453,7 @@ export class AgentSession {
|
|
|
8441
8453
|
});
|
|
8442
8454
|
}
|
|
8443
8455
|
|
|
8444
|
-
async #tryRetryModelFallback(currentSelector: string): Promise<boolean> {
|
|
8456
|
+
async #tryRetryModelFallback(currentSelector: string, options?: { pinFallback?: boolean }): Promise<boolean> {
|
|
8445
8457
|
const role = this.#activeRetryFallback?.role ?? this.#resolveRetryFallbackRole(currentSelector);
|
|
8446
8458
|
if (!role) return false;
|
|
8447
8459
|
|
|
@@ -8451,7 +8463,7 @@ export class AgentSession {
|
|
|
8451
8463
|
if (!candidate) continue;
|
|
8452
8464
|
const apiKey = await this.#modelRegistry.getApiKey(candidate, this.sessionId);
|
|
8453
8465
|
if (!apiKey) continue;
|
|
8454
|
-
await this.#applyRetryFallbackCandidate(role, selector, currentSelector);
|
|
8466
|
+
await this.#applyRetryFallbackCandidate(role, selector, currentSelector, options);
|
|
8455
8467
|
return true;
|
|
8456
8468
|
}
|
|
8457
8469
|
|
|
@@ -8460,6 +8472,7 @@ export class AgentSession {
|
|
|
8460
8472
|
|
|
8461
8473
|
async #maybeRestoreRetryFallbackPrimary(): Promise<void> {
|
|
8462
8474
|
if (!this.#activeRetryFallback) return;
|
|
8475
|
+
if (this.#activeRetryFallback.pinned) return;
|
|
8463
8476
|
if (this.#getRetryFallbackRevertPolicy() !== "cooldown-expiry") return;
|
|
8464
8477
|
|
|
8465
8478
|
const {
|
|
@@ -8557,6 +8570,7 @@ export class AgentSession {
|
|
|
8557
8570
|
async #handleRetryableError(message: AssistantMessage): Promise<boolean> {
|
|
8558
8571
|
const retrySettings = this.settings.getGroup("retry");
|
|
8559
8572
|
if (!retrySettings.enabled) return false;
|
|
8573
|
+
const classifierRefusal = this.#isClassifierRefusal(message);
|
|
8560
8574
|
|
|
8561
8575
|
const generation = this.#promptGeneration;
|
|
8562
8576
|
this.#retryAttempt++;
|
|
@@ -8630,8 +8644,10 @@ export class AgentSession {
|
|
|
8630
8644
|
const currentSelector = this.model ? formatRetryFallbackSelector(this.model, this.thinkingLevel) : undefined;
|
|
8631
8645
|
if (!switchedCredential && currentSelector) {
|
|
8632
8646
|
if (retrySettings.modelFallback) {
|
|
8633
|
-
|
|
8634
|
-
|
|
8647
|
+
if (!classifierRefusal) {
|
|
8648
|
+
this.#noteRetryFallbackCooldown(currentSelector, parsedRetryAfterMs, errorMessage);
|
|
8649
|
+
}
|
|
8650
|
+
switchedModel = await this.#tryRetryModelFallback(currentSelector, { pinFallback: classifierRefusal });
|
|
8635
8651
|
}
|
|
8636
8652
|
if (switchedModel) {
|
|
8637
8653
|
delayMs = 0;
|
|
@@ -8639,6 +8655,11 @@ export class AgentSession {
|
|
|
8639
8655
|
delayMs = parsedRetryAfterMs;
|
|
8640
8656
|
}
|
|
8641
8657
|
}
|
|
8658
|
+
if (classifierRefusal && !switchedModel) {
|
|
8659
|
+
this.#retryAttempt = 0;
|
|
8660
|
+
this.#resolveRetry();
|
|
8661
|
+
return false;
|
|
8662
|
+
}
|
|
8642
8663
|
|
|
8643
8664
|
// Fail-fast cap: if the provider asks us to wait longer than
|
|
8644
8665
|
// retry.maxDelayMs and we have no fallback credential or model to
|
package/src/task/index.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Supports:
|
|
10
10
|
* - Single agent spawn per call (parallelism = parallel task calls)
|
|
11
11
|
* - Batch spawning + shared context per call when `task.batch` is enabled
|
|
12
|
-
* -
|
|
12
|
+
* - Background execution through AsyncJobManager when `async.enabled` is enabled
|
|
13
13
|
* - Progress tracking via JSON events
|
|
14
14
|
* - Session artifacts for debugging
|
|
15
15
|
*/
|
|
@@ -190,6 +190,7 @@ function renderDescription(
|
|
|
190
190
|
isolationEnabled: boolean,
|
|
191
191
|
disabledAgents: string[],
|
|
192
192
|
batchEnabled: boolean,
|
|
193
|
+
asyncEnabled: boolean,
|
|
193
194
|
ircEnabled: boolean,
|
|
194
195
|
parentSpawns: string,
|
|
195
196
|
): string {
|
|
@@ -217,6 +218,7 @@ function renderDescription(
|
|
|
217
218
|
MAX_CONCURRENCY: maxConcurrency,
|
|
218
219
|
isolationEnabled,
|
|
219
220
|
batchEnabled,
|
|
221
|
+
asyncEnabled,
|
|
220
222
|
ircEnabled,
|
|
221
223
|
});
|
|
222
224
|
}
|
|
@@ -374,8 +376,8 @@ function discoverAgentsForCreate(cwd: string): Promise<DiscoveryResult> {
|
|
|
374
376
|
* Task tool - Delegate tasks to specialized agents.
|
|
375
377
|
*
|
|
376
378
|
* Each call spawns one subagent — or, with `task.batch`, one per `tasks[]`
|
|
377
|
-
* item.
|
|
378
|
-
*
|
|
379
|
+
* item. When `async.enabled` is on, spawns run as AsyncJobManager jobs; when
|
|
380
|
+
* disabled, the tool blocks until every spawn finishes.
|
|
379
381
|
*/
|
|
380
382
|
export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetails, Theme> {
|
|
381
383
|
readonly name = "task";
|
|
@@ -411,7 +413,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
411
413
|
return lines;
|
|
412
414
|
};
|
|
413
415
|
readonly label = "Task";
|
|
414
|
-
readonly summary = "Spawn
|
|
416
|
+
readonly summary = "Spawn subagents to complete delegated tasks";
|
|
415
417
|
readonly strict = true;
|
|
416
418
|
readonly loadMode = "discoverable";
|
|
417
419
|
readonly renderResult = renderResult;
|
|
@@ -448,6 +450,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
448
450
|
isolationMode !== "none",
|
|
449
451
|
disabledAgents,
|
|
450
452
|
this.#isBatchEnabled(),
|
|
453
|
+
this.session.settings.get("async.enabled"),
|
|
451
454
|
isIrcEnabled(this.session.settings, this.session.taskDepth ?? 0),
|
|
452
455
|
this.session.getSessionSpawns() ?? "*",
|
|
453
456
|
);
|
|
@@ -492,12 +495,14 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
492
495
|
|
|
493
496
|
const spawnItems = resolveSpawnItems(params);
|
|
494
497
|
const selectedAgent = this.#discoveredAgents.find(agent => agent.name === params.agent);
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
//
|
|
499
|
-
//
|
|
500
|
-
|
|
498
|
+
const asyncEnabled = this.session.settings.get("async.enabled");
|
|
499
|
+
const manager = asyncEnabled ? this.session.asyncJobManager : undefined;
|
|
500
|
+
if (!asyncEnabled || !manager || selectedAgent?.blocking === true) {
|
|
501
|
+
// Sync fallback: async execution disabled, orphaned host that never
|
|
502
|
+
// wired a job manager, or an agent definition that declares
|
|
503
|
+
// `blocking: true`. The session-scoped semaphore still bounds fan-out
|
|
504
|
+
// across parallel task calls.
|
|
505
|
+
if (asyncEnabled && !manager) {
|
|
501
506
|
logger.warn("task: no AsyncJobManager registered; falling back to sync execution");
|
|
502
507
|
}
|
|
503
508
|
return this.#executeSyncFanout(toolCallId, params, spawnItems, signal, onUpdate);
|
package/src/task/render.ts
CHANGED
|
@@ -631,12 +631,18 @@ export function renderCall(
|
|
|
631
631
|
// same agent (and the assignment brief) itself, so showing it here would
|
|
632
632
|
// repeat what the result frame already shows.
|
|
633
633
|
if (!options.renderContext?.hasResult) {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
634
|
+
// Mirror renderResult's layout — context, assignment, then the
|
|
635
|
+
// per-agent list — so the agent rows do not jump from above the
|
|
636
|
+
// brief to below it when the first progress snapshot replaces the
|
|
637
|
+
// call view. This also matches the schema's field order (`context`
|
|
638
|
+
// streams before `tasks`), so the streaming preview grows
|
|
639
|
+
// append-only instead of inserting agent rows above the
|
|
640
|
+
// already-rendered markdown and pushing it down on every item.
|
|
638
641
|
if (contextSection) sections.push(contextSection(width));
|
|
639
642
|
if (assignmentSection) sections.push(assignmentSection(width));
|
|
643
|
+
const callLines = renderTaskCallLines(args, theme);
|
|
644
|
+
// Guarded: an empty trailing section would still draw its divider.
|
|
645
|
+
if (callLines.length > 0) sections.push({ separator: true, lines: callLines });
|
|
640
646
|
}
|
|
641
647
|
|
|
642
648
|
return {
|
|
@@ -779,6 +779,62 @@ export function createCachedComponent(
|
|
|
779
779
|
};
|
|
780
780
|
}
|
|
781
781
|
|
|
782
|
+
/**
|
|
783
|
+
* Single-slot memo for an expensive rendered string (syntax highlighting, diff
|
|
784
|
+
* coloring) keyed by the exact inputs that shape the bytes: theme instance,
|
|
785
|
+
* expanded state, a caller-chosen salt (path/language), and the source content.
|
|
786
|
+
* Field-wise comparison instead of a concatenated key string: a cache hit costs
|
|
787
|
+
* one string value-compare (engines short-circuit on length) and a miss never
|
|
788
|
+
* allocates a key. Comparing the {@link Theme} by reference is sound because
|
|
789
|
+
* theme switches replace the instance wholesale (`setTheme`/`previewTheme`/
|
|
790
|
+
* `setSymbolPreset` in modes/theme/theme.ts) — themes are never mutated in
|
|
791
|
+
* place.
|
|
792
|
+
*/
|
|
793
|
+
export interface RenderedStringCache {
|
|
794
|
+
theme: Theme | null;
|
|
795
|
+
expanded: boolean;
|
|
796
|
+
salt: string;
|
|
797
|
+
content: string;
|
|
798
|
+
value: string;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
export function createRenderedStringCache(): RenderedStringCache {
|
|
802
|
+
return { theme: null, expanded: false, salt: "", content: "", value: "" };
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/** Drop the memo so the next lookup re-renders (e.g. the render function identity changed). */
|
|
806
|
+
export function invalidateRenderedStringCache(cache: RenderedStringCache): void {
|
|
807
|
+
cache.theme = null;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
export function cachedRenderedString(
|
|
811
|
+
cache: RenderedStringCache | undefined,
|
|
812
|
+
theme: Theme,
|
|
813
|
+
expanded: boolean,
|
|
814
|
+
salt: string,
|
|
815
|
+
content: string,
|
|
816
|
+
render: () => string,
|
|
817
|
+
): string {
|
|
818
|
+
if (
|
|
819
|
+
cache !== undefined &&
|
|
820
|
+
cache.theme === theme &&
|
|
821
|
+
cache.expanded === expanded &&
|
|
822
|
+
cache.salt === salt &&
|
|
823
|
+
cache.content === content
|
|
824
|
+
) {
|
|
825
|
+
return cache.value;
|
|
826
|
+
}
|
|
827
|
+
const value = render();
|
|
828
|
+
if (cache !== undefined) {
|
|
829
|
+
cache.theme = theme;
|
|
830
|
+
cache.expanded = expanded;
|
|
831
|
+
cache.salt = salt;
|
|
832
|
+
cache.content = content;
|
|
833
|
+
cache.value = value;
|
|
834
|
+
}
|
|
835
|
+
return value;
|
|
836
|
+
}
|
|
837
|
+
|
|
782
838
|
/**
|
|
783
839
|
* Append the indented bullet list of parse errors (capped at
|
|
784
840
|
* {@link PARSE_ERRORS_LIMIT}) to `lines`, with an overflow summary line if the
|
package/src/tools/write.ts
CHANGED
|
@@ -37,12 +37,15 @@ import { type OutputMeta, outputMeta } from "./output-meta";
|
|
|
37
37
|
import { formatPathRelativeToCwd, isInternalUrlPath } from "./path-utils";
|
|
38
38
|
import { enforcePlanModeWrite, resolvePlanPath } from "./plan-mode-guard";
|
|
39
39
|
import {
|
|
40
|
+
cachedRenderedString,
|
|
41
|
+
createRenderedStringCache,
|
|
40
42
|
formatDiagnostics,
|
|
41
43
|
formatErrorDetail,
|
|
42
44
|
formatExpandHint,
|
|
43
45
|
formatMoreItems,
|
|
44
46
|
formatStatusIcon,
|
|
45
47
|
getLspBatchRequest,
|
|
48
|
+
type RenderedStringCache,
|
|
46
49
|
replaceTabs,
|
|
47
50
|
shortenPath,
|
|
48
51
|
} from "./render-utils";
|
|
@@ -1042,37 +1045,40 @@ function formatStreamingContent(
|
|
|
1042
1045
|
language: string | undefined,
|
|
1043
1046
|
uiTheme: Theme,
|
|
1044
1047
|
spinnerFrame?: number,
|
|
1048
|
+
cache?: RenderedStringCache,
|
|
1045
1049
|
): string {
|
|
1046
1050
|
if (!content) return "";
|
|
1047
|
-
const
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1051
|
+
const bodyText = cachedRenderedString(cache, uiTheme, expanded, language ?? "", content, () => {
|
|
1052
|
+
const lines = normalizeDisplayText(content).split("\n");
|
|
1053
|
+
const totalLines = lines.length;
|
|
1054
|
+
// Collapsed: follow the streaming edge with a bounded tail window so the box
|
|
1055
|
+
// stays short enough not to strand its scrolled-off head above the viewport
|
|
1056
|
+
// while the block is volatile. `Ctrl+O` (expanded) lifts the cap for a
|
|
1057
|
+
// deliberate full view — matching the eval streaming preview.
|
|
1058
|
+
const startIndex = expanded ? 0 : Math.max(0, totalLines - WRITE_STREAMING_PREVIEW_LINES);
|
|
1059
|
+
const visibleLines = lines.slice(startIndex);
|
|
1060
|
+
const hidden = startIndex;
|
|
1061
|
+
const highlighted = highlightCode(visibleLines.join("\n"), language);
|
|
1062
|
+
const lineNumberWidth = Math.max(WRITE_GUTTER_MIN_WIDTH, String(totalLines).length);
|
|
1063
|
+
|
|
1064
|
+
let text = "\n\n";
|
|
1065
|
+
if (hidden > 0) {
|
|
1066
|
+
text += `${uiTheme.fg("dim", `… (${hidden} earlier line${hidden === 1 ? "" : "s"})`)}\n`;
|
|
1067
|
+
}
|
|
1068
|
+
for (let i = 0; i < highlighted.length; i++) {
|
|
1069
|
+
const lineNum = startIndex + i + 1;
|
|
1070
|
+
const gutter = uiTheme.fg("dim", `${String(lineNum).padStart(lineNumberWidth, " ")} `);
|
|
1071
|
+
const body = replaceTabs(highlighted[i] ?? "");
|
|
1072
|
+
text += `${gutter}${body}\n`;
|
|
1073
|
+
}
|
|
1074
|
+
return text;
|
|
1075
|
+
});
|
|
1069
1076
|
// The animated glyph lives on this trailing line — inside the transcript's
|
|
1070
1077
|
// volatile-tail holdback — never in the header: an animating head row pins
|
|
1071
1078
|
// the native-scrollback commit boundary at the top of the block, so a long
|
|
1072
1079
|
// expanded preview could never scroll-append mid-stream.
|
|
1073
1080
|
const spinner = spinnerFrame !== undefined ? `${formatStatusIcon("running", uiTheme, spinnerFrame)} ` : "";
|
|
1074
|
-
|
|
1075
|
-
return text;
|
|
1081
|
+
return `${bodyText}${spinner}${uiTheme.fg("dim", `… (streaming)`)}`;
|
|
1076
1082
|
}
|
|
1077
1083
|
|
|
1078
1084
|
function renderContentPreview(
|
|
@@ -1080,29 +1086,32 @@ function renderContentPreview(
|
|
|
1080
1086
|
expanded: boolean,
|
|
1081
1087
|
language: string | undefined,
|
|
1082
1088
|
uiTheme: Theme,
|
|
1089
|
+
cache?: RenderedStringCache,
|
|
1083
1090
|
): string {
|
|
1084
1091
|
if (!content) return "";
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1092
|
+
return cachedRenderedString(cache, uiTheme, expanded, language ?? "", content, () => {
|
|
1093
|
+
const rawLines = normalizeDisplayText(content).split("\n");
|
|
1094
|
+
const totalLines = rawLines.length;
|
|
1095
|
+
const maxLines = expanded ? totalLines : Math.min(totalLines, WRITE_PREVIEW_LINES);
|
|
1096
|
+
const visibleLines = rawLines.slice(0, maxLines);
|
|
1097
|
+
const highlighted = highlightCode(visibleLines.join("\n"), language);
|
|
1098
|
+
const lineNumberWidth = Math.max(WRITE_GUTTER_MIN_WIDTH, String(totalLines).length);
|
|
1099
|
+
const hidden = totalLines - maxLines;
|
|
1100
|
+
|
|
1101
|
+
let text = "\n\n";
|
|
1102
|
+
for (let i = 0; i < highlighted.length; i++) {
|
|
1103
|
+
const lineNum = i + 1;
|
|
1104
|
+
const gutter = uiTheme.fg("dim", `${String(lineNum).padStart(lineNumberWidth, " ")} `);
|
|
1105
|
+
const body = replaceTabs(highlighted[i] ?? "");
|
|
1106
|
+
text += `${gutter}${body}\n`;
|
|
1107
|
+
}
|
|
1108
|
+
if (!expanded && hidden > 0) {
|
|
1109
|
+
const hint = formatExpandHint(uiTheme, expanded, hidden > 0);
|
|
1110
|
+
const moreLine = `${formatMoreItems(hidden, "line")}${hint ? ` ${hint}` : ""}`;
|
|
1111
|
+
text += uiTheme.fg("dim", moreLine);
|
|
1112
|
+
}
|
|
1113
|
+
return text.trimEnd();
|
|
1114
|
+
});
|
|
1106
1115
|
}
|
|
1107
1116
|
|
|
1108
1117
|
export const writeToolRenderer = {
|
|
@@ -1125,9 +1134,17 @@ export const writeToolRenderer = {
|
|
|
1125
1134
|
},
|
|
1126
1135
|
uiTheme,
|
|
1127
1136
|
);
|
|
1137
|
+
const streamingCache = createRenderedStringCache();
|
|
1128
1138
|
return framedBlock(uiTheme, width => {
|
|
1129
1139
|
const body = args.content
|
|
1130
|
-
? formatStreamingContent(
|
|
1140
|
+
? formatStreamingContent(
|
|
1141
|
+
args.content,
|
|
1142
|
+
Boolean(options?.expanded),
|
|
1143
|
+
lang,
|
|
1144
|
+
uiTheme,
|
|
1145
|
+
options?.spinnerFrame,
|
|
1146
|
+
streamingCache,
|
|
1147
|
+
)
|
|
1131
1148
|
: "";
|
|
1132
1149
|
const bodyLines = body ? body.split("\n") : [];
|
|
1133
1150
|
while (bodyLines.length > 0 && bodyLines[0].trim() === "") bodyLines.shift();
|
|
@@ -1189,9 +1206,10 @@ export const writeToolRenderer = {
|
|
|
1189
1206
|
);
|
|
1190
1207
|
const diagnostics = result.details?.diagnostics;
|
|
1191
1208
|
|
|
1209
|
+
const previewCache = createRenderedStringCache();
|
|
1192
1210
|
return framedBlock(uiTheme, width => {
|
|
1193
1211
|
const { expanded } = options;
|
|
1194
|
-
let body = renderContentPreview(fileContent, expanded, lang, uiTheme);
|
|
1212
|
+
let body = renderContentPreview(fileContent, expanded, lang, uiTheme, previewCache);
|
|
1195
1213
|
if (diagnostics) {
|
|
1196
1214
|
const diagText = formatDiagnostics(diagnostics, expanded, uiTheme, fp =>
|
|
1197
1215
|
uiTheme.getLangIcon(getLanguageFromPath(fp)),
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
buildAnthropicSystemBlocks,
|
|
15
15
|
buildAnthropicUrl,
|
|
16
16
|
type FetchImpl,
|
|
17
|
+
resolveAnthropicMetadataUserId,
|
|
17
18
|
stripClaudeToolPrefix,
|
|
18
19
|
withAuth,
|
|
19
20
|
wrapFetchForCch,
|
|
@@ -82,6 +83,7 @@ function buildSystemBlocks(
|
|
|
82
83
|
* @param auth - Authentication configuration (API key or OAuth)
|
|
83
84
|
* @param model - Model identifier to use
|
|
84
85
|
* @param query - Search query from the user
|
|
86
|
+
* @param metadataUserId - Optional Anthropic Messages metadata.user_id (already shaped for OAuth)
|
|
85
87
|
* @param systemPrompt - Optional system prompt for guiding response style
|
|
86
88
|
* @returns Raw API response from Anthropic
|
|
87
89
|
* @throws {SearchProviderError} If the API request fails
|
|
@@ -90,6 +92,7 @@ async function callSearch(
|
|
|
90
92
|
auth: AnthropicAuthConfig,
|
|
91
93
|
model: string,
|
|
92
94
|
query: string,
|
|
95
|
+
metadataUserId?: string,
|
|
93
96
|
systemPrompt?: string,
|
|
94
97
|
maxTokens?: number,
|
|
95
98
|
temperature?: number,
|
|
@@ -113,6 +116,10 @@ async function callSearch(
|
|
|
113
116
|
],
|
|
114
117
|
};
|
|
115
118
|
|
|
119
|
+
if (metadataUserId) {
|
|
120
|
+
body.metadata = { user_id: metadataUserId };
|
|
121
|
+
}
|
|
122
|
+
|
|
116
123
|
if (temperature !== undefined) {
|
|
117
124
|
body.temperature = temperature;
|
|
118
125
|
}
|
|
@@ -273,19 +280,37 @@ export async function searchAnthropic(
|
|
|
273
280
|
const model = getModel();
|
|
274
281
|
const systemPrompt = "authStorage" in params ? params.systemPrompt : params.system_prompt;
|
|
275
282
|
const maxTokens = "authStorage" in params ? params.maxOutputTokens : params.max_tokens;
|
|
283
|
+
const callerSessionId = "authStorage" in params ? params.sessionId : undefined;
|
|
284
|
+
const accountId =
|
|
285
|
+
"authStorage" in params ? params.authStorage.getOAuthAccountId("anthropic", params.sessionId) : undefined;
|
|
276
286
|
const response = await withAuth(
|
|
277
287
|
keyOrResolver,
|
|
278
|
-
key =>
|
|
279
|
-
|
|
280
|
-
|
|
288
|
+
key => {
|
|
289
|
+
const auth = buildAnthropicAuthConfig(key, searchBaseUrl);
|
|
290
|
+
// Mirror the main Messages path: OAuth requests need a Claude-Code-shaped
|
|
291
|
+
// metadata.user_id (`{session_id, account_uuid?, device_id}`) so the
|
|
292
|
+
// CC billing header + system fingerprint installed by
|
|
293
|
+
// `buildAnthropicSearchHeaders`/`buildSystemBlocks` line up with the
|
|
294
|
+
// attribution Anthropic and enterprise gateways expect. API-key tokens
|
|
295
|
+
// forward the raw session id verbatim.
|
|
296
|
+
const metadataUserId = resolveAnthropicMetadataUserId(
|
|
297
|
+
callerSessionId,
|
|
298
|
+
auth.isOAuth,
|
|
299
|
+
callerSessionId,
|
|
300
|
+
accountId,
|
|
301
|
+
);
|
|
302
|
+
return callSearch(
|
|
303
|
+
auth,
|
|
281
304
|
model,
|
|
282
305
|
params.query,
|
|
306
|
+
metadataUserId,
|
|
283
307
|
systemPrompt,
|
|
284
308
|
maxTokens,
|
|
285
309
|
params.temperature,
|
|
286
310
|
params.signal,
|
|
287
311
|
params.fetch,
|
|
288
|
-
)
|
|
312
|
+
);
|
|
313
|
+
},
|
|
289
314
|
{
|
|
290
315
|
signal: params.signal,
|
|
291
316
|
missingKeyMessage:
|