Skip to content

Commit fad1fcc

Browse files
authored
Merge branch 'dev' into fix/settings-auto-accept-new-session
2 parents 298c74a + 95daf90 commit fad1fcc

7 files changed

Lines changed: 495 additions & 46 deletions

File tree

packages/opencode/src/acp/config-option.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,11 @@ export function buildEffortSelectOption(input: {
6161
description: "Available effort levels for this model",
6262
category: "thought_level",
6363
type: "select",
64-
currentValue: selectVariant(input.currentVariant, input.variants),
65-
options: input.variants.map((variant) => ({
64+
currentValue:
65+
input.currentVariant === DEFAULT_VARIANT_VALUE
66+
? DEFAULT_VARIANT_VALUE
67+
: selectVariant(input.currentVariant, input.variants),
68+
options: [...new Set([...input.variants, DEFAULT_VARIANT_VALUE])].map((variant) => ({
6669
value: variant,
6770
name: formatVariantName(variant),
6871
})),

packages/opencode/src/acp/event.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export class Subscription {
134134
sessionId: message.info.sessionID,
135135
update: {
136136
sessionUpdate,
137-
messageId: message.info.id,
137+
messageId: part.type === "reasoning" ? part.id : message.info.id,
138138
...chunk,
139139
},
140140
})
@@ -248,7 +248,7 @@ export class Subscription {
248248
sessionId: session.id,
249249
update: {
250250
sessionUpdate: "agent_thought_chunk",
251-
messageId: props.messageID,
251+
messageId: props.partID,
252252
content: {
253253
type: "text",
254254
text: props.delta,

packages/opencode/src/acp/service.ts

Lines changed: 157 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ import {
3131
} from "@agentclientprotocol/sdk"
3232
import { InstallationVersion } from "@opencode-ai/core/installation/version"
3333
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
34-
import type { AssistantMessage, Message, OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2"
34+
import type { AssistantMessage, Message, OpencodeClient, Session, SessionMessageResponse } from "@opencode-ai/sdk/v2"
3535
import { Context, Effect, Layer, ManagedRuntime } from "effect"
3636
import * as ACPError from "./error"
37-
import { buildConfigOptions, parseModelSelection } from "./config-option"
37+
import { buildConfigOptions, DEFAULT_VARIANT_VALUE, parseModelSelection } from "./config-option"
3838
import { promptContentToParts } from "./content"
3939
import { Directory } from "./directory"
4040
import { ACPEvent } from "./event"
@@ -210,23 +210,26 @@ export function make(input: {
210210

211211
const loadSession = Effect.fn("ACP.loadSession")(function* (params: LoadSessionRequest) {
212212
const snapshot = yield* directorySnapshot(params.cwd)
213-
yield* request(
213+
const backing = yield* request(
214214
() => input.sdk.session.get({ directory: params.cwd, sessionID: params.sessionId }, { throwOnError: true }),
215215
"session",
216216
)
217217
const messages = yield* request(
218218
() => input.sdk.session.messages({ directory: params.cwd, sessionID: params.sessionId }, { throwOnError: true }),
219219
"session",
220220
)
221-
const restored = restoreFromMessages(messages.map((item) => item.info))
222-
const model = restored.model ?? selectDefaultModel(snapshot)
221+
const restored = restoreSession(
222+
snapshot,
223+
backing,
224+
messages.map((item) => item.info),
225+
)
223226
const state = yield* session.load({
224227
id: params.sessionId,
225228
cwd: params.cwd,
226229
mcpServers: params.mcpServers,
227-
model,
228-
variant: restored.variant ?? selectVariant(snapshot, model),
229-
modeId: restored.modeId ?? (snapshot.availableModes.length > 0 ? snapshot.defaultModeID : undefined),
230+
model: restored.model,
231+
variant: restored.variant,
232+
modeId: restored.modeId,
230233
})
231234
sessionSnapshots.set(state.id, snapshot)
232235

@@ -236,7 +239,7 @@ export function make(input: {
236239

237240
return {
238241
configOptions: configOptions(snapshot, {
239-
model: state.model ?? model,
242+
model: state.model ?? restored.model,
240243
variant: state.variant,
241244
modeId: state.modeId,
242245
}),
@@ -291,7 +294,7 @@ export function make(input: {
291294

292295
const resumeSession = Effect.fn("ACP.resumeSession")(function* (params: ResumeSessionRequest) {
293296
const snapshot = yield* directorySnapshot(params.cwd)
294-
yield* request(
297+
const backing = yield* request(
295298
() => input.sdk.session.get({ directory: params.cwd, sessionID: params.sessionId }, { throwOnError: true }),
296299
"session",
297300
)
@@ -303,15 +306,18 @@ export function make(input: {
303306
),
304307
"session",
305308
)
306-
const restored = restoreFromMessages(messages.map((item) => item.info))
307-
const model = restored.model ?? selectDefaultModel(snapshot)
309+
const restored = restoreSession(
310+
snapshot,
311+
backing,
312+
messages.map((item) => item.info),
313+
)
308314
const state = yield* session.load({
309315
id: params.sessionId,
310316
cwd: params.cwd,
311317
mcpServers: params.mcpServers ?? [],
312-
model,
313-
variant: restored.variant ?? selectVariant(snapshot, model),
314-
modeId: restored.modeId ?? (snapshot.availableModes.length > 0 ? snapshot.defaultModeID : undefined),
318+
model: restored.model,
319+
variant: restored.variant,
320+
modeId: restored.modeId,
315321
})
316322
sessionSnapshots.set(state.id, snapshot)
317323

@@ -320,7 +326,7 @@ export function make(input: {
320326

321327
return {
322328
configOptions: configOptions(snapshot, {
323-
model: state.model ?? model,
329+
model: state.model ?? restored.model,
324330
variant: state.variant,
325331
modeId: state.modeId,
326332
}),
@@ -371,15 +377,18 @@ export function make(input: {
371377
input.sdk.session.messages({ directory: params.cwd, sessionID: forked.id, limit: 20 }, { throwOnError: true }),
372378
"session",
373379
)
374-
const restored = restoreFromMessages(messages.map((item) => item.info))
375-
const model = restored.model ?? selectDefaultModel(snapshot)
380+
const restored = restoreSession(
381+
snapshot,
382+
forked,
383+
messages.map((item) => item.info),
384+
)
376385
const state = yield* session.load({
377386
id: forked.id,
378387
cwd: params.cwd,
379388
mcpServers: params.mcpServers ?? [],
380-
model,
381-
variant: restored.variant ?? selectVariant(snapshot, model),
382-
modeId: restored.modeId ?? (snapshot.availableModes.length > 0 ? snapshot.defaultModeID : undefined),
389+
model: restored.model,
390+
variant: restored.variant,
391+
modeId: restored.modeId,
383392
})
384393
sessionSnapshots.set(state.id, snapshot)
385394

@@ -390,7 +399,7 @@ export function make(input: {
390399
return {
391400
sessionId: state.id,
392401
configOptions: configOptions(snapshot, {
393-
model: state.model ?? model,
402+
model: state.model ?? restored.model,
394403
variant: state.variant,
395404
modeId: state.modeId,
396405
}),
@@ -408,23 +417,25 @@ export function make(input: {
408417

409418
if (params.configId === "model") {
410419
const selected = yield* parseSelectedModel(snapshot, params.value)
411-
const variant = selected.variant ?? selectVariant(snapshot, selected.model)
420+
const variant = selectModelVariant(snapshot, current, selected)
412421
const state = yield* session
413422
.setVariant(params.sessionId, Directory.variants(snapshot, selected.model) ? variant : undefined)
414423
.pipe(Effect.andThen(session.setModel(params.sessionId, selected.model)))
424+
const options = configOptions(snapshot, {
425+
model: state.model ?? selected.model,
426+
variant: state.variant,
427+
modeId: state.modeId,
428+
})
429+
yield* sendConfigOptionUpdate(input.connection, params.sessionId, options)
415430
return {
416-
configOptions: configOptions(snapshot, {
417-
model: state.model ?? selected.model,
418-
variant: state.variant,
419-
modeId: state.modeId,
420-
}),
431+
configOptions: options,
421432
}
422433
}
423434

424435
if (params.configId === "effort") {
425436
const model = current.model ?? selectDefaultModel(snapshot)
426437
const variants = Directory.variants(snapshot, model)
427-
if (!variants || !Object.keys(variants).includes(params.value)) {
438+
if (!variants || !hasVariant(variants, params.value)) {
428439
return yield* new ACPError.InvalidEffortError({ effort: params.value })
429440
}
430441
const state = yield* session.setVariant(params.sessionId, params.value)
@@ -468,14 +479,18 @@ export function make(input: {
468479
const current = yield* session.get(params.sessionId)
469480
const snapshot = yield* configSnapshot(current)
470481
const selected = yield* parseSelectedModel(snapshot, params.modelId)
471-
yield* session
472-
.setVariant(
473-
params.sessionId,
474-
Directory.variants(snapshot, selected.model)
475-
? (selected.variant ?? selectVariant(snapshot, selected.model))
476-
: undefined,
477-
)
482+
const state = yield* session
483+
.setVariant(params.sessionId, selectModelVariant(snapshot, current, selected))
478484
.pipe(Effect.andThen(session.setModel(params.sessionId, selected.model)))
485+
yield* sendConfigOptionUpdate(
486+
input.connection,
487+
params.sessionId,
488+
configOptions(snapshot, {
489+
model: state.model ?? selected.model,
490+
variant: state.variant,
491+
modeId: state.modeId,
492+
}),
493+
)
479494
return {}
480495
})
481496

@@ -899,6 +914,24 @@ function selectVariant(snapshot: Directory.Snapshot, model: Directory.DefaultMod
899914
return Object.keys(variants)[0]
900915
}
901916

917+
function selectModelVariant(
918+
snapshot: Directory.Snapshot,
919+
current: ACPSession.Info,
920+
selected: { model: Directory.DefaultModel; variant?: string },
921+
) {
922+
const variants = Directory.variants(snapshot, selected.model)
923+
if (!variants) return
924+
if (selected.variant) return selected.variant
925+
if (sameModel(selected.model, current.model) && current.variant && hasVariant(variants, current.variant))
926+
return current.variant
927+
return selectVariant(snapshot, selected.model)
928+
}
929+
930+
function hasVariant(variants: Directory.ModelVariants, variant: string) {
931+
// "default" is also the persisted sentinel for no explicit variant override.
932+
return variant === DEFAULT_VARIANT_VALUE || Object.hasOwn(variants, variant)
933+
}
934+
902935
function configOptions(snapshot: Directory.Snapshot, session: ConfigState) {
903936
return buildConfigOptions({
904937
providers: Object.values(snapshot.providers),
@@ -909,6 +942,25 @@ function configOptions(snapshot: Directory.Snapshot, session: ConfigState) {
909942
})
910943
}
911944

945+
function sendConfigOptionUpdate(
946+
connection: ServiceConnection | undefined,
947+
sessionId: string,
948+
options: ReturnType<typeof configOptions>,
949+
) {
950+
if (!connection) return Effect.void
951+
return Effect.tryPromise({
952+
try: () =>
953+
connection.sessionUpdate({
954+
sessionId,
955+
update: {
956+
sessionUpdate: "config_option_update",
957+
configOptions: options,
958+
},
959+
}),
960+
catch: () => undefined,
961+
}).pipe(Effect.ignore)
962+
}
963+
912964
function parseSelectedModel(snapshot: Directory.Snapshot, modelId: string) {
913965
const selected = parseModelSelection(modelId, Object.values(snapshot.providers))
914966
const provider = snapshot.providers[ProviderV2.ID.make(selected.model.providerID)]
@@ -1034,6 +1086,75 @@ function stableStringify(value: unknown): string {
10341086
.join(",")}}`
10351087
}
10361088

1089+
function restoreSession(
1090+
snapshot: Directory.Snapshot,
1091+
backing: Pick<Session, "agent" | "model">,
1092+
messages: MessageInfo[],
1093+
) {
1094+
const history = restoreFromMessages(messages)
1095+
const durable = restoreDurableModel(backing.model)
1096+
const model = restoreModel(snapshot, durable.model, history.model)
1097+
return {
1098+
model,
1099+
variant: restoreVariant(snapshot, model, durable, history),
1100+
modeId: restoreMode(snapshot, backing.agent, history.modeId),
1101+
}
1102+
}
1103+
1104+
function restoreDurableModel(model: Session["model"] | undefined) {
1105+
if (!model) return {}
1106+
return {
1107+
model: {
1108+
providerID: ProviderV2.ID.make(model.providerID),
1109+
modelID: ModelV2.ID.make(model.id),
1110+
},
1111+
variant: model.variant,
1112+
}
1113+
}
1114+
1115+
function restoreModel(
1116+
snapshot: Directory.Snapshot,
1117+
durable: Directory.DefaultModel | undefined,
1118+
history: Directory.DefaultModel | undefined,
1119+
) {
1120+
if (durable && hasModel(snapshot, durable)) return durable
1121+
if (history && hasModel(snapshot, history)) return history
1122+
return selectDefaultModel(snapshot)
1123+
}
1124+
1125+
function restoreVariant(
1126+
snapshot: Directory.Snapshot,
1127+
model: Directory.DefaultModel,
1128+
durable: { model?: Directory.DefaultModel; variant?: string },
1129+
history: { model?: Directory.DefaultModel; variant?: string },
1130+
) {
1131+
const variants = Directory.variants(snapshot, model)
1132+
if (!variants) return
1133+
if (sameModel(model, durable.model) && durable.variant && hasVariant(variants, durable.variant))
1134+
return durable.variant
1135+
if (sameModel(model, history.model) && history.variant && hasVariant(variants, history.variant))
1136+
return history.variant
1137+
return selectVariant(snapshot, model)
1138+
}
1139+
1140+
function restoreMode(snapshot: Directory.Snapshot, durable: string | undefined, history: string | undefined) {
1141+
if (hasMode(snapshot, durable)) return durable
1142+
if (hasMode(snapshot, history)) return history
1143+
if (snapshot.availableModes.length > 0) return snapshot.defaultModeID
1144+
}
1145+
1146+
function hasModel(snapshot: Directory.Snapshot, model: Directory.DefaultModel) {
1147+
return Boolean(snapshot.providers[model.providerID]?.models[model.modelID])
1148+
}
1149+
1150+
function hasMode(snapshot: Directory.Snapshot, modeId: string | undefined) {
1151+
return Boolean(modeId && snapshot.availableModes.some((mode) => mode.id === modeId))
1152+
}
1153+
1154+
function sameModel(left: Directory.DefaultModel, right: Directory.DefaultModel | undefined) {
1155+
return left.providerID === right?.providerID && left.modelID === right.modelID
1156+
}
1157+
10371158
function restoreFromMessages(messages: readonly MessageInfo[]) {
10381159
const user = messages.findLast(
10391160
(message) => message.role === "user" && message.model?.providerID && message.model.modelID,

packages/opencode/test/acp/config-option.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,17 @@ describe("acp config options", () => {
114114
expect(buildEffortSelectOption({ variants: [] })).toBeUndefined()
115115
})
116116

117+
test("exposes an explicit default even when the provider only lists named variants", () => {
118+
expect(buildEffortSelectOption({ variants: ["low", "medium"], currentVariant: "default" })).toMatchObject({
119+
currentValue: "default",
120+
options: [
121+
{ value: "low", name: "Low" },
122+
{ value: "medium", name: "Medium" },
123+
{ value: "default", name: "Default" },
124+
],
125+
})
126+
})
127+
117128
test("builds the mode select option with descriptions when present", () => {
118129
expect(
119130
buildModeSelectOption({

0 commit comments

Comments
 (0)