From 3506e8d6a9df66db45b1f41cc06f8e259b521f2f Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 8 Nov 2026 12:54:53 +0100 Subject: [PATCH] Return the draft's options from AgentClient.listControls listControls now answers { controls, options } like AgentManager.listDraftControls: the options a session opened from the draft would run with, with config defaults merged in and anything the provider resolves natively for the ones nobody set. ACP fills unset feature selects from the probe session's currentValue, so a fresh Cursor draft shows the Fast selection the agent opens with instead of no selection. The legacy features response and the MCP draft listing already project from the manager's result, so they follow. --- docs/providers.md | 2 +- .../src/server/agent/agent-manager.test.ts | 65 +++++++++++++++++-- .../server/src/server/agent/agent-manager.ts | 7 +- .../src/server/agent/agent-sdk-types.ts | 12 +++- .../server/agent/provider-registry.test.ts | 45 +++++++------ .../server/agent/providers/acp-agent.test.ts | 35 ++++++---- .../src/server/agent/providers/acp-agent.ts | 45 ++++++++----- .../agent/providers/claude/agent.test.ts | 26 ++++++-- .../server/agent/providers/claude/agent.ts | 8 ++- .../agent/providers/cursor-acp-agent.test.ts | 47 +++++++------- .../src/server/agent/providers/omp/agent.ts | 5 +- .../opencode-agent.full-access.test.ts | 19 +++--- .../server/agent/providers/opencode-agent.ts | 5 +- .../providers/opencode/runtime-client.test.ts | 14 ++-- .../agent/providers/opencode/v2/agent.ts | 6 +- .../server/agent/providers/pi/agent.test.ts | 2 +- .../src/server/agent/providers/pi/agent.ts | 6 +- 17 files changed, 234 insertions(+), 115 deletions(-) diff --git a/docs/providers.md b/docs/providers.md index 510ddd7677..10dc7b4656 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -174,7 +174,7 @@ Submitted user-message wire items carry the same Paseo ID in `messageId` and `cl Provider adapters must terminalize every transient timeline row before emitting the turn's terminal event. Codex may omit the completed `contextCompaction` item when a turn ends during compaction, so its adapter closes any pending root compaction before forwarding `turn_completed`, `turn_failed`, or `turn_canceled`. A terminal turn must never leave the client showing an operation as still loading. -Draft metadata lookups should avoid creating provider sessions when the upstream provider has top-level APIs for that metadata. Prefer `AgentClient.fetchCatalog`, `listCommands`, or `listControls` over creating a scratch `AgentSession`; scratch sessions can show up as empty native sessions in provider import/history UIs. `fetchCatalog` is the single discovery API for models and modes — provider implementations may use one process, separate upstream calls, or static data internally, but callers outside the provider do not get separate runtime model/mode probes. Draft command listing and scratch-session control listing require an explicit draft model. Do not resolve a default model through catalog discovery. A client-level `listControls` implementation may return controls from an incomplete, model-less draft and owns which features are valid in that state. +Draft metadata lookups should avoid creating provider sessions when the upstream provider has top-level APIs for that metadata. Prefer `AgentClient.fetchCatalog`, `listCommands`, or `listControls` over creating a scratch `AgentSession`; scratch sessions can show up as empty native sessions in provider import/history UIs. `fetchCatalog` is the single discovery API for models and modes — provider implementations may use one process, separate upstream calls, or static data internally, but callers outside the provider do not get separate runtime model/mode probes. Draft command listing and scratch-session control listing require an explicit draft model. Do not resolve a default model through catalog discovery. A client-level `listControls` implementation may return controls from an incomplete, model-less draft and owns which features are valid in that state. It returns `{ controls, options }`: the options a session opened from the draft would run with, so a value the provider resolves natively (an ACP select nobody set) shows in the draft the same way it would on the agent. Provider session import has its own contract. The picker calls `listImportableSessions` and receives rows only: provider handle, cwd, title, prompt previews, and last activity. Import calls `importSession({ providerHandleId, cwd })` for the selected row and must not call listing again. The provider returns the resumed session, storage config, persistence handle, and hydrated timeline for that one native session; `AgentManager.importProviderSession` seeds the daemon timeline and publishes the Paseo agent only after it is ready. diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index 78506ed9bd..29b7fa06f8 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -39,6 +39,7 @@ import type { import type { AgentClient, AgentCreateSessionOptions, + AgentDraftControls, AgentOptionControl, AgentLaunchContext, AgentPromptInput, @@ -61,6 +62,8 @@ import type { ProviderDefinition } from "./provider-registry.js"; import { mergeProviderOptions } from "./provider-options.js"; import { OpenCodeV2AgentClient } from "./providers/opencode/v2/agent.js"; +import { CursorACPAgentClient } from "./providers/cursor-acp-agent.js"; +import { fileURLToPath } from "node:url"; import { V2Harness } from "./providers/opencode/test-utils/v2-harness.js"; const DESKTOP_OPEN_AGENT_TAB_LABEL = getOpenAgentTabLabel("desktop-client"); @@ -2493,9 +2496,9 @@ test("listDraftControls uses client feature listing without a model", async () = return await super.createSession(config); } - async listControls(config: AgentSessionConfig): Promise { + async listControls(config: AgentSessionConfig): Promise { this.featureConfigs.push(config); - return [draftControl]; + return { controls: [draftControl], options: config.providerOptions ?? {} }; } } const client = new DraftFeatureClient(); @@ -2559,9 +2562,9 @@ test("listDraftControls uses explicit model config without default model fetchin return await super.createSession(config); } - async listControls(config: AgentSessionConfig): Promise { + async listControls(config: AgentSessionConfig): Promise { this.featureConfigs.push(config); - return [draftControl]; + return { controls: [draftControl], options: config.providerOptions ?? {} }; } } const client = new DraftFeatureClient(); @@ -2591,6 +2594,60 @@ test("listDraftControls uses explicit model config without default model fetchin ]); }); +test("listDraftControls reports the Fast selection a fresh Cursor draft opens with", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-draft-cursor-")); + const storage = new AgentStorage(join(workdir, "agents"), logger); + const fakeAgentPath = fileURLToPath( + new URL("./providers/test-utils/fake-acp-agent.mjs", import.meta.url), + ); + const scenario = { + options: [ + { + id: "fast", + name: "Fast", + type: "select", + currentValue: "true", + options: [ + { value: "false", name: "Off" }, + { value: "true", name: "Fast" }, + ], + }, + ], + }; + // The registry maps "cursor" onto the generic ACP provider id; this manager has no registry. + const client = new CursorACPAgentClient({ + logger, + command: [process.execPath, fakeAgentPath, JSON.stringify(scenario)], + }); + const manager = new AgentManager({ clients: { acp: client }, registry: storage, logger }); + const controls = [ + expect.objectContaining({ type: "toggle", key: "auto_accept" }), + expect.objectContaining({ + type: "select", + key: "fast", + choices: [ + { value: "false", label: "Off" }, + { value: "true", label: "Fast" }, + ], + }), + ]; + + // Nobody set Fast, so the draft shows the value the agent opens with. + await expect(manager.listDraftControls({ provider: "acp", cwd: workdir })).resolves.toEqual({ + controls, + options: { fast: "true" }, + }); + + // A set value stands over the probe's reading. + await expect( + manager.listDraftControls({ + provider: "acp", + cwd: workdir, + providerOptions: { fast: "false" }, + }), + ).resolves.toEqual({ controls, options: { fast: "false" } }); +}); + test("createAgent injects daemon append system prompt at runtime only", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); const storagePath = join(workdir, "agents"); diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index 043cf01877..85b798235c 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -35,6 +35,7 @@ import { type AgentCreateSessionOptions, type AgentResumePurpose, type AgentResumeSessionOptions, + type AgentDraftControls, type AgentOptionControl, type AgentLaunchContext, type AgentSlashCommand, @@ -1324,9 +1325,7 @@ export class AgentManager { } } - async listDraftControls( - config: AgentSessionConfig, - ): Promise<{ controls: AgentOptionControl[]; options: ProviderOptions }> { + async listDraftControls(config: AgentSessionConfig): Promise { const normalizedConfig = await this.normalizeConfig(config, { resolveDefaultModel: false }); const client = this.requireClient(normalizedConfig.provider); const options = this.effectiveOptionsFor( @@ -1344,7 +1343,7 @@ export class AgentManager { } if (client.listControls) { - return { controls: await client.listControls(normalizedConfig), options }; + return await client.listControls(normalizedConfig); } const session = await client.createSession(normalizedConfig); diff --git a/packages/server/src/server/agent/agent-sdk-types.ts b/packages/server/src/server/agent/agent-sdk-types.ts index f29994e465..bd72f4ed08 100644 --- a/packages/server/src/server/agent/agent-sdk-types.ts +++ b/packages/server/src/server/agent/agent-sdk-types.ts @@ -798,6 +798,11 @@ export interface ResolveAgentDefaultModeInput { signal?: AbortSignal; } +export interface AgentDraftControls { + controls: AgentOptionControl[]; + options: ProviderOptions; +} + export interface AgentClient { readonly provider: AgentProvider; readonly capabilities: AgentCapabilityFlags; @@ -840,7 +845,12 @@ export interface AgentClient { resolveCreateConfig?(input: ResolveAgentCreateConfigInput): ResolveAgentCreateConfigResult; isCreateConfigUnattended?(input: AgentCreateConfigUnattendedInput): boolean; listCommands?(config: AgentSessionConfig): Promise; - listControls?(config: AgentSessionConfig): Promise; + /** + * The controls of a draft, with the options a session opened from `config` would run with: + * `config.providerOptions` (defaults already merged in) plus what the provider resolves + * natively for the ones nobody set. + */ + listControls?(config: AgentSessionConfig): Promise; /** Effective options: provider defaults from `config.json` deep-merged with the agent's own. */ effectiveOptions?(own: ProviderOptions | undefined): ProviderOptions; listImportableSessions?( diff --git a/packages/server/src/server/agent/provider-registry.test.ts b/packages/server/src/server/agent/provider-registry.test.ts index 04c7effc78..6621a92046 100644 --- a/packages/server/src/server/agent/provider-registry.test.ts +++ b/packages/server/src/server/agent/provider-registry.test.ts @@ -4,6 +4,7 @@ import type { ToolPolicy } from "@getpaseo/protocol/agent-types"; import { createTestLogger } from "../../test-utils/test-logger.js"; import type { AgentClient, + AgentDraftControls, AgentOptionControl, AgentModelDefinition, AgentMode, @@ -392,17 +393,20 @@ vi.mock("./providers/cursor-acp-agent.js", () => ({ return true; } - async listControls(config: AgentSessionConfig): Promise { + async listControls(config: AgentSessionConfig): Promise { mockState.cursorListFeaturesConfigs.push(config); - return [ - { - type: "select", - key: "fast", - label: "Fast", - defaultValue: "false", - choices: [{ value: "false", label: "Off" }], - }, - ]; + return { + options: config.providerOptions ?? {}, + controls: [ + { + type: "select", + key: "fast", + label: "Fast", + defaultValue: "false", + choices: [{ value: "false", label: "Off" }], + }, + ], + }; } }, })); @@ -868,15 +872,18 @@ test("wrapped cursor client lists ACP controls through the inner provider", asyn provider: "cursor", cwd: "/tmp/cursor", }), - ).resolves.toEqual([ - { - type: "select", - key: "fast", - label: "Fast", - defaultValue: "false", - choices: [{ value: "false", label: "Off" }], - }, - ]); + ).resolves.toEqual({ + controls: [ + { + type: "select", + key: "fast", + label: "Fast", + defaultValue: "false", + choices: [{ value: "false", label: "Off" }], + }, + ], + options: {}, + }); expect(mockState.cursorListFeaturesConfigs).toEqual([ { provider: "acp", diff --git a/packages/server/src/server/agent/providers/acp-agent.test.ts b/packages/server/src/server/agent/providers/acp-agent.test.ts index 4e6a9313c9..b94a6de940 100644 --- a/packages/server/src/server/agent/providers/acp-agent.test.ts +++ b/packages/server/src/server/agent/providers/acp-agent.test.ts @@ -2334,9 +2334,12 @@ describe("ACPAgentClient config features", () => { cwd: "/tmp/acp-features", providerOptions: { auto_accept: true }, }), - ).resolves.toEqual([ - expect.objectContaining({ type: "toggle", key: "auto_accept", label: "Auto Accept" }), - ]); + ).resolves.toEqual({ + controls: [ + expect.objectContaining({ type: "toggle", key: "auto_accept", label: "Auto Accept" }), + ], + options: { auto_accept: true }, + }); }); test("derives controls from configured ACP select options", async () => { @@ -2364,22 +2367,26 @@ describe("ACPAgentClient config features", () => { configFeatureOptions: [COPILOT_AGENT_FEATURE_OPTION], }); + // The select nobody set reads as the agent opened it. await expect( client.listControls({ provider: "copilot", cwd: "/tmp/acp-features", }), - ).resolves.toEqual([ - expect.objectContaining({ type: "toggle", key: "auto_accept" }), - expect.objectContaining({ - type: "select", - key: "agent", - choices: [ - expect.objectContaining({ value: "", label: "Default" }), - expect.objectContaining({ value: "Probe Agent", label: "Probe Agent" }), - ], - }), - ]); + ).resolves.toEqual({ + controls: [ + expect.objectContaining({ type: "toggle", key: "auto_accept" }), + expect.objectContaining({ + type: "select", + key: "agent", + choices: [ + expect.objectContaining({ value: "", label: "Default" }), + expect.objectContaining({ value: "Probe Agent", label: "Probe Agent" }), + ], + }), + ], + options: { agent: "Probe Agent" }, + }); }); }); diff --git a/packages/server/src/server/agent/providers/acp-agent.ts b/packages/server/src/server/agent/providers/acp-agent.ts index 384c6a8f24..33664f7ad8 100644 --- a/packages/server/src/server/agent/providers/acp-agent.ts +++ b/packages/server/src/server/agent/providers/acp-agent.ts @@ -70,6 +70,7 @@ import { type AgentCapabilityFlags, type AgentClient, type AgentCreateConfigUnattendedInput, + type AgentDraftControls, type AgentOptionControl, type AgentLaunchContext, type AgentMetadata, @@ -893,6 +894,19 @@ export function deriveControlsFromACP( }); } +/** The feature selects as the agent reports them, keyed by feature id. */ +function featureValuesFromACP( + configOptions: SessionConfigOption[] | null | undefined, + featureOptions: ACPConfigFeatureOption[], +): Record { + const values: Record = {}; + for (const featureOption of featureOptions) { + const current = findSelectConfigFeatureOption(configOptions, featureOption)?.currentValue; + if (current != null) values[featureOption.id] = current; + } + return values; +} + function isACPAutoAcceptEnabled(config: AgentSessionConfig): boolean { return config.providerOptions?.[ACP_AUTO_ACCEPT_FEATURE_ID] === true; } @@ -1209,9 +1223,10 @@ export class ACPAgentClient implements AgentClient { } } - async listControls(config: AgentSessionConfig): Promise { + async listControls(config: AgentSessionConfig): Promise { + const options = config.providerOptions ?? {}; if (this.configFeatureOptions.length === 0) { - return [ACP_AUTO_ACCEPT_CONTROL]; + return { controls: [ACP_AUTO_ACCEPT_CONTROL], options }; } this.assertProvider(config); @@ -1226,10 +1241,18 @@ export class ACPAgentClient implements AgentClient { ); probeSessionId = response.sessionId; const transformed = this.transformSessionResponse(response); - return [ - ACP_AUTO_ACCEPT_CONTROL, - ...deriveControlsFromACP(transformed.configOptions, this.configFeatureOptions), - ]; + return { + controls: [ + ACP_AUTO_ACCEPT_CONTROL, + ...deriveControlsFromACP(transformed.configOptions, this.configFeatureOptions), + ], + // The probe opened with the agent's own defaults, which a session would overwrite with + // the set values, so those stand over the probe's reading. + options: { + ...featureValuesFromACP(transformed.configOptions, this.configFeatureOptions), + ...options, + }, + }; } finally { await this.closeProbe(probe, probeSessionId); } @@ -2502,15 +2525,7 @@ export class ACPAgentSession implements AgentSession, ACPClient { /** The selects as the provider reports them now. */ private featureValues(): Record { - const values: Record = {}; - for (const featureOption of this.configFeatureOptions) { - const current = findSelectConfigFeatureOption( - this.configOptions, - featureOption, - )?.currentValue; - if (current != null) values[featureOption.id] = current; - } - return values; + return featureValuesFromACP(this.configOptions, this.configFeatureOptions); } /** The selects whose value is not what `expected` says, including ones it does not name. */ diff --git a/packages/server/src/server/agent/providers/claude/agent.test.ts b/packages/server/src/server/agent/providers/claude/agent.test.ts index 26265394eb..fd989e8cd1 100644 --- a/packages/server/src/server/agent/providers/claude/agent.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.test.ts @@ -1153,7 +1153,10 @@ describe("ClaudeAgentSession features", () => { cwd: process.cwd(), model: "claude-opus-4-8", }), - ).resolves.toEqual([expect.objectContaining({ key: "fast_mode", type: "toggle" })]); + ).resolves.toEqual({ + controls: [expect.objectContaining({ key: "fast_mode", type: "toggle" })], + options: {}, + }); await expect( client.listControls({ @@ -1161,7 +1164,10 @@ describe("ClaudeAgentSession features", () => { cwd: process.cwd(), model: "claude-opus-4-8[1m]", }), - ).resolves.toEqual([expect.objectContaining({ key: "fast_mode", type: "toggle" })]); + ).resolves.toEqual({ + controls: [expect.objectContaining({ key: "fast_mode", type: "toggle" })], + options: {}, + }); await expect( client.listControls({ @@ -1169,7 +1175,10 @@ describe("ClaudeAgentSession features", () => { cwd: process.cwd(), model: "claude-opus-4-8-20260101", }), - ).resolves.toEqual([expect.objectContaining({ key: "fast_mode", type: "toggle" })]); + ).resolves.toEqual({ + controls: [expect.objectContaining({ key: "fast_mode", type: "toggle" })], + options: {}, + }); await expect( client.listControls({ @@ -1177,7 +1186,10 @@ describe("ClaudeAgentSession features", () => { cwd: process.cwd(), model: "claude-opus-5", }), - ).resolves.toEqual([expect.objectContaining({ key: "fast_mode", type: "toggle" })]); + ).resolves.toEqual({ + controls: [expect.objectContaining({ key: "fast_mode", type: "toggle" })], + options: {}, + }); await expect( client.listControls({ @@ -1185,7 +1197,7 @@ describe("ClaudeAgentSession features", () => { cwd: process.cwd(), model: "openrouter/anthropic/claude-opus-4-8", }), - ).resolves.toEqual([]); + ).resolves.toEqual({ controls: [], options: {} }); await expect( client.listControls({ @@ -1193,7 +1205,7 @@ describe("ClaudeAgentSession features", () => { cwd: process.cwd(), model: "claude-sonnet-5", }), - ).resolves.toEqual([]); + ).resolves.toEqual({ controls: [], options: {} }); await expect( client.listControls({ @@ -1201,7 +1213,7 @@ describe("ClaudeAgentSession features", () => { cwd: process.cwd(), model: "claude-sonnet-4-6", }), - ).resolves.toEqual([]); + ).resolves.toEqual({ controls: [], options: {} }); }); test("opens with Fast off on a model that has none, so unrelated options still apply", async () => { diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts index cdcf80408a..d34f7c3707 100644 --- a/packages/server/src/server/agent/providers/claude/agent.ts +++ b/packages/server/src/server/agent/providers/claude/agent.ts @@ -106,6 +106,7 @@ import { type AgentCapabilityFlags, type AgentClient, type AgentCreateSessionOptions, + type AgentDraftControls, type AgentOptionControl, type AgentLaunchContext, type AgentMetadata, @@ -1760,8 +1761,11 @@ export class ClaudeAgentClient implements AgentClient { }); } - async listControls(config: AgentSessionConfig): Promise { - return buildClaudeControls(this.assertConfig(config).model); + async listControls(config: AgentSessionConfig): Promise { + return { + controls: buildClaudeControls(this.assertConfig(config).model), + options: config.providerOptions ?? {}, + }; } async listImportableSessions( diff --git a/packages/server/src/server/agent/providers/cursor-acp-agent.test.ts b/packages/server/src/server/agent/providers/cursor-acp-agent.test.ts index 300cc463cf..f858e965aa 100644 --- a/packages/server/src/server/agent/providers/cursor-acp-agent.test.ts +++ b/packages/server/src/server/agent/providers/cursor-acp-agent.test.ts @@ -167,28 +167,31 @@ describe("CursorACPAgentClient model discovery", () => { provider: "acp", cwd: "/tmp/cursor", }), - ).resolves.toEqual([ - { - type: "toggle", - key: "auto_accept", - label: "Auto Accept", - description: "Automatically approves ACP permission prompts.", - tooltip: "Auto accept permission prompts", - icon: "shield-check", - }, - { - type: "select", - key: CURSOR_FAST_FEATURE_OPTION.id, - label: "Fast", - description: "Cursor fast mode", - tooltip: "Select Cursor fast mode", - icon: "zap", - choices: [ - { value: "false", label: "Off" }, - { value: "true", label: "Fast" }, - ], - }, - ]); + ).resolves.toEqual({ + controls: [ + { + type: "toggle", + key: "auto_accept", + label: "Auto Accept", + description: "Automatically approves ACP permission prompts.", + tooltip: "Auto accept permission prompts", + icon: "shield-check", + }, + { + type: "select", + key: CURSOR_FAST_FEATURE_OPTION.id, + label: "Fast", + description: "Cursor fast mode", + tooltip: "Select Cursor fast mode", + icon: "zap", + choices: [ + { value: "false", label: "Off" }, + { value: "true", label: "Fast" }, + ], + }, + ], + options: { fast: "false" }, + }); }); }); diff --git a/packages/server/src/server/agent/providers/omp/agent.ts b/packages/server/src/server/agent/providers/omp/agent.ts index ddc79851e3..d10692ca6f 100644 --- a/packages/server/src/server/agent/providers/omp/agent.ts +++ b/packages/server/src/server/agent/providers/omp/agent.ts @@ -10,6 +10,7 @@ import stripAnsi from "strip-ansi"; import { type AgentCapabilityFlags, type AgentClient, + type AgentDraftControls, type AgentOptionControl, type AgentLaunchContext, type AgentMetadata, @@ -2584,8 +2585,8 @@ export class OmpAgentClient implements AgentClient { } } - async listControls(_config: AgentSessionConfig): Promise { - return []; + async listControls(config: AgentSessionConfig): Promise { + return { controls: [], options: config.providerOptions ?? {} }; } async listImportableSessions( diff --git a/packages/server/src/server/agent/providers/opencode-agent.full-access.test.ts b/packages/server/src/server/agent/providers/opencode-agent.full-access.test.ts index c96ce93676..e785695b77 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.full-access.test.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.full-access.test.ts @@ -115,15 +115,18 @@ describe("OpenCode auto_accept feature", () => { serverManager: runtime, createClient: runtime.createClient, }); - const controls = await client.listControls({ - provider: "opencode", - cwd: "/tmp/project", - providerOptions: { auto_accept: true }, + await expect( + client.listControls({ + provider: "opencode", + cwd: "/tmp/project", + providerOptions: { auto_accept: true }, + }), + ).resolves.toEqual({ + controls: [ + expect.objectContaining({ type: "toggle", key: "auto_accept", label: "Auto Accept" }), + ], + options: { auto_accept: true }, }); - - expect(controls).toEqual([ - expect.objectContaining({ type: "toggle", key: "auto_accept", label: "Auto Accept" }), - ]); }); test("keeps legacy full-access as an alias for build plus auto accept", async () => { diff --git a/packages/server/src/server/agent/providers/opencode-agent.ts b/packages/server/src/server/agent/providers/opencode-agent.ts index 13cd586f51..8549caeccf 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.ts @@ -27,6 +27,7 @@ import { type AgentCapabilityFlags, type AgentClient, type AgentCreateSessionOptions, + type AgentDraftControls, type AgentOptionControl, type AgentLaunchContext, type AgentMode, @@ -1686,9 +1687,9 @@ export class OpenCodeAgentClient implements AgentClient { } } - async listControls(config: AgentSessionConfig): Promise { + async listControls(config: AgentSessionConfig): Promise { this.assertConfig(config); - return [OPENCODE_AUTO_ACCEPT_CONTROL]; + return { controls: [OPENCODE_AUTO_ACCEPT_CONTROL], options: config.providerOptions ?? {} }; } async listImportableSessions( diff --git a/packages/server/src/server/agent/providers/opencode/runtime-client.test.ts b/packages/server/src/server/agent/providers/opencode/runtime-client.test.ts index 7b2ab8882b..910a68cfef 100644 --- a/packages/server/src/server/agent/providers/opencode/runtime-client.test.ts +++ b/packages/server/src/server/agent/providers/opencode/runtime-client.test.ts @@ -100,13 +100,13 @@ test.each([ }); try { await writeFile(script, initialSource); - expect((await client.listControls({ provider: "opencode", cwd: root }))[0]?.label).toBe( - "Auto Accept", - ); + expect( + (await client.listControls({ provider: "opencode", cwd: root })).controls[0]?.label, + ).toBe("Auto Accept"); await writeFile(script, 'console.log("opencode v2.0.10")'); - expect((await client.listControls({ provider: "opencode", cwd: root }))[0]?.label).toBe( - "Auto-accept", - ); + expect( + (await client.listControls({ provider: "opencode", cwd: root })).controls[0]?.label, + ).toBe("Auto-accept"); } finally { await client.shutdown(); await rm(root, { recursive: true, force: true }); @@ -221,7 +221,7 @@ test("selects v2 without probing the binary when an external server is configure env: { PASEO_OPENCODE_SERVER_URL: "http://127.0.0.1:4096" }, }); try { - const controls = await client.listControls({ provider: "opencode", cwd: "/tmp/project" }); + const { controls } = await client.listControls({ provider: "opencode", cwd: "/tmp/project" }); expect(controls.map((control) => control.label)).toEqual(["Auto-accept"]); } finally { await client.shutdown(); diff --git a/packages/server/src/server/agent/providers/opencode/v2/agent.ts b/packages/server/src/server/agent/providers/opencode/v2/agent.ts index 2953328c95..44b4647955 100644 --- a/packages/server/src/server/agent/providers/opencode/v2/agent.ts +++ b/packages/server/src/server/agent/providers/opencode/v2/agent.ts @@ -17,7 +17,7 @@ import type { AgentCapabilityFlags, AgentClient, AgentCreateSessionOptions, - AgentOptionControl, + AgentDraftControls, AgentLaunchContext, AgentPersistenceHandle, AgentSession, @@ -222,8 +222,8 @@ export class OpenCodeV2AgentClient implements AgentClient { throw error; } } - async listControls(): Promise { - return controls; + async listControls(config: AgentSessionConfig): Promise { + return { controls, options: config.providerOptions ?? {} }; } async listCommands(config: AgentSessionConfig) { const connection = await this.runtime.acquire(); diff --git a/packages/server/src/server/agent/providers/pi/agent.test.ts b/packages/server/src/server/agent/providers/pi/agent.test.ts index 891f519ea9..d152810a92 100644 --- a/packages/server/src/server/agent/providers/pi/agent.test.ts +++ b/packages/server/src/server/agent/providers/pi/agent.test.ts @@ -3084,7 +3084,7 @@ describe("PiRpcAgentClient", () => { await expect( client.listControls(createConfig({ model: "openrouter/test/model" })), - ).resolves.toEqual([]); + ).resolves.toEqual({ controls: [], options: {} }); expect(pi.recordedLaunches).toHaveLength(0); }); diff --git a/packages/server/src/server/agent/providers/pi/agent.ts b/packages/server/src/server/agent/providers/pi/agent.ts index 8ea483d7d2..60d745ea24 100644 --- a/packages/server/src/server/agent/providers/pi/agent.ts +++ b/packages/server/src/server/agent/providers/pi/agent.ts @@ -10,7 +10,7 @@ import { z } from "zod"; import { type AgentCapabilityFlags, type AgentClient, - type AgentOptionControl, + type AgentDraftControls, type AgentLaunchContext, type AgentMetadata, type AgentMode, @@ -2833,8 +2833,8 @@ export class PiRpcAgentClient implements AgentClient { } } - async listControls(_config: AgentSessionConfig): Promise { - return []; + async listControls(config: AgentSessionConfig): Promise { + return { controls: [], options: config.providerOptions ?? {} }; } async listImportableSessions(