chore: generate

This commit is contained in:
opencode-agent[bot] 2026-06-07 03:28:52 +00:00
commit 155e1f20d6
25 changed files with 492 additions and 329 deletions

View file

@ -115,7 +115,8 @@ export const layer = Layer.effect(
if (found) yield* stopProcess(found).pipe(Effect.ignore) if (found) yield* stopProcess(found).pipe(Effect.ignore)
const entrypoint = compiled ? undefined : process.argv[1] const entrypoint = compiled ? undefined : process.argv[1]
if (!compiled && entrypoint === undefined) return yield* Effect.fail(new Error("Failed to resolve CLI entrypoint")) if (!compiled && entrypoint === undefined)
return yield* Effect.fail(new Error("Failed to resolve CLI entrypoint"))
yield* Effect.try({ yield* Effect.try({
try: () => { try: () => {
spawn(process.execPath, [...(entrypoint ? [entrypoint] : []), "serve", "--register"], { spawn(process.execPath, [...(entrypoint ? [entrypoint] : []), "serve", "--register"], {

View file

@ -106,10 +106,13 @@ const legacyDefaults: Record<string, unknown> = {
"/config": {}, "/config": {},
} }
const gracefulFetch = Object.assign(async (input: RequestInfo | URL, init?: RequestInit) => { const gracefulFetch = Object.assign(
const response = await fetch(input, init) async (input: RequestInfo | URL, init?: RequestInit) => {
if (response.status !== 404) return response const response = await fetch(input, init)
const fallback = legacyDefaults[new URL(input instanceof Request ? input.url : input).pathname] if (response.status !== 404) return response
if (fallback === undefined) return response const fallback = legacyDefaults[new URL(input instanceof Request ? input.url : input).pathname]
return Response.json(fallback) if (fallback === undefined) return response
}, { preconnect: fetch.preconnect }) return Response.json(fallback)
},
{ preconnect: fetch.preconnect },
)

View file

@ -100,9 +100,7 @@ function normalizePack(pack: TuiAttentionSoundPack): RegisteredSoundPack | undef
sounds: Object.fromEntries( sounds: Object.fromEntries(
Object.entries(pack.sounds).filter( Object.entries(pack.sounds).filter(
(item): item is [TuiAttentionSoundName, string] => (item): item is [TuiAttentionSoundName, string] =>
Schema.is(AttentionSoundName)(item[0]) && Schema.is(AttentionSoundName)(item[0]) && typeof item[1] === "string" && item[1].trim().length > 0,
typeof item[1] === "string" &&
item[1].trim().length > 0,
), ),
), ),
} }
@ -200,9 +198,7 @@ export function createTuiAttention(input: {
const requestedSound = typeof request.sound === "object" ? request.sound : undefined const requestedSound = typeof request.sound === "object" ? request.sound : undefined
const soundSkip = volume === undefined ? undefined : focusSkip(requestedSound?.when ?? "always", focus) const soundSkip = volume === undefined ? undefined : focusSkip(requestedSound?.when ?? "always", focus)
const soundName = const soundName =
requestedSound?.name && Schema.is(AttentionSoundName)(requestedSound.name) requestedSound?.name && Schema.is(AttentionSoundName)(requestedSound.name) ? requestedSound.name : "default"
? requestedSound.name
: "default"
const sound = volume === undefined || soundSkip ? false : await playSound(soundName, volume) const sound = volume === undefined || soundSkip ? false : await playSound(soundName, volume)
if (!notification && !sound) { if (!notification && !sound) {

View file

@ -86,13 +86,15 @@ export function discoverEditorConnection(directory: string) {
: [] : []
const score = Math.max(0, ...folders.map(contains)) const score = Math.max(0, ...folders.map(contains))
if (!score) return [] if (!score) return []
return [{ return [
url: `ws://127.0.0.1:${port}`, {
authToken: typeof value.authToken === "string" ? value.authToken : undefined, url: `ws://127.0.0.1:${port}`,
source: `lock:${port}`, authToken: typeof value.authToken === "string" ? value.authToken : undefined,
score, source: `lock:${port}`,
mtime: statSync(file).mtimeMs, score,
}] mtime: statSync(file).mtimeMs,
},
]
} catch { } catch {
return [] return []
} }
@ -110,11 +112,13 @@ function resolveZedDbPath() {
path.join(os.homedir(), "Library", "Application Support", "Zed", "db", "0-stable", "db.sqlite"), path.join(os.homedir(), "Library", "Application Support", "Zed", "db", "0-stable", "db.sqlite"),
path.join(os.homedir(), ".local", "share", "zed", "db", "0-stable", "db.sqlite"), path.join(os.homedir(), ".local", "share", "zed", "db", "0-stable", "db.sqlite"),
].filter((item): item is string => Boolean(item)) ].filter((item): item is string => Boolean(item))
return candidates.find((item) => { return (
try { candidates.find((item) => {
return statSync(item).isFile() try {
} catch { return statSync(item).isFile()
return false } catch {
} return false
}) ?? "" }
}) ?? ""
)
} }

View file

@ -4,7 +4,10 @@ import { HttpEffect, HttpRouter, HttpServerRequest, HttpServerResponse } from "e
import { HttpApiError, HttpApiMiddleware } from "effect/unstable/httpapi" import { HttpApiError, HttpApiMiddleware } from "effect/unstable/httpapi"
import { hasPtyConnectTicketURL } from "@/server/shared/pty-ticket" import { hasPtyConnectTicketURL } from "@/server/shared/pty-ticket"
import { isPublicUIPath } from "@/server/shared/public-ui" import { isPublicUIPath } from "@/server/shared/public-ui"
export { Authorization as ServerAuthorization, authorizationLayer as serverAuthorizationLayer } from "@opencode-ai/server/middleware/authorization" export {
Authorization as ServerAuthorization,
authorizationLayer as serverAuthorizationLayer,
} from "@opencode-ai/server/middleware/authorization"
const AUTH_TOKEN_QUERY = "auth_token" const AUTH_TOKEN_QUERY = "auth_token"
const UNAUTHORIZED = 401 const UNAUTHORIZED = 401

View file

@ -3,12 +3,7 @@ import { mkdir, symlink } from "node:fs/promises"
import os from "node:os" import os from "node:os"
import path from "node:path" import path from "node:path"
import { afterEach, expect, spyOn, test } from "bun:test" import { afterEach, expect, spyOn, test } from "bun:test"
import { import { isZedTerminal, offsetToPosition, resolveZedDbPath, resolveZedSelection } from "../../../src/cli/tui/editor-zed"
isZedTerminal,
offsetToPosition,
resolveZedDbPath,
resolveZedSelection,
} from "../../../src/cli/tui/editor-zed"
import { tmpdir } from "../../fixture/fixture" import { tmpdir } from "../../fixture/fixture"
const originalZedTerm = process.env.ZED_TERM const originalZedTerm = process.env.ZED_TERM

File diff suppressed because it is too large Load diff

View file

@ -1,15 +1,14 @@
import { Schema } from "effect" import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
export const HealthGroup = HttpApiGroup.make("server.health") export const HealthGroup = HttpApiGroup.make("server.health").add(
.add( HttpApiEndpoint.get("health.get", "/api/health", {
HttpApiEndpoint.get("health.get", "/api/health", { success: Schema.Struct({ healthy: Schema.Literal(true) }),
success: Schema.Struct({ healthy: Schema.Literal(true) }), }).annotateMerge(
}).annotateMerge( OpenApi.annotations({
OpenApi.annotations({ identifier: "v2.health.get",
identifier: "v2.health.get", summary: "Check server health",
summary: "Check server health", description: "Check whether the API server is ready to accept requests.",
description: "Check whether the API server is ready to accept requests.", }),
}), ),
), )
)

View file

@ -83,6 +83,4 @@ export const PermissionGroup = HttpApiGroup.make("server.permission")
}), }),
), ),
) )
.annotateMerge( .annotateMerge(OpenApi.annotations({ title: "permissions", description: "Experimental permission routes." }))
OpenApi.annotations({ title: "permissions", description: "Experimental permission routes." }),
)

View file

@ -33,8 +33,7 @@ export const ProviderGroup = HttpApiGroup.make("server.provider")
OpenApi.annotations({ OpenApi.annotations({
identifier: "v2.provider.get", identifier: "v2.provider.get",
summary: "Get provider", summary: "Get provider",
description: description: "Retrieve a single AI provider so clients can inspect its availability and endpoint settings.",
"Retrieve a single AI provider so clients can inspect its availability and endpoint settings.",
}), }),
), ),
) )

View file

@ -43,7 +43,11 @@ export const PermissionHandler = HttpApiBuilder.group(Api, "server.permission",
"permission.saved.list", "permission.saved.list",
Effect.fn(function* (ctx) { Effect.fn(function* (ctx) {
const location = yield* Location.Service const location = yield* Location.Service
return { data: yield* (yield* PermissionSaved.Service).list({ projectID: ctx.query.projectID ?? location.project.id }) } return {
data: yield* (yield* PermissionSaved.Service).list({
projectID: ctx.query.projectID ?? location.project.id,
}),
}
}), }),
) )
.handle( .handle(

View file

@ -7,12 +7,9 @@ import { HttpApiMiddleware } from "effect/unstable/httpapi"
const AUTH_TOKEN_QUERY = "auth_token" const AUTH_TOKEN_QUERY = "auth_token"
const WWW_AUTHENTICATE = 'Basic realm="Secure Area"' const WWW_AUTHENTICATE = 'Basic realm="Secure Area"'
export class Authorization extends HttpApiMiddleware.Service<Authorization>()( export class Authorization extends HttpApiMiddleware.Service<Authorization>()("@opencode/HttpApiAuthorization", {
"@opencode/HttpApiAuthorization", error: UnauthorizedError,
{ }) {}
error: UnauthorizedError,
},
) {}
function emptyCredential() { function emptyCredential() {
return { username: "", password: Redacted.make("") } return { username: "", password: Redacted.make("") }

View file

@ -480,30 +480,33 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
const attention = props.host.attention({ renderer, config: tuiConfig, kv }) const attention = props.host.attention({ renderer, config: tuiConfig, kv })
const platform = useTuiPlatform() const platform = useTuiPlatform()
const api = createTuiApi(createTuiApiAdapters({ const api = createTuiApi(
version: build.version, createTuiApiAdapters({
tuiConfig, version: build.version,
dialog, tuiConfig,
keymap, dialog,
kv, keymap,
route, kv,
routes: pluginRuntime.routes, route,
event, routes: pluginRuntime.routes,
sdk, event,
sync, sdk,
theme: themeState, sync,
toast, theme: themeState,
renderer, toast,
attention, renderer,
Slot: pluginRuntime.Slot, attention,
})) Slot: pluginRuntime.Slot,
}),
)
const [ready, setReady] = createSignal(false) const [ready, setReady] = createSignal(false)
props.pluginHost.start({ props.pluginHost
api, .start({
config: tuiConfig, api,
runtime: pluginRuntime, config: tuiConfig,
dispose: () => attention.dispose(), runtime: pluginRuntime,
}) dispose: () => attention.dispose(),
})
.catch((error) => { .catch((error) => {
console.error("Failed to load TUI plugins", error) console.error("Failed to load TUI plugins", error)
}) })
@ -529,7 +532,8 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
renderer.console.onCopySelection = async (text: string) => { renderer.console.onCopySelection = async (text: string) => {
if (!text || text.length === 0) return if (!text || text.length === 0) return
await platform.clipboard?.write?.(text) await platform.clipboard
?.write?.(text)
.then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) .then(() => toast.show({ message: "Copied to clipboard", variant: "info" }))
.catch(toast.error) .catch(toast.error)
@ -691,7 +695,8 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
run: async () => { run: async () => {
const workspace = currentWorktreeWorkspace() const workspace = currentWorktreeWorkspace()
if (!workspace?.directory) return if (!workspace?.directory) return
await platform.clipboard?.write?.(workspace.directory) await platform.clipboard
?.write?.(workspace.directory)
.then(() => toast.show({ message: "Copied worktree path", variant: "info" })) .then(() => toast.show({ message: "Copied worktree path", variant: "info" }))
.catch(toast.error) .catch(toast.error)
dialog.clear() dialog.clear()

View file

@ -16,10 +16,7 @@ export const AttentionSoundName = Schema.Literals([
export type AttentionSoundName = Schema.Schema.Type<typeof AttentionSoundName> export type AttentionSoundName = Schema.Schema.Type<typeof AttentionSoundName>
export const PluginOptions = Schema.Record(Schema.String, Schema.Unknown) export const PluginOptions = Schema.Record(Schema.String, Schema.Unknown)
export const PluginSpec = Schema.Union([ export const PluginSpec = Schema.Union([Schema.String, Schema.mutable(Schema.Tuple([Schema.String, PluginOptions]))])
Schema.String,
Schema.mutable(Schema.Tuple([Schema.String, PluginOptions])),
])
export const LeaderTimeoutDefault = 2000 export const LeaderTimeoutDefault = 2000
export const LeaderTimeout = Schema.Int.check(Schema.isGreaterThan(0)).annotate({ export const LeaderTimeout = Schema.Int.check(Schema.isGreaterThan(0)).annotate({

View file

@ -55,7 +55,11 @@ export type SyncDependencies = {
logger: { error(message: string, extra?: Record<string, unknown>): void } logger: { error(message: string, extra?: Record<string, unknown>): void }
} }
export const { context: SyncContext, use: useSync, provider: SyncProvider } = createSimpleContext({ export const {
context: SyncContext,
use: useSync,
provider: SyncProvider,
} = createSimpleContext({
name: "Sync", name: "Sync",
init: (dependencies: SyncDependencies) => { init: (dependencies: SyncDependencies) => {
const environment = useTuiEnvironment() const environment = useTuiEnvironment()

View file

@ -96,14 +96,15 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
}) })
function syncCustomThemes() { function syncCustomThemes() {
return (platform?.themes?.discover() ?? Promise.resolve({})).then((themes) => { return (platform?.themes?.discover() ?? Promise.resolve({}))
setCustomThemes( .then((themes) => {
Object.entries(themes).reduce<Record<string, ThemeJson>>((result, [name, theme]) => { setCustomThemes(
if (isTheme(theme)) result[name] = theme Object.entries(themes).reduce<Record<string, ThemeJson>>((result, [name, theme]) => {
return result if (isTheme(theme)) result[name] = theme
}, {}), return result
) }, {}),
}) )
})
.catch(() => setStore("active", "opencode")) .catch(() => setStore("active", "opencode"))
} }

View file

@ -136,10 +136,12 @@ export function Tips(props: { api: TuiPluginApi; connected?: boolean }) {
} }
const tip = createMemo(() => { const tip = createMemo(() => {
if (props.connected === false) return NO_MODELS_TIP if (props.connected === false) return NO_MODELS_TIP
const tips = [...TIPS, environment.capabilities.terminalSuspend ? TERMINAL_SUSPEND_TIP : INPUT_UNDO_TIP].flatMap((item) => { const tips = [...TIPS, environment.capabilities.terminalSuspend ? TERMINAL_SUSPEND_TIP : INPUT_UNDO_TIP].flatMap(
const value = typeof item === "string" ? item : item(shortcuts) (item) => {
return value ? [value] : [] const value = typeof item === "string" ? item : item(shortcuts)
}) return value ? [value] : []
},
)
return tips[Math.floor(tipOffset * tips.length)] ?? NO_MODELS_TIP return tips[Math.floor(tipOffset * tips.length)] ?? NO_MODELS_TIP
}, NO_MODELS_TIP) }, NO_MODELS_TIP)
// Solid can expose a memo's initial value while a pure computation is pending. // Solid can expose a memo's initial value while a pure computation is pending.

View file

@ -211,11 +211,7 @@ export function formatKeyBindings(bindings: Parameters<typeof formatCommandBindi
return formatCommandBindingsExtra(bindings, formatOptions(config)) return formatCommandBindingsExtra(bindings, formatOptions(config))
} }
export function registerOpencodeKeymap( export function registerOpencodeKeymap(keymap: OpenTuiKeymap, renderer: CliRenderer, config: ResolvedKeymapConfig) {
keymap: OpenTuiKeymap,
renderer: CliRenderer,
config: ResolvedKeymapConfig,
) {
const modeStack = createOpencodeModeStack(keymap) const modeStack = createOpencodeModeStack(keymap)
const offCommaBindings = registerCommaBindings(keymap) const offCommaBindings = registerCommaBindings(keymap)
const offAliasExpander = registerKeyAliases(keymap) const offAliasExpander = registerKeyAliases(keymap)

View file

@ -5,10 +5,7 @@ export function stripPromptPartIDs<Part extends { id: string; messageID: string;
return rest return rest
} }
export function expandPastedTextPlaceholders( export function expandPastedTextPlaceholders(text: string, parts: readonly unknown[]) {
text: string,
parts: readonly unknown[],
) {
return parts.reduce<string>((result, part) => { return parts.reduce<string>((result, part) => {
if (!isPastedTextPart(part)) return result if (!isPastedTextPart(part)) return result
return result.replace(part.source.text.value, part.text) return result.replace(part.source.text.value, part.text)

View file

@ -460,7 +460,8 @@ export function Session() {
}, },
run: async () => { run: async () => {
const copy = (url: string) => const copy = (url: string) =>
platform.clipboard?.write?.(url) platform.clipboard
?.write?.(url)
.then(() => toast.show({ message: "Share URL copied to clipboard!", variant: "success" })) .then(() => toast.show({ message: "Share URL copied to clipboard!", variant: "success" }))
.catch(() => toast.show({ message: "Failed to copy URL to clipboard", variant: "error" })) .catch(() => toast.show({ message: "Failed to copy URL to clipboard", variant: "error" }))
const url = session()?.share?.url const url = session()?.share?.url
@ -895,7 +896,8 @@ export function Session() {
return return
} }
platform.clipboard?.write?.(text) platform.clipboard
?.write?.(text)
.then(() => toast.show({ message: "Message copied to clipboard!", variant: "success" })) .then(() => toast.show({ message: "Message copied to clipboard!", variant: "success" }))
.catch(() => toast.show({ message: "Failed to copy to clipboard", variant: "error" })) .catch(() => toast.show({ message: "Failed to copy to clipboard", variant: "error" }))
dialog.clear() dialog.clear()

View file

@ -30,7 +30,8 @@ export function cliErrorMessage(input: unknown): string | undefined {
} }
const provider = configData(input, "ProviderInitError") const provider = configData(input, "ProviderInitError")
if (provider) return `Failed to initialize provider "${field(provider, "providerID")}". Check credentials and configuration.` if (provider)
return `Failed to initialize provider "${field(provider, "providerID")}". Check credentials and configuration.`
const json = configData(input, "ConfigJsonError") const json = configData(input, "ConfigJsonError")
if (json) { if (json) {

View file

@ -20,14 +20,7 @@ test("defines package-owned plugin specs and attention sound names", () => {
expect(decodePlugin("example-plugin")).toBe("example-plugin") expect(decodePlugin("example-plugin")).toBe("example-plugin")
expect(decodePlugin(["example-plugin", { enabled: true }])).toEqual(["example-plugin", { enabled: true }]) expect(decodePlugin(["example-plugin", { enabled: true }])).toEqual(["example-plugin", { enabled: true }])
expect(() => decodePlugin(["example-plugin"])).toThrow() expect(() => decodePlugin(["example-plugin"])).toThrow()
expect(AttentionSoundName.literals).toEqual([ expect(AttentionSoundName.literals).toEqual(["default", "question", "permission", "error", "done", "subagent_done"])
"default",
"question",
"permission",
"error",
"done",
"subagent_done",
])
}) })
test("validates config constraints", () => { test("validates config constraints", () => {

View file

@ -43,8 +43,19 @@ export function createFetch(override?: FetchHandler) {
const overridden = await override?.(url) const overridden = await override?.(url)
if (overridden) return overridden if (overridden) return overridden
if (["/agent", "/command", "/experimental/workspace", "/experimental/workspace/status", "/formatter", "/lsp"].includes(url.pathname)) return json([]) if (
if (["/config", "/experimental/resource", "/mcp", "/provider/auth", "/session/status"].includes(url.pathname)) return json({}) [
"/agent",
"/command",
"/experimental/workspace",
"/experimental/workspace/status",
"/formatter",
"/lsp",
].includes(url.pathname)
)
return json([])
if (["/config", "/experimental/resource", "/mcp", "/provider/auth", "/session/status"].includes(url.pathname))
return json({})
if (url.pathname === "/config/providers") return json({ providers: {}, default: {} }) if (url.pathname === "/config/providers") return json({ providers: {}, default: {} })
if (url.pathname === "/experimental/console") return json({ consoleManagedProviders: [], switchableOrgCount: 0 }) if (url.pathname === "/experimental/console") return json({ consoleManagedProviders: [], switchableOrgCount: 0 })
if (url.pathname === "/path") return json({ home: "", state: "", config: "", worktree, directory }) if (url.pathname === "/path") return json({ home: "", state: "", config: "", worktree, directory })

View file

@ -5,12 +5,7 @@ import { testRender, useRenderer } from "@opentui/solid"
import { expect, test } from "bun:test" import { expect, test } from "bun:test"
import { onCleanup } from "solid-js" import { onCleanup } from "solid-js"
import { TuiKeybind } from "../src/config/keybind" import { TuiKeybind } from "../src/config/keybind"
import { import { getOpencodeModeStack, OPENCODE_BASE_MODE, OpencodeKeymapProvider, registerOpencodeKeymap } from "../src/keymap"
getOpencodeModeStack,
OPENCODE_BASE_MODE,
OpencodeKeymapProvider,
registerOpencodeKeymap,
} from "../src/keymap"
function createResolvedKeymapConfig(input: TuiKeybind.KeybindOverrides = {}) { function createResolvedKeymapConfig(input: TuiKeybind.KeybindOverrides = {}) {
const keybinds = TuiKeybind.parse(input) const keybinds = TuiKeybind.parse(input)

View file

@ -1,10 +1,5 @@
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"
import { import { formatAssistantHeader, formatMessage, formatPart, formatTranscript } from "../../src/util/transcript"
formatAssistantHeader,
formatMessage,
formatPart,
formatTranscript,
} from "../../src/util/transcript"
import type { AssistantMessage, Part, Provider, UserMessage } from "@opencode-ai/sdk/v2" import type { AssistantMessage, Part, Provider, UserMessage } from "@opencode-ai/sdk/v2"
const providers: Provider[] = [ const providers: Provider[] = [