@oh-my-pi/pi-coding-agent 16.2.2 → 16.2.4
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 +60 -0
- package/dist/cli.js +3682 -3615
- package/dist/types/advisor/__tests__/config.test.d.ts +1 -0
- package/dist/types/advisor/advise-tool.d.ts +8 -4
- package/dist/types/advisor/config.d.ts +88 -0
- package/dist/types/advisor/index.d.ts +1 -0
- package/dist/types/advisor/transcript-recorder.d.ts +13 -2
- package/dist/types/advisor/watchdog.d.ts +20 -0
- package/dist/types/cli/update-cli.d.ts +15 -0
- package/dist/types/collab/guest.d.ts +29 -0
- package/dist/types/collab/replication-shrink.d.ts +39 -0
- package/dist/types/config/provider-globals.d.ts +7 -0
- package/dist/types/config/settings-schema.d.ts +81 -0
- package/dist/types/debug/log-viewer.d.ts +1 -0
- package/dist/types/debug/raw-sse.d.ts +1 -0
- package/dist/types/edit/hashline/diff.d.ts +0 -11
- package/dist/types/extensibility/tool-event-input.d.ts +7 -0
- package/dist/types/extensibility/utils.d.ts +12 -0
- package/dist/types/mcp/transports/index.d.ts +1 -0
- package/dist/types/mcp/transports/sse.d.ts +20 -0
- package/dist/types/memories/index.d.ts +20 -1
- package/dist/types/modes/components/advisor-config.d.ts +59 -0
- package/dist/types/modes/components/index.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +9 -1
- package/dist/types/modes/components/settings-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/component.d.ts +35 -1
- package/dist/types/modes/components/status-line/types.d.ts +13 -1
- package/dist/types/modes/controllers/command-controller.d.ts +3 -3
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +11 -4
- package/dist/types/modes/skill-command.d.ts +32 -0
- package/dist/types/modes/types.d.ts +9 -3
- package/dist/types/session/agent-session.d.ts +58 -10
- package/dist/types/session/indexed-session-storage.d.ts +7 -1
- package/dist/types/session/messages.d.ts +26 -0
- package/dist/types/session/messages.test.d.ts +1 -0
- package/dist/types/session/session-entries.d.ts +31 -3
- package/dist/types/session/session-history-format.d.ts +6 -0
- package/dist/types/session/session-loader.d.ts +9 -1
- package/dist/types/session/session-manager.d.ts +8 -7
- package/dist/types/session/session-storage.d.ts +11 -0
- package/dist/types/session/session-title-slot.d.ts +19 -0
- package/dist/types/ssh/connection-manager.d.ts +47 -0
- package/dist/types/ssh/utils.d.ts +16 -0
- package/dist/types/task/executor.d.ts +3 -16
- package/dist/types/task/render.d.ts +0 -5
- package/dist/types/task/renderer.d.ts +13 -0
- package/dist/types/task/types.d.ts +16 -0
- package/dist/types/task/yield-assembly.d.ts +28 -0
- package/dist/types/tiny/text.d.ts +8 -0
- package/dist/types/tools/render-utils.d.ts +2 -0
- package/dist/types/tools/review.d.ts +6 -4
- package/dist/types/tools/ssh.d.ts +1 -1
- package/dist/types/tools/todo.d.ts +6 -0
- package/dist/types/tools/yield.d.ts +8 -3
- package/dist/types/utils/thinking-display.d.ts +4 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +242 -10
- package/src/advisor/__tests__/config.test.ts +173 -0
- package/src/advisor/advise-tool.ts +11 -6
- package/src/advisor/config.ts +256 -0
- package/src/advisor/index.ts +1 -0
- package/src/advisor/runtime.ts +12 -2
- package/src/advisor/transcript-recorder.ts +25 -2
- package/src/advisor/watchdog.ts +57 -31
- package/src/autolearn/controller.ts +13 -22
- package/src/autoresearch/index.ts +7 -2
- package/src/cli/gc-cli.ts +17 -10
- package/src/cli/update-cli.ts +254 -0
- package/src/collab/guest.ts +43 -7
- package/src/collab/host.ts +13 -8
- package/src/collab/replication-shrink.ts +111 -0
- package/src/config/model-registry.ts +80 -18
- package/src/config/provider-globals.ts +25 -0
- package/src/config/settings-schema.ts +77 -0
- package/src/debug/index.ts +32 -7
- package/src/debug/log-viewer.ts +111 -53
- package/src/debug/raw-sse.ts +68 -48
- package/src/discovery/codex.ts +13 -5
- package/src/edit/hashline/diff.ts +57 -4
- package/src/eval/__tests__/julia-prelude.test.ts +2 -2
- package/src/eval/js/shared/local-module-loader.ts +23 -1
- package/src/export/html/template.js +13 -7
- package/src/extensibility/extensions/loader.ts +5 -3
- package/src/extensibility/extensions/wrapper.ts +9 -3
- package/src/extensibility/hooks/loader.ts +3 -3
- package/src/extensibility/hooks/tool-wrapper.ts +13 -4
- package/src/extensibility/plugins/manager.ts +2 -1
- package/src/extensibility/tool-event-input.ts +23 -0
- package/src/extensibility/utils.ts +74 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/mcp/client.ts +3 -1
- package/src/mcp/manager.ts +12 -5
- package/src/mcp/transports/index.ts +1 -0
- package/src/mcp/transports/sse.ts +377 -0
- package/src/memories/index.ts +130 -15
- package/src/memory-backend/local-backend.ts +5 -3
- package/src/modes/components/advisor-config.ts +555 -0
- package/src/modes/components/advisor-message.ts +9 -2
- package/src/modes/components/agent-hub.ts +9 -4
- package/src/modes/components/index.ts +2 -0
- package/src/modes/components/model-selector.ts +79 -48
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/component.ts +150 -5
- package/src/modes/components/status-line/segments.ts +46 -21
- package/src/modes/components/status-line/types.ts +13 -1
- package/src/modes/components/tool-execution.ts +47 -6
- package/src/modes/controllers/command-controller.ts +27 -36
- package/src/modes/controllers/event-controller.ts +113 -1
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +61 -61
- package/src/modes/controllers/selector-controller.ts +100 -9
- package/src/modes/interactive-mode.ts +74 -25
- package/src/modes/skill-command.ts +116 -0
- package/src/modes/types.ts +9 -3
- package/src/modes/utils/ui-helpers.ts +41 -23
- package/src/prompts/agents/reviewer.md +11 -10
- package/src/prompts/goals/goal-todo-context.md +12 -0
- package/src/prompts/review-custom-request.md +1 -2
- package/src/prompts/review-request.md +1 -2
- package/src/prompts/system/interrupted-thinking.md +7 -0
- package/src/prompts/system/recap-user.md +9 -0
- package/src/prompts/system/subagent-system-prompt.md +8 -5
- package/src/prompts/system/subagent-yield-reminder.md +6 -5
- package/src/prompts/system/system-prompt.md +0 -1
- package/src/prompts/tools/irc.md +2 -2
- package/src/prompts/tools/read.md +2 -2
- package/src/sdk.ts +28 -24
- package/src/session/agent-session.ts +899 -429
- package/src/session/indexed-session-storage.ts +86 -13
- package/src/session/messages.test.ts +125 -0
- package/src/session/messages.ts +172 -9
- package/src/session/redis-session-storage.ts +49 -2
- package/src/session/session-entries.ts +39 -2
- package/src/session/session-history-format.ts +29 -2
- package/src/session/session-listing.ts +54 -24
- package/src/session/session-loader.ts +66 -3
- package/src/session/session-manager.ts +115 -22
- package/src/session/session-persistence.ts +95 -1
- package/src/session/session-storage.ts +36 -0
- package/src/session/session-title-slot.ts +141 -0
- package/src/session/sql-session-storage.ts +71 -11
- package/src/slash-commands/builtin-registry.ts +23 -24
- package/src/ssh/__tests__/connection-manager-args.test.ts +123 -1
- package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +55 -18
- package/src/ssh/connection-manager.ts +139 -12
- package/src/ssh/file-transfer.ts +23 -18
- package/src/ssh/ssh-executor.ts +2 -13
- package/src/ssh/utils.ts +19 -0
- package/src/task/executor.ts +21 -23
- package/src/task/render.ts +162 -20
- package/src/task/renderer.ts +14 -0
- package/src/task/types.ts +17 -0
- package/src/task/yield-assembly.ts +207 -0
- package/src/tiny/text.ts +23 -0
- package/src/tools/ask.ts +55 -4
- package/src/tools/render-utils.ts +2 -0
- package/src/tools/renderers.ts +8 -2
- package/src/tools/review.ts +17 -7
- package/src/tools/ssh.ts +8 -4
- package/src/tools/todo.ts +17 -1
- package/src/tools/yield.ts +140 -31
- package/src/utils/thinking-display.ts +15 -0
- package/src/prompts/system/autolearn-nudge.md +0 -5
package/src/cli/update-cli.ts
CHANGED
|
@@ -272,6 +272,252 @@ function compareVersions(a: string, b: string): number {
|
|
|
272
272
|
return 0;
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
+
interface BunInstallCachePruneResult {
|
|
276
|
+
scannedPackages: number;
|
|
277
|
+
removedEntries: number;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
interface BunCachePackageGroup {
|
|
281
|
+
actualDirs: Map<string, string[]>;
|
|
282
|
+
markerDir?: string;
|
|
283
|
+
markerEntries: Map<string, string[]>;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function stripBunCacheVersionSuffix(name: string): string {
|
|
287
|
+
const metadataIndex = name.indexOf("@@");
|
|
288
|
+
return metadataIndex === -1 ? name : name.slice(0, metadataIndex);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function compareSemverIdentifier(a: string, b: string): number {
|
|
292
|
+
const aNumber = /^\d+$/.test(a);
|
|
293
|
+
const bNumber = /^\d+$/.test(b);
|
|
294
|
+
if (aNumber && bNumber) return Number(a) - Number(b);
|
|
295
|
+
if (aNumber) return -1;
|
|
296
|
+
if (bNumber) return 1;
|
|
297
|
+
return a.localeCompare(b);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function compareSemverLikeVersions(a: string, b: string): number {
|
|
301
|
+
const [aCoreWithPrerelease] = a.split("+", 1);
|
|
302
|
+
const [bCoreWithPrerelease] = b.split("+", 1);
|
|
303
|
+
const [aCore, aPrerelease] = aCoreWithPrerelease.split("-", 2);
|
|
304
|
+
const [bCore, bPrerelease] = bCoreWithPrerelease.split("-", 2);
|
|
305
|
+
const aParts = aCore.split(".");
|
|
306
|
+
const bParts = bCore.split(".");
|
|
307
|
+
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
|
|
308
|
+
const diff = Number(aParts[i] ?? 0) - Number(bParts[i] ?? 0);
|
|
309
|
+
if (diff !== 0 && Number.isFinite(diff)) return diff;
|
|
310
|
+
}
|
|
311
|
+
if (!aPrerelease && !bPrerelease) return 0;
|
|
312
|
+
if (!aPrerelease) return 1;
|
|
313
|
+
if (!bPrerelease) return -1;
|
|
314
|
+
const aPrereleaseParts = aPrerelease.split(".");
|
|
315
|
+
const bPrereleaseParts = bPrerelease.split(".");
|
|
316
|
+
for (let i = 0; i < Math.max(aPrereleaseParts.length, bPrereleaseParts.length); i++) {
|
|
317
|
+
const aPart = aPrereleaseParts[i];
|
|
318
|
+
const bPart = bPrereleaseParts[i];
|
|
319
|
+
if (aPart === undefined) return -1;
|
|
320
|
+
if (bPart === undefined) return 1;
|
|
321
|
+
const diff = compareSemverIdentifier(aPart, bPart);
|
|
322
|
+
if (diff !== 0) return diff;
|
|
323
|
+
}
|
|
324
|
+
return 0;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function readdirIfExists(dir: string): Promise<fs.Dirent[]> {
|
|
328
|
+
try {
|
|
329
|
+
return await fs.promises.readdir(dir, { withFileTypes: true });
|
|
330
|
+
} catch (err) {
|
|
331
|
+
if (isEnoent(err)) return [];
|
|
332
|
+
throw err;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function getBunCacheGroup(groups: Map<string, BunCachePackageGroup>, packageName: string): BunCachePackageGroup {
|
|
337
|
+
let group = groups.get(packageName);
|
|
338
|
+
if (!group) {
|
|
339
|
+
group = { actualDirs: new Map(), markerEntries: new Map() };
|
|
340
|
+
groups.set(packageName, group);
|
|
341
|
+
}
|
|
342
|
+
return group;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function addVersionPath(entries: Map<string, string[]>, version: string, entryPath: string): void {
|
|
346
|
+
const paths = entries.get(version);
|
|
347
|
+
if (paths) {
|
|
348
|
+
paths.push(entryPath);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
entries.set(version, [entryPath]);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function addBunCacheActualDir(
|
|
355
|
+
groups: Map<string, BunCachePackageGroup>,
|
|
356
|
+
dirPath: string,
|
|
357
|
+
packageNames: Set<string> | undefined,
|
|
358
|
+
): Promise<void> {
|
|
359
|
+
try {
|
|
360
|
+
const manifest = (await Bun.file(path.join(dirPath, "package.json")).json()) as Partial<
|
|
361
|
+
Record<"name" | "version", unknown>
|
|
362
|
+
>;
|
|
363
|
+
if (typeof manifest.name !== "string" || typeof manifest.version !== "string") return;
|
|
364
|
+
if (packageNames && !packageNames.has(manifest.name)) return;
|
|
365
|
+
const group = getBunCacheGroup(groups, manifest.name);
|
|
366
|
+
addVersionPath(group.actualDirs, manifest.version, dirPath);
|
|
367
|
+
} catch (err) {
|
|
368
|
+
if (isEnoent(err)) return;
|
|
369
|
+
throw err;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function addBunCacheMarkerDir(
|
|
374
|
+
groups: Map<string, BunCachePackageGroup>,
|
|
375
|
+
packageName: string,
|
|
376
|
+
markerDir: string,
|
|
377
|
+
packageNames: Set<string> | undefined,
|
|
378
|
+
): Promise<void> {
|
|
379
|
+
if (packageNames && !packageNames.has(packageName)) return;
|
|
380
|
+
const markerEntries = await readdirIfExists(markerDir);
|
|
381
|
+
const group = getBunCacheGroup(groups, packageName);
|
|
382
|
+
group.markerDir = markerDir;
|
|
383
|
+
for (const entry of markerEntries) {
|
|
384
|
+
const cacheVersion = stripBunCacheVersionSuffix(entry.name);
|
|
385
|
+
addVersionPath(group.markerEntries, cacheVersion, path.join(markerDir, entry.name));
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function collectBunCacheGroups(
|
|
390
|
+
cacheDir: string,
|
|
391
|
+
packageNames: Set<string> | undefined,
|
|
392
|
+
): Promise<Map<string, BunCachePackageGroup>> {
|
|
393
|
+
const groups = new Map<string, BunCachePackageGroup>();
|
|
394
|
+
for (const entry of await readdirIfExists(cacheDir)) {
|
|
395
|
+
if (!entry.isDirectory()) continue;
|
|
396
|
+
const entryPath = path.join(cacheDir, entry.name);
|
|
397
|
+
if (entry.name.startsWith("@")) {
|
|
398
|
+
for (const scopedEntry of await readdirIfExists(entryPath)) {
|
|
399
|
+
if (!scopedEntry.isDirectory()) continue;
|
|
400
|
+
const scopedEntryPath = path.join(entryPath, scopedEntry.name);
|
|
401
|
+
const versionSeparator = scopedEntry.name.lastIndexOf("@");
|
|
402
|
+
if (versionSeparator === -1) {
|
|
403
|
+
await addBunCacheMarkerDir(groups, `${entry.name}/${scopedEntry.name}`, scopedEntryPath, packageNames);
|
|
404
|
+
} else {
|
|
405
|
+
await addBunCacheActualDir(groups, scopedEntryPath, packageNames);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
const versionSeparator = entry.name.lastIndexOf("@");
|
|
411
|
+
if (versionSeparator === -1) {
|
|
412
|
+
await addBunCacheMarkerDir(groups, entry.name, entryPath, packageNames);
|
|
413
|
+
} else {
|
|
414
|
+
await addBunCacheActualDir(groups, entryPath, packageNames);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return groups;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async function removeCacheEntries(paths: string[]): Promise<number> {
|
|
421
|
+
for (const entryPath of paths) {
|
|
422
|
+
await fs.promises.rm(entryPath, { recursive: true, force: true });
|
|
423
|
+
}
|
|
424
|
+
return paths.length;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Prune Bun's package cache so each package keeps only its newest cached version.
|
|
429
|
+
*
|
|
430
|
+
* Bun stores package cache entries as both a package marker directory
|
|
431
|
+
* (`react/19.2.6@@@1`) and a materialized package directory
|
|
432
|
+
* (`react@19.2.6@@@1`). Global `omp` updates can leave one full copy per
|
|
433
|
+
* release. The marker and materialized entries are removed together so the
|
|
434
|
+
* cache stays internally consistent.
|
|
435
|
+
*/
|
|
436
|
+
export async function pruneBunInstallCache(
|
|
437
|
+
cacheDir: string,
|
|
438
|
+
packageNames?: Set<string>,
|
|
439
|
+
): Promise<BunInstallCachePruneResult> {
|
|
440
|
+
const groups = await collectBunCacheGroups(cacheDir, packageNames);
|
|
441
|
+
let scannedPackages = 0;
|
|
442
|
+
let removedEntries = 0;
|
|
443
|
+
for (const group of groups.values()) {
|
|
444
|
+
if (group.actualDirs.size === 0) continue;
|
|
445
|
+
scannedPackages++;
|
|
446
|
+
let latestVersion: string | undefined;
|
|
447
|
+
for (const version of group.actualDirs.keys()) {
|
|
448
|
+
if (!latestVersion || compareSemverLikeVersions(version, latestVersion) > 0) latestVersion = version;
|
|
449
|
+
}
|
|
450
|
+
if (!latestVersion) continue;
|
|
451
|
+
for (const [version, paths] of group.actualDirs) {
|
|
452
|
+
if (version !== latestVersion) removedEntries += await removeCacheEntries(paths);
|
|
453
|
+
}
|
|
454
|
+
for (const [version, paths] of group.markerEntries) {
|
|
455
|
+
if (version !== latestVersion) removedEntries += await removeCacheEntries(paths);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return { scannedPackages, removedEntries };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async function resolveBunInstallCacheDir(): Promise<string | undefined> {
|
|
462
|
+
try {
|
|
463
|
+
const result = await $`bun pm cache`.quiet().nothrow();
|
|
464
|
+
if (result.exitCode !== 0) return undefined;
|
|
465
|
+
const output = result.text().trim();
|
|
466
|
+
return output.length > 0 ? output : undefined;
|
|
467
|
+
} catch {
|
|
468
|
+
return undefined;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export function resolveBunGlobalNodeModulesDirFromLocations(
|
|
473
|
+
globalBinDir: string | undefined,
|
|
474
|
+
cacheDir: string | undefined,
|
|
475
|
+
): string | undefined {
|
|
476
|
+
if (globalBinDir && globalBinDir.length > 0) {
|
|
477
|
+
return path.join(path.dirname(globalBinDir), "install", "global", "node_modules");
|
|
478
|
+
}
|
|
479
|
+
if (cacheDir && cacheDir.length > 0) {
|
|
480
|
+
return path.join(path.dirname(cacheDir), "global", "node_modules");
|
|
481
|
+
}
|
|
482
|
+
return undefined;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async function resolveBunGlobalNodeModulesDir(cacheDir: string): Promise<string | undefined> {
|
|
486
|
+
try {
|
|
487
|
+
const result = await $`bun pm bin -g`.quiet().nothrow();
|
|
488
|
+
const globalBinDir = result.exitCode === 0 ? result.text().trim() : undefined;
|
|
489
|
+
return resolveBunGlobalNodeModulesDirFromLocations(globalBinDir, cacheDir);
|
|
490
|
+
} catch {
|
|
491
|
+
return resolveBunGlobalNodeModulesDirFromLocations(undefined, cacheDir);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
async function collectInstalledPackageNames(nodeModulesDir: string): Promise<Set<string>> {
|
|
496
|
+
const packageNames = new Set<string>();
|
|
497
|
+
for (const entry of await readdirIfExists(nodeModulesDir)) {
|
|
498
|
+
if (!entry.isDirectory() || entry.name === ".bin") continue;
|
|
499
|
+
if (entry.name.startsWith("@")) {
|
|
500
|
+
for (const scopedEntry of await readdirIfExists(path.join(nodeModulesDir, entry.name))) {
|
|
501
|
+
if (scopedEntry.isDirectory()) packageNames.add(`${entry.name}/${scopedEntry.name}`);
|
|
502
|
+
}
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
packageNames.add(entry.name);
|
|
506
|
+
}
|
|
507
|
+
return packageNames;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async function pruneBunCacheAfterGlobalInstall(): Promise<BunInstallCachePruneResult | undefined> {
|
|
511
|
+
const cacheDir = await resolveBunInstallCacheDir();
|
|
512
|
+
if (!cacheDir) return undefined;
|
|
513
|
+
const globalNodeModulesDir = await resolveBunGlobalNodeModulesDir(cacheDir);
|
|
514
|
+
const packageNames = globalNodeModulesDir
|
|
515
|
+
? await collectInstalledPackageNames(globalNodeModulesDir)
|
|
516
|
+
: new Set<string>();
|
|
517
|
+
if (packageNames.size === 0 && !path.basename(cacheDir).toLowerCase().includes("omp")) return undefined;
|
|
518
|
+
return await pruneBunInstallCache(cacheDir, packageNames.size === 0 ? undefined : packageNames);
|
|
519
|
+
}
|
|
520
|
+
|
|
275
521
|
/**
|
|
276
522
|
* Get the appropriate binary name for this platform.
|
|
277
523
|
*/
|
|
@@ -524,6 +770,14 @@ async function updateViaBun(expectedVersion: string): Promise<void> {
|
|
|
524
770
|
}
|
|
525
771
|
|
|
526
772
|
await printVerification(expectedVersion);
|
|
773
|
+
try {
|
|
774
|
+
const pruneResult = await pruneBunCacheAfterGlobalInstall();
|
|
775
|
+
if (pruneResult && pruneResult.removedEntries > 0) {
|
|
776
|
+
console.log(chalk.dim(`Pruned ${pruneResult.removedEntries} stale Bun cache entries`));
|
|
777
|
+
}
|
|
778
|
+
} catch (err) {
|
|
779
|
+
console.log(chalk.yellow(`Warning: could not prune stale Bun cache entries: ${err}`));
|
|
780
|
+
}
|
|
527
781
|
}
|
|
528
782
|
|
|
529
783
|
async function updateViaHomebrew(expectedVersion: string, force: boolean): Promise<void> {
|
package/src/collab/guest.ts
CHANGED
|
@@ -78,6 +78,46 @@ interface PendingSnapshot {
|
|
|
78
78
|
isResync: boolean;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/** Minimal context surface the idle-state reconciler mutates. */
|
|
82
|
+
export interface GuestIdleReconcilerCtx {
|
|
83
|
+
statusLine: { markActivityEnd: () => void };
|
|
84
|
+
loadingAnimation: { stop: () => void } | undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Close the guest UI state held open by an earlier `agent_start` whose
|
|
89
|
+
* matching `agent_end` never reached us — most often because a reconnect
|
|
90
|
+
* dropped the event mid-stream. Triggered from {@link CollabGuestLink}'s
|
|
91
|
+
* `state` reconciler when the host reports `isStreaming === false`:
|
|
92
|
+
* folds the in-flight active-time window into the per-session meter (so
|
|
93
|
+
* `time_spent` stops ticking) and stops the `Working…` loader if one is
|
|
94
|
+
* still animating. No-op when the host is still streaming.
|
|
95
|
+
*
|
|
96
|
+
* Exported for direct unit testing; mutates the loader field on `ctx` so
|
|
97
|
+
* the same loader is not stopped twice on subsequent reconciliations.
|
|
98
|
+
*/
|
|
99
|
+
export function reconcileGuestIdleHostState(ctx: GuestIdleReconcilerCtx, isStreaming: boolean): void {
|
|
100
|
+
if (isStreaming) return;
|
|
101
|
+
ctx.statusLine.markActivityEnd();
|
|
102
|
+
if (ctx.loadingAnimation) {
|
|
103
|
+
ctx.loadingAnimation.stop();
|
|
104
|
+
ctx.loadingAnimation = undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Reconcile a welcome/resync snapshot's host activity state into the guest meter. */
|
|
109
|
+
export interface GuestSnapshotActivityReconcilerCtx extends GuestIdleReconcilerCtx {
|
|
110
|
+
statusLine: GuestIdleReconcilerCtx["statusLine"] & { markActivityStart: () => void };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function reconcileGuestSnapshotHostState(ctx: GuestSnapshotActivityReconcilerCtx, isStreaming: boolean): void {
|
|
114
|
+
if (isStreaming) {
|
|
115
|
+
ctx.statusLine.markActivityStart();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
reconcileGuestIdleHostState(ctx, false);
|
|
119
|
+
}
|
|
120
|
+
|
|
81
121
|
export class CollabGuestLink {
|
|
82
122
|
#ctx: InteractiveModeContext;
|
|
83
123
|
#socket: CollabSocket | null = null;
|
|
@@ -355,6 +395,7 @@ export class CollabGuestLink {
|
|
|
355
395
|
this.#clearAgentMirror();
|
|
356
396
|
await this.#ctx.session.switchSession(replicaPath);
|
|
357
397
|
this.state = pending.state;
|
|
398
|
+
reconcileGuestSnapshotHostState(this.#ctx, pending.state.isStreaming);
|
|
358
399
|
this.#applyHostState(pending.state);
|
|
359
400
|
this.#ctx.resetObserverRegistry();
|
|
360
401
|
this.#applyAgentSnapshots(pending.agents);
|
|
@@ -425,12 +466,7 @@ export class CollabGuestLink {
|
|
|
425
466
|
this.#applyHostState(frame.state);
|
|
426
467
|
setSessionTerminalTitle(frame.state.sessionName, frame.state.cwd);
|
|
427
468
|
this.#updateStatusSegment();
|
|
428
|
-
|
|
429
|
-
// the host reports idle (e.g. events lost across a reconnect).
|
|
430
|
-
if (!frame.state.isStreaming && this.#ctx.loadingAnimation) {
|
|
431
|
-
this.#ctx.loadingAnimation.stop();
|
|
432
|
-
this.#ctx.loadingAnimation = undefined;
|
|
433
|
-
}
|
|
469
|
+
reconcileGuestIdleHostState(this.#ctx, frame.state.isStreaming);
|
|
434
470
|
this.#ctx.statusLine.invalidate();
|
|
435
471
|
this.#ctx.ui.requestRender();
|
|
436
472
|
break;
|
|
@@ -588,7 +624,7 @@ export class CollabGuestLink {
|
|
|
588
624
|
await this.#ctx.session.newSession();
|
|
589
625
|
setSessionTerminalTitle(this.#ctx.sessionManager.getSessionName(), this.#ctx.sessionManager.getCwd());
|
|
590
626
|
this.#ctx.statusLine.invalidate();
|
|
591
|
-
this.#ctx.statusLine.
|
|
627
|
+
this.#ctx.statusLine.resetActiveTime();
|
|
592
628
|
this.#ctx.updateEditorTopBorder();
|
|
593
629
|
this.#ctx.updateEditorBorderColor();
|
|
594
630
|
this.#ctx.renderInitialMessages({ clearTerminalHistory: true });
|
package/src/collab/host.ts
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
parseCollabLink,
|
|
38
38
|
} from "./protocol";
|
|
39
39
|
import { CollabSocket } from "./relay-client";
|
|
40
|
+
import { shrinkForReplication } from "./replication-shrink";
|
|
40
41
|
|
|
41
42
|
/** Events that change the footer state guests render. */
|
|
42
43
|
const STATE_TRIGGER_EVENTS: Record<string, true> = {
|
|
@@ -212,7 +213,7 @@ export class CollabHost {
|
|
|
212
213
|
}
|
|
213
214
|
|
|
214
215
|
this.#unsubscribe = this.#ctx.session.subscribe(event => {
|
|
215
|
-
if (isWireAgentEvent(event)) this.#broadcast({ t: "event", event });
|
|
216
|
+
if (isWireAgentEvent(event)) this.#broadcast({ t: "event", event: shrinkForReplication(event) });
|
|
216
217
|
this.#onEventForState(event);
|
|
217
218
|
});
|
|
218
219
|
const bus = this.#ctx.eventBus;
|
|
@@ -223,7 +224,7 @@ export class CollabHost {
|
|
|
223
224
|
}
|
|
224
225
|
this.#registryUnsubscribe = AgentRegistry.global().onChange(() => this.#scheduleAgentsBroadcast());
|
|
225
226
|
this.#ctx.sessionManager.onEntryAppended = entry => {
|
|
226
|
-
if (isWireSessionEntry(entry)) this.#broadcast({ t: "entry", entry });
|
|
227
|
+
if (isWireSessionEntry(entry)) this.#broadcast({ t: "entry", entry: shrinkForReplication(entry) });
|
|
227
228
|
// Model/thinking/title changes land as entries while idle; refresh
|
|
228
229
|
// guest state promptly (debounce + JSON diff dedupe).
|
|
229
230
|
this.#scheduleStateBroadcast();
|
|
@@ -358,10 +359,13 @@ export class CollabHost {
|
|
|
358
359
|
|
|
359
360
|
/**
|
|
360
361
|
* Slice {@link entries} into byte-bounded `snapshot-chunk` frames targeted
|
|
361
|
-
* at {@link fromPeer}.
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
* `
|
|
362
|
+
* at {@link fromPeer}. Each entry is first run through
|
|
363
|
+
* {@link shrinkForReplication} so a single oversized tool-result entry
|
|
364
|
+
* cannot ship as an oversized chunk that trips the relay's per-frame
|
|
365
|
+
* `maxPayloadLength` (issue #3739). Every batch carries at least one
|
|
366
|
+
* entry, and the last batch is tagged `final: true` so the guest can
|
|
367
|
+
* finalize the replica. An empty snapshot still emits one `final` chunk
|
|
368
|
+
* so the guest never blocks on a missing terminator.
|
|
365
369
|
*/
|
|
366
370
|
#sendSnapshotChunks(entries: (StoredSessionEntry & WireSessionEntry)[], fromPeer: number): void {
|
|
367
371
|
const socket = this.#socket;
|
|
@@ -377,9 +381,10 @@ export class CollabHost {
|
|
|
377
381
|
while (i < entries.length) {
|
|
378
382
|
const entry = entries[i];
|
|
379
383
|
if (!entry) break;
|
|
380
|
-
const
|
|
384
|
+
const shrunk = shrinkForReplication(entry);
|
|
385
|
+
const entryBytes = JSON.stringify(shrunk).length;
|
|
381
386
|
if (batch.length > 0 && batchBytes + entryBytes > SNAPSHOT_CHUNK_BYTES) break;
|
|
382
|
-
batch.push(
|
|
387
|
+
batch.push(shrunk);
|
|
383
388
|
batchBytes += entryBytes;
|
|
384
389
|
i++;
|
|
385
390
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hard-cap helper for host→guest collab frames.
|
|
3
|
+
*
|
|
4
|
+
* The host wraps every {@link CollabFrame} in an AES-GCM envelope and ships it
|
|
5
|
+
* through the relay's WebSocket. WebSocket servers enforce a per-frame
|
|
6
|
+
* `maxPayloadLength` (Bun's default is 16 MB; many proxies cap lower). A
|
|
7
|
+
* single oversized payload — typically a `read`/`bash`/`search` tool result
|
|
8
|
+
* captured as one multi-megabyte string, or a tool result whose `content`
|
|
9
|
+
* array holds thousands of small blocks — would otherwise ship as its own
|
|
10
|
+
* oversized frame and trip that limit, killing the host's WebSocket with
|
|
11
|
+
* `1006 Received too big message`. `CollabSocket` treats 1006 as transient
|
|
12
|
+
* and reconnects, the next guest hello triggers the same oversized send, and
|
|
13
|
+
* the loop never breaks (issue #3739).
|
|
14
|
+
*
|
|
15
|
+
* This helper bounds any JSON-serializable payload below
|
|
16
|
+
* {@link MAX_REPLICATED_PAYLOAD_BYTES}. Already-small payloads pass through
|
|
17
|
+
* untouched; oversized ones are returned as a deep-cloned shadow where long
|
|
18
|
+
* strings are head-truncated AND long arrays are head-clipped, with
|
|
19
|
+
* `[…N chars elided for collab session]` / `[…N items elided for collab
|
|
20
|
+
* session]` markers. Both axes are needed: string truncation alone leaves
|
|
21
|
+
* the cap unenforced for a payload built of many short strings, where no
|
|
22
|
+
* field exceeds the per-string floor.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Per-payload ceiling for host→guest frames. Bun's default WebSocket
|
|
27
|
+
* `maxPayloadLength` is 16 MB; we leave a generous margin so the AES-GCM
|
|
28
|
+
* envelope (+ IV + tag), the 4-byte peer header, and the outer wire wrapper
|
|
29
|
+
* fit comfortably under that on every reasonable relay.
|
|
30
|
+
*/
|
|
31
|
+
export const MAX_REPLICATED_PAYLOAD_BYTES = 1 * 1024 * 1024;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Progressive shrink passes. Each pass tightens both the per-string cap and
|
|
35
|
+
* the per-array head limit; the loop stops at the first pass whose output
|
|
36
|
+
* fits {@link MAX_REPLICATED_PAYLOAD_BYTES}. The schedule is concrete (not
|
|
37
|
+
* recomputed) so the failure modes the helper guards against are visible:
|
|
38
|
+
*
|
|
39
|
+
* - One giant string → the first pass already truncates it under 64 KB.
|
|
40
|
+
* - Array of many small blocks (e.g. a tool result with thousands of
|
|
41
|
+
* `{type:"text", text:"..."}` content items) → later passes head-clip the
|
|
42
|
+
* array to a small sample with a `[…N items elided]` summary element.
|
|
43
|
+
*
|
|
44
|
+
* The final pass clamps every string to 64 B and every array to one element,
|
|
45
|
+
* so even pathological mixes converge.
|
|
46
|
+
*/
|
|
47
|
+
interface ShrinkPass {
|
|
48
|
+
stringCap: number;
|
|
49
|
+
arrayLimit: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const SHRINK_PASSES: readonly ShrinkPass[] = [
|
|
53
|
+
{ stringCap: 64 * 1024, arrayLimit: 256 },
|
|
54
|
+
{ stringCap: 16 * 1024, arrayLimit: 128 },
|
|
55
|
+
{ stringCap: 4 * 1024, arrayLimit: 64 },
|
|
56
|
+
{ stringCap: 1 * 1024, arrayLimit: 32 },
|
|
57
|
+
{ stringCap: 256, arrayLimit: 16 },
|
|
58
|
+
{ stringCap: 256, arrayLimit: 4 },
|
|
59
|
+
{ stringCap: 64, arrayLimit: 1 },
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const STRING_ELISION_RESERVE = 80;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Recursively walk `value`, head-truncating any string longer than
|
|
66
|
+
* `stringCap` and head-clipping any array longer than `arrayLimit`. Returns
|
|
67
|
+
* a freshly built deep clone — every object/array is rebuilt so the
|
|
68
|
+
* recursive output can be safely serialized in isolation. Cheap to call on
|
|
69
|
+
* small values: short strings, numbers, and booleans pass through without
|
|
70
|
+
* allocation.
|
|
71
|
+
*/
|
|
72
|
+
function shrinkWalk(value: unknown, stringCap: number, arrayLimit: number): unknown {
|
|
73
|
+
if (typeof value === "string") {
|
|
74
|
+
if (value.length <= stringCap) return value;
|
|
75
|
+
const headLen = Math.max(0, stringCap - STRING_ELISION_RESERVE);
|
|
76
|
+
return `${value.slice(0, headLen)}\n…[${value.length - headLen} chars elided for collab session]`;
|
|
77
|
+
}
|
|
78
|
+
if (Array.isArray(value)) {
|
|
79
|
+
const keep = Math.min(value.length, arrayLimit);
|
|
80
|
+
const elided = value.length - keep;
|
|
81
|
+
const out: unknown[] = new Array(elided > 0 ? keep + 1 : keep);
|
|
82
|
+
for (let i = 0; i < keep; i++) out[i] = shrinkWalk(value[i], stringCap, arrayLimit);
|
|
83
|
+
if (elided > 0) out[keep] = `…[${elided} items elided for collab session]`;
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
if (value && typeof value === "object") {
|
|
87
|
+
const src = value as Record<string, unknown>;
|
|
88
|
+
const out: Record<string, unknown> = {};
|
|
89
|
+
for (const k in src) out[k] = shrinkWalk(src[k], stringCap, arrayLimit);
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Return `value` unchanged when its JSON serialization already fits
|
|
97
|
+
* {@link MAX_REPLICATED_PAYLOAD_BYTES}; otherwise return a deep-cloned
|
|
98
|
+
* shadow shrunk along both string and array axes until the payload fits.
|
|
99
|
+
* The function is generic over `T` because the wire shape is preserved:
|
|
100
|
+
* only string leaves and array tails change; discriminator fields, ids, and
|
|
101
|
+
* other small metadata pass through untouched.
|
|
102
|
+
*/
|
|
103
|
+
export function shrinkForReplication<T>(value: T): T {
|
|
104
|
+
if (JSON.stringify(value).length <= MAX_REPLICATED_PAYLOAD_BYTES) return value;
|
|
105
|
+
let shrunk: unknown = value;
|
|
106
|
+
for (const pass of SHRINK_PASSES) {
|
|
107
|
+
shrunk = shrinkWalk(value, pass.stringCap, pass.arrayLimit);
|
|
108
|
+
if (JSON.stringify(shrunk).length <= MAX_REPLICATED_PAYLOAD_BYTES) return shrunk as T;
|
|
109
|
+
}
|
|
110
|
+
return shrunk as T;
|
|
111
|
+
}
|
|
@@ -303,16 +303,82 @@ function resolveConfigValue(valueConfig: string): string | undefined {
|
|
|
303
303
|
return valueConfig;
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
-
|
|
307
|
-
|
|
306
|
+
type HeaderSource = Record<string, string> | undefined;
|
|
307
|
+
|
|
308
|
+
interface HeaderResolutionOptions {
|
|
309
|
+
authHeader?: boolean;
|
|
310
|
+
apiKeyConfig?: string;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function materializeConfigHeaderSources(
|
|
314
|
+
sources: readonly HeaderSource[],
|
|
315
|
+
options?: HeaderResolutionOptions,
|
|
316
|
+
): Record<string, string> | undefined {
|
|
308
317
|
const resolved: Record<string, string> = {};
|
|
309
|
-
for (const
|
|
310
|
-
|
|
311
|
-
|
|
318
|
+
for (const source of sources) {
|
|
319
|
+
if (!source) continue;
|
|
320
|
+
for (const [key, value] of Object.entries(source)) {
|
|
321
|
+
const next = resolveConfigValue(value);
|
|
322
|
+
if (next) resolved[key] = next;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (options?.authHeader && options.apiKeyConfig) {
|
|
326
|
+
const resolvedKey = resolveConfigValue(options.apiKeyConfig);
|
|
327
|
+
if (resolvedKey) resolved.Authorization = `Bearer ${resolvedKey}`;
|
|
312
328
|
}
|
|
313
329
|
return Object.keys(resolved).length > 0 ? resolved : undefined;
|
|
314
330
|
}
|
|
315
331
|
|
|
332
|
+
function createLiveConfigHeaders(
|
|
333
|
+
sources: readonly HeaderSource[],
|
|
334
|
+
options?: HeaderResolutionOptions,
|
|
335
|
+
): Record<string, string> | undefined {
|
|
336
|
+
const liveSources = sources.filter((source): source is Record<string, string> => source !== undefined);
|
|
337
|
+
if (liveSources.length === 0 && (!options?.authHeader || !options.apiKeyConfig)) return undefined;
|
|
338
|
+
|
|
339
|
+
const localHeaders: Record<string, string> = {};
|
|
340
|
+
const allSources = [...liveSources, localHeaders];
|
|
341
|
+
const current = () => materializeConfigHeaderSources(allSources, options) ?? {};
|
|
342
|
+
return new Proxy(localHeaders, {
|
|
343
|
+
get(target, property, receiver) {
|
|
344
|
+
if (typeof property !== "string") return Reflect.get(target, property, receiver);
|
|
345
|
+
return current()[property];
|
|
346
|
+
},
|
|
347
|
+
set(target, property, value) {
|
|
348
|
+
if (typeof property !== "string" || typeof value !== "string") return false;
|
|
349
|
+
target[property] = value;
|
|
350
|
+
return true;
|
|
351
|
+
},
|
|
352
|
+
deleteProperty(target, property) {
|
|
353
|
+
if (typeof property !== "string") return false;
|
|
354
|
+
delete target[property];
|
|
355
|
+
return true;
|
|
356
|
+
},
|
|
357
|
+
has(_target, property) {
|
|
358
|
+
if (typeof property !== "string") return false;
|
|
359
|
+
return Object.hasOwn(current(), property);
|
|
360
|
+
},
|
|
361
|
+
ownKeys() {
|
|
362
|
+
return Reflect.ownKeys(current());
|
|
363
|
+
},
|
|
364
|
+
getOwnPropertyDescriptor(_target, property) {
|
|
365
|
+
if (typeof property !== "string") return undefined;
|
|
366
|
+
const headers = current();
|
|
367
|
+
if (!Object.hasOwn(headers, property)) return undefined;
|
|
368
|
+
return {
|
|
369
|
+
configurable: true,
|
|
370
|
+
enumerable: true,
|
|
371
|
+
value: headers[property],
|
|
372
|
+
writable: true,
|
|
373
|
+
};
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function resolveConfigHeaders(headers: Record<string, string> | undefined): Record<string, string> | undefined {
|
|
379
|
+
return materializeConfigHeaderSources([headers]);
|
|
380
|
+
}
|
|
381
|
+
|
|
316
382
|
function extractGoogleOAuthToken(value: string | undefined): string | undefined {
|
|
317
383
|
if (!isAuthenticated(value)) return undefined;
|
|
318
384
|
try {
|
|
@@ -495,21 +561,15 @@ function mergeCustomModelHeaders(
|
|
|
495
561
|
authHeader: boolean | undefined,
|
|
496
562
|
apiKeyConfig: string | undefined,
|
|
497
563
|
): Record<string, string> | undefined {
|
|
498
|
-
|
|
499
|
-
return mergeAuthHeader({ ...providerHeaders, ...resolvedModelHeaders }, authHeader, apiKeyConfig);
|
|
564
|
+
return createLiveConfigHeaders([providerHeaders, modelHeaders], { authHeader, apiKeyConfig });
|
|
500
565
|
}
|
|
501
566
|
|
|
502
|
-
function
|
|
503
|
-
|
|
567
|
+
function mergeAuthHeaderSources(
|
|
568
|
+
sources: readonly HeaderSource[],
|
|
504
569
|
authHeader: boolean | undefined,
|
|
505
570
|
apiKeyConfig: string | undefined,
|
|
506
571
|
): Record<string, string> | undefined {
|
|
507
|
-
|
|
508
|
-
if (!authHeader || !apiKeyConfig) {
|
|
509
|
-
return nextHeaders;
|
|
510
|
-
}
|
|
511
|
-
const resolvedKey = resolveConfigValue(apiKeyConfig);
|
|
512
|
-
return resolvedKey ? { ...nextHeaders, Authorization: `Bearer ${resolvedKey}` } : nextHeaders;
|
|
572
|
+
return createLiveConfigHeaders(sources, { authHeader, apiKeyConfig });
|
|
513
573
|
}
|
|
514
574
|
|
|
515
575
|
/**
|
|
@@ -1661,7 +1721,9 @@ export class ModelRegistry {
|
|
|
1661
1721
|
baseUrl: override.baseUrl ?? baseOverride?.baseUrl,
|
|
1662
1722
|
apiKey: override.apiKey ?? baseOverride?.apiKey,
|
|
1663
1723
|
authHeader: override.authHeader ?? baseOverride?.authHeader,
|
|
1664
|
-
headers: override.headers
|
|
1724
|
+
headers: override.headers
|
|
1725
|
+
? createLiveConfigHeaders([baseOverride?.headers, override.headers])
|
|
1726
|
+
: baseOverride?.headers,
|
|
1665
1727
|
compat: override.compat ? mergeCompat(baseOverride?.compat, override.compat) : baseOverride?.compat,
|
|
1666
1728
|
remoteCompaction: mergeRemoteCompactionConfig(baseOverride?.remoteCompaction, override.remoteCompaction),
|
|
1667
1729
|
transport: override.transport ?? baseOverride?.transport,
|
|
@@ -1676,8 +1738,8 @@ export class ModelRegistry {
|
|
|
1676
1738
|
"baseUrl" | "headers" | "authHeader" | "apiKey" | "remoteCompaction" | "transport"
|
|
1677
1739
|
>,
|
|
1678
1740
|
): T {
|
|
1679
|
-
const headers =
|
|
1680
|
-
override.headers ?
|
|
1741
|
+
const headers = mergeAuthHeaderSources(
|
|
1742
|
+
override.headers ? [entry.headers, override.headers] : [entry.headers],
|
|
1681
1743
|
override.authHeader,
|
|
1682
1744
|
override.apiKey,
|
|
1683
1745
|
);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as imageGen from "../tools/image-gen";
|
|
2
|
+
import * as webSearch from "../web/search";
|
|
3
|
+
|
|
4
|
+
interface ProviderGlobalSettings {
|
|
5
|
+
get(path: "providers.webSearchExclude"): unknown;
|
|
6
|
+
get(path: "providers.webSearch"): unknown;
|
|
7
|
+
get(path: "providers.image"): unknown;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function applyProviderGlobalsFromSettings(settings: ProviderGlobalSettings): void {
|
|
11
|
+
const excludedWebSearchProviders = settings.get("providers.webSearchExclude");
|
|
12
|
+
if (Array.isArray(excludedWebSearchProviders)) {
|
|
13
|
+
webSearch.setExcludedSearchProviders(excludedWebSearchProviders.filter(webSearch.isSearchProviderId));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const webSearchProvider = settings.get("providers.webSearch");
|
|
17
|
+
if (typeof webSearchProvider === "string" && webSearch.isSearchProviderPreference(webSearchProvider)) {
|
|
18
|
+
webSearch.setPreferredSearchProvider(webSearchProvider);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const imageProvider = settings.get("providers.image");
|
|
22
|
+
if (imageGen.isImageProviderPreference(imageProvider)) {
|
|
23
|
+
imageGen.setPreferredImageProvider(imageProvider);
|
|
24
|
+
}
|
|
25
|
+
}
|