@oh-my-pi/pi-coding-agent 15.11.0 → 15.11.2
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 +57 -2
- package/dist/cli.js +678 -657
- package/dist/types/capability/mcp.d.ts +1 -0
- package/dist/types/config/settings-schema.d.ts +49 -4
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/custom-commands/types.d.ts +6 -3
- package/dist/types/extensibility/custom-tools/loader.d.ts +2 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +8 -4
- package/dist/types/extensibility/extensions/types.d.ts +2 -2
- package/dist/types/extensibility/hooks/types.d.ts +8 -4
- package/dist/types/irc/bus.d.ts +15 -2
- 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/plan-review-overlay.d.ts +2 -0
- 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/rpc/rpc-client.d.ts +10 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +2 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +30 -0
- package/dist/types/modes/theme/theme.d.ts +3 -2
- package/dist/types/session/agent-session.d.ts +17 -3
- package/dist/types/slash-commands/available-commands.d.ts +34 -0
- package/dist/types/task/index.d.ts +3 -3
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/browser/attach.d.ts +4 -4
- package/dist/types/tools/browser/registry.d.ts +1 -0
- package/dist/types/tools/irc.d.ts +3 -2
- package/dist/types/tools/path-utils.d.ts +0 -4
- 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 +55 -4
- package/src/edit/renderer.ts +96 -46
- package/src/exec/bash-executor.ts +21 -6
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +6 -1
- package/src/extensibility/custom-commands/loader.ts +3 -1
- package/src/extensibility/custom-commands/types.ts +6 -3
- package/src/extensibility/custom-tools/loader.ts +4 -7
- package/src/extensibility/custom-tools/types.ts +8 -4
- package/src/extensibility/extensions/loader.ts +2 -1
- package/src/extensibility/extensions/types.ts +2 -2
- package/src/extensibility/hooks/loader.ts +3 -1
- package/src/extensibility/hooks/types.ts +8 -4
- package/src/internal-urls/docs-index.generated.ts +8 -8
- package/src/irc/bus.ts +14 -3
- package/src/lsp/defaults.json +6 -0
- package/src/lsp/render.ts +2 -28
- 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/memories/index.ts +2 -0
- package/src/modes/acp/acp-agent.ts +4 -67
- 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/plan-review-overlay.ts +32 -3
- 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/streaming-reveal.ts +16 -8
- package/src/modes/controllers/tool-args-reveal.ts +174 -0
- package/src/modes/interactive-mode.ts +41 -2
- package/src/modes/rpc/rpc-client.ts +32 -0
- package/src/modes/rpc/rpc-mode.ts +82 -7
- package/src/modes/rpc/rpc-types.ts +23 -0
- package/src/modes/theme/theme.ts +13 -7
- package/src/modes/utils/ui-helpers.ts +13 -4
- package/src/prompts/memories/consolidation_system.md +4 -0
- package/src/prompts/system/irc-autoreply.md +6 -0
- package/src/prompts/system/irc-incoming.md +1 -1
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/irc.md +1 -1
- package/src/prompts/tools/task.md +7 -2
- package/src/session/agent-session.ts +120 -10
- package/src/slash-commands/available-commands.ts +105 -0
- package/src/task/index.ts +15 -10
- package/src/task/render.ts +10 -4
- package/src/tools/bash.ts +5 -1
- package/src/tools/browser/attach.ts +26 -7
- package/src/tools/browser/registry.ts +11 -1
- package/src/tools/irc.ts +16 -4
- package/src/tools/job.ts +7 -3
- package/src/tools/path-utils.ts +22 -15
- 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,6 +1,6 @@
|
|
|
1
1
|
import * as net from "node:net";
|
|
2
2
|
import { Process, ProcessStatus } from "@oh-my-pi/pi-natives";
|
|
3
|
-
import type
|
|
3
|
+
import { type Browser, type Page, TargetType } from "puppeteer-core";
|
|
4
4
|
import { ToolError, throwIfAborted } from "../tool-errors";
|
|
5
5
|
|
|
6
6
|
const ATTACH_TARGET_SKIP_PATTERN =
|
|
@@ -119,22 +119,41 @@ export async function findReusableCdp(
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
/**
|
|
122
|
-
* Pick the best page target on an attached browser.
|
|
123
|
-
*
|
|
124
|
-
*
|
|
122
|
+
* Pick the best page target on an attached browser. Prefer discoverable page
|
|
123
|
+
* targets first so Chromium/Edge attach flows that hide pages from
|
|
124
|
+
* `browser.pages()` can still return a usable tab.
|
|
125
125
|
*/
|
|
126
126
|
export async function pickElectronTarget(browser: Browser, matcher?: string): Promise<Page> {
|
|
127
|
-
const
|
|
128
|
-
|
|
127
|
+
const discoveredPages = await Promise.all(
|
|
128
|
+
browser.targets().map(async target => {
|
|
129
|
+
if (target.type() !== TargetType.PAGE) return null;
|
|
130
|
+
return await target.page().catch(() => null);
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
const usablePages = discoveredPages.filter((page): page is Page => page !== null);
|
|
134
|
+
if (usablePages.length > 0) {
|
|
135
|
+
return pickPageFromList(usablePages, matcher);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const fallbackPages = await browser.pages();
|
|
139
|
+
if (!fallbackPages.length) {
|
|
129
140
|
throw new ToolError("No page targets available on the attached browser");
|
|
130
141
|
}
|
|
131
|
-
|
|
142
|
+
return pickPageFromList(fallbackPages, matcher);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function enrichPages(pages: Page[]): Promise<Array<{ page: Page; url: string; title: string }>> {
|
|
146
|
+
return await Promise.all(
|
|
132
147
|
pages.map(async page => ({
|
|
133
148
|
page,
|
|
134
149
|
url: page.url(),
|
|
135
150
|
title: ((await page.title().catch(() => "")) ?? "").trim(),
|
|
136
151
|
})),
|
|
137
152
|
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function pickPageFromList(pages: Page[], matcher?: string): Promise<Page> {
|
|
156
|
+
const enriched = await enrichPages(pages);
|
|
138
157
|
if (matcher) {
|
|
139
158
|
const needle = matcher.toLowerCase();
|
|
140
159
|
const hit = enriched.find(p => p.url.toLowerCase().includes(needle) || p.title.toLowerCase().includes(needle));
|
|
@@ -58,6 +58,16 @@ export async function acquireBrowser(kind: BrowserKind, opts: AcquireBrowserOpti
|
|
|
58
58
|
return handle;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
export function normalizeConnectedCdpUrl(rawCdpUrl: string): string {
|
|
62
|
+
const cdpUrl = rawCdpUrl.replace(/\/+$/, "");
|
|
63
|
+
if (/^wss?:\/\//i.test(cdpUrl)) {
|
|
64
|
+
throw new ToolError(
|
|
65
|
+
"browser app.cdp_url must be the HTTP CDP discovery endpoint (for example http://127.0.0.1:9222), not a ws:// browser websocket URL.",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return cdpUrl;
|
|
69
|
+
}
|
|
70
|
+
|
|
61
71
|
async function openBrowserHandle(kind: BrowserKind, opts: AcquireBrowserOptions): Promise<BrowserHandle> {
|
|
62
72
|
if (kind.kind === "headless") {
|
|
63
73
|
const browser = await launchHeadlessBrowser({ headless: kind.headless, viewport: opts.viewport });
|
|
@@ -70,7 +80,7 @@ async function openBrowserHandle(kind: BrowserKind, opts: AcquireBrowserOptions)
|
|
|
70
80
|
};
|
|
71
81
|
}
|
|
72
82
|
if (kind.kind === "connected") {
|
|
73
|
-
const cdpUrl = kind.cdpUrl
|
|
83
|
+
const cdpUrl = normalizeConnectedCdpUrl(kind.cdpUrl);
|
|
74
84
|
await waitForCdp(cdpUrl, 5_000, opts.signal);
|
|
75
85
|
const puppeteer = await loadPuppeteer();
|
|
76
86
|
const browser = await puppeteer.connect({
|
package/src/tools/irc.ts
CHANGED
|
@@ -234,7 +234,15 @@ export class IrcTool implements AgentTool<typeof ircSchema, IrcDetails> {
|
|
|
234
234
|
// through the bus unfiltered so parked recipients are revived.
|
|
235
235
|
const targets = isBroadcast ? registry.listVisibleTo(senderId).map(ref => ref.id) : [to];
|
|
236
236
|
const receipts = await Promise.all(
|
|
237
|
-
targets.map(target =>
|
|
237
|
+
targets.map(target =>
|
|
238
|
+
bus.send(
|
|
239
|
+
{ from: senderId, to: target, body: message, replyTo: params.replyTo },
|
|
240
|
+
// Awaited sends mark the sender as blocked on an answer so a
|
|
241
|
+
// busy recipient that cannot reach a step boundary (async
|
|
242
|
+
// disabled) auto-replies instead of stranding the sender.
|
|
243
|
+
params.await ? { expectsReply: true } : undefined,
|
|
244
|
+
),
|
|
245
|
+
),
|
|
238
246
|
);
|
|
239
247
|
|
|
240
248
|
const lines: string[] = [];
|
|
@@ -457,13 +465,14 @@ function callMeta(args: IrcRenderArgs | undefined): string[] {
|
|
|
457
465
|
|
|
458
466
|
/**
|
|
459
467
|
* Display-only transcript card for live IRC traffic: `irc:incoming` DMs
|
|
460
|
-
* delivered to this session
|
|
468
|
+
* delivered to this session, `irc:autoreply` side-channel replies sent on
|
|
469
|
+
* this session's behalf, and `irc:relay` observations of agent↔agent
|
|
461
470
|
* traffic. Shares the tool renderer's glyph + quote-border conventions so
|
|
462
471
|
* cards and `irc` tool output look identical in the transcript.
|
|
463
472
|
*/
|
|
464
473
|
export function createIrcMessageCard(
|
|
465
474
|
card: {
|
|
466
|
-
kind: "incoming" | "relay";
|
|
475
|
+
kind: "incoming" | "autoreply" | "relay";
|
|
467
476
|
from?: string;
|
|
468
477
|
to?: string;
|
|
469
478
|
body?: string;
|
|
@@ -477,9 +486,12 @@ export function createIrcMessageCard(
|
|
|
477
486
|
const title =
|
|
478
487
|
card.kind === "incoming"
|
|
479
488
|
? `IRC ${uiTheme.nav.back} ${from}`
|
|
480
|
-
:
|
|
489
|
+
: card.kind === "autoreply"
|
|
490
|
+
? `IRC ${uiTheme.nav.selected} ${card.to?.trim() || "?"}`
|
|
491
|
+
: `IRC ${from} ${uiTheme.nav.selected} ${card.to?.trim() || "?"}`;
|
|
481
492
|
const body = card.body ?? "";
|
|
482
493
|
const meta: string[] = [];
|
|
494
|
+
if (card.kind === "autoreply") meta.push("auto");
|
|
483
495
|
if (card.replyTo) meta.push("reply");
|
|
484
496
|
const age = messageAge(card.timestamp);
|
|
485
497
|
if (age) meta.push(age);
|
package/src/tools/job.ts
CHANGED
|
@@ -449,17 +449,21 @@ export const jobToolRenderer = {
|
|
|
449
449
|
const counts = { completed: 0, failed: 0, cancelled: 0, running: 0 };
|
|
450
450
|
for (const job of jobs) counts[job.status]++;
|
|
451
451
|
|
|
452
|
+
// The title already carries the running count, so meta lists only the
|
|
453
|
+
// settled categories — "waiting on 19 of 19 · 19 running" read awkward.
|
|
452
454
|
const meta: string[] = [];
|
|
453
455
|
if (counts.completed > 0) meta.push(uiTheme.fg("success", `${counts.completed} done`));
|
|
454
456
|
if (counts.failed > 0) meta.push(uiTheme.fg("error", `${counts.failed} failed`));
|
|
455
457
|
if (counts.cancelled > 0) meta.push(uiTheme.fg("warning", `${counts.cancelled} cancelled`));
|
|
456
|
-
if (counts.running > 0) meta.push(uiTheme.fg("accent", `${counts.running} running`));
|
|
457
458
|
|
|
458
459
|
const headerIcon: ToolUIStatus = counts.failed > 0 ? "warning" : counts.running > 0 ? "info" : "success";
|
|
460
|
+
const jobsNoun = jobs.length === 1 ? "job" : "jobs";
|
|
459
461
|
const description =
|
|
460
462
|
counts.running > 0
|
|
461
|
-
?
|
|
462
|
-
|
|
463
|
+
? counts.running === jobs.length
|
|
464
|
+
? `waiting on ${jobs.length} ${jobsNoun}`
|
|
465
|
+
: `waiting on ${counts.running} of ${jobs.length} ${jobsNoun}`
|
|
466
|
+
: `${jobs.length} ${jobsNoun} settled`;
|
|
463
467
|
|
|
464
468
|
const header = renderStatusLine(
|
|
465
469
|
{
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -398,6 +398,11 @@ export function formatPathRelativeToCwd(
|
|
|
398
398
|
export function stripOuterDoubleQuotes(input: string): string {
|
|
399
399
|
return input.startsWith('"') && input.endsWith('"') && input.length > 1 ? input.slice(1, -1) : input;
|
|
400
400
|
}
|
|
401
|
+
function normalizePathSeparators(input: string): string {
|
|
402
|
+
if (isInternalUrlPath(input)) return input;
|
|
403
|
+
if (!input.includes("\\")) return input;
|
|
404
|
+
return input.replace(/\\/g, "/");
|
|
405
|
+
}
|
|
401
406
|
|
|
402
407
|
export function normalizePathLikeInput(input: string): string {
|
|
403
408
|
return stripOuterDoubleQuotes(input.trim());
|
|
@@ -582,19 +587,20 @@ export interface ResolvedMultiFindPattern {
|
|
|
582
587
|
targets: ResolvedFindTarget[];
|
|
583
588
|
scopePath: string;
|
|
584
589
|
}
|
|
585
|
-
|
|
586
|
-
/**
|
|
587
|
-
* Split a user path into a base path + glob pattern for tools that delegate to
|
|
588
|
-
* APIs accepting separate `path` and `glob` arguments.
|
|
589
|
-
*/
|
|
590
590
|
export function parseSearchPath(filePath: string): ParsedSearchPath {
|
|
591
|
-
const normalizedPath = filePath
|
|
592
|
-
|
|
593
|
-
|
|
591
|
+
const normalizedPath = normalizePathSeparators(filePath);
|
|
592
|
+
const segments = normalizedPath.split("/");
|
|
593
|
+
let firstGlobIndex = -1;
|
|
594
|
+
for (let i = 0; i < segments.length; i++) {
|
|
595
|
+
if (hasGlobPathChars(segments[i])) {
|
|
596
|
+
firstGlobIndex = i;
|
|
597
|
+
break;
|
|
598
|
+
}
|
|
594
599
|
}
|
|
595
600
|
|
|
596
|
-
|
|
597
|
-
|
|
601
|
+
if (firstGlobIndex === -1) {
|
|
602
|
+
return { basePath: normalizedPath };
|
|
603
|
+
}
|
|
598
604
|
|
|
599
605
|
if (firstGlobIndex <= 0) {
|
|
600
606
|
return { basePath: ".", glob: normalizedPath };
|
|
@@ -617,7 +623,7 @@ export async function parseSearchPathPreferringLiteral(filePath: string, cwd: st
|
|
|
617
623
|
if (!hasGlobPathChars(filePath) || isInternalUrlPath(filePath)) return parseSearchPath(filePath);
|
|
618
624
|
try {
|
|
619
625
|
await fs.promises.stat(resolveToCwd(filePath, cwd));
|
|
620
|
-
return { basePath: filePath };
|
|
626
|
+
return { basePath: normalizePathSeparators(filePath) };
|
|
621
627
|
} catch {
|
|
622
628
|
return parseSearchPath(filePath);
|
|
623
629
|
}
|
|
@@ -632,7 +638,8 @@ export async function parseSearchPathPreferringLiteral(filePath: string, cwd: st
|
|
|
632
638
|
// /abs/path/**/\*.ts -> { basePath: "/abs/path", globPattern: "**/*.ts", hasGlob: true }
|
|
633
639
|
// src/app -> { basePath: "src/app", globPattern: "**/*", hasGlob: false }
|
|
634
640
|
export function parseFindPattern(pattern: string): ParsedFindPattern {
|
|
635
|
-
const
|
|
641
|
+
const normalizedPattern = normalizePathSeparators(pattern);
|
|
642
|
+
const segments = normalizedPattern.split("/");
|
|
636
643
|
let firstGlobIndex = -1;
|
|
637
644
|
for (let i = 0; i < segments.length; i++) {
|
|
638
645
|
if (hasGlobPathChars(segments[i])) {
|
|
@@ -642,14 +649,14 @@ export function parseFindPattern(pattern: string): ParsedFindPattern {
|
|
|
642
649
|
}
|
|
643
650
|
|
|
644
651
|
if (firstGlobIndex === -1) {
|
|
645
|
-
return { basePath:
|
|
652
|
+
return { basePath: normalizedPattern, globPattern: "**/*", hasGlob: false };
|
|
646
653
|
}
|
|
647
654
|
|
|
648
655
|
if (firstGlobIndex === 0) {
|
|
649
|
-
const needsRecursive = !
|
|
656
|
+
const needsRecursive = !normalizedPattern.startsWith("**/");
|
|
650
657
|
return {
|
|
651
658
|
basePath: ".",
|
|
652
|
-
globPattern: needsRecursive ? `**/${
|
|
659
|
+
globPattern: needsRecursive ? `**/${normalizedPattern}` : normalizedPattern,
|
|
653
660
|
hasGlob: true,
|
|
654
661
|
};
|
|
655
662
|
}
|
|
@@ -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:
|