@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
package/src/sdk.ts
CHANGED
|
@@ -190,13 +190,17 @@ import {
|
|
|
190
190
|
HIDDEN_TOOLS,
|
|
191
191
|
isMountableUnderXdev,
|
|
192
192
|
type LspStartupServerInfo,
|
|
193
|
+
listXdevTools,
|
|
193
194
|
ReadTool,
|
|
194
195
|
releaseComputerSessionsForOwner,
|
|
196
|
+
resolveMountedXdevExecutable,
|
|
195
197
|
type Tool,
|
|
196
198
|
type ToolSession,
|
|
197
199
|
WebSearchTool,
|
|
198
200
|
WriteTool,
|
|
199
201
|
warmupLspServers,
|
|
202
|
+
xdevDocsAll,
|
|
203
|
+
xdevEntries,
|
|
200
204
|
} from "./tools";
|
|
201
205
|
import { isMCPToolName, normalizeToolNames } from "./tools/builtin-names";
|
|
202
206
|
import { ToolContextStore } from "./tools/context";
|
|
@@ -1620,6 +1624,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1620
1624
|
// mutation (any tool) bumped it in the meantime.
|
|
1621
1625
|
const fileMutationVersions = new Map<string, number>();
|
|
1622
1626
|
const activeToolNames = new Set<string>();
|
|
1627
|
+
const toolRegistry = new Map<string, Tool>();
|
|
1623
1628
|
const setActiveToolNames = (names: Iterable<string>): void => {
|
|
1624
1629
|
activeToolNames.clear();
|
|
1625
1630
|
for (const name of names) {
|
|
@@ -1632,6 +1637,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1632
1637
|
},
|
|
1633
1638
|
isToolActive: name => activeToolNames.has(name),
|
|
1634
1639
|
setActiveToolNames,
|
|
1640
|
+
toolRegistry,
|
|
1635
1641
|
hasUI: options.hasUI ?? false,
|
|
1636
1642
|
get additionalDirectories() {
|
|
1637
1643
|
return sessionManager.getAdditionalDirectories();
|
|
@@ -1682,6 +1688,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1682
1688
|
getModelString: () => (hasExplicitModel && model ? formatModelString(model) : undefined),
|
|
1683
1689
|
getActiveModelString,
|
|
1684
1690
|
getActiveModel: () => agent?.state.model ?? model,
|
|
1691
|
+
getInspectImageModeOverride: () => session?.getInspectImageModeOverride(),
|
|
1685
1692
|
getServiceTierByFamily: () => session?.serviceTierByFamily,
|
|
1686
1693
|
getImageAttachments: () => session?.getImageAttachments() ?? [],
|
|
1687
1694
|
getPlanModeState: () => session?.getPlanModeState(),
|
|
@@ -1776,7 +1783,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1776
1783
|
);
|
|
1777
1784
|
|
|
1778
1785
|
// Create built-in tools (already wrapped with meta notice formatting)
|
|
1779
|
-
|
|
1786
|
+
await logger.time("createAllTools", createTools, toolSession, options.toolNames);
|
|
1780
1787
|
|
|
1781
1788
|
// Restricted sessions cannot inherit or discover MCP capabilities.
|
|
1782
1789
|
const enableMCP = !restrictToolNames && (options.enableMCP ?? true);
|
|
@@ -1868,7 +1875,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1868
1875
|
// to mirror the AsyncJobManager ownership rule.
|
|
1869
1876
|
if (mcpManager && !options.parentTaskPrefix) MCPManager.setInstance(mcpManager);
|
|
1870
1877
|
|
|
1871
|
-
const builtInToolNames =
|
|
1878
|
+
const builtInToolNames = [...toolRegistry.keys()];
|
|
1872
1879
|
let customToolPaths: ToolPathWithSource[] = [];
|
|
1873
1880
|
const inlineExtensions: ExtensionFactory[] = [];
|
|
1874
1881
|
if (!restrictToolNames) {
|
|
@@ -2549,12 +2556,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2549
2556
|
);
|
|
2550
2557
|
|
|
2551
2558
|
// All built-in tools are active (conditional tools like git/ask return null from factory if disabled)
|
|
2552
|
-
const builtInRegistryToolNames = new Set
|
|
2553
|
-
const toolRegistry = new Map<string, Tool>();
|
|
2554
|
-
for (const tool of builtinTools) {
|
|
2555
|
-
toolRegistry.set(tool.name, tool);
|
|
2556
|
-
builtInRegistryToolNames.add(tool.name);
|
|
2557
|
-
}
|
|
2559
|
+
const builtInRegistryToolNames = toolSession.xdev?.builtInNames ?? new Set(toolRegistry.keys());
|
|
2558
2560
|
if (!restrictToolNames && !toolRegistry.has("goal") && settings.get("goal.enabled")) {
|
|
2559
2561
|
const goalTool = await logger.time("createTools:goal:session", HIDDEN_TOOLS.goal, toolSession);
|
|
2560
2562
|
if (goalTool) {
|
|
@@ -2606,31 +2608,26 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2606
2608
|
// Existing staged/device paths need write registered before active-set assembly.
|
|
2607
2609
|
// Deferred MCP also registers it now, but refresh activates it only after a server connects.
|
|
2608
2610
|
const hasDeferrableTools = Array.from(toolRegistry.values()).some(tool => tool.deferrable === true);
|
|
2609
|
-
const hasXdevTools = (toolSession.
|
|
2611
|
+
const hasXdevTools = (toolSession.xdev?.mountedNames.size ?? 0) > 0;
|
|
2610
2612
|
const planModeAvailable = settings.get("plan.enabled");
|
|
2611
2613
|
if (!restrictToolNames && (hasDeferrableTools || hasXdevTools || planModeAvailable || deferMCPDiscoveryForUI)) {
|
|
2612
2614
|
await ensureWriteRegistered();
|
|
2613
2615
|
}
|
|
2614
2616
|
|
|
2615
2617
|
let cursorEventEmitter: ((event: AgentEvent) => void) | undefined;
|
|
2616
|
-
//
|
|
2617
|
-
//
|
|
2618
|
-
//
|
|
2619
|
-
//
|
|
2620
|
-
// tier gate, but Cursor invokes advertised devices via `tool.execute()`
|
|
2621
|
-
// directly, and the agent loop's fallback resolver executes mounted
|
|
2622
|
-
// devices the model called by their top-level name — so wrap unwrapped
|
|
2623
|
-
// devices here to keep the approval/deny/prompt gate. Dynamic mounts
|
|
2624
|
-
// (custom/MCP) already come from the wrapped registry.
|
|
2618
|
+
// Cursor and the agent loop may call a mounted device by its top-level
|
|
2619
|
+
// name. Resolve that name from the canonical map and apply the same
|
|
2620
|
+
// execution-only ACP decorator used by `write xd://<tool>`; docs and
|
|
2621
|
+
// renderer lookup continue to use the undecorated canonical instance.
|
|
2625
2622
|
const resolveDeviceTool = (name: string): AgentTool | undefined => {
|
|
2626
|
-
const
|
|
2627
|
-
if (!
|
|
2628
|
-
return
|
|
2623
|
+
const state = toolSession.xdev;
|
|
2624
|
+
if (!state) return undefined;
|
|
2625
|
+
return resolveMountedXdevExecutable(state, name);
|
|
2629
2626
|
};
|
|
2630
2627
|
const cursorExecHandlers = new CursorExecHandlers({
|
|
2631
2628
|
cwd,
|
|
2632
2629
|
tools: toolRegistry,
|
|
2633
|
-
|
|
2630
|
+
getExecutableTool: resolveDeviceTool,
|
|
2634
2631
|
getToolContext: () => toolContextStore.getContext(),
|
|
2635
2632
|
emitEvent: event => cursorEventEmitter?.(event),
|
|
2636
2633
|
getTodoPhases: () => session.getTodoPhases(),
|
|
@@ -2680,7 +2677,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2680
2677
|
if (memoryInstructions) appendParts.push(memoryInstructions);
|
|
2681
2678
|
if (autoLearnInstructions) appendParts.push(autoLearnInstructions);
|
|
2682
2679
|
const projection = projectMountedMCPXdevGuidance(
|
|
2683
|
-
collectMountedMCPToolRoutes(toolSession.
|
|
2680
|
+
collectMountedMCPToolRoutes(toolSession.xdev ? listXdevTools(toolSession.xdev) : []),
|
|
2684
2681
|
);
|
|
2685
2682
|
if (projection.mappings.length > 0 || projection.hasOmittedMappings) {
|
|
2686
2683
|
appendParts.push(
|
|
@@ -2723,13 +2720,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2723
2720
|
const defaultPrompt = await buildSystemPromptInternal({
|
|
2724
2721
|
cwd,
|
|
2725
2722
|
additionalWorkspaceRoots: sessionManager.getAdditionalDirectories(),
|
|
2726
|
-
xdevTools: toolSession.
|
|
2727
|
-
xdevDocs:
|
|
2728
|
-
toolSession.
|
|
2729
|
-
|
|
2730
|
-
settings.get("tools.xdevInlineDevices"),
|
|
2731
|
-
) ?? "",
|
|
2732
|
-
autoQaEnabled: !restrictToolNames && isAutoQaEnabled(settings),
|
|
2723
|
+
xdevTools: toolSession.xdev ? xdevEntries(toolSession.xdev) : [],
|
|
2724
|
+
xdevDocs: toolSession.xdev
|
|
2725
|
+
? xdevDocsAll(toolSession.xdev, settings.get("tools.xdevDocs"), settings.get("tools.xdevInlineDevices"))
|
|
2726
|
+
: "",
|
|
2733
2727
|
resolvedCustomPrompt: options.customSystemPrompt,
|
|
2734
2728
|
skills: session?.skills ?? skills,
|
|
2735
2729
|
contextFiles,
|
|
@@ -2747,6 +2741,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2747
2741
|
taskBatch: settings.get("task.batch"),
|
|
2748
2742
|
taskMaxConcurrency: settings.get("task.maxConcurrency"),
|
|
2749
2743
|
taskIrcEnabled: !restrictToolNames && isIrcEnabled(settings, options.taskDepth ?? 0),
|
|
2744
|
+
autoQaEnabled: !restrictToolNames && isAutoQaEnabled(settings),
|
|
2750
2745
|
secretsEnabled,
|
|
2751
2746
|
workspaceTree: workspaceTreePromise,
|
|
2752
2747
|
includeWorkspaceTree,
|
|
@@ -2853,31 +2848,24 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2853
2848
|
// constructed and attached. Startup failure therefore leaves it revivable.
|
|
2854
2849
|
hasRegistered = options.expectedAgentRef === undefined || options.expectedAgentRef === null;
|
|
2855
2850
|
|
|
2856
|
-
// Partition the initial enabled set for the xd:// transport
|
|
2857
|
-
//
|
|
2858
|
-
|
|
2859
|
-
// default-set built-in devices from createTools; this reconciles dynamic
|
|
2860
|
-
// mounts (image-gen, TTS, startup MCP, active extension tools).
|
|
2861
|
-
let initialMountedXdevToolNames: string[] = [];
|
|
2862
|
-
if (toolSession.xdevRegistry) {
|
|
2851
|
+
// Partition the initial enabled set for the xd:// transport. Tool instances
|
|
2852
|
+
// remain in the canonical map; only presentation names move between layers.
|
|
2853
|
+
if (toolSession.xdev) {
|
|
2863
2854
|
const topLevelToolNames: string[] = [];
|
|
2864
|
-
const
|
|
2855
|
+
const mountedNames: string[] = [];
|
|
2865
2856
|
for (const name of initialToolNames) {
|
|
2866
2857
|
const tool = toolRegistry.get(name);
|
|
2867
2858
|
const explicitlyRequested = explicitlyRequestedToolNameSet?.has(name) === true;
|
|
2868
2859
|
if (tool && xdevReadAvailable && !explicitlyRequested && isMountableUnderXdev(tool))
|
|
2869
|
-
|
|
2860
|
+
mountedNames.push(name);
|
|
2870
2861
|
else topLevelToolNames.push(name);
|
|
2871
2862
|
}
|
|
2872
|
-
const writeTransportAvailable =
|
|
2863
|
+
const writeTransportAvailable = mountedNames.length === 0 || (await ensureWriteRegistered());
|
|
2864
|
+
toolSession.xdev.mountedNames.clear();
|
|
2873
2865
|
if (writeTransportAvailable) {
|
|
2874
|
-
toolSession.
|
|
2875
|
-
initialMountedXdevToolNames = mountedTools.map(tool => tool.name);
|
|
2866
|
+
for (const name of mountedNames) toolSession.xdev.mountedNames.add(name);
|
|
2876
2867
|
initialToolNames = topLevelToolNames;
|
|
2877
|
-
if (
|
|
2878
|
-
initialToolNames.push("write");
|
|
2879
|
-
} else {
|
|
2880
|
-
toolSession.xdevRegistry.reconcile([]);
|
|
2868
|
+
if (mountedNames.length > 0 && !initialToolNames.includes("write")) initialToolNames.push("write");
|
|
2881
2869
|
}
|
|
2882
2870
|
}
|
|
2883
2871
|
|
|
@@ -3055,7 +3043,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
3055
3043
|
return settingsAwareStreamFn(streamModel, context, streamOptions);
|
|
3056
3044
|
},
|
|
3057
3045
|
cursorExecHandlers,
|
|
3058
|
-
getCursorTools: () =>
|
|
3046
|
+
getCursorTools: () => (toolSession.xdev ? listXdevTools(toolSession.xdev) : []),
|
|
3059
3047
|
transformToolCallArguments,
|
|
3060
3048
|
resolveFallbackTool: resolveDeviceTool,
|
|
3061
3049
|
intentTracing: !!intentField,
|
|
@@ -3111,6 +3099,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
3111
3099
|
return id ? `${id}-advisor` : null;
|
|
3112
3100
|
},
|
|
3113
3101
|
getAgentId: () => "advisor",
|
|
3102
|
+
// The primary's availability signals are wrong for advisors: their tool
|
|
3103
|
+
// slate is filtered separately at runtime (default read/grep/glob, no
|
|
3104
|
+
// write transport), so xd:// devices are unreachable and read must never
|
|
3105
|
+
// advertise inspect_image — images are inlined, and the provider
|
|
3106
|
+
// boundary handles text-only advisor models.
|
|
3107
|
+
xdev: undefined,
|
|
3108
|
+
isToolActive: name => name !== "inspect_image" && toolSession.isToolActive?.(name) === true,
|
|
3114
3109
|
};
|
|
3115
3110
|
const advisorToolBuilds: Array<Tool | null | Promise<Tool | null>> = [];
|
|
3116
3111
|
for (const name in BUILTIN_TOOLS) {
|
|
@@ -3178,6 +3173,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
3178
3173
|
createComputerTool: restrictToolNames
|
|
3179
3174
|
? undefined
|
|
3180
3175
|
: async () => (await BUILTIN_TOOLS.computer(toolSession)) ?? null,
|
|
3176
|
+
createInspectImageTool: restrictToolNames
|
|
3177
|
+
? undefined
|
|
3178
|
+
: async () => (await BUILTIN_TOOLS.inspect_image(toolSession)) ?? null,
|
|
3181
3179
|
createVibeTools:
|
|
3182
3180
|
(options.taskDepth ?? 0) === 0 && !options.parentTaskPrefix
|
|
3183
3181
|
? () => createVibeTools(toolSession)
|
|
@@ -3192,9 +3190,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
3192
3190
|
preferWebsockets: preferOpenAICodexWebsockets,
|
|
3193
3191
|
convertToLlm: convertToLlmFinal,
|
|
3194
3192
|
rebuildSystemPrompt,
|
|
3195
|
-
getXdevToolEntries: () => toolSession.
|
|
3196
|
-
|
|
3197
|
-
initialMountedXdevToolNames,
|
|
3193
|
+
getXdevToolEntries: () => (toolSession.xdev ? xdevEntries(toolSession.xdev) : []),
|
|
3194
|
+
xdev: toolSession.xdev,
|
|
3198
3195
|
presentationPinnedToolNames: explicitlyRequestedToolNameSet,
|
|
3199
3196
|
setActiveToolNames,
|
|
3200
3197
|
ensureWriteRegistered,
|
|
@@ -26,7 +26,7 @@ import type { Skill, SkillWarning } from "../extensibility/skills";
|
|
|
26
26
|
import type { FileSlashCommand } from "../extensibility/slash-commands";
|
|
27
27
|
import type { SecretObfuscator } from "../secrets/obfuscator";
|
|
28
28
|
import type { ConfiguredThinkingLevel } from "../thinking";
|
|
29
|
-
import type {
|
|
29
|
+
import type { XdevState } from "../tools/xdev";
|
|
30
30
|
import type { SessionManager } from "./session-manager";
|
|
31
31
|
|
|
32
32
|
/** Maximum time the interactive shutdown path waits for Mnemopi consolidation. */
|
|
@@ -139,6 +139,8 @@ export interface AgentSessionConfig {
|
|
|
139
139
|
createMemoryTools?: () => Promise<AgentTool[]>;
|
|
140
140
|
/** Creates the built-in `computer` tool for session-scoped runtime enablement (see {@link AgentSession.setComputerToolEnabled}). */
|
|
141
141
|
createComputerTool?: () => Promise<AgentTool | null>;
|
|
142
|
+
/** Creates the built-in `inspect_image` tool for session-scoped runtime enablement (see {@link AgentSession.setInspectImageMode}). */
|
|
143
|
+
createInspectImageTool?: () => Promise<AgentTool | null>;
|
|
142
144
|
/** Model registry for API key resolution and model discovery. */
|
|
143
145
|
modelRegistry: ModelRegistry;
|
|
144
146
|
/** Tool registry for LSP and settings. */
|
|
@@ -177,10 +179,8 @@ export interface AgentSessionConfig {
|
|
|
177
179
|
getLocalCalendarDate?: () => string;
|
|
178
180
|
/** Tools mounted under `xd://`, for `/tools` display. */
|
|
179
181
|
getXdevToolEntries?: () => Array<{ name: string; summary: string }>;
|
|
180
|
-
/**
|
|
181
|
-
|
|
182
|
-
/** Discoverable tools mounted under `xd://` in the initial enabled set. */
|
|
183
|
-
initialMountedXdevToolNames?: string[];
|
|
182
|
+
/** `xd://` presentation state backed by the canonical tool map. */
|
|
183
|
+
xdev?: XdevState;
|
|
184
184
|
/** Names pinned top-level during runtime repartitioning. */
|
|
185
185
|
presentationPinnedToolNames?: ReadonlySet<string>;
|
|
186
186
|
/** Accessor for live MCP server instructions. */
|
|
@@ -35,6 +35,8 @@ import {
|
|
|
35
35
|
type AgentTurnEndContext,
|
|
36
36
|
AppendOnlyContextManager,
|
|
37
37
|
type AsideMessage,
|
|
38
|
+
type BeforeToolCallContext,
|
|
39
|
+
type BeforeToolCallResult,
|
|
38
40
|
resolveTelemetry,
|
|
39
41
|
type StreamFn,
|
|
40
42
|
TERMINAL_TOOL_RESULT_ABORT_REASON,
|
|
@@ -70,6 +72,7 @@ import type {
|
|
|
70
72
|
ToolChoice,
|
|
71
73
|
ToolResultMessage,
|
|
72
74
|
UsageReport,
|
|
75
|
+
UserMessage,
|
|
73
76
|
} from "@oh-my-pi/pi-ai";
|
|
74
77
|
import { type Effort, streamSimple } from "@oh-my-pi/pi-ai";
|
|
75
78
|
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
@@ -133,6 +136,7 @@ import type { CompactOptions, ContextUsage } from "../extensibility/extensions/t
|
|
|
133
136
|
import type { HookCommandContext } from "../extensibility/hooks/types";
|
|
134
137
|
import type { Skill, SkillWarning } from "../extensibility/skills";
|
|
135
138
|
import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slash-commands";
|
|
139
|
+
import { normalizeToolEventInput, resolveToolEventInput } from "../extensibility/tool-event-input";
|
|
136
140
|
import { GoalRuntime } from "../goals/runtime";
|
|
137
141
|
import type { GoalModeState } from "../goals/state";
|
|
138
142
|
import type { HindsightSessionState } from "../hindsight/state";
|
|
@@ -176,6 +180,7 @@ import {
|
|
|
176
180
|
toReasoningEffort,
|
|
177
181
|
} from "../thinking";
|
|
178
182
|
import { shutdownTinyTitleClient } from "../tiny/title-client";
|
|
183
|
+
import { resolveApproval } from "../tools/approval";
|
|
179
184
|
import { type AskToolDetails, type AskToolInput, recoverAskQuestions } from "../tools/ask";
|
|
180
185
|
import { releaseTabsForOwner } from "../tools/browser/tab-supervisor";
|
|
181
186
|
import type { CheckpointState, CompletedRewindState } from "../tools/checkpoint";
|
|
@@ -196,6 +201,7 @@ import type { EditMode } from "../utils/edit-mode";
|
|
|
196
201
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
197
202
|
import { extractFileMentions, generateFileMentionMessages } from "../utils/file-mentions";
|
|
198
203
|
import { normalizeModelContextImages } from "../utils/image-loading";
|
|
204
|
+
import type { InspectImageMode } from "../utils/inspect-image-mode";
|
|
199
205
|
import { generateSessionTitle } from "../utils/title-generator";
|
|
200
206
|
import { buildNamedToolChoice, isToolChoiceActive } from "../utils/tool-choice";
|
|
201
207
|
import type { VibeModeState } from "../vibe/state";
|
|
@@ -429,6 +435,8 @@ export class AgentSession {
|
|
|
429
435
|
#scheduledHiddenNextTurnGeneration: number | undefined = undefined;
|
|
430
436
|
#queuedMessageDrainScheduled = false;
|
|
431
437
|
#planModeState: PlanModeState | undefined;
|
|
438
|
+
/** Session-scoped `/vision` override; undefined = follow persisted `inspect_image.mode`. */
|
|
439
|
+
#inspectImageModeOverride: InspectImageMode | undefined;
|
|
432
440
|
#vibeModeState: VibeModeState | undefined;
|
|
433
441
|
#goalModeState: GoalModeState | undefined;
|
|
434
442
|
#goalRuntime: GoalRuntime;
|
|
@@ -479,6 +487,13 @@ export class AgentSession {
|
|
|
479
487
|
readonly #asyncJobManager: AsyncJobManager | undefined;
|
|
480
488
|
/** Clears this session's owner delivery sink registration; set when a manager + agent id exist. */
|
|
481
489
|
#unregisterAsyncDeliverySink: (() => void) | undefined;
|
|
490
|
+
/**
|
|
491
|
+
* Async-delivery generation, bumped on every session transition that evicts
|
|
492
|
+
* this owner's jobs (see {@link AgentSession.#cancelOwnAsyncJobs}). Stamped
|
|
493
|
+
* onto each queued async-result follow-up so a delivery formatted or drained
|
|
494
|
+
* across a `/new` is dropped regardless of job-id reuse.
|
|
495
|
+
*/
|
|
496
|
+
#asyncDeliveryEpoch = 0;
|
|
482
497
|
|
|
483
498
|
readonly #irc: IrcBridge;
|
|
484
499
|
// Agent identity (registry id) used for IRC routing and job ownership.
|
|
@@ -1081,20 +1096,24 @@ export class AgentSession {
|
|
|
1081
1096
|
emitNotice: (level, message, source) => this.emitNotice(level, message, source),
|
|
1082
1097
|
notifyCommandMetadataChanged: () => this.#notifyCommandMetadataChanged(),
|
|
1083
1098
|
localProtocolOptions: () => this.#localProtocolOptions(),
|
|
1099
|
+
getInspectImageModeOverride: () => this.#inspectImageModeOverride,
|
|
1100
|
+
setInspectImageModeOverride: mode => {
|
|
1101
|
+
this.#inspectImageModeOverride = mode;
|
|
1102
|
+
},
|
|
1084
1103
|
};
|
|
1085
1104
|
this.#tools = new SessionTools(sessionToolsHost, {
|
|
1086
1105
|
autoApprove: config.autoApprove,
|
|
1087
1106
|
toolRegistry: config.toolRegistry,
|
|
1088
1107
|
createVibeTools: config.createVibeTools,
|
|
1089
1108
|
createComputerTool: config.createComputerTool,
|
|
1109
|
+
createInspectImageTool: config.createInspectImageTool,
|
|
1090
1110
|
builtInToolNames: config.builtInToolNames,
|
|
1091
1111
|
presentationPinnedToolNames: config.presentationPinnedToolNames,
|
|
1092
1112
|
ensureWriteRegistered: config.ensureWriteRegistered,
|
|
1093
1113
|
rebuildSystemPrompt: config.rebuildSystemPrompt,
|
|
1094
1114
|
getLocalCalendarDate: config.getLocalCalendarDate,
|
|
1095
1115
|
getMcpServerInstructions: config.getMcpServerInstructions,
|
|
1096
|
-
|
|
1097
|
-
initialMountedXdevToolNames: config.initialMountedXdevToolNames,
|
|
1116
|
+
xdev: config.xdev,
|
|
1098
1117
|
setActiveToolNames: config.setActiveToolNames,
|
|
1099
1118
|
baseSystemPrompt: this.agent.state.systemPrompt,
|
|
1100
1119
|
skills: config.skills,
|
|
@@ -1161,7 +1180,7 @@ export class AgentSession {
|
|
|
1161
1180
|
this.#deliverAsyncJobResult(manager, jobId, text, job),
|
|
1162
1181
|
);
|
|
1163
1182
|
this.yieldQueue.register<AsyncResultEntry>("async-result", {
|
|
1164
|
-
isStale: entry => manager.isDeliverySuppressed(entry.jobId),
|
|
1183
|
+
isStale: entry => entry.epoch !== this.#asyncDeliveryEpoch || manager.isDeliverySuppressed(entry.jobId),
|
|
1165
1184
|
build: buildAsyncResultBatchMessage,
|
|
1166
1185
|
});
|
|
1167
1186
|
}
|
|
@@ -1177,6 +1196,10 @@ export class AgentSession {
|
|
|
1177
1196
|
});
|
|
1178
1197
|
// Tool-result hook owns synchronous post-tool actions that must affect the current loop.
|
|
1179
1198
|
this.agent.afterToolCall = ctx => this.#afterToolCall(ctx);
|
|
1199
|
+
// Pre-scheduling tool_call wiring: extension handlers run at arg-prep
|
|
1200
|
+
// time so a block/revision lands before concurrency resolution,
|
|
1201
|
+
// tool_execution_start, and the wrapper's approval gate.
|
|
1202
|
+
this.agent.beforeToolCall = ctx => this.#beforeToolCall(ctx);
|
|
1180
1203
|
this.agent.providerSessionState = this.#providerSessionState;
|
|
1181
1204
|
this.#syncAgentSessionId();
|
|
1182
1205
|
this.#todo.syncFromBranch();
|
|
@@ -1596,7 +1619,9 @@ export class AgentSession {
|
|
|
1596
1619
|
* transitions (newSession, switchSession, handoff, dispose) so a subagent
|
|
1597
1620
|
* cleans up its own background work without touching its parent's jobs.
|
|
1598
1621
|
*
|
|
1599
|
-
*
|
|
1622
|
+
* Cleanup runs against this session's scoped manager: running jobs are
|
|
1623
|
+
* cancelled, finished rows are evicted with their pending deliveries, and any
|
|
1624
|
+
* async-result follow-up already queued for injection is dropped. Subagents have
|
|
1600
1625
|
* unique agent ids and inherit the parent's manager to clean up their own
|
|
1601
1626
|
* jobs. A secondary in-process top-level session gets no scoped manager,
|
|
1602
1627
|
* because it defaults to `MAIN_AGENT_ID`; reaching through the global
|
|
@@ -1609,6 +1634,12 @@ export class AgentSession {
|
|
|
1609
1634
|
if (!this.#agentId) return;
|
|
1610
1635
|
const manager = this.#asyncJobManager;
|
|
1611
1636
|
manager?.cancelAll({ ownerId: this.#agentId });
|
|
1637
|
+
manager?.evictCompletedJobs({ ownerId: this.#agentId });
|
|
1638
|
+
// Invalidate this owner's in-flight/drained deliveries against the new
|
|
1639
|
+
// generation, then drop any async-result follow-up already queued, so a
|
|
1640
|
+
// prior session's background result cannot inject into the next transcript.
|
|
1641
|
+
this.#asyncDeliveryEpoch += 1;
|
|
1642
|
+
this.yieldQueue.clear("async-result");
|
|
1612
1643
|
}
|
|
1613
1644
|
|
|
1614
1645
|
/**
|
|
@@ -1674,10 +1705,17 @@ export class AgentSession {
|
|
|
1674
1705
|
async #deliverAsyncJobResult(manager: AsyncJobManager, jobId: string, text: string, job?: AsyncJob): Promise<void> {
|
|
1675
1706
|
if (this.#isDisposed) return;
|
|
1676
1707
|
if (manager.isDeliverySuppressed(jobId)) return;
|
|
1708
|
+
// Snapshot the generation before the async format step: a `/new` during it
|
|
1709
|
+
// bumps the epoch, so this delivery belongs to the replaced session and
|
|
1710
|
+
// must not enqueue — the suppression marker alone is unreliable because
|
|
1711
|
+
// job-id reuse clears it.
|
|
1712
|
+
const epoch = this.#asyncDeliveryEpoch;
|
|
1677
1713
|
const formatted = await this.#formatAsyncResultForFollowUp(text);
|
|
1714
|
+
if (this.#isDisposed) return;
|
|
1715
|
+
if (epoch !== this.#asyncDeliveryEpoch) return;
|
|
1678
1716
|
if (manager.isDeliverySuppressed(jobId)) return;
|
|
1679
1717
|
const durationMs = job ? Math.max(0, Date.now() - job.startTime) : undefined;
|
|
1680
|
-
this.yieldQueue.enqueue<AsyncResultEntry>("async-result", { jobId, result: formatted, job, durationMs });
|
|
1718
|
+
this.yieldQueue.enqueue<AsyncResultEntry>("async-result", { jobId, result: formatted, job, durationMs, epoch });
|
|
1681
1719
|
}
|
|
1682
1720
|
|
|
1683
1721
|
async #formatAsyncResultForFollowUp(result: string): Promise<string> {
|
|
@@ -2961,6 +2999,50 @@ export class AgentSession {
|
|
|
2961
2999
|
}
|
|
2962
3000
|
return this.#ttsr.afterToolCall(ctx);
|
|
2963
3001
|
}
|
|
3002
|
+
/**
|
|
3003
|
+
* Emits the extension `tool_call` event for a loop-dispatched call at
|
|
3004
|
+
* arg-prep time — before concurrency scheduling, `tool_execution_start`,
|
|
3005
|
+
* and the wrapper's approval gate. A handler block becomes a blocked tool
|
|
3006
|
+
* result; a handler `input` revision becomes the arguments the loop
|
|
3007
|
+
* schedules, displays, persists, and executes, so approval resolves against
|
|
3008
|
+
* what actually runs. Marks the dispatch so `ExtensionToolWrapper` does not
|
|
3009
|
+
* emit a second event (nested xd:// device dispatches and direct non-loop
|
|
3010
|
+
* execution still emit there).
|
|
3011
|
+
*/
|
|
3012
|
+
async #beforeToolCall(ctx: BeforeToolCallContext): Promise<BeforeToolCallResult | undefined> {
|
|
3013
|
+
const runner = this.#extensionRunner;
|
|
3014
|
+
if (!runner?.hasHandlers("tool_call")) return undefined;
|
|
3015
|
+
const metadata = ctx.toolCall.providerMetadata;
|
|
3016
|
+
const computer = metadata?.type === "computer" ? metadata : undefined;
|
|
3017
|
+
// Parity with the wrapper's pre-emit short-circuit: an already-denied
|
|
3018
|
+
// call never reaches extensions. Deny is mode-independent (tool decision
|
|
3019
|
+
// or user policy), so resolving under the most permissive mode is exact;
|
|
3020
|
+
// the wrapper still enforces the mode-accurate gate before execution.
|
|
3021
|
+
const userPolicies = (this.settings.get("tools.approval") ?? {}) as Record<string, unknown>;
|
|
3022
|
+
const approvalArgs = computer ? { actions: computer.actions } : ctx.args;
|
|
3023
|
+
if (resolveApproval(ctx.tool, approvalArgs, "yolo", userPolicies).policy === "deny") {
|
|
3024
|
+
return undefined;
|
|
3025
|
+
}
|
|
3026
|
+
const eventArgs = computer
|
|
3027
|
+
? { actions: computer.actions, pendingSafetyChecks: computer.pendingSafetyChecks }
|
|
3028
|
+
: ctx.args;
|
|
3029
|
+
runner.markToolCallEmitted(ctx.toolCall.id, ctx.tool.name);
|
|
3030
|
+
const callResult = await runner.emitToolCall({
|
|
3031
|
+
type: "tool_call",
|
|
3032
|
+
toolName: ctx.tool.name,
|
|
3033
|
+
toolCallId: ctx.toolCall.id,
|
|
3034
|
+
input: normalizeToolEventInput(ctx.tool.name, resolveToolEventInput(ctx.tool, eventArgs)),
|
|
3035
|
+
});
|
|
3036
|
+
if (callResult?.block) {
|
|
3037
|
+
return { block: true, reason: callResult.reason || "Tool execution was blocked by an extension" };
|
|
3038
|
+
}
|
|
3039
|
+
// A computer call's event input is a synthetic {actions, pendingSafetyChecks}
|
|
3040
|
+
// view, not the execution params — a revision cannot map back onto them.
|
|
3041
|
+
if (callResult?.input !== undefined && !computer) {
|
|
3042
|
+
return { args: callResult.input };
|
|
3043
|
+
}
|
|
3044
|
+
return undefined;
|
|
3045
|
+
}
|
|
2964
3046
|
|
|
2965
3047
|
/** Find the last assistant message in agent state (including aborted ones) */
|
|
2966
3048
|
#findLastAssistantMessage(): AssistantMessage | undefined {
|
|
@@ -3950,6 +4032,34 @@ export class AgentSession {
|
|
|
3950
4032
|
return this.#tools.setComputerToolEnabled(enabled);
|
|
3951
4033
|
}
|
|
3952
4034
|
|
|
4035
|
+
/**
|
|
4036
|
+
* Session-scoped inspect_image mode (`/vision`). `auto` clears the override
|
|
4037
|
+
* and returns to the persisted `inspect_image.mode` setting; `on`/`off`
|
|
4038
|
+
* force the tool for this session only. See {@link SessionTools.setInspectImageMode}.
|
|
4039
|
+
*/
|
|
4040
|
+
setInspectImageMode(mode: InspectImageMode): Promise<boolean> {
|
|
4041
|
+
return this.#tools.setInspectImageMode(mode);
|
|
4042
|
+
}
|
|
4043
|
+
|
|
4044
|
+
/** Effective inspect_image state for `/vision status`. */
|
|
4045
|
+
inspectImageState(): { mode: InspectImageMode; active: boolean; model: string | undefined } {
|
|
4046
|
+
return this.#tools.inspectImageState();
|
|
4047
|
+
}
|
|
4048
|
+
|
|
4049
|
+
/** Session-scoped `/vision` override; undefined means "follow the persisted setting". */
|
|
4050
|
+
getInspectImageModeOverride(): InspectImageMode | undefined {
|
|
4051
|
+
return this.#inspectImageModeOverride;
|
|
4052
|
+
}
|
|
4053
|
+
|
|
4054
|
+
/**
|
|
4055
|
+
* Reconciles the inspect_image tool set after the persisted
|
|
4056
|
+
* `inspect_image.mode` setting changed (e.g. via the settings selector), so
|
|
4057
|
+
* the new value takes effect immediately instead of on the next model switch.
|
|
4058
|
+
*/
|
|
4059
|
+
applyInspectImageModeChange(): Promise<boolean> {
|
|
4060
|
+
return this.#tools.reconcileInspectImageTool();
|
|
4061
|
+
}
|
|
4062
|
+
|
|
3953
4063
|
/** Cancels the local rollout-memory startup owned by this session. */
|
|
3954
4064
|
cancelLocalMemoryStartup(): void {
|
|
3955
4065
|
this.#memory.cancelLocalMemoryStartup();
|
|
@@ -4477,7 +4587,9 @@ export class AgentSession {
|
|
|
4477
4587
|
return {
|
|
4478
4588
|
role: "custom",
|
|
4479
4589
|
customType: "vibe-mode-context",
|
|
4480
|
-
content: prompt.render(vibeModeActivePrompt
|
|
4590
|
+
content: prompt.render(vibeModeActivePrompt, {
|
|
4591
|
+
todoAvailable: this.getActiveToolNames().includes("todo"),
|
|
4592
|
+
}),
|
|
4481
4593
|
display: false,
|
|
4482
4594
|
attribution: "agent",
|
|
4483
4595
|
timestamp: Date.now(),
|
|
@@ -6405,7 +6517,7 @@ export class AgentSession {
|
|
|
6405
6517
|
return true;
|
|
6406
6518
|
}
|
|
6407
6519
|
|
|
6408
|
-
#setModelWithProviderSessionReset(model: Model): void {
|
|
6520
|
+
async #setModelWithProviderSessionReset(model: Model): Promise<void> {
|
|
6409
6521
|
const currentModel = this.model;
|
|
6410
6522
|
if (currentModel) {
|
|
6411
6523
|
this.#closeProviderSessionsForModelSwitch(currentModel, model);
|
|
@@ -6417,6 +6529,16 @@ export class AgentSession {
|
|
|
6417
6529
|
|
|
6418
6530
|
// Re-evaluate append-only context mode — provider or setting may have changed
|
|
6419
6531
|
this.#syncAppendOnlyContext(model);
|
|
6532
|
+
|
|
6533
|
+
// inspect_image auto mode keys off model image capability. Reconcile
|
|
6534
|
+
// centrally here so retry-fallback model changes (turn-recovery.ts),
|
|
6535
|
+
// which bypass syncAfterModelChange, cannot leave the tool set stale —
|
|
6536
|
+
// callers await, so a scheduled retry never races the reconciled slate.
|
|
6537
|
+
try {
|
|
6538
|
+
await this.#tools.reconcileInspectImageAfterModelChange();
|
|
6539
|
+
} catch (error) {
|
|
6540
|
+
logger.warn("inspect_image reconcile after model change failed", { error: String(error) });
|
|
6541
|
+
}
|
|
6420
6542
|
}
|
|
6421
6543
|
|
|
6422
6544
|
#closeCodexProviderSessionsForHistoryRewrite(): void {
|
|
@@ -6981,7 +7103,7 @@ export class AgentSession {
|
|
|
6981
7103
|
currentModel.id !== match.id ||
|
|
6982
7104
|
currentModel.api !== match.api));
|
|
6983
7105
|
if (shouldResetProviderState) {
|
|
6984
|
-
this.#setModelWithProviderSessionReset(match);
|
|
7106
|
+
await this.#setModelWithProviderSessionReset(match);
|
|
6985
7107
|
} else {
|
|
6986
7108
|
this.agent.setModel(match);
|
|
6987
7109
|
}
|
|
@@ -7108,10 +7230,12 @@ export class AgentSession {
|
|
|
7108
7230
|
* @param entryId ID of the entry to branch from
|
|
7109
7231
|
* @returns Object with:
|
|
7110
7232
|
* - selectedText: The text of the selected user message (for editor pre-fill)
|
|
7233
|
+
* - selectedImages: Image attachments of the selected user message (for editor draft restore)
|
|
7111
7234
|
* - cancelled: True if a hook cancelled the branch
|
|
7112
7235
|
*/
|
|
7113
7236
|
async branch(entryId: string): Promise<{
|
|
7114
7237
|
selectedText: string;
|
|
7238
|
+
selectedImages: ImageContent[];
|
|
7115
7239
|
cancelled: boolean;
|
|
7116
7240
|
}> {
|
|
7117
7241
|
const previousSessionFile = this.sessionFile;
|
|
@@ -7122,6 +7246,7 @@ export class AgentSession {
|
|
|
7122
7246
|
}
|
|
7123
7247
|
|
|
7124
7248
|
const selectedText = this.#extractUserMessageText(selectedEntry.message.content);
|
|
7249
|
+
const selectedImages = this.#extractUserMessageImages(selectedEntry.message.content);
|
|
7125
7250
|
|
|
7126
7251
|
let skipConversationRestore = false;
|
|
7127
7252
|
|
|
@@ -7133,7 +7258,7 @@ export class AgentSession {
|
|
|
7133
7258
|
})) as SessionBeforeBranchResult | undefined;
|
|
7134
7259
|
|
|
7135
7260
|
if (result?.cancel) {
|
|
7136
|
-
return { selectedText, cancelled: true };
|
|
7261
|
+
return { selectedText, selectedImages, cancelled: true };
|
|
7137
7262
|
}
|
|
7138
7263
|
skipConversationRestore = result?.skipConversationRestore ?? false;
|
|
7139
7264
|
}
|
|
@@ -7189,7 +7314,7 @@ export class AgentSession {
|
|
|
7189
7314
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
7190
7315
|
}
|
|
7191
7316
|
|
|
7192
|
-
return { selectedText, cancelled: false };
|
|
7317
|
+
return { selectedText, selectedImages, cancelled: false };
|
|
7193
7318
|
}
|
|
7194
7319
|
|
|
7195
7320
|
async branchFromBtw(
|
|
@@ -7304,7 +7429,7 @@ export class AgentSession {
|
|
|
7304
7429
|
* @param targetId The entry ID to navigate to
|
|
7305
7430
|
* @param options.summarize Whether user wants to summarize abandoned branch
|
|
7306
7431
|
* @param options.customInstructions Custom instructions for summarizer
|
|
7307
|
-
* @returns Result with editorText (if user message) and cancelled status
|
|
7432
|
+
* @returns Result with editorText/editorImages (if user message) and cancelled status
|
|
7308
7433
|
*/
|
|
7309
7434
|
async navigateTree(
|
|
7310
7435
|
targetId: string,
|
|
@@ -7334,6 +7459,8 @@ export class AgentSession {
|
|
|
7334
7459
|
} = {},
|
|
7335
7460
|
): Promise<{
|
|
7336
7461
|
editorText?: string;
|
|
7462
|
+
/** Image attachments of the target user message, parallel to the positional `[Image #N]` markers in {@link editorText}. */
|
|
7463
|
+
editorImages?: ImageContent[];
|
|
7337
7464
|
cancelled: boolean;
|
|
7338
7465
|
aborted?: boolean;
|
|
7339
7466
|
summaryEntry?: BranchSummaryEntry;
|
|
@@ -7506,6 +7633,7 @@ export class AgentSession {
|
|
|
7506
7633
|
// Determine the new leaf position based on target type
|
|
7507
7634
|
let newLeafId: string | null;
|
|
7508
7635
|
let editorText: string | undefined;
|
|
7636
|
+
let editorImages: ImageContent[] | undefined;
|
|
7509
7637
|
// Set when the second-pass `ask` re-answer branch below actually commits a
|
|
7510
7638
|
// new sibling answer — the trigger for resuming the agent afterwards so the
|
|
7511
7639
|
// model consumes it, mirroring a live `ask` completion (issue #6483).
|
|
@@ -7515,6 +7643,8 @@ export class AgentSession {
|
|
|
7515
7643
|
// User message: leaf = parent (null if root), text goes to editor
|
|
7516
7644
|
newLeafId = targetEntry.parentId;
|
|
7517
7645
|
editorText = this.#extractUserMessageText(targetEntry.message.content);
|
|
7646
|
+
const targetImages = this.#extractUserMessageImages(targetEntry.message.content);
|
|
7647
|
+
if (targetImages.length > 0) editorImages = targetImages;
|
|
7518
7648
|
} else if (targetEntry.type === "custom_message" && targetEntry.customType !== SKILL_PROMPT_MESSAGE_TYPE) {
|
|
7519
7649
|
// Custom message: leaf = parent (null if root), text goes to editor
|
|
7520
7650
|
newLeafId = targetEntry.parentId;
|
|
@@ -7614,6 +7744,7 @@ export class AgentSession {
|
|
|
7614
7744
|
const rawContext = this.sessionManager.buildSessionContext();
|
|
7615
7745
|
return {
|
|
7616
7746
|
editorText,
|
|
7747
|
+
editorImages,
|
|
7617
7748
|
cancelled: false,
|
|
7618
7749
|
summaryEntry,
|
|
7619
7750
|
sessionContext: rawContext,
|
|
@@ -7622,6 +7753,7 @@ export class AgentSession {
|
|
|
7622
7753
|
}
|
|
7623
7754
|
return {
|
|
7624
7755
|
editorText,
|
|
7756
|
+
editorImages,
|
|
7625
7757
|
cancelled: false,
|
|
7626
7758
|
summaryEntry,
|
|
7627
7759
|
sessionContext: stateContext,
|
|
@@ -7740,6 +7872,14 @@ export class AgentSession {
|
|
|
7740
7872
|
return "";
|
|
7741
7873
|
}
|
|
7742
7874
|
|
|
7875
|
+
/** Image parts of a stored user message, in submission order — index N-1 backs the
|
|
7876
|
+
* `[Image #N]` marker in the message text, so restoring them alongside the text keeps
|
|
7877
|
+
* positional markers resolvable on resubmit. */
|
|
7878
|
+
#extractUserMessageImages(content: UserMessage["content"]): ImageContent[] {
|
|
7879
|
+
if (!Array.isArray(content)) return [];
|
|
7880
|
+
return content.filter((c): c is ImageContent => c.type === "image");
|
|
7881
|
+
}
|
|
7882
|
+
|
|
7743
7883
|
/**
|
|
7744
7884
|
* Get session statistics.
|
|
7745
7885
|
*/
|
|
@@ -29,6 +29,14 @@ export interface AsyncResultEntry {
|
|
|
29
29
|
result: string;
|
|
30
30
|
job: AsyncJob | undefined;
|
|
31
31
|
durationMs: number | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Owning session's async-delivery generation at enqueue time. A session
|
|
34
|
+
* transition (`/new`, switch, handoff) bumps the generation, so an entry
|
|
35
|
+
* whose generation no longer matches belongs to a replaced transcript and
|
|
36
|
+
* is dropped at flush — even after its job id has been reused, which clears
|
|
37
|
+
* the manager's per-id suppression marker.
|
|
38
|
+
*/
|
|
39
|
+
epoch: number;
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
type AsyncResultJobDetails = {
|