@oh-my-pi/pi-coding-agent 17.1.6 → 17.1.8
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 +58 -0
- package/dist/{CHANGELOG-x9zt79k8.md → CHANGELOG-vt8ene9g.md} +58 -0
- package/dist/cli.js +4309 -7287
- package/dist/template-dys3vk5b.js +1671 -0
- package/dist/template-f8wx9vfn.css +1355 -0
- package/dist/template-qat058wr.html +55 -0
- package/dist/tool-views.generated-jdfmzwmn.js +35 -0
- package/dist/types/advisor/advise-tool.d.ts +0 -7
- package/dist/types/advisor/runtime.d.ts +0 -9
- package/dist/types/async/job-manager.d.ts +11 -0
- package/dist/types/cleanse/agent.d.ts +19 -0
- package/dist/types/cleanse/balance.d.ts +7 -0
- package/dist/types/cleanse/checkers.d.ts +13 -0
- package/dist/types/cleanse/index.d.ts +16 -0
- package/dist/types/cleanse/loop.d.ts +16 -0
- package/dist/types/cleanse/parsers.d.ts +13 -0
- package/dist/types/cleanse/progress.d.ts +14 -0
- package/dist/types/cleanse/types.d.ts +64 -0
- package/dist/types/commands/cleanse.d.ts +23 -0
- package/dist/types/config/settings-schema.d.ts +16 -1
- package/dist/types/cursor.d.ts +2 -1
- package/dist/types/export/html/index.d.ts +2 -0
- package/dist/types/extensibility/extensions/runner.d.ts +4 -0
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +27 -1
- package/dist/types/extensibility/shared-events.d.ts +18 -1
- package/dist/types/modes/components/custom-editor.d.ts +5 -0
- package/dist/types/modes/interactive-mode.d.ts +4 -3
- package/dist/types/session/agent-session-types.d.ts +5 -5
- package/dist/types/session/agent-session.d.ts +26 -1
- package/dist/types/session/async-job-delivery.d.ts +8 -0
- package/dist/types/session/model-controls.d.ts +4 -4
- package/dist/types/session/session-tools.d.ts +44 -6
- package/dist/types/session/streaming-output.d.ts +7 -2
- package/dist/types/session/turn-recovery.d.ts +1 -1
- package/dist/types/task/isolation-ownership.d.ts +34 -0
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +1 -2
- package/dist/types/tools/browser/tab-worker.d.ts +1 -4
- package/dist/types/tools/index.d.ts +8 -3
- package/dist/types/tools/output-meta.d.ts +5 -0
- package/dist/types/tools/read.d.ts +9 -1
- package/dist/types/tools/xdev.d.ts +53 -67
- package/dist/types/tui/output-block.d.ts +5 -5
- package/dist/types/utils/cpuprofile.d.ts +51 -0
- package/dist/types/utils/inspect-image-mode.d.ts +29 -0
- package/dist/types/utils/profile-tree.d.ts +47 -0
- package/dist/types/utils/sample-profile.d.ts +67 -0
- package/package.json +16 -12
- package/scripts/bundle-dist.ts +3 -1
- package/src/advisor/advise-tool.ts +0 -11
- package/src/advisor/runtime.ts +0 -12
- package/src/async/job-manager.ts +30 -7
- package/src/cleanse/agent.ts +226 -0
- package/src/cleanse/balance.ts +79 -0
- package/src/cleanse/checkers.ts +996 -0
- package/src/cleanse/index.ts +190 -0
- package/src/cleanse/loop.ts +51 -0
- package/src/cleanse/parsers.ts +726 -0
- package/src/cleanse/progress.ts +50 -0
- package/src/cleanse/prompts/assignment.md +47 -0
- package/src/cleanse/types.ts +72 -0
- package/src/cli/update-cli.ts +3 -0
- package/src/cli/usage-cli.ts +29 -4
- package/src/cli/worktree-cli.ts +28 -11
- package/src/cli-commands.ts +1 -0
- package/src/cli.ts +2 -1
- package/src/commands/cleanse.ts +45 -0
- package/src/config/settings-schema.ts +15 -1
- package/src/config/settings.ts +35 -0
- package/src/cursor.ts +4 -3
- package/src/discovery/builtin-rules/index.ts +2 -0
- package/src/discovery/builtin-rules/ts-no-local-is-record.md +48 -0
- package/src/discovery/claude-plugins.ts +144 -34
- package/src/export/html/index.ts +17 -10
- package/src/extensibility/extensions/runner.ts +26 -0
- package/src/extensibility/extensions/wrapper.ts +74 -42
- package/src/extensibility/hooks/tool-wrapper.ts +11 -4
- package/src/extensibility/legacy-pi-ai-shim.ts +46 -1
- package/src/extensibility/shared-events.ts +18 -1
- package/src/launch/broker.ts +14 -4
- package/src/modes/acp/acp-agent.ts +12 -9
- package/src/modes/acp/acp-event-mapper.ts +38 -4
- package/src/modes/components/custom-editor.ts +39 -16
- package/src/modes/components/settings-selector.ts +9 -1
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/tool-execution.ts +8 -7
- package/src/modes/controllers/extension-ui-controller.ts +4 -4
- package/src/modes/controllers/selector-controller.ts +7 -2
- package/src/modes/interactive-mode.ts +36 -47
- package/src/modes/prompt-action-autocomplete.ts +5 -3
- package/src/prompts/goals/guided-goal-interview.md +41 -6
- package/src/prompts/system/vibe-mode-active.md +4 -1
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/goal.md +1 -1
- package/src/sdk.ts +47 -50
- package/src/session/agent-session-types.ts +5 -5
- package/src/session/agent-session.ts +151 -11
- package/src/session/async-job-delivery.ts +8 -0
- package/src/session/model-controls.ts +9 -9
- package/src/session/session-advisors.ts +2 -7
- package/src/session/session-history-format.ts +31 -6
- package/src/session/session-listing.ts +66 -4
- package/src/session/session-tools.ts +158 -52
- package/src/session/streaming-output.ts +18 -6
- package/src/session/turn-recovery.ts +4 -4
- package/src/slash-commands/builtin-registry.ts +74 -2
- package/src/task/isolation-ownership.ts +106 -0
- package/src/task/worktree.ts +8 -0
- package/src/tools/ask.ts +3 -3
- package/src/tools/ast-edit.ts +9 -2
- package/src/tools/bash.ts +16 -9
- package/src/tools/browser/cmux/cmux-tab.ts +9 -14
- package/src/tools/browser/tab-worker.ts +12 -35
- package/src/tools/browser.ts +2 -2
- package/src/tools/gh-renderer.ts +3 -3
- package/src/tools/index.ts +46 -17
- package/src/tools/output-meta.ts +20 -0
- package/src/tools/read.ts +87 -13
- package/src/tools/write.ts +51 -7
- package/src/tools/xdev.ts +198 -210
- package/src/tui/code-cell.ts +4 -4
- package/src/tui/output-block.ts +25 -8
- package/src/utils/cpuprofile.ts +235 -0
- package/src/utils/inspect-image-mode.ts +39 -0
- package/src/utils/profile-tree.ts +111 -0
- package/src/utils/sample-profile.ts +437 -0
- package/src/utils/shell-snapshot-fn-env.sh +5 -2
- package/src/web/search/render.ts +2 -2
- package/dist/types/goals/guided-setup.d.ts +0 -30
- package/src/goals/guided-setup.ts +0 -171
- package/src/prompts/goals/guided-goal-system.md +0 -33
|
@@ -52,7 +52,7 @@ export interface ModelControlsHost {
|
|
|
52
52
|
promptGeneration(): number;
|
|
53
53
|
resolveActiveEditMode(): EditMode;
|
|
54
54
|
syncAfterModelChange(previousEditMode: EditMode): Promise<void>;
|
|
55
|
-
setModelWithProviderSessionReset(model: Model): void
|
|
55
|
+
setModelWithProviderSessionReset(model: Model): Promise<void>;
|
|
56
56
|
clearActiveRetryFallback(): void;
|
|
57
57
|
clearInheritedProviderPromptCacheKey(): void;
|
|
58
58
|
magicKeywordEnabled(keyword: "orchestrate" | "ultrathink" | "workflow"): boolean;
|
|
@@ -220,7 +220,7 @@ export class ModelControls {
|
|
|
220
220
|
|
|
221
221
|
this.#host.modelRegistry.clearSuppressedSelector(formatModelStringWithRouting(targetModel));
|
|
222
222
|
this.#host.clearActiveRetryFallback();
|
|
223
|
-
this.#host.setModelWithProviderSessionReset(targetModel);
|
|
223
|
+
await this.#host.setModelWithProviderSessionReset(targetModel);
|
|
224
224
|
this.#host.sessionManager.appendModelChange(`${targetModel.provider}/${targetModel.id}`, role);
|
|
225
225
|
if (options?.persist) {
|
|
226
226
|
this.#host.settings.setModelRole(
|
|
@@ -265,7 +265,7 @@ export class ModelControls {
|
|
|
265
265
|
|
|
266
266
|
this.#host.modelRegistry.clearSuppressedSelector(formatModelStringWithRouting(targetModel));
|
|
267
267
|
this.#host.clearActiveRetryFallback();
|
|
268
|
-
this.#host.setModelWithProviderSessionReset(targetModel);
|
|
268
|
+
await this.#host.setModelWithProviderSessionReset(targetModel);
|
|
269
269
|
this.#host.sessionManager.appendModelChange(
|
|
270
270
|
`${targetModel.provider}/${targetModel.id}`,
|
|
271
271
|
options?.ephemeral ? EPHEMERAL_MODEL_CHANGE_ROLE : "temporary",
|
|
@@ -425,7 +425,7 @@ export class ModelControls {
|
|
|
425
425
|
// Apply model
|
|
426
426
|
this.#host.modelRegistry.clearSuppressedSelector(formatModelStringWithRouting(next.model));
|
|
427
427
|
this.#host.clearActiveRetryFallback();
|
|
428
|
-
this.#host.setModelWithProviderSessionReset(next.model);
|
|
428
|
+
await this.#host.setModelWithProviderSessionReset(next.model);
|
|
429
429
|
this.#host.sessionManager.appendModelChange(`${next.model.provider}/${next.model.id}`);
|
|
430
430
|
this.#host.settings.getStorage()?.recordModelUsage(`${next.model.provider}/${next.model.id}`);
|
|
431
431
|
|
|
@@ -456,7 +456,7 @@ export class ModelControls {
|
|
|
456
456
|
|
|
457
457
|
this.#host.modelRegistry.clearSuppressedSelector(formatModelStringWithRouting(nextModel));
|
|
458
458
|
this.#host.clearActiveRetryFallback();
|
|
459
|
-
this.#host.setModelWithProviderSessionReset(nextModel);
|
|
459
|
+
await this.#host.setModelWithProviderSessionReset(nextModel);
|
|
460
460
|
this.#host.sessionManager.appendModelChange(`${nextModel.provider}/${nextModel.id}`);
|
|
461
461
|
this.#host.settings.getStorage()?.recordModelUsage(`${nextModel.provider}/${nextModel.id}`);
|
|
462
462
|
// Re-apply the current thinking level (or auto) for the newly selected model
|
|
@@ -581,9 +581,9 @@ export class ModelControls {
|
|
|
581
581
|
|
|
582
582
|
/**
|
|
583
583
|
* Classify the current user turn and set the effective thinking level for it.
|
|
584
|
-
* Bounded by a timeout + abort; on
|
|
585
|
-
*
|
|
586
|
-
* throws into the turn, and never clears `#autoThinking
|
|
584
|
+
* Bounded by a timeout + abort; on failure it preserves the last classified
|
|
585
|
+
* level, or uses the provisional concrete level before the first resolution.
|
|
586
|
+
* Never throws into the turn, and never clears `#autoThinking`.
|
|
587
587
|
*/
|
|
588
588
|
async applyAutoThinkingLevel(promptText: string, generation: number): Promise<void> {
|
|
589
589
|
const model = this.#model;
|
|
@@ -625,7 +625,7 @@ export class ModelControls {
|
|
|
625
625
|
|
|
626
626
|
const effort = clampThinkingLevelToCeiling(
|
|
627
627
|
model,
|
|
628
|
-
resolved ?? resolveProvisionalAutoLevel(model),
|
|
628
|
+
resolved ?? this.#autoResolvedLevel ?? resolveProvisionalAutoLevel(model),
|
|
629
629
|
this.#thinkingLevelCeiling,
|
|
630
630
|
);
|
|
631
631
|
if (effort === undefined) return;
|
|
@@ -48,7 +48,6 @@ import {
|
|
|
48
48
|
type AdvisorSeverity,
|
|
49
49
|
AdvisorTranscriptRecorder,
|
|
50
50
|
advisorTranscriptFilename,
|
|
51
|
-
annotateForStaleness,
|
|
52
51
|
buildAdvisorQuarantineSourceText,
|
|
53
52
|
formatAdvisorBatchContent,
|
|
54
53
|
getOrCreateAdvisorProviderSessionId,
|
|
@@ -890,10 +889,6 @@ export class SessionAdvisors {
|
|
|
890
889
|
logger.debug("advisor advice suppressed by emission guard", { severity, advisor: advisor.name });
|
|
891
890
|
return;
|
|
892
891
|
}
|
|
893
|
-
// When newer primary turns already arrived while the advisor model was
|
|
894
|
-
// processing this batch, the advice was generated without seeing them.
|
|
895
|
-
// Append a lightweight staleness caveat so the primary can weigh recency.
|
|
896
|
-
const deliveredNote = annotateForStaleness(note, advisor.runtime.hasFreshBacklog);
|
|
897
892
|
// The implicit single ("default") advisor stamps no source name, so its
|
|
898
893
|
// agent-facing `<advisory>` bytes stay identical to the pre-multi-advisor path.
|
|
899
894
|
const source = advisor.slug ? advisor.name : undefined;
|
|
@@ -911,10 +906,10 @@ export class SessionAdvisors {
|
|
|
911
906
|
interruptImmuneTurnActive: interrupting && this.#isAdvisorInterruptImmuneTurnActive(),
|
|
912
907
|
});
|
|
913
908
|
if (channel === "aside") {
|
|
914
|
-
this.#host.yieldQueue.enqueue("advisor", { note
|
|
909
|
+
this.#host.yieldQueue.enqueue("advisor", { note, severity, advisor: source });
|
|
915
910
|
return;
|
|
916
911
|
}
|
|
917
|
-
const notes: AdvisorNote[] = [{ note
|
|
912
|
+
const notes: AdvisorNote[] = [{ note, severity, advisor: source }];
|
|
918
913
|
const content = formatAdvisorBatchContent(notes);
|
|
919
914
|
const details = { notes } satisfies AdvisorMessageDetails;
|
|
920
915
|
if (channel === "preserve") {
|
|
@@ -218,7 +218,10 @@ function toolCallLine(
|
|
|
218
218
|
return base;
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
/** One line for a user-initiated `!`/`$` execution.
|
|
221
|
+
/** One line for a user-initiated `!`/`$` execution. Always attributed to the
|
|
222
|
+
* user: these roles never carry agent-run commands (the model's bash goes
|
|
223
|
+
* through `toolCall`), so the `user-` prefix makes provenance explicit for the
|
|
224
|
+
* advisor and history readers regardless of render mode. */
|
|
222
225
|
function executionLine(
|
|
223
226
|
kind: "bash" | "python",
|
|
224
227
|
source: string,
|
|
@@ -231,7 +234,7 @@ function executionLine(
|
|
|
231
234
|
: "ok";
|
|
232
235
|
const lines = lineCount(msg.output);
|
|
233
236
|
const sourcePreview = formatExecutionSourcePreview(source);
|
|
234
|
-
return `→
|
|
237
|
+
return `→ user-${kind}! ${sourcePreview} ⇒ ${status} · ${lines} ${lines === 1 ? "line" : "lines"}`;
|
|
235
238
|
}
|
|
236
239
|
|
|
237
240
|
/**
|
|
@@ -311,6 +314,18 @@ export function formatSessionHistoryMarkdown(messages: unknown[], opts?: History
|
|
|
311
314
|
// every call repeats `**agent**:`). Cleared whenever a
|
|
312
315
|
// non-role-labeled line is emitted so the next turn re-labels.
|
|
313
316
|
let lastWatchedLabel: string | undefined;
|
|
317
|
+
// Emit a watched-mode role label, collapsing consecutive same-role turns
|
|
318
|
+
// under one label (matching the user/assistant paths). Used for the
|
|
319
|
+
// user-attributed `!`/`$` execution lines so the advisor never reads them
|
|
320
|
+
// as agent actions.
|
|
321
|
+
const pushWatchedRole = (label: string, body: string): void => {
|
|
322
|
+
if (lastWatchedLabel === label) {
|
|
323
|
+
lines.push(body, "");
|
|
324
|
+
} else {
|
|
325
|
+
lines.push(label, body, "");
|
|
326
|
+
lastWatchedLabel = label;
|
|
327
|
+
}
|
|
328
|
+
};
|
|
314
329
|
|
|
315
330
|
for (const msg of typed) {
|
|
316
331
|
switch (msg.role) {
|
|
@@ -372,15 +387,25 @@ export function formatSessionHistoryMarkdown(messages: unknown[], opts?: History
|
|
|
372
387
|
case "bashExecution": {
|
|
373
388
|
const bashMsg = msg as BashExecutionMessage;
|
|
374
389
|
if (bashMsg.excludeFromContext) break;
|
|
375
|
-
|
|
376
|
-
|
|
390
|
+
const bashLine = executionLine("bash", bashMsg.command, bashMsg);
|
|
391
|
+
if (opts?.watchedRoles) {
|
|
392
|
+
pushWatchedRole("**user**:", bashLine);
|
|
393
|
+
} else {
|
|
394
|
+
lines.push(bashLine, "");
|
|
395
|
+
lastWatchedLabel = undefined;
|
|
396
|
+
}
|
|
377
397
|
break;
|
|
378
398
|
}
|
|
379
399
|
case "pythonExecution": {
|
|
380
400
|
const pythonMsg = msg as PythonExecutionMessage;
|
|
381
401
|
if (pythonMsg.excludeFromContext) break;
|
|
382
|
-
|
|
383
|
-
|
|
402
|
+
const pythonLine = executionLine("python", pythonMsg.code, pythonMsg);
|
|
403
|
+
if (opts?.watchedRoles) {
|
|
404
|
+
pushWatchedRole("**user**:", pythonLine);
|
|
405
|
+
} else {
|
|
406
|
+
lines.push(pythonLine, "");
|
|
407
|
+
lastWatchedLabel = undefined;
|
|
408
|
+
}
|
|
384
409
|
break;
|
|
385
410
|
}
|
|
386
411
|
case "custom":
|
|
@@ -2,8 +2,9 @@ import * as os from "node:os";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { Message } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import { getAgentDir as getDefaultAgentDir, logger, parseJsonlLenient, toError } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { LRUCache } from "lru-cache/raw";
|
|
5
6
|
import { computeDefaultSessionDir } from "./session-paths";
|
|
6
|
-
import { FileSessionStorage, type SessionStorage } from "./session-storage";
|
|
7
|
+
import { FileSessionStorage, type SessionStorage, type SessionStorageStat } from "./session-storage";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Coarse lifecycle status of a session, derived from its last persisted message.
|
|
@@ -65,6 +66,44 @@ const SESSION_LIST_SUFFIX_BYTES = 32_768;
|
|
|
65
66
|
const SESSION_LIST_PARALLEL_THRESHOLD = 64;
|
|
66
67
|
const SESSION_LIST_MAX_WORKERS = 16;
|
|
67
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Memoizes {@link scanSessionFile} results keyed by stat identity so listing
|
|
71
|
+
* refreshes (resume picker opens, startup recent-sessions, cross-project
|
|
72
|
+
* scans) skip the open+read+parse for unchanged files. The `statSync` still
|
|
73
|
+
* runs on every scan — it IS the invalidation check: a hit requires both
|
|
74
|
+
* `mtimeMs` and `size` to match. This covers the two mutation paths:
|
|
75
|
+
* - streaming appends grow `size` (and bump `mtimeMs`);
|
|
76
|
+
* - `updateSessionTitle` rewrites the fixed-width title slot in place via
|
|
77
|
+
* `writeSync`, which leaves `size` unchanged but updates `mtimeMs`.
|
|
78
|
+
* Negative results (unparseable files) are cached too, as `undefined` info.
|
|
79
|
+
* Entries are small header objects, so a generous cap is cheap.
|
|
80
|
+
*/
|
|
81
|
+
const SESSION_SCAN_CACHE_MAX = 4096;
|
|
82
|
+
|
|
83
|
+
interface SessionScanCacheEntry {
|
|
84
|
+
mtimeMs: number;
|
|
85
|
+
size: number;
|
|
86
|
+
info: SessionInfo | undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
type SessionScanCache = LRUCache<string, SessionScanCacheEntry>;
|
|
90
|
+
|
|
91
|
+
/** All {@link FileSessionStorage} instances view the same real filesystem, so they share one cache. */
|
|
92
|
+
const fileSessionScanCache: SessionScanCache = new LRUCache({ max: SESSION_SCAN_CACHE_MAX });
|
|
93
|
+
/** Other storages (in-memory test doubles) each carry their own cache to avoid cross-instance path collisions. */
|
|
94
|
+
const kScanCache = Symbol("session-listing.scanCache");
|
|
95
|
+
|
|
96
|
+
interface StorageWithScanCache extends SessionStorage {
|
|
97
|
+
[kScanCache]?: SessionScanCache;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getSessionScanCache(storage: SessionStorage): SessionScanCache {
|
|
101
|
+
if (storage instanceof FileSessionStorage) return fileSessionScanCache;
|
|
102
|
+
const holder = storage as StorageWithScanCache;
|
|
103
|
+
if (!holder[kScanCache]) holder[kScanCache] = new LRUCache({ max: SESSION_SCAN_CACHE_MAX });
|
|
104
|
+
return holder[kScanCache];
|
|
105
|
+
}
|
|
106
|
+
|
|
68
107
|
function sanitizeSessionName(value: string | undefined): string | undefined {
|
|
69
108
|
if (!value) return undefined;
|
|
70
109
|
const firstLine = value.split(/\r?\n/)[0] ?? "";
|
|
@@ -355,8 +394,22 @@ async function scanSessionFile(
|
|
|
355
394
|
storage: SessionStorage,
|
|
356
395
|
withStatus: boolean,
|
|
357
396
|
): Promise<SessionInfo | undefined> {
|
|
397
|
+
let stat: SessionStorageStat;
|
|
398
|
+
try {
|
|
399
|
+
stat = storage.statSync(file);
|
|
400
|
+
} catch {
|
|
401
|
+
// Missing/unstatable file: no stat identity to cache under.
|
|
402
|
+
return undefined;
|
|
403
|
+
}
|
|
404
|
+
const cache = getSessionScanCache(storage);
|
|
405
|
+
// `withStatus` changes what a scan reads (tail window) and returns, so the
|
|
406
|
+
// two variants are cached under distinct keys.
|
|
407
|
+
const cacheKey = withStatus ? `s\0${file}` : `h\0${file}`;
|
|
408
|
+
const cached = cache.get(cacheKey);
|
|
409
|
+
if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
|
|
410
|
+
return cached.info ? { ...cached.info } : undefined;
|
|
411
|
+
}
|
|
358
412
|
try {
|
|
359
|
-
const stat = storage.statSync(file);
|
|
360
413
|
const [content, suffix] = await storage.readTextSlices(
|
|
361
414
|
file,
|
|
362
415
|
SESSION_LIST_PREFIX_BYTES,
|
|
@@ -365,7 +418,12 @@ async function scanSessionFile(
|
|
|
365
418
|
const { size, mtime } = stat;
|
|
366
419
|
const entries = parseJsonlLenient<Record<string, unknown>>(content);
|
|
367
420
|
const header = parseSessionListHeader(content, entries);
|
|
368
|
-
if (!header)
|
|
421
|
+
if (!header) {
|
|
422
|
+
// Cache the negative result too: an unparseable file stays unparseable
|
|
423
|
+
// until its stat identity changes.
|
|
424
|
+
cache.set(cacheKey, { mtimeMs: stat.mtimeMs, size: stat.size, info: undefined });
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
369
427
|
|
|
370
428
|
let parsedMessageCount = 0;
|
|
371
429
|
let firstMessage = "";
|
|
@@ -398,7 +456,7 @@ async function scanSessionFile(
|
|
|
398
456
|
|
|
399
457
|
firstMessage ||= extractFirstDisplayMessageFromPrefix(content) ?? "";
|
|
400
458
|
const messageCount = Math.max(parsedMessageCount, countMessageMarkers(content));
|
|
401
|
-
|
|
459
|
+
const info: SessionInfo = {
|
|
402
460
|
path: file,
|
|
403
461
|
id: header.id,
|
|
404
462
|
cwd: header.cwd ?? "",
|
|
@@ -412,6 +470,10 @@ async function scanSessionFile(
|
|
|
412
470
|
allMessagesText: allMessages.length > 0 ? allMessages.join(" ") : firstMessage,
|
|
413
471
|
status: withStatus ? deriveSessionStatus(suffix) : undefined,
|
|
414
472
|
};
|
|
473
|
+
// The cache keeps its own shallow copy; hits also hand out copies, so
|
|
474
|
+
// callers can never mutate the shared cached object.
|
|
475
|
+
cache.set(cacheKey, { mtimeMs: stat.mtimeMs, size: stat.size, info: { ...info } });
|
|
476
|
+
return info;
|
|
415
477
|
} catch {
|
|
416
478
|
return undefined;
|
|
417
479
|
}
|
|
@@ -21,8 +21,9 @@ import { isMCPToolName, normalizeToolNames } from "../tools/builtin-names";
|
|
|
21
21
|
import { computerExposureMode } from "../tools/computer/exposure";
|
|
22
22
|
import { wrapToolWithMetaNotice } from "../tools/output-meta";
|
|
23
23
|
import { ToolAbortError, ToolError } from "../tools/tool-errors";
|
|
24
|
-
import { isMountableUnderXdev, type
|
|
24
|
+
import { isMountableUnderXdev, listXdevTools, type XdevState, xdevDocsFor, xdevEntries } from "../tools/xdev";
|
|
25
25
|
import { type EditMode, resolveEditMode } from "../utils/edit-mode";
|
|
26
|
+
import { type InspectImageMode, isInspectImageToolActive } from "../utils/inspect-image-mode";
|
|
26
27
|
import { formatLocalCalendarDate } from "../utils/local-date";
|
|
27
28
|
import {
|
|
28
29
|
extractPermissionLocations,
|
|
@@ -56,6 +57,9 @@ export interface SessionToolsHost {
|
|
|
56
57
|
emitNotice(level: "info" | "warning" | "error", message: string, source?: string): void;
|
|
57
58
|
notifyCommandMetadataChanged(): void;
|
|
58
59
|
localProtocolOptions(): LocalProtocolOptions;
|
|
60
|
+
/** Session-scoped `/vision` override; undefined means "follow the persisted setting". */
|
|
61
|
+
getInspectImageModeOverride(): InspectImageMode | undefined;
|
|
62
|
+
setInspectImageModeOverride(mode: InspectImageMode | undefined): void;
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
interface SessionToolsOptions {
|
|
@@ -63,14 +67,15 @@ interface SessionToolsOptions {
|
|
|
63
67
|
toolRegistry?: Map<string, AgentTool>;
|
|
64
68
|
createVibeTools?: () => AgentTool[];
|
|
65
69
|
createComputerTool?: () => Promise<AgentTool | null>;
|
|
70
|
+
/** Creates the built-in `inspect_image` tool for session-scoped runtime enablement (see {@link SessionTools.setInspectImageMode}). */
|
|
71
|
+
createInspectImageTool?: () => Promise<AgentTool | null>;
|
|
66
72
|
builtInToolNames?: Iterable<string>;
|
|
67
73
|
presentationPinnedToolNames?: ReadonlySet<string>;
|
|
68
74
|
ensureWriteRegistered?: () => Promise<boolean>;
|
|
69
75
|
rebuildSystemPrompt?: (toolNames: string[], tools: Map<string, AgentTool>) => Promise<{ systemPrompt: string[] }>;
|
|
70
76
|
getLocalCalendarDate?: () => string;
|
|
71
77
|
getMcpServerInstructions?: () => Map<string, string> | undefined;
|
|
72
|
-
|
|
73
|
-
initialMountedXdevToolNames?: string[];
|
|
78
|
+
xdev?: XdevState;
|
|
74
79
|
setActiveToolNames?: (names: Iterable<string>) => void;
|
|
75
80
|
baseSystemPrompt: string[];
|
|
76
81
|
skills?: Skill[];
|
|
@@ -161,11 +166,11 @@ export class SessionTools {
|
|
|
161
166
|
#toolRegistry: Map<string, AgentTool>;
|
|
162
167
|
#createVibeTools: (() => AgentTool[]) | undefined;
|
|
163
168
|
#createComputerTool: SessionToolsOptions["createComputerTool"];
|
|
169
|
+
#createInspectImageTool: SessionToolsOptions["createInspectImageTool"];
|
|
164
170
|
#installedVibeToolNames = new Set<string>();
|
|
165
171
|
#builtInToolNames: Set<string>;
|
|
166
172
|
#rpcHostToolNames = new Set<string>();
|
|
167
|
-
#
|
|
168
|
-
#mountedXdevToolNames: Set<string>;
|
|
173
|
+
#xdev: XdevState | undefined;
|
|
169
174
|
#pendingXdevMountDelta: { added: Set<string>; removed: Set<string> } | undefined;
|
|
170
175
|
#presentationPinnedToolNames: ReadonlySet<string> | undefined;
|
|
171
176
|
#runtimeSelectedToolNames: ReadonlySet<string> | undefined;
|
|
@@ -190,14 +195,18 @@ export class SessionTools {
|
|
|
190
195
|
this.#toolRegistry = options.toolRegistry ?? new Map();
|
|
191
196
|
this.#createVibeTools = options.createVibeTools;
|
|
192
197
|
this.#createComputerTool = options.createComputerTool;
|
|
198
|
+
this.#createInspectImageTool = options.createInspectImageTool;
|
|
193
199
|
this.#builtInToolNames = new Set(options.builtInToolNames ?? []);
|
|
194
200
|
this.#presentationPinnedToolNames = options.presentationPinnedToolNames;
|
|
195
201
|
this.#ensureWriteRegistered = options.ensureWriteRegistered;
|
|
196
202
|
this.#rebuildSystemPrompt = options.rebuildSystemPrompt;
|
|
197
203
|
this.#getLocalCalendarDate = options.getLocalCalendarDate ?? formatLocalCalendarDate;
|
|
198
204
|
this.#getMcpServerInstructions = options.getMcpServerInstructions;
|
|
199
|
-
this.#
|
|
200
|
-
this.#
|
|
205
|
+
this.#xdev = options.xdev;
|
|
206
|
+
if (this.#xdev && this.#xdev.tools !== this.#toolRegistry) {
|
|
207
|
+
throw new Error("xd:// state must reference the canonical session tool map");
|
|
208
|
+
}
|
|
209
|
+
if (this.#xdev) this.#xdev.decorateExecution = tool => this.#wrapToolForAcpPermission(tool);
|
|
201
210
|
this.#setActiveToolNames = options.setActiveToolNames;
|
|
202
211
|
this.#baseSystemPrompt = options.baseSystemPrompt;
|
|
203
212
|
this.#skills = options.skills ?? [];
|
|
@@ -242,7 +251,7 @@ export class SessionTools {
|
|
|
242
251
|
this.#acpPermissionDecisions.clear();
|
|
243
252
|
}
|
|
244
253
|
|
|
245
|
-
/**
|
|
254
|
+
/** Drops cached ACP decisions and re-wraps active tools after the client changes. */
|
|
246
255
|
refreshAcpPermissionGates(): void {
|
|
247
256
|
this.#acpPermissionDecisions.clear();
|
|
248
257
|
const activeTools = this.getActiveToolNames()
|
|
@@ -250,11 +259,6 @@ export class SessionTools {
|
|
|
250
259
|
.filter((tool): tool is AgentTool => tool !== undefined)
|
|
251
260
|
.map(tool => this.#wrapToolForAcpPermission(tool));
|
|
252
261
|
this.#host.agent.setTools(activeTools);
|
|
253
|
-
const mountedTools = [...this.#mountedXdevToolNames]
|
|
254
|
-
.map(name => this.#toolRegistry.get(name))
|
|
255
|
-
.filter((tool): tool is AgentTool => tool !== undefined)
|
|
256
|
-
.map(tool => this.#wrapToolForAcpPermission(tool));
|
|
257
|
-
this.#xdevRegistry?.reconcile(mountedTools);
|
|
258
262
|
}
|
|
259
263
|
|
|
260
264
|
#getActiveNonMCPToolNames(): string[] {
|
|
@@ -268,13 +272,14 @@ export class SessionTools {
|
|
|
268
272
|
|
|
269
273
|
/** Enabled top-level and discoverable tool names. */
|
|
270
274
|
getEnabledToolNames(): string[] {
|
|
271
|
-
|
|
272
|
-
return
|
|
275
|
+
const mountedNames = this.#xdev?.mountedNames;
|
|
276
|
+
if (!mountedNames || mountedNames.size === 0) return this.getActiveToolNames();
|
|
277
|
+
return [...this.getActiveToolNames(), ...mountedNames];
|
|
273
278
|
}
|
|
274
279
|
|
|
275
|
-
/** Names
|
|
280
|
+
/** Names currently presented as `xd://` devices. */
|
|
276
281
|
getMountedXdevToolNames(): string[] {
|
|
277
|
-
return [...this.#
|
|
282
|
+
return [...(this.#xdev?.mountedNames ?? [])];
|
|
278
283
|
}
|
|
279
284
|
|
|
280
285
|
/** Whether the edit tool is registered. */
|
|
@@ -403,6 +408,10 @@ export class SessionTools {
|
|
|
403
408
|
} else if (computerExpected) {
|
|
404
409
|
this.#logComputerState("Computer tool retained after model change", true);
|
|
405
410
|
}
|
|
411
|
+
|
|
412
|
+
// inspect_image auto mode keys off model image capability, so a model
|
|
413
|
+
// switch can flip the tool either way.
|
|
414
|
+
await this.reconcileInspectImageAfterModelChange();
|
|
406
415
|
}
|
|
407
416
|
|
|
408
417
|
/** Enabled MCP tools in their current presentation partition. */
|
|
@@ -543,10 +552,7 @@ export class SessionTools {
|
|
|
543
552
|
this.#presentationPinnedToolNames?.has(name) === true || this.#runtimeSelectedToolNames?.has(name) === true;
|
|
544
553
|
const mountCandidates = selectedTools.filter(
|
|
545
554
|
({ name, tool }) =>
|
|
546
|
-
this.#
|
|
547
|
-
xdevReadAvailable &&
|
|
548
|
-
!isPresentationPinned(name) &&
|
|
549
|
-
isMountableUnderXdev(tool),
|
|
555
|
+
this.#xdev !== undefined && xdevReadAvailable && !isPresentationPinned(name) && isMountableUnderXdev(tool),
|
|
550
556
|
);
|
|
551
557
|
|
|
552
558
|
let builtInWriteAvailable = this.#builtInToolNames.has("write");
|
|
@@ -557,19 +563,15 @@ export class SessionTools {
|
|
|
557
563
|
const mountNames = builtInWriteAvailable ? new Set(mountCandidates.map(({ name }) => name)) : new Set<string>();
|
|
558
564
|
const tools: AgentTool[] = [];
|
|
559
565
|
const validToolNames: string[] = [];
|
|
560
|
-
const mountedTools: AgentTool[] = [];
|
|
561
566
|
for (const { name, tool } of selectedTools) {
|
|
562
|
-
if (mountNames.has(name))
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
tools.push(this.#wrapToolForAcpPermission(tool));
|
|
566
|
-
validToolNames.push(name);
|
|
567
|
-
}
|
|
567
|
+
if (mountNames.has(name)) continue;
|
|
568
|
+
tools.push(this.#wrapToolForAcpPermission(tool));
|
|
569
|
+
validToolNames.push(name);
|
|
568
570
|
}
|
|
569
571
|
|
|
570
572
|
const pinnedWrite = isPresentationPinned("write");
|
|
571
573
|
const activeDeferrableTool = tools.some(tool => tool.deferrable === true);
|
|
572
|
-
const transportNeeded =
|
|
574
|
+
const transportNeeded = mountNames.size > 0 || activeDeferrableTool || this.#host.planModeEnabled();
|
|
573
575
|
if (transportNeeded && !builtInWriteAvailable) {
|
|
574
576
|
builtInWriteAvailable = (await this.#ensureWriteRegistered?.()) === true;
|
|
575
577
|
if (builtInWriteAvailable) this.#builtInToolNames.add("write");
|
|
@@ -590,14 +592,9 @@ export class SessionTools {
|
|
|
590
592
|
if (writeToolIndex >= 0) tools.splice(writeToolIndex, 1);
|
|
591
593
|
}
|
|
592
594
|
|
|
593
|
-
const previousMounted = this.#
|
|
594
|
-
const previousMountedTools = [...previousMounted].flatMap(name => {
|
|
595
|
-
const tool = this.#xdevRegistry?.get(name);
|
|
596
|
-
return tool ? [tool] : [];
|
|
597
|
-
});
|
|
595
|
+
const previousMounted = new Set(this.#xdev?.mountedNames ?? []);
|
|
598
596
|
const previousActiveToolNames = this.getActiveToolNames();
|
|
599
|
-
this.#
|
|
600
|
-
this.#xdevRegistry?.reconcile(mountedTools);
|
|
597
|
+
this.#setMountedNames(mountNames);
|
|
601
598
|
this.#setActiveToolNames?.(validToolNames);
|
|
602
599
|
|
|
603
600
|
let rebuiltSystemPrompt: string[] | undefined;
|
|
@@ -612,15 +609,13 @@ export class SessionTools {
|
|
|
612
609
|
}
|
|
613
610
|
}
|
|
614
611
|
} catch (error) {
|
|
615
|
-
this.#
|
|
616
|
-
this.#xdevRegistry?.reconcile(previousMountedTools);
|
|
612
|
+
this.#setMountedNames(previousMounted);
|
|
617
613
|
this.#setActiveToolNames?.(previousActiveToolNames);
|
|
618
614
|
throw error;
|
|
619
615
|
}
|
|
620
616
|
|
|
621
617
|
if (this.#host.isDisposed()) {
|
|
622
|
-
this.#
|
|
623
|
-
this.#xdevRegistry?.reconcile(previousMountedTools);
|
|
618
|
+
this.#setMountedNames(previousMounted);
|
|
624
619
|
this.#setActiveToolNames?.(previousActiveToolNames);
|
|
625
620
|
return;
|
|
626
621
|
}
|
|
@@ -637,6 +632,13 @@ export class SessionTools {
|
|
|
637
632
|
}
|
|
638
633
|
}
|
|
639
634
|
|
|
635
|
+
#setMountedNames(names: Iterable<string>): void {
|
|
636
|
+
const mountedNames = this.#xdev?.mountedNames;
|
|
637
|
+
if (!mountedNames) return;
|
|
638
|
+
mountedNames.clear();
|
|
639
|
+
for (const name of names) mountedNames.add(name);
|
|
640
|
+
}
|
|
641
|
+
|
|
640
642
|
/**
|
|
641
643
|
* Record a mid-session `xd://` mount delta for the model. Non-MCP mount
|
|
642
644
|
* churn remains notice-only, leaving the system prompt and provider cache
|
|
@@ -649,9 +651,8 @@ export class SessionTools {
|
|
|
649
651
|
* Full docs join the system prompt opportunistically on a rebuild.
|
|
650
652
|
*/
|
|
651
653
|
#notifyXdevMountDelta(previousMounted: ReadonlySet<string>): void {
|
|
652
|
-
const
|
|
653
|
-
if (!
|
|
654
|
-
const current = this.#mountedXdevToolNames;
|
|
654
|
+
const current = this.#xdev?.mountedNames;
|
|
655
|
+
if (!current) return;
|
|
655
656
|
const addedNames = [...current].filter(name => !previousMounted.has(name));
|
|
656
657
|
const removedNames = [...previousMounted].filter(name => !current.has(name));
|
|
657
658
|
if (addedNames.length === 0 && removedNames.length === 0) return;
|
|
@@ -678,14 +679,17 @@ export class SessionTools {
|
|
|
678
679
|
const pending = this.#pendingXdevMountDelta;
|
|
679
680
|
if (!pending) return undefined;
|
|
680
681
|
this.#pendingXdevMountDelta = undefined;
|
|
681
|
-
const summaries = new Map(this.#
|
|
682
|
+
const summaries = new Map(this.#xdev ? xdevEntries(this.#xdev).map(entry => [entry.name, entry.summary]) : []);
|
|
682
683
|
const added = [...pending.added].map(name => ({ name, summary: summaries.get(name) ?? "" }));
|
|
683
684
|
const removed = [...pending.removed].map(name => ({ name }));
|
|
684
|
-
const docs = this.#
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
685
|
+
const docs = this.#xdev
|
|
686
|
+
? xdevDocsFor(
|
|
687
|
+
this.#xdev,
|
|
688
|
+
pending.added,
|
|
689
|
+
this.#host.settings.get("tools.xdevDocs"),
|
|
690
|
+
this.#host.settings.get("tools.xdevInlineDevices"),
|
|
691
|
+
)
|
|
692
|
+
: "";
|
|
689
693
|
return {
|
|
690
694
|
role: "custom",
|
|
691
695
|
customType: XDEV_MOUNT_NOTICE_MESSAGE_TYPE,
|
|
@@ -727,7 +731,7 @@ export class SessionTools {
|
|
|
727
731
|
// selection change should not demote `write` unless it is already active.
|
|
728
732
|
await this.#applyToolPresentation(
|
|
729
733
|
normalized,
|
|
730
|
-
this.#
|
|
734
|
+
this.#xdev?.mountedNames ?? new Set(),
|
|
731
735
|
this.getActiveToolNames().includes("write"),
|
|
732
736
|
);
|
|
733
737
|
}
|
|
@@ -736,7 +740,7 @@ export class SessionTools {
|
|
|
736
740
|
* Restore an enabled tool set with its exact top-level versus `xd://` partition.
|
|
737
741
|
*
|
|
738
742
|
* Both inputs are required because {@link setActiveToolsByName} only receives the
|
|
739
|
-
* enabled name list and classifies mounts from the current
|
|
743
|
+
* enabled name list and classifies mounts from the current presentation set.
|
|
740
744
|
* Rollback/restore callers must pass the snapshotted mounted subset so names that
|
|
741
745
|
* were top-level stay pinned (`#runtimeSelectedToolNames`) and names that were under
|
|
742
746
|
* `xd://` remain mount-eligible, even when the live mount set has drifted.
|
|
@@ -849,6 +853,108 @@ export class SessionTools {
|
|
|
849
853
|
return true;
|
|
850
854
|
}
|
|
851
855
|
|
|
856
|
+
/** Current effective inspect_image state for `/vision status`. */
|
|
857
|
+
inspectImageState(): { mode: InspectImageMode; active: boolean; model: string | undefined } {
|
|
858
|
+
const model = this.#host.model();
|
|
859
|
+
return {
|
|
860
|
+
mode: this.#host.getInspectImageModeOverride() ?? this.#host.settings.get("inspect_image.mode"),
|
|
861
|
+
active: this.getEnabledToolNames().includes("inspect_image"),
|
|
862
|
+
model: model ? formatModelString(model) : undefined,
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Brings the active tool set in line with the effective inspect_image state
|
|
868
|
+
* (mode setting, `/vision` override, active-model image capability).
|
|
869
|
+
* Mirrors {@link setComputerToolEnabled}: enabling builds the tool through
|
|
870
|
+
* the config factory on first use and reuses the registry entry afterwards.
|
|
871
|
+
* Idempotent — safe to call from every model/settings change path.
|
|
872
|
+
*
|
|
873
|
+
* @returns false when the tool should be active but this session cannot
|
|
874
|
+
* build it (e.g. restricted child sessions have no factory).
|
|
875
|
+
*/
|
|
876
|
+
async reconcileInspectImageTool(): Promise<boolean> {
|
|
877
|
+
const expected = isInspectImageToolActive({
|
|
878
|
+
settings: this.#host.settings,
|
|
879
|
+
getActiveModel: () => this.#host.model(),
|
|
880
|
+
getInspectImageModeOverride: () => this.#host.getInspectImageModeOverride(),
|
|
881
|
+
});
|
|
882
|
+
// Keep the read tool's advertised description in sync BEFORE any prompt
|
|
883
|
+
// rebuild below, passing the post-change availability so the prompt never
|
|
884
|
+
// lags a flip in either direction. Per-read lazy sync is the backstop.
|
|
885
|
+
const syncReadDescription = (available: boolean): void => {
|
|
886
|
+
const readTool = this.#toolRegistry.get("read") as
|
|
887
|
+
| { syncInspectImageState?: (available?: boolean) => boolean }
|
|
888
|
+
| undefined;
|
|
889
|
+
readTool?.syncInspectImageState?.(available);
|
|
890
|
+
};
|
|
891
|
+
const active = this.getEnabledToolNames();
|
|
892
|
+
const isActive = active.includes("inspect_image");
|
|
893
|
+
if (expected === isActive) {
|
|
894
|
+
syncReadDescription(isActive);
|
|
895
|
+
return true;
|
|
896
|
+
}
|
|
897
|
+
if (!expected) {
|
|
898
|
+
syncReadDescription(false);
|
|
899
|
+
await this.applyActiveToolsByName(active.filter(name => name !== "inspect_image"));
|
|
900
|
+
return true;
|
|
901
|
+
}
|
|
902
|
+
if (!this.#toolRegistry.has("inspect_image")) {
|
|
903
|
+
const tool = await this.#createInspectImageTool?.();
|
|
904
|
+
if (tool?.name !== "inspect_image") {
|
|
905
|
+
logger.warn("inspect_image tool could not be created", {
|
|
906
|
+
model: this.#host.model()?.id,
|
|
907
|
+
});
|
|
908
|
+
syncReadDescription(false);
|
|
909
|
+
return false;
|
|
910
|
+
}
|
|
911
|
+
const wrapped = this.#wrapRuntimeTool(tool);
|
|
912
|
+
this.#toolRegistry.set(wrapped.name, wrapped);
|
|
913
|
+
this.#builtInToolNames.add(wrapped.name);
|
|
914
|
+
}
|
|
915
|
+
syncReadDescription(true);
|
|
916
|
+
await this.applyActiveToolsByName([...active, "inspect_image"]);
|
|
917
|
+
return true;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Reconciles inspect_image after a model change and surfaces a notice when
|
|
922
|
+
* the visible tool set actually flipped. Called from every model-change
|
|
923
|
+
* path — including retry-fallback switches that bypass
|
|
924
|
+
* {@link syncAfterModelChange}.
|
|
925
|
+
*/
|
|
926
|
+
async reconcileInspectImageAfterModelChange(): Promise<void> {
|
|
927
|
+
const before = this.getEnabledToolNames().includes("inspect_image");
|
|
928
|
+
const reconciled = await this.reconcileInspectImageTool();
|
|
929
|
+
const after = this.getEnabledToolNames().includes("inspect_image");
|
|
930
|
+
if (!reconciled || before === after) return;
|
|
931
|
+
const model = this.#host.model();
|
|
932
|
+
const modelName = model ? formatModelString(model) : "the current model";
|
|
933
|
+
this.#host.emitNotice(
|
|
934
|
+
"info",
|
|
935
|
+
after
|
|
936
|
+
? `inspect_image is now available: ${modelName} has no native image input.`
|
|
937
|
+
: `inspect_image is now hidden: ${modelName} supports image input natively. Override with /vision on.`,
|
|
938
|
+
"vision",
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Session-scoped `/vision` override. `auto` clears the override so the
|
|
944
|
+
* persisted `inspect_image.mode` setting (itself possibly `auto`) decides;
|
|
945
|
+
* `on`/`off` force the tool for this session only. Takes effect before the
|
|
946
|
+
* next model call.
|
|
947
|
+
*
|
|
948
|
+
* @returns false when `on` was requested but the tool cannot be built here.
|
|
949
|
+
*/
|
|
950
|
+
async setInspectImageMode(mode: InspectImageMode): Promise<boolean> {
|
|
951
|
+
this.#host.setInspectImageModeOverride(mode === "auto" ? undefined : mode);
|
|
952
|
+
const applied = await this.reconcileInspectImageTool();
|
|
953
|
+
const { active, model } = this.inspectImageState();
|
|
954
|
+
logger.debug("inspect_image mode changed", { mode, active, model });
|
|
955
|
+
return applied;
|
|
956
|
+
}
|
|
957
|
+
|
|
852
958
|
/** Rebuilds the stable base prompt for the current tools and model. */
|
|
853
959
|
async refreshBaseSystemPrompt(): Promise<void> {
|
|
854
960
|
if (this.#host.isDisposed() || !this.#rebuildSystemPrompt) return;
|
|
@@ -964,7 +1070,7 @@ export class SessionTools {
|
|
|
964
1070
|
`${tool.name}=${tool.label ?? ""}|${tool.description ?? ""}|${tool.customWireName ?? ""}`;
|
|
965
1071
|
const descriptionSegment = tools.map(describeTool).join("\u0002");
|
|
966
1072
|
const mountedMCPProjection = projectMountedMCPXdevGuidance(
|
|
967
|
-
collectMountedMCPToolRoutes(this.#
|
|
1073
|
+
collectMountedMCPToolRoutes(this.#xdev ? listXdevTools(this.#xdev) : []),
|
|
968
1074
|
);
|
|
969
1075
|
const mountedMCPRouteSegment =
|
|
970
1076
|
JSON.stringify({
|