Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions packages/cli/src/acp/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,15 +124,17 @@ export const make = Effect.fnUntraced(function* (client: OpenCodeClient) {
} satisfies Interface
})

const load = (client: OpenCodeClient, cwd: string) =>
read(client, cwd).pipe(
// Providers may still be discovering models after startup.
Effect.retry({
while: (error) => error._tag === "ACPCatalogNotReadyError",
schedule: Schedule.spaced("25 millis").pipe(Schedule.upTo({ duration: "5 seconds" })),
}),
Effect.withSpan("cli.acp.catalog.load"),
const poll = Schedule.spaced("25 millis").pipe(Schedule.upTo({ duration: "5 seconds" }))

// A cold Location lists no plugins until activation finishes, and providers may still discover models after that.
const load = Effect.fn("cli.acp.catalog.load")(function* (client: OpenCodeClient, cwd: string) {
yield* client.plugin
.list({ location: { directory: cwd } })
.pipe(Effect.repeat({ until: (plugins) => plugins.data.length > 0, schedule: poll }), Effect.ignore)
return yield* read(client, cwd).pipe(
Effect.retry({ while: (error) => error._tag === "ACPCatalogNotReadyError", schedule: poll }),
)
})

const read = Effect.fnUntraced(function* (client: OpenCodeClient, cwd: string) {
const location = { directory: cwd }
Expand Down
32 changes: 31 additions & 1 deletion packages/cli/test/acp/catalog.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test"
import { currentValue } from "./select-options"
import { rpcError, secondModel, startSession } from "./wire-fixture"
import { rpcError, secondModel, startSession, startWire } from "./wire-fixture"

describe("acp catalog and config options over the wire", () => {
test("switches model, effort, and mode against the warm catalog", async () => {
Expand Down Expand Up @@ -33,4 +33,34 @@ describe("acp catalog and config options over the wire", () => {
data: { modelId: "test/missing-model" },
})
})

test("answers the first session after a cold location activates its plugins (#52729, #52472)", async () => {
await using acp = await startWire()
const plugins = acp.server.catalog.plugins.splice(0)
acp.server.catalog.models.splice(1)
await acp.initialize()
const activated = Bun.sleep(200).then(() => {
acp.server.catalog.plugins.push(...plugins)
acp.server.catalog.models.push(secondModel)
})

const session = await acp.newSession()
const selected = await acp.request("session/set_config_option", {
sessionId: session.sessionId,
configId: "model",
value: "test/second-model",
})
await activated

expect(session.configOptions).toContainEqual(
expect.objectContaining({
id: "model",
options: [
{ value: "test/second-model", name: "test/Second Model" },
{ value: "test/test-model", name: "test/Test Model" },
],
}),
)
expect(currentValue(selected, "model")).toBe("test/second-model")
})
})
12 changes: 12 additions & 0 deletions packages/cli/test/acp/wire-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type { Command } from "@opencode/schema/command"
import { Form } from "@opencode/schema/form"
import type { Location } from "@opencode/schema/location"
import type { Model } from "@opencode/schema/model"
import type { Plugin } from "@opencode/schema/plugin"
import type { Session } from "@opencode/schema/session"
import type { SessionMessage } from "@opencode/schema/session-message"
import type { TokenUsage } from "@opencode/schema/token-usage"
Expand All @@ -40,6 +41,7 @@ type CommandInfo = typeof Command.Info.Encoded
type LocationRef = typeof Location.PublicRef.Encoded
type ModelInfo = typeof Model.Info.Encoded
type ModelRef = typeof Model.Ref.Encoded
type PluginInfo = typeof Plugin.Info.Encoded
type SessionInfo = typeof Session.Info.Encoded
type SessionMessageInfo = typeof SessionMessage.Info.Encoded
type TokenUsageInfo = typeof TokenUsage.Info.Encoded
Expand Down Expand Up @@ -155,6 +157,7 @@ type Catalog = {
models: ModelInfo[]
agents: AgentInfo[]
commands: CommandInfo[]
plugins: PluginInfo[]
}

export type InitializeOptions = {
Expand Down Expand Up @@ -559,6 +562,14 @@ function startServer(options: WireOptions, changed: () => void) {
models: [testModel, secondModel],
agents: [buildAgent, planAgent],
commands: [reviewCommand],
plugins: [
{
id: "opencode.models.dev",
source: { type: "builtin" },
features: { server: true },
state: { status: "active" },
},
],
}
const requests: ServerRequest[] = []
const submissions: Submission[] = []
Expand Down Expand Up @@ -697,6 +708,7 @@ function startServer(options: WireOptions, changed: () => void) {
"/api/model/default": { GET: catalogRoute(() => catalog.models[0] ?? null) },
"/api/agent": { GET: catalogRoute(() => catalog.agents) },
"/api/command": { GET: catalogRoute(() => catalog.commands) },
"/api/plugin": { GET: catalogRoute(() => catalog.plugins) },
"/api/session": {
GET: route((_req, query) => {
const sessions = [...fake.sessions.values()]
Expand Down
Loading