diff --git a/V2_HTTP_API_AUDIT.md b/V2_HTTP_API_AUDIT.md index 1f3e26fd06b..ba1d3bab223 100644 --- a/V2_HTTP_API_AUDIT.md +++ b/V2_HTTP_API_AUDIT.md @@ -97,9 +97,9 @@ Review endpoints in document order. For each endpoint, select one disposition an | [x] 018 | `GET` | `/api/command` | `command.list` | Keep | Request and response accepted as-is. | | [x] 019 | `GET` | `/api/skill` | `skill.list` | Keep | Renamed `location` to `path`; removed the skill-specific `slash` flag and slash-command behavior. | | [x] 020 | `GET` | `/api/reference` | `reference.list` | Keep | Removed duplicate `description` and `hidden` fields from nested `source`. | -| [ ] 021 | `GET` | `/api/config` | `config.get` | Change | Removed `claude` and `agents` entry variants; one compatibility plugin now normalizes their skills. | -| [ ] 022 | `GET` | `/api/config/preferences` | `config.preferences` | | | -| [ ] 023 | `PATCH` | `/api/config/preferences` | `config.updatePreferences` | | | +| [x] 021 | `GET` | `/api/config` | `config.get` | Keep | Compatibility entries removed; response now contains only documents and OpenCode directories. | +| [x] 022 | `GET` | `/api/config/preferences` | `config.preferences` | Remove | Redundant special projection of global config. | +| [x] 023 | `PATCH` | `/api/config/preferences` | `config.updatePreferences` | Remove | Redundant field-specific config mutation API. | | [ ] 024 | `GET` | `/api/config/shell` | `config.shells` | | | ## Group 3: Credentials, integrations, MCP, and web search diff --git a/packages/app/e2e/regression/remote-session-settings.spec.ts b/packages/app/e2e/regression/remote-session-settings.spec.ts index 14e1617589a..316e1d3edf2 100644 --- a/packages/app/e2e/regression/remote-session-settings.spec.ts +++ b/packages/app/e2e/regression/remote-session-settings.spec.ts @@ -20,7 +20,6 @@ test("session settings use the remote server context", async ({ page }) => { // one toggle sweeps every connected server, not just the focused one. await mockServers(page, permissionRequests, permissionResponses, { pending: { [serverA]: [pendingPermission("permission-pending-a", sessionA.id)] }, - preferencesUnavailable: true, }) await configureServers(page) @@ -326,7 +325,6 @@ type MockServerOptions = { listFailures?: Record // Records /api/session/:id GETs so tests can assert session resyncs. sessionGets?: string[] - preferencesUnavailable?: boolean } async function mockServers( @@ -416,11 +414,8 @@ async function mockServers( directory, project: { id: remote ? sessionB.projectID : "project-server-a", directory, canonical: directory }, }) - if (url.pathname === "/api/config/preferences") return json(route, {}, options.preferencesUnavailable ? 404 : 200) - if (url.pathname === "/api/config/shell") - return json(route, options.preferencesUnavailable ? {} : [], options.preferencesUnavailable ? 404 : 200) - if (url.pathname === "/api/websearch/provider") - return json(route, { location: { directory }, data: [] }, options.preferencesUnavailable ? 404 : 200) + if (url.pathname === "/api/config/shell") return json(route, []) + if (url.pathname === "/api/websearch/provider") return json(route, { location: { directory }, data: [] }) if (url.pathname === "/api/worktree") return json(route, [{ directory }]) if (url.pathname === "/api/vcs") return json(route, { location: { directory }, data: { branch: "main", defaultBranch: "main" } }) diff --git a/packages/app/e2e/regression/settings-loading.spec.ts b/packages/app/e2e/regression/settings-loading.spec.ts index 37212b5071f..498a4bef8f6 100644 --- a/packages/app/e2e/regression/settings-loading.spec.ts +++ b/packages/app/e2e/regression/settings-loading.spec.ts @@ -24,9 +24,6 @@ test.beforeEach(async ({ page }) => { sandboxes, }, provider: { all: [], connected: [], default: {} }, - preferences: { shell: "zsh", websearch: { provider: "exa" } }, - shells: [{ path: "/bin/zsh", name: "zsh", acceptable: true }], - websearchProviders: [{ id: "exa", name: "Exa" }], sessions: sandboxes.map((directory, index) => ({ id: `ses_settings_${index + 1}`, title: `Workspace ${index + 1} session`, @@ -78,38 +75,12 @@ test("single-server settings expose scoped pages without a server picker", async await settings.getByRole("tab", { name: "Server", exact: true }).click() await expect(settings.getByRole("button", { name: "Add server", exact: true })).toBeVisible() await expect(settings.getByRole("heading", { name: "Connection", exact: true })).toBeVisible() - await expect(settings.getByRole("heading", { name: "Preferences", exact: true })).toBeVisible() - await expect(settings.getByText("Terminal shell", { exact: true })).toBeVisible() - await expect(settings.getByText("Third-party search", { exact: true })).toBeVisible() - await expect(settings.getByText("zsh", { exact: true })).toBeVisible() - await expect(settings.getByText("Exa", { exact: true })).toBeVisible() - - await settings.getByText("Exa", { exact: true }).click() - const updated = page.waitForRequest( - (request) => request.method() === "PATCH" && new URL(request.url()).pathname === "/api/config/preferences", - ) - await page.getByRole("option", { name: "Any", exact: true }).click() - expect((await updated).postDataJSON()).toEqual({ websearch: { provider: "random" } }) -}) - -test("server details tolerate unavailable preference endpoints", async ({ page }) => { - await page.route( - (url) => - url.pathname === "/api/config/preferences" || - url.pathname === "/api/config/shell" || - url.pathname === "/api/websearch/provider", - (route) => route.fulfill({ status: 404, json: {} }), - ) - const settings = page.getByTestId("settings-screen") - await settings.getByRole("tab", { name: "Server", exact: true }).click() - const connection = settings.locator('[data-component="settings-server-connection"]') await expect(connection.getByRole("heading", { name: "Connection", exact: true })).toBeVisible() await expect(connection.locator('[data-component="settings-list"]')).toHaveCSS("padding-left", "16px") await expect(connection.locator(".settings-servers-row")).toHaveCSS("padding-top", "20px") await expect(connection.locator(".settings-servers-lead")).toHaveCSS("column-gap", "4px") await expect(connection.locator(".settings-servers-copy")).toHaveCSS("row-gap", "6px") - await expect(page.getByText("Server request failed", { exact: true })).toHaveCount(0) }) test("project settings open as a nested autosaving view", async ({ page }) => { diff --git a/packages/app/e2e/utils/mock-api.ts b/packages/app/e2e/utils/mock-api.ts index a12b62b6d4e..4a3a3cc6481 100644 --- a/packages/app/e2e/utils/mock-api.ts +++ b/packages/app/e2e/utils/mock-api.ts @@ -77,13 +77,6 @@ const Group = HttpApiGroup.make("mock") success: Json, }), ) - .add(HttpApiEndpoint.get("configPreferences", "/api/config/preferences", { success: Json })) - .add( - HttpApiEndpoint.patch("configUpdatePreferences", "/api/config/preferences", { - payload: JsonPayload, - success: Json, - }), - ) .add(HttpApiEndpoint.get("configShells", "/api/config/shell", { success: Json })) .add(HttpApiEndpoint.get("websearchProviders", "/api/websearch/provider", { success: Json })) .add( diff --git a/packages/app/e2e/utils/mock-server.ts b/packages/app/e2e/utils/mock-server.ts index b844fd550c8..9254a959753 100644 --- a/packages/app/e2e/utils/mock-server.ts +++ b/packages/app/e2e/utils/mock-server.ts @@ -10,7 +10,6 @@ export interface MockServerConfig { provider: unknown | (() => unknown) integrationMethods?: Record onConnectKey?: (input: { integrationID: string; body: unknown }) => void - preferences?: Record shells?: unknown[] websearchProviders?: unknown[] directory: string @@ -198,7 +197,6 @@ const corsHeaders = { function mockHandlers(config: MockServerConfig, state: { cursors: Map; nextCursor: number }) { const noContent = Effect.succeed(HttpApiSchema.NoContent.make()) const delay = config.messageDelay === undefined ? Effect.void : Effect.sleep(Duration.millis(config.messageDelay)) - const preferences = { current: config.preferences ?? {} } return HttpApiBuilder.group(MockApi, "mock", (handlers) => handlers .handleRaw("event", () => { @@ -285,12 +283,6 @@ function mockHandlers(config: MockServerConfig, state: { cursors: Map Effect.succeed(preferences.current), - configUpdatePreferences: (ctx) => - Effect.sync(() => { - preferences.current = { ...preferences.current, ...ctx.payload } - return preferences.current - }), configShells: () => Effect.succeed(config.shells ?? []), websearchProviders: () => Effect.succeed({ location: location(config), data: config.websearchProviders ?? [] }), worktreeList: () => diff --git a/packages/app/src/settings/general/controllers.ts b/packages/app/src/settings/general/controllers.ts index 9538f2b38f0..5f484f9f850 100644 --- a/packages/app/src/settings/general/controllers.ts +++ b/packages/app/src/settings/general/controllers.ts @@ -1,5 +1,4 @@ -import { createMemo, createResource, onMount, type Accessor } from "solid-js" -import type { ConfigPreferences, ConfigUpdatePreferencesInput } from "@opencode/client/promise" +import { createMemo, onMount, type Accessor } from "solid-js" import type { ColorScheme } from "@opencode/ui/theme/context" import { useTheme } from "@opencode/ui/theme/context" import { @@ -16,103 +15,10 @@ import { } from "@/settings/model" import { playSoundById, SOUND_OPTIONS } from "@/shell/notifications/sound" import { createSoundPreviewController } from "./behavior" -import { ServerConnection } from "@/runtime/server/registry" -import { useServerCtx } from "@/runtime/server/runtime" -import { useLanguage } from "@/runtime/i18n/language" -import { showToast } from "@/shell/notifications/toast" export { createShellOptions, createSoundPreviewController } from "./behavior" export type { ShellOption, ShellSelectOption } from "./behavior" -export function createServerPreferencesController(server: Accessor) { - const language = useLanguage() - const serverCtx = useServerCtx(server) - const source = () => ServerConnection.key(server()) - const [preferences, preferencesActions] = createResource( - source, - () => - serverCtx() - .sdk.api.config.preferences() - .catch(() => ({})), - { initialValue: {} }, - ) - const [shells] = createResource( - source, - () => - serverCtx() - .sdk.api.config.shells() - .catch(() => []), - { initialValue: [] }, - ) - const [providers] = createResource( - source, - () => - serverCtx() - .sdk.api.websearch.providers() - .then((result) => result.data) - .catch(() => []), - { initialValue: [] }, - ) - - const update = async (patch: ConfigUpdatePreferencesInput) => { - const context = serverCtx() - const previous = preferences.latest - preferencesActions.mutate({ - ...previous, - ...(patch.shell === undefined ? {} : { shell: patch.shell ?? undefined }), - ...(patch.websearch === undefined ? {} : { websearch: patch.websearch ?? undefined }), - }) - await context.sdk.api.config - .updatePreferences(patch) - .then(preferencesActions.mutate) - .catch((error: unknown) => { - preferencesActions.mutate(previous) - showToast({ - variant: "error", - title: language.t("common.requestFailed"), - description: error instanceof Error ? error.message : language.t("common.requestFailed"), - }) - }) - } - - const websearchOptions = createMemo(() => { - const options = providers.latest.map((provider) => ({ value: provider.id, label: provider.name })) - const selected = preferences.latest.websearch - const configured = selected && selected.provider !== "random" ? selected.provider : undefined - return [ - { value: "random" as const, label: language.t("session.websearch.any") }, - ...options, - ...(configured && !options.some((option) => option.value === configured) - ? [{ value: configured, label: configured }] - : []), - { value: false as const, label: language.t("session.websearch.disable") }, - ] - }) - const websearchCurrent = createMemo(() => { - const selection = preferences.latest.websearch - const value = selection === false ? false : (selection?.provider ?? "random") - return websearchOptions().find((option) => option.value === value) ?? websearchOptions()[0] - }) - - return { - shell: { - shells: () => shells.latest, - current: () => preferences.latest.shell ?? "", - select: (value: string) => { - if (value === (preferences.latest.shell ?? "")) return - void update({ shell: value || null }) - }, - }, - websearch: { - options: websearchOptions, - current: websearchCurrent, - select: (value: string | false) => { - void update({ websearch: value === false ? false : { provider: value } }) - }, - }, - } -} - export function createAppearanceSettingsController() { const settings = useSettings() const theme = useTheme() @@ -208,6 +114,5 @@ export function createSoundSettingsController() { } } -export type ShellSettingsController = ReturnType["shell"] export type AppearanceSettingsController = ReturnType export type SoundSettingsController = ReturnType diff --git a/packages/app/src/settings/general/general.tsx b/packages/app/src/settings/general/general.tsx index 10b2aa001a1..53114fdebb9 100644 --- a/packages/app/src/settings/general/general.tsx +++ b/packages/app/src/settings/general/general.tsx @@ -18,12 +18,7 @@ import { formatKeybind } from "@/shell/commands/command" import { ExternalLink } from "@/runtime/platform/external-link" import { SettingsList } from "@/settings/list" import { SettingsRow } from "@/settings/row" -import { - createAppearanceSettingsController, - createShellOptions, - type AppearanceSettingsController, - type ShellSettingsController, -} from "./controllers" +import { createAppearanceSettingsController, type AppearanceSettingsController } from "./controllers" import "@/settings/settings.css" const schemeOptions: ("system" | "light" | "dark")[] = ["system", "light", "dark"] @@ -96,37 +91,6 @@ const WorkspaceDestinationSetting: Component = () => { ) } -export const ShellSetting: Component<{ controller: ShellSettingsController }> = (props) => { - const language = useLanguage() - const options = createMemo(() => - createShellOptions({ - shells: props.controller.shells(), - current: props.controller.current(), - }), - ) - return ( - - String(option.value)} - label={(option) => option.label} - placement="bottom-end" - gutter={6} - onSelect={(option) => option && props.controller.select(option.value)} - /> - - ) -} - export const SettingsServerGeneral: Component<{ entry: SettingsServer nested?: boolean @@ -116,25 +89,7 @@ export const SettingsServerGeneral: Component<{ - - - {(server) => } - ) } - -function ServerPreferences(props: { server: ServerConnection.Any }) { - const language = useLanguage() - const preferences = createServerPreferencesController(() => props.server) - return ( -
-

{language.t("settings.tab.preferences")}

- - - - -
- ) -} diff --git a/packages/client/src/effect/api/api.ts b/packages/client/src/effect/api/api.ts index ae0dc2a692f..4b52d37edfd 100644 --- a/packages/client/src/effect/api/api.ts +++ b/packages/client/src/effect/api/api.ts @@ -2307,18 +2307,6 @@ export type ConfigGetInput = { readonly location?: { readonly directory?: string export type ConfigGetOutput = ReadonlyArray export type ConfigGetOperation = (input?: ConfigGetInput) => Effect.Effect -export type ConfigPreferencesOutput = Config.Preferences -export type ConfigPreferencesOperation = () => Effect.Effect - -export type ConfigUpdatePreferencesInput = { - readonly shell?: string | null | undefined - readonly websearch?: false | { readonly provider: "random" | WebSearch.ID } | null | undefined -} -export type ConfigUpdatePreferencesOutput = Config.Preferences -export type ConfigUpdatePreferencesOperation = ( - input?: ConfigUpdatePreferencesInput, -) => Effect.Effect - export type ConfigShellsOutput = ReadonlyArray<{ readonly path: string readonly name: string @@ -2328,8 +2316,6 @@ export type ConfigShellsOperation = () => Effect.Effect { readonly get: ConfigGetOperation - readonly preferences: ConfigPreferencesOperation - readonly updatePreferences: ConfigUpdatePreferencesOperation readonly shells: ConfigShellsOperation } diff --git a/packages/client/src/effect/generated/client.ts b/packages/client/src/effect/generated/client.ts index c09ed0a7a56..d50aad4ae85 100644 --- a/packages/client/src/effect/generated/client.ts +++ b/packages/client/src/effect/generated/client.ts @@ -263,9 +263,6 @@ import type { WebsearchQueryOutput, ConfigGetInput, ConfigGetOutput, - ConfigPreferencesOutput, - ConfigUpdatePreferencesInput, - ConfigUpdatePreferencesOutput, ConfigShellsOutput, } from "../api/api.js" import { ClientError } from "./client-error.js" @@ -1556,23 +1553,11 @@ const EndpointConfigGet = (raw: RawClient["server.config"]) => (input?: ConfigGe raw["config.get"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), ) -const EndpointConfigPreferences = (raw: RawClient["server.config"]) => () => - preserveEffect()(raw["config.preferences"]({}).pipe(Effect.mapError(mapClientError))) - -const EndpointConfigUpdatePreferences = (raw: RawClient["server.config"]) => (input?: ConfigUpdatePreferencesInput) => - preserveEffect()( - raw["config.updatePreferences"]({ payload: { shell: input?.["shell"], websearch: input?.["websearch"] } }).pipe( - Effect.mapError(mapClientError), - ), - ) - const EndpointConfigShells = (raw: RawClient["server.config"]) => () => preserveEffect()(raw["config.shells"]({}).pipe(Effect.mapError(mapClientError))) const adaptGroupConfig = (raw: RawClient["server.config"]) => ({ get: EndpointConfigGet(raw), - preferences: EndpointConfigPreferences(raw), - updatePreferences: EndpointConfigUpdatePreferences(raw), shells: EndpointConfigShells(raw), }) diff --git a/packages/client/src/promise/generated/client.ts b/packages/client/src/promise/generated/client.ts index 63c07025ffa..663cff7e504 100644 --- a/packages/client/src/promise/generated/client.ts +++ b/packages/client/src/promise/generated/client.ts @@ -259,9 +259,6 @@ import type { WebsearchQueryOutput, ConfigGetInput, ConfigGetOutput, - ConfigPreferencesOutput, - ConfigUpdatePreferencesInput, - ConfigUpdatePreferencesOutput, ConfigShellsOutput, } from "./types.js" import { ClientError } from "./client-error.js" @@ -2155,29 +2152,6 @@ export function make(options: ClientOptions) { }, requestOptions, ), - preferences: (requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/config/preferences`, - successStatus: 200, - declaredStatuses: [400, 401], - empty: false, - }, - requestOptions, - ), - updatePreferences: (input?: ConfigUpdatePreferencesInput, requestOptions?: RequestOptions) => - request( - { - method: "PATCH", - path: `/api/config/preferences`, - body: { shell: input?.["shell"], websearch: input?.["websearch"] }, - successStatus: 200, - declaredStatuses: [400, 401], - empty: false, - }, - requestOptions, - ), shells: (requestOptions?: RequestOptions) => request( { method: "GET", path: `/api/config/shell`, successStatus: 200, declaredStatuses: [400, 401], empty: false }, diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index 1f4b013b4b1..e01cc4391c0 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -424,8 +424,6 @@ export type WebSearchResult = { url: string; title?: string; content?: string; t export type ConfigWorktree = { directory: string } -export type ConfigPreferences = { shell?: string; websearch?: false | { provider: "random" | (string & {}) } } - export type ConfigShellOption = { path: string; name: string; acceptable: boolean } export type SessionMessageLocationSwitched = { @@ -6211,19 +6209,4 @@ export type ConfigGetInput = { export type ConfigGetOutput = Array -export type ConfigPreferencesOutput = ConfigPreferences - -export type ConfigUpdatePreferencesInput = { - readonly shell?: { - readonly shell?: string | null - readonly websearch?: false | { readonly provider: "random" | (string & {}) } | null - }["shell"] - readonly websearch?: { - readonly shell?: string | null - readonly websearch?: false | { readonly provider: "random" | (string & {}) } | null - }["websearch"] -} - -export type ConfigUpdatePreferencesOutput = ConfigPreferences - export type ConfigShellsOutput = Array diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index e1694459ded..841ae6903c8 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -3,9 +3,9 @@ export * as Config from "./config.js" import { makeLocationNode } from "@opencode/util/effect/app-node" import path from "path" import { isDeepStrictEqual } from "node:util" -import { applyEdits, modify, type ParseError, parse } from "jsonc-parser" +import { type ParseError, parse } from "jsonc-parser" import { Context, Effect, FiberMap, Layer, Option, PubSub, Ref, Schema, Semaphore, Stream } from "effect" -import { Directory, Document, Info, type Preferences, type PreferencesPatch, type Entry, Event } from "@opencode/schema/config" +import { Directory, Document, Info, type Entry, Event } from "@opencode/schema/config" import { Credential } from "./credential.js" import { Bus } from "./bus.js" import { Watcher } from "./filesystem/watcher.js" @@ -38,10 +38,6 @@ export interface Interface { * source files they parse and rebuild their own state. */ readonly changes: () => Stream.Stream - /** Returns preferences from the highest-precedence global config document. */ - readonly preferences?: () => Effect.Effect - /** Patches preferences in the highest-precedence global config document. */ - readonly updatePreferences?: (patch: PreferencesPatch) => Effect.Effect } export const Options = Schema.Struct({ @@ -88,19 +84,6 @@ export const testLayer = ( }), ) -function decodePreferences(text: string): Preferences { - const errors: ParseError[] = [] - const input: unknown = parse(text, errors, { allowTrailingComma: true }) - if (errors.length) return {} - const normalized = ConfigNormalize.normalize(input) - if (normalized.type === "rejected") return {} - const info = Option.getOrUndefined(Schema.decodeUnknownOption(Info)(normalized.encoded)) - return { - ...(info?.shell === undefined ? {} : { shell: info.shell }), - ...(info?.websearch === undefined ? {} : { websearch: info.websearch }), - } -} - export const layer = (options?: Options) => Layer.effect( Service, @@ -113,7 +96,6 @@ export const layer = (options?: Options) => const globalService = yield* Global.Service const wellknown = yield* WellKnown.Service const reloadLock = Semaphore.makeUnsafe(1) - const updateLock = Semaphore.makeUnsafe(1) const decodeOptions = { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" } as const const decodeInfo = Schema.decodeUnknownOption(Info, decodeOptions) const parseInfo = Effect.fn("Config.parseInfo")(function* (text: string, source: string) { @@ -341,43 +323,6 @@ export const layer = (options?: Options) => ) yield* reloadLock.withPermit(reconcile(initial)) - const globalConfigPath = Effect.fn("Config.globalConfigPath")(function* () { - const directory = initial.global ?? AbsolutePath.make(globalService.config) - const candidates = ConfigDiscovery.names.map((name) => path.join(directory, name)) - const existing = yield* Effect.filter(candidates, fs.isFile) - return existing.at(-1) ?? path.join(directory, "opencode.jsonc") - }) - - const preferences = Effect.fn("Config.preferences")(function* () { - const filepath = yield* globalConfigPath() - const text = yield* fs.readFileStringSafe(filepath) - return text === undefined ? {} : decodePreferences(text) - }) - - const updatePreferences = Effect.fn("Config.updatePreferences")( - function* (patch: PreferencesPatch) { - const filepath = yield* globalConfigPath() - const text = (yield* fs.readFileStringSafe(filepath)) ?? "{}\n" - const updated = yield* Effect.try({ - try: () => - (["shell", "websearch"] as const).reduce((content, key) => { - if (!Object.prototype.hasOwnProperty.call(patch, key)) return content - return applyEdits( - content, - modify(content, [key], patch[key] === null ? undefined : patch[key], { - formattingOptions: { tabSize: 2, insertSpaces: true }, - }), - ) - }, text), - catch: (cause) => new FSUtil.FileSystemError({ method: "config.updatePreferences", cause }), - }) - yield* fs.writeWithDirs(filepath, updated.endsWith("\n") ? updated : `${updated}\n`) - yield* requestReload - return decodePreferences(updated) - }, - (effect) => updateLock.withPermit(effect), - ) - return Service.of({ entries: Effect.fnUntraced(function* () { return configs @@ -388,8 +333,6 @@ export const layer = (options?: Options) => agents: Effect.filter(sources.agents, fs.isDir), }), changes: () => Stream.fromPubSub(updates), - preferences, - updatePreferences, }) }), ) diff --git a/packages/protocol/openapi.json b/packages/protocol/openapi.json index aa6e09d0f99..d97b029109a 100644 --- a/packages/protocol/openapi.json +++ b/packages/protocol/openapi.json @@ -12724,98 +12724,6 @@ "summary": "Get configuration" } }, - "/api/config/preferences": { - "get": { - "tags": ["config"], - "operationId": "config.preferences", - "parameters": [], - "security": [], - "responses": { - "200": { - "description": "Config.Preferences", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Config.Preferences" - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - } - } - } - }, - "description": "Return preferences from the highest-precedence global configuration document.", - "summary": "Get global preferences" - }, - "patch": { - "tags": ["config"], - "operationId": "config.updatePreferences", - "parameters": [], - "security": [], - "responses": { - "200": { - "description": "Config.Preferences", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Config.Preferences" - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - } - } - } - }, - "description": "Patch preferences in the highest-precedence global configuration document.", - "summary": "Update global preferences", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Config.PreferencesPatch" - } - } - }, - "required": true - } - } - }, "/api/config/shell": { "get": { "tags": ["config"], @@ -13686,60 +13594,6 @@ "required": ["package"], "additionalProperties": false }, - "Config.Preferences": { - "type": "object", - "properties": { - "shell": { - "type": "string" - }, - "websearch": { - "anyOf": [ - { - "type": "boolean", - "enum": [false] - }, - { - "$ref": "#/components/schemas/ConfigWebSearch.InfoEncoded" - } - ] - } - }, - "additionalProperties": false - }, - "Config.PreferencesPatch": { - "type": "object", - "properties": { - "shell": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "websearch": { - "anyOf": [ - { - "anyOf": [ - { - "type": "boolean", - "enum": [false] - }, - { - "$ref": "#/components/schemas/ConfigWebSearch.InfoEncoded" - } - ] - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, "Config.ProviderEncoded": { "type": "object", "properties": { diff --git a/packages/protocol/src/groups/config.ts b/packages/protocol/src/groups/config.ts index f16e45fcf98..4e4a345d9fd 100644 --- a/packages/protocol/src/groups/config.ts +++ b/packages/protocol/src/groups/config.ts @@ -20,29 +20,6 @@ export const ConfigGroup = HttpApiGroup.make("server.config") }), ), ) - .add( - HttpApiEndpoint.get("config.preferences", "/api/config/preferences", { - success: Config.Preferences, - }).annotateMerge( - OpenApi.annotations({ - identifier: "config.preferences", - summary: "Get global preferences", - description: "Return preferences from the highest-precedence global configuration document.", - }), - ), - ) - .add( - HttpApiEndpoint.patch("config.updatePreferences", "/api/config/preferences", { - payload: Config.PreferencesPatch, - success: Config.Preferences, - }).annotateMerge( - OpenApi.annotations({ - identifier: "config.updatePreferences", - summary: "Update global preferences", - description: "Patch preferences in the highest-precedence global configuration document.", - }), - ), - ) .add( HttpApiEndpoint.get("config.shells", "/api/config/shell", { success: Schema.Array(ConfigShell.Option), diff --git a/packages/schema/src/config.ts b/packages/schema/src/config.ts index 222bbef33c2..4c7b597f02c 100644 --- a/packages/schema/src/config.ts +++ b/packages/schema/src/config.ts @@ -109,18 +109,6 @@ export class Info extends Schema.Class("Config.Info")({ experimental: ConfigExperimental.Info.pipe(optional), }) {} -export const Preferences = Schema.Struct({ - shell: Schema.String.pipe(optional), - websearch: ConfigWebSearch.Selection.pipe(optional), -}).annotate({ identifier: "Config.Preferences" }) -export interface Preferences extends Schema.Schema.Type {} - -export const PreferencesPatch = Schema.Struct({ - shell: Schema.NullOr(Schema.String).pipe(optional), - websearch: Schema.NullOr(ConfigWebSearch.Selection).pipe(optional), -}).annotate({ identifier: "Config.PreferencesPatch" }) -export interface PreferencesPatch extends Schema.Schema.Type {} - export class Document extends Schema.Class("Config.Document")({ type: Schema.Literal("document"), path: AbsolutePath.pipe(optional), diff --git a/packages/server/src/handlers/config.ts b/packages/server/src/handlers/config.ts index 845071bd658..fb81bb1dcd3 100644 --- a/packages/server/src/handlers/config.ts +++ b/packages/server/src/handlers/config.ts @@ -7,22 +7,6 @@ import { Api } from "../api" export const ConfigHandler = HttpApiBuilder.group(Api, "server.config", (handlers) => handlers .handle("config.get", () => Config.Service.use((config) => config.entries())) - .handle( - "config.preferences", - Effect.fn(function* () { - const config = yield* Config.Service - if (!config.preferences) return yield* Effect.die(new Error("Config preferences are unavailable")) - return yield* config.preferences().pipe(Effect.orDie) - }), - ) - .handle( - "config.updatePreferences", - Effect.fn(function* (ctx) { - const config = yield* Config.Service - if (!config.updatePreferences) return yield* Effect.die(new Error("Config preference updates are unavailable")) - return yield* config.updatePreferences(ctx.payload).pipe(Effect.orDie) - }), - ) .handle( "config.shells", Effect.fn(function* () { diff --git a/packages/server/test/config.test.ts b/packages/server/test/config.test.ts index fbc76010d62..4c7ed58fb1c 100644 --- a/packages/server/test/config.test.ts +++ b/packages/server/test/config.test.ts @@ -59,46 +59,12 @@ it.live("returns ordered config entries for the requested directory", () => }), ) -it.live("updates global preferences without replacing unrelated JSONC", () => +it.live("lists available shells", () => Effect.gen(function* () { - const tmp = yield* Effect.acquireDisposable(Effect.promise(() => tmpdir("opencode-config-preferences-"))) + const tmp = yield* Effect.acquireDisposable(Effect.promise(() => tmpdir("opencode-config-shells-"))) const global = path.join(tmp.path, "global") - const config = path.join(global, "opencode.jsonc") yield* Effect.promise(() => fs.mkdir(global, { recursive: true })) - yield* Effect.promise(() => - fs.writeFile( - config, - `{ - // keep this comment - "model": "provider/model", - "shell": "bash" -} -`, - ), - ) const server = yield* startServer(global) - const response = yield* Effect.promise(() => - fetch(new URL("/api/config/preferences", server.base), { - method: "PATCH", - headers: { ...server.headers, "content-type": "application/json" }, - body: JSON.stringify({ shell: null, websearch: { provider: "random" } }), - }), - ) - - expect(response.status).toBe(200) - expect(yield* Effect.promise(() => response.json())).toEqual({ websearch: { provider: "random" } }) - const text = yield* Effect.promise(() => fs.readFile(config, "utf8")) - expect(text).toContain("// keep this comment") - expect(text).toContain('"model": "provider/model"') - expect(text).not.toContain('"shell"') - expect(text).toContain('"websearch"') - - const preferences = yield* Effect.promise(() => - fetch(new URL("/api/config/preferences", server.base), { headers: server.headers }), - ) - expect(preferences.status).toBe(200) - expect(yield* Effect.promise(() => preferences.json())).toEqual({ websearch: { provider: "random" } }) - const shells = yield* Effect.promise(() => fetch(new URL("/api/config/shell", server.base), { headers: server.headers }), ) diff --git a/services/www/openapi.json b/services/www/openapi.json index aa6e09d0f99..d97b029109a 100644 --- a/services/www/openapi.json +++ b/services/www/openapi.json @@ -12724,98 +12724,6 @@ "summary": "Get configuration" } }, - "/api/config/preferences": { - "get": { - "tags": ["config"], - "operationId": "config.preferences", - "parameters": [], - "security": [], - "responses": { - "200": { - "description": "Config.Preferences", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Config.Preferences" - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - } - } - } - }, - "description": "Return preferences from the highest-precedence global configuration document.", - "summary": "Get global preferences" - }, - "patch": { - "tags": ["config"], - "operationId": "config.updatePreferences", - "parameters": [], - "security": [], - "responses": { - "200": { - "description": "Config.Preferences", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Config.Preferences" - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - } - } - } - }, - "description": "Patch preferences in the highest-precedence global configuration document.", - "summary": "Update global preferences", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Config.PreferencesPatch" - } - } - }, - "required": true - } - } - }, "/api/config/shell": { "get": { "tags": ["config"], @@ -13686,60 +13594,6 @@ "required": ["package"], "additionalProperties": false }, - "Config.Preferences": { - "type": "object", - "properties": { - "shell": { - "type": "string" - }, - "websearch": { - "anyOf": [ - { - "type": "boolean", - "enum": [false] - }, - { - "$ref": "#/components/schemas/ConfigWebSearch.InfoEncoded" - } - ] - } - }, - "additionalProperties": false - }, - "Config.PreferencesPatch": { - "type": "object", - "properties": { - "shell": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "websearch": { - "anyOf": [ - { - "anyOf": [ - { - "type": "boolean", - "enum": [false] - }, - { - "$ref": "#/components/schemas/ConfigWebSearch.InfoEncoded" - } - ] - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, "Config.ProviderEncoded": { "type": "object", "properties": { diff --git a/services/www/public/openapi.json b/services/www/public/openapi.json index aa6e09d0f99..d97b029109a 100644 --- a/services/www/public/openapi.json +++ b/services/www/public/openapi.json @@ -12724,98 +12724,6 @@ "summary": "Get configuration" } }, - "/api/config/preferences": { - "get": { - "tags": ["config"], - "operationId": "config.preferences", - "parameters": [], - "security": [], - "responses": { - "200": { - "description": "Config.Preferences", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Config.Preferences" - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - } - } - } - }, - "description": "Return preferences from the highest-precedence global configuration document.", - "summary": "Get global preferences" - }, - "patch": { - "tags": ["config"], - "operationId": "config.updatePreferences", - "parameters": [], - "security": [], - "responses": { - "200": { - "description": "Config.Preferences", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Config.Preferences" - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - } - } - } - }, - "description": "Patch preferences in the highest-precedence global configuration document.", - "summary": "Update global preferences", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Config.PreferencesPatch" - } - } - }, - "required": true - } - } - }, "/api/config/shell": { "get": { "tags": ["config"], @@ -13686,60 +13594,6 @@ "required": ["package"], "additionalProperties": false }, - "Config.Preferences": { - "type": "object", - "properties": { - "shell": { - "type": "string" - }, - "websearch": { - "anyOf": [ - { - "type": "boolean", - "enum": [false] - }, - { - "$ref": "#/components/schemas/ConfigWebSearch.InfoEncoded" - } - ] - } - }, - "additionalProperties": false - }, - "Config.PreferencesPatch": { - "type": "object", - "properties": { - "shell": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "websearch": { - "anyOf": [ - { - "anyOf": [ - { - "type": "boolean", - "enum": [false] - }, - { - "$ref": "#/components/schemas/ConfigWebSearch.InfoEncoded" - } - ] - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, "Config.ProviderEncoded": { "type": "object", "properties": {