@oh-my-pi/pi-coding-agent 17.1.5 → 17.1.7
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 +54 -0
- package/dist/{CHANGELOG-qs3vd6xf.md → CHANGELOG-5k4dq4g6.md} +54 -0
- package/dist/cli.js +3342 -3274
- package/dist/types/capability/index.d.ts +1 -1
- package/dist/types/capability/types.d.ts +23 -1
- package/dist/types/config/settings-schema.d.ts +39 -2
- package/dist/types/cursor.d.ts +2 -1
- package/dist/types/extensibility/extensions/runner.d.ts +4 -0
- package/dist/types/extensibility/shared-events.d.ts +18 -1
- package/dist/types/internal-urls/mcp-protocol.d.ts +3 -2
- package/dist/types/internal-urls/parse.d.ts +12 -0
- package/dist/types/internal-urls/router.d.ts +6 -0
- package/dist/types/internal-urls/types.d.ts +6 -0
- package/dist/types/lsp/config.d.ts +1 -0
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/mcp/manager.d.ts +5 -0
- package/dist/types/mcp/tool-bridge.d.ts +13 -0
- package/dist/types/modes/components/custom-editor.d.ts +5 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +2 -0
- package/dist/types/sdk.d.ts +3 -1
- package/dist/types/session/agent-session-types.d.ts +8 -6
- package/dist/types/session/agent-session.d.ts +28 -1
- package/dist/types/session/model-controls.d.ts +4 -1
- package/dist/types/session/session-advisors.d.ts +7 -1
- package/dist/types/session/session-tools.d.ts +44 -6
- package/dist/types/session/streaming-output.d.ts +7 -2
- package/dist/types/session/tool-choice-queue.d.ts +6 -4
- package/dist/types/session/turn-recovery.d.ts +5 -3
- package/dist/types/task/index.d.ts +1 -1
- package/dist/types/task/types.d.ts +3 -11
- package/dist/types/thinking.d.ts +21 -2
- 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/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/dist/types/utils/title-generator.d.ts +17 -16
- package/package.json +12 -12
- package/src/capability/index.ts +43 -12
- package/src/capability/mcp.ts +21 -0
- package/src/capability/types.ts +20 -1
- package/src/cli/read-cli.ts +44 -2
- package/src/config/settings-schema.ts +42 -2
- 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/eval/py/runner.py +16 -2
- package/src/extensibility/extensions/runner.ts +117 -5
- package/src/extensibility/extensions/types.ts +0 -1
- package/src/extensibility/extensions/wrapper.ts +74 -42
- package/src/extensibility/hooks/tool-wrapper.ts +11 -4
- package/src/extensibility/hooks/types.ts +0 -1
- package/src/extensibility/shared-events.ts +18 -1
- package/src/internal-urls/mcp-protocol.ts +17 -3
- package/src/internal-urls/parse.ts +31 -0
- package/src/internal-urls/router.ts +24 -4
- package/src/internal-urls/types.ts +6 -0
- package/src/live/transport.ts +2 -2
- package/src/lsp/client.ts +2 -2
- package/src/lsp/config.ts +4 -0
- package/src/lsp/types.ts +2 -0
- package/src/mcp/config.ts +26 -14
- package/src/mcp/manager.ts +26 -9
- package/src/mcp/tool-bridge.ts +52 -1
- package/src/memories/index.ts +25 -6
- package/src/modes/components/custom-editor.ts +39 -16
- package/src/modes/components/status-line/segments.ts +3 -1
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/tool-execution.ts +8 -7
- package/src/modes/controllers/command-controller.ts +10 -10
- package/src/modes/controllers/extension-ui-controller.ts +7 -7
- package/src/modes/controllers/selector-controller.ts +7 -2
- package/src/modes/rpc/rpc-mode.ts +60 -47
- package/src/prompts/steering/user-interjection.md +2 -5
- package/src/prompts/tools/task.md +4 -2
- package/src/sdk.ts +64 -58
- package/src/session/agent-session-types.ts +8 -5
- package/src/session/agent-session.ts +144 -11
- package/src/session/model-controls.ts +48 -12
- package/src/session/session-advisors.ts +30 -14
- package/src/session/session-listing.ts +66 -4
- package/src/session/session-tools.ts +162 -54
- package/src/session/streaming-output.ts +18 -6
- package/src/session/tool-choice-queue.ts +19 -4
- package/src/session/turn-recovery.ts +25 -6
- package/src/slash-commands/builtin-registry.ts +69 -0
- package/src/task/executor.ts +11 -3
- package/src/task/index.ts +43 -32
- package/src/task/types.ts +12 -17
- package/src/thinking.ts +68 -5
- package/src/tools/bash.ts +16 -9
- package/src/tools/index.ts +36 -16
- package/src/tools/output-meta.ts +20 -0
- package/src/tools/read.ts +89 -15
- package/src/tools/write.ts +16 -7
- package/src/tools/xdev.ts +198 -210
- 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/title-generator.ts +88 -34
- package/dist/types/advisor/__tests__/advisor.test.d.ts +0 -1
- package/dist/types/advisor/__tests__/config.test.d.ts +0 -1
- package/dist/types/advisor/__tests__/emission-guard.test.d.ts +0 -1
- package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +0 -1
- package/dist/types/cli/update-cli.test.d.ts +0 -1
- package/dist/types/config/__tests__/model-registry.test.d.ts +0 -1
- package/dist/types/eval/__tests__/agent-bridge.test.d.ts +0 -1
- package/dist/types/eval/__tests__/bridge-timeout.test.d.ts +0 -1
- package/dist/types/eval/__tests__/budget-bridge.test.d.ts +0 -1
- package/dist/types/eval/__tests__/completion-bridge.test.d.ts +0 -1
- package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +0 -1
- package/dist/types/eval/__tests__/idle-timeout.test.d.ts +0 -1
- package/dist/types/eval/__tests__/js-context-manager.test.d.ts +0 -1
- package/dist/types/eval/__tests__/julia-prelude.test.d.ts +0 -1
- package/dist/types/eval/__tests__/kernel-spawn.test.d.ts +0 -1
- package/dist/types/eval/__tests__/prelude-agent.test.d.ts +0 -1
- package/dist/types/eval/__tests__/process-entry-import.test.d.ts +0 -1
- package/dist/types/eval/py/__tests__/prelude.test.d.ts +0 -1
- package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +0 -1
- package/dist/types/hindsight/client.test.d.ts +0 -1
- package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +0 -1
- package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +0 -1
- package/dist/types/launch/broker-list-order.test.d.ts +0 -1
- package/dist/types/launch/broker-output-snapshot.test.d.ts +0 -1
- package/dist/types/launch/protocol.test.d.ts +0 -1
- package/dist/types/launch/spawn-options.test.d.ts +0 -1
- package/dist/types/launch/terminal-output.test.d.ts +0 -1
- package/dist/types/live/protocol.test.d.ts +0 -1
- package/dist/types/mcp/config-writer.test.d.ts +0 -1
- package/dist/types/mcp/smithery-auth.test.d.ts +0 -1
- package/dist/types/mcp/smithery-registry.test.d.ts +0 -1
- package/dist/types/mcp/transports/stdio.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/skill-message.test.d.ts +0 -1
- package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +0 -1
- package/dist/types/modes/components/custom-editor.test.d.ts +0 -1
- package/dist/types/modes/components/login-dialog.test.d.ts +0 -1
- package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +0 -1
- package/dist/types/modes/components/status-line/component.test.d.ts +0 -1
- package/dist/types/modes/components/tool-execution.test.d.ts +0 -1
- package/dist/types/modes/controllers/extension-ui-controller.test.d.ts +0 -1
- package/dist/types/modes/noninteractive-dispose.test.d.ts +0 -1
- package/dist/types/modes/print-mode.test.d.ts +0 -1
- package/dist/types/modes/session-teardown.test.d.ts +0 -1
- package/dist/types/modes/theme/mermaid-rendering.test.d.ts +0 -1
- package/dist/types/modes/utils/transcript-render-helpers.test.d.ts +0 -1
- package/dist/types/modes/warp-events.test.d.ts +0 -1
- package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +0 -1
- package/dist/types/plan-mode/model-transition.test.d.ts +0 -1
- package/dist/types/plan-mode/reentry-prompt.test.d.ts +0 -1
- package/dist/types/session/agent-session-error-log.test.d.ts +0 -1
- package/dist/types/session/blob-store.test.d.ts +0 -1
- package/dist/types/session/messages.test.d.ts +0 -1
- package/dist/types/session/session-context.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +0 -1
- package/dist/types/system-prompt.test.d.ts +0 -1
- package/dist/types/task/render.test.d.ts +0 -1
- package/dist/types/task/spawn-policy.test.d.ts +0 -1
- package/dist/types/tools/__tests__/eval-description.test.d.ts +0 -1
- package/dist/types/tools/__tests__/glob.test.d.ts +0 -1
- package/dist/types/tools/__tests__/json-tree.test.d.ts +0 -1
- package/dist/types/tools/__tests__/vibe-render.test.d.ts +0 -1
- package/dist/types/tools/hub/launch-compat.test.d.ts +0 -1
- package/dist/types/vibe/__tests__/token-rate.test.d.ts +0 -1
- package/src/advisor/__tests__/advisor.test.ts +0 -4889
- package/src/advisor/__tests__/config.test.ts +0 -349
- package/src/advisor/__tests__/emission-guard.test.ts +0 -147
- package/src/cli/__tests__/auth-gateway-catalog.test.ts +0 -111
- package/src/cli/update-cli.test.ts +0 -28
- package/src/config/__tests__/model-registry.test.ts +0 -182
- package/src/eval/__tests__/agent-bridge.test.ts +0 -1509
- package/src/eval/__tests__/bridge-timeout.test.ts +0 -170
- package/src/eval/__tests__/budget-bridge.test.ts +0 -80
- package/src/eval/__tests__/completion-bridge.test.ts +0 -412
- package/src/eval/__tests__/helpers-local-roots.test.ts +0 -55
- package/src/eval/__tests__/idle-timeout.test.ts +0 -80
- package/src/eval/__tests__/js-context-manager.test.ts +0 -456
- package/src/eval/__tests__/julia-prelude.test.ts +0 -66
- package/src/eval/__tests__/kernel-spawn.test.ts +0 -115
- package/src/eval/__tests__/prelude-agent.test.ts +0 -156
- package/src/eval/__tests__/process-entry-import.test.ts +0 -137
- package/src/eval/py/__tests__/prelude.test.ts +0 -104
- package/src/eval/py/__tests__/runner-shell-output.test.ts +0 -157
- package/src/hindsight/client.test.ts +0 -75
- package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +0 -141
- package/src/internal-urls/__tests__/ssh-protocol.test.ts +0 -331
- package/src/launch/broker-list-order.test.ts +0 -89
- package/src/launch/broker-output-snapshot.test.ts +0 -126
- package/src/launch/protocol.test.ts +0 -59
- package/src/launch/spawn-options.test.ts +0 -31
- package/src/launch/terminal-output.test.ts +0 -107
- package/src/live/protocol.test.ts +0 -140
- package/src/mcp/config-writer.test.ts +0 -43
- package/src/mcp/smithery-auth.test.ts +0 -29
- package/src/mcp/smithery-registry.test.ts +0 -51
- package/src/mcp/transports/stdio.test.ts +0 -427
- package/src/modes/components/__tests__/dynamic-border.test.ts +0 -55
- package/src/modes/components/__tests__/move-overlay.test.ts +0 -252
- package/src/modes/components/__tests__/pause-screen.test.ts +0 -143
- package/src/modes/components/__tests__/skill-message.test.ts +0 -94
- package/src/modes/components/custom-editor-plugin-ctor.test.ts +0 -36
- package/src/modes/components/custom-editor.test.ts +0 -510
- package/src/modes/components/login-dialog.test.ts +0 -56
- package/src/modes/components/status-line/component.jj-cache.test.ts +0 -229
- package/src/modes/components/status-line/component.test.ts +0 -84
- package/src/modes/components/tool-execution.test.ts +0 -162
- package/src/modes/controllers/extension-ui-controller.test.ts +0 -250
- package/src/modes/noninteractive-dispose.test.ts +0 -73
- package/src/modes/print-mode.test.ts +0 -71
- package/src/modes/session-teardown.test.ts +0 -219
- package/src/modes/theme/mermaid-rendering.test.ts +0 -53
- package/src/modes/utils/transcript-render-helpers.test.ts +0 -38
- package/src/modes/warp-events.test.ts +0 -794
- package/src/plan-mode/approved-plan-prompt.test.ts +0 -36
- package/src/plan-mode/model-transition.test.ts +0 -60
- package/src/plan-mode/reentry-prompt.test.ts +0 -41
- package/src/session/agent-session-error-log.test.ts +0 -59
- package/src/session/blob-store.test.ts +0 -56
- package/src/session/messages.test.ts +0 -282
- package/src/session/session-context.test.ts +0 -384
- package/src/ssh/__tests__/connection-manager-args.test.ts +0 -191
- package/src/ssh/__tests__/connection-manager-timeout.test.ts +0 -61
- package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +0 -105
- package/src/ssh/__tests__/sshfs-mount.test.ts +0 -13
- package/src/system-prompt.test.ts +0 -236
- package/src/task/render.test.ts +0 -290
- package/src/task/spawn-policy.test.ts +0 -62
- package/src/tools/__tests__/eval-description.test.ts +0 -18
- package/src/tools/__tests__/glob.test.ts +0 -37
- package/src/tools/__tests__/json-tree.test.ts +0 -35
- package/src/tools/__tests__/vibe-render.test.ts +0 -210
- package/src/tools/hub/launch-compat.test.ts +0 -40
- package/src/vibe/__tests__/token-rate.test.ts +0 -96
|
@@ -254,6 +254,7 @@ export class SessionAdvisors {
|
|
|
254
254
|
#advisorConfigs: AdvisorConfig[] | undefined;
|
|
255
255
|
#advisorStatuses = new Map<string, { name: string; status: AdvisorRuntimeStatus }>();
|
|
256
256
|
#advisorProviderSessionIds = new Map<string, string>();
|
|
257
|
+
#advisorCosts = new Map<string, number>();
|
|
257
258
|
#advisorRecorderClosed: Promise<void> = Promise.resolve();
|
|
258
259
|
#advisorAutoResumeSuppressed = false;
|
|
259
260
|
#preserveAdvisorAdvice = false;
|
|
@@ -326,8 +327,13 @@ export class SessionAdvisors {
|
|
|
326
327
|
}
|
|
327
328
|
|
|
328
329
|
/** Re-primes advisor transcript views across a conversation boundary. */
|
|
329
|
-
resetSessionState(): void {
|
|
330
|
-
this.#resetAdvisorSessionState();
|
|
330
|
+
resetSessionState(options: { preserveCost?: boolean } = {}): void {
|
|
331
|
+
this.#resetAdvisorSessionState(options.preserveCost === true);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Drop the recorded spend once a conversation boundary has committed. */
|
|
335
|
+
clearCost(): void {
|
|
336
|
+
this.#advisorCosts.clear();
|
|
331
337
|
}
|
|
332
338
|
|
|
333
339
|
/**
|
|
@@ -451,7 +457,8 @@ export class SessionAdvisors {
|
|
|
451
457
|
* agent steer/follow-up queues, and preserved cards deferred to the next turn —
|
|
452
458
|
* so none of them inject into the new conversation.
|
|
453
459
|
*/
|
|
454
|
-
#resetAdvisorSessionState(): void {
|
|
460
|
+
#resetAdvisorSessionState(preserveCost: boolean): void {
|
|
461
|
+
if (!preserveCost) this.#advisorCosts.clear();
|
|
455
462
|
// Mute the recorder across the re-prime: AdvisorRuntime.reset() aborts the advisor
|
|
456
463
|
// loop, and that abort can emit an `aborted` message_end we must not attribute to
|
|
457
464
|
// either session's transcript. Detach, reset, then re-attach the live agent's feed.
|
|
@@ -959,8 +966,7 @@ export class SessionAdvisors {
|
|
|
959
966
|
.catch(err => logger.debug("advisor delivery failed", { err: String(err) }));
|
|
960
967
|
}
|
|
961
968
|
|
|
962
|
-
/** Re-prime every advisor's transcript view
|
|
963
|
-
* session-level latch reset {@link #resetAdvisorSessionState} performs. */
|
|
969
|
+
/** Re-prime every advisor's transcript view after an in-conversation history rewrite. */
|
|
964
970
|
#resetAllAdvisorRuntimes(): void {
|
|
965
971
|
for (const a of this.#advisors) a.runtime.reset();
|
|
966
972
|
}
|
|
@@ -985,12 +991,18 @@ export class SessionAdvisors {
|
|
|
985
991
|
this.#advisorYieldQueueUnsubscribe = undefined;
|
|
986
992
|
}
|
|
987
993
|
|
|
994
|
+
#recordAdvisorCost(advisor: ActiveAdvisor, message: AssistantMessage): void {
|
|
995
|
+
this.#advisorCosts.set(advisor.slug, (this.#advisorCosts.get(advisor.slug) ?? 0) + message.usage.cost.total);
|
|
996
|
+
}
|
|
997
|
+
|
|
988
998
|
/** Subscribe the advisor agent's finalized messages into the transcript recorder.
|
|
989
999
|
* Idempotent-by-replacement: callers detach the prior feed first. Kept separate
|
|
990
1000
|
* so the re-prime path can mute the feed across an abort-driven reset. */
|
|
991
1001
|
#attachAdvisorRecorderFeed(advisor: ActiveAdvisor): void {
|
|
992
1002
|
advisor.agentUnsubscribe = advisor.agent.subscribe(event => {
|
|
993
|
-
if (event.type
|
|
1003
|
+
if (event.type !== "message_end") return;
|
|
1004
|
+
if (event.message.role === "assistant") this.#recordAdvisorCost(advisor, event.message);
|
|
1005
|
+
advisor.recorder.record(event.message);
|
|
994
1006
|
});
|
|
995
1007
|
}
|
|
996
1008
|
|
|
@@ -1507,6 +1519,13 @@ export class SessionAdvisors {
|
|
|
1507
1519
|
}));
|
|
1508
1520
|
return { configured: this.#advisorEnabled, advisors };
|
|
1509
1521
|
}
|
|
1522
|
+
|
|
1523
|
+
/** Return cumulative advisor cost recorded for the current session. */
|
|
1524
|
+
getAdvisorCost(): number {
|
|
1525
|
+
let cost = 0;
|
|
1526
|
+
for (const advisorCost of this.#advisorCosts.values()) cost += advisorCost;
|
|
1527
|
+
return cost;
|
|
1528
|
+
}
|
|
1510
1529
|
/**
|
|
1511
1530
|
* Return structured advisor stats for the status command and TUI panel.
|
|
1512
1531
|
*/
|
|
@@ -1530,12 +1549,13 @@ export class SessionAdvisors {
|
|
|
1530
1549
|
contextWindow: 0,
|
|
1531
1550
|
contextTokens: 0,
|
|
1532
1551
|
tokens: { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
1533
|
-
cost: 0,
|
|
1552
|
+
cost: this.#advisorCosts.get(slug) ?? 0,
|
|
1534
1553
|
messages: { user: 0, assistant: 0, total: 0 },
|
|
1535
1554
|
});
|
|
1536
1555
|
}
|
|
1537
1556
|
}
|
|
1538
1557
|
const active = liveAdvisors.length > 0;
|
|
1558
|
+
const cost = this.getAdvisorCost();
|
|
1539
1559
|
if (liveAdvisors.length === 0) {
|
|
1540
1560
|
return {
|
|
1541
1561
|
configured,
|
|
@@ -1543,14 +1563,13 @@ export class SessionAdvisors {
|
|
|
1543
1563
|
contextWindow: 0,
|
|
1544
1564
|
contextTokens: 0,
|
|
1545
1565
|
tokens: { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
1546
|
-
cost
|
|
1566
|
+
cost,
|
|
1547
1567
|
messages: { user: 0, assistant: 0, total: 0 },
|
|
1548
1568
|
advisors: roster,
|
|
1549
1569
|
};
|
|
1550
1570
|
}
|
|
1551
1571
|
const tokens = { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
|
|
1552
1572
|
const messages = { user: 0, assistant: 0, total: 0 };
|
|
1553
|
-
let cost = 0;
|
|
1554
1573
|
let contextTokens = 0;
|
|
1555
1574
|
for (const a of liveAdvisors) {
|
|
1556
1575
|
tokens.input += a.tokens.input;
|
|
@@ -1562,7 +1581,6 @@ export class SessionAdvisors {
|
|
|
1562
1581
|
messages.user += a.messages.user;
|
|
1563
1582
|
messages.assistant += a.messages.assistant;
|
|
1564
1583
|
messages.total += a.messages.total;
|
|
1565
|
-
cost += a.cost;
|
|
1566
1584
|
contextTokens += a.contextTokens;
|
|
1567
1585
|
}
|
|
1568
1586
|
// Single-advisor displays read the top-level model/window directly; surface the
|
|
@@ -1591,7 +1609,6 @@ export class SessionAdvisors {
|
|
|
1591
1609
|
let cacheRead = 0;
|
|
1592
1610
|
let cacheWrite = 0;
|
|
1593
1611
|
let totalTokens = 0;
|
|
1594
|
-
let cost = 0;
|
|
1595
1612
|
let user = 0;
|
|
1596
1613
|
let assistant = 0;
|
|
1597
1614
|
for (const message of messages) {
|
|
@@ -1605,7 +1622,6 @@ export class SessionAdvisors {
|
|
|
1605
1622
|
cacheRead += assistantMsg.usage.cacheRead;
|
|
1606
1623
|
cacheWrite += assistantMsg.usage.cacheWrite;
|
|
1607
1624
|
totalTokens += assistantMsg.usage.totalTokens;
|
|
1608
|
-
cost += assistantMsg.usage.cost.total;
|
|
1609
1625
|
}
|
|
1610
1626
|
}
|
|
1611
1627
|
return {
|
|
@@ -1619,7 +1635,7 @@ export class SessionAdvisors {
|
|
|
1619
1635
|
contextWindow: model.contextWindow ?? 0,
|
|
1620
1636
|
contextTokens,
|
|
1621
1637
|
tokens: { input, output, reasoning, cacheRead, cacheWrite, total: totalTokens },
|
|
1622
|
-
cost,
|
|
1638
|
+
cost: this.#advisorCosts.get(advisor.slug) ?? 0,
|
|
1623
1639
|
messages: { user, assistant, total: messages.length },
|
|
1624
1640
|
sessionId: advisor.agent.sessionId,
|
|
1625
1641
|
};
|
|
@@ -1649,7 +1665,7 @@ export class SessionAdvisors {
|
|
|
1649
1665
|
const spendParts = [`${s.tokens.input.toLocaleString()} input`, `${s.tokens.output.toLocaleString()} output`];
|
|
1650
1666
|
if (s.tokens.cacheRead > 0) spendParts.push(`${s.tokens.cacheRead.toLocaleString()} cache read`);
|
|
1651
1667
|
if (s.tokens.cacheWrite > 0) spendParts.push(`${s.tokens.cacheWrite.toLocaleString()} cache write`);
|
|
1652
|
-
const spendLine = `Spend: ${spendParts.join(", ")}, $${
|
|
1668
|
+
const spendLine = `Spend: ${spendParts.join(", ")}, $${stats.cost.toFixed(4)}`;
|
|
1653
1669
|
if (!s.model || s.status !== "running") return `Advisor "${s.name}" is ${s.status.replace("_", " ")}.`;
|
|
1654
1670
|
return `Advisor is enabled (${s.model.provider}/${s.model.id}). ${contextLine}. ${spendLine}.`;
|
|
1655
1671
|
}
|
|
@@ -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
|
}
|
|
@@ -11,6 +11,7 @@ import type { ExtensionRunner } from "../extensibility/extensions";
|
|
|
11
11
|
import { ExtensionToolWrapper } from "../extensibility/extensions/wrapper";
|
|
12
12
|
import { loadSkills, type Skill, type SkillWarning, setActiveSkills } from "../extensibility/skills";
|
|
13
13
|
import { type LocalProtocolOptions, XD_URL_PREFIX } from "../internal-urls";
|
|
14
|
+
import { deduplicateMCPToolsByName } from "../mcp/tool-bridge";
|
|
14
15
|
import { resolveMemoryBackend } from "../memory-backend/resolve";
|
|
15
16
|
import { MEMORY_BACKEND_TOOL_NAMES } from "../memory-backend/tool-names";
|
|
16
17
|
import type { MemoryBackendStartOptions } from "../memory-backend/types";
|
|
@@ -20,8 +21,9 @@ import { isMCPToolName, normalizeToolNames } from "../tools/builtin-names";
|
|
|
20
21
|
import { computerExposureMode } from "../tools/computer/exposure";
|
|
21
22
|
import { wrapToolWithMetaNotice } from "../tools/output-meta";
|
|
22
23
|
import { ToolAbortError, ToolError } from "../tools/tool-errors";
|
|
23
|
-
import { isMountableUnderXdev, type
|
|
24
|
+
import { isMountableUnderXdev, listXdevTools, type XdevState, xdevDocsFor, xdevEntries } from "../tools/xdev";
|
|
24
25
|
import { type EditMode, resolveEditMode } from "../utils/edit-mode";
|
|
26
|
+
import { type InspectImageMode, isInspectImageToolActive } from "../utils/inspect-image-mode";
|
|
25
27
|
import { formatLocalCalendarDate } from "../utils/local-date";
|
|
26
28
|
import {
|
|
27
29
|
extractPermissionLocations,
|
|
@@ -55,6 +57,9 @@ export interface SessionToolsHost {
|
|
|
55
57
|
emitNotice(level: "info" | "warning" | "error", message: string, source?: string): void;
|
|
56
58
|
notifyCommandMetadataChanged(): void;
|
|
57
59
|
localProtocolOptions(): LocalProtocolOptions;
|
|
60
|
+
/** Session-scoped `/vision` override; undefined means "follow the persisted setting". */
|
|
61
|
+
getInspectImageModeOverride(): InspectImageMode | undefined;
|
|
62
|
+
setInspectImageModeOverride(mode: InspectImageMode | undefined): void;
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
interface SessionToolsOptions {
|
|
@@ -62,14 +67,15 @@ interface SessionToolsOptions {
|
|
|
62
67
|
toolRegistry?: Map<string, AgentTool>;
|
|
63
68
|
createVibeTools?: () => AgentTool[];
|
|
64
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>;
|
|
65
72
|
builtInToolNames?: Iterable<string>;
|
|
66
73
|
presentationPinnedToolNames?: ReadonlySet<string>;
|
|
67
74
|
ensureWriteRegistered?: () => Promise<boolean>;
|
|
68
75
|
rebuildSystemPrompt?: (toolNames: string[], tools: Map<string, AgentTool>) => Promise<{ systemPrompt: string[] }>;
|
|
69
76
|
getLocalCalendarDate?: () => string;
|
|
70
77
|
getMcpServerInstructions?: () => Map<string, string> | undefined;
|
|
71
|
-
|
|
72
|
-
initialMountedXdevToolNames?: string[];
|
|
78
|
+
xdev?: XdevState;
|
|
73
79
|
setActiveToolNames?: (names: Iterable<string>) => void;
|
|
74
80
|
baseSystemPrompt: string[];
|
|
75
81
|
skills?: Skill[];
|
|
@@ -160,11 +166,11 @@ export class SessionTools {
|
|
|
160
166
|
#toolRegistry: Map<string, AgentTool>;
|
|
161
167
|
#createVibeTools: (() => AgentTool[]) | undefined;
|
|
162
168
|
#createComputerTool: SessionToolsOptions["createComputerTool"];
|
|
169
|
+
#createInspectImageTool: SessionToolsOptions["createInspectImageTool"];
|
|
163
170
|
#installedVibeToolNames = new Set<string>();
|
|
164
171
|
#builtInToolNames: Set<string>;
|
|
165
172
|
#rpcHostToolNames = new Set<string>();
|
|
166
|
-
#
|
|
167
|
-
#mountedXdevToolNames: Set<string>;
|
|
173
|
+
#xdev: XdevState | undefined;
|
|
168
174
|
#pendingXdevMountDelta: { added: Set<string>; removed: Set<string> } | undefined;
|
|
169
175
|
#presentationPinnedToolNames: ReadonlySet<string> | undefined;
|
|
170
176
|
#runtimeSelectedToolNames: ReadonlySet<string> | undefined;
|
|
@@ -189,14 +195,18 @@ export class SessionTools {
|
|
|
189
195
|
this.#toolRegistry = options.toolRegistry ?? new Map();
|
|
190
196
|
this.#createVibeTools = options.createVibeTools;
|
|
191
197
|
this.#createComputerTool = options.createComputerTool;
|
|
198
|
+
this.#createInspectImageTool = options.createInspectImageTool;
|
|
192
199
|
this.#builtInToolNames = new Set(options.builtInToolNames ?? []);
|
|
193
200
|
this.#presentationPinnedToolNames = options.presentationPinnedToolNames;
|
|
194
201
|
this.#ensureWriteRegistered = options.ensureWriteRegistered;
|
|
195
202
|
this.#rebuildSystemPrompt = options.rebuildSystemPrompt;
|
|
196
203
|
this.#getLocalCalendarDate = options.getLocalCalendarDate ?? formatLocalCalendarDate;
|
|
197
204
|
this.#getMcpServerInstructions = options.getMcpServerInstructions;
|
|
198
|
-
this.#
|
|
199
|
-
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);
|
|
200
210
|
this.#setActiveToolNames = options.setActiveToolNames;
|
|
201
211
|
this.#baseSystemPrompt = options.baseSystemPrompt;
|
|
202
212
|
this.#skills = options.skills ?? [];
|
|
@@ -241,7 +251,7 @@ export class SessionTools {
|
|
|
241
251
|
this.#acpPermissionDecisions.clear();
|
|
242
252
|
}
|
|
243
253
|
|
|
244
|
-
/**
|
|
254
|
+
/** Drops cached ACP decisions and re-wraps active tools after the client changes. */
|
|
245
255
|
refreshAcpPermissionGates(): void {
|
|
246
256
|
this.#acpPermissionDecisions.clear();
|
|
247
257
|
const activeTools = this.getActiveToolNames()
|
|
@@ -249,11 +259,6 @@ export class SessionTools {
|
|
|
249
259
|
.filter((tool): tool is AgentTool => tool !== undefined)
|
|
250
260
|
.map(tool => this.#wrapToolForAcpPermission(tool));
|
|
251
261
|
this.#host.agent.setTools(activeTools);
|
|
252
|
-
const mountedTools = [...this.#mountedXdevToolNames]
|
|
253
|
-
.map(name => this.#toolRegistry.get(name))
|
|
254
|
-
.filter((tool): tool is AgentTool => tool !== undefined)
|
|
255
|
-
.map(tool => this.#wrapToolForAcpPermission(tool));
|
|
256
|
-
this.#xdevRegistry?.reconcile(mountedTools);
|
|
257
262
|
}
|
|
258
263
|
|
|
259
264
|
#getActiveNonMCPToolNames(): string[] {
|
|
@@ -267,13 +272,14 @@ export class SessionTools {
|
|
|
267
272
|
|
|
268
273
|
/** Enabled top-level and discoverable tool names. */
|
|
269
274
|
getEnabledToolNames(): string[] {
|
|
270
|
-
|
|
271
|
-
return
|
|
275
|
+
const mountedNames = this.#xdev?.mountedNames;
|
|
276
|
+
if (!mountedNames || mountedNames.size === 0) return this.getActiveToolNames();
|
|
277
|
+
return [...this.getActiveToolNames(), ...mountedNames];
|
|
272
278
|
}
|
|
273
279
|
|
|
274
|
-
/** Names
|
|
280
|
+
/** Names currently presented as `xd://` devices. */
|
|
275
281
|
getMountedXdevToolNames(): string[] {
|
|
276
|
-
return [...this.#
|
|
282
|
+
return [...(this.#xdev?.mountedNames ?? [])];
|
|
277
283
|
}
|
|
278
284
|
|
|
279
285
|
/** Whether the edit tool is registered. */
|
|
@@ -402,6 +408,10 @@ export class SessionTools {
|
|
|
402
408
|
} else if (computerExpected) {
|
|
403
409
|
this.#logComputerState("Computer tool retained after model change", true);
|
|
404
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();
|
|
405
415
|
}
|
|
406
416
|
|
|
407
417
|
/** Enabled MCP tools in their current presentation partition. */
|
|
@@ -542,10 +552,7 @@ export class SessionTools {
|
|
|
542
552
|
this.#presentationPinnedToolNames?.has(name) === true || this.#runtimeSelectedToolNames?.has(name) === true;
|
|
543
553
|
const mountCandidates = selectedTools.filter(
|
|
544
554
|
({ name, tool }) =>
|
|
545
|
-
this.#
|
|
546
|
-
xdevReadAvailable &&
|
|
547
|
-
!isPresentationPinned(name) &&
|
|
548
|
-
isMountableUnderXdev(tool),
|
|
555
|
+
this.#xdev !== undefined && xdevReadAvailable && !isPresentationPinned(name) && isMountableUnderXdev(tool),
|
|
549
556
|
);
|
|
550
557
|
|
|
551
558
|
let builtInWriteAvailable = this.#builtInToolNames.has("write");
|
|
@@ -556,19 +563,15 @@ export class SessionTools {
|
|
|
556
563
|
const mountNames = builtInWriteAvailable ? new Set(mountCandidates.map(({ name }) => name)) : new Set<string>();
|
|
557
564
|
const tools: AgentTool[] = [];
|
|
558
565
|
const validToolNames: string[] = [];
|
|
559
|
-
const mountedTools: AgentTool[] = [];
|
|
560
566
|
for (const { name, tool } of selectedTools) {
|
|
561
|
-
if (mountNames.has(name))
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
tools.push(this.#wrapToolForAcpPermission(tool));
|
|
565
|
-
validToolNames.push(name);
|
|
566
|
-
}
|
|
567
|
+
if (mountNames.has(name)) continue;
|
|
568
|
+
tools.push(this.#wrapToolForAcpPermission(tool));
|
|
569
|
+
validToolNames.push(name);
|
|
567
570
|
}
|
|
568
571
|
|
|
569
572
|
const pinnedWrite = isPresentationPinned("write");
|
|
570
573
|
const activeDeferrableTool = tools.some(tool => tool.deferrable === true);
|
|
571
|
-
const transportNeeded =
|
|
574
|
+
const transportNeeded = mountNames.size > 0 || activeDeferrableTool || this.#host.planModeEnabled();
|
|
572
575
|
if (transportNeeded && !builtInWriteAvailable) {
|
|
573
576
|
builtInWriteAvailable = (await this.#ensureWriteRegistered?.()) === true;
|
|
574
577
|
if (builtInWriteAvailable) this.#builtInToolNames.add("write");
|
|
@@ -589,14 +592,9 @@ export class SessionTools {
|
|
|
589
592
|
if (writeToolIndex >= 0) tools.splice(writeToolIndex, 1);
|
|
590
593
|
}
|
|
591
594
|
|
|
592
|
-
const previousMounted = this.#
|
|
593
|
-
const previousMountedTools = [...previousMounted].flatMap(name => {
|
|
594
|
-
const tool = this.#xdevRegistry?.get(name);
|
|
595
|
-
return tool ? [tool] : [];
|
|
596
|
-
});
|
|
595
|
+
const previousMounted = new Set(this.#xdev?.mountedNames ?? []);
|
|
597
596
|
const previousActiveToolNames = this.getActiveToolNames();
|
|
598
|
-
this.#
|
|
599
|
-
this.#xdevRegistry?.reconcile(mountedTools);
|
|
597
|
+
this.#setMountedNames(mountNames);
|
|
600
598
|
this.#setActiveToolNames?.(validToolNames);
|
|
601
599
|
|
|
602
600
|
let rebuiltSystemPrompt: string[] | undefined;
|
|
@@ -611,15 +609,13 @@ export class SessionTools {
|
|
|
611
609
|
}
|
|
612
610
|
}
|
|
613
611
|
} catch (error) {
|
|
614
|
-
this.#
|
|
615
|
-
this.#xdevRegistry?.reconcile(previousMountedTools);
|
|
612
|
+
this.#setMountedNames(previousMounted);
|
|
616
613
|
this.#setActiveToolNames?.(previousActiveToolNames);
|
|
617
614
|
throw error;
|
|
618
615
|
}
|
|
619
616
|
|
|
620
617
|
if (this.#host.isDisposed()) {
|
|
621
|
-
this.#
|
|
622
|
-
this.#xdevRegistry?.reconcile(previousMountedTools);
|
|
618
|
+
this.#setMountedNames(previousMounted);
|
|
623
619
|
this.#setActiveToolNames?.(previousActiveToolNames);
|
|
624
620
|
return;
|
|
625
621
|
}
|
|
@@ -636,6 +632,13 @@ export class SessionTools {
|
|
|
636
632
|
}
|
|
637
633
|
}
|
|
638
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
|
+
|
|
639
642
|
/**
|
|
640
643
|
* Record a mid-session `xd://` mount delta for the model. Non-MCP mount
|
|
641
644
|
* churn remains notice-only, leaving the system prompt and provider cache
|
|
@@ -648,9 +651,8 @@ export class SessionTools {
|
|
|
648
651
|
* Full docs join the system prompt opportunistically on a rebuild.
|
|
649
652
|
*/
|
|
650
653
|
#notifyXdevMountDelta(previousMounted: ReadonlySet<string>): void {
|
|
651
|
-
const
|
|
652
|
-
if (!
|
|
653
|
-
const current = this.#mountedXdevToolNames;
|
|
654
|
+
const current = this.#xdev?.mountedNames;
|
|
655
|
+
if (!current) return;
|
|
654
656
|
const addedNames = [...current].filter(name => !previousMounted.has(name));
|
|
655
657
|
const removedNames = [...previousMounted].filter(name => !current.has(name));
|
|
656
658
|
if (addedNames.length === 0 && removedNames.length === 0) return;
|
|
@@ -677,14 +679,17 @@ export class SessionTools {
|
|
|
677
679
|
const pending = this.#pendingXdevMountDelta;
|
|
678
680
|
if (!pending) return undefined;
|
|
679
681
|
this.#pendingXdevMountDelta = undefined;
|
|
680
|
-
const summaries = new Map(this.#
|
|
682
|
+
const summaries = new Map(this.#xdev ? xdevEntries(this.#xdev).map(entry => [entry.name, entry.summary]) : []);
|
|
681
683
|
const added = [...pending.added].map(name => ({ name, summary: summaries.get(name) ?? "" }));
|
|
682
684
|
const removed = [...pending.removed].map(name => ({ name }));
|
|
683
|
-
const docs = this.#
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
+
: "";
|
|
688
693
|
return {
|
|
689
694
|
role: "custom",
|
|
690
695
|
customType: XDEV_MOUNT_NOTICE_MESSAGE_TYPE,
|
|
@@ -726,7 +731,7 @@ export class SessionTools {
|
|
|
726
731
|
// selection change should not demote `write` unless it is already active.
|
|
727
732
|
await this.#applyToolPresentation(
|
|
728
733
|
normalized,
|
|
729
|
-
this.#
|
|
734
|
+
this.#xdev?.mountedNames ?? new Set(),
|
|
730
735
|
this.getActiveToolNames().includes("write"),
|
|
731
736
|
);
|
|
732
737
|
}
|
|
@@ -735,7 +740,7 @@ export class SessionTools {
|
|
|
735
740
|
* Restore an enabled tool set with its exact top-level versus `xd://` partition.
|
|
736
741
|
*
|
|
737
742
|
* Both inputs are required because {@link setActiveToolsByName} only receives the
|
|
738
|
-
* enabled name list and classifies mounts from the current
|
|
743
|
+
* enabled name list and classifies mounts from the current presentation set.
|
|
739
744
|
* Rollback/restore callers must pass the snapshotted mounted subset so names that
|
|
740
745
|
* were top-level stay pinned (`#runtimeSelectedToolNames`) and names that were under
|
|
741
746
|
* `xd://` remain mount-eligible, even when the live mount set has drifted.
|
|
@@ -848,6 +853,108 @@ export class SessionTools {
|
|
|
848
853
|
return true;
|
|
849
854
|
}
|
|
850
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
|
+
|
|
851
958
|
/** Rebuilds the stable base prompt for the current tools and model. */
|
|
852
959
|
async refreshBaseSystemPrompt(): Promise<void> {
|
|
853
960
|
if (this.#host.isDisposed() || !this.#rebuildSystemPrompt) return;
|
|
@@ -963,7 +1070,7 @@ export class SessionTools {
|
|
|
963
1070
|
`${tool.name}=${tool.label ?? ""}|${tool.description ?? ""}|${tool.customWireName ?? ""}`;
|
|
964
1071
|
const descriptionSegment = tools.map(describeTool).join("\u0002");
|
|
965
1072
|
const mountedMCPProjection = projectMountedMCPXdevGuidance(
|
|
966
|
-
collectMountedMCPToolRoutes(this.#
|
|
1073
|
+
collectMountedMCPToolRoutes(this.#xdev ? listXdevTools(this.#xdev) : []),
|
|
967
1074
|
);
|
|
968
1075
|
const mountedMCPRouteSegment =
|
|
969
1076
|
JSON.stringify({
|
|
@@ -1040,7 +1147,8 @@ export class SessionTools {
|
|
|
1040
1147
|
});
|
|
1041
1148
|
|
|
1042
1149
|
const extensionRunner = this.#host.extensionRunner();
|
|
1043
|
-
|
|
1150
|
+
const uniqueMcpTools = deduplicateMCPToolsByName(mcpTools);
|
|
1151
|
+
for (const customTool of uniqueMcpTools) {
|
|
1044
1152
|
const wrapped = wrapToolWithMetaNotice(CustomToolAdapter.wrap(customTool, getCustomToolContext) as AgentTool);
|
|
1045
1153
|
const finalTool = (
|
|
1046
1154
|
extensionRunner ? new ExtensionToolWrapper(wrapped, extensionRunner) : wrapped
|
|
@@ -1050,7 +1158,7 @@ export class SessionTools {
|
|
|
1050
1158
|
|
|
1051
1159
|
// Every connected MCP tool is selected; centralized repartitioning owns
|
|
1052
1160
|
// presentation pins and write-transport activation/removal.
|
|
1053
|
-
const nextActive = [...new Set([...this.#getActiveNonMCPToolNames(), ...
|
|
1161
|
+
const nextActive = [...new Set([...this.#getActiveNonMCPToolNames(), ...uniqueMcpTools.map(tool => tool.name)])];
|
|
1054
1162
|
try {
|
|
1055
1163
|
await this.applyActiveToolsByName(nextActive);
|
|
1056
1164
|
if (this.#host.isDisposed()) restorePreviousMcpTools();
|