From 2385123f03ed4ae6820e2b235cb8d32919f70fb8 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 15 May 2026 20:27:36 -0400 Subject: [PATCH 001/585] Fix thinking toggle defaults --- packages/core/src/flag/flag.ts | 1 - .../src/cli/cmd/tui/context/thinking.ts | 17 ++++++------ .../tui/feature-plugins/system/session-v2.tsx | 4 +-- .../src/cli/cmd/tui/routes/session/index.tsx | 27 ++++++------------- 4 files changed, 18 insertions(+), 31 deletions(-) diff --git a/packages/core/src/flag/flag.ts b/packages/core/src/flag/flag.ts index 88270e3c20..3ed67bb785 100644 --- a/packages/core/src/flag/flag.ts +++ b/packages/core/src/flag/flag.ts @@ -38,7 +38,6 @@ export const Flag = { ), OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT: copy === undefined ? process.platform === "win32" : truthy("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT"), - OPENCODE_EXPERIMENTAL_MINIMAL_THINKING: truthy("OPENCODE_EXPERIMENTAL_MINIMAL_THINKING"), OPENCODE_MODELS_URL: process.env["OPENCODE_MODELS_URL"], OPENCODE_MODELS_PATH: process.env["OPENCODE_MODELS_PATH"], OPENCODE_DB: process.env["OPENCODE_DB"], diff --git a/packages/opencode/src/cli/cmd/tui/context/thinking.ts b/packages/opencode/src/cli/cmd/tui/context/thinking.ts index c5cae734b9..55e995df11 100644 --- a/packages/opencode/src/cli/cmd/tui/context/thinking.ts +++ b/packages/opencode/src/cli/cmd/tui/context/thinking.ts @@ -1,10 +1,9 @@ import { createMemo, type Setter } from "solid-js" -import { Flag } from "@opencode-ai/core/flag/flag" import { useKV } from "./kv" -export type ThinkingMode = "show" | "minimal" | "hide" +export type ThinkingMode = "show" | "hide" -const MODES: readonly ThinkingMode[] = ["show", "minimal", "hide"] as const +const MODES: readonly ThinkingMode[] = ["show", "hide"] as const // OpenAI's Responses API surfaces reasoning summaries that start with a bolded // title line: "**Inspecting PR workflow**\n\n". GitHub Copilot routes @@ -20,7 +19,7 @@ export function isThinkingMode(value: unknown): value is ThinkingMode { return typeof value === "string" && (MODES as readonly string[]).includes(value) } -// Cycle order matches the slash command: show → minimal → hide → show. +// Cycle order matches the slash command: show → hide → show. export function nextThinkingMode(current: ThinkingMode): ThinkingMode { const idx = MODES.indexOf(current) return MODES[(idx + 1) % MODES.length] ?? "show" @@ -33,7 +32,7 @@ export function useThinkingMode() { // The KVProvider only renders children once kv.ready, so reads here are safe. const hadStored = kv.get("thinking_mode") !== undefined const legacy = kv.get("thinking_visibility") - const [stored, setStored] = kv.signal("thinking_mode", "minimal") + const [stored, setStored] = kv.signal("thinking_mode", "hide") // The kv signal exposes its setter typed as `Setter` which carries Solid's // overload set; passing an updater fn through a property access loses the @@ -47,21 +46,21 @@ export function useThinkingMode() { // Preserve previous experience for users who had explicitly toggled the // legacy `thinking_visibility` boolean. First-time users (no legacy key) - // get the new "minimal" default. + // get the new "hide" default (collapsed thinking). if (!hadStored) { if (legacy === true) set("show") else if (legacy === false) set("hide") } + if ((stored() as string) === "minimal") set("hide") + const mode = createMemo(() => { - if (Flag.OPENCODE_EXPERIMENTAL_MINIMAL_THINKING) return "minimal" const value = stored() - return isThinkingMode(value) ? value : "minimal" + return isThinkingMode(value) ? value : "hide" }) return { mode, set, - locked: () => Flag.OPENCODE_EXPERIMENTAL_MINIMAL_THINKING === true, } } diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx index dda6309d93..5017b77b00 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx @@ -392,7 +392,7 @@ function AssistantReasoning(props: { const thinking = useThinkingMode() const [expanded, setExpanded] = createSignal(false) const content = createMemo(() => props.part.text.replace("[REDACTED]", "").trim()) - const inMinimal = createMemo(() => thinking.mode() === "minimal") + const inMinimal = createMemo(() => thinking.mode() === "hide") // v2 reasoning parts have no per-part `time.end` (see SessionMessageAssistantReasoning // in the v2 SDK); we settle on parent-message completion instead. const isDone = createMemo(() => props.completedAt() !== undefined) @@ -404,7 +404,7 @@ function AssistantReasoning(props: { } return ( - + thinkingMode() !== "hide") + const showThinking = createMemo(() => true) const [timestamps, setTimestamps] = kv.signal<"hide" | "show">("timestamps", "hide") const [showDetails, setShowDetails] = kv.signal("tool_details_visibility", true) const [showAssistantMetadata, _setShowAssistantMetadata] = kv.signal("assistant_metadata_visibility", true) @@ -689,9 +689,8 @@ export function Session() { { title: (() => { const next = nextThinkingMode(thinkingMode()) - if (next === "minimal") return "Switch thinking to minimal" - if (next === "hide") return "Hide thinking" - return "Show thinking" + if (next === "hide") return "Collapse thinking" + return "Expand thinking" })(), value: "session.toggle.thinking", category: "Session", @@ -700,16 +699,6 @@ export function Session() { aliases: ["toggle-thinking"], }, run: () => { - // Env override forces minimal for the process. Updating KV here would - // silently diverge from what's rendered; tell the user instead. - if (thinking.locked()) { - toast.show({ - message: "Thinking mode is locked to minimal by OPENCODE_EXPERIMENTAL_MINIMAL_THINKING", - variant: "info", - }) - dialog.clear() - return - } thinking.set(nextThinkingMode(thinkingMode())) dialog.clear() }, @@ -1512,7 +1501,7 @@ const PART_MAPPING = { function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: AssistantMessage }) { const { theme, subtleSyntax } = useTheme() const ctx = use() - // Collapsed by default in minimal mode: a single line throughout, so the + // Collapsed by default in hide mode: a single line throughout, so the // layout never shifts. Click to open the full markdown block, click to close. const [expanded, setExpanded] = createSignal(false) @@ -1523,7 +1512,7 @@ function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: Ass // Reasoning is finalized when the server sets `time.end` (see processor.ts). // Flips independently of the parent message completing. const isDone = createMemo(() => props.part.time.end !== undefined) - const inMinimal = createMemo(() => ctx.thinkingMode() === "minimal") + const inMinimal = createMemo(() => ctx.thinkingMode() === "hide") const duration = createMemo(() => { const end = props.part.time.end return end === undefined ? 0 : Math.max(0, end - props.part.time.start) @@ -1539,10 +1528,10 @@ function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: Ass } return ( - + - {/* Full markdown block: `show` mode, or `minimal` after the user opens it. */} + {/* Full markdown block: `show` mode, or `hide` after the user opens it. */} From 5911bd532d7c80e8426783e5151dd00f92dd1e76 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 15 May 2026 20:42:56 -0400 Subject: [PATCH 002/585] fix(tui): show config error details on startup (#27803) --- packages/opencode/specs/effect/errors.md | 81 +++++++++++++++++++ .../cli/cmd/tui/context/aggregate-failures.ts | 19 ++++- packages/opencode/src/cli/error.ts | 29 ++++--- .../instance/httpapi/middleware/error.ts | 8 ++ .../cli/cmd/tui/aggregate-failures.test.ts | 44 +++++++++- .../server/httpapi-error-middleware.test.ts | 22 +++++ 6 files changed, 185 insertions(+), 18 deletions(-) diff --git a/packages/opencode/specs/effect/errors.md b/packages/opencode/specs/effect/errors.md index 69298bde5c..310857dfd9 100644 --- a/packages/opencode/specs/effect/errors.md +++ b/packages/opencode/specs/effect/errors.md @@ -70,11 +70,51 @@ Endpoint definitions declare which public errors can be emitted. Public HTTP error schemas carry their response status with `httpApiStatus` or the equivalent HttpApi schema annotation. +Effect's own HttpApi examples follow this pattern: + +```ts +export class Unauthorized extends Schema.TaggedErrorClass()( + "Unauthorized", + { message: Schema.String }, + { httpApiStatus: 401 }, +) {} + +export class Authorization extends HttpApiMiddleware.Service()("app/Authorization", { + security: { bearer: HttpApiSecurity.bearer }, + error: Unauthorized, +}) {} +``` + +Endpoint-level errors use the same idea: + +```ts +export class ConfigApiError extends Schema.ErrorClass("ConfigApiError")( + { + name: Schema.Union(Schema.Literal("ConfigInvalidError"), Schema.Literal("ConfigJsonError")), + data: Schema.Struct({ message: Schema.optional(Schema.String), path: Schema.String }), + }, + { httpApiStatus: 400 }, +) {} + +HttpApiEndpoint.get("get", "/config", { + success: Config.Info, + error: ConfigApiError, +}) +``` + The service error and HTTP error may be the same class only when the wire shape is intentionally public. Use separate HTTP error schemas when the service error contains internals, low-level causes, retry hints, or data that should not be exposed to API clients. +Do not map every domain error into one universal HTTP error class. Prefer a +small public error vocabulary by route group: shared shapes like +`ApiNotFoundError`, route-specific shapes like `ConfigApiError`, and built-in +empty `HttpApiError.*` only when an empty/no-content body is the intended SDK +contract. + ## Mapping Guidance - Keep one-off translations inline in the handler. @@ -86,6 +126,35 @@ that should not be exposed to API clients. breaking API change. - Use built-in `HttpApiError.*` only when its generated body and SDK surface are intentionally the public contract. +- Prefer `Schema.ErrorClass` for public HTTP error bodies whose wire shape is + not the same as the internal domain error shape. +- Prefer `Schema.TaggedErrorClass` for service/domain errors and middleware + errors that are naturally tagged by `_tag`. +- If preserving a legacy `{ name, data }` body, model that shape explicitly in + the public API error schema instead of relying on `NamedError.toObject()` in + generic middleware. + +## User-Facing Rendering + +HTTP serialization and user rendering are separate boundaries. The server +should send structured public errors; CLI and TUI code should format those +structures through one shared formatter. + +For SDK calls using `{ throwOnError: true }`, the generated client may wrap the +decoded response body in an `Error`. The original body should remain available +under `error.cause.body`; `FormatError` is the right place to unwrap and render +that body. TUI aggregation helpers should call `FormatError` first, then fall +back to generic `Error.message` / string rendering. + +When several parallel startup requests fail from the same underlying issue, +group identical rendered messages and list the affected request names once. +For example: + +```text +Configuration is invalid at /path/to/opencode.json +↳ Expected object, got "not-object" provider.bad.options +Affected startup requests: config.providers, provider.list, app.agents, config.get +``` ## Middleware Guidance @@ -99,6 +168,15 @@ middleware should shrink. It should not gain new name checks. Unknown `500` responses should log full details server-side with `Cause.pretty(cause)` and return a safe public body. +The config startup regression in #27056 is the failure mode this rule is meant +to avoid: a user-authored invalid `opencode.json` crossed the HttpApi boundary +as a defect, so middleware replaced a useful `ConfigInvalidError` with a safe +generic `UnknownError`. The compatibility fix is to preserve config parse and +validation errors as client-visible `400`s. The target architecture is better: +config loading should fail on the typed error channel, config HTTP handlers +should map those errors to declared `ConfigApiError` responses, and the generic +middleware should never see them. + ## Migration Order Prefer small vertical slices: @@ -113,6 +191,9 @@ Prefer small vertical slices: Good early domains are storage not-found, worktree errors, and provider auth validation errors because they currently drive HTTP behavior. +Config parse and validation errors are also a good early slice because they +are startup-blocking and must be rendered clearly in both CLI and TUI flows. + ## Checklist For A PR - [ ] Expected failures are typed errors, not defects. diff --git a/packages/opencode/src/cli/cmd/tui/context/aggregate-failures.ts b/packages/opencode/src/cli/cmd/tui/context/aggregate-failures.ts index 63b3fb4487..8b652b6512 100644 --- a/packages/opencode/src/cli/cmd/tui/context/aggregate-failures.ts +++ b/packages/opencode/src/cli/cmd/tui/context/aggregate-failures.ts @@ -1,3 +1,5 @@ +import { FormatError } from "@/cli/error" + /** * Aggregate Promise.allSettled results into a single Error that names every * failed endpoint, or return null when all fulfilled. Used at TUI bootstrap @@ -15,7 +17,19 @@ export function aggregateFailures(labeled: LabeledSettled[]): Error | null { ) if (failed.length === 0) return null - const reasons = failed.map((f) => `${f.name}: ${reasonMessage(f.result.reason)}`).join("; ") + const reasons = Array.from( + failed + .map((f) => ({ name: f.name, message: reasonMessage(f.result.reason) })) + .reduce((grouped, failure) => { + grouped.set(failure.message, [...(grouped.get(failure.message) ?? []), failure.name]) + return grouped + }, new Map()) + .entries(), + ) + .map(([message, names]) => + names.length === 1 ? `${names[0]}: ${message}` : `${message}\nAffected startup requests: ${names.join(", ")}`, + ) + .join("; ") const summary = `${failed.length} of ${labeled.length} requests failed: ${reasons}` const err = new Error(summary) err.cause = { failures: failed.map((f) => ({ name: f.name, reason: f.result.reason })) } @@ -23,6 +37,9 @@ export function aggregateFailures(labeled: LabeledSettled[]): Error | null { } function reasonMessage(reason: unknown): string { + const formatted = FormatError(reason) + if (formatted) return formatted + if (reason instanceof Error) return reason.message if (typeof reason === "string") return reason if (reason && typeof reason === "object") { diff --git a/packages/opencode/src/cli/error.ts b/packages/opencode/src/cli/error.ts index c92369b0af..ef724bbbef 100644 --- a/packages/opencode/src/cli/error.ts +++ b/packages/opencode/src/cli/error.ts @@ -2,16 +2,9 @@ import { NamedError } from "@opencode-ai/core/util/error" import { errorFormat } from "@/util/error" import { isRecord } from "@/util/record" -interface ErrorLike { - name?: string - _tag?: string - message?: string - data?: Record -} - type ConfigIssue = { message: string; path: string[] } -function isTaggedError(error: unknown, tag: string): boolean { +function isTaggedError(error: unknown, tag: string): error is Record { return isRecord(error) && error._tag === tag } @@ -39,22 +32,27 @@ function configIssues(input: Record): ConfigIssue[] { : [] } -export function FormatError(input: unknown) { +export function FormatError(input: unknown): string | undefined { + if (input instanceof Error && isRecord(input.cause) && "body" in input.cause) { + const formatted = FormatError(input.cause.body) + if (formatted) return formatted + } + // CliError: domain failure surfaced from an effectCmd handler via fail("...") if (isTaggedError(input, "CliError")) { - const data = input as ErrorLike & { exitCode?: number } - if (data.exitCode != null) process.exitCode = data.exitCode - return data.message ?? "" + if (typeof input.exitCode === "number") process.exitCode = input.exitCode + return stringField(input, "message") ?? "" } // MCPFailed: { name: string } if (NamedError.hasName(input, "MCPFailed")) { - return `MCP server "${(input as ErrorLike).data?.name}" failed. Note, opencode does not support MCP authentication yet.` + const data = isRecord(input) && isRecord(input.data) ? stringField(input.data, "name") : undefined + return `MCP server "${data}" failed. Note, opencode does not support MCP authentication yet.` } // AccountServiceError, AccountTransportError: TaggedErrorClass if (isTaggedError(input, "AccountServiceError") || isTaggedError(input, "AccountTransportError")) { - return (input as ErrorLike).message ?? "" + return stringField(input, "message") ?? "" } // ProviderModelNotFoundError: { providerID: string, modelID: string, suggestions?: string[] } @@ -64,7 +62,7 @@ export function FormatError(input: unknown) { ? providerModelNotFound.suggestions.filter((x) => typeof x === "string") : [] return [ - `Model not found: ${providerModelNotFound.providerID}/${providerModelNotFound.modelID}`, + `Model not found: ${stringField(providerModelNotFound, "providerID")}/${stringField(providerModelNotFound, "modelID")}`, ...(suggestions.length ? ["Did you mean: " + suggestions.join(", ")] : []), `Try: \`opencode models\` to list available models`, `Or check your config (opencode.json) provider/model names`, @@ -112,6 +110,7 @@ export function FormatError(input: unknown) { if (isTaggedError(input, "UICancelledError") || NamedError.hasName(input, "UICancelledError")) { return "" } + return undefined } export function FormatUnknownError(input: unknown): string { diff --git a/packages/opencode/src/server/routes/instance/httpapi/middleware/error.ts b/packages/opencode/src/server/routes/instance/httpapi/middleware/error.ts index 74c690ad6c..7b5643fd68 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/middleware/error.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/middleware/error.ts @@ -1,5 +1,6 @@ import { NamedError } from "@opencode-ai/core/util/error" import * as Log from "@opencode-ai/core/util/log" +import { ConfigError } from "@/config/error" import { Cause, Effect } from "effect" import { HttpRouter, HttpServerError, HttpServerRespondable, HttpServerResponse } from "effect/unstable/http" @@ -18,6 +19,13 @@ export const errorLayer = HttpRouter.middleware<{ handles: unknown }>()((effect) if (!defect) return Effect.failCause(cause) const error = defect.defect + if ( + error instanceof NamedError && + (ConfigError.InvalidError.isInstance(error) || ConfigError.JsonError.isInstance(error)) + ) { + return Effect.succeed(HttpServerResponse.jsonUnsafe(error.toObject(), { status: 400 })) + } + log.error("failed", { error, cause: Cause.pretty(cause) }) return Effect.succeed( diff --git a/packages/opencode/test/cli/cmd/tui/aggregate-failures.test.ts b/packages/opencode/test/cli/cmd/tui/aggregate-failures.test.ts index c9b3551d9a..8256974f64 100644 --- a/packages/opencode/test/cli/cmd/tui/aggregate-failures.test.ts +++ b/packages/opencode/test/cli/cmd/tui/aggregate-failures.test.ts @@ -5,6 +5,7 @@ */ import { describe, expect, test } from "bun:test" import { aggregateFailures } from "@/cli/cmd/tui/context/aggregate-failures" +import { ConfigError } from "@/config/error" describe("aggregateFailures", () => { test("returns null when every result is fulfilled", () => { @@ -41,11 +42,50 @@ describe("aggregateFailures", () => { expect(err!.message).toContain("agents: boom") }) + test("formats structured config errors hidden inside SDK error causes", () => { + const configError = new ConfigError.InvalidError({ + path: "/tmp/opencode.json", + issues: [{ message: "Expected object", path: ["provider", "anthropic", "options"] }], + }) + const err = aggregateFailures([ + { + name: "config.get", + result: { + status: "rejected", + reason: new Error("ConfigInvalidError", { + cause: { + body: configError.toObject(), + }, + }), + }, + }, + ]) + + expect(err!.message).toContain("config.get: Configuration is invalid at /tmp/opencode.json") + expect(err!.message).toContain("Expected object provider.anthropic.options") + }) + + test("deduplicates identical failure messages across startup requests", () => { + const reason = new Error("same config problem") + const err = aggregateFailures([ + { name: "config.providers", result: { status: "rejected", reason } }, + { name: "provider.list", result: { status: "rejected", reason } }, + { name: "app.agents", result: { status: "rejected", reason } }, + { name: "config.get", result: { status: "rejected", reason } }, + { name: "project.sync", result: { status: "fulfilled", value: undefined } }, + ]) + + expect(err!.message).toContain("4 of 5 requests failed: same config problem") + expect(err!.message).toContain( + "Affected startup requests: config.providers, provider.list, app.agents, config.get", + ) + expect(err!.message.match(/same config problem/g)?.length).toBe(1) + }) + test("attaches structured failure list under .cause", () => { const reason = new Error("nope") const err = aggregateFailures([{ name: "providers", result: { status: "rejected", reason } }]) - const cause = err!.cause as { failures: Array<{ name: string; reason: unknown }> } - expect(cause.failures).toEqual([{ name: "providers", reason }]) + expect(err!.cause).toEqual({ failures: [{ name: "providers", reason }] }) }) test("falls back to String() for opaque reasons", () => { diff --git a/packages/opencode/test/server/httpapi-error-middleware.test.ts b/packages/opencode/test/server/httpapi-error-middleware.test.ts index 15f8aa2026..51d4cf9e0c 100644 --- a/packages/opencode/test/server/httpapi-error-middleware.test.ts +++ b/packages/opencode/test/server/httpapi-error-middleware.test.ts @@ -1,6 +1,7 @@ import { NodeHttpServer, NodeServices } from "@effect/platform-node" import { NamedError } from "@opencode-ai/core/util/error" import { describe, expect } from "bun:test" +import { ConfigError } from "../../src/config/error" import { Effect, Layer } from "effect" import { HttpClient, HttpClientRequest, HttpRouter } from "effect/unstable/http" import { errorLayer } from "../../src/server/routes/instance/httpapi/middleware/error" @@ -50,6 +51,27 @@ describe("HttpApi error middleware", () => { }), ) + it.live("preserves config defects as client-visible bad requests", () => + Effect.gen(function* () { + const configError = new ConfigError.InvalidError({ + path: "/tmp/opencode.json", + issues: [{ message: "Expected object", path: ["provider", "anthropic", "options"] }], + }) + + yield* HttpRouter.add("GET", "/config-error", Effect.die(configError)).pipe( + Layer.provide(errorLayer), + HttpRouter.serve, + Layer.build, + ) + + const response = yield* HttpClientRequest.get("/config-error").pipe(HttpClient.execute) + const body = yield* response.json + + expect(response.status).toBe(400) + expect(JSON.stringify(body)).toBe(JSON.stringify(configError.toObject())) + }), + ) + it.live("does not map storage not-found defects to 404", () => Effect.gen(function* () { yield* HttpRouter.add( From d6b23fd8f65f1ff175e5e609c4b922e4d4d4a8b4 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 16 May 2026 00:44:10 +0000 Subject: [PATCH 003/585] chore: generate --- packages/opencode/specs/effect/errors.md | 9 ++++++--- .../opencode/test/cli/cmd/tui/aggregate-failures.test.ts | 4 +--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/opencode/specs/effect/errors.md b/packages/opencode/specs/effect/errors.md index 310857dfd9..fe526c2faa 100644 --- a/packages/opencode/specs/effect/errors.md +++ b/packages/opencode/specs/effect/errors.md @@ -79,9 +79,12 @@ export class Unauthorized extends Schema.TaggedErrorClass()( { httpApiStatus: 401 }, ) {} -export class Authorization extends HttpApiMiddleware.Service()("app/Authorization", { +export class Authorization extends HttpApiMiddleware.Service< + Authorization, + { + provides: CurrentUser + } +>()("app/Authorization", { security: { bearer: HttpApiSecurity.bearer }, error: Unauthorized, }) {} diff --git a/packages/opencode/test/cli/cmd/tui/aggregate-failures.test.ts b/packages/opencode/test/cli/cmd/tui/aggregate-failures.test.ts index 8256974f64..c30d719252 100644 --- a/packages/opencode/test/cli/cmd/tui/aggregate-failures.test.ts +++ b/packages/opencode/test/cli/cmd/tui/aggregate-failures.test.ts @@ -76,9 +76,7 @@ describe("aggregateFailures", () => { ]) expect(err!.message).toContain("4 of 5 requests failed: same config problem") - expect(err!.message).toContain( - "Affected startup requests: config.providers, provider.list, app.agents, config.get", - ) + expect(err!.message).toContain("Affected startup requests: config.providers, provider.list, app.agents, config.get") expect(err!.message.match(/same config problem/g)?.length).toBe(1) }) From ad79ad9ea855cb9e0d327f76fad765b952cb0ef1 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 16 May 2026 03:05:54 +0200 Subject: [PATCH 004/585] upgrade opentui to 0.2.11 (#27808) --- bun.lock | 30 +++++++++++++++--------------- package.json | 6 +++--- packages/plugin/package.json | 6 +++--- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/bun.lock b/bun.lock index 8fa8d02544..b9318708a7 100644 --- a/bun.lock +++ b/bun.lock @@ -536,9 +536,9 @@ "typescript": "catalog:", }, "peerDependencies": { - "@opentui/core": ">=0.2.10", - "@opentui/keymap": ">=0.2.10", - "@opentui/solid": ">=0.2.10", + "@opentui/core": ">=0.2.11", + "@opentui/keymap": ">=0.2.11", + "@opentui/solid": ">=0.2.11", }, "optionalPeers": [ "@opentui/core", @@ -721,9 +721,9 @@ "@npmcli/arborist": "9.4.0", "@octokit/rest": "22.0.0", "@openauthjs/openauth": "0.0.0-20250322224806", - "@opentui/core": "0.2.10", - "@opentui/keymap": "0.2.10", - "@opentui/solid": "0.2.10", + "@opentui/core": "0.2.11", + "@opentui/keymap": "0.2.11", + "@opentui/solid": "0.2.11", "@pierre/diffs": "1.1.0-beta.18", "@playwright/test": "1.59.1", "@sentry/solid": "10.36.0", @@ -1590,23 +1590,23 @@ "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], - "@opentui/core": ["@opentui/core@0.2.10", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.2.10", "@opentui/core-darwin-x64": "0.2.10", "@opentui/core-linux-arm64": "0.2.10", "@opentui/core-linux-x64": "0.2.10", "@opentui/core-win32-arm64": "0.2.10", "@opentui/core-win32-x64": "0.2.10" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-oviCtx0jYjc7F8X2b8+0IkQLg6WH47Nwl6CFeZo5dU0k6OpSbTbi07ZleObaiECAp+S1YLhAtVdgzHU7hBZlaw=="], + "@opentui/core": ["@opentui/core@0.2.11", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.2.11", "@opentui/core-darwin-x64": "0.2.11", "@opentui/core-linux-arm64": "0.2.11", "@opentui/core-linux-x64": "0.2.11", "@opentui/core-win32-arm64": "0.2.11", "@opentui/core-win32-x64": "0.2.11" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-X0zLmcDEvMrPzWYp769I7VEVb+og38vaete9tGZXu9HnJgu/paPUUplUT+6denBQccr2qx1rBYV6EtgbBpLEyw=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+lbDDj42Og+UtTZEwlHhGXichmOlkxSqn0J+Jqjat5/Tt5oZykj1NZjFIQ7ZSz4Miz7EmZwgYKE2CyOmmm9MoQ=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-h2MXtE2Cu3XlKVoQMXthnbhleO68zGXkoh/r1Q5pCoZh6RuXqns5/94D/aZThXBWwzPuEoyarMlxxR9OqrpvHw=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-5iAoA0aqMWWAQ93nh8Bb0ipwt9h+tvEFc88+YO9St43uUJ+XrXcmMj3T8wtl6dSu/SN0UoDWNaUMHUmtykiPtg=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-Y0jbPClnOBTPSIy+2THG86MTqIG/jGFlOOKuw4JfCDqEjPBM3pLWIHnJb3WxHRi2LlvfyBxvrUTXWlW6JpI0QQ=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-EnrkxgH5K76Oi/Br1UHPZblXG5P60snmtySfnxuVaeECNZrbTkV6BV/A0WoBeWshJweGbx1D+eTF+sEEjQCi8w=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-blQyyuTaW4q/OQ3whs7Kt7GCXhBUR5EQHHDdjOqQAr0HYpohUa6sbHMbiBcX2Ehc9ZWwtiaOoWiyZ5YXy2SAvg=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.10", "", { "os": "linux", "cpu": "x64" }, "sha512-fI+r3kCPqIxsWwPVGpKUQy4zHK8y+jkDRCwa3UbaUy48RQ44jMuf2RhVhmi4xmCvSc8UPJBbYsw1tLuh9kmXjg=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.11", "", { "os": "linux", "cpu": "x64" }, "sha512-0nEB5+MgzQRYiVcQd1vHXPWNPWGh4JEmQTJKyG3OHnTzPaJ1FVSQ/V71ECyRSl3ymY3F+U0eW9cFgw1hCieK2w=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-8F4z2hIRgkVWcr6CMVeJ9N4+1rmURPt2Pq2GBPko8ch6rxHR+a//KD1MfphyuLTHBS1tJ4vfZSWSoiaESImtrA=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-+KKH77fzm0qF8py9G2pU32DzB1bAgDMfBajrs7gKL5NtSEnknrwfh7hIs/tq41aF6j9zvIzgtykByh26tcjFog=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.10", "", { "os": "win32", "cpu": "x64" }, "sha512-Ki+qNBlIFW5K2wcG/RHrlPp7yEQKXeiNX3mlje25iwX62Ac5w391HBpOmUjbPoq20McPyDRnhbLfbXQSPtickg=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.11", "", { "os": "win32", "cpu": "x64" }, "sha512-dMmb9DX0W0HWadLdgciMbonqIc1xdcKiVmaQSYxw5eGCzFRPZIOrKHByesP+2ipkMuLx85W/MJUFal/lW8XSNg=="], - "@opentui/keymap": ["@opentui/keymap@0.2.10", "", { "dependencies": { "@opentui/core": "0.2.10" }, "peerDependencies": { "@opentui/react": "0.2.10", "@opentui/solid": "0.2.10", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-80fU3Lr/98sNIpVYd8PApAeQw8A8D9BemyOGi6jGvTQCl0rxKgvaVBviDRGKxl1INTVjZy9By8UPncc2KJOuWQ=="], + "@opentui/keymap": ["@opentui/keymap@0.2.11", "", { "dependencies": { "@opentui/core": "0.2.11" }, "peerDependencies": { "@opentui/react": "0.2.11", "@opentui/solid": "0.2.11", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-pCrJrY3mTuXdDaaRneId1JsJCtGE+7prTtWihzOLZzVJTJYyYtT38gMI7MpyAoloVDfEL5cTe8C+v7wv+IYREw=="], - "@opentui/solid": ["@opentui/solid@0.2.10", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.10", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-+4/MB90yIQiPwg8Y4wY092yva9BvRTsJeeeEO3e2H7P8k8zxYk4G9bzuhqYLxA9mTVQ+zVDlrmFoPQhT7vpIRw=="], + "@opentui/solid": ["@opentui/solid@0.2.11", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.11", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-M3WHxBFORHVE0yqMJYpi9PfjXWlnRTw/LYuBhZaJv0HTo+zTs60P/ukGcwnHDWnMpTGf3BH9x0Yi2dIqjHRY6Q=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], diff --git a/package.json b/package.json index a3400fbfb9..fb44c2f8b7 100644 --- a/package.json +++ b/package.json @@ -35,9 +35,9 @@ "@types/cross-spawn": "6.0.6", "@octokit/rest": "22.0.0", "@hono/zod-validator": "0.4.2", - "@opentui/core": "0.2.10", - "@opentui/keymap": "0.2.10", - "@opentui/solid": "0.2.10", + "@opentui/core": "0.2.11", + "@opentui/keymap": "0.2.11", + "@opentui/solid": "0.2.11", "ulid": "3.0.1", "@kobalte/core": "0.13.11", "@types/luxon": "3.7.1", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 67055fdcd9..93882a77f5 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -22,9 +22,9 @@ "zod": "catalog:" }, "peerDependencies": { - "@opentui/core": ">=0.2.10", - "@opentui/keymap": ">=0.2.10", - "@opentui/solid": ">=0.2.10" + "@opentui/core": ">=0.2.11", + "@opentui/keymap": ">=0.2.11", + "@opentui/solid": ">=0.2.11" }, "peerDependenciesMeta": { "@opentui/core": { From d441e931f995d6be39058dfd9b19e5603385af36 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 16 May 2026 03:11:46 +0200 Subject: [PATCH 005/585] add dialog prompt submit keybind (#27807) --- .../src/cli/cmd/tui/config/keybind.ts | 1 + .../src/cli/cmd/tui/ui/dialog-prompt.tsx | 41 ++++- .../test/cli/tui/dialog-prompt.test.tsx | 146 ++++++++++++++++++ packages/opencode/test/config/tui.test.ts | 2 + packages/web/src/content/docs/keybinds.mdx | 1 + 5 files changed, 183 insertions(+), 8 deletions(-) create mode 100644 packages/opencode/test/cli/tui/dialog-prompt.test.tsx diff --git a/packages/opencode/src/cli/cmd/tui/config/keybind.ts b/packages/opencode/src/cli/cmd/tui/config/keybind.ts index bd26cd5d95..a375573828 100644 --- a/packages/opencode/src/cli/cmd/tui/config/keybind.ts +++ b/packages/opencode/src/cli/cmd/tui/config/keybind.ts @@ -188,6 +188,7 @@ export const Definitions = { "dialog.select.home": keybind("home", "Move to first dialog item"), "dialog.select.end": keybind("end", "Move to last dialog item"), "dialog.select.submit": keybind("return", "Submit selected dialog item"), + "dialog.prompt.submit": keybind("return", "Submit dialog prompt"), "dialog.mcp.toggle": keybind("space", "Toggle MCP in MCP dialog"), "prompt.autocomplete.prev": keybind("up,ctrl+p", "Move to previous autocomplete item"), "prompt.autocomplete.next": keybind("down,ctrl+n", "Move to next autocomplete item"), diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx index 34ab9161f6..dfd8091852 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx @@ -1,8 +1,10 @@ import { TextareaRenderable, TextAttributes } from "@opentui/core" import { useTheme } from "../context/theme" import { useDialog, type DialogContext } from "./dialog" -import { Show, createEffect, onMount, type JSX } from "solid-js" +import { Show, createEffect, createSignal, onMount, type JSX } from "solid-js" import { Spinner } from "../component/spinner" +import { useTuiConfig } from "../context/tui-config" +import { useBindings, useCommandShortcut } from "../keymap" export type DialogPromptProps = { title: string @@ -18,8 +20,32 @@ export type DialogPromptProps = { export function DialogPrompt(props: DialogPromptProps) { const dialog = useDialog() const { theme } = useTheme() + const tuiConfig = useTuiConfig() + const submitShortcut = useCommandShortcut("dialog.prompt.submit") + const [textareaTarget, setTextareaTarget] = createSignal() let textarea: TextareaRenderable + function confirm() { + if (props.busy) return + props.onConfirm?.(textarea.plainText) + } + + useBindings(() => ({ + target: textareaTarget, + enabled: textareaTarget() !== undefined && !props.busy, + // Dialog form semantics must win over the global managed textarea input layer. + priority: 1, + commands: [ + { + name: "dialog.prompt.submit", + title: "Submit dialog prompt", + category: "Dialog", + run: confirm, + }, + ], + bindings: tuiConfig.keybinds.gather("dialog.prompt", ["dialog.prompt.submit"]), + })) + onMount(() => { dialog.setSize("medium") setTimeout(() => { @@ -59,13 +85,10 @@ export function DialogPrompt(props: DialogPromptProps) { {props.description}