Skip to content

Commit 2ede0b9

Browse files
authored
Merge branch 'dev' into fix/permission-absolute-rules-not-matching-external-files
2 parents 5d2b43a + 7753211 commit 2ede0b9

11 files changed

Lines changed: 516 additions & 10 deletions

File tree

packages/opencode/src/acp-next/agent.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import {
88
type LoadSessionRequest,
99
type NewSessionRequest,
1010
type PromptRequest,
11+
type SetSessionConfigOptionRequest,
12+
type SetSessionModelRequest,
13+
type SetSessionModeRequest,
1114
} from "@agentclientprotocol/sdk"
1215
import { Effect } from "effect"
1316
import type { OpencodeClient } from "@opencode-ai/sdk/v2"
@@ -41,6 +44,18 @@ export class Agent implements ACPAgent {
4144
return run(this.service.loadSession(params))
4245
}
4346

47+
setSessionConfigOption(params: SetSessionConfigOptionRequest) {
48+
return run(this.service.setSessionConfigOption(params))
49+
}
50+
51+
setSessionMode(params: SetSessionModeRequest) {
52+
return run(this.service.setSessionMode(params))
53+
}
54+
55+
unstable_setSessionModel(params: SetSessionModelRequest) {
56+
return run(this.service.setSessionModel(params))
57+
}
58+
4459
prompt(params: PromptRequest) {
4560
return run(this.service.prompt(params))
4661
}

packages/opencode/src/acp-next/service.ts

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,18 @@ import {
1313
type NewSessionResponse,
1414
type PromptRequest,
1515
type PromptResponse,
16+
type SetSessionConfigOptionRequest,
17+
type SetSessionConfigOptionResponse,
18+
type SetSessionModelRequest,
19+
type SetSessionModelResponse,
20+
type SetSessionModeRequest,
21+
type SetSessionModeResponse,
1622
} from "@agentclientprotocol/sdk"
1723
import { InstallationVersion } from "@opencode-ai/core/installation/version"
1824
import type { OpencodeClient } from "@opencode-ai/sdk/v2"
1925
import { Context, Effect, Layer, ManagedRuntime } from "effect"
2026
import * as ACPNextError from "./error"
21-
import { buildConfigOptions } from "./config-option"
27+
import { buildConfigOptions, parseModelSelection } from "./config-option"
2228
import { Directory } from "./directory"
2329
import { ACPNextSession } from "./session"
2430
import { ModelID, ProviderID } from "@/provider/schema"
@@ -34,6 +40,11 @@ export type Interface = {
3440
readonly authenticate: (input: AuthenticateRequest) => Effect.Effect<AuthenticateResponse, Error>
3541
readonly newSession: (input: NewSessionRequest) => Effect.Effect<NewSessionResponse, Error>
3642
readonly loadSession: (input: LoadSessionRequest) => Effect.Effect<LoadSessionResponse, Error>
43+
readonly setSessionConfigOption: (
44+
input: SetSessionConfigOptionRequest,
45+
) => Effect.Effect<SetSessionConfigOptionResponse, Error>
46+
readonly setSessionMode: (input: SetSessionModeRequest) => Effect.Effect<SetSessionModeResponse, Error>
47+
readonly setSessionModel: (input: SetSessionModelRequest) => Effect.Effect<SetSessionModelResponse, Error>
3748
readonly prompt: (input: PromptRequest) => Effect.Effect<PromptResponse, Error>
3849
readonly cancel: (input: CancelNotification) => Effect.Effect<void, Error>
3950
}
@@ -180,11 +191,96 @@ export function make(input: {
180191
}
181192
})
182193

194+
const setSessionConfigOption = Effect.fn("ACPNext.setSessionConfigOption")(function* (
195+
params: SetSessionConfigOptionRequest,
196+
) {
197+
const current = yield* session.get(params.sessionId)
198+
const snapshot = yield* directorySnapshot(current.cwd)
199+
if (typeof params.value !== "string") {
200+
return yield* new ACPNextError.InvalidConfigOptionError({ configId: params.configId })
201+
}
202+
203+
if (params.configId === "model") {
204+
const selected = yield* parseSelectedModel(snapshot, params.value)
205+
const variant = selected.variant ?? selectVariant(snapshot, selected.model)
206+
const state = yield* session
207+
.setVariant(params.sessionId, Directory.variants(snapshot, selected.model) ? variant : undefined)
208+
.pipe(Effect.andThen(session.setModel(params.sessionId, selected.model)))
209+
return {
210+
configOptions: configOptions(snapshot, {
211+
model: state.model ?? selected.model,
212+
variant: state.variant,
213+
modeId: state.modeId,
214+
}),
215+
}
216+
}
217+
218+
if (params.configId === "effort") {
219+
const model = current.model ?? selectDefaultModel(snapshot)
220+
const variants = Directory.variants(snapshot, model)
221+
if (!variants || !Object.keys(variants).includes(params.value)) {
222+
return yield* new ACPNextError.InvalidEffortError({ effort: params.value })
223+
}
224+
const state = yield* session.setVariant(params.sessionId, params.value)
225+
return {
226+
configOptions: configOptions(snapshot, {
227+
model: state.model ?? model,
228+
variant: state.variant,
229+
modeId: state.modeId,
230+
}),
231+
}
232+
}
233+
234+
if (params.configId === "mode") {
235+
if (!snapshot.availableModes.some((mode) => mode.id === params.value)) {
236+
return yield* new ACPNextError.InvalidModeError({ mode: params.value })
237+
}
238+
const state = yield* session.setMode(params.sessionId, params.value)
239+
return {
240+
configOptions: configOptions(snapshot, {
241+
model: state.model ?? selectDefaultModel(snapshot),
242+
variant: state.variant,
243+
modeId: state.modeId,
244+
}),
245+
}
246+
}
247+
248+
return yield* new ACPNextError.InvalidConfigOptionError({ configId: params.configId })
249+
})
250+
251+
const setSessionMode = Effect.fn("ACPNext.setSessionMode")(function* (params: SetSessionModeRequest) {
252+
const current = yield* session.get(params.sessionId)
253+
const snapshot = yield* directorySnapshot(current.cwd)
254+
if (!snapshot.availableModes.some((mode) => mode.id === params.modeId)) {
255+
return yield* new ACPNextError.InvalidModeError({ mode: params.modeId })
256+
}
257+
yield* session.setMode(params.sessionId, params.modeId)
258+
return {}
259+
})
260+
261+
const setSessionModel = Effect.fn("ACPNext.setSessionModel")(function* (params: SetSessionModelRequest) {
262+
const current = yield* session.get(params.sessionId)
263+
const snapshot = yield* directorySnapshot(current.cwd)
264+
const selected = yield* parseSelectedModel(snapshot, params.modelId)
265+
yield* session
266+
.setVariant(
267+
params.sessionId,
268+
Directory.variants(snapshot, selected.model)
269+
? (selected.variant ?? selectVariant(snapshot, selected.model))
270+
: undefined,
271+
)
272+
.pipe(Effect.andThen(session.setModel(params.sessionId, selected.model)))
273+
return {}
274+
})
275+
183276
return {
184277
initialize,
185278
authenticate,
186279
newSession,
187280
loadSession,
281+
setSessionConfigOption,
282+
setSessionMode,
283+
setSessionModel,
188284
prompt: Effect.fn("ACPNext.prompt")(function* (_input: PromptRequest) {
189285
return yield* new ACPNextError.UnsupportedOperationError({ method: "session/prompt" })
190286
}),
@@ -371,6 +467,30 @@ function configOptions(snapshot: Directory.Snapshot, session: ConfigState) {
371467
})
372468
}
373469

470+
function parseSelectedModel(snapshot: Directory.Snapshot, modelId: string) {
471+
const selected = parseModelSelection(modelId, Object.values(snapshot.providers))
472+
const provider = snapshot.providers[ProviderID.make(selected.model.providerID)]
473+
const model = provider?.models[ModelID.make(selected.model.modelID)]
474+
if (!model) {
475+
return Effect.fail(
476+
new ACPNextError.InvalidModelError({
477+
providerId: selected.model.providerID,
478+
modelId,
479+
}),
480+
)
481+
}
482+
if (selected.variant && !model.variants?.[selected.variant]) {
483+
return Effect.fail(new ACPNextError.InvalidEffortError({ effort: selected.variant }))
484+
}
485+
return Effect.succeed({
486+
model: {
487+
providerID: provider.id,
488+
modelID: model.id,
489+
},
490+
variant: selected.variant,
491+
})
492+
}
493+
374494
function sendAvailableCommands(
375495
connection: Pick<AgentSideConnection, "sessionUpdate"> | undefined,
376496
sessionId: string,

packages/opencode/src/server/routes/instance/httpapi/groups/experimental.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Session } from "@/session/session"
55
import { Worktree } from "@/worktree"
66
import { NonNegativeInt } from "@opencode-ai/core/schema"
77
import { Schema } from "effect"
8-
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
8+
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
99
import { Authorization } from "../middleware/authorization"
1010
import { InstanceContextMiddleware } from "../middleware/instance-context"
1111
import {
@@ -168,7 +168,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
168168
HttpApiEndpoint.post("worktreeCreate", ExperimentalPaths.worktree, {
169169
disableCodecs: true,
170170
query: WorkspaceRoutingQuery,
171-
payload: Schema.UndefinedOr(Worktree.CreateInput),
171+
payload: [HttpApiSchema.NoContent, Worktree.CreateInput],
172172
success: described(Worktree.Info, "Worktree created"),
173173
error: WorktreeApiError,
174174
}).annotateMerge(

packages/opencode/src/server/routes/instance/httpapi/groups/global.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { BusEvent } from "@/bus/bus-event"
33
import { SyncEvent } from "@/sync"
44
import "@/server/event"
55
import { Schema } from "effect"
6-
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
6+
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
77
import { described } from "./metadata"
88

99
const GlobalHealth = Schema.Struct({
@@ -92,7 +92,7 @@ export const GlobalApi = HttpApi.make("global").add(
9292
}),
9393
),
9494
HttpApiEndpoint.post("upgrade", GlobalPaths.upgrade, {
95-
payload: GlobalUpgradeInput,
95+
payload: [HttpApiSchema.NoContent, GlobalUpgradeInput],
9696
success: described(GlobalUpgradeResult, "Upgrade result"),
9797
error: HttpApiError.BadRequest,
9898
}).annotateMerge(

packages/opencode/src/server/routes/instance/httpapi/groups/session.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ export const SessionApi = HttpApi.make("session")
236236
HttpApiEndpoint.post("fork", SessionPaths.fork, {
237237
params: { sessionID: SessionID },
238238
query: WorkspaceRoutingQuery,
239-
payload: Schema.optional(ForkPayload),
239+
payload: [HttpApiSchema.NoContent, ForkPayload],
240240
success: described(Session.Info, "200"),
241241
error: [HttpApiError.BadRequest, ApiNotFoundError],
242242
}).annotateMerge(

packages/opencode/src/server/routes/instance/httpapi/handlers/experimental.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,9 @@ export const experimentalHandlers = HttpApiBuilder.group(InstanceHttpApi, "exper
104104
})
105105

106106
const worktreeCreate = Effect.fn("ExperimentalHttpApi.worktreeCreate")(function* (ctx: {
107-
payload: Worktree.CreateInput | undefined
107+
payload: typeof Worktree.CreateInput.Type | void
108108
}) {
109-
return yield* mapWorktreeError(worktreeSvc.create(ctx.payload))
109+
return yield* mapWorktreeError(worktreeSvc.create(ctx.payload ?? undefined))
110110
})
111111

112112
const worktreeRemove = Effect.fn("ExperimentalHttpApi.worktreeRemove")(function* (input: {

0 commit comments

Comments
 (0)