From b2121c963fe65add9514d932b24f7844c82b2d12 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 2 Feb 2026 00:20:15 -0500 Subject: [PATCH 01/24] core: enable plan mode by default --- packages/opencode/src/flag/flag.ts | 1 - packages/opencode/src/session/prompt.ts | 28 ------------------------- packages/opencode/src/tool/registry.ts | 2 +- 3 files changed, 1 insertion(+), 30 deletions(-) diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index 64ae801d18..510fdcccfe 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -44,7 +44,6 @@ export namespace Flag { export const OPENCODE_EXPERIMENTAL_LSP_TY = truthy("OPENCODE_EXPERIMENTAL_LSP_TY") export const OPENCODE_EXPERIMENTAL_LSP_TOOL = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_LSP_TOOL") export const OPENCODE_DISABLE_FILETIME_CHECK = truthy("OPENCODE_DISABLE_FILETIME_CHECK") - export const OPENCODE_EXPERIMENTAL_PLAN_MODE = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_PLAN_MODE") export const OPENCODE_EXPERIMENTAL_MARKDOWN = truthy("OPENCODE_EXPERIMENTAL_MARKDOWN") export const OPENCODE_MODELS_URL = process.env["OPENCODE_MODELS_URL"] export const OPENCODE_MODELS_PATH = process.env["OPENCODE_MODELS_PATH"] diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 98dce97ba9..d1b527efbb 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -17,7 +17,6 @@ import { ProviderTransform } from "../provider/transform" import { SystemPrompt } from "./system" import { InstructionPrompt } from "./instruction" import { Plugin } from "../plugin" -import PROMPT_PLAN from "../session/prompt/plan.txt" import BUILD_SWITCH from "../session/prompt/build-switch.txt" import MAX_STEPS from "../session/prompt/max-steps.txt" import { defer } from "../util/defer" @@ -1232,33 +1231,6 @@ export namespace SessionPrompt { const userMessage = input.messages.findLast((msg) => msg.info.role === "user") if (!userMessage) return input.messages - // Original logic when experimental plan mode is disabled - if (!Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE) { - if (input.agent.name === "plan") { - userMessage.parts.push({ - id: Identifier.ascending("part"), - messageID: userMessage.info.id, - sessionID: userMessage.info.sessionID, - type: "text", - text: PROMPT_PLAN, - synthetic: true, - }) - } - const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.agent === "plan") - if (wasPlan && input.agent.name === "build") { - userMessage.parts.push({ - id: Identifier.ascending("part"), - messageID: userMessage.info.id, - sessionID: userMessage.info.sessionID, - type: "text", - text: BUILD_SWITCH, - synthetic: true, - }) - } - return input.messages - } - - // New plan mode logic when flag is enabled const assistantMessage = input.messages.findLast((msg) => msg.info.role === "assistant") // Switching from plan mode to build mode diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 7b3a458897..4989e5c2d8 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -117,7 +117,7 @@ export namespace ToolRegistry { ApplyPatchTool, ...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [LspTool] : []), ...(config.experimental?.batch_tool === true ? [BatchTool] : []), - ...(Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE && Flag.OPENCODE_CLIENT === "cli" ? [PlanExitTool, PlanEnterTool] : []), + ...(Flag.OPENCODE_CLIENT === "cli" ? [PlanExitTool, PlanEnterTool] : []), ...custom, ] } From cb3337f01e1cdd235e97be71c28854eccb311295 Mon Sep 17 00:00:00 2001 From: Jigar Date: Mon, 2 Feb 2026 10:58:28 +0530 Subject: [PATCH 02/24] fix: convert system message content to string for Copilot provider (#11600) Co-authored-by: Claude Opus 4.5 --- ...nvert-to-openai-compatible-chat-messages.ts | 7 +------ .../convert-to-copilot-messages.test.ts | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts b/packages/opencode/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts index d6f7cb34bb..e1e3ed4c20 100644 --- a/packages/opencode/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts +++ b/packages/opencode/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts @@ -18,12 +18,7 @@ export function convertToOpenAICompatibleChatMessages(prompt: LanguageModelV2Pro case "system": { messages.push({ role: "system", - content: [ - { - type: "text", - text: content, - }, - ], + content: content, ...metadata, }) break diff --git a/packages/opencode/test/provider/copilot/convert-to-copilot-messages.test.ts b/packages/opencode/test/provider/copilot/convert-to-copilot-messages.test.ts index 9f305123af..6f874db6d2 100644 --- a/packages/opencode/test/provider/copilot/convert-to-copilot-messages.test.ts +++ b/packages/opencode/test/provider/copilot/convert-to-copilot-messages.test.ts @@ -1,6 +1,24 @@ import { convertToOpenAICompatibleChatMessages as convertToCopilotMessages } from "@/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages" import { describe, test, expect } from "bun:test" +describe("system messages", () => { + test("should convert system message content to string", () => { + const result = convertToCopilotMessages([ + { + role: "system", + content: "You are a helpful assistant with AGENTS.md instructions.", + }, + ]) + + expect(result).toEqual([ + { + role: "system", + content: "You are a helpful assistant with AGENTS.md instructions.", + }, + ]) + }) +}) + describe("user messages", () => { test("should convert messages with only a text part to a string content", () => { const result = convertToCopilotMessages([ From 14dc940aac178701f446315425ac87679388ef7b Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 2 Feb 2026 00:29:52 -0500 Subject: [PATCH 03/24] zen: rate limit (#11735) --- .../app/src/routes/zen/util/rateLimiter.ts | 17 ++++++++++++++--- packages/console/core/src/model.ts | 7 ++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/console/app/src/routes/zen/util/rateLimiter.ts b/packages/console/app/src/routes/zen/util/rateLimiter.ts index 244db072c6..d54bd0306d 100644 --- a/packages/console/app/src/routes/zen/util/rateLimiter.ts +++ b/packages/console/app/src/routes/zen/util/rateLimiter.ts @@ -2,13 +2,17 @@ import { Database, eq, and, sql, inArray } from "@opencode-ai/console-core/drizz import { IpRateLimitTable } from "@opencode-ai/console-core/schema/ip.sql.js" import { RateLimitError } from "./error" import { logger } from "./logger" +import { ZenData } from "@opencode-ai/console-core/model.js" -export function createRateLimiter(limit: number | undefined, rawIp: string) { +export function createRateLimiter(limit: ZenData.RateLimit | undefined, rawIp: string) { if (!limit) return const ip = !rawIp.length ? "unknown" : rawIp const now = Date.now() - const intervals = [buildYYYYMMDDHH(now), buildYYYYMMDDHH(now - 3_600_000), buildYYYYMMDDHH(now - 7_200_000)] + const intervals = + limit.period === "day" + ? [buildYYYYMMDD(now)] + : [buildYYYYMMDDHH(now), buildYYYYMMDDHH(now - 3_600_000), buildYYYYMMDDHH(now - 7_200_000)] return { track: async () => { @@ -28,11 +32,18 @@ export function createRateLimiter(limit: number | undefined, rawIp: string) { ) const total = rows.reduce((sum, r) => sum + r.count, 0) logger.debug(`rate limit total: ${total}`) - if (total >= limit) throw new RateLimitError(`Rate limit exceeded. Please try again later.`) + if (total >= limit.value) throw new RateLimitError(`Rate limit exceeded. Please try again later.`) }, } } +function buildYYYYMMDD(timestamp: number) { + return new Date(timestamp) + .toISOString() + .replace(/[^0-9]/g, "") + .substring(0, 8) +} + function buildYYYYMMDDHH(timestamp: number) { return new Date(timestamp) .toISOString() diff --git a/packages/console/core/src/model.ts b/packages/console/core/src/model.ts index 880c63a190..fc9674cedb 100644 --- a/packages/console/core/src/model.ts +++ b/packages/console/core/src/model.ts @@ -18,8 +18,13 @@ export namespace ZenData { }), ), }) + const RateLimitSchema = z.object({ + period: z.enum(["day", "rolling"]), + value: z.number().int(), + }) export type Format = z.infer export type Trial = z.infer + export type RateLimit = z.infer const ModelCostSchema = z.object({ input: z.number(), @@ -37,7 +42,7 @@ export namespace ZenData { byokProvider: z.enum(["openai", "anthropic", "google"]).optional(), stickyProvider: z.enum(["strict", "prefer"]).optional(), trial: TrialSchema.optional(), - rateLimit: z.number().optional(), + rateLimit: RateLimitSchema.optional(), fallbackProvider: z.string().optional(), providers: z.array( z.object({ From 2d99b33466fe6f7146fdd15b2a411c55330323e5 Mon Sep 17 00:00:00 2001 From: mohammad Date: Mon, 2 Feb 2026 11:00:46 +0530 Subject: [PATCH 04/24] fix(desktop): kill zombie server process on startup timeout (#11602) Co-authored-by: Brendan Allan --- packages/desktop/src-tauri/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index 29ac86f29a..0958481ad2 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -526,6 +526,7 @@ async fn spawn_local_server( let timestamp = Instant::now(); loop { if timestamp.elapsed() > Duration::from_secs(30) { + let _ = child.kill(); break Err(format!( "Failed to spawn OpenCode Server. Logs:\n{}", get_logs(app.clone()).await.unwrap() From f51a9c4fce611977baca36a6d3cbe3bace796971 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 2 Feb 2026 08:12:50 +0100 Subject: [PATCH 05/24] Use opentui OSC52 clipboard, again (#11744) --- bun.lock | 20 +++++++------- packages/opencode/package.json | 4 +-- packages/opencode/src/cli/cmd/tui/app.tsx | 2 ++ .../src/cli/cmd/tui/util/clipboard.ts | 26 ++++++++----------- 4 files changed, 25 insertions(+), 27 deletions(-) diff --git a/bun.lock b/bun.lock index 0857283634..c98144251c 100644 --- a/bun.lock +++ b/bun.lock @@ -298,8 +298,8 @@ "@opencode-ai/sdk": "workspace:*", "@opencode-ai/util": "workspace:*", "@openrouter/ai-sdk-provider": "1.5.4", - "@opentui/core": "0.1.75", - "@opentui/solid": "0.1.75", + "@opentui/core": "0.1.77", + "@opentui/solid": "0.1.77", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", @@ -1227,21 +1227,21 @@ "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], - "@opentui/core": ["@opentui/core@0.1.75", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.75", "@opentui/core-darwin-x64": "0.1.75", "@opentui/core-linux-arm64": "0.1.75", "@opentui/core-linux-x64": "0.1.75", "@opentui/core-win32-arm64": "0.1.75", "@opentui/core-win32-x64": "0.1.75", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-8ARRZxSG+BXkJmEVtM2DQ4se7DAF1ZCKD07d+AklgTr2mxCzmdxxPbOwRzboSQ6FM7qGuTVPVbV4O2W9DpUmoA=="], + "@opentui/core": ["@opentui/core@0.1.77", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.77", "@opentui/core-darwin-x64": "0.1.77", "@opentui/core-linux-arm64": "0.1.77", "@opentui/core-linux-x64": "0.1.77", "@opentui/core-win32-arm64": "0.1.77", "@opentui/core-win32-x64": "0.1.77", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-lE3kabm6jdqK3AuBq+O0zZrXdxt6ulmibTc57sf+AsPny6cmwYHnWI4wD6hcreFiYoQVNVvdiJchVgPtowMlEg=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.75", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gGaGZjkFpqcXJk6321JzhRl66pM2VxBlI470L8W4DQUW4S6iDT1R9L7awSzGB4Cn9toUl7DTV8BemaXZYXV4SA=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.77", "", { "os": "darwin", "cpu": "arm64" }, "sha512-SNqmygCMEsPCW7xWjzCZ5caBf36xaprwVdAnFijGDOuIzLA4iaDa6um8cj3TJh7awenN3NTRsuRc7OuH42UH+g=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.75", "", { "os": "darwin", "cpu": "x64" }, "sha512-tPlvqQI0whZ76amHydpJs5kN+QeWAIcFbI8RAtlAo9baj2EbxTDC+JGwgb9Fnt0/YQx831humbtaNDhV2Jt1bw=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.77", "", { "os": "darwin", "cpu": "x64" }, "sha512-/8fsa03swEHTQt/9NrGm98kemlU+VuTURI/OFZiH53vPDRrOYIYoa4Jyga/H7ZMcG+iFhkq97zIe+0Kw95LGmA=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.75", "", { "os": "linux", "cpu": "arm64" }, "sha512-nVxIQ4Hqf84uBergDpWiVzU6pzpjy6tqBHRQpySxZ2flkJ/U6/aMEizVrQ1jcgIdxZtvqWDETZhzxhG0yDx+cw=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.77", "", { "os": "linux", "cpu": "arm64" }, "sha512-QfUXZJPc69OvqoMu+AlLgjqXrwu4IeqcBuUWYMuH8nPTeLsVUc3CBbXdV2lv9UDxWzxzrxdS4ALPaxvmEv9lsQ=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.75", "", { "os": "linux", "cpu": "x64" }, "sha512-1CnApef4kxA+ORyLfbuCLgZfEjp4wr3HjFnt7FAfOb73kIZH82cb7JYixeqRyy9eOcKfKqxLmBYy3o8IDkc4Rg=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.77", "", { "os": "linux", "cpu": "x64" }, "sha512-Kmfx0yUKnPj67AoXYIgL7qQo0QVsUG5Iw8aRtv6XFzXqa5SzBPhaKkKZ9yHPjOmTalZquUs+9zcCRNKpYYuL7A=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.75", "", { "os": "win32", "cpu": "arm64" }, "sha512-j0UB95nmkYGNzmOrs6GqaddO1S90R0YC6IhbKnbKBdjchFPNVLz9JpexAs6MBDXPZwdKAywMxtwG2h3aTJtxng=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.77", "", { "os": "win32", "cpu": "arm64" }, "sha512-HGTscPXc7gdd23Nh1DbzUNjog1I+5IZp95XPtLftGTpjrWs60VcetXcyJqK2rQcXNxewJK5yDyaa5QyMjfEhCQ=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.75", "", { "os": "win32", "cpu": "x64" }, "sha512-ESpVZVGewe3JkB2TwrG3VRbkxT909iPdtvgNT7xTCIYH2VB4jqZomJfvERPTE0tvqAZJm19mHECzJFI8asSJgQ=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.77", "", { "os": "win32", "cpu": "x64" }, "sha512-c7GijsbvVgnlzd2murIbwuwrGbcv76KdUw6WlVv7a0vex50z6xJCpv1keGzpe0QfxrZ/6fFEFX7JnwGLno0wjA=="], - "@opentui/solid": ["@opentui/solid@0.1.75", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.75", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-WjKsZIfrm29znfRlcD9w3uUn/+uvoy2MmeoDwTvg1YOa0OjCTCmjZ43L9imp0m9S4HmVU8ma6o2bR4COzcyDdg=="], + "@opentui/solid": ["@opentui/solid@0.1.77", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.77", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-JY+hUbXVV+XCk6bC8dvcwawWCEmC3Gid6GDs23AJWBgHZ3TU2kRKrgwTdltm45DOq2cZXrYCt690/yE8bP+Gxg=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 4afb724300..c86aa734d8 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -82,8 +82,8 @@ "@opencode-ai/sdk": "workspace:*", "@opencode-ai/util": "workspace:*", "@openrouter/ai-sdk-provider": "1.5.4", - "@opentui/core": "0.1.75", - "@opentui/solid": "0.1.75", + "@opentui/core": "0.1.77", + "@opentui/solid": "0.1.77", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 713def2e5a..c5f88a7c67 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -169,6 +169,7 @@ export function tui(input: { gatherStats: false, exitOnCtrlC: false, useKittyKeyboard: {}, + autoFocus: false, consoleOptions: { keyBindings: [{ name: "y", ctrl: true, action: "copy-selection" }], onCopySelection: (text) => { @@ -186,6 +187,7 @@ function App() { const route = useRoute() const dimensions = useTerminalDimensions() const renderer = useRenderer() + Clipboard.setRenderer(renderer) renderer.disableStdoutInterception() const dialog = useDialog() const local = useLocal() diff --git a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts index 0e287fbc41..5c27a26cd0 100644 --- a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts +++ b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts @@ -1,24 +1,12 @@ import { $ } from "bun" +import type { CliRenderer } from "@opentui/core" import { platform, release } from "os" import clipboardy from "clipboardy" import { lazy } from "../../../../util/lazy.js" import { tmpdir } from "os" import path from "path" -/** - * Writes text to clipboard via OSC 52 escape sequence. - * This allows clipboard operations to work over SSH by having - * the terminal emulator handle the clipboard locally. - */ -function writeOsc52(text: string): void { - if (!process.stdout.isTTY) return - const base64 = Buffer.from(text).toString("base64") - const osc52 = `\x1b]52;c;${base64}\x07` - // tmux and screen require DCS passthrough wrapping - const passthrough = process.env["TMUX"] || process.env["STY"] - const sequence = passthrough ? `\x1bPtmux;\x1b${osc52}\x1b\\` : osc52 - process.stdout.write(sequence) -} +const rendererRef = { current: undefined as CliRenderer | undefined } export namespace Clipboard { export interface Content { @@ -26,6 +14,10 @@ export namespace Clipboard { mime: string } + export function setRenderer(renderer: CliRenderer | undefined): void { + rendererRef.current = renderer + } + export async function read(): Promise { const os = platform() @@ -154,7 +146,11 @@ export namespace Clipboard { }) export async function copy(text: string): Promise { - writeOsc52(text) + const renderer = rendererRef.current + if (renderer) { + const copied = renderer.copyToClipboardOSC52(text) + if (copied) return + } await getCopyMethod()(text) } } From 8aba57455591f35935200bcd4fbecafb849c69d2 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Mon, 2 Feb 2026 07:21:25 +0000 Subject: [PATCH 06/24] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 6fe8f61d3d..5180219533 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-06Otz3loT4vn0578VDxUqVudtzQvV7oM3EIzjZnsejo=", - "aarch64-linux": "sha256-88Qai5RkSenCZkakOg52b6xU2ok+h/Ns4/5L3+55sFY=", - "aarch64-darwin": "sha256-x8dgCF0CJBWi2dZLDHMGdlTqys1X755ok0PM6x0HAGo=", - "x86_64-darwin": "sha256-FkLDqorfIfOw+tB7SW5vgyhOIoI0IV9lqPW1iEmvUiI=" + "x86_64-linux": "sha256-4I0lpBnbAi7IZMURTMLysjrqdsNvXJf8802NrJnpdks=", + "aarch64-linux": "sha256-WOGKsPlcQVSbL8TDr1JYO/2ucPTV2Hy0TXJKWv8EoVw=", + "aarch64-darwin": "sha256-LuvjwGm1QsHoLxuvSSp4VsDIv02Z/rTONsU32arQMuw=", + "x86_64-darwin": "sha256-AbglfgCWj/r+wHfle+e+D3b/xPcwwg4IK7j5iwn9nzw=" } } From bcf8c33b11941962785c80d9a9554df0b08108b1 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Mon, 2 Feb 2026 15:54:25 +0800 Subject: [PATCH 07/24] fix(desktop): keep mac titlebar stable under zoom (#11747) --- packages/app/src/components/titlebar.tsx | 5 +++- packages/app/src/context/platform.tsx | 4 +++ packages/desktop/src/index.tsx | 4 ++- packages/desktop/src/webview-zoom.ts | 38 ++++++++++++++---------- 4 files changed, 33 insertions(+), 18 deletions(-) diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index 001f7a5679..affb685411 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -24,6 +24,8 @@ export function Titlebar() { const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos") const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows") const web = createMemo(() => platform.platform === "web") + const zoom = () => platform.webviewZoom?.() ?? 1 + const minHeight = () => (mac() ? `${40 / zoom()}px` : undefined) const [history, setHistory] = createStore({ stack: [] as string[], @@ -134,6 +136,7 @@ export function Titlebar() { return (
-
+
+ + /** Webview zoom level (desktop only) */ + webviewZoom?: Accessor } export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({ diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index 2e7ca136ac..505a00b16a 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -1,5 +1,5 @@ // @refresh reload -import "./webview-zoom" +import { webviewZoom } from "./webview-zoom" import { render } from "solid-js/web" import { AppBaseProviders, AppInterface, PlatformProvider, Platform } from "@opencode-ai/app" import { open, save } from "@tauri-apps/plugin-dialog" @@ -346,6 +346,8 @@ const createPlatform = (password: Accessor): Platform => ({ parseMarkdown: async (markdown: string) => { return invoke("parse_markdown_command", { markdown }) }, + + webviewZoom, }) createMenu() diff --git a/packages/desktop/src/webview-zoom.ts b/packages/desktop/src/webview-zoom.ts index 9fa9bb9ed9..06f46a3afd 100644 --- a/packages/desktop/src/webview-zoom.ts +++ b/packages/desktop/src/webview-zoom.ts @@ -4,28 +4,34 @@ import { invoke } from "@tauri-apps/api/core" import { type as ostype } from "@tauri-apps/plugin-os" +import { createSignal } from "solid-js" const OS_NAME = ostype() -let zoomLevel = 1 +const [webviewZoom, setWebviewZoom] = createSignal(1) const MAX_ZOOM_LEVEL = 10 const MIN_ZOOM_LEVEL = 0.2 +const clamp = (value: number) => Math.min(Math.max(value, MIN_ZOOM_LEVEL), MAX_ZOOM_LEVEL) + +const applyZoom = (next: number) => { + setWebviewZoom(next) + invoke("plugin:webview|set_webview_zoom", { + value: next, + }) +} + window.addEventListener("keydown", (event) => { - if (OS_NAME === "macos" ? event.metaKey : event.ctrlKey) { - if (event.key === "-") { - zoomLevel -= 0.2 - } else if (event.key === "=" || event.key === "+") { - zoomLevel += 0.2 - } else if (event.key === "0") { - zoomLevel = 1 - } else { - return - } - zoomLevel = Math.min(Math.max(zoomLevel, MIN_ZOOM_LEVEL), MAX_ZOOM_LEVEL) - invoke("plugin:webview|set_webview_zoom", { - value: zoomLevel, - }) - } + if (!(OS_NAME === "macos" ? event.metaKey : event.ctrlKey)) return + + let newZoom = webviewZoom() + + if (event.key === "-") newZoom -= 0.2 + if (event.key === "=" || event.key === "+") newZoom += 0.2 + if (event.key === "0") newZoom = 1 + + applyZoom(clamp(newZoom)) }) + +export { webviewZoom } From 2c12ee5a2080d2056ebb77ce8e9d0b8279db5bc8 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Mon, 2 Feb 2026 15:58:08 +0800 Subject: [PATCH 08/24] chore(desktop): integrate tauri-specta (#11740) --- packages/desktop/src-tauri/Cargo.lock | 247 +++++++++++++++++---- packages/desktop/src-tauri/Cargo.toml | 14 +- packages/desktop/src-tauri/src/cli.rs | 1 + packages/desktop/src-tauri/src/lib.rs | 41 +++- packages/desktop/src-tauri/src/markdown.rs | 5 +- packages/desktop/src/bindings.ts | 20 ++ packages/desktop/src/cli.ts | 4 +- packages/desktop/src/index.tsx | 20 +- packages/desktop/src/menu.ts | 4 +- packages/desktop/src/updater.ts | 6 +- 10 files changed, 289 insertions(+), 73 deletions(-) create mode 100644 packages/desktop/src/bindings.ts diff --git a/packages/desktop/src-tauri/Cargo.lock b/packages/desktop/src-tauri/Cargo.lock index 51e5e00490..14030218ed 100644 --- a/packages/desktop/src-tauri/Cargo.lock +++ b/packages/desktop/src-tauri/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" + [[package]] name = "adler2" version = "2.0.1" @@ -1994,9 +2000,9 @@ dependencies = [ [[package]] name = "ico" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" dependencies = [ "byteorder", "png 0.17.16", @@ -3065,12 +3071,14 @@ dependencies = [ "listeners", "objc2 0.6.3", "objc2-web-kit", - "reqwest", + "reqwest 0.12.24", "semver", "serde", "serde_json", + "specta", + "specta-typescript", "tauri", - "tauri-build", + "tauri-build 2.5.2", "tauri-plugin-clipboard-manager", "tauri-plugin-decorum", "tauri-plugin-deep-link", @@ -3085,6 +3093,7 @@ dependencies = [ "tauri-plugin-store", "tauri-plugin-updater", "tauri-plugin-window-state", + "tauri-specta", "tokio", "uuid", "webkit2gtk", @@ -3221,6 +3230,12 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pathdiff" version = "0.2.3" @@ -3947,6 +3962,40 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "reqwest" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + [[package]] name = "rfd" version = "0.15.4" @@ -4497,6 +4546,44 @@ dependencies = [ "system-deps", ] +[[package]] +name = "specta" +version = "2.0.0-rc.22" +source = "git+https://github.com/specta-rs/specta?rev=106425eac4964d8ff34d3a02f1612e33117b08bb#106425eac4964d8ff34d3a02f1612e33117b08bb" +dependencies = [ + "paste", + "rustc_version", + "specta-macros", +] + +[[package]] +name = "specta-macros" +version = "2.0.0-rc.18" +source = "git+https://github.com/specta-rs/specta?rev=106425eac4964d8ff34d3a02f1612e33117b08bb#106425eac4964d8ff34d3a02f1612e33117b08bb" +dependencies = [ + "Inflector", + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "specta-serde" +version = "0.0.9" +source = "git+https://github.com/specta-rs/specta?rev=106425eac4964d8ff34d3a02f1612e33117b08bb#106425eac4964d8ff34d3a02f1612e33117b08bb" +dependencies = [ + "specta", +] + +[[package]] +name = "specta-typescript" +version = "0.0.9" +source = "git+https://github.com/specta-rs/specta?rev=106425eac4964d8ff34d3a02f1612e33117b08bb#106425eac4964d8ff34d3a02f1612e33117b08bb" +dependencies = [ + "specta", + "specta-serde", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -4712,9 +4799,8 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e492485dd390b35f7497401f67694f46161a2a00ffd800938d5dd3c898fb9d8" +version = "2.9.5" +source = "git+https://github.com/tauri-apps/tauri?rev=4d5d78daf636feaac20c5bc48a6071491c4291ee#4d5d78daf636feaac20c5bc48a6071491c4291ee" dependencies = [ "anyhow", "bytes", @@ -4740,17 +4826,18 @@ dependencies = [ "percent-encoding", "plist", "raw-window-handle", - "reqwest", + "reqwest 0.13.1", "serde", "serde_json", "serde_repr", "serialize-to-javascript", + "specta", "swift-rs", - "tauri-build", + "tauri-build 2.5.3", "tauri-macros", "tauri-runtime", "tauri-runtime-wry", - "tauri-utils", + "tauri-utils 2.8.1", "thiserror 2.0.17", "tokio", "tray-icon", @@ -4777,7 +4864,28 @@ dependencies = [ "semver", "serde", "serde_json", - "tauri-utils", + "tauri-utils 2.8.0", + "tauri-winres", + "toml 0.9.8", + "walkdir", +] + +[[package]] +name = "tauri-build" +version = "2.5.3" +source = "git+https://github.com/tauri-apps/tauri?rev=4d5d78daf636feaac20c5bc48a6071491c4291ee#4d5d78daf636feaac20c5bc48a6071491c4291ee" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils 2.8.1", "tauri-winres", "toml 0.9.8", "walkdir", @@ -4785,9 +4893,8 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "2.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7ef707148f0755110ca54377560ab891d722de4d53297595380a748026f139f" +version = "2.5.2" +source = "git+https://github.com/tauri-apps/tauri?rev=4d5d78daf636feaac20c5bc48a6071491c4291ee#4d5d78daf636feaac20c5bc48a6071491c4291ee" dependencies = [ "base64 0.22.1", "brotli", @@ -4802,7 +4909,7 @@ dependencies = [ "serde_json", "sha2", "syn 2.0.110", - "tauri-utils", + "tauri-utils 2.8.1", "thiserror 2.0.17", "time", "url", @@ -4812,16 +4919,15 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71664fd715ee6e382c05345ad258d6d1d50f90cf1b58c0aa726638b33c2a075d" +version = "2.5.2" +source = "git+https://github.com/tauri-apps/tauri?rev=4d5d78daf636feaac20c5bc48a6071491c4291ee#4d5d78daf636feaac20c5bc48a6071491c4291ee" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.110", "tauri-codegen", - "tauri-utils", + "tauri-utils 2.8.1", ] [[package]] @@ -4836,7 +4942,7 @@ dependencies = [ "schemars 0.8.22", "serde", "serde_json", - "tauri-utils", + "tauri-utils 2.8.0", "toml 0.9.8", "walkdir", ] @@ -4886,7 +4992,7 @@ dependencies = [ "serde_json", "tauri", "tauri-plugin", - "tauri-utils", + "tauri-utils 2.8.0", "thiserror 2.0.17", "tracing", "url", @@ -4928,7 +5034,7 @@ dependencies = [ "serde_repr", "tauri", "tauri-plugin", - "tauri-utils", + "tauri-utils 2.8.0", "thiserror 2.0.17", "toml 0.9.8", "url", @@ -4945,7 +5051,7 @@ dependencies = [ "data-url", "http", "regex", - "reqwest", + "reqwest 0.12.24", "schemars 0.8.22", "serde", "serde_json", @@ -5096,7 +5202,7 @@ dependencies = [ "minisign-verify", "osakit", "percent-encoding", - "reqwest", + "reqwest 0.12.24", "semver", "serde", "serde_json", @@ -5129,9 +5235,8 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9368f09358496f2229313fccb37682ad116b7f46fa76981efe116994a0628926" +version = "2.9.2" +source = "git+https://github.com/tauri-apps/tauri?rev=4d5d78daf636feaac20c5bc48a6071491c4291ee#4d5d78daf636feaac20c5bc48a6071491c4291ee" dependencies = [ "cookie", "dpi", @@ -5144,7 +5249,7 @@ dependencies = [ "raw-window-handle", "serde", "serde_json", - "tauri-utils", + "tauri-utils 2.8.1", "thiserror 2.0.17", "url", "webkit2gtk", @@ -5154,9 +5259,8 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "929f5df216f5c02a9e894554401bcdab6eec3e39ec6a4a7731c7067fc8688a93" +version = "2.9.3" +source = "git+https://github.com/tauri-apps/tauri?rev=4d5d78daf636feaac20c5bc48a6071491c4291ee#4d5d78daf636feaac20c5bc48a6071491c4291ee" dependencies = [ "gtk", "http", @@ -5171,7 +5275,7 @@ dependencies = [ "softbuffer", "tao", "tauri-runtime", - "tauri-utils", + "tauri-utils 2.8.1", "url", "webkit2gtk", "webview2-com", @@ -5179,11 +5283,74 @@ dependencies = [ "wry", ] +[[package]] +name = "tauri-specta" +version = "2.0.0-rc.21" +source = "git+https://github.com/specta-rs/tauri-specta?rev=6720b2848eff9a3e40af54c48d65f6d56b640c0b#6720b2848eff9a3e40af54c48d65f6d56b640c0b" +dependencies = [ + "heck 0.5.0", + "serde", + "serde_json", + "specta", + "specta-typescript", + "tauri", + "tauri-specta-macros", + "thiserror 2.0.17", +] + +[[package]] +name = "tauri-specta-macros" +version = "2.0.0-rc.16" +source = "git+https://github.com/specta-rs/tauri-specta?rev=6720b2848eff9a3e40af54c48d65f6d56b640c0b#6720b2848eff9a3e40af54c48d65f6d56b640c0b" +dependencies = [ + "darling", + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "tauri-utils" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6b8bbe426abdbf52d050e52ed693130dbd68375b9ad82a3fb17efb4c8d85673" +dependencies = [ + "anyhow", + "cargo_metadata", + "ctor", + "dunce", + "glob", + "html5ever", + "http", + "infer", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.3", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.17", + "toml 0.9.8", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-utils" +version = "2.8.1" +source = "git+https://github.com/tauri-apps/tauri?rev=4d5d78daf636feaac20c5bc48a6071491c4291ee#4d5d78daf636feaac20c5bc48a6071491c4291ee" dependencies = [ "anyhow", "brotli", @@ -5547,9 +5714,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "bitflags 2.10.0", "bytes", @@ -6034,9 +6201,9 @@ dependencies = [ [[package]] name = "webkit2gtk" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" dependencies = [ "bitflags 1.3.2", "cairo-rs", @@ -6058,9 +6225,9 @@ dependencies = [ [[package]] name = "webkit2gtk-sys" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" dependencies = [ "bitflags 1.3.2", "cairo-sys-rs", @@ -6719,9 +6886,9 @@ checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "wry" -version = "0.53.5" +version = "0.54.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728b7d4c8ec8d81cab295e0b5b8a4c263c0d41a785fb8f8c4df284e5411140a2" +checksum = "5ed1a195b0375491dd15a7066a10251be217ce743cf4bbbbdcf5391d6473bee0" dependencies = [ "base64 0.22.1", "block2 0.6.2", diff --git a/packages/desktop/src-tauri/Cargo.toml b/packages/desktop/src-tauri/Cargo.toml index e87bf77c2e..2efa484e82 100644 --- a/packages/desktop/src-tauri/Cargo.toml +++ b/packages/desktop/src-tauri/Cargo.toml @@ -18,7 +18,7 @@ crate-type = ["staticlib", "cdylib", "rlib"] tauri-build = { version = "2", features = [] } [dependencies] -tauri = { version = "2", features = ["macos-private-api", "devtools"] } +tauri = { version = "2.9.5", features = ["macos-private-api", "devtools"] } tauri-plugin-opener = "2" tauri-plugin-deep-link = "2.4.6" tauri-plugin-shell = "2" @@ -43,10 +43,13 @@ reqwest = { version = "0.12", default-features = false, features = ["rustls-tls" uuid = { version = "1.19.0", features = ["v4"] } tauri-plugin-decorum = "1.1.1" comrak = { version = "0.50", default-features = false } +specta = "=2.0.0-rc.22" +specta-typescript = "0.0.9" +tauri-specta = { version = "=2.0.0-rc.21", features = ["derive", "typescript"] } [target.'cfg(target_os = "linux")'.dependencies] gtk = "0.18.2" -webkit2gtk = "=2.0.1" +webkit2gtk = "=2.0.2" [target.'cfg(target_os = "macos")'.dependencies] objc2 = "0.6" @@ -59,3 +62,10 @@ windows = { version = "0.61", features = [ "Win32_System_Threading", "Win32_Security" ] } + +[patch.crates-io] +specta = { git = "https://github.com/specta-rs/specta", rev = "106425eac4964d8ff34d3a02f1612e33117b08bb" } +specta-typescript = { git = "https://github.com/specta-rs/specta", rev = "106425eac4964d8ff34d3a02f1612e33117b08bb" } +tauri-specta = { git = "https://github.com/specta-rs/tauri-specta", rev = "6720b2848eff9a3e40af54c48d65f6d56b640c0b" } +# TODO: https://github.com/tauri-apps/tauri/pull/14812 +tauri = { git = "https://github.com/tauri-apps/tauri", rev = "4d5d78daf636feaac20c5bc48a6071491c4291ee" } diff --git a/packages/desktop/src-tauri/src/cli.rs b/packages/desktop/src-tauri/src/cli.rs index f64beed6a1..16e4bfec99 100644 --- a/packages/desktop/src-tauri/src/cli.rs +++ b/packages/desktop/src-tauri/src/cli.rs @@ -51,6 +51,7 @@ fn is_cli_installed() -> bool { const INSTALL_SCRIPT: &str = include_str!("../../../../install"); #[tauri::command] +#[specta::specta] pub fn install_cli(app: tauri::AppHandle) -> Result { if cfg!(not(unix)) { return Err("CLI installation is only supported on macOS & Linux".to_string()); diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index 0958481ad2..2fe7c4aa17 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -16,10 +16,10 @@ use std::{ time::{Duration, Instant}, }; use tauri::{AppHandle, LogicalSize, Manager, RunEvent, State, WebviewWindowBuilder}; -#[cfg(any(target_os = "linux", all(debug_assertions, windows)))] -use tauri_plugin_deep_link::DeepLinkExt; #[cfg(windows)] use tauri_plugin_decorum::WebviewWindowExt; +#[cfg(any(target_os = "linux", all(debug_assertions, windows)))] +use tauri_plugin_deep_link::DeepLinkExt; use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult}; use tauri_plugin_shell::process::{CommandChild, CommandEvent}; use tauri_plugin_store::StoreExt; @@ -30,7 +30,7 @@ use crate::window_customizer::PinchZoomDisablePlugin; const SETTINGS_STORE: &str = "opencode.settings.dat"; const DEFAULT_SERVER_URL_KEY: &str = "defaultServerUrl"; -#[derive(Clone, serde::Serialize)] +#[derive(Clone, serde::Serialize, specta::Type)] struct ServerReadyData { url: String, password: Option, @@ -64,6 +64,7 @@ struct LogState(Arc>>); const MAX_LOG_ENTRIES: usize = 200; #[tauri::command] +#[specta::specta] fn kill_sidecar(app: AppHandle) { let Some(server_state) = app.try_state::() else { println!("Server not running"); @@ -97,6 +98,7 @@ async fn get_logs(app: AppHandle) -> Result { } #[tauri::command] +#[specta::specta] async fn ensure_server_ready(state: State<'_, ServerState>) -> Result { state .status @@ -106,6 +108,7 @@ async fn ensure_server_ready(state: State<'_, ServerState>) -> Result Result, String> { let store = app .store(SETTINGS_STORE) @@ -119,6 +122,7 @@ fn get_default_server_url(app: AppHandle) -> Result, String> { } #[tauri::command] +#[specta::specta] async fn set_default_server_url(app: AppHandle, url: Option) -> Result<(), String> { let store = app .store(SETTINGS_STORE) @@ -252,6 +256,26 @@ async fn check_server_health(url: &str, password: Option<&str>) -> bool { pub fn run() { let updater_enabled = option_env!("TAURI_SIGNING_PRIVATE_KEY").is_some(); + let builder = tauri_specta::Builder::::new() + // Then register them (separated by a comma) + .commands(tauri_specta::collect_commands![ + kill_sidecar, + install_cli, + ensure_server_ready, + get_default_server_url, + set_default_server_url, + markdown::parse_markdown_command + ]) + .error_handling(tauri_specta::ErrorHandlingMode::Throw); + + #[cfg(debug_assertions)] // <- Only export on non-release builds + builder + .export( + specta_typescript::Typescript::default(), + "../src/bindings.ts", + ) + .expect("Failed to export typescript bindings"); + #[cfg(all(target_os = "macos", not(debug_assertions)))] let _ = std::process::Command::new("killall") .arg("opencode-cli") @@ -285,15 +309,10 @@ pub fn run() { .plugin(tauri_plugin_notification::init()) .plugin(PinchZoomDisablePlugin) .plugin(tauri_plugin_decorum::init()) - .invoke_handler(tauri::generate_handler![ - kill_sidecar, - install_cli, - ensure_server_ready, - get_default_server_url, - set_default_server_url, - markdown::parse_markdown_command - ]) + .invoke_handler(builder.invoke_handler()) .setup(move |app| { + builder.mount_events(app); + #[cfg(any(target_os = "linux", all(debug_assertions, windows)))] app.deep_link().register_all().ok(); diff --git a/packages/desktop/src-tauri/src/markdown.rs b/packages/desktop/src-tauri/src/markdown.rs index c3ca73857e..39a64a4318 100644 --- a/packages/desktop/src-tauri/src/markdown.rs +++ b/packages/desktop/src-tauri/src/markdown.rs @@ -1,4 +1,6 @@ -use comrak::{create_formatter, parse_document, Arena, Options, html::ChildRendering, nodes::NodeValue}; +use comrak::{ + Arena, Options, create_formatter, html::ChildRendering, nodes::NodeValue, parse_document, +}; use std::fmt::Write; create_formatter!(ExternalLinkFormatter, { @@ -55,6 +57,7 @@ pub fn parse_markdown(input: &str) -> String { } #[tauri::command] +#[specta::specta] pub async fn parse_markdown_command(markdown: String) -> Result { Ok(parse_markdown(&markdown)) } diff --git a/packages/desktop/src/bindings.ts b/packages/desktop/src/bindings.ts new file mode 100644 index 0000000000..eb5498fa68 --- /dev/null +++ b/packages/desktop/src/bindings.ts @@ -0,0 +1,20 @@ +// This file has been generated by Tauri Specta. Do not edit this file manually. + +import { invoke as __TAURI_INVOKE, Channel } from '@tauri-apps/api/core'; + +/** Commands */ +export const commands = { + killSidecar: () => __TAURI_INVOKE("kill_sidecar"), + installCli: () => __TAURI_INVOKE("install_cli"), + ensureServerReady: () => __TAURI_INVOKE("ensure_server_ready"), + getDefaultServerUrl: () => __TAURI_INVOKE("get_default_server_url"), + setDefaultServerUrl: (url: string | null) => __TAURI_INVOKE("set_default_server_url", { url }), + parseMarkdownCommand: (markdown: string) => __TAURI_INVOKE("parse_markdown_command", { markdown }), +}; + +/* Types */ +export type ServerReadyData = { + url: string, + password: string | null, + }; + diff --git a/packages/desktop/src/cli.ts b/packages/desktop/src/cli.ts index 5a8875cf89..28623bdf78 100644 --- a/packages/desktop/src/cli.ts +++ b/packages/desktop/src/cli.ts @@ -1,13 +1,13 @@ -import { invoke } from "@tauri-apps/api/core" import { message } from "@tauri-apps/plugin-dialog" import { initI18n, t } from "./i18n" +import { commands } from "./bindings" export async function installCli(): Promise { await initI18n() try { - const path = await invoke("install_cli") + const path = await commands.installCli() await message(t("desktop.cli.installed.message", { path }), { title: t("desktop.cli.installed.title") }) } catch (e) { await message(t("desktop.cli.failed.message", { error: String(e) }), { title: t("desktop.cli.failed.title") }) diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index 505a00b16a..2b44061626 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -7,7 +7,6 @@ import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link" import { open as shellOpen } from "@tauri-apps/plugin-shell" import { type as ostype } from "@tauri-apps/plugin-os" import { check, Update } from "@tauri-apps/plugin-updater" -import { invoke } from "@tauri-apps/api/core" import { getCurrentWindow } from "@tauri-apps/api/window" import { isPermissionGranted, requestPermission } from "@tauri-apps/plugin-notification" import { relaunch } from "@tauri-apps/plugin-process" @@ -22,6 +21,7 @@ import { createMenu } from "./menu" import { initI18n, t } from "./i18n" import pkg from "../package.json" import "./styles.css" +import { commands } from "./bindings" const root = document.getElementById("root") if (import.meta.env.DEV && !(root instanceof HTMLElement)) { @@ -274,12 +274,12 @@ const createPlatform = (password: Accessor): Platform => ({ update: async () => { if (!UPDATER_ENABLED || !update) return - if (ostype() === "windows") await invoke("kill_sidecar").catch(() => undefined) + if (ostype() === "windows") await commands.killSidecar().catch(() => undefined) await update.install().catch(() => undefined) }, restart: async () => { - await invoke("kill_sidecar").catch(() => undefined) + await commands.killSidecar().catch(() => undefined) await relaunch() }, @@ -335,17 +335,13 @@ const createPlatform = (password: Accessor): Platform => ({ }, getDefaultServerUrl: async () => { - const result = await invoke("get_default_server_url").catch(() => null) + const result = await commands.getDefaultServerUrl().catch(() => null) return result }, - setDefaultServerUrl: async (url: string | null) => { - await invoke("set_default_server_url", { url }) - }, + setDefaultServerUrl: async (url: string | null) => { await commands.setDefaultServerUrl(url) }, - parseMarkdown: async (markdown: string) => { - return invoke("parse_markdown_command", { markdown }) - }, + parseMarkdown: (markdown: string) => commands.parseMarkdownCommand(markdown), webviewZoom, }) @@ -394,7 +390,7 @@ type ServerReadyData = { url: string; password: string | null } // Gate component that waits for the server to be ready function ServerGate(props: { children: (data: Accessor) => JSX.Element }) { const [serverData] = createResource(() => - invoke("ensure_server_ready").then((v) => { + commands.ensureServerReady().then((v) => { return new Promise((res) => setTimeout(() => res(v as ServerReadyData), 2000)) }), ) @@ -408,7 +404,7 @@ function ServerGate(props: { children: (data: Accessor) => JSX. } const restartApp = async () => { - await invoke("kill_sidecar").catch(() => undefined) + await commands.killSidecar().catch(() => undefined) await relaunch().catch(() => undefined) } diff --git a/packages/desktop/src/menu.ts b/packages/desktop/src/menu.ts index 2edeff42b2..d410844042 100644 --- a/packages/desktop/src/menu.ts +++ b/packages/desktop/src/menu.ts @@ -1,11 +1,11 @@ import { Menu, MenuItem, PredefinedMenuItem, Submenu } from "@tauri-apps/api/menu" import { type as ostype } from "@tauri-apps/plugin-os" -import { invoke } from "@tauri-apps/api/core" import { relaunch } from "@tauri-apps/plugin-process" import { runUpdater, UPDATER_ENABLED } from "./updater" import { installCli } from "./cli" import { initI18n, t } from "./i18n" +import { commands } from "./bindings" export async function createMenu() { if (ostype() !== "macos") return @@ -35,7 +35,7 @@ export async function createMenu() { }), await MenuItem.new({ action: async () => { - await invoke("kill_sidecar").catch(() => undefined) + await commands.killSidecar().catch(() => undefined) await relaunch().catch(() => undefined) }, text: t("desktop.menu.restart"), diff --git a/packages/desktop/src/updater.ts b/packages/desktop/src/updater.ts index b48bb6be02..7326696338 100644 --- a/packages/desktop/src/updater.ts +++ b/packages/desktop/src/updater.ts @@ -1,10 +1,10 @@ import { check } from "@tauri-apps/plugin-updater" import { relaunch } from "@tauri-apps/plugin-process" import { ask, message } from "@tauri-apps/plugin-dialog" -import { invoke } from "@tauri-apps/api/core" import { type as ostype } from "@tauri-apps/plugin-os" import { initI18n, t } from "./i18n" +import { commands } from "./bindings" export const UPDATER_ENABLED = window.__OPENCODE__?.updaterEnabled ?? false @@ -39,13 +39,13 @@ export async function runUpdater({ alertOnFail }: { alertOnFail: boolean }) { if (!shouldUpdate) return try { - if (ostype() === "windows") await invoke("kill_sidecar") + if (ostype() === "windows") await commands.killSidecar() await update.install() } catch { await message(t("desktop.updater.installFailed.message"), { title: t("desktop.updater.installFailed.title") }) return } - await invoke("kill_sidecar") + await commands.killSidecar() await relaunch() } From f890f45df30cb08041776b6998561012b6eec25a Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Mon, 2 Feb 2026 07:58:52 +0000 Subject: [PATCH 09/24] chore: generate --- packages/desktop/src/bindings.ts | 23 +++++++++++------------ packages/desktop/src/index.tsx | 4 +++- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/desktop/src/bindings.ts b/packages/desktop/src/bindings.ts index eb5498fa68..440e138b4f 100644 --- a/packages/desktop/src/bindings.ts +++ b/packages/desktop/src/bindings.ts @@ -1,20 +1,19 @@ // This file has been generated by Tauri Specta. Do not edit this file manually. -import { invoke as __TAURI_INVOKE, Channel } from '@tauri-apps/api/core'; +import { invoke as __TAURI_INVOKE, Channel } from "@tauri-apps/api/core" /** Commands */ export const commands = { - killSidecar: () => __TAURI_INVOKE("kill_sidecar"), - installCli: () => __TAURI_INVOKE("install_cli"), - ensureServerReady: () => __TAURI_INVOKE("ensure_server_ready"), - getDefaultServerUrl: () => __TAURI_INVOKE("get_default_server_url"), - setDefaultServerUrl: (url: string | null) => __TAURI_INVOKE("set_default_server_url", { url }), - parseMarkdownCommand: (markdown: string) => __TAURI_INVOKE("parse_markdown_command", { markdown }), -}; + killSidecar: () => __TAURI_INVOKE("kill_sidecar"), + installCli: () => __TAURI_INVOKE("install_cli"), + ensureServerReady: () => __TAURI_INVOKE("ensure_server_ready"), + getDefaultServerUrl: () => __TAURI_INVOKE("get_default_server_url"), + setDefaultServerUrl: (url: string | null) => __TAURI_INVOKE("set_default_server_url", { url }), + parseMarkdownCommand: (markdown: string) => __TAURI_INVOKE("parse_markdown_command", { markdown }), +} /* Types */ export type ServerReadyData = { - url: string, - password: string | null, - }; - + url: string + password: string | null +} diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index 2b44061626..bb3265db88 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -339,7 +339,9 @@ const createPlatform = (password: Accessor): Platform => ({ return result }, - setDefaultServerUrl: async (url: string | null) => { await commands.setDefaultServerUrl(url) }, + setDefaultServerUrl: async (url: string | null) => { + await commands.setDefaultServerUrl(url) + }, parseMarkdown: (markdown: string) => commands.parseMarkdownCommand(markdown), From 0ccae0cea68faee4318e6f95c5aba0f1b449e3e9 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Mon, 2 Feb 2026 17:54:57 +0800 Subject: [PATCH 10/24] fix(desktop): throttle window state persistence (#11746) --- packages/desktop/src-tauri/src/lib.rs | 45 ++++++++++++++++--- .../src-tauri/src/window_customizer.rs | 4 +- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index 2fe7c4aa17..a177e6ceed 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -23,13 +23,18 @@ use tauri_plugin_deep_link::DeepLinkExt; use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult}; use tauri_plugin_shell::process::{CommandChild, CommandEvent}; use tauri_plugin_store::StoreExt; -use tokio::sync::oneshot; +use tauri_plugin_window_state::{AppHandleExt, StateFlags}; +use tokio::sync::{mpsc, oneshot}; use crate::window_customizer::PinchZoomDisablePlugin; const SETTINGS_STORE: &str = "opencode.settings.dat"; const DEFAULT_SERVER_URL_KEY: &str = "defaultServerUrl"; +fn window_state_flags() -> StateFlags { + StateFlags::all() - StateFlags::DECORATIONS +} + #[derive(Clone, serde::Serialize, specta::Type)] struct ServerReadyData { url: String, @@ -293,10 +298,7 @@ pub fn run() { .plugin(tauri_plugin_os::init()) .plugin( tauri_plugin_window_state::Builder::new() - .with_state_flags( - tauri_plugin_window_state::StateFlags::all() - - tauri_plugin_window_state::StateFlags::DECORATIONS, - ) + .with_state_flags(window_state_flags()) .build(), ) .plugin(tauri_plugin_store::Builder::new().build()) @@ -365,6 +367,8 @@ pub fn run() { let window = window_builder.build().expect("Failed to create window"); + setup_window_state_listener(&app, &window); + #[cfg(windows)] let _ = window.create_overlay_titlebar(); @@ -560,3 +564,34 @@ async fn spawn_local_server( } } } + +fn setup_window_state_listener(app: &tauri::AppHandle, window: &tauri::WebviewWindow) { + let (tx, mut rx) = mpsc::channel::<()>(1); + + window.on_window_event(move |event| { + if !matches!(event, WindowEvent::Moved(_) | WindowEvent::Resized(_)) { + return; + } + let _ = tx.try_send(()); + }); + + tauri::async_runtime::spawn({ + let app = app.clone(); + + async move { + let save = || { + let handle = app.clone(); + let app = app.clone(); + let _ = handle.run_on_main_thread(move || { + let _ = app.save_window_state(window_state_flags()); + }); + }; + + while rx.recv().await.is_some() { + tokio::time::sleep(Duration::from_millis(200)).await; + + save(); + } + } + }); +} diff --git a/packages/desktop/src-tauri/src/window_customizer.rs b/packages/desktop/src-tauri/src/window_customizer.rs index 682f57f247..d73662120a 100644 --- a/packages/desktop/src-tauri/src/window_customizer.rs +++ b/packages/desktop/src-tauri/src/window_customizer.rs @@ -1,4 +1,4 @@ -use tauri::{plugin::Plugin, Manager, Runtime, Window}; +use tauri::{Manager, Runtime, Window, plugin::Plugin}; pub struct PinchZoomDisablePlugin; @@ -21,8 +21,8 @@ impl Plugin for PinchZoomDisablePlugin { let _ = webview_window.with_webview(|_webview| { #[cfg(target_os = "linux")] unsafe { - use gtk::glib::ObjectExt; use gtk::GestureZoom; + use gtk::glib::ObjectExt; use webkit2gtk::glib::gobject_ffi; if let Some(data) = _webview.inner().data::("wk-view-zoom-gesture") { From f148d38b49dd42cd779252cba5f7239792ac31c8 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Mon, 2 Feb 2026 18:19:49 +0800 Subject: [PATCH 11/24] fix(desktop): remove unnecessary setTimeout --- packages/desktop/src/index.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index bb3265db88..9ef680ed86 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -391,11 +391,7 @@ type ServerReadyData = { url: string; password: string | null } // Gate component that waits for the server to be ready function ServerGate(props: { children: (data: Accessor) => JSX.Element }) { - const [serverData] = createResource(() => - commands.ensureServerReady().then((v) => { - return new Promise((res) => setTimeout(() => res(v as ServerReadyData), 2000)) - }), - ) + const [serverData] = createResource(() => commands.ensureServerReady()) const errorMessage = () => { const error = serverData.error From 7a9e1fcb40f6c391459039900b560aebb24621dc Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Mon, 2 Feb 2026 18:52:57 +0800 Subject: [PATCH 12/24] desktop: fix rust build + bindings formatting --- .prettierignore | 3 ++- packages/desktop/src-tauri/src/lib.rs | 1 + packages/desktop/src/bindings.ts | 23 ++++++++++++----------- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/.prettierignore b/.prettierignore index aa3a7ce238..5f86f710fb 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1,2 @@ -sst-env.d.ts \ No newline at end of file +sst-env.d.ts +desktop/src/bindings.ts diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index a177e6ceed..d309b918b6 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -569,6 +569,7 @@ fn setup_window_state_listener(app: &tauri::AppHandle, window: &tauri::WebviewWi let (tx, mut rx) = mpsc::channel::<()>(1); window.on_window_event(move |event| { + use tauri::WindowEvent; if !matches!(event, WindowEvent::Moved(_) | WindowEvent::Resized(_)) { return; } diff --git a/packages/desktop/src/bindings.ts b/packages/desktop/src/bindings.ts index 440e138b4f..eb5498fa68 100644 --- a/packages/desktop/src/bindings.ts +++ b/packages/desktop/src/bindings.ts @@ -1,19 +1,20 @@ // This file has been generated by Tauri Specta. Do not edit this file manually. -import { invoke as __TAURI_INVOKE, Channel } from "@tauri-apps/api/core" +import { invoke as __TAURI_INVOKE, Channel } from '@tauri-apps/api/core'; /** Commands */ export const commands = { - killSidecar: () => __TAURI_INVOKE("kill_sidecar"), - installCli: () => __TAURI_INVOKE("install_cli"), - ensureServerReady: () => __TAURI_INVOKE("ensure_server_ready"), - getDefaultServerUrl: () => __TAURI_INVOKE("get_default_server_url"), - setDefaultServerUrl: (url: string | null) => __TAURI_INVOKE("set_default_server_url", { url }), - parseMarkdownCommand: (markdown: string) => __TAURI_INVOKE("parse_markdown_command", { markdown }), -} + killSidecar: () => __TAURI_INVOKE("kill_sidecar"), + installCli: () => __TAURI_INVOKE("install_cli"), + ensureServerReady: () => __TAURI_INVOKE("ensure_server_ready"), + getDefaultServerUrl: () => __TAURI_INVOKE("get_default_server_url"), + setDefaultServerUrl: (url: string | null) => __TAURI_INVOKE("set_default_server_url", { url }), + parseMarkdownCommand: (markdown: string) => __TAURI_INVOKE("parse_markdown_command", { markdown }), +}; /* Types */ export type ServerReadyData = { - url: string - password: string | null -} + url: string, + password: string | null, + }; + From a4509af4acc4e83327f88949fb398846ef05cf96 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Mon, 2 Feb 2026 10:53:46 +0000 Subject: [PATCH 13/24] chore: generate --- packages/desktop/src/bindings.ts | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/desktop/src/bindings.ts b/packages/desktop/src/bindings.ts index eb5498fa68..440e138b4f 100644 --- a/packages/desktop/src/bindings.ts +++ b/packages/desktop/src/bindings.ts @@ -1,20 +1,19 @@ // This file has been generated by Tauri Specta. Do not edit this file manually. -import { invoke as __TAURI_INVOKE, Channel } from '@tauri-apps/api/core'; +import { invoke as __TAURI_INVOKE, Channel } from "@tauri-apps/api/core" /** Commands */ export const commands = { - killSidecar: () => __TAURI_INVOKE("kill_sidecar"), - installCli: () => __TAURI_INVOKE("install_cli"), - ensureServerReady: () => __TAURI_INVOKE("ensure_server_ready"), - getDefaultServerUrl: () => __TAURI_INVOKE("get_default_server_url"), - setDefaultServerUrl: (url: string | null) => __TAURI_INVOKE("set_default_server_url", { url }), - parseMarkdownCommand: (markdown: string) => __TAURI_INVOKE("parse_markdown_command", { markdown }), -}; + killSidecar: () => __TAURI_INVOKE("kill_sidecar"), + installCli: () => __TAURI_INVOKE("install_cli"), + ensureServerReady: () => __TAURI_INVOKE("ensure_server_ready"), + getDefaultServerUrl: () => __TAURI_INVOKE("get_default_server_url"), + setDefaultServerUrl: (url: string | null) => __TAURI_INVOKE("set_default_server_url", { url }), + parseMarkdownCommand: (markdown: string) => __TAURI_INVOKE("parse_markdown_command", { markdown }), +} /* Types */ export type ServerReadyData = { - url: string, - password: string | null, - }; - + url: string + password: string | null +} From 89109da9c2e8b9ba3e7bd54fbb1d08638100fb4f Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Mon, 2 Feb 2026 19:05:47 +0800 Subject: [PATCH 14/24] feat(app): unread session navigation keybinds (#11750) --- packages/app/src/i18n/ar.ts | 2 ++ packages/app/src/i18n/br.ts | 2 ++ packages/app/src/i18n/da.ts | 2 ++ packages/app/src/i18n/de.ts | 2 ++ packages/app/src/i18n/en.ts | 2 ++ packages/app/src/i18n/es.ts | 2 ++ packages/app/src/i18n/fr.ts | 2 ++ packages/app/src/i18n/ja.ts | 2 ++ packages/app/src/i18n/ko.ts | 2 ++ packages/app/src/i18n/no.ts | 2 ++ packages/app/src/i18n/pl.ts | 2 ++ packages/app/src/i18n/ru.ts | 2 ++ packages/app/src/i18n/th.ts | 2 ++ packages/app/src/i18n/zh.ts | 2 ++ packages/app/src/i18n/zht.ts | 2 ++ packages/app/src/pages/layout.tsx | 60 +++++++++++++++++++++++++++++++ 16 files changed, 90 insertions(+) diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index e3831e23c4..3718303e5a 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -28,6 +28,8 @@ export const dict = { "command.settings.open": "فتح الإعدادات", "command.session.previous": "الجلسة السابقة", "command.session.next": "الجلسة التالية", + "command.session.previous.unseen": "Previous unread session", + "command.session.next.unseen": "Next unread session", "command.session.archive": "أرشفة الجلسة", "command.palette": "لوحة الأوامر", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index f930a66aff..43336f8d6f 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -28,6 +28,8 @@ export const dict = { "command.settings.open": "Abrir configurações", "command.session.previous": "Sessão anterior", "command.session.next": "Próxima sessão", + "command.session.previous.unseen": "Previous unread session", + "command.session.next.unseen": "Next unread session", "command.session.archive": "Arquivar sessão", "command.palette": "Paleta de comandos", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index 2b7d77456d..69e8e8114f 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -28,6 +28,8 @@ export const dict = { "command.settings.open": "Åbn indstillinger", "command.session.previous": "Forrige session", "command.session.next": "Næste session", + "command.session.previous.unseen": "Previous unread session", + "command.session.next.unseen": "Next unread session", "command.session.archive": "Arkivér session", "command.palette": "Kommandopalette", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index 4648ad9c41..1c28e4a16e 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -32,6 +32,8 @@ export const dict = { "command.settings.open": "Einstellungen öffnen", "command.session.previous": "Vorherige Sitzung", "command.session.next": "Nächste Sitzung", + "command.session.previous.unseen": "Previous unread session", + "command.session.next.unseen": "Next unread session", "command.session.archive": "Sitzung archivieren", "command.palette": "Befehlspalette", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 12ddcb4cd8..5589337e56 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -28,6 +28,8 @@ export const dict = { "command.settings.open": "Open settings", "command.session.previous": "Previous session", "command.session.next": "Next session", + "command.session.previous.unseen": "Previous unread session", + "command.session.next.unseen": "Next unread session", "command.session.archive": "Archive session", "command.palette": "Command palette", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index 5d396f0b4f..6e3eac0dd3 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -28,6 +28,8 @@ export const dict = { "command.settings.open": "Abrir ajustes", "command.session.previous": "Sesión anterior", "command.session.next": "Siguiente sesión", + "command.session.previous.unseen": "Previous unread session", + "command.session.next.unseen": "Next unread session", "command.session.archive": "Archivar sesión", "command.palette": "Paleta de comandos", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index 4226d0c7e2..fa3dccd9af 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -28,6 +28,8 @@ export const dict = { "command.settings.open": "Ouvrir les paramètres", "command.session.previous": "Session précédente", "command.session.next": "Session suivante", + "command.session.previous.unseen": "Previous unread session", + "command.session.next.unseen": "Next unread session", "command.session.archive": "Archiver la session", "command.palette": "Palette de commandes", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index 28a925a0d3..4fccbd77e7 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -28,6 +28,8 @@ export const dict = { "command.settings.open": "設定を開く", "command.session.previous": "前のセッション", "command.session.next": "次のセッション", + "command.session.previous.unseen": "Previous unread session", + "command.session.next.unseen": "Next unread session", "command.session.archive": "セッションをアーカイブ", "command.palette": "コマンドパレット", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index 1be4e1eb4b..5b5d29c0e0 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -32,6 +32,8 @@ export const dict = { "command.settings.open": "설정 열기", "command.session.previous": "이전 세션", "command.session.next": "다음 세션", + "command.session.previous.unseen": "Previous unread session", + "command.session.next.unseen": "Next unread session", "command.session.archive": "세션 보관", "command.palette": "명령 팔레트", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index 0a3b398856..89614ce853 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -31,6 +31,8 @@ export const dict = { "command.settings.open": "Åpne innstillinger", "command.session.previous": "Forrige sesjon", "command.session.next": "Neste sesjon", + "command.session.previous.unseen": "Previous unread session", + "command.session.next.unseen": "Next unread session", "command.session.archive": "Arkiver sesjon", "command.palette": "Kommandopalett", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index f4457c6acf..b89921a9bc 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -28,6 +28,8 @@ export const dict = { "command.settings.open": "Otwórz ustawienia", "command.session.previous": "Poprzednia sesja", "command.session.next": "Następna sesja", + "command.session.previous.unseen": "Previous unread session", + "command.session.next.unseen": "Next unread session", "command.session.archive": "Zarchiwizuj sesję", "command.palette": "Paleta poleceń", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index d5a4014d36..e99abbd081 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -28,6 +28,8 @@ export const dict = { "command.settings.open": "Открыть настройки", "command.session.previous": "Предыдущая сессия", "command.session.next": "Следующая сессия", + "command.session.previous.unseen": "Previous unread session", + "command.session.next.unseen": "Next unread session", "command.session.archive": "Архивировать сессию", "command.palette": "Палитра команд", diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index 1914b8e5bd..0da6f9acc7 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -28,6 +28,8 @@ export const dict = { "command.settings.open": "เปิดการตั้งค่า", "command.session.previous": "เซสชันก่อนหน้า", "command.session.next": "เซสชันถัดไป", + "command.session.previous.unseen": "Previous unread session", + "command.session.next.unseen": "Next unread session", "command.session.archive": "จัดเก็บเซสชัน", "command.palette": "คำสั่งค้นหา", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index b9d5395730..a7e1659ec3 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -32,6 +32,8 @@ export const dict = { "command.settings.open": "打开设置", "command.session.previous": "上一个会话", "command.session.next": "下一个会话", + "command.session.previous.unseen": "Previous unread session", + "command.session.next.unseen": "Next unread session", "command.session.archive": "归档会话", "command.palette": "命令面板", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index 23d3d80e13..7b8849b9a0 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -32,6 +32,8 @@ export const dict = { "command.settings.open": "開啟設定", "command.session.previous": "上一個工作階段", "command.session.next": "下一個工作階段", + "command.session.previous.unseen": "Previous unread session", + "command.session.next.unseen": "Next unread session", "command.session.archive": "封存工作階段", "command.palette": "命令面板", diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 845a4fc834..a970bf667e 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -886,6 +886,52 @@ export default function Layout(props: ParentProps) { queueMicrotask(() => scrollToSession(session.id, `${session.directory}:${session.id}`)) } + function navigateSessionByUnseen(offset: number) { + const sessions = currentSessions() + if (sessions.length === 0) return + + const hasUnseen = sessions.some((session) => notification.session.unseen(session.id).length > 0) + if (!hasUnseen) return + + const activeIndex = params.id ? sessions.findIndex((s) => s.id === params.id) : -1 + const start = activeIndex === -1 ? (offset > 0 ? -1 : 0) : activeIndex + + for (let i = 1; i <= sessions.length; i++) { + const index = offset > 0 ? (start + i) % sessions.length : (start - i + sessions.length) % sessions.length + const session = sessions[index] + if (!session) continue + if (notification.session.unseen(session.id).length === 0) continue + + prefetchSession(session, "high") + + const next = sessions[(index + 1) % sessions.length] + const prev = sessions[(index - 1 + sessions.length) % sessions.length] + + if (offset > 0) { + if (next) prefetchSession(next, "high") + if (prev) prefetchSession(prev) + } + + if (offset < 0) { + if (prev) prefetchSession(prev, "high") + if (next) prefetchSession(next) + } + + if (import.meta.env.DEV) { + navStart({ + dir: base64Encode(session.directory), + from: params.id, + to: session.id, + trigger: offset > 0 ? "shift+alt+arrowdown" : "shift+alt+arrowup", + }) + } + + navigateToSession(session) + queueMicrotask(() => scrollToSession(session.id, `${session.directory}:${session.id}`)) + return + } + } + async function archiveSession(session: Session) { const [store, setStore] = globalSync.child(session.directory) const sessions = store.session ?? [] @@ -1024,6 +1070,20 @@ export default function Layout(props: ParentProps) { keybind: "alt+arrowdown", onSelect: () => navigateSessionByOffset(1), }, + { + id: "session.previous.unseen", + title: language.t("command.session.previous.unseen"), + category: language.t("command.category.session"), + keybind: "shift+alt+arrowup", + onSelect: () => navigateSessionByUnseen(-1), + }, + { + id: "session.next.unseen", + title: language.t("command.session.next.unseen"), + category: language.t("command.category.session"), + keybind: "shift+alt+arrowdown", + onSelect: () => navigateSessionByUnseen(1), + }, { id: "session.archive", title: language.t("command.session.archive"), From 512ddf396378f88479750862231ddb17c1c88f79 Mon Sep 17 00:00:00 2001 From: Lucio Delelis Date: Mon, 2 Feb 2026 08:20:30 -0300 Subject: [PATCH 15/24] fix(ui): adjusts alignment of elements to prevent incomplete scroll (#11649) --- packages/ui/src/components/message-nav.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/components/message-nav.css b/packages/ui/src/components/message-nav.css index b1454ad425..79bfdc0b34 100644 --- a/packages/ui/src/components/message-nav.css +++ b/packages/ui/src/components/message-nav.css @@ -103,7 +103,7 @@ display: flex; padding: 4px 4px 6px 4px; justify-content: center; - align-items: center; + align-items: start; border-radius: var(--radius-md); background: var(--surface-raised-stronger-non-alpha); max-height: calc(100vh - 6rem); From 0833265440c326b7d43cb7355a6642e146b62851 Mon Sep 17 00:00:00 2001 From: Sam Huckaby Date: Mon, 2 Feb 2026 06:30:44 -0500 Subject: [PATCH 16/24] Fix(app): the Vesper theme's light mode (#9892) --- packages/ui/src/theme/themes/vesper.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/ui/src/theme/themes/vesper.json b/packages/ui/src/theme/themes/vesper.json index 3c5e44cbd5..040bdc049b 100644 --- a/packages/ui/src/theme/themes/vesper.json +++ b/packages/ui/src/theme/themes/vesper.json @@ -18,8 +18,7 @@ "background-base": "#FFF", "background-weak": "#F8F8F8", "background-strong": "#F0F0F0", - "background-stronger": "#E8E8E8", - "border-weak-base": "#E8E8E8", + "background-stronger": "#FBFBFB", "border-weak-hover": "#E0E0E0", "border-weak-active": "#D8D8D8", "border-weak-selected": "#D0D0D0", @@ -41,14 +40,15 @@ "surface-diff-delete-base": "#f5e8e8", "surface-diff-hidden-base": "#F0F0F0", "text-base": "#101010", - "text-weak": "#A0A0A0", + "text-invert-strong": "var(--smoke-dark-alpha-12)", + "text-weak": "#606060", "text-strong": "#000000", - "syntax-string": "#99FFE4", - "syntax-primitive": "#FF8080", - "syntax-property": "#FFC799", - "syntax-type": "#FFC799", - "syntax-constant": "#A0A0A0", - "syntax-info": "#A0A0A0", + "syntax-string": "#0D5C4F", + "syntax-primitive": "#B30000", + "syntax-property": "#C66C00", + "syntax-type": "#9C5C12", + "syntax-constant": "#404040", + "syntax-info": "#606060", "markdown-heading": "#FFC799", "markdown-text": "#101010", "markdown-link": "#FFC799", From 08a49167448c26dbeb985954a1093ff8b9dea5e8 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 2 Feb 2026 05:42:55 -0600 Subject: [PATCH 17/24] chore: update website stats --- packages/console/app/src/config.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/console/app/src/config.ts b/packages/console/app/src/config.ts index 1d99def1b9..be53ad909b 100644 --- a/packages/console/app/src/config.ts +++ b/packages/console/app/src/config.ts @@ -9,8 +9,8 @@ export const config = { github: { repoUrl: "https://github.com/anomalyco/opencode", starsFormatted: { - compact: "80K", - full: "80,000", + compact: "95K", + full: "95,000", }, }, @@ -22,8 +22,8 @@ export const config = { // Static stats (used on landing page) stats: { - contributors: "600", - commits: "7,500", - monthlyUsers: "1.5M", + contributors: "650", + commits: "8,500", + monthlyUsers: "2.5M", }, } as const From 4cfcc422f4adafa330caf4cf66227c94900829c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20S=C3=BAkup?= Date: Mon, 2 Feb 2026 13:01:49 +0100 Subject: [PATCH 18/24] feat(opencode): ormolu code formatter for haskell (#10274) --- packages/opencode/src/format/formatter.ts | 9 +++++++++ packages/web/src/content/docs/formatters.mdx | 1 + packages/web/src/content/docs/lsp.mdx | 1 + 3 files changed, 11 insertions(+) diff --git a/packages/opencode/src/format/formatter.ts b/packages/opencode/src/format/formatter.ts index 5f0624d6c9..9e97fae9df 100644 --- a/packages/opencode/src/format/formatter.ts +++ b/packages/opencode/src/format/formatter.ts @@ -355,3 +355,12 @@ export const pint: Info = { return false }, } + +export const ormolu: Info = { + name: "ormolu", + command: ["ormolu", "-i", "$FILE"], + extensions: [".hs"], + async enabled() { + return Bun.which("ormolu") !== null + }, +} diff --git a/packages/web/src/content/docs/formatters.mdx b/packages/web/src/content/docs/formatters.mdx index 225875f625..54f36e0cd0 100644 --- a/packages/web/src/content/docs/formatters.mdx +++ b/packages/web/src/content/docs/formatters.mdx @@ -36,6 +36,7 @@ OpenCode comes with several built-in formatters for popular languages and framew | shfmt | .sh, .bash | `shfmt` command available | | pint | .php | `laravel/pint` dependency in `composer.json` | | oxfmt (Experimental) | .js, .jsx, .ts, .tsx | `oxfmt` dependency in `package.json` and an [experimental env variable flag](/docs/cli/#experimental) | +| ormolu | .hs | `ormolu` command available | So if your project has `prettier` in your `package.json`, OpenCode will automatically use it. diff --git a/packages/web/src/content/docs/lsp.mdx b/packages/web/src/content/docs/lsp.mdx index 707af84a01..ac788fc600 100644 --- a/packages/web/src/content/docs/lsp.mdx +++ b/packages/web/src/content/docs/lsp.mdx @@ -25,6 +25,7 @@ OpenCode comes with several built-in LSP servers for popular languages: | fsharp | .fs, .fsi, .fsx, .fsscript | `.NET SDK` installed | | gleam | .gleam | `gleam` command available | | gopls | .go | `go` command available | +| hls | .hs, .lhs | `haskell-language-server-wrapper` command available | jdtls | .java | `Java SDK (version 21+)` installed | | kotlin-ls | .kt, .kts | Auto-installs for Kotlin projects | | lua-ls | .lua | Auto-installs for Lua projects | From 97b3e39d4c6ddcdc77ff08f36555ad4b6dfa0f1a Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Mon, 2 Feb 2026 12:02:35 +0000 Subject: [PATCH 19/24] chore: generate --- packages/web/src/content/docs/lsp.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/src/content/docs/lsp.mdx b/packages/web/src/content/docs/lsp.mdx index ac788fc600..95c306fcc0 100644 --- a/packages/web/src/content/docs/lsp.mdx +++ b/packages/web/src/content/docs/lsp.mdx @@ -25,7 +25,7 @@ OpenCode comes with several built-in LSP servers for popular languages: | fsharp | .fs, .fsi, .fsx, .fsscript | `.NET SDK` installed | | gleam | .gleam | `gleam` command available | | gopls | .go | `go` command available | -| hls | .hs, .lhs | `haskell-language-server-wrapper` command available +| hls | .hs, .lhs | `haskell-language-server-wrapper` command available | | jdtls | .java | `Java SDK (version 21+)` installed | | kotlin-ls | .kt, .kts | Auto-installs for Kotlin projects | | lua-ls | .lua | Auto-installs for Lua projects | From 596563f8c1849bf720229d4e75d92ccb9b00827c Mon Sep 17 00:00:00 2001 From: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Date: Mon, 2 Feb 2026 13:03:55 +0100 Subject: [PATCH 20/24] fix(desktop): added inverted svg for steps expanded for nice UX (#10462) Co-authored-by: opencode Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: opencode-agent[bot] --- packages/ui/src/components/session-turn.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index 48d6337edb..29c5566a65 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -610,7 +610,7 @@ export function SessionTurn( - + + + + + + From a36db14008897cca29e58421052cfb0b77d0ed47 Mon Sep 17 00:00:00 2001 From: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Date: Mon, 2 Feb 2026 13:06:45 +0100 Subject: [PATCH 21/24] feat(app): enhance responsive design with additional breakpoints for larger screen layout adjustments (#10459) Co-authored-by: opencode Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: opencode-agent[bot] --- packages/app/src/pages/session.tsx | 11 +++++++---- packages/ui/src/styles/tailwind/index.css | 3 +++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index d3e74072a8..d03c4186c5 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1940,7 +1940,8 @@ export default function Page() { "sticky top-0 z-30 bg-background-stronger": true, "w-full": true, "px-4 md:px-6": true, - "md:max-w-200 md:mx-auto": centered(), + "md:max-w-200 md:mx-auto 3xl:max-w-[1200px] 3xl:mx-auto 4xl:max-w-[1600px] 4xl:mx-auto 5xl:max-w-[1900px] 5xl:mx-auto": + centered(), }} >
@@ -1968,7 +1969,8 @@ export default function Page() { class="flex flex-col gap-32 items-start justify-start pb-[calc(var(--prompt-height,8rem)+64px)] md:pb-[calc(var(--prompt-height,10rem)+64px)] transition-[margin]" classList={{ "w-full": true, - "md:max-w-200 md:mx-auto": centered(), + "md:max-w-200 md:mx-auto 3xl:max-w-[1200px] 3xl:mx-auto 4xl:max-w-[1600px] 4xl:mx-auto 5xl:max-w-[1900px] 5xl:mx-auto": + centered(), "mt-0.5": centered(), "mt-0": !centered(), }} @@ -2021,7 +2023,8 @@ export default function Page() { data-message-id={message.id} classList={{ "min-w-0 w-full max-w-full": true, - "md:max-w-200": centered(), + "md:max-w-200 3xl:max-w-[1200px] 4xl:max-w-[1600px] 5xl:max-w-[1900px]": + centered(), }} > diff --git a/packages/ui/src/styles/tailwind/index.css b/packages/ui/src/styles/tailwind/index.css index f7ce21ad96..d8b0b2a1a0 100644 --- a/packages/ui/src/styles/tailwind/index.css +++ b/packages/ui/src/styles/tailwind/index.css @@ -17,6 +17,9 @@ --breakpoint-lg: 64rem; --breakpoint-xl: 80rem; --breakpoint-2xl: 96rem; + --breakpoint-3xl: 112rem; + --breakpoint-4xl: 128rem; + --breakpoint-5xl: 144rem; --container-3xs: 16rem; --container-2xs: 18rem; From 1db70ac06a893f87e54cc54ce78c0722ec1f9c03 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Mon, 2 Feb 2026 12:07:56 +0000 Subject: [PATCH 22/24] chore: generate --- packages/app/src/pages/session.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index d03c4186c5..da12be8f56 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1941,7 +1941,7 @@ export default function Page() { "w-full": true, "px-4 md:px-6": true, "md:max-w-200 md:mx-auto 3xl:max-w-[1200px] 3xl:mx-auto 4xl:max-w-[1600px] 4xl:mx-auto 5xl:max-w-[1900px] 5xl:mx-auto": - centered(), + centered(), }} >
@@ -1970,7 +1970,7 @@ export default function Page() { classList={{ "w-full": true, "md:max-w-200 md:mx-auto 3xl:max-w-[1200px] 3xl:mx-auto 4xl:max-w-[1600px] 4xl:mx-auto 5xl:max-w-[1900px] 5xl:mx-auto": - centered(), + centered(), "mt-0.5": centered(), "mt-0": !centered(), }} @@ -2023,8 +2023,7 @@ export default function Page() { data-message-id={message.id} classList={{ "min-w-0 w-full max-w-full": true, - "md:max-w-200 3xl:max-w-[1200px] 4xl:max-w-[1600px] 5xl:max-w-[1900px]": - centered(), + "md:max-w-200 3xl:max-w-[1200px] 4xl:max-w-[1600px] 5xl:max-w-[1900px]": centered(), }} > Date: Mon, 2 Feb 2026 18:20:06 +0530 Subject: [PATCH 23/24] feat(app): add tab close keybind (#11780) --- .../src/components/session/session-sortable-tab.tsx | 12 +++++++++--- packages/app/src/i18n/en.ts | 1 + packages/app/src/pages/session.tsx | 12 ++++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/app/src/components/session/session-sortable-tab.tsx b/packages/app/src/components/session/session-sortable-tab.tsx index 06609fcfb8..516f3c8ede 100644 --- a/packages/app/src/components/session/session-sortable-tab.tsx +++ b/packages/app/src/components/session/session-sortable-tab.tsx @@ -3,11 +3,12 @@ import type { JSX } from "solid-js" import { createSortable } from "@thisbeyond/solid-dnd" import { FileIcon } from "@opencode-ai/ui/file-icon" import { IconButton } from "@opencode-ai/ui/icon-button" -import { Tooltip } from "@opencode-ai/ui/tooltip" +import { TooltipKeybind } from "@opencode-ai/ui/tooltip" import { Tabs } from "@opencode-ai/ui/tabs" import { getFilename } from "@opencode-ai/util/path" import { useFile } from "@/context/file" import { useLanguage } from "@/context/language" +import { useCommand } from "@/context/command" export function FileVisual(props: { path: string; active?: boolean }): JSX.Element { return ( @@ -27,6 +28,7 @@ export function FileVisual(props: { path: string; active?: boolean }): JSX.Eleme export function SortableTab(props: { tab: string; onTabClose: (tab: string) => void }): JSX.Element { const file = useFile() const language = useLanguage() + const command = useCommand() const sortable = createSortable(props.tab) const path = createMemo(() => file.pathFromTab(props.tab)) return ( @@ -36,7 +38,11 @@ export function SortableTab(props: { tab: string; onTabClose: (tab: string) => v + v onClick={() => props.onTabClose(props.tab)} aria-label={language.t("common.closeTab")} /> - + } hideCloseButton onMiddleClick={() => props.onTabClose(props.tab)} diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 5589337e56..169d09cd38 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -45,6 +45,7 @@ export const dict = { "command.session.new": "New session", "command.file.open": "Open file", "command.file.open.description": "Search files and commands", + "command.tab.close": "Close tab", "command.context.addSelection": "Add selection to context", "command.context.addSelection.description": "Add selected lines from the current file", "command.terminal.toggle": "Toggle terminal", diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index da12be8f56..772ad063ba 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -689,6 +689,18 @@ export default function Page() { slash: "open", onSelect: () => dialog.show(() => showAllFiles()} />), }, + { + id: "tab.close", + title: language.t("command.tab.close"), + category: language.t("command.category.file"), + keybind: "mod+w", + disabled: !tabs().active(), + onSelect: () => { + const active = tabs().active() + if (!active) return + tabs().close(active) + }, + }, { id: "context.addSelection", title: language.t("command.context.addSelection"), From 1c34d87a16ea60e7c96eaba834dabc510e9cc6a7 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 2 Feb 2026 10:31:36 -0500 Subject: [PATCH 24/24] ci: upgrade checkout action to v4 and add timeouts --- .github/workflows/publish.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a1b492258b..2fc4796cfb 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -33,9 +33,10 @@ permissions: jobs: version: runs-on: blacksmith-4vcpu-ubuntu-2404 + timeout-minutes: 30 if: github.repository == 'anomalyco/opencode' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 @@ -63,7 +64,7 @@ jobs: runs-on: blacksmith-4vcpu-ubuntu-2404 if: github.repository == 'anomalyco/opencode' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-tags: true @@ -107,7 +108,7 @@ jobs: target: aarch64-unknown-linux-gnu runs-on: ${{ matrix.settings.host }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-tags: true @@ -208,8 +209,9 @@ jobs: - build-cli - build-tauri runs-on: blacksmith-4vcpu-ubuntu-2404 + timeout-minutes: 60 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ./.github/actions/setup-bun