From ada7cca10dd4c5c9fb6aa467e0a12724df2c5e8b Mon Sep 17 00:00:00 2001 From: Anthony Shew Date: Fri, 5 Dec 2025 09:01:20 -0700 Subject: [PATCH 1/8] feat(theme): Vercel (#5119) Co-authored-by: opencode-agent[bot] Co-authored-by: rekram1-node --- .../src/cli/cmd/tui/context/theme.tsx | 2 + .../src/cli/cmd/tui/context/theme/vercel.json | 245 ++++++++++++++++++ 2 files changed, 247 insertions(+) create mode 100644 packages/opencode/src/cli/cmd/tui/context/theme/vercel.json diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index 4223657ba..49b85d1c7 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -25,6 +25,7 @@ import rosepine from "./theme/rosepine.json" with { type: "json" } import solarized from "./theme/solarized.json" with { type: "json" } import synthwave84 from "./theme/synthwave84.json" with { type: "json" } import tokyonight from "./theme/tokyonight.json" with { type: "json" } +import vercel from "./theme/vercel.json" with { type: "json" } import vesper from "./theme/vesper.json" with { type: "json" } import zenburn from "./theme/zenburn.json" with { type: "json" } import { useKV } from "./kv" @@ -149,6 +150,7 @@ export const DEFAULT_THEMES: Record = { synthwave84, tokyonight, vesper, + vercel, zenburn, } diff --git a/packages/opencode/src/cli/cmd/tui/context/theme/vercel.json b/packages/opencode/src/cli/cmd/tui/context/theme/vercel.json new file mode 100644 index 000000000..86b965b10 --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/context/theme/vercel.json @@ -0,0 +1,245 @@ +{ + "$schema": "https://opencode.ai/theme.json", + "defs": { + "background100": "#0A0A0A", + "background200": "#000000", + "gray100": "#1A1A1A", + "gray200": "#1F1F1F", + "gray300": "#292929", + "gray400": "#2E2E2E", + "gray500": "#454545", + "gray600": "#878787", + "gray700": "#8F8F8F", + "gray900": "#A1A1A1", + "gray1000": "#EDEDED", + "blue600": "#0099FF", + "blue700": "#0070F3", + "blue900": "#52A8FF", + "blue1000": "#EBF8FF", + "red700": "#E5484D", + "red900": "#FF6166", + "red1000": "#FDECED", + "amber700": "#FFB224", + "amber900": "#F2A700", + "amber1000": "#FDF4DC", + "green700": "#46A758", + "green900": "#63C46D", + "green1000": "#E6F9E9", + "teal700": "#12A594", + "teal900": "#0AC7AC", + "purple700": "#8E4EC6", + "purple900": "#BF7AF0", + "pink700": "#E93D82", + "pink900": "#F75590", + "highlightPink": "#FF0080", + "highlightPurple": "#F81CE5", + "cyan": "#50E3C2", + "lightBackground": "#FFFFFF", + "lightGray100": "#FAFAFA", + "lightGray200": "#EAEAEA", + "lightGray600": "#666666", + "lightGray1000": "#171717" + }, + "theme": { + "primary": { + "dark": "blue700", + "light": "blue700" + }, + "secondary": { + "dark": "blue900", + "light": "#0062D1" + }, + "accent": { + "dark": "purple700", + "light": "purple700" + }, + "error": { + "dark": "red700", + "light": "#DC3545" + }, + "warning": { + "dark": "amber700", + "light": "#FF9500" + }, + "success": { + "dark": "green700", + "light": "#388E3C" + }, + "info": { + "dark": "blue900", + "light": "blue700" + }, + "text": { + "dark": "gray1000", + "light": "lightGray1000" + }, + "textMuted": { + "dark": "gray600", + "light": "lightGray600" + }, + "background": { + "dark": "background200", + "light": "lightBackground" + }, + "backgroundPanel": { + "dark": "gray100", + "light": "lightGray100" + }, + "backgroundElement": { + "dark": "gray300", + "light": "lightGray200" + }, + "border": { + "dark": "gray200", + "light": "lightGray200" + }, + "borderActive": { + "dark": "gray500", + "light": "#999999" + }, + "borderSubtle": { + "dark": "gray100", + "light": "#EAEAEA" + }, + "diffAdded": { + "dark": "green900", + "light": "green700" + }, + "diffRemoved": { + "dark": "red900", + "light": "red700" + }, + "diffContext": { + "dark": "gray600", + "light": "lightGray600" + }, + "diffHunkHeader": { + "dark": "gray600", + "light": "lightGray600" + }, + "diffHighlightAdded": { + "dark": "green900", + "light": "green700" + }, + "diffHighlightRemoved": { + "dark": "red900", + "light": "red700" + }, + "diffAddedBg": { + "dark": "#0B1D0F", + "light": "#E6F9E9" + }, + "diffRemovedBg": { + "dark": "#2A1314", + "light": "#FDECED" + }, + "diffContextBg": { + "dark": "background200", + "light": "lightBackground" + }, + "diffLineNumber": { + "dark": "gray600", + "light": "lightGray600" + }, + "diffAddedLineNumberBg": { + "dark": "#0F2613", + "light": "#D6F5D6" + }, + "diffRemovedLineNumberBg": { + "dark": "#3C1618", + "light": "#FFE5E5" + }, + "markdownText": { + "dark": "gray1000", + "light": "lightGray1000" + }, + "markdownHeading": { + "dark": "purple900", + "light": "purple700" + }, + "markdownLink": { + "dark": "blue900", + "light": "blue700" + }, + "markdownLinkText": { + "dark": "teal900", + "light": "teal700" + }, + "markdownCode": { + "dark": "green900", + "light": "green700" + }, + "markdownBlockQuote": { + "dark": "gray600", + "light": "lightGray600" + }, + "markdownEmph": { + "dark": "amber900", + "light": "amber700" + }, + "markdownStrong": { + "dark": "pink900", + "light": "pink700" + }, + "markdownHorizontalRule": { + "dark": "gray500", + "light": "#999999" + }, + "markdownListItem": { + "dark": "gray1000", + "light": "lightGray1000" + }, + "markdownListEnumeration": { + "dark": "blue900", + "light": "blue700" + }, + "markdownImage": { + "dark": "teal900", + "light": "teal700" + }, + "markdownImageText": { + "dark": "cyan", + "light": "teal700" + }, + "markdownCodeBlock": { + "dark": "gray1000", + "light": "lightGray1000" + }, + "syntaxComment": { + "dark": "gray600", + "light": "#888888" + }, + "syntaxKeyword": { + "dark": "pink900", + "light": "pink700" + }, + "syntaxFunction": { + "dark": "purple900", + "light": "purple700" + }, + "syntaxVariable": { + "dark": "blue900", + "light": "blue700" + }, + "syntaxString": { + "dark": "green900", + "light": "green700" + }, + "syntaxNumber": { + "dark": "amber900", + "light": "amber700" + }, + "syntaxType": { + "dark": "teal900", + "light": "teal700" + }, + "syntaxOperator": { + "dark": "pink900", + "light": "pink700" + }, + "syntaxPunctuation": { + "dark": "gray1000", + "light": "lightGray1000" + } + } +} From 87a791fdb9432d457202da85ec5e23e42f91db4d Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Fri, 5 Dec 2025 09:49:21 -0600 Subject: [PATCH 2/8] fix(desktop): new session not selecting tab --- packages/desktop/src/context/session.tsx | 37 +++++++++++++++--------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/packages/desktop/src/context/session.tsx b/packages/desktop/src/context/session.tsx index 4e9fe71f8..690653992 100644 --- a/packages/desktop/src/context/session.tsx +++ b/packages/desktop/src/context/session.tsx @@ -201,20 +201,14 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex sdk.client.pty.create({ body: { title: `Terminal ${store.terminals.all.length + 1}` } }).then((pty) => { const id = pty.data?.id if (!id) return - batch(() => { - setStore("terminals", "all", [ - ...store.terminals.all, - { - id, - title: pty.data?.title ?? "Terminal", - // rows: pty.data?.rows ?? 24, - // cols: pty.data?.cols ?? 80, - // buffer: "", - // scrollY: 0, - }, - ]) - setStore("terminals", "active", id) - }) + setStore("terminals", "all", [ + ...store.terminals.all, + { + id, + title: pty.data?.title ?? "Terminal", + }, + ]) + setStore("terminals", "active", id) }) }, update(pty: Partial & { id: string }) { @@ -224,6 +218,21 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex body: { title: pty.title, size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined }, }) }, + async clone(id: string) { + const index = store.terminals.all.findIndex((x) => x.id === id) + const pty = store.terminals.all[index] + if (!pty) return + const clone = await sdk.client.pty.create({ + body: { + title: pty.title, + }, + }) + if (!clone.data) return + setStore("terminals", "all", index, { + ...pty, + ...clone.data, + }) + }, open(id: string) { setStore("terminals", "active", id) }, From cfbaf81ef8f360d1df621d800d10b0ac2b3019a8 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Fri, 5 Dec 2025 10:30:44 -0600 Subject: [PATCH 3/8] fix(desktop): clone pty session on reconnect --- packages/desktop/src/components/terminal.tsx | 9 +-- packages/desktop/src/context/session.tsx | 5 +- packages/desktop/src/pages/session.tsx | 10 +++- packages/opencode/src/server/server.ts | 58 +++++++++----------- 4 files changed, 44 insertions(+), 38 deletions(-) diff --git a/packages/desktop/src/components/terminal.tsx b/packages/desktop/src/components/terminal.tsx index 49a45a432..f7b44b0aa 100644 --- a/packages/desktop/src/components/terminal.tsx +++ b/packages/desktop/src/components/terminal.tsx @@ -1,6 +1,5 @@ import { init, Terminal as Term, FitAddon } from "ghostty-web" import { ComponentProps, onCleanup, onMount, splitProps } from "solid-js" -import { createReconnectingWS, ReconnectingWebSocket } from "@solid-primitives/websocket" import { useSDK } from "@/context/sdk" import { SerializeAddon } from "@/addons/serialize" import { LocalPTY } from "@/context/session" @@ -11,19 +10,20 @@ export interface TerminalProps extends ComponentProps<"div"> { pty: LocalPTY onSubmit?: () => void onCleanup?: (pty: LocalPTY) => void + onConnectError?: (error: unknown) => void } export const Terminal = (props: TerminalProps) => { const sdk = useSDK() let container!: HTMLDivElement - const [local, others] = splitProps(props, ["pty", "class", "classList"]) - let ws: ReconnectingWebSocket + const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnectError"]) + let ws: WebSocket let term: Term let serializeAddon: SerializeAddon let fitAddon: FitAddon onMount(async () => { - ws = createReconnectingWS(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`) + ws = new WebSocket(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`) term = new Term({ cursorBlink: true, fontSize: 14, @@ -115,6 +115,7 @@ export const Terminal = (props: TerminalProps) => { }) ws.addEventListener("error", (error) => { console.error("WebSocket error:", error) + props.onConnectError?.(error) }) ws.addEventListener("close", () => { console.log("WebSocket disconnected") diff --git a/packages/desktop/src/context/session.tsx b/packages/desktop/src/context/session.tsx index 690653992..b5972f3e3 100644 --- a/packages/desktop/src/context/session.tsx +++ b/packages/desktop/src/context/session.tsx @@ -26,7 +26,7 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex const params = useParams() const sync = useSync() const name = createMemo( - () => `______${base64Encode(sync.data.project.worktree)}/session${params.id ? "/" + params.id : ""}`, + () => `${base64Encode(sync.data.project.worktree)}/session${params.id ? "/" + params.id : ""}.v1`, ) const [store, setStore] = makePersisted( @@ -232,6 +232,9 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex ...pty, ...clone.data, }) + if (store.terminals.active === pty.id) { + setStore("terminals", "active", clone.data.id) + } }, open(id: string) { setStore("terminals", "active", id) diff --git a/packages/desktop/src/pages/session.tsx b/packages/desktop/src/pages/session.tsx index 773625334..8cd9e9d65 100644 --- a/packages/desktop/src/pages/session.tsx +++ b/packages/desktop/src/pages/session.tsx @@ -84,6 +84,10 @@ export default function Page() { } if (event.ctrlKey && event.key.toLowerCase() === "`") { event.preventDefault() + if (event.shiftKey) { + session.terminal.new() + return + } layout.terminal.toggle() return } @@ -663,7 +667,11 @@ export default function Page() { {(terminal) => ( - + session.terminal.clone(terminal.id)} + /> )} diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index a74b7876f..7a105e746 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -208,30 +208,6 @@ export namespace Server { return c.json(info) }, ) - .put( - "/pty/:id", - describeRoute({ - description: "Update PTY session", - operationId: "pty.update", - responses: { - 200: { - description: "Updated session", - content: { - "application/json": { - schema: resolver(Pty.Info), - }, - }, - }, - ...errors(400), - }, - }), - validator("param", z.object({ id: z.string() })), - validator("json", Pty.UpdateInput), - async (c) => { - const info = await Pty.update(c.req.valid("param").id, c.req.valid("json")) - return c.json(info) - }, - ) .get( "/pty/:id", describeRoute({ @@ -258,6 +234,30 @@ export namespace Server { return c.json(info) }, ) + .put( + "/pty/:id", + describeRoute({ + description: "Update PTY session", + operationId: "pty.update", + responses: { + 200: { + description: "Updated session", + content: { + "application/json": { + schema: resolver(Pty.Info), + }, + }, + }, + ...errors(400), + }, + }), + validator("param", z.object({ id: z.string() })), + validator("json", Pty.UpdateInput), + async (c) => { + const info = await Pty.update(c.req.valid("param").id, c.req.valid("json")) + return c.json(info) + }, + ) .delete( "/pty/:id", describeRoute({ @@ -295,20 +295,14 @@ export namespace Server { }, }, }, - 404: { - description: "Session not found", - content: { - "application/json": { - schema: resolver(z.boolean()), - }, - }, - }, + ...errors(404), }, }), validator("param", z.object({ id: z.string() })), upgradeWebSocket((c) => { const id = c.req.param("id") let handler: ReturnType + if (!Pty.get(id)) throw new Error("Session not found") return { onOpen(_event, ws) { handler = Pty.connect(id, ws) From 864c098701c27016afad20bc121cb1c307eb3dbc Mon Sep 17 00:00:00 2001 From: Noam Bressler Date: Fri, 5 Dec 2025 18:48:22 +0200 Subject: [PATCH 4/8] add experimental.open_telemetry config option to enable OTEL spans (#4978) Co-authored-by: noamzbr Co-authored-by: opencode-agent[bot] Co-authored-by: rekram1-node --- packages/opencode/src/agent/agent.ts | 2 ++ packages/opencode/src/config/config.ts | 4 ++++ packages/opencode/src/session/compaction.ts | 3 +++ packages/opencode/src/session/prompt.ts | 5 +++++ packages/opencode/src/session/summary.ts | 4 ++++ 5 files changed, 18 insertions(+) diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 0e7a7c5d3..ea967616b 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -222,6 +222,7 @@ export namespace Agent { } export async function generate(input: { description: string }) { + const cfg = await Config.get() const defaultModel = await Provider.defaultModel() const model = await Provider.getModel(defaultModel.providerID, defaultModel.modelID) const language = await Provider.getLanguage(model) @@ -229,6 +230,7 @@ export namespace Agent { system.push(PROMPT_GENERATE) const existing = await list() const result = await generateObject({ + experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry }, temperature: 0.3, prompt: [ ...system.map( diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 2c691cedb..03c4a39fb 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -670,6 +670,10 @@ export namespace Config { chatMaxRetries: z.number().optional().describe("Number of retries for chat completions on failure"), disable_paste_summary: z.boolean().optional(), batch_tool: z.boolean().optional().describe("Enable the batch tool"), + openTelemetry: z + .boolean() + .optional() + .describe("Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)"), primary_tools: z .array(z.string()) .optional() diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 07468995b..de75eda6e 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -10,6 +10,7 @@ import z from "zod" import { SessionPrompt } from "./prompt" import { Flag } from "../flag/flag" import { Token } from "../util/token" +import { Config } from "../config/config" import { Log } from "../util/log" import { ProviderTransform } from "@/provider/transform" import { SessionProcessor } from "./processor" @@ -96,6 +97,7 @@ export namespace SessionCompaction { abort: AbortSignal auto: boolean }) { + const cfg = await Config.get() const model = await Provider.getModel(input.model.providerID, input.model.modelID) const language = await Provider.getLanguage(model) const system = [...SystemPrompt.compaction(model.providerID)] @@ -191,6 +193,7 @@ export namespace SessionCompaction { }, ], }), + experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry }, }) if (result === "continue" && input.auto) { const continueMsg = await Session.updateMessage({ diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index ebf0a57d0..2f0bc0902 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -42,6 +42,7 @@ import { Command } from "../command" import { $, fileURLToPath } from "bun" import { ConfigMarkdown } from "../config/markdown" import { SessionSummary } from "./summary" +import { Config } from "../config/config" import { NamedError } from "@opencode-ai/util/error" import { fn } from "@/util/fn" import { SessionProcessor } from "./processor" @@ -433,6 +434,7 @@ export namespace SessionPrompt { } // normal processing + const cfg = await Config.get() const agent = await Agent.get(lastUser.agent) msgs = insertReminders({ messages: msgs, @@ -613,6 +615,7 @@ export namespace SessionPrompt { }, ], }), + experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry }, }) if (result === "stop") break continue @@ -1418,6 +1421,7 @@ export namespace SessionPrompt { input.history.filter((m) => m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic)) .length === 1 if (!isFirst) return + const cfg = await Config.get() const small = (await Provider.getSmallModel(input.providerID)) ?? (await Provider.getModel(input.providerID, input.modelID)) const language = await Provider.getLanguage(small) @@ -1464,6 +1468,7 @@ export namespace SessionPrompt { ], headers: small.headers, model: language, + experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry }, }) .then((result) => { if (result.text) diff --git a/packages/opencode/src/session/summary.ts b/packages/opencode/src/session/summary.ts index 8d366e499..ba0a1a00c 100644 --- a/packages/opencode/src/session/summary.ts +++ b/packages/opencode/src/session/summary.ts @@ -1,4 +1,5 @@ import { Provider } from "@/provider/provider" +import { Config } from "@/config/config" import { fn } from "@/util/fn" import z from "zod" import { Session } from "." @@ -60,6 +61,7 @@ export namespace SessionSummary { } async function summarizeMessage(input: { messageID: string; messages: MessageV2.WithParts[] }) { + const cfg = await Config.get() const messages = input.messages.filter( (m) => m.info.id === input.messageID || (m.info.role === "assistant" && m.info.parentID === input.messageID), ) @@ -109,6 +111,7 @@ export namespace SessionSummary { ], headers: small.headers, model: language, + experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry }, }) log.info("title", { title: result.text }) userMsg.summary.title = result.text @@ -150,6 +153,7 @@ export namespace SessionSummary { }, ], headers: small.headers, + experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry }, }).catch(() => {}) if (result) summary = result.text } From 85974e9acd0ab66ba1fe16a078b1c6df57160e9b Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 5 Dec 2025 10:50:14 -0600 Subject: [PATCH 5/8] ignore: regen sdk --- packages/sdk/js/src/gen/types.gen.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index 58ba58d35..60a68840c 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -1317,6 +1317,10 @@ export type Config = { * Enable the batch tool */ batch_tool?: boolean + /** + * Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag) + */ + openTelemetry?: boolean /** * Tools that should only be available to primary agents. */ @@ -1816,9 +1820,9 @@ export type PtyConnectData = { export type PtyConnectErrors = { /** - * Session not found + * Not found */ - 404: boolean + 404: NotFoundError } export type PtyConnectError = PtyConnectErrors[keyof PtyConnectErrors] From 81ee2d2332ce71ba1232387be4ad26ee37bb7a3b Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Fri, 5 Dec 2025 10:51:29 -0600 Subject: [PATCH 6/8] fix(desktop): prompting --- packages/desktop/src/pages/layout.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx index 106a2e733..658ad4bcb 100644 --- a/packages/desktop/src/pages/layout.tsx +++ b/packages/desktop/src/pages/layout.tsx @@ -22,9 +22,10 @@ export default function Layout(props: ParentProps) { const layout = useLayout() const currentDirectory = createMemo(() => base64Decode(params.dir ?? "")) const sessions = createMemo(() => globalSync.child(currentDirectory())[0].session ?? []) - const currentSession = createMemo(() => sessions().find((s) => s.id === params.id) ?? sessions().at(0)) + const currentSession = createMemo(() => sessions().find((s) => s.id === params.id)) function navigateToSession(session: Session | undefined) { + if (!session) return navigate(`/${params.dir}/session/${session?.id}`) } @@ -59,6 +60,7 @@ export default function Layout(props: ParentProps) {