From 43c08387f1b3bf5db0afec22a0d5c14571fe2af3 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 20 Jul 2026 21:42:02 -0400 Subject: [PATCH] refactor: isolate legacy flags --- packages/cli/src/index.ts | 1 + packages/cli/src/mini-host.ts | 3 +- packages/cli/src/server-process.ts | 4 ++ packages/cli/src/services/updater.ts | 6 ++- packages/core/src/config.ts | 27 +++++++++++ packages/core/src/models-dev.ts | 7 ++- packages/core/src/observability.ts | 2 + packages/core/src/observability/otlp.ts | 14 ++++-- packages/core/src/session/compaction.ts | 21 +++++---- packages/core/src/session/generate-node.ts | 18 +++++--- packages/core/src/session/model-headers.ts | 14 ++++-- packages/core/src/session/model-request.ts | 14 +++--- packages/core/src/session/title.ts | 21 +++++---- packages/core/src/tool/websearch.ts | 10 ++-- packages/core/test/config/config.test.ts | 46 +++++++++++++++++++ .../core/test/effect/observability.test.ts | 10 ++-- packages/core/test/session-compact.test.ts | 2 +- packages/core/test/session-compaction.test.ts | 3 +- packages/core/test/session-runner.test.ts | 3 +- packages/core/test/session-title.test.ts | 3 +- packages/opencode/src/cli/cmd/debug/index.ts | 2 +- packages/opencode/src/cli/cmd/serve.ts | 2 +- packages/opencode/src/cli/cmd/session.ts | 2 +- packages/opencode/src/cli/cmd/web.ts | 2 +- packages/opencode/src/cli/heap.ts | 2 +- packages/opencode/src/cli/upgrade.ts | 2 +- packages/opencode/src/config/config.ts | 2 +- packages/opencode/src/config/paths.ts | 2 +- packages/opencode/src/config/tui-migrate.ts | 2 +- packages/opencode/src/config/tui.ts | 2 +- packages/{core => opencode}/src/flag/flag.ts | 16 +++---- .../opencode/src/installation/database.ts | 2 +- packages/opencode/src/plugin/meta.ts | 2 +- packages/opencode/src/plugin/tui/runtime.ts | 2 +- packages/opencode/src/project/project.ts | 2 +- packages/opencode/src/server/auth.ts | 2 +- .../instance/httpapi/middleware/fence.ts | 2 +- .../httpapi/middleware/workspace-routing.ts | 2 +- packages/opencode/src/session/instruction.ts | 2 +- packages/opencode/test/fixture/flag.ts | 2 +- packages/opencode/test/server/auth.test.ts | 2 +- .../opencode/test/server/httpapi-cors.test.ts | 2 +- .../server/httpapi-exercise/environment.ts | 2 +- .../test/server/httpapi-exercise/runner.ts | 2 +- .../test/server/httpapi-instance.test.ts | 2 +- .../test/server/httpapi-listen.test.ts | 2 +- .../opencode/test/server/httpapi-mdns.test.ts | 2 +- .../server/httpapi-query-schema-drift.test.ts | 2 +- .../opencode/test/server/httpapi-sdk.test.ts | 2 +- .../test/server/httpapi-session.test.ts | 2 +- .../opencode/test/server/httpapi-sync.test.ts | 2 +- .../opencode/test/server/httpapi-ui.test.ts | 2 +- .../test/server/httpapi-workspace.test.ts | 2 +- .../server/worktree-endpoint-repro.test.ts | 2 +- packages/server/src/auth.ts | 23 ++-------- packages/server/src/options.ts | 4 ++ packages/server/src/process.ts | 2 +- packages/server/src/routes.ts | 31 +++++++++---- packages/server/test/auth.test.ts | 13 ++++++ packages/simulation/src/backend/index.ts | 2 +- packages/tui/src/app.tsx | 16 +++---- packages/tui/src/component/dialog-config.tsx | 16 +++++++ packages/tui/src/component/prompt/index.tsx | 1 - packages/tui/src/config/index.tsx | 2 + packages/tui/src/ui/dialog.tsx | 8 ++-- 65 files changed, 282 insertions(+), 147 deletions(-) rename packages/{core => opencode}/src/flag/flag.ts (92%) create mode 100644 packages/server/test/auth.test.ts diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index cea7992f82..db269e507f 100755 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -72,6 +72,7 @@ Effect.logInfo("cli starting", { Observability.layer({ endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT, headers: process.env.OTEL_EXPORTER_OTLP_HEADERS, + client: process.env.OPENCODE_CLIENT ?? "cli", }), ), Effect.provide(NodeServices.layer), diff --git a/packages/cli/src/mini-host.ts b/packages/cli/src/mini-host.ts index d410dce09a..935248691f 100644 --- a/packages/cli/src/mini-host.ts +++ b/packages/cli/src/mini-host.ts @@ -1,6 +1,5 @@ import type { MiniFrontendInput } from "@opencode-ai/tui/mini" import { createModelPreferenceRepository } from "@opencode-ai/tui/model-preference" -import { Flag } from "@opencode-ai/core/flag/flag" import { Global } from "@opencode-ai/core/global" import fs from "node:fs" import { readFile } from "node:fs/promises" @@ -165,7 +164,7 @@ export function createMiniHost(input: { sigusr2: signal("SIGUSR2"), }, startup: { - showTiming: Flag.OPENCODE_SHOW_TTFD, + showTiming: ["1", "true"].includes(process.env.OPENCODE_SHOW_TTFD?.toLowerCase() ?? ""), now: () => performance.now(), }, diagnostics: { diff --git a/packages/cli/src/server-process.ts b/packages/cli/src/server-process.ts index f0629f12b0..fd952139e5 100644 --- a/packages/cli/src/server-process.ts +++ b/packages/cli/src/server-process.ts @@ -69,9 +69,11 @@ const processEffect = Effect.fnUntraced(function* (options: Options) { const instanceID = randomUUID() const server = yield* start( { + client: process.env.OPENCODE_CLIENT ?? "cli", hostname, port, password, + simulation: truthy(process.env.OPENCODE_SIMULATE), database: { path: process.env.OPENCODE_DB, }, @@ -89,6 +91,8 @@ const processEffect = Effect.fnUntraced(function* (options: Options) { project: !truthy( process.env.OPENCODE_CONFIG_PROJECT_DISABLE ?? process.env.OPENCODE_DISABLE_PROJECT_CONFIG, ), + file: process.env.OPENCODE_CONFIG, + content: process.env.OPENCODE_CONFIG_CONTENT, }, windows: { gitbash: process.env.OPENCODE_GIT_BASH_PATH, diff --git a/packages/cli/src/services/updater.ts b/packages/cli/src/services/updater.ts index ea0e70fab5..db43fe334f 100644 --- a/packages/cli/src/services/updater.ts +++ b/packages/cli/src/services/updater.ts @@ -1,5 +1,4 @@ import { Global } from "@opencode-ai/core/global" -import { Flag } from "@opencode-ai/core/flag/flag" import { AppProcess } from "@opencode-ai/core/process" import { InstallationChannel, @@ -138,7 +137,10 @@ export const layer = Layer.effect( }) const check = Effect.fn("cli.updater.check")(function* () { - if (InstallationLocal || Flag.OPENCODE_DISABLE_AUTOUPDATE) + if ( + InstallationLocal || + ["1", "true"].includes(process.env.OPENCODE_DISABLE_AUTOUPDATE?.toLowerCase() ?? "") + ) return yield* Effect.logInfo("update check skipped", { reason: InstallationLocal ? "local-install" : "disabled", version: InstallationVersion, diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 5d7c2c5210..575e35f4c4 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -155,6 +155,8 @@ export interface Interface { export const Options = Schema.Struct({ project: Schema.optional(Schema.Boolean), + file: Schema.optional(Schema.String), + content: Schema.optional(Schema.String), }) export type Options = typeof Options.Type @@ -293,14 +295,39 @@ export const layer = (options?: Options) => Layer.effect( Effect.map((entries) => entries.flat()), ) + const file = options?.file + const explicit = file + ? yield* loadFile(path.resolve(file)).pipe( + Effect.map((config) => [ + ...(config ? [config] : []), + new File({ type: "file", path: AbsolutePath.make(path.resolve(file)) }), + ]), + Effect.orDie, + ) + : [] + const content = options?.content + ? yield* ConfigVariable.substitute({ + type: "virtual", + source: "OPENCODE_CONFIG_CONTENT", + dir: location.directory, + text: options.content, + }).pipe( + Effect.map(parseInfo), + Effect.map((info) => (info ? [new Document({ type: "document", info })] : [])), + Effect.orDie, + ) + : [] + const supplementary = yield* Effect.forEach(directories, loadDirectory).pipe(Effect.orDie) return [ ...claude, ...agents, ...(supplementary[0] ?? []), + ...explicit, ...direct, ...supplementary.slice(1).flat(), ...(yield* loadWellknown().pipe(Effect.orDie)), + ...content, ] }) diff --git a/packages/core/src/models-dev.ts b/packages/core/src/models-dev.ts index 857f28d945..2e181493e4 100644 --- a/packages/core/src/models-dev.ts +++ b/packages/core/src/models-dev.ts @@ -4,7 +4,6 @@ import { HttpClient, HttpClientRequest } from "effect/unstable/http" import { ModelsDev } from "@opencode-ai/schema/models-dev" import { Money } from "@opencode-ai/schema/money" import { Global } from "./global" -import { Flag } from "./flag/flag" import { Flock } from "./util/flock" import { Hash } from "./util/hash" import { FSUtil } from "./fs-util" @@ -18,8 +17,6 @@ import { ProviderV2 } from "./provider" export const CatalogModelStatus = Schema.Literals(["alpha", "beta", "deprecated"]) export type CatalogModelStatus = typeof CatalogModelStatus.Type -const USER_AGENT = `opencode/${InstallationChannel}/${InstallationVersion}/${Flag.OPENCODE_CLIENT}` - type Cost = { readonly input: Money.USDPerMillionTokens readonly output: Money.USDPerMillionTokens @@ -534,6 +531,7 @@ export const Options = Schema.Struct({ url: Schema.optional(Schema.String), file: Schema.optional(Schema.String), fetch: Schema.optional(Schema.Boolean), + client: Schema.optional(Schema.String), }) export type Options = typeof Options.Type @@ -556,6 +554,7 @@ export const layer = (options?: Options) => Layer.effect( const source = options?.url ?? "https://models.dev" const fetch = options?.fetch ?? true + const userAgent = `opencode/${InstallationChannel}/${InstallationVersion}/${options?.client ?? "cli"}` const filepath = path.join( Global.Path.cache, source === "https://models.dev" ? "models.json" : `models-${Hash.fast(source)}.json`, @@ -572,7 +571,7 @@ export const layer = (options?: Options) => Layer.effect( const fetchApi = Effect.fn("ModelsDev.fetchApi")(function* () { return yield* HttpClientRequest.get(`${source}/api.json`).pipe( - HttpClientRequest.setHeader("User-Agent", USER_AGENT), + HttpClientRequest.setHeader("User-Agent", userAgent), http.execute, Effect.flatMap((res) => res.text), Effect.timeout("10 seconds"), diff --git a/packages/core/src/observability.ts b/packages/core/src/observability.ts index 71a99ce2e7..8581eab42e 100644 --- a/packages/core/src/observability.ts +++ b/packages/core/src/observability.ts @@ -11,6 +11,7 @@ import { Otlp } from "./observability/otlp" export const Options = Schema.Struct({ endpoint: Schema.optional(Schema.String), headers: Schema.optional(Schema.String), + client: Schema.optional(Schema.String), }) export type Options = typeof Options.Type @@ -18,6 +19,7 @@ export function layer( options: Options = { endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT, headers: process.env.OTEL_EXPORTER_OTLP_HEADERS, + client: process.env.OPENCODE_CLIENT ?? "cli", }, ) { const local = Logger.layer(Logging.loggers(), { mergeWithExisting: false }).pipe( diff --git a/packages/core/src/observability/otlp.ts b/packages/core/src/observability/otlp.ts index d64fbe9940..edf2e71254 100644 --- a/packages/core/src/observability/otlp.ts +++ b/packages/core/src/observability/otlp.ts @@ -1,12 +1,12 @@ import { Layer } from "effect" import { OtlpLogger } from "effect/unstable/observability" -import { Flag } from "../flag/flag" import { InstallationChannel, InstallationVersion } from "../installation/version" import { runID } from "./shared" export interface Options { readonly endpoint?: string readonly headers?: string + readonly client?: string } function parseHeaders(value?: string) { @@ -38,14 +38,14 @@ function resourceAttributes() { } } -export function resource(): { serviceName: string; serviceVersion: string; attributes: Record } { +export function resource(client = "cli"): { serviceName: string; serviceVersion: string; attributes: Record } { return { serviceName: "opencode", serviceVersion: InstallationVersion, attributes: { ...resourceAttributes(), "deployment.environment.name": InstallationChannel, - "opencode.client": Flag.OPENCODE_CLIENT, + "opencode.client": client, "opencode.run": runID, "service.instance.id": runID, }, @@ -55,7 +55,11 @@ export function resource(): { serviceName: string; serviceVersion: string; attri export function loggers(options?: Options) { if (!options?.endpoint) return [] return [ - OtlpLogger.make({ url: `${options.endpoint}/v1/logs`, resource: resource(), headers: parseHeaders(options.headers) }), + OtlpLogger.make({ + url: `${options.endpoint}/v1/logs`, + resource: resource(options.client), + headers: parseHeaders(options.headers), + }), ] } @@ -73,7 +77,7 @@ export async function tracingLayer(options?: Options) { context.setGlobalContextManager(manager) return NodeSdk.layer(() => ({ - resource: resource(), + resource: resource(options.client), spanProcessor: new SdkBase.BatchSpanProcessor( new OTLP.OTLPTraceExporter({ url: `${options.endpoint}/v1/traces`, diff --git a/packages/core/src/session/compaction.ts b/packages/core/src/session/compaction.ts index b49fc50ba8..f67d65e2fd 100644 --- a/packages/core/src/session/compaction.ts +++ b/packages/core/src/session/compaction.ts @@ -60,6 +60,7 @@ type Settings = { } type Dependencies = { + readonly headers?: SessionModelHeaders.Options readonly events: EventV2.Interface readonly llm: { readonly stream: (request: LLMRequest) => Stream.Stream @@ -258,7 +259,7 @@ const make = (dependencies: Dependencies) => { .stream( LLM.request({ model: plan.model, - http: { headers: SessionModelHeaders.make(plan.session) }, + http: { headers: SessionModelHeaders.make(plan.session, dependencies.headers) }, messages: [Message.user(plan.prompt)], tools: [], }), @@ -390,19 +391,23 @@ const make = (dependencies: Dependencies) => { }) } -export const layer = Layer.effect( +export const layer = (options?: SessionModelHeaders.Options) => Layer.effect( Service, Effect.gen(function* () { const events = yield* EventV2.Service const llm = yield* LLMClient.Service const config = yield* Config.Service const models = yield* SessionRunnerModel.Service - return make({ events, llm, models, config: settings(yield* config.entries()) }) + return make({ events, llm, models, config: settings(yield* config.entries()), headers: options }) }), ) -export const node = makeLocationNode({ - service: Service, - layer, - deps: [EventV2.node, llmClient, Config.node, SessionRunnerModel.node], -}) +export function configured(options?: SessionModelHeaders.Options) { + return makeLocationNode({ + service: Service, + layer: layer(options), + deps: [EventV2.node, llmClient, Config.node, SessionRunnerModel.node], + }) +} + +export const node = configured() diff --git a/packages/core/src/session/generate-node.ts b/packages/core/src/session/generate-node.ts index 7d7e88e90f..6fb5b37f37 100644 --- a/packages/core/src/session/generate-node.ts +++ b/packages/core/src/session/generate-node.ts @@ -14,7 +14,7 @@ import { SessionRunnerModel } from "./runner/model" import PROMPT_DEFAULT from "./runner/prompt/base.txt" import { toLLMMessages } from "./runner/to-llm-message" -const layer = Layer.effect( +export const layer = (options?: SessionModelHeaders.Options) => Layer.effect( SessionGenerate.Service, Effect.gen(function* () { const context = yield* SessionContext.Service @@ -49,7 +49,7 @@ const layer = Layer.effect( return (yield* llm.generate( LLM.request({ model: model.model, - http: { headers: SessionModelHeaders.make(selection.session) }, + http: { headers: SessionModelHeaders.make(selection.session, options) }, providerOptions: { openai: { promptCacheKey } }, system: contextEvent.system, messages: contextEvent.messages, @@ -62,8 +62,12 @@ const layer = Layer.effect( }), ) -export const node = makeLocationNode({ - service: SessionGenerate.Service, - layer, - deps: [SessionContext.node, Database.node, PluginHooks.node, SessionRunnerModel.node, llmClient], -}) +export function configured(options?: SessionModelHeaders.Options) { + return makeLocationNode({ + service: SessionGenerate.Service, + layer: layer(options), + deps: [SessionContext.node, Database.node, PluginHooks.node, SessionRunnerModel.node, llmClient], + }) +} + +export const node = configured() diff --git a/packages/core/src/session/model-headers.ts b/packages/core/src/session/model-headers.ts index efc6d5e23b..8b835da010 100644 --- a/packages/core/src/session/model-headers.ts +++ b/packages/core/src/session/model-headers.ts @@ -1,15 +1,23 @@ export * as SessionModelHeaders from "./model-headers" -import { Flag } from "../flag/flag" import { InstallationVersion } from "../installation/version" import { SessionSchema } from "./schema" +import { Schema } from "effect" -export const make = (session: Pick) => ({ +export const Options = Schema.Struct({ + client: Schema.optional(Schema.String), +}) +export type Options = typeof Options.Type + +export const make = ( + session: Pick, + options?: Options, +) => ({ "x-session-affinity": session.id, "X-Session-Id": session.id, ...(session.parentID ? { "x-parent-session-id": session.parentID } : {}), "User-Agent": `opencode/${InstallationVersion}`, "x-opencode-project": session.projectID, "x-opencode-session": session.id, - "x-opencode-client": Flag.OPENCODE_CLIENT, + "x-opencode-client": options?.client ?? "cli", }) diff --git a/packages/core/src/session/model-request.ts b/packages/core/src/session/model-request.ts index 936c30591a..2d76c20023 100644 --- a/packages/core/src/session/model-request.ts +++ b/packages/core/src/session/model-request.ts @@ -39,7 +39,7 @@ export interface Interface { /** Location-scoped outbound model-request preparation. */ export class Service extends Context.Service()("@opencode/v2/SessionModelRequest") {} -const layer = Layer.effect( +export const layer = (options?: SessionModelHeaders.Options) => Layer.effect( Service, Effect.gen(function* () { const hooks = yield* PluginHooks.Service @@ -81,7 +81,7 @@ const layer = Layer.effect( const request = LLM.request({ model, http: { - headers: SessionModelHeaders.make(session), + headers: SessionModelHeaders.make(session, options), }, providerOptions: { openai: { promptCacheKey } }, system: contextEvent.system, @@ -112,8 +112,8 @@ const layer = Layer.effect( }), ) -export const node = makeLocationNode({ - service: Service, - layer, - deps: [PluginHooks.node, ToolRegistry.node], -}) +export function configured(options?: SessionModelHeaders.Options) { + return makeLocationNode({ service: Service, layer: layer(options), deps: [PluginHooks.node, ToolRegistry.node] }) +} + +export const node = configured() diff --git a/packages/core/src/session/title.ts b/packages/core/src/session/title.ts index f86741d5b2..58359c73d5 100644 --- a/packages/core/src/session/title.ts +++ b/packages/core/src/session/title.ts @@ -17,6 +17,7 @@ import { SessionUsage } from "./usage" const MAX_LENGTH = 100 type Dependencies = { + readonly headers?: SessionModelHeaders.Options readonly events: EventV2.Interface readonly llm: { readonly stream: (request: LLMRequest) => Stream.Stream @@ -66,7 +67,7 @@ const make = (dependencies: Dependencies) => { .stream( LLM.request({ model: resolved.model, - http: { headers: SessionModelHeaders.make(session) }, + http: { headers: SessionModelHeaders.make(session, dependencies.headers) }, system: agent.system, messages: [Message.user(firstUser.text)], tools: [], @@ -102,7 +103,7 @@ const make = (dependencies: Dependencies) => { return { generateForFirstPrompt } } -export const layer = Layer.effect( +export const layer = (options?: SessionModelHeaders.Options) => Layer.effect( Service, Effect.gen(function* () { const events = yield* EventV2.Service @@ -110,15 +111,19 @@ export const layer = Layer.effect( const agents = yield* AgentV2.Service const models = yield* SessionRunnerModel.Service const database = yield* Database.Service - const title = make({ events, llm, agents, models }) + const title = make({ events, llm, agents, models, headers: options }) return Service.of({ generateForFirstPrompt: (session) => title.generateForFirstPrompt(database.db, session), }) }), ) -export const node = makeLocationNode({ - service: Service, - layer, - deps: [EventV2.node, llmClient, AgentV2.node, SessionRunnerModel.node, Database.node], -}) +export function configured(options?: SessionModelHeaders.Options) { + return makeLocationNode({ + service: Service, + layer: layer(options), + deps: [EventV2.node, llmClient, AgentV2.node, SessionRunnerModel.node, Database.node], + }) +} + +export const node = configured() diff --git a/packages/core/src/tool/websearch.ts b/packages/core/src/tool/websearch.ts index a92ce5f8ef..803f6588c8 100644 --- a/packages/core/src/tool/websearch.ts +++ b/packages/core/src/tool/websearch.ts @@ -5,7 +5,6 @@ import { ToolFailure } from "@opencode-ai/ai" import { Context, Duration, Effect, Layer, Schema } from "effect" import { HttpClient, HttpClientRequest } from "effect/unstable/http" import { makeLocationNode } from "../effect/app-node" -import { truthy } from "../flag/flag" import { InstallationVersion } from "../installation/version" import { PositiveInt } from "../schema" import { PermissionV2 } from "../permission" @@ -74,8 +73,13 @@ export const defaultConfigLayer = Layer.sync(ConfigService, () => process.env.OPENCODE_WEBSEARCH_PROVIDER === "exa" || process.env.OPENCODE_WEBSEARCH_PROVIDER === "parallel" ? process.env.OPENCODE_WEBSEARCH_PROVIDER : undefined, - enableExa: truthy("OPENCODE_EXPERIMENTAL") || truthy("OPENCODE_ENABLE_EXA") || truthy("OPENCODE_EXPERIMENTAL_EXA"), - enableParallel: truthy("OPENCODE_ENABLE_PARALLEL") || truthy("OPENCODE_EXPERIMENTAL_PARALLEL"), + enableExa: + ["1", "true"].includes(process.env.OPENCODE_EXPERIMENTAL?.toLowerCase() ?? "") || + ["1", "true"].includes(process.env.OPENCODE_ENABLE_EXA?.toLowerCase() ?? "") || + ["1", "true"].includes(process.env.OPENCODE_EXPERIMENTAL_EXA?.toLowerCase() ?? ""), + enableParallel: + ["1", "true"].includes(process.env.OPENCODE_ENABLE_PARALLEL?.toLowerCase() ?? "") || + ["1", "true"].includes(process.env.OPENCODE_EXPERIMENTAL_PARALLEL?.toLowerCase() ?? ""), exaApiKey: process.env.EXA_API_KEY, parallelApiKey: process.env.PARALLEL_API_KEY, }), diff --git a/packages/core/test/config/config.test.ts b/packages/core/test/config/config.test.ts index 77ed9bbb73..dbef8cf06a 100644 --- a/packages/core/test/config/config.test.ts +++ b/packages/core/test/config/config.test.ts @@ -100,6 +100,52 @@ const provider = { } describe("Config", () => { + it.live("loads explicit file and content overrides in priority order", () => + Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ).pipe( + Effect.flatMap((tmp) => { + const global = path.join(tmp.path, "global") + const project = path.join(tmp.path, "project") + const explicit = path.join(tmp.path, "custom.json") + return Effect.promise(async () => { + await fs.mkdir(global, { recursive: true }) + await fs.mkdir(project, { recursive: true }) + await fs.writeFile(path.join(global, "opencode.json"), JSON.stringify({ shell: "global" })) + await fs.writeFile(explicit, JSON.stringify({ shell: "explicit" })) + await fs.writeFile(path.join(project, "opencode.json"), JSON.stringify({ shell: "project" })) + }).pipe( + Effect.andThen( + Effect.gen(function* () { + const config = yield* Config.Service + const entries = yield* config.entries() + expect( + entries.flatMap((entry) => + entry.type === "document" && entry.info.shell ? [entry.info.shell] : [], + ), + ).toEqual(["global", "explicit", "project", "content"]) + expect(Config.latest(entries, "shell")).toBe("content") + }).pipe( + Effect.provide( + testLayer( + project, + global, + project, + undefined, + undefined, + emptyCredentialNode, + emptyWellknownNode, + { file: explicit, content: JSON.stringify({ shell: "content" }) }, + ), + ), + ), + ), + ) + }), + ), + ) + it.live("skips project configuration when project discovery is disabled", () => Effect.acquireRelease( Effect.promise(() => tmpdir()), diff --git a/packages/core/test/effect/observability.test.ts b/packages/core/test/effect/observability.test.ts index 792978446b..39fe91a4e5 100644 --- a/packages/core/test/effect/observability.test.ts +++ b/packages/core/test/effect/observability.test.ts @@ -8,14 +8,11 @@ import { fileLogger } from "../../src/observability/logging" import { resource } from "../../src/observability/otlp" const otelResourceAttributes = process.env.OTEL_RESOURCE_ATTRIBUTES -const opencodeClient = process.env.OPENCODE_CLIENT afterEach(() => { if (otelResourceAttributes === undefined) delete process.env.OTEL_RESOURCE_ATTRIBUTES else process.env.OTEL_RESOURCE_ATTRIBUTES = otelResourceAttributes - if (opencodeClient === undefined) delete process.env.OPENCODE_CLIENT - else process.env.OPENCODE_CLIENT = opencodeClient }) describe("resource", () => { @@ -39,16 +36,15 @@ describe("resource", () => { }) test("keeps built-in attributes when env values conflict", () => { - process.env.OPENCODE_CLIENT = "cli" process.env.OTEL_RESOURCE_ATTRIBUTES = "opencode.client=web,service.instance.id=override,service.namespace=anomalyco" - expect(resource().attributes).toMatchObject({ + expect(resource("cli").attributes).toMatchObject({ "opencode.client": "cli", "service.namespace": "anomalyco", }) - expect(resource().attributes["service.instance.id"]).not.toBe("override") - expect(resource().attributes["opencode.run"]).toMatch(/^[0-9a-f]{8}$/) + expect(resource("cli").attributes["service.instance.id"]).not.toBe("override") + expect(resource("cli").attributes["opencode.run"]).toMatch(/^[0-9a-f]{8}$/) }) }) diff --git a/packages/core/test/session-compact.test.ts b/packages/core/test/session-compact.test.ts index da90068ea4..07e15e35cb 100644 --- a/packages/core/test/session-compact.test.ts +++ b/packages/core/test/session-compact.test.ts @@ -56,7 +56,7 @@ const locations = Layer.effect( () => // The test only needs the compaction location service used by SessionV2.compact. // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion - SessionCompaction.layer.pipe( + SessionCompaction.layer().pipe( Layer.provide(client), Layer.provide(config), Layer.provide(models), diff --git a/packages/core/test/session-compaction.test.ts b/packages/core/test/session-compaction.test.ts index ad3e0a2bfb..48bf53030c 100644 --- a/packages/core/test/session-compaction.test.ts +++ b/packages/core/test/session-compaction.test.ts @@ -18,7 +18,6 @@ import { SessionStore } from "@opencode-ai/core/session/store" import { SessionV2 } from "@opencode-ai/core/session" import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" -import { Flag } from "@opencode-ai/core/flag/flag" import { InstallationVersion } from "@opencode-ai/core/installation/version" import { AbsolutePath } from "@opencode-ai/core/schema" import { Money } from "@opencode-ai/schema/money" @@ -190,7 +189,7 @@ it.effect("manual compaction summarizes short context instead of no-op", () => "User-Agent": `opencode/${InstallationVersion}`, "x-opencode-project": Project.ID.global, "x-opencode-session": sessionID, - "x-opencode-client": Flag.OPENCODE_CLIENT, + "x-opencode-client": "cli", }) expect(requests[0]?.generation).toBeUndefined() expect(JSON.stringify(requests[0]?.messages)).toContain("Manual compaction should include this short conversation.") diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index e3297a54a1..e6c160f937 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -22,7 +22,6 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { EventV2 } from "@opencode-ai/core/event" -import { Flag } from "@opencode-ai/core/flag/flag" import { InstallationVersion } from "@opencode-ai/core/installation/version" import { PermissionV2 } from "@opencode-ai/core/permission" import { EventTable } from "@opencode-ai/core/event/sql" @@ -3183,7 +3182,7 @@ describe("SessionRunnerLLM", () => { "User-Agent": `opencode/${InstallationVersion}`, "x-opencode-project": Project.ID.global, "x-opencode-session": sessionID, - "x-opencode-client": Flag.OPENCODE_CLIENT, + "x-opencode-client": "cli", }) }), ) diff --git a/packages/core/test/session-title.test.ts b/packages/core/test/session-title.test.ts index cf3d7f4ea0..f44090dd5f 100644 --- a/packages/core/test/session-title.test.ts +++ b/packages/core/test/session-title.test.ts @@ -17,7 +17,6 @@ import { SessionTitle } from "@opencode-ai/core/session/title" import { SessionV2 } from "@opencode-ai/core/session" import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" -import { Flag } from "@opencode-ai/core/flag/flag" import { InstallationVersion } from "@opencode-ai/core/installation/version" import { AbsolutePath } from "@opencode-ai/core/schema" import { Money } from "@opencode-ai/schema/money" @@ -153,7 +152,7 @@ it.effect("generates a title from the sole user message and renames the session" "User-Agent": `opencode/${InstallationVersion}`, "x-opencode-project": Project.ID.global, "x-opencode-session": sessionID, - "x-opencode-client": Flag.OPENCODE_CLIENT, + "x-opencode-client": "cli", }) expect(JSON.stringify(requests[0]?.messages)).toContain("Help me debug the failing build") const renamed = yield* store.get(sessionID) diff --git a/packages/opencode/src/cli/cmd/debug/index.ts b/packages/opencode/src/cli/cmd/debug/index.ts index 9dcaa33b36..b35f12fc79 100644 --- a/packages/opencode/src/cli/cmd/debug/index.ts +++ b/packages/opencode/src/cli/cmd/debug/index.ts @@ -1,6 +1,6 @@ import { Global } from "@opencode-ai/core/global" import { InstallationVersion } from "@opencode-ai/core/installation/version" -import { Flag } from "@opencode-ai/core/flag/flag" +import { Flag } from "@/flag/flag" import os from "os" import { Duration, Effect } from "effect" import { effectCmd } from "../../effect-cmd" diff --git a/packages/opencode/src/cli/cmd/serve.ts b/packages/opencode/src/cli/cmd/serve.ts index c0f62b3ca0..f4e8aa2983 100644 --- a/packages/opencode/src/cli/cmd/serve.ts +++ b/packages/opencode/src/cli/cmd/serve.ts @@ -1,7 +1,7 @@ import { Effect } from "effect" import { effectCmd } from "../effect-cmd" import { withNetworkOptions, resolveNetworkOptions } from "../network" -import { Flag } from "@opencode-ai/core/flag/flag" +import { Flag } from "@/flag/flag" export const ServeCommand = effectCmd({ command: "serve", diff --git a/packages/opencode/src/cli/cmd/session.ts b/packages/opencode/src/cli/cmd/session.ts index 9e6ddda9d2..f52ca59db0 100644 --- a/packages/opencode/src/cli/cmd/session.ts +++ b/packages/opencode/src/cli/cmd/session.ts @@ -6,7 +6,7 @@ import { Session } from "@/session/session" import { SessionID } from "../../session/schema" import { UI } from "../ui" import { Locale } from "@/util/locale" -import { Flag } from "@opencode-ai/core/flag/flag" +import { Flag } from "@/flag/flag" import { Filesystem } from "@/util/filesystem" import { Process } from "@/util/process" import { NotFoundError } from "@/storage/storage" diff --git a/packages/opencode/src/cli/cmd/web.ts b/packages/opencode/src/cli/cmd/web.ts index 69a981aada..0dd7ad375a 100644 --- a/packages/opencode/src/cli/cmd/web.ts +++ b/packages/opencode/src/cli/cmd/web.ts @@ -2,7 +2,7 @@ import { Effect } from "effect" import { UI } from "../ui" import { effectCmd } from "../effect-cmd" import { withNetworkOptions, resolveNetworkOptions } from "../network" -import { Flag } from "@opencode-ai/core/flag/flag" +import { Flag } from "@/flag/flag" import open from "open" import { networkInterfaces } from "os" diff --git a/packages/opencode/src/cli/heap.ts b/packages/opencode/src/cli/heap.ts index e8ec8f1bd0..97222c8a2d 100644 --- a/packages/opencode/src/cli/heap.ts +++ b/packages/opencode/src/cli/heap.ts @@ -1,6 +1,6 @@ import path from "path" import { writeHeapSnapshot } from "node:v8" -import { Flag } from "@opencode-ai/core/flag/flag" +import { Flag } from "@/flag/flag" import { Global } from "@opencode-ai/core/global" const MINUTE = 60_000 const LIMIT = 2 * 1024 * 1024 * 1024 diff --git a/packages/opencode/src/cli/upgrade.ts b/packages/opencode/src/cli/upgrade.ts index 62b230a633..eeaad0606f 100644 --- a/packages/opencode/src/cli/upgrade.ts +++ b/packages/opencode/src/cli/upgrade.ts @@ -1,6 +1,6 @@ import { Config } from "@/config/config" import { AppRuntime } from "@/effect/app-runtime" -import { Flag } from "@opencode-ai/core/flag/flag" +import { Flag } from "@/flag/flag" import { Installation } from "@/installation" import { InstallationVersion } from "@opencode-ai/core/installation/version" import { GlobalBus } from "@/bus/global" diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 86238f1a84..815316ff98 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -7,7 +7,7 @@ import os from "os" import { mergeDeep } from "remeda" import { Global } from "@opencode-ai/core/global" import fsNode from "fs/promises" -import { Flag } from "@opencode-ai/core/flag/flag" +import { Flag } from "@/flag/flag" import { Auth } from "../auth" import { Env } from "../env" import { applyEdits, modify } from "jsonc-parser" diff --git a/packages/opencode/src/config/paths.ts b/packages/opencode/src/config/paths.ts index 11d90f1292..afdfc18b05 100644 --- a/packages/opencode/src/config/paths.ts +++ b/packages/opencode/src/config/paths.ts @@ -1,7 +1,7 @@ export * as ConfigPaths from "./paths" import path from "path" -import { Flag } from "@opencode-ai/core/flag/flag" +import { Flag } from "@/flag/flag" import { Global } from "@opencode-ai/core/global" import { unique } from "remeda" import * as Effect from "effect/Effect" diff --git a/packages/opencode/src/config/tui-migrate.ts b/packages/opencode/src/config/tui-migrate.ts index 63dd36ec62..8a91bd6ba9 100644 --- a/packages/opencode/src/config/tui-migrate.ts +++ b/packages/opencode/src/config/tui-migrate.ts @@ -3,7 +3,7 @@ import { type ParseError as JsoncParseError, applyEdits, modify, parse as parseJ import { unique } from "remeda" import { Option, Schema } from "effect" import { TuiConfig } from "@opencode-ai/tui/config/v1" -import { Flag } from "@opencode-ai/core/flag/flag" +import { Flag } from "@/flag/flag" import { Global } from "@opencode-ai/core/global" import { Filesystem } from "@/util/filesystem" import * as ConfigPaths from "@/config/paths" diff --git a/packages/opencode/src/config/tui.ts b/packages/opencode/src/config/tui.ts index 2c69c91bb8..4a605dc670 100644 --- a/packages/opencode/src/config/tui.ts +++ b/packages/opencode/src/config/tui.ts @@ -9,7 +9,7 @@ import { ConfigParse } from "@/config/parse" import * as ConfigPaths from "@/config/paths" import { migrateTuiConfig } from "./tui-migrate" import { resolveHostAttentionSoundPaths } from "./tui-host-attention" -import { Flag } from "@opencode-ai/core/flag/flag" +import { Flag } from "@/flag/flag" import { isRecord } from "@opencode-ai/tui/util/record" import { Global } from "@opencode-ai/core/global" import { FSUtil } from "@opencode-ai/core/fs-util" diff --git a/packages/core/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts similarity index 92% rename from packages/core/src/flag/flag.ts rename to packages/opencode/src/flag/flag.ts index b60aa8a6cb..57a5a451c7 100644 --- a/packages/core/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -1,5 +1,3 @@ -import { Config } from "effect" - export function truthy(key: string) { const value = process.env[key]?.toLowerCase() return value === "true" || value === "1" @@ -17,30 +15,33 @@ export const Flag = { OTEL_EXPORTER_OTLP_ENDPOINT: process.env["OTEL_EXPORTER_OTLP_ENDPOINT"], // V2: ServerOptions.observability.headers OTEL_EXPORTER_OTLP_HEADERS: process.env["OTEL_EXPORTER_OTLP_HEADERS"], - OPENCODE_AUTO_HEAP_SNAPSHOT: truthy("OPENCODE_AUTO_HEAP_SNAPSHOT"), // V2: ServerOptions.windows.gitbash OPENCODE_GIT_BASH_PATH: process.env["OPENCODE_GIT_BASH_PATH"], OPENCODE_CONFIG: process.env["OPENCODE_CONFIG"], OPENCODE_CONFIG_CONTENT: process.env["OPENCODE_CONFIG_CONTENT"], + // V2: CLI updater environment adapter OPENCODE_DISABLE_AUTOUPDATE: truthy("OPENCODE_DISABLE_AUTOUPDATE"), OPENCODE_ALWAYS_NOTIFY_UPDATE: truthy("OPENCODE_ALWAYS_NOTIFY_UPDATE"), OPENCODE_DISABLE_PRUNE: truthy("OPENCODE_DISABLE_PRUNE"), + // V2: TUI config terminal.title OPENCODE_DISABLE_TERMINAL_TITLE: truthy("OPENCODE_DISABLE_TERMINAL_TITLE"), + // V2: CLI Mini environment adapter and TUI config debug.timing OPENCODE_SHOW_TTFD: truthy("OPENCODE_SHOW_TTFD"), OPENCODE_DISABLE_AUTOCOMPACT: truthy("OPENCODE_DISABLE_AUTOCOMPACT"), // V2: ServerOptions.models.fetch OPENCODE_DISABLE_MODELS_FETCH: truthy("OPENCODE_DISABLE_MODELS_FETCH"), + // V2: TUI config mouse OPENCODE_DISABLE_MOUSE: truthy("OPENCODE_DISABLE_MOUSE"), OPENCODE_FAKE_VCS: process.env["OPENCODE_FAKE_VCS"], + // V2: CLI password environment adapter OPENCODE_SERVER_PASSWORD: process.env["OPENCODE_SERVER_PASSWORD"], OPENCODE_SERVER_USERNAME: process.env["OPENCODE_SERVER_USERNAME"], // V2: ServerOptions.fs.fff OPENCODE_DISABLE_FFF: fff === undefined ? process.platform === "win32" : truthy("OPENCODE_DISABLE_FFF"), // V2: ServerOptions.fs.filewatcher OPENCODE_DISABLE_FILEWATCHER: truthy("OPENCODE_DISABLE_FILEWATCHER"), - - // Experimental + // V2: TUI config terminal.copy_on_select OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT: copy === undefined ? process.platform === "win32" : truthy("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT"), // V2: ServerOptions.models.url @@ -49,12 +50,8 @@ export const Flag = { OPENCODE_MODELS_PATH: process.env["OPENCODE_MODELS_PATH"], // V2: ServerOptions.database.path OPENCODE_DB: process.env["OPENCODE_DB"], - OPENCODE_WORKSPACE_ID: process.env["OPENCODE_WORKSPACE_ID"], OPENCODE_EXPERIMENTAL_WORKSPACES: enabledByExperimental("OPENCODE_EXPERIMENTAL_WORKSPACES"), - - // Evaluated at access time (not module load) because tests, the CLI, and - // external tooling set these env vars at runtime. // V2: ServerOptions.config.project get OPENCODE_DISABLE_PROJECT_CONFIG() { return truthy("OPENCODE_DISABLE_PROJECT_CONFIG") @@ -78,6 +75,7 @@ export const Flag = { get OPENCODE_PLUGIN_META_FILE() { return process.env["OPENCODE_PLUGIN_META_FILE"] }, + // V2: ServerOptions.client get OPENCODE_CLIENT() { return process.env["OPENCODE_CLIENT"] ?? "cli" }, diff --git a/packages/opencode/src/installation/database.ts b/packages/opencode/src/installation/database.ts index b6d00a5cf8..451b9bbd08 100644 --- a/packages/opencode/src/installation/database.ts +++ b/packages/opencode/src/installation/database.ts @@ -1,6 +1,6 @@ export * as InstallationDatabase from "./database" -import { Flag } from "@opencode-ai/core/flag/flag" +import { Flag } from "@/flag/flag" import { Global } from "@opencode-ai/core/global" import { InstallationChannel } from "@opencode-ai/core/installation/version" import { isAbsolute, join } from "node:path" diff --git a/packages/opencode/src/plugin/meta.ts b/packages/opencode/src/plugin/meta.ts index 54f784d179..7ef2e71cdb 100644 --- a/packages/opencode/src/plugin/meta.ts +++ b/packages/opencode/src/plugin/meta.ts @@ -1,7 +1,7 @@ import path from "path" import { fileURLToPath } from "url" -import { Flag } from "@opencode-ai/core/flag/flag" +import { Flag } from "@/flag/flag" import { Global } from "@opencode-ai/core/global" import { Filesystem } from "@/util/filesystem" import { Flock } from "@opencode-ai/core/util/flock" diff --git a/packages/opencode/src/plugin/tui/runtime.ts b/packages/opencode/src/plugin/tui/runtime.ts index e39484dd19..a8061ec088 100644 --- a/packages/opencode/src/plugin/tui/runtime.ts +++ b/packages/opencode/src/plugin/tui/runtime.ts @@ -34,7 +34,7 @@ import { Global } from "@opencode-ai/core/global" import { Filesystem } from "@/util/filesystem" import { Process } from "@/util/process" import { Flock } from "@opencode-ai/core/util/flock" -import { Flag } from "@opencode-ai/core/flag/flag" +import { Flag } from "@/flag/flag" import { internalTuiPlugins, type InternalTuiPlugin } from "./internal" import type { HostPluginApi, HostSlots } from "@opencode-ai/tui/plugin/slots" import { ConfigPlugin } from "@/config/plugin" diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 19d79445f7..ef2f290891 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -5,7 +5,7 @@ import { ProjectDirectoryTable, ProjectTable } from "@opencode-ai/core/project/s import { ProjectDirectories } from "@opencode-ai/core/project/directories" import { SessionTable } from "@opencode-ai/core/session/sql" import { WorkspaceTable } from "@opencode-ai/core/control-plane/workspace.sql" -import { Flag } from "@opencode-ai/core/flag/flag" +import { Flag } from "@/flag/flag" import { GlobalBus } from "@/bus/global" import { which } from "@opencode-ai/core/util/which" import { Command } from "@/command" diff --git a/packages/opencode/src/server/auth.ts b/packages/opencode/src/server/auth.ts index 9630ddbe20..3398d10ee2 100644 --- a/packages/opencode/src/server/auth.ts +++ b/packages/opencode/src/server/auth.ts @@ -1,7 +1,7 @@ export * as ServerAuth from "./auth" import { ConfigService } from "@/effect/config-service" -import { Flag } from "@opencode-ai/core/flag/flag" +import { Flag } from "@/flag/flag" import { Config as EffectConfig, Context, Option, Redacted } from "effect" export type Credentials = { diff --git a/packages/opencode/src/server/routes/instance/httpapi/middleware/fence.ts b/packages/opencode/src/server/routes/instance/httpapi/middleware/fence.ts index c5cbc7b820..e900764628 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/middleware/fence.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/middleware/fence.ts @@ -1,4 +1,4 @@ -import { Flag } from "@opencode-ai/core/flag/flag" +import { Flag } from "@/flag/flag" import { Database } from "@opencode-ai/core/database/database" import { Effect } from "effect" import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http" diff --git a/packages/opencode/src/server/routes/instance/httpapi/middleware/workspace-routing.ts b/packages/opencode/src/server/routes/instance/httpapi/middleware/workspace-routing.ts index 873abd8349..510a8d6236 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/middleware/workspace-routing.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/middleware/workspace-routing.ts @@ -7,7 +7,7 @@ import { HttpApiProxy } from "./proxy" import * as Fence from "@/server/shared/fence" import { getWorkspaceRouteSessionID, isLocalWorkspaceRoute, workspaceProxyURL } from "@/server/shared/workspace-routing" import { NotFoundError } from "@/storage/storage" -import { Flag } from "@opencode-ai/core/flag/flag" +import { Flag } from "@/flag/flag" import { Context, Data, Effect, Layer, Option, Schema } from "effect" import { HttpClient, HttpServerRequest, HttpServerResponse } from "effect/unstable/http" import { HttpApiMiddleware } from "effect/unstable/httpapi" diff --git a/packages/opencode/src/session/instruction.ts b/packages/opencode/src/session/instruction.ts index ebdae0dbe3..2374337c48 100644 --- a/packages/opencode/src/session/instruction.ts +++ b/packages/opencode/src/session/instruction.ts @@ -7,7 +7,7 @@ import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/ import { Config } from "@/config/config" import { InstanceState } from "@/effect/instance-state" import { RuntimeFlags } from "@/effect/runtime-flags" -import { Flag } from "@opencode-ai/core/flag/flag" +import { Flag } from "@/flag/flag" import { FSUtil } from "@opencode-ai/core/fs-util" import { withTransientReadRetry } from "@/util/effect-http-client" import { Global } from "@opencode-ai/core/global" diff --git a/packages/opencode/test/fixture/flag.ts b/packages/opencode/test/fixture/flag.ts index cf00d9e7b2..9110a8ebb8 100644 --- a/packages/opencode/test/fixture/flag.ts +++ b/packages/opencode/test/fixture/flag.ts @@ -1,5 +1,5 @@ import type { WorkspaceV2 } from "@opencode-ai/core/workspace" -import { Flag } from "@opencode-ai/core/flag/flag" +import { Flag } from "@/flag/flag" import { Effect, Scope } from "effect" /** diff --git a/packages/opencode/test/server/auth.test.ts b/packages/opencode/test/server/auth.test.ts index 1278e8c72e..81b18bed5d 100644 --- a/packages/opencode/test/server/auth.test.ts +++ b/packages/opencode/test/server/auth.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, test } from "bun:test" import { Option, Redacted } from "effect" -import { Flag } from "@opencode-ai/core/flag/flag" +import { Flag } from "@/flag/flag" import { ServerAuth } from "../../src/server/auth" const original = { diff --git a/packages/opencode/test/server/httpapi-cors.test.ts b/packages/opencode/test/server/httpapi-cors.test.ts index 4e9680c7ce..ae81d6796c 100644 --- a/packages/opencode/test/server/httpapi-cors.test.ts +++ b/packages/opencode/test/server/httpapi-cors.test.ts @@ -1,5 +1,5 @@ import { NodeHttpServer, NodeServices } from "@effect/platform-node" -import { Flag } from "@opencode-ai/core/flag/flag" +import { Flag } from "@/flag/flag" import { describe, expect } from "bun:test" import { Config, ConfigProvider, Effect, Layer } from "effect" import { HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/unstable/http" diff --git a/packages/opencode/test/server/httpapi-exercise/environment.ts b/packages/opencode/test/server/httpapi-exercise/environment.ts index 9d3eaa0e53..fbd3bd5ac6 100644 --- a/packages/opencode/test/server/httpapi-exercise/environment.ts +++ b/packages/opencode/test/server/httpapi-exercise/environment.ts @@ -1,4 +1,4 @@ -import { Flag } from "@opencode-ai/core/flag/flag" +import { Flag } from "@/flag/flag" import { Effect } from "effect" import path from "path" diff --git a/packages/opencode/test/server/httpapi-exercise/runner.ts b/packages/opencode/test/server/httpapi-exercise/runner.ts index 37e24b790b..13405b0650 100644 --- a/packages/opencode/test/server/httpapi-exercise/runner.ts +++ b/packages/opencode/test/server/httpapi-exercise/runner.ts @@ -1,4 +1,4 @@ -import { Flag } from "@opencode-ai/core/flag/flag" +import { Flag } from "@/flag/flag" import { ConfigV1 } from "@opencode-ai/core/v1/config/config" import { SessionV1 } from "@opencode-ai/core/v1/session" import { Cause, Duration, Effect, Layer, Scope } from "effect" diff --git a/packages/opencode/test/server/httpapi-instance.test.ts b/packages/opencode/test/server/httpapi-instance.test.ts index 5bbd6d7cc0..5f9e2b8725 100644 --- a/packages/opencode/test/server/httpapi-instance.test.ts +++ b/packages/opencode/test/server/httpapi-instance.test.ts @@ -1,6 +1,6 @@ import { PermissionV1 } from "@opencode-ai/core/v1/permission" import { NodeHttpServer, NodeServices } from "@effect/platform-node" -import { Flag } from "@opencode-ai/core/flag/flag" +import { Flag } from "@/flag/flag" import { describe, expect } from "bun:test" import { Config, Context, Effect, FileSystem, Layer, Path } from "effect" import { HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/unstable/http" diff --git a/packages/opencode/test/server/httpapi-listen.test.ts b/packages/opencode/test/server/httpapi-listen.test.ts index 585c59cb43..42bb81fa2e 100644 --- a/packages/opencode/test/server/httpapi-listen.test.ts +++ b/packages/opencode/test/server/httpapi-listen.test.ts @@ -2,7 +2,7 @@ import { afterEach, describe, expect, test } from "bun:test" import net from "node:net" import path from "node:path" import { pathToFileURL } from "node:url" -import { Flag } from "@opencode-ai/core/flag/flag" +import { Flag } from "@/flag/flag" import { Server } from "../../src/server/server" import { PtyPaths } from "../../src/server/routes/instance/httpapi/groups/pty" import { withTimeout } from "../../src/util/timeout" diff --git a/packages/opencode/test/server/httpapi-mdns.test.ts b/packages/opencode/test/server/httpapi-mdns.test.ts index 6dc122384e..fa1b95629b 100644 --- a/packages/opencode/test/server/httpapi-mdns.test.ts +++ b/packages/opencode/test/server/httpapi-mdns.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, mock, test } from "bun:test" -import { Flag } from "@opencode-ai/core/flag/flag" +import { Flag } from "@/flag/flag" import { withTimeout } from "../../src/util/timeout" import { resetDatabase } from "../fixture/db" import { disposeAllInstances } from "../fixture/fixture" diff --git a/packages/opencode/test/server/httpapi-query-schema-drift.test.ts b/packages/opencode/test/server/httpapi-query-schema-drift.test.ts index f5411767ed..a247cb9651 100644 --- a/packages/opencode/test/server/httpapi-query-schema-drift.test.ts +++ b/packages/opencode/test/server/httpapi-query-schema-drift.test.ts @@ -1,7 +1,7 @@ import { afterEach, describe, expect } from "bun:test" import { Effect, Schema } from "effect" import { OpenApi } from "effect/unstable/httpapi" -import { Flag } from "@opencode-ai/core/flag/flag" +import { Flag } from "@/flag/flag" import { Server } from "../../src/server/server" import { SessionID } from "../../src/session/schema" import { PublicApi } from "../../src/server/routes/instance/httpapi/public" diff --git a/packages/opencode/test/server/httpapi-sdk.test.ts b/packages/opencode/test/server/httpapi-sdk.test.ts index a6f308f032..14e3fa274a 100644 --- a/packages/opencode/test/server/httpapi-sdk.test.ts +++ b/packages/opencode/test/server/httpapi-sdk.test.ts @@ -9,7 +9,7 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { FSUtil } from "@opencode-ai/core/fs-util" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" -import { Flag } from "@opencode-ai/core/flag/flag" +import { Flag } from "@/flag/flag" import { createOpencodeClient } from "@opencode-ai/sdk/v2" import { validateSession } from "../../src/cli/tui/validate-session" import { InstanceBootstrap } from "../../src/project/bootstrap" diff --git a/packages/opencode/test/server/httpapi-session.test.ts b/packages/opencode/test/server/httpapi-session.test.ts index fab8d4e57b..9bc996a06c 100644 --- a/packages/opencode/test/server/httpapi-session.test.ts +++ b/packages/opencode/test/server/httpapi-session.test.ts @@ -10,7 +10,7 @@ import { layerWebSocketConstructorGlobal } from "effect/unstable/socket/Socket" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" -import { Flag } from "@opencode-ai/core/flag/flag" +import { Flag } from "@/flag/flag" import { Ripgrep } from "@opencode-ai/core/ripgrep" import { registerAdapter } from "../../src/control-plane/adapters" import type { WorkspaceAdapter } from "../../src/control-plane/types" diff --git a/packages/opencode/test/server/httpapi-sync.test.ts b/packages/opencode/test/server/httpapi-sync.test.ts index 0577e6bf02..0a4ed650e6 100644 --- a/packages/opencode/test/server/httpapi-sync.test.ts +++ b/packages/opencode/test/server/httpapi-sync.test.ts @@ -1,7 +1,7 @@ import { afterEach, describe, expect, mock } from "bun:test" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { Context, Effect, Layer } from "effect" -import { Flag } from "@opencode-ai/core/flag/flag" +import { Flag } from "@/flag/flag" import { SyncPaths } from "../../src/server/routes/instance/httpapi/groups/sync" import { HttpApiApp } from "../../src/server/routes/instance/httpapi/server" import { Session } from "@/session/session" diff --git a/packages/opencode/test/server/httpapi-ui.test.ts b/packages/opencode/test/server/httpapi-ui.test.ts index 2f10193f3d..e477badf7a 100644 --- a/packages/opencode/test/server/httpapi-ui.test.ts +++ b/packages/opencode/test/server/httpapi-ui.test.ts @@ -1,6 +1,6 @@ import { createHash } from "node:crypto" import { describe, expect } from "bun:test" -import { Flag } from "@opencode-ai/core/flag/flag" +import { Flag } from "@/flag/flag" import { ConfigProvider, Effect, Layer, Option } from "effect" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { diff --git a/packages/opencode/test/server/httpapi-workspace.test.ts b/packages/opencode/test/server/httpapi-workspace.test.ts index 4cb9f55c84..973436470e 100644 --- a/packages/opencode/test/server/httpapi-workspace.test.ts +++ b/packages/opencode/test/server/httpapi-workspace.test.ts @@ -4,7 +4,7 @@ import path from "node:path" import { Effect, Layer, Stream } from "effect" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/core/effect/layer-node" -import { Flag } from "@opencode-ai/core/flag/flag" +import { Flag } from "@/flag/flag" import { registerAdapter } from "../../src/control-plane/adapters" import { WorkspaceV2 } from "@opencode-ai/core/workspace" import type { WorkspaceAdapter } from "../../src/control-plane/types" diff --git a/packages/opencode/test/server/worktree-endpoint-repro.test.ts b/packages/opencode/test/server/worktree-endpoint-repro.test.ts index 62a6185886..11b01609ef 100644 --- a/packages/opencode/test/server/worktree-endpoint-repro.test.ts +++ b/packages/opencode/test/server/worktree-endpoint-repro.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from "bun:test" import { Effect, Layer, Queue } from "effect" -import { Flag } from "@opencode-ai/core/flag/flag" +import { Flag } from "@/flag/flag" import { GlobalBus, type GlobalEvent } from "@/bus/global" import { Worktree } from "@/worktree" import { Server } from "../../src/server/server" diff --git a/packages/server/src/auth.ts b/packages/server/src/auth.ts index 239eb0d237..1f9632fede 100644 --- a/packages/server/src/auth.ts +++ b/packages/server/src/auth.ts @@ -1,11 +1,9 @@ export * as ServerAuth from "./auth" -import { Context, Effect, Layer, Option, Redacted } from "effect" -import { all, option, string, withDefault } from "effect/Config" +import { Context, Layer, Option, Redacted } from "effect" export type Credentials = { password?: string - username?: string } export type DecodedCredentials = { @@ -15,7 +13,6 @@ export type DecodedCredentials = { export type Info = { readonly password: Option.Option - readonly username: string } export class Config extends Context.Service()("@opencode/ServerAuthConfig") { @@ -24,17 +21,7 @@ export class Config extends Context.Service()("@opencode/ServerAut } static get layer() { - return Layer.effect( - this, - Effect.gen(function* () { - return Config.of( - yield* all({ - password: string("OPENCODE_SERVER_PASSWORD").pipe(option), - username: string("OPENCODE_SERVER_USERNAME").pipe(withDefault("opencode")), - }), - ) - }), - ) + return this.configLayer({ password: Option.none() }) } } @@ -45,16 +32,16 @@ export function required(config: Info) { export function authorized(credentials: DecodedCredentials, config: Info) { return ( Option.isSome(config.password) && - credentials.username === config.username && + credentials.username === "opencode" && Redacted.value(credentials.password) === config.password.value ) } export function header(credentials?: Credentials) { - const password = credentials?.password ?? process.env.OPENCODE_SERVER_PASSWORD + const password = credentials?.password if (!password) return undefined - return `Basic ${Buffer.from(`${credentials?.username ?? process.env.OPENCODE_SERVER_USERNAME ?? "opencode"}:${password}`).toString("base64")}` + return `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}` } export function headers(credentials?: Credentials) { diff --git a/packages/server/src/options.ts b/packages/server/src/options.ts index 834a297749..30625dcfa0 100644 --- a/packages/server/src/options.ts +++ b/packages/server/src/options.ts @@ -4,11 +4,13 @@ import { Observability } from "@opencode-ai/core/observability" import { Schema } from "effect" export const ServerOptions = Schema.Struct({ + client: Schema.optional(Schema.String), hostname: Schema.optional(Schema.String), port: Schema.optional( Schema.Int.check(Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(65_535)), ), password: Schema.optional(Schema.String), + simulation: Schema.optional(Schema.Boolean), database: Schema.optional(Database.Options), models: Schema.optional(ModelsDev.Options), observability: Schema.optional(Observability.Options), @@ -16,6 +18,8 @@ export const ServerOptions = Schema.Struct({ Schema.Struct({ directory: Schema.optional(Schema.String), project: Schema.optional(Schema.Boolean), + file: Schema.optional(Schema.String), + content: Schema.optional(Schema.String), }), ), windows: Schema.optional( diff --git a/packages/server/src/process.ts b/packages/server/src/process.ts index 2189f2a824..b8559cdcf7 100644 --- a/packages/server/src/process.ts +++ b/packages/server/src/process.ts @@ -148,7 +148,7 @@ function dispatch( application: Ref.Ref>, shutdown: Deferred.Deferred, ): App { - const auth = ServerAuth.Config.of({ username: "opencode", password: Option.some(password) }) + const auth = ServerAuth.Config.of({ password: Option.some(password) }) return Effect.gen(function* () { const request = yield* HttpServerRequest.HttpServerRequest const url = new URL(request.url, "http://localhost") diff --git a/packages/server/src/routes.ts b/packages/server/src/routes.ts index cfcb14c95a..37878b9f7d 100644 --- a/packages/server/src/routes.ts +++ b/packages/server/src/routes.ts @@ -14,6 +14,10 @@ import { PtyTicket } from "@opencode-ai/core/pty/ticket" import { Pty } from "@opencode-ai/core/pty" import { Project } from "@opencode-ai/core/project" import { SessionV2 } from "@opencode-ai/core/session" +import { SessionCompaction } from "@opencode-ai/core/session/compaction" +import { SessionGenerateNode } from "@opencode-ai/core/session/generate-node" +import { SessionModelRequest } from "@opencode-ai/core/session/model-request" +import { SessionTitle } from "@opencode-ai/core/session/title" import { Shell } from "@opencode-ai/core/shell" import { Job } from "@opencode-ai/core/job" import { Global } from "@opencode-ai/core/global" @@ -64,7 +68,7 @@ const applicationServices = LayerNode.group([ export function createRoutes(options: ServerOptions = {}, serviceURLs: () => ReadonlyArray = () => []) { return makeRoutes( options.password - ? ServerAuth.Config.configLayer({ username: "opencode", password: Option.some(options.password) }) + ? ServerAuth.Config.configLayer({ password: Option.some(options.password) }) : ServerAuth.Config.layer, options, serviceURLs, @@ -72,7 +76,7 @@ export function createRoutes(options: ServerOptions = {}, serviceURLs: () => Rea } export function createEmbeddedRoutes(options: ServerOptions = {}) { - return makeRoutes(ServerAuth.Config.configLayer({ username: "opencode", password: Option.none() }), options, () => []) + return makeRoutes(ServerAuth.Config.configLayer({ password: Option.none() }), options, () => []) } function makeRoutes( @@ -83,19 +87,30 @@ function makeRoutes( const pluginRuntimeCell = PluginRuntime.makeCell() const replacements: LayerNode.Replacements = [ [Database.node, Database.configured(options.database)], - [ModelsDev.node, ModelsDev.configured(options.models)], + [ModelsDev.node, ModelsDev.configured({ ...options.models, client: options.client })], [Watcher.node, Watcher.configured({ enabled: options.fs?.filewatcher })], [FileSystemSearch.node, FileSystemSearch.configured({ fff: options.fs?.fff })], [Global.node, Global.layerWith(options.config?.directory ? { config: options.config.directory } : {})], - [Config.node, Config.configured({ project: options.config?.project })], + [ + Config.node, + Config.configured({ + project: options.config?.project, + file: options.config?.file, + content: options.config?.content, + }), + ], [InstructionDiscovery.node, InstructionDiscovery.configured({ project: options.config?.project })], [CommandV2.node, CommandV2.configured({ gitbash: options.windows?.gitbash })], [Pty.node, Pty.configured({ gitbash: options.windows?.gitbash })], [Shell.node, Shell.configured({ gitbash: options.windows?.gitbash })], + [SessionCompaction.node, SessionCompaction.configured({ client: options.client })], + [SessionGenerateNode.node, SessionGenerateNode.configured({ client: options.client })], + [SessionModelRequest.node, SessionModelRequest.configured({ client: options.client })], + [SessionTitle.node, SessionTitle.configured({ client: options.client })], [PluginRuntime.node, PluginRuntime.layerWithCell(pluginRuntimeCell)], [PluginRuntime.providerNode, PluginRuntime.providerNodeWithCell(pluginRuntimeCell)], ] - const serviceLayer = simulateEnabled() + const serviceLayer = options.simulation ? Layer.unwrap( Effect.gen(function* () { const { simulationReplacements } = yield* Effect.promise(() => import("@opencode-ai/simulation/backend")) @@ -120,7 +135,7 @@ function makeRoutes( Layer.provide(authorizationLayer), Layer.provide(schemaErrorLayer), Layer.provide(auth), - Layer.provide(Observability.layer(options.observability)), + Layer.provide(Observability.layer({ ...options.observability, client: options.client })), HttpRouter.provideRequest(requestServices), Layer.provideMerge(services), Layer.provideMerge(HttpRouter.layer), @@ -129,8 +144,4 @@ function makeRoutes( ) } -function simulateEnabled() { - return !!process.env.OPENCODE_SIMULATE -} - export const webHandler = () => HttpRouter.toWebHandler(createRoutes().pipe(Layer.provide(HttpServer.layerServices))) diff --git a/packages/server/test/auth.test.ts b/packages/server/test/auth.test.ts new file mode 100644 index 0000000000..f113cfab1d --- /dev/null +++ b/packages/server/test/auth.test.ts @@ -0,0 +1,13 @@ +import { expect, test } from "bun:test" +import { ServerAuth } from "@opencode-ai/server/auth" +import { Option, Redacted } from "effect" + +test("accepts only the fixed opencode username", () => { + const config = { password: Option.some("secret") } + expect(ServerAuth.authorized({ username: "opencode", password: Redacted.make("secret") }, config)).toBe(true) + expect(ServerAuth.authorized({ username: "custom", password: Redacted.make("secret") }, config)).toBe(false) +}) + +test("encodes the fixed opencode username", () => { + expect(ServerAuth.header({ password: "secret" })).toBe(`Basic ${Buffer.from("opencode:secret").toString("base64")}`) +}) diff --git a/packages/simulation/src/backend/index.ts b/packages/simulation/src/backend/index.ts index a1044058db..2eef5a6706 100644 --- a/packages/simulation/src/backend/index.ts +++ b/packages/simulation/src/backend/index.ts @@ -12,7 +12,7 @@ import { SimulatedProvider } from "./simulated-provider" /** * Layer replacements applied when the server is built in simulation mode. * - * The server merges these into the app node build when `OPENCODE_SIMULATE` + * The server merges these into the app node build when simulation is enabled * is enabled, via a dynamic import so this module is never loaded eagerly. * * - Network: all outbound HTTP resolves against the simulated route table; diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index f0ec2a59b4..47f5168dbf 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -4,7 +4,6 @@ import { Deferred, Effect } from "effect" import { Service, type Endpoint } from "@opencode-ai/client/effect/service" import { OpenCode } from "@opencode-ai/client" import { Global } from "@opencode-ai/core/global" -import { Flag } from "@opencode-ai/core/flag/flag" import { ClipboardProvider, useClipboard } from "./context/clipboard" import { LogProvider, useLog, type LogSink } from "./context/log" import { ExitProvider, useExit } from "./context/exit" @@ -211,7 +210,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) { useKittyKeyboard: {}, autoFocus: false, openConsoleOnError: false, - useMouse: !Flag.OPENCODE_DISABLE_MOUSE && config.mouse, + useMouse: config.mouse, consoleOptions: { keyBindings: [{ name: "y", ctrl: true, action: "copy-selection" }], }, @@ -466,7 +465,7 @@ function App(props: { pair?: DialogPairCredentials; started: number }) { const offSelectionKeys = keymap.intercept( "key", ({ event }) => { - if (!Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT) return + if (config.data.terminal?.copy_on_select ?? process.platform !== "win32") return Selection.handleSelectionKey(renderer, toast, event, clipboard) }, { priority: 1 }, @@ -487,15 +486,16 @@ function App(props: { pair?: DialogPairCredentials; started: number }) { renderer.clearSelection() } const terminalTitleEnabled = () => config.data.terminal?.title ?? true + const copyOnSelectEnabled = () => config.data.terminal?.copy_on_select ?? process.platform !== "win32" const pasteSummaryEnabled = () => config.data.prompt?.paste !== "full" createEffect(() => { - renderer.useMouse = !Flag.OPENCODE_DISABLE_MOUSE && config.data.mouse + renderer.useMouse = config.data.mouse }) // Update terminal window title based on current route and session createEffect(() => { - if (!terminalTitleEnabled() || Flag.OPENCODE_DISABLE_TERMINAL_TITLE) return + if (!terminalTitleEnabled()) return if (route.data.type === "home") { renderer.setTerminalTitle("OpenCode") @@ -1090,7 +1090,7 @@ function App(props: { pair?: DialogPairCredentials; started: number }) { flexDirection="column" backgroundColor={themeV2.background.default} onMouseDown={(evt) => { - if (!Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT) return + if (copyOnSelectEnabled()) return if (evt.button !== MouseButton.RIGHT) return if (!Selection.copy(renderer, toast, clipboard)) return @@ -1098,12 +1098,12 @@ function App(props: { pair?: DialogPairCredentials; started: number }) { evt.stopPropagation() }} onMouseUp={ - !Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT + copyOnSelectEnabled() ? () => Selection.copy(renderer, toast, clipboard) : undefined } > - + diff --git a/packages/tui/src/component/dialog-config.tsx b/packages/tui/src/component/dialog-config.tsx index 01cdb950dc..bdacb53b45 100644 --- a/packages/tui/src/component/dialog-config.tsx +++ b/packages/tui/src/component/dialog-config.tsx @@ -206,6 +206,14 @@ const settings: Setting[] = [ values: [false, true], labels: ["off", "on"], }, + { + title: "Copy on select", + category: "Terminal", + path: ["terminal", "copy_on_select"], + default: process.platform !== "win32", + values: [false, true], + labels: ["off", "on"], + }, { title: "DevTools", category: "Debug", @@ -214,6 +222,14 @@ const settings: Setting[] = [ values: [false, true], labels: ["off", "on"], }, + { + title: "Timing", + category: "Debug", + path: ["debug", "timing"], + default: false, + values: [false, true], + labels: ["off", "on"], + }, ] export function DialogConfig() { diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx index d6b39d85d4..1d1a223e10 100644 --- a/packages/tui/src/component/prompt/index.tsx +++ b/packages/tui/src/component/prompt/index.tsx @@ -12,7 +12,6 @@ import { registerOpencodeSpinner } from "../register-spinner" import path from "path" import { fileURLToPath } from "url" import { useLocal } from "../../context/local" -import { Flag } from "@opencode-ai/core/flag/flag" import { useTheme } from "../../context/theme" import { tint } from "../../theme/color" import { EmptyBorder, SplitBorder } from "../../ui/border" diff --git a/packages/tui/src/config/index.tsx b/packages/tui/src/config/index.tsx index 5ce9e5a19a..ee6eff84cd 100644 --- a/packages/tui/src/config/index.tsx +++ b/packages/tui/src/config/index.tsx @@ -92,6 +92,7 @@ export const Info = Schema.Struct({ terminal: Schema.optional( Schema.Struct({ title: Schema.optional(Schema.Boolean).annotate({ description: "Update the terminal window title" }), + copy_on_select: Schema.optional(Schema.Boolean).annotate({ description: "Copy selected terminal text" }), }), ).annotate({ description: "Terminal integration settings" }), prompt: Schema.optional( @@ -129,6 +130,7 @@ export const Info = Schema.Struct({ debug: Schema.optional( Schema.Struct({ devtools: Schema.optional(Schema.Boolean).annotate({ description: "Show the DevTools sidebar" }), + timing: Schema.optional(Schema.Boolean).annotate({ description: "Show time-to-first-draw diagnostics" }), }), ).annotate({ description: "Debugging settings" }), animations: Schema.optional(Schema.Boolean).annotate({ description: "Enable interface animations" }), diff --git a/packages/tui/src/ui/dialog.tsx b/packages/tui/src/ui/dialog.tsx index 773a3f2579..8ab8a2269a 100644 --- a/packages/tui/src/ui/dialog.tsx +++ b/packages/tui/src/ui/dialog.tsx @@ -5,8 +5,8 @@ import { useTheme } from "../context/theme" import { MouseButton, Renderable, RGBA } from "@opentui/core" import { createStore } from "solid-js/store" import { useToast } from "./toast" -import { Flag } from "@opencode-ai/core/flag/flag" import { useClipboard } from "../context/clipboard" +import { useConfig } from "../config" export function Dialog( props: ParentProps<{ @@ -197,6 +197,8 @@ export function DialogProvider(props: ParentProps) { const renderer = useRenderer() const toast = useToast() const clipboard = useClipboard() + const config = useConfig() + const copyOnSelectEnabled = () => config.data.terminal?.copy_on_select ?? process.platform !== "win32" function copySelection() { const text = renderer.getSelection()?.getSelectedText() @@ -216,14 +218,14 @@ export function DialogProvider(props: ParentProps) { position="absolute" zIndex={3000} onMouseDown={(evt: { button: number; preventDefault(): void; stopPropagation(): void }) => { - if (!Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT) return + if (copyOnSelectEnabled()) return if (evt.button !== MouseButton.RIGHT) return if (!copySelection()) return evt.preventDefault() evt.stopPropagation() }} - onMouseUp={!Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT ? copySelection : undefined} + onMouseUp={copyOnSelectEnabled() ? copySelection : undefined} > value.clear()} size={value.size} centered={value.centered}>