Compare commits

...

14 Commits

Author SHA1 Message Date
Dax
0ea7e75e0b Merge branch 'dev' into refactor/node-server-adapter 2026-03-10 11:40:35 -04:00
Aiden Cline
ad08fd57df chore: rekram1-node is no longer on vacation (#16905) 2026-03-10 10:27:04 -05:00
opencode-agent[bot]
54ba59d3e1 chore: generate 2026-03-10 15:14:46 +00:00
James Long
a4330a225d feat(core): allow passing workspaceID into session create endpoint (#16798) 2026-03-10 11:12:51 -04:00
James Long
69ddc91c35 fix(core): a chunk timeout when processing llm stream (#16366) 2026-03-10 11:12:14 -04:00
James Long
4c4aed5a87 fix(core): make worktrees read the project id from local workspace (#16795) 2026-03-10 11:11:28 -04:00
opencode-agent[bot]
5a40158abf chore: generate 2026-03-10 15:07:35 +00:00
Jérôme Benoit
4dce485854 fix(opencode): add thinking variants support for SAP AI provider (#14958)
Co-authored-by: Test <test@test.com>
Co-authored-by: Stephen Collings <stevoland@gmail.com>
2026-03-10 10:05:45 -05:00
Adam
5ec5d1dace chore(app): debug window 2026-03-10 07:05:54 -05:00
opencode-agent[bot]
d2c765e2b3 chore: generate 2026-03-10 11:01:22 +00:00
bhaktatejas922
d036c57d59 docs: update opencode-morph-plugin in all language ecosystem pages (#16869) 2026-03-10 06:00:13 -05:00
opencode-agent[bot]
e7493e2204 chore: update nix node_modules hashes 2026-03-10 10:16:15 +00:00
Sebastian
3500bf64b8 upgrade opentui to v0.1.87 (#16772) 2026-03-10 11:03:05 +01:00
Dax Raad
2724335b28 refactor(server): replace Bun serve with Hono node adapters 2026-03-09 17:57:00 -04:00
47 changed files with 488 additions and 159 deletions

View File

@@ -5,16 +5,8 @@ import DESCRIPTION from "./github-triage.txt"
const TEAM = {
desktop: ["adamdotdevin", "iamdavidhill", "Brendonovich", "nexxeln"],
zen: ["fwang", "MrMushrooooom"],
tui: [
"thdxr",
"kommander",
// "rekram1-node" (on vacation)
],
core: [
"thdxr",
// "rekram1-node", (on vacation)
"jlongster",
],
tui: ["thdxr", "kommander", "rekram1-node"],
core: ["thdxr", "rekram1-node", "jlongster"],
docs: ["R44VC0RP"],
windows: ["Hona"],
} as const
@@ -50,7 +42,10 @@ async function githubFetch(endpoint: string, options: RequestInit = {}) {
export default tool({
description: DESCRIPTION,
args: {
assignee: tool.schema.enum(ASSIGNEES as [string, ...string[]]).describe("The username of the assignee"),
assignee: tool.schema
.enum(ASSIGNEES as [string, ...string[]])
.describe("The username of the assignee")
.default("rekram1-node"),
labels: tool.schema
.array(tool.schema.enum(["nix", "opentui", "perf", "web", "desktop", "zen", "docs", "windows", "core"]))
.describe("The labels(s) to add to the issue")
@@ -73,8 +68,7 @@ export default tool({
results.push("Dropped label: nix (issue does not mention nix)")
}
// const assignee = nix ? "rekram1-node" : web ? pick(TEAM.desktop) : args.assignee
const assignee = web ? pick(TEAM.desktop) : args.assignee
const assignee = nix ? "rekram1-node" : web ? pick(TEAM.desktop) : args.assignee
if (labels.includes("zen") && !zen) {
throw new Error("Only add the zen label when issue title/body contains 'zen'")

View File

@@ -4,5 +4,3 @@ Choose labels and assignee using the current triage policy and ownership rules.
Pick the most fitting labels for the issue and assign one owner.
If unsure, choose the team/section with the most overlap with the issue and assign a member from that team at random.
(Note: rekram1-node is on vacation, do not assign issues to him.)

View File

@@ -335,8 +335,8 @@
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*",
"@openrouter/ai-sdk-provider": "1.5.4",
"@opentui/core": "0.1.86",
"@opentui/solid": "0.1.86",
"@opentui/core": "0.1.87",
"@opentui/solid": "0.1.87",
"@parcel/watcher": "2.5.1",
"@pierre/diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2",
@@ -1422,21 +1422,21 @@
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
"@opentui/core": ["@opentui/core@0.1.86", "", { "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.86", "@opentui/core-darwin-x64": "0.1.86", "@opentui/core-linux-arm64": "0.1.86", "@opentui/core-linux-x64": "0.1.86", "@opentui/core-win32-arm64": "0.1.86", "@opentui/core-win32-x64": "0.1.86", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-3tRLbI9ADrQE1jEEn4x2aJexEOQZkv9Emk2BixMZqxfVhz2zr2SxtpimDAX0vmZK3+GnWAwBWxuaCAsxZpY4+w=="],
"@opentui/core": ["@opentui/core@0.1.87", "", { "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.87", "@opentui/core-darwin-x64": "0.1.87", "@opentui/core-linux-arm64": "0.1.87", "@opentui/core-linux-x64": "0.1.87", "@opentui/core-win32-arm64": "0.1.87", "@opentui/core-win32-x64": "0.1.87", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-dhsmMv0IqKftwG7J/pBrLBj2armsYIg5R3LBvciRQI/6X89GufP4l1u0+QTACAx6iR4SYJJNVNQ2tdX8LM9rMw=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.86", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Zp7q64+d+Dcx6YrH3mRcnHq8EOBnrfc1RvjgSWLhpXr49hY6LzuhqpfZM57aGErPYlR+ff8QM6e5FUkFnDfyjw=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.87", "", { "os": "darwin", "cpu": "arm64" }, "sha512-G8oq85diOfkU6n0T1CxCle7oDmpKxwhcdhZ9khBMU5IrfLx9ZDuCM3F6MsiRQWdvPPCq2oomNbd64bYkPamYgw=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.86", "", { "os": "darwin", "cpu": "x64" }, "sha512-NcxfjCJm1kLnTMVOpAPdRYNi8W8XdAXNa6N7i9khiVFrl2v5KRQfUjbrSOUYVxFJNc3jKFG6rsn3jEApvn92qA=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.87", "", { "os": "darwin", "cpu": "x64" }, "sha512-MYTFQfOHm6qO7YaY4GHK9u/oJlXY6djaaxl5I+k4p2mk3vvuFIl/AP1ypITwBFjyV5gyp7PRWFp4nGfY9oN8bw=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.86", "", { "os": "linux", "cpu": "arm64" }, "sha512-EDHAvqSOr8CXzbDvo1aE5blJ6wu1aSbR2LqoXtoeXHemr2T2W42D2TdIWewG6K+/BuRbzZnqt9wnYFBksLW6lw=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.87", "", { "os": "linux", "cpu": "arm64" }, "sha512-he8o1h5M6oskRJ7wE+xKJgmWnv5ZwN6gB3M/Z+SeHtOMPa5cZmi3TefTjG54llEgFfx0F9RcqHof7TJ/GNxRkw=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.86", "", { "os": "linux", "cpu": "x64" }, "sha512-VBaBkVdQDxYV4WcKjb+jgyMS5PiVHepvfaoKWpz1Bq+J01xXW4XPcXyPGkgR1+2R93KzaugEnLscTW4mWtLHlQ=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.87", "", { "os": "linux", "cpu": "x64" }, "sha512-aiUwjPlH4yDcB8/6YDKSmMkaoGAAltL0Xo0AzXyAtJXWK5tkCSaYjEVwzJ/rYRkr4Magnad+Mjth4AQUWdR2AA=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.86", "", { "os": "win32", "cpu": "arm64" }, "sha512-xKbT7sEKYKGwUPkoqmLfHjbJU+vwHPDwf/r/mIunL41JXQBB35CSZ3/QgIwpp2kkteu7oE1tdBdg15ogUU4OMg=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.87", "", { "os": "win32", "cpu": "arm64" }, "sha512-cmP0pOyREjWGniHqbDmaMY7U+1AyagrD8VseJbU0cGpNgVpG2/gbrJUGdfdLB0SNb+mzLdx6SOjdxtrElwRCQA=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.86", "", { "os": "win32", "cpu": "x64" }, "sha512-HRfgAUlcu71/MrtgfX4Gj7PsDtfXZiuC506Pkn1OnRN1Xomcu10BVRDweUa0/g8ldU9i9kLjMGGnpw6/NjaBFg=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.87", "", { "os": "win32", "cpu": "x64" }, "sha512-N2GErAAP8iODf2RPp86pilPaVKiD6G4pkpZL5nLGbKsl0bndrVTpSqZcn8+/nQwFZDPD/AsiRTYNOfWOblhzOw=="],
"@opentui/solid": ["@opentui/solid@0.1.86", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.86", "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-pOZC9dlZIH+bpstVVZ2AvYukBnslZTKSl/y5H8FWcMTHGv/BzpGxXBxstL65E/IQASqPFbvFcs7yMRzdLhynmA=="],
"@opentui/solid": ["@opentui/solid@0.1.87", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.87", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-lRT9t30l8+FtgOjjWJcdb2MT6hP8/RKqwGgYwTI7fXrOqdhxxwdP2SM+rH2l3suHeASheiTdlvPAo230iUcsvg=="],
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],

View File

@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-+SMpaj0jeIHjlddAu6QIwojmWFVIiA8/G32hiQMjcOk=",
"aarch64-linux": "sha256-uo63IF6OCMab+O3ngn1sVxqIGJMm04HXuDgIRmXNTNk=",
"aarch64-darwin": "sha256-yB2tWm6AsX6UifnDqe7VldhN5zTQkDoqZ87AGQYjxT4=",
"x86_64-darwin": "sha256-nNhtqMSG4/y+uxjj14Jc5QQ7X6hQli9ni4v56XAvaAU="
"x86_64-linux": "sha256-duBedS4ZTc1as03OM0KB9mKKU21Cywv4o9GHwQZv6Ts=",
"aarch64-linux": "sha256-juvQfuNBqqzeB/TIY9PuUDqgpsdyI54ImowjQLrNhns=",
"aarch64-darwin": "sha256-kKgcuEN1oJqHJc+sGjcZ4INWvbZczSTDJ8VHIWAquD4=",
"x86_64-darwin": "sha256-hXkFWOL4wi9s8HSrChpqtH4PKSNzbzVgU+0GbAxEUT4="
}
}

View File

@@ -49,14 +49,19 @@ const bad = (n: number | undefined, limit: number, low = false) => {
const session = (path: string) => path.includes("/session")
function Cell(props: { bad?: boolean; dim?: boolean; label: string; tip: string; value: string }) {
function Cell(props: { bad?: boolean; dim?: boolean; label: string; tip: string; value: string; wide?: boolean }) {
return (
<Tooltip value={props.tip} placement="left">
<div class="flex w-full flex-col items-center px-0.5 py-1 text-center">
<div class="text-[7px] font-black uppercase tracking-[0.04em] opacity-70 leading-none">{props.label}</div>
<Tooltip value={props.tip} placement="top">
<div
classList={{
"flex min-h-[42px] w-full min-w-0 flex-col items-center justify-center rounded-[8px] bg-white/5 px-0.5 py-1 text-center": true,
"col-span-2": !!props.wide,
}}
>
<div class="text-[10px] leading-none font-black uppercase tracking-[0.04em] opacity-70">{props.label}</div>
<div
classList={{
"text-[9px] font-semibold leading-none tabular-nums": true,
"text-[13px] leading-none font-bold tabular-nums sm:text-[14px]": true,
"text-text-on-critical-base": !!props.bad,
"opacity-70": !!props.dim,
}}
@@ -355,10 +360,13 @@ export function DebugBar() {
return (
<aside
aria-label="Development performance diagnostics"
class="pointer-events-auto h-full min-h-0 w-[36px] shrink-0 overflow-y-auto text-text-on-interactive-base no-scrollbar sm:w-[38px]"
style={{ "background-color": "color-mix(in srgb, var(--icon-interactive-base) 42%, black)" }}
class="pointer-events-auto fixed bottom-3 right-3 z-50 w-[308px] max-w-[calc(100vw-1.5rem)] overflow-hidden rounded-xl border p-0.5 text-text-on-interactive-base shadow-[var(--shadow-lg-border-base)] sm:bottom-4 sm:right-4 sm:w-[324px]"
style={{
"background-color": "color-mix(in srgb, var(--icon-interactive-base) 42%, black)",
"border-color": "color-mix(in srgb, white 14%, transparent)",
}}
>
<div class="flex min-h-full flex-col gap-0.5 py-2 font-mono">
<div class="grid grid-cols-5 gap-px font-mono">
<Cell
label="NAV"
tip="Last completed route transition touching a session page, measured from router start until the first paint after it settles."
@@ -425,6 +433,7 @@ export function DebugBar() {
value={heapv()}
bad={bad(heap(), 0.8)}
dim={state.heap.used === undefined}
wide
/>
</div>
</aside>

View File

@@ -81,6 +81,8 @@
"@gitlab/gitlab-ai-provider": "3.6.0",
"@gitlab/opencode-gitlab-auth": "1.3.3",
"@hono/standard-validator": "0.1.5",
"@hono/node-server": "1.19.11",
"@hono/node-ws": "1.3.0",
"@hono/zod-validator": "catalog:",
"@modelcontextprotocol/sdk": "1.25.2",
"@octokit/graphql": "9.0.2",
@@ -91,8 +93,8 @@
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*",
"@openrouter/ai-sdk-provider": "1.5.4",
"@opentui/core": "0.1.86",
"@opentui/solid": "0.1.86",
"@opentui/core": "0.1.87",
"@opentui/solid": "0.1.87",
"@parcel/watcher": "2.5.1",
"@pierre/diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2",

View File

@@ -23,7 +23,7 @@ export const AcpCommand = cmd({
process.env.OPENCODE_CLIENT = "acp"
await bootstrap(process.cwd(), async () => {
const opts = await resolveNetworkOptions(args)
const server = Server.listen(opts)
const server = await Server.listen(opts)
const sdk = createOpencodeClient({
baseUrl: `http://${server.hostname}:${server.port}`,

View File

@@ -15,7 +15,7 @@ export const ServeCommand = cmd({
console.log("Warning: OPENCODE_SERVER_PASSWORD is not set; server is unsecured.")
}
const opts = await resolveNetworkOptions(args)
const server = Server.listen(opts)
const server = await Server.listen(opts)
console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
await new Promise(() => {})

View File

@@ -402,9 +402,12 @@ function App() {
const current = promptRef.current
// Don't require focus - if there's any text, preserve it
const currentPrompt = current?.current?.input ? current.current : undefined
const workspaceID =
route.data.type === "session" ? sync.session.get(route.data.sessionID)?.workspaceID : undefined
route.navigate({
type: "home",
initialPrompt: currentPrompt,
workspaceID,
})
dialog.clear()
},

View File

@@ -47,7 +47,7 @@ async function openWorkspace(input: {
}
let created: Session | undefined
while (!created) {
const result = await client.session.create({}).catch(() => undefined)
const result = await client.session.create({ workspaceID: input.workspaceID }).catch(() => undefined)
if (!result) {
input.toast.show({
message: "Failed to open workspace",

View File

@@ -37,6 +37,7 @@ import { DialogSkill } from "../dialog-skill"
export type PromptProps = {
sessionID?: string
workspaceID?: string
visible?: boolean
disabled?: boolean
onSubmit?: () => void
@@ -542,7 +543,9 @@ export function Prompt(props: PromptProps) {
let sessionID = props.sessionID
if (sessionID == null) {
const res = await sdk.client.session.create({})
const res = await sdk.client.session.create({
workspaceID: props.workspaceID,
})
if (res.error) {
console.log("Creating a session failed:", res.error)

View File

@@ -5,6 +5,7 @@ import type { PromptInfo } from "../component/prompt/history"
export type HomeRoute = {
type: "home"
initialPrompt?: PromptInfo
workspaceID?: string
}
export type SessionRoute = {

View File

@@ -121,6 +121,7 @@ export function Home() {
promptRef.set(r)
}}
hint={Hint}
workspaceID={route.workspaceID}
/>
</box>
<box height={4} minHeight={0} width="100%" maxWidth={75} alignItems="center" paddingTop={3} flexShrink={1}>

View File

@@ -8,7 +8,6 @@ import { upgrade } from "@/cli/upgrade"
import { Config } from "@/config/config"
import { GlobalBus } from "@/bus/global"
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2"
import type { BunWebSocketData } from "hono/bun"
import { Flag } from "@/flag/flag"
import { setTimeout as sleep } from "node:timers/promises"
@@ -38,7 +37,7 @@ GlobalBus.on("event", (event) => {
Rpc.emit("global.event", event)
})
let server: Bun.Server<BunWebSocketData> | undefined
let server: Awaited<ReturnType<typeof Server.listen>> | undefined
const eventStream = {
abort: undefined as AbortController | undefined,
@@ -120,7 +119,7 @@ export const rpc = {
},
async server(input: { port: number; hostname: string; mdns?: boolean; cors?: string[] }) {
if (server) await server.stop(true)
server = Server.listen(input)
server = await Server.listen(input)
return { url: server.url.toString() }
},
async checkUpgrade(input: { directory: string }) {
@@ -143,7 +142,7 @@ export const rpc = {
Log.Default.info("worker shutting down")
if (eventStream.abort) eventStream.abort.abort()
await Instance.disposeAll()
if (server) server.stop(true)
if (server) await server.stop(true)
},
}

View File

@@ -37,7 +37,7 @@ export const WebCommand = cmd({
UI.println(UI.Style.TEXT_WARNING_BOLD + "! " + "OPENCODE_SERVER_PASSWORD is not set; server is unsecured.")
}
const opts = await resolveNetworkOptions(args)
const server = Server.listen(opts)
const server = await Server.listen(opts)
UI.empty()
UI.println(UI.logo(" "))
UI.empty()

View File

@@ -972,6 +972,14 @@ export namespace Config {
.describe(
"Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
),
chunkTimeout: z
.number()
.int()
.positive()
.optional()
.describe(
"Timeout in milliseconds between streamed SSE chunks for this provider. If no chunk arrives within this window, the request is aborted.",
),
})
.catchall(z.any())
.optional(),

View File

@@ -88,6 +88,12 @@ export namespace Project {
}
}
function readCachedId(dir: string) {
return Filesystem.readText(path.join(dir, "opencode"))
.then((x) => x.trim())
.catch(() => undefined)
}
export async function fromDirectory(directory: string) {
log.info("fromDirectory", { directory })
@@ -101,19 +107,43 @@ export namespace Project {
const gitBinary = which("git")
// cached id calculation
let id = await Filesystem.readText(path.join(dotgit, "opencode"))
.then((x) => x.trim())
.catch(() => undefined)
let id = await readCachedId(dotgit)
if (!gitBinary) {
return {
id: id ?? "global",
worktree: sandbox,
sandbox: sandbox,
sandbox,
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
}
}
const worktree = await git(["rev-parse", "--git-common-dir"], {
cwd: sandbox,
})
.then(async (result) => {
const common = gitpath(sandbox, await result.text())
// Avoid going to parent of sandbox when git-common-dir is empty.
return common === sandbox ? sandbox : path.dirname(common)
})
.catch(() => undefined)
if (!worktree) {
return {
id: id ?? "global",
worktree: sandbox,
sandbox,
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
}
}
// In the case of a git worktree, it can't cache the id
// because `.git` is not a folder, but it always needs the
// same project id as the common dir, so we resolve it now
if (id == null) {
id = await readCachedId(path.join(worktree, ".git"))
}
// generate id from root commit
if (!id) {
const roots = await git(["rev-list", "--max-parents=0", "--all"], {
@@ -132,7 +162,7 @@ export namespace Project {
return {
id: "global",
worktree: sandbox,
sandbox: sandbox,
sandbox,
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
}
}
@@ -147,7 +177,7 @@ export namespace Project {
return {
id: "global",
worktree: sandbox,
sandbox: sandbox,
sandbox,
vcs: "git",
}
}
@@ -161,33 +191,14 @@ export namespace Project {
if (!top) {
return {
id,
sandbox,
worktree: sandbox,
sandbox,
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
}
}
sandbox = top
const worktree = await git(["rev-parse", "--git-common-dir"], {
cwd: sandbox,
})
.then(async (result) => {
const common = gitpath(sandbox, await result.text())
// Avoid going to parent of sandbox when git-common-dir is empty.
return common === sandbox ? sandbox : path.dirname(common)
})
.catch(() => undefined)
if (!worktree) {
return {
id,
sandbox,
worktree: sandbox,
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
}
}
return {
id,
sandbox,

View File

@@ -46,6 +46,8 @@ import { GoogleAuth } from "google-auth-library"
import { ProviderTransform } from "./transform"
import { Installation } from "../installation"
const DEFAULT_CHUNK_TIMEOUT = 120_000
export namespace Provider {
const log = Log.create({ service: "provider" })
@@ -85,6 +87,54 @@ export namespace Provider {
})
}
function wrapSSE(res: Response, ms: number, ctl: AbortController) {
if (typeof ms !== "number" || ms <= 0) return res
if (!res.body) return res
if (!res.headers.get("content-type")?.includes("text/event-stream")) return res
const reader = res.body.getReader()
const body = new ReadableStream<Uint8Array>({
async pull(ctrl) {
const part = await new Promise<Awaited<ReturnType<typeof reader.read>>>((resolve, reject) => {
const id = setTimeout(() => {
const err = new Error("SSE read timed out")
ctl.abort(err)
void reader.cancel(err)
reject(err)
}, ms)
reader.read().then(
(part) => {
clearTimeout(id)
resolve(part)
},
(err) => {
clearTimeout(id)
reject(err)
},
)
})
if (part.done) {
ctrl.close()
return
}
ctrl.enqueue(part.value)
},
async cancel(reason) {
ctl.abort(reason)
await reader.cancel(reason)
},
})
return new Response(body, {
headers: new Headers(res.headers),
status: res.status,
statusText: res.statusText,
})
}
const BUNDLED_PROVIDERS: Record<string, (options: any) => SDK> = {
"@ai-sdk/amazon-bedrock": createAmazonBedrock,
"@ai-sdk/anthropic": createAnthropic,
@@ -1092,21 +1142,23 @@ export namespace Provider {
if (existing) return existing
const customFetch = options["fetch"]
const chunkTimeout = options["chunkTimeout"] || DEFAULT_CHUNK_TIMEOUT
delete options["chunkTimeout"]
options["fetch"] = async (input: any, init?: BunFetchRequestInit) => {
// Preserve custom fetch if it exists, wrap it with timeout logic
const fetchFn = customFetch ?? fetch
const opts = init ?? {}
const chunkAbortCtl = typeof chunkTimeout === "number" && chunkTimeout > 0 ? new AbortController() : undefined
const signals: AbortSignal[] = []
if (options["timeout"] !== undefined && options["timeout"] !== null) {
const signals: AbortSignal[] = []
if (opts.signal) signals.push(opts.signal)
if (options["timeout"] !== false) signals.push(AbortSignal.timeout(options["timeout"]))
if (opts.signal) signals.push(opts.signal)
if (chunkAbortCtl) signals.push(chunkAbortCtl.signal)
if (options["timeout"] !== undefined && options["timeout"] !== null && options["timeout"] !== false)
signals.push(AbortSignal.timeout(options["timeout"]))
const combined = signals.length > 1 ? AbortSignal.any(signals) : signals[0]
opts.signal = combined
}
const combined = signals.length === 0 ? null : signals.length === 1 ? signals[0] : AbortSignal.any(signals)
if (combined) opts.signal = combined
// Strip openai itemId metadata following what codex does
// Codex uses #[serde(skip_serializing)] on id fields for all item types:
@@ -1126,11 +1178,14 @@ export namespace Provider {
}
}
return fetchFn(input, {
const res = await fetchFn(input, {
...opts,
// @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682
timeout: false,
})
if (!chunkAbortCtl) return res
return wrapSSE(res, chunkTimeout, chunkAbortCtl)
}
const bundledFn = BUNDLED_PROVIDERS[model.api.npm]

View File

@@ -657,9 +657,21 @@ export namespace ProviderTransform {
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/perplexity
return {}
case "@mymediset/sap-ai-provider":
case "@jerome-benoit/sap-ai-provider-v2":
if (model.api.id.includes("anthropic")) {
if (isAnthropicAdaptive) {
return Object.fromEntries(
adaptiveEfforts.map((effort) => [
effort,
{
thinking: {
type: "adaptive",
},
effort,
},
]),
)
}
return {
high: {
thinking: {
@@ -675,7 +687,26 @@ export namespace ProviderTransform {
},
}
}
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
if (model.api.id.includes("gemini") && id.includes("2.5")) {
return {
high: {
thinkingConfig: {
includeThoughts: true,
thinkingBudget: 16000,
},
},
max: {
thinkingConfig: {
includeThoughts: true,
thinkingBudget: 24576,
},
},
}
}
if (model.api.id.includes("gpt") || /\bo[1-9]/.test(model.api.id)) {
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
}
return {}
}
return {}
}

View File

@@ -23,6 +23,8 @@ export namespace Pty {
close: (code?: number, reason?: string) => void
}
const key = (ws: Socket) => (ws.data && typeof ws.data === "object" ? ws.data : ws)
// WebSocket control frame: 0x00 + UTF-8 JSON.
const meta = (cursor: number) => {
const json = JSON.stringify({ cursor })
@@ -97,9 +99,9 @@ export namespace Pty {
try {
session.process.kill()
} catch {}
for (const [key, ws] of session.subscribers.entries()) {
for (const [id, ws] of session.subscribers.entries()) {
try {
if (ws.data === key) ws.close()
if (key(ws) === id) ws.close()
} catch {
// ignore
}
@@ -170,21 +172,21 @@ export namespace Pty {
ptyProcess.onData((chunk) => {
session.cursor += chunk.length
for (const [key, ws] of session.subscribers.entries()) {
for (const [id, ws] of session.subscribers.entries()) {
if (ws.readyState !== 1) {
session.subscribers.delete(key)
session.subscribers.delete(id)
continue
}
if (ws.data !== key) {
session.subscribers.delete(key)
if (key(ws) !== id) {
session.subscribers.delete(id)
continue
}
try {
ws.send(chunk)
} catch {
session.subscribers.delete(key)
session.subscribers.delete(id)
}
}
@@ -226,9 +228,9 @@ export namespace Pty {
try {
session.process.kill()
} catch {}
for (const [key, ws] of session.subscribers.entries()) {
for (const [id, ws] of session.subscribers.entries()) {
try {
if (ws.data === key) ws.close()
if (key(ws) === id) ws.close()
} catch {
// ignore
}
@@ -259,16 +261,13 @@ export namespace Pty {
}
log.info("client connected to session", { id })
// Use ws.data as the unique key for this connection lifecycle.
// If ws.data is undefined, fallback to ws object.
const connectionKey = ws.data && typeof ws.data === "object" ? ws.data : ws
const sub = key(ws)
// Optionally cleanup if the key somehow exists
session.subscribers.delete(connectionKey)
session.subscribers.set(connectionKey, ws)
session.subscribers.delete(sub)
session.subscribers.set(sub, ws)
const cleanup = () => {
session.subscribers.delete(connectionKey)
session.subscribers.delete(sub)
}
const start = session.bufferCursor

View File

@@ -1,14 +1,13 @@
import { Hono } from "hono"
import { describeRoute, validator, resolver } from "hono-openapi"
import { upgradeWebSocket } from "hono/bun"
import type { UpgradeWebSocket } from "hono/ws"
import z from "zod"
import { Pty } from "@/pty"
import { NotFoundError } from "../../storage/db"
import { errors } from "../error"
import { lazy } from "../../util/lazy"
export const PtyRoutes = lazy(() =>
new Hono()
export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
return new Hono()
.get(
"/",
describeRoute({
@@ -196,5 +195,5 @@ export const PtyRoutes = lazy(() =>
},
}
}),
),
)
)
}

View File

@@ -34,7 +34,8 @@ import { ProviderRoutes } from "./routes/provider"
import { InstanceBootstrap } from "../project/bootstrap"
import { NotFoundError } from "../storage/db"
import type { ContentfulStatusCode } from "hono/utils/http-status"
import { websocket } from "hono/bun"
import { createAdaptorServer, type ServerType } from "@hono/node-server"
import { createNodeWebSocket } from "@hono/node-ws"
import { HTTPException } from "hono/http-exception"
import { errors } from "./error"
import { Filesystem } from "@/util/filesystem"
@@ -48,13 +49,20 @@ import { lazy } from "@/util/lazy"
globalThis.AI_SDK_LOG_WARNINGS = false
export namespace Server {
const log = Log.create({ service: "server" })
export type Listener = {
hostname: string
port: number
url: URL
stop: (close?: boolean) => Promise<void>
}
export const Default = lazy(() => createApp({}))
export const Default = lazy(() => create({}).app)
export const createApp = (opts: { cors?: string[] }): Hono => {
function create(opts: { cors?: string[] }) {
const log = Log.create({ service: "server" })
const app = new Hono()
return app
const ws = createNodeWebSocket({ app })
const route = app
.onError((err, c) => {
log.error("failed", {
error: err,
@@ -239,7 +247,6 @@ export namespace Server {
),
)
.route("/project", ProjectRoutes())
.route("/pty", PtyRoutes())
.route("/config", ConfigRoutes())
.route("/experimental", ExperimentalRoutes())
.route("/session", SessionRoutes())
@@ -552,6 +559,7 @@ export namespace Server {
})
},
)
.route("/pty", PtyRoutes(ws.upgradeWebSocket))
.all("/*", async (c) => {
const path = c.req.path
@@ -568,6 +576,11 @@ export namespace Server {
)
return response
})
return {
app: route as Hono,
ws,
}
}
export async function openapi() {
@@ -585,48 +598,86 @@ export namespace Server {
return result
}
export function listen(opts: {
export async function listen(opts: {
port: number
hostname: string
mdns?: boolean
mdnsDomain?: string
cors?: string[]
}) {
const app = createApp(opts)
const args = {
hostname: opts.hostname,
idleTimeout: 0,
fetch: app.fetch,
websocket: websocket,
} as const
const tryServe = (port: number) => {
try {
return Bun.serve({ ...args, port })
} catch {
return undefined
}
}): Promise<Listener> {
const log = Log.create({ service: "server" })
const built = create({
...opts,
})
const start = (port: number) =>
new Promise<ServerType>((resolve, reject) => {
const server = createAdaptorServer({ fetch: built.app.fetch })
built.ws.injectWebSocket(server)
const fail = (err: Error) => {
cleanup()
reject(err)
}
const ready = () => {
cleanup()
resolve(server)
}
const cleanup = () => {
server.off("error", fail)
server.off("listening", ready)
}
server.once("error", fail)
server.once("listening", ready)
server.listen(port, opts.hostname)
})
const server = opts.port === 0 ? await start(4096).catch(() => start(0)) : await start(opts.port)
const addr = server.address()
if (!addr || typeof addr === "string") {
throw new Error(`Failed to resolve server address for port ${opts.port}`)
}
const server = opts.port === 0 ? (tryServe(4096) ?? tryServe(0)) : tryServe(opts.port)
if (!server) throw new Error(`Failed to start server on port ${opts.port}`)
const url = new URL("http://localhost")
url.hostname = opts.hostname
url.port = String(addr.port)
const shouldPublishMDNS =
opts.mdns &&
server.port &&
addr.port &&
opts.hostname !== "127.0.0.1" &&
opts.hostname !== "localhost" &&
opts.hostname !== "::1"
if (shouldPublishMDNS) {
MDNS.publish(server.port!, opts.mdnsDomain)
MDNS.publish(addr.port, opts.mdnsDomain)
} else if (opts.mdns) {
log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish")
}
const originalStop = server.stop.bind(server)
server.stop = async (closeActiveConnections?: boolean) => {
if (shouldPublishMDNS) MDNS.unpublish()
return originalStop(closeActiveConnections)
let closing: Promise<void> | undefined
return {
hostname: opts.hostname,
port: addr.port,
url,
stop(close?: boolean) {
closing ??= new Promise((resolve, reject) => {
if (shouldPublishMDNS) MDNS.unpublish()
server.close((err) => {
if (err) {
reject(err)
return
}
resolve()
})
if (close) {
if ("closeAllConnections" in server && typeof server.closeAllConnections === "function") {
server.closeAllConnections()
}
if ("closeIdleConnections" in server && typeof server.closeIdleConnections === "function") {
server.closeIdleConnections()
}
}
})
return closing
},
}
return server
}
}

View File

@@ -219,6 +219,7 @@ export namespace Session {
parentID: Identifier.schema("session").optional(),
title: z.string().optional(),
permission: Info.shape.permission,
workspaceID: Identifier.schema("workspace").optional(),
})
.optional(),
async (input) => {
@@ -227,6 +228,7 @@ export namespace Session {
directory: Instance.directory,
title: input?.title,
permission: input?.permission,
workspaceID: input?.workspaceID,
})
},
)
@@ -242,6 +244,7 @@ export namespace Session {
const title = getForkedTitle(original.title)
const session = await createNext({
directory: Instance.directory,
workspaceID: original.workspaceID,
title,
})
const msgs = await messages({ sessionID: input.sessionID })
@@ -292,6 +295,7 @@ export namespace Session {
id?: string
title?: string
parentID?: string
workspaceID?: string
directory: string
permission?: PermissionNext.Ruleset
}) {
@@ -301,7 +305,7 @@ export namespace Session {
version: Installation.VERSION,
projectID: Instance.project.id,
directory: input.directory,
workspaceID: WorkspaceContext.workspaceID,
workspaceID: input.workspaceID,
parentID: input.parentID,
title: input.title ?? createDefaultTitle(!!input.parentID),
permission: input.permission,

View File

@@ -413,7 +413,7 @@ export namespace Worktree {
await runStartScripts(info.directory, { projectID, extra })
}
void start().catch((error) => {
return start().catch((error) => {
log.error("worktree start task failed", { directory: info.directory, error })
})
}

View File

@@ -260,6 +260,7 @@ test("env variable takes precedence, config merges options", async () => {
anthropic: {
options: {
timeout: 60000,
chunkTimeout: 15000,
},
},
},
@@ -277,6 +278,7 @@ test("env variable takes precedence, config merges options", async () => {
expect(providers["anthropic"]).toBeDefined()
// Config options should be merged
expect(providers["anthropic"].options.timeout).toBe(60000)
expect(providers["anthropic"].options.chunkTimeout).toBe(15000)
},
})
})

View File

@@ -2479,4 +2479,144 @@ describe("ProviderTransform.variants", () => {
expect(result).toEqual({})
})
})
describe("@jerome-benoit/sap-ai-provider-v2", () => {
test("anthropic models return thinking variants", () => {
const model = createMockModel({
id: "sap-ai-core/anthropic--claude-sonnet-4",
providerID: "sap-ai-core",
api: {
id: "anthropic--claude-sonnet-4",
url: "https://api.ai.sap",
npm: "@jerome-benoit/sap-ai-provider-v2",
},
})
const result = ProviderTransform.variants(model)
expect(Object.keys(result)).toEqual(["high", "max"])
expect(result.high).toEqual({
thinking: {
type: "enabled",
budgetTokens: 16000,
},
})
expect(result.max).toEqual({
thinking: {
type: "enabled",
budgetTokens: 31999,
},
})
})
test("anthropic 4.6 models return adaptive thinking variants", () => {
const model = createMockModel({
id: "sap-ai-core/anthropic--claude-sonnet-4-6",
providerID: "sap-ai-core",
api: {
id: "anthropic--claude-sonnet-4-6",
url: "https://api.ai.sap",
npm: "@jerome-benoit/sap-ai-provider-v2",
},
})
const result = ProviderTransform.variants(model)
expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"])
expect(result.low).toEqual({
thinking: {
type: "adaptive",
},
effort: "low",
})
expect(result.max).toEqual({
thinking: {
type: "adaptive",
},
effort: "max",
})
})
test("gemini 2.5 models return thinkingConfig variants", () => {
const model = createMockModel({
id: "sap-ai-core/gcp--gemini-2.5-pro",
providerID: "sap-ai-core",
api: {
id: "gcp--gemini-2.5-pro",
url: "https://api.ai.sap",
npm: "@jerome-benoit/sap-ai-provider-v2",
},
})
const result = ProviderTransform.variants(model)
expect(Object.keys(result)).toEqual(["high", "max"])
expect(result.high).toEqual({
thinkingConfig: {
includeThoughts: true,
thinkingBudget: 16000,
},
})
expect(result.max).toEqual({
thinkingConfig: {
includeThoughts: true,
thinkingBudget: 24576,
},
})
})
test("gpt models return reasoningEffort variants", () => {
const model = createMockModel({
id: "sap-ai-core/azure-openai--gpt-4o",
providerID: "sap-ai-core",
api: {
id: "azure-openai--gpt-4o",
url: "https://api.ai.sap",
npm: "@jerome-benoit/sap-ai-provider-v2",
},
})
const result = ProviderTransform.variants(model)
expect(Object.keys(result)).toEqual(["low", "medium", "high"])
expect(result.low).toEqual({ reasoningEffort: "low" })
expect(result.high).toEqual({ reasoningEffort: "high" })
})
test("o-series models return reasoningEffort variants", () => {
const model = createMockModel({
id: "sap-ai-core/azure-openai--o3-mini",
providerID: "sap-ai-core",
api: {
id: "azure-openai--o3-mini",
url: "https://api.ai.sap",
npm: "@jerome-benoit/sap-ai-provider-v2",
},
})
const result = ProviderTransform.variants(model)
expect(Object.keys(result)).toEqual(["low", "medium", "high"])
expect(result.low).toEqual({ reasoningEffort: "low" })
expect(result.high).toEqual({ reasoningEffort: "high" })
})
test("sonar models return empty object", () => {
const model = createMockModel({
id: "sap-ai-core/perplexity--sonar-pro",
providerID: "sap-ai-core",
api: {
id: "perplexity--sonar-pro",
url: "https://api.ai.sap",
npm: "@jerome-benoit/sap-ai-provider-v2",
},
})
const result = ProviderTransform.variants(model)
expect(result).toEqual({})
})
test("mistral models return empty object", () => {
const model = createMockModel({
id: "sap-ai-core/mistral--mistral-large",
providerID: "sap-ai-core",
api: {
id: "mistral--mistral-large",
url: "https://api.ai.sap",
npm: "@jerome-benoit/sap-ai-provider-v2",
},
})
const result = ProviderTransform.variants(model)
expect(result).toEqual({})
})
})
})

View File

@@ -1295,6 +1295,7 @@ export class Session2 extends HeyApiClient {
parentID?: string
title?: string
permission?: PermissionRuleset
workspaceID?: string
},
options?: Options<never, ThrowOnError>,
) {
@@ -1308,6 +1309,7 @@ export class Session2 extends HeyApiClient {
{ in: "body", key: "parentID" },
{ in: "body", key: "title" },
{ in: "body", key: "permission" },
{ in: "body", key: "workspaceID" },
],
},
],

View File

@@ -1225,7 +1225,11 @@ export type ProviderConfig = {
* Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.
*/
timeout?: number | false
[key: string]: unknown | string | boolean | number | false | undefined
/**
* Timeout in milliseconds between streamed SSE chunks for this provider. If no chunk arrives within this window, the request is aborted.
*/
chunkTimeout?: number
[key: string]: unknown | string | boolean | number | false | number | undefined
}
}
@@ -2764,6 +2768,7 @@ export type SessionCreateData = {
parentID?: string
title?: string
permission?: PermissionRuleset
workspaceID?: string
}
path?: never
query?: {

View File

@@ -1832,6 +1832,10 @@
},
"permission": {
"$ref": "#/components/schemas/PermissionRuleset"
},
"workspaceID": {
"type": "string",
"pattern": "^wrk.*"
}
}
}
@@ -10108,6 +10112,12 @@
"const": false
}
]
},
"chunkTimeout": {
"description": "Timeout in milliseconds between streamed SSE chunks for this provider. If no chunk arrives within this window, the request is aborted.",
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
}
},
"additionalProperties": {}

View File

@@ -32,7 +32,7 @@ description: مشاريع وتكاملات مبنية باستخدام OpenCode.
| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | تعليمات لأوامر shell غير التفاعلية - تمنع التعليق الناتج عن العمليات المعتمدة على TTY |
| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | تتبع استخدام OpenCode باستخدام Wakatime |
| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | تنظيف جداول markdown التي تنتجها LLMs |
| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | تحرير الكود أسرع بـ 10 مرات باستخدام Morph Fast Apply API وعلامات التحرير الكسولة |
| [opencode-morph-plugin](https://github.com/morphllm/opencode-morph-plugin) | تحرير Fast Apply وبحث WarpGrep في قاعدة الكود وضغط السياق عبر Morph |
| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | وكلاء الخلفية، وأدوات LSP/AST/MCP المعدة مسبقًا، ووكلاء مختارون، متوافق مع Claude Code |
| [opencode-notificator](https://github.com/panta82/opencode-notificator) | إشعارات سطح المكتب وتنبيهات صوتية لجلسات OpenCode |
| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | إشعارات سطح المكتب وتنبيهات صوتية لأحداث الإذن والاكتمال والخطأ |

View File

@@ -32,7 +32,7 @@ Također možete pogledati [awesome-opencode](https://github.com/awesome-opencod
| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Upute za neinteraktivne naredbe ljuske - sprječava visi od TTY ovisnih operacija |
| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Pratite upotrebu OpenCode sa Wakatime |
| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Očistite tabele umanjenja vrijednosti koje su izradili LLM |
| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 10x brže uređivanje koda s Morph Fast Apply API-jem i markerima za lijeno uređivanje |
| [opencode-morph-plugin](https://github.com/morphllm/opencode-morph-plugin) | Fast Apply uređivanje, WarpGrep pretraga koda i kompresija konteksta putem Morph-a |
| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Pozadinski agenti, unapred izgrađeni LSP/AST/MCP alati, kurirani agenti, kompatibilni sa Claude Code |
| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Obavještenja na radnoj površini i zvučna upozorenja za OpenCode sesije |
| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Obavještenja na radnoj površini i zvučna upozorenja za dozvole, završetak i događaje greške |

View File

@@ -244,7 +244,7 @@ You can configure the providers and models you want to use in your OpenCode conf
The `small_model` option configures a separate model for lightweight tasks like title generation. By default, OpenCode tries to use a cheaper model if one is available from your provider, otherwise it falls back to your main model.
Provider options can include `timeout` and `setCacheKey`:
Provider options can include `timeout`, `chunkTimeout`, and `setCacheKey`:
```json title="opencode.json"
{
@@ -253,6 +253,7 @@ Provider options can include `timeout` and `setCacheKey`:
"anthropic": {
"options": {
"timeout": 600000,
"chunkTimeout": 30000,
"setCacheKey": true
}
}
@@ -261,6 +262,7 @@ Provider options can include `timeout` and `setCacheKey`:
```
- `timeout` - Request timeout in milliseconds (default: 300000). Set to `false` to disable.
- `chunkTimeout` - Timeout in milliseconds between streamed response chunks. If no chunk arrives in time, the request is aborted.
- `setCacheKey` - Ensure a cache key is always set for designated provider.
You can also configure [local models](/docs/models#local). [Learn more](/docs/models).

View File

@@ -32,7 +32,7 @@ Du kan også tjekke [awesome-opencode](https://github.com/awesome-opencode/aweso
| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Instruktioner til ikke-interaktive shell-kommandoer - forhindrer hænger fra TTY-afhængige operationer |
| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Spor OpenCode brug med Wakatime |
| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Ryd op afmærkningstabeller produceret af LLMs |
| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 10x hurtigere koderedigering med Morph Fast Apply API og dovne redigeringsmarkører |
| [opencode-morph-plugin](https://github.com/morphllm/opencode-morph-plugin) | Fast Apply-redigering, WarpGrep-kodesøgning og kontekstkomprimering via Morph |
| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Baggrundsagenter, præbyggede LSP/AST/MCP værktøjer, kuraterede agenter, Claude Kodekompatibel |
| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Skrivebordsmeddelelser og lydadvarsler for OpenCode-sessioner |
| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Skrivebordsmeddelelser og lydadvarsler for tilladelser, fuldførelse og fejlhændelser |

View File

@@ -32,7 +32,7 @@ Sie können sich auch [awesome-opencode](https://github.com/awesome-opencode/awe
| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Anweisungen für nicht interaktive Shell-Befehle verhindert Abstürze bei TTY-abhängigen Vorgängen |
| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Verfolgen Sie die Nutzung von OpenCode mit Wakatime |
| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Von LLMs erstellte Abschriftentabellen bereinigen |
| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 10x schnellere Codebearbeitung mit Morph Fast Apply API und Lazy-Edit-Markern |
| [opencode-morph-plugin](https://github.com/morphllm/opencode-morph-plugin) | Fast Apply-Bearbeitung, WarpGrep-Codesuche und Kontextkomprimierung über Morph |
| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Hintergrundagenten, vorgefertigte LSP/AST/MCP-Tools, kuratierte Agenten, Claude Code-kompatibel |
| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Desktop-Benachrichtigungen und akustische Warnungen für OpenCode-Sitzungen |
| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Desktop-Benachrichtigungen und akustische Warnungen für Berechtigungs-, Abschluss- und Fehlerereignisse |

View File

@@ -32,7 +32,7 @@ También puedes consultar [awesome-opencode](https://github.com/awesome-opencode
| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Instrucciones para comandos de shell no interactivos: evita bloqueos de operaciones dependientes de TTY |
| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Seguimiento del uso de OpenCode con Wakatime |
| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Limpiar tablas de Markdown producidas por LLMs |
| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | Edición de código 10 veces más rápida con Morph Fast Apply API y marcadores de edición diferidos |
| [opencode-morph-plugin](https://github.com/morphllm/opencode-morph-plugin) | Edición Fast Apply, búsqueda de código con WarpGrep y compactación de contexto a través de Morph |
| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Agentes en segundo plano, herramientas LSP/AST/MCP prediseñadas, agentes seleccionados, compatible con Claude Code |
| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Notificaciones de escritorio y alertas sonoras para sesiones OpenCode |
| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Notificaciones de escritorio y alertas sonoras para eventos de permiso, finalización y error |

View File

@@ -32,7 +32,7 @@ Vous pouvez également consulter [awesome-opencode](https://github.com/awesome-o
| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Instructions pour les commandes shell non interactives - empêche les blocages dus aux opérations dépendantes du TTY |
| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Suit l'utilisation d'OpenCode avec Wakatime |
| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Nettoie les tableaux Markdown produits par les LLM |
| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | Édition de code 10x plus rapide avec l'API Morph Fast Apply et des marqueurs d'édition différée |
| [opencode-morph-plugin](https://github.com/morphllm/opencode-morph-plugin) | Édition Fast Apply, recherche de code WarpGrep et compaction de contexte via Morph |
| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Agents d'arrière-plan, outils LSP/AST/MCP pré-construits, agents sélectionnés, compatible Claude Code |
| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Notifications de bureau et alertes sonores pour les sessions OpenCode |
| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Notifications de bureau et alertes sonores pour les événements de permission, d'achèvement et d'erreur |

View File

@@ -32,7 +32,7 @@ Puoi anche dare un'occhiata a [awesome-opencode](https://github.com/awesome-open
| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Istruzioni per comandi shell non interattivi: evita blocchi dovuti a operazioni dipendenti da TTY |
| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Traccia l'uso di OpenCode con Wakatime |
| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Ripulisce le tabelle markdown prodotte dai LLM |
| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | Editing del codice 10x più veloce con Morph Fast Apply API e marker lazy edit |
| [opencode-morph-plugin](https://github.com/morphllm/opencode-morph-plugin) | Editing Fast Apply, ricerca codebase WarpGrep e compattazione del contesto tramite Morph |
| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Agenti in background, tool LSP/AST/MCP predefiniti, agenti curati, compatibile con Claude Code |
| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Notifiche desktop e avvisi sonori per le sessioni OpenCode |
| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Notifiche desktop e avvisi sonori per eventi di permesso, completamento ed errore |

View File

@@ -32,7 +32,7 @@ OpenCode 関連プロジェクトをこのリストに追加したいですか?
| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | 非対話型シェルコマンドの手順 - TTY に依存する操作によるハングの防止 |
| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | wakatime で OpenCode の使用状況を追跡する |
| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | LLM によって生成された Markdown テーブルをクリーンアップする |
| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | Morph Fast apply API と遅延編集マーカーにより 10 倍高速なコード編集 |
| [opencode-morph-plugin](https://github.com/morphllm/opencode-morph-plugin) | Morph による Fast Apply 編集、WarpGrep コードベース検索、コンテキスト圧縮 |
| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | バックグラウンドエージェント、事前構築された LSP/AST/MCP ツール、厳選されたエージェント、Claude Code 互換 |
| [opencode-notificator](https://github.com/panta82/opencode-notificator) | OpenCode セッションのデスクトップ通知とサウンドアラート |
| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | 許可、完了、エラーイベントのデスクトップ通知とサウンドアラート |

View File

@@ -32,7 +32,7 @@ OpenCode를 기반으로 만들어진 커뮤니티 프로젝트 모음입니다.
| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | 비대화형 shell 명령 실행 지침을 제공해 TTY 의존 작업으로 인한 멈춤을 방지합니다. |
| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Wakatime으로 OpenCode 사용량을 추적합니다. |
| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | LLM이 생성한 markdown 표를 정리합니다. |
| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | Morph Fast Apply API와 lazy edit marker를 활용해 코드 편집 속도를 크게 높입니다. |
| [opencode-morph-plugin](https://github.com/morphllm/opencode-morph-plugin) | Morph를 통한 Fast Apply 편집, WarpGrep 코드베이스 검색 및 컨텍스트 압축 |
| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | background agent, 사전 구성된 LSP/AST/MCP tool, curated agent, Claude Code 호환성을 제공합니다. |
| [opencode-notificator](https://github.com/panta82/opencode-notificator) | OpenCode 세션에 데스크톱 알림과 사운드 알림을 제공합니다. |
| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | permission, 완료, 오류 이벤트에 대한 데스크톱 알림과 사운드 알림을 제공합니다. |

View File

@@ -32,7 +32,7 @@ Du kan også sjekke ut [awesome-opencode](https://github.com/awesome-opencode/aw
| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Instruksjoner for ikke-interaktive skallkommandoer - forhindrer heng ved TTY-avhengige operasjoner |
| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Spor OpenCode-bruk med Wakatime |
| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Rydd opp i markdown-tabeller produsert av LLMs |
| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 10 ganger raskere koderedigering med Morph Fast Apply API og lazy-redigeringsmarkører |
| [opencode-morph-plugin](https://github.com/morphllm/opencode-morph-plugin) | Fast Apply-redigering, WarpGrep-kodesøk og kontekstkomprimering via Morph |
| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Bakgrunnsagenter, forhåndsbygde LSP/AST/MCP verktøy, kurerte agenter, Claude Code-kompatibel |
| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Skrivebordsvarsler og lydvarsler for OpenCode-økter |
| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Skrivebordsvarsler og lydvarsler for tillatelse, fullføring og feilhendelser |

View File

@@ -32,7 +32,7 @@ Możesz również sprawdzić [awesome-opencode](https://github.com/awesome-openc
| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Instrukcje dla nieinteraktywnych poleceń powłoki - zapobiega zawieszeniom operacji zależnych od TTY |
| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Śledź użycie OpenCode za pomocą Wakatime |
| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Oczyść tabele markdown generowane przez LLM |
| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 10x szybsza edycja kodu dzięki API Morph Fast Apply i leniwym znacznikom edycji |
| [opencode-morph-plugin](https://github.com/morphllm/opencode-morph-plugin) | Edycja Fast Apply, wyszukiwanie kodu WarpGrep i kompaktowanie kontekstu przez Morph |
| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Agenci w tle, wbudowane narzędzia LSP/AST/MCP, wyselekcjonowani agenci, kompatybilność z Claude Code |
| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Powiadomienia na pulpicie i alerty dźwiękowe dla sesji OpenCode |
| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Powiadomienia na pulpicie i alerty dźwiękowe dla uprawnień, zakończeń zadań i błędów |

View File

@@ -32,7 +32,7 @@ Você também pode conferir [awesome-opencode](https://github.com/awesome-openco
| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Instruções para comandos de shell não interativos - evita travamentos de operações dependentes de TTY |
| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Acompanhe o uso do OpenCode com Wakatime |
| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Limpe tabelas markdown produzidas por LLMs |
| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | Edição de código 10x mais rápida com a API Morph Fast Apply e marcadores de edição preguiçosos |
| [opencode-morph-plugin](https://github.com/morphllm/opencode-morph-plugin) | Edição Fast Apply, busca de código WarpGrep e compactação de contexto via Morph |
| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Agentes em segundo plano, ferramentas LSP/AST/MCP pré-construídas, agentes curados, compatível com Claude Code |
| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Notificações de desktop e alertas sonoros para sessões do OpenCode |
| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Notificações de desktop e alertas sonoros para eventos de permissão, conclusão e erro |

View File

@@ -32,7 +32,7 @@ description: Проекты и интеграции, созданные с по
| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Инструкции для неинтерактивных shell-команд — предотвращают зависания из-за операций, зависящих от TTY |
| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Отслеживайте использование OpenCode с помощью Wakatime |
| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Очистка таблиц Markdown, созданных LLM |
| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | Редактирование кода в 10 раз быстрее с помощью API Morph Fast Apply и маркеров отложенного редактирования |
| [opencode-morph-plugin](https://github.com/morphllm/opencode-morph-plugin) | Редактирование Fast Apply, поиск по кодовой базе WarpGrep и сжатие контекста через Morph |
| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Фоновые агенты, встроенные инструменты LSP/AST/MCP, курируемые агенты, совместимость с Claude Code |
| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Уведомления на рабочем столе и звуковые оповещения для сеансов OpenCode |
| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Уведомления на рабочем столе и звуковые оповещения о разрешениях, завершении и событиях ошибок |

View File

@@ -32,7 +32,7 @@ description: โปรเจ็กต์และการผสานรวม
| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | คำแนะนำสำหรับคำสั่ง shell แบบไม่โต้ตอบ - ป้องกันการแฮงค์จากการดำเนินการที่ขึ้นอยู่กับ TTY |
| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | ติดตามการใช้งาน OpenCode ด้วย Wakatime |
| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | ทำความสะอาดตาราง Markdown ที่ผลิตโดย LLM |
| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | การแก้ไขโค้ดเร็วขึ้น 10 เท่าด้วย Morph Fast Apply API และเครื่องหมายแก้ไขแบบ Lazy |
| [opencode-morph-plugin](https://github.com/morphllm/opencode-morph-plugin) | การแก้ไข Fast Apply, การค้นหาโค้ดเบส WarpGrep และการบีบอัดบริบทผ่าน Morph |
| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | ตัวแทนเบื้องหลัง, เครื่องมือ LSP/AST/MCP ที่สร้างไว้ล่วงหน้า, ตัวแทนที่ได้รับการดูแลจัดการ, เข้ากันได้กับ Claude Code |
| [opencode-notificator](https://github.com/panta82/opencode-notificator) | การแจ้งเตือนบนเดสก์ท็อปและเสียงเตือนสำหรับเซสชัน OpenCode |
| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | การแจ้งเตือนบนเดสก์ท็อปและเสียงเตือนสำหรับการอนุญาต การดำเนินการเสร็จสิ้น และเหตุการณ์ข้อผิดพลาด |

View File

@@ -32,7 +32,7 @@ Ayrıca ekosistemi ve topluluğu bir araya getiren [awesome-opencode](https://gi
| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Etkileşimli olmayan kabuk komutları için talimatlar - TTY bağımlı işlemlerden kaynaklanan takılmaları önler |
| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Wakatime ile OpenCode kullanımını takip edin |
| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | LLM'ler tarafından üretilen markdown tablolarını temizleyin |
| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | Morph Fast Apply API ve tembel düzenleme işaretçileriyle 10 kat daha hızlı kod düzenleme |
| [opencode-morph-plugin](https://github.com/morphllm/opencode-morph-plugin) | Fast Apply düzenleme, WarpGrep kod tabanı araması ve Morph ile bağlam sıkıştırma |
| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Arka plan aracıları, hazır LSP/AST/MCP araçları, seçilmiş aracılar, Claude Code uyumlu |
| [opencode-notificator](https://github.com/panta82/opencode-notificator) | OpenCode oturumları için masaüstü bildirimleri ve sesli uyarılar |
| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | İzin, tamamlanma ve hata olayları için masaüstü bildirimleri ve sesli uyarılar |

View File

@@ -32,7 +32,7 @@ description: 基于 OpenCode 构建的项目与集成。
| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | 非交互式 shell 命令指令——防止依赖 TTY 的操作导致挂起 |
| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | 使用 Wakatime 追踪 OpenCode 的使用情况 |
| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | 清理 LLM 生成的 Markdown 表格 |
| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 通过 Morph Fast Apply API 和惰性编辑标记实现 10 倍更快的代码编辑 |
| [opencode-morph-plugin](https://github.com/morphllm/opencode-morph-plugin) | 通过 Morph 提供 Fast Apply 编辑、WarpGrep 代码搜索和上下文压缩 |
| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | 后台代理、预构建的 LSP/AST/MCP 工具、精选代理,兼容 Claude Code |
| [opencode-notificator](https://github.com/panta82/opencode-notificator) | OpenCode 会话的桌面通知和声音提醒 |
| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | 针对权限请求、任务完成和错误事件的桌面通知与声音提醒 |

View File

@@ -32,7 +32,7 @@ description: 基於 OpenCode 建置的專案與整合。
| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | 非互動式 shell 指令說明——防止依賴 TTY 的操作導致卡住 |
| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | 使用 Wakatime 追蹤 OpenCode 的使用情況 |
| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | 清理 LLM 生成的 Markdown 表格 |
| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 透過 Morph Fast Apply API 和惰性編輯標記實現 10 倍更快的程式碼編輯 |
| [opencode-morph-plugin](https://github.com/morphllm/opencode-morph-plugin) | 透過 Morph 提供 Fast Apply 編輯、WarpGrep 程式碼搜尋和上下文壓縮 |
| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | 背景代理、預建置的 LSP/AST/MCP 工具、精選代理,相容 Claude Code |
| [opencode-notificator](https://github.com/panta82/opencode-notificator) | OpenCode 工作階段的桌面通知和聲音提醒 |
| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | 針對權限請求、任務完成和錯誤事件的桌面通知與聲音提醒 |