From 2faa28e162bbc231ba2912d4f8e0814417253ae4 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 6 Oct 2025 12:04:17 +0000 Subject: [PATCH 01/22] ignore: update download stats 2025-10-06 --- STATS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/STATS.md b/STATS.md index 73ff2169c..af7357da0 100644 --- a/STATS.md +++ b/STATS.md @@ -100,3 +100,4 @@ | 2025-10-03 | 446,829 (+5,977) | 359,937 (+11,838) | 806,766 (+17,815) | | 2025-10-04 | 452,561 (+5,732) | 370,386 (+10,449) | 822,947 (+16,181) | | 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) | +| 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) | From b351b75156b3dec1496e849f0d55e8d5655876e4 Mon Sep 17 00:00:00 2001 From: Jay V Date: Mon, 6 Oct 2025 16:13:21 -0400 Subject: [PATCH 02/22] docs: share page css --- packages/web/src/styles/custom.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/web/src/styles/custom.css b/packages/web/src/styles/custom.css index 308e94a18..86628fe9f 100644 --- a/packages/web/src/styles/custom.css +++ b/packages/web/src/styles/custom.css @@ -24,6 +24,10 @@ --color-border-weak: hsl(0, 1%, 85%); --color-icon: hsl(0, 1%, 55%); + + /* For the share component */ + --sl-color-bg-surface: var(--sl-color-bg-nav); + --sl-color-divider: var(--sl-color-gray-5); } From 1db028dc05700806aa007520522a121f6c481355 Mon Sep 17 00:00:00 2001 From: Jay V Date: Mon, 6 Oct 2025 17:00:10 -0400 Subject: [PATCH 03/22] docs: fix styles and zen doc, closes #2912 --- packages/web/src/content/docs/config.mdx | 34 ++++++++++++------------ packages/web/src/content/docs/zen.mdx | 6 +++++ packages/web/src/styles/custom.css | 12 ++++++--- 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx index f00af8ecb..53b06f9d0 100644 --- a/packages/web/src/content/docs/config.mdx +++ b/packages/web/src/content/docs/config.mdx @@ -69,6 +69,23 @@ Your editor should be able to validate and autocomplete based on the schema. --- +### TUI + +You can configure TUI-specific settings through the `tui` option. + +```json title="opencode.json" +{ + "$schema": "https://opencode.ai/config.json", + "tui": { + "scroll_speed": 3 + } +} +``` + +[Learn more about using the TUI here](/docs/tui). + +--- + ### Models You can configure the providers and models you want to use in your OpenCode config through the `provider`, `model` and `small_model` options. @@ -204,23 +221,6 @@ OpenCode will automatically download any new updates when it starts up. You can --- -### TUI - -You can configure TUI-specific settings through the `tui` option. - -```json title="opencode.json" -{ - "$schema": "https://opencode.ai/config.json", - "tui": { - "scroll_speed": 3 - } -} -``` - -[Learn more about using the TUI here](/docs/tui). - ---- - ### Formatters You can configure code formatters through the `formatter` option. diff --git a/packages/web/src/content/docs/zen.mdx b/packages/web/src/content/docs/zen.mdx index 0ccfbedd2..6bbe34a84 100644 --- a/packages/web/src/content/docs/zen.mdx +++ b/packages/web/src/content/docs/zen.mdx @@ -73,6 +73,10 @@ You can also access our models through the following API endpoints. | Grok Code Fast 1 | grok-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K2 | kimi-k2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +The [model id](/docs/config/#models) in your OpenCode config +uses the format `opencode/`. For example, for GPT 5 Codex, you would +use `opencode/gpt-5-codex` in your config. + --- ## Pricing @@ -94,6 +98,8 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**. | GPT 5 | $1.25 | $10.00 | $0.125 | - | | GPT 5 Codex | $1.25 | $10.00 | $0.125 | - | +You might notice _Claude Haiku 3.5_ in your usage history. This is a [low cost model](/docs/config/#models) that's used to generate the titles of your sessions. + :::note Credit card fees are passed along at cost; we don't charge anything beyond that. ::: diff --git a/packages/web/src/styles/custom.css b/packages/web/src/styles/custom.css index 86628fe9f..1dcc08904 100644 --- a/packages/web/src/styles/custom.css +++ b/packages/web/src/styles/custom.css @@ -298,14 +298,16 @@ body > .page > header, :root[data-has-sidebar] body > .page > header { } -.sl-container ul li a { +nav.sidebar .sl-container ul li a, +div.right-sidebar .sl-container ul li a { padding: 4px 24px !important; width: 100% !important; color: var(--color-text-weaker); opacity: 50%; } -.sl-container ul li a:hover { +nav.sidebar .sl-container ul li a:hover, +div.right-sidebar .sl-container ul li a:hover { background: var(--color-background-weak); @media (prefers-color-scheme: dark) { @@ -313,12 +315,14 @@ body > .page > header, :root[data-has-sidebar] body > .page > header { } } -.sl-container ul li ul li { +nav.sidebar .sl-container ul li ul li, +div.right-sidebar .sl-container ul li ul li { padding: 4px 12px 0 12px !important; } -.sl-container ul li a[aria-current="true"] { +nav.sidebar .sl-container ul li a[aria-current="true"], +div.right-sidebar .sl-container ul li a[aria-current="true"] { color: var(--color-text-strong) !important; opacity: 100%; } From 1b17d8070bcddeddaea3dea403f031a161539901 Mon Sep 17 00:00:00 2001 From: Jay V Date: Mon, 6 Oct 2025 17:05:45 -0400 Subject: [PATCH 04/22] docs: update footer --- bun.lock | 4 ++-- packages/web/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bun.lock b/bun.lock index 159e57360..eef4aa56f 100644 --- a/bun.lock +++ b/bun.lock @@ -243,7 +243,7 @@ "sharp": "0.32.5", "shiki": "3.4.2", "solid-js": "catalog:", - "toolbeam-docs-theme": "0.4.6", + "toolbeam-docs-theme": "0.4.7", }, "devDependencies": { "@types/node": "catalog:", @@ -2801,7 +2801,7 @@ "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - "toolbeam-docs-theme": ["toolbeam-docs-theme@0.4.6", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-s4yKn3PYnmPCl6MHPkQ2MedSgVh7FMAP64DiXnAfCls/H/RR6iTe0/SFgPTprz+HXXaKvv2iPo3XuvmDRY6cxQ=="], + "toolbeam-docs-theme": ["toolbeam-docs-theme@0.4.7", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-oVA/V4M4s4vtLljfnZrOSuCNomek5h9jIYkr92l4QgAQvB3ht+D7xAJIy27IGVJzYA5scUE1OK84ZZqeajoeWw=="], "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], diff --git a/packages/web/package.json b/packages/web/package.json index 0b74294c1..ec0ced0ca 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -31,7 +31,7 @@ "sharp": "0.32.5", "shiki": "3.4.2", "solid-js": "catalog:", - "toolbeam-docs-theme": "0.4.6" + "toolbeam-docs-theme": "0.4.7" }, "devDependencies": { "opencode": "workspace:*", From 9e8fd16e6e4154ae0bccff8342e4b0c7780d8db8 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 6 Oct 2025 16:15:10 -0400 Subject: [PATCH 05/22] wip: zen --- packages/console/app/src/context/auth.ts | 1 + packages/console/app/src/lib/beta.ts | 7 + packages/console/app/src/routes/auth/index.ts | 22 ++- .../app/src/routes/workspace-picker.css | 184 ++++++++++++++++++ .../app/src/routes/workspace-picker.tsx | 144 ++++++++++++++ packages/console/app/src/routes/workspace.tsx | 11 +- .../console/app/src/routes/workspace/[id].tsx | 14 +- packages/console/core/src/account.ts | 17 +- packages/console/core/src/actor.ts | 9 + .../console/core/src/schema/workspace.sql.ts | 2 +- packages/console/core/src/user.ts | 2 - packages/console/core/src/workspace.ts | 64 +++--- packages/console/function/src/auth.ts | 22 ++- 13 files changed, 437 insertions(+), 62 deletions(-) create mode 100644 packages/console/app/src/lib/beta.ts create mode 100644 packages/console/app/src/routes/workspace-picker.css create mode 100644 packages/console/app/src/routes/workspace-picker.tsx diff --git a/packages/console/app/src/context/auth.ts b/packages/console/app/src/context/auth.ts index 14a876fda..14f275565 100644 --- a/packages/console/app/src/context/auth.ts +++ b/packages/console/app/src/context/auth.ts @@ -73,6 +73,7 @@ export const getActor = async (workspace?: string): Promise => { properties: { userID: user.id, workspaceID: user.workspaceID, + accountID: user.accountID, }, } } diff --git a/packages/console/app/src/lib/beta.ts b/packages/console/app/src/lib/beta.ts new file mode 100644 index 000000000..910731c54 --- /dev/null +++ b/packages/console/app/src/lib/beta.ts @@ -0,0 +1,7 @@ +import { query } from "@solidjs/router" +import { Resource } from "@opencode/console-resource" + +export const beta = query(async (workspaceID?: string) => { + "use server" + return Resource.App.stage === "production" ? workspaceID === "wrk_01K46JDFR0E75SG2Q8K172KF3Y" : true +}, "beta") diff --git a/packages/console/app/src/routes/auth/index.ts b/packages/console/app/src/routes/auth/index.ts index 59d172386..f522e761d 100644 --- a/packages/console/app/src/routes/auth/index.ts +++ b/packages/console/app/src/routes/auth/index.ts @@ -1,11 +1,29 @@ -import { Account } from "@opencode-ai/console-core/account.js" +import { Actor } from "@opencode-ai/console-core/actor.js" +import { and, Database, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js" +import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js" +import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js" import { redirect } from "@solidjs/router" import type { APIEvent } from "@solidjs/start/server" import { withActor } from "~/context/auth.withActor" export async function GET(input: APIEvent) { try { - const workspaces = await withActor(async () => Account.workspaces()) + const workspaces = await withActor(async () => { + const actor = Actor.assert("account") + return Database.transaction(async (tx) => + tx + .select({ id: WorkspaceTable.id }) + .from(UserTable) + .innerJoin(WorkspaceTable, eq(UserTable.workspaceID, WorkspaceTable.id)) + .where( + and( + eq(UserTable.accountID, actor.properties.accountID), + isNull(UserTable.timeDeleted), + isNull(WorkspaceTable.timeDeleted), + ), + ), + ) + }) return redirect(`/workspace/${workspaces[0].id}`) } catch { return redirect("/auth/authorize") diff --git a/packages/console/app/src/routes/workspace-picker.css b/packages/console/app/src/routes/workspace-picker.css new file mode 100644 index 000000000..c22ced867 --- /dev/null +++ b/packages/console/app/src/routes/workspace-picker.css @@ -0,0 +1,184 @@ +[data-component="workspace-picker"] { + position: relative; + /* Override blue accent colors with neutral colors */ + --color-accent: var(--color-border); + --color-accent-hover: var(--color-border); + --color-accent-active: var(--color-border); + --color-primary: var(--color-border); + --color-primary-hover: var(--color-border); + --color-primary-active: var(--color-border); + --color-primary-alpha-20: transparent; + + [data-slot="trigger"] { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-2); + padding: var(--space-2) var(--space-3); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + background-color: var(--color-bg); + color: var(--color-text); + font-size: var(--font-size-sm); + font-family: var(--font-sans); + cursor: pointer; + min-width: 200px; + + span { + flex: 1; + text-align: left; + font-weight: 500; + } + } + + [data-slot="chevron"] { + flex-shrink: 0; + color: var(--color-text-secondary); + } + + [data-slot="dropdown"] button { + text-decoration: none !important; + } + + /* Ensure text inside buttons has no underline */ + [data-slot="dropdown"] button * { + text-decoration: none !important; + } + + [data-slot="dropdown"] { + position: absolute; + top: 100%; + left: 0; + right: 0; + z-index: 1000; + margin-top: var(--space-1); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + background-color: var(--color-bg); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + max-height: 240px; + overflow-y: auto; + + @media (prefers-color-scheme: dark) { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + } + } + + [data-slot="option"], + [data-slot="create-option"] { + width: 100%; + padding: var(--space-2-5) var(--space-3); + border: none; + background: none; + color: var(--color-text); + font-size: var(--font-size-sm); + font-family: var(--font-sans); + text-align: left; + cursor: pointer; + text-decoration: none; + + &:hover { + background-color: var(--color-surface); + text-decoration: none; + } + + &:focus { + text-decoration: none; + } + + &:active { + text-decoration: none; + } + + &:first-child { + border-top-left-radius: var(--border-radius-sm); + border-top-right-radius: var(--border-radius-sm); + } + + &:last-child { + border-bottom-left-radius: var(--border-radius-sm); + border-bottom-right-radius: var(--border-radius-sm); + } + } + + [data-slot="option"][data-selected="true"] { + background-color: transparent; + color: var(--color-text); + } + + [data-slot="create-option"] { + color: var(--color-text-secondary); + font-weight: 500; + } + + [data-slot="create-form"] { + margin-top: var(--space-4); + padding: var(--space-4); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + background-color: var(--color-surface); + } + + [data-slot="create-input-group"] { + display: flex; + gap: var(--space-2); + align-items: center; + + @media (max-width: 30rem) { + flex-direction: column; + align-items: stretch; + } + } + + [data-slot="create-input"] { + flex: 1; + padding: var(--space-2-5) var(--space-3); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-sm); + background-color: var(--color-bg); + color: var(--color-text); + font-size: var(--font-size-sm); + font-family: var(--font-sans); + + &:focus { + outline: none; + border-color: var(--color-border); + box-shadow: none; + } + + &::placeholder { + color: var(--color-text-muted); + } + } + + button[type="submit"], + button[type="button"] { + padding: var(--space-2-5) var(--space-4); + background-color: var(--color-bg); + color: var(--color-text); + font-size: var(--font-size-sm); + font-family: var(--font-sans); + font-weight: 500; + cursor: pointer; + white-space: nowrap; + + &:focus { + outline: none; + box-shadow: none; + } + + &:active { + transform: translateY(1px); + } + + &[data-color="primary"] { + background-color: var(--color-text-secondary); + border-color: var(--color-text-secondary); + color: var(--color-bg); + } + + @media (max-width: 30rem) { + flex: 1; + } + } +} \ No newline at end of file diff --git a/packages/console/app/src/routes/workspace-picker.tsx b/packages/console/app/src/routes/workspace-picker.tsx new file mode 100644 index 000000000..181826335 --- /dev/null +++ b/packages/console/app/src/routes/workspace-picker.tsx @@ -0,0 +1,144 @@ +import { query, useParams, action, createAsync, redirect } from "@solidjs/router" +import { For, Show, createEffect, onCleanup } from "solid-js" +import { createStore } from "solid-js/store" +import { withActor } from "~/context/auth.withActor" +import { Actor } from "@opencode-ai/console-core/actor.js" +import { and, Database, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js" +import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js" +import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js" +import { Workspace } from "@opencode-ai/console-core/workspace.js" +import "./workspace-picker.css" + +const getWorkspaces = query(async () => { + "use server" + return withActor(async () => { + return Database.transaction((tx) => + tx + .select({ + id: WorkspaceTable.id, + name: WorkspaceTable.name, + slug: WorkspaceTable.slug, + }) + .from(UserTable) + .innerJoin(WorkspaceTable, eq(UserTable.workspaceID, WorkspaceTable.id)) + .where(and(eq(UserTable.accountID, Actor.account()), isNull(WorkspaceTable.timeDeleted))), + ) + }) +}, "workspaces") + +const createWorkspace = action(async (form: FormData) => { + "use server" + const name = form.get("workspaceName") as string + if (name?.trim()) { + return withActor(async () => { + const workspaceID = await Workspace.create({ name: name.trim() }) + return redirect(`/workspace/${workspaceID}`) + }) + } +}, "createWorkspace") + +export function WorkspacePicker() { + const params = useParams() + const workspaces = createAsync(() => getWorkspaces()) + const [store, setStore] = createStore({ + showForm: false, + showDropdown: false, + }) + let dropdownRef: HTMLDivElement | undefined + + const currentWorkspace = () => { + const ws = workspaces()?.find((w) => w.id === params.id) + return ws ? ws.name : "Select workspace" + } + + const handleWorkspaceNew = () => { + setStore({ showForm: true, showDropdown: false }) + } + + const handleSelectWorkspace = (workspaceID: string) => { + if (workspaceID === params.id) { + setStore("showDropdown", false) + return + } + + window.location.href = `/workspace/${workspaceID}` + } + + // Reset signals when workspace ID changes + createEffect(() => { + params.id + setStore("showForm", false) + setStore("showDropdown", false) + }) + + createEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef && !dropdownRef.contains(event.target as Node)) { + setStore("showDropdown", false) + } + } + + document.addEventListener("click", handleClickOutside) + onCleanup(() => document.removeEventListener("click", handleClickOutside)) + }) + + return ( +
+
+
setStore("showDropdown", !store.showDropdown)}> + {currentWorkspace()} + + + +
+ + +
+ + {(workspace) => ( + + )} + + +
+
+
+ + +
+
+ + + +
+
+
+
+ ) +} diff --git a/packages/console/app/src/routes/workspace.tsx b/packages/console/app/src/routes/workspace.tsx index 8e42815f7..ac394f585 100644 --- a/packages/console/app/src/routes/workspace.tsx +++ b/packages/console/app/src/routes/workspace.tsx @@ -1,11 +1,14 @@ +import { Show } from "solid-js" +import { getRequestEvent } from "solid-js/web" +import { query, action, redirect, createAsync, RouteSectionProps, useParams, A } from "@solidjs/router" import "./workspace.css" import { useAuthSession } from "~/context/auth.session" import { IconLogo } from "../component/icon" +import { WorkspacePicker } from "./workspace-picker" import { withActor } from "~/context/auth.withActor" -import { query, action, redirect, createAsync, RouteSectionProps, useParams, A } from "@solidjs/router" import { User } from "@opencode-ai/console-core/user.js" import { Actor } from "@opencode-ai/console-core/actor.js" -import { getRequestEvent } from "solid-js/web" +import { beta } from "~/lib/beta" const getUserInfo = query(async (workspaceID: string) => { "use server" @@ -35,6 +38,7 @@ const logout = action(async () => { export default function WorkspaceLayout(props: RouteSectionProps) { const params = useParams() const userInfo = createAsync(() => getUserInfo(params.id)) + const isBeta = createAsync(() => beta(params.id)) return (
@@ -44,6 +48,9 @@ export default function WorkspaceLayout(props: RouteSectionProps) {
+ + + {userInfo()?.email}
+ +
+ + } + > + + + + + + ) +} diff --git a/packages/console/core/src/workspace.ts b/packages/console/core/src/workspace.ts index 36d66e15a..f9591632a 100644 --- a/packages/console/core/src/workspace.ts +++ b/packages/console/core/src/workspace.ts @@ -7,6 +7,7 @@ import { UserTable } from "./schema/user.sql" import { BillingTable } from "./schema/billing.sql" import { WorkspaceTable } from "./schema/workspace.sql" import { Key } from "./key" +import { eq } from "drizzle-orm" export namespace Workspace { export const create = fn( @@ -45,4 +46,21 @@ export namespace Workspace { return workspaceID }, ) + + export const update = fn( + z.object({ + name: z.string().min(1).max(255), + }), + async ({ name }) => { + const workspaceID = Actor.workspace() + return await Database.use((tx) => + tx + .update(WorkspaceTable) + .set({ + name, + }) + .where(eq(WorkspaceTable.id, workspaceID)), + ) + }, + ) } From f47c7c5a07ce353ee5f8a436e51798f9dffd4477 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 6 Oct 2025 17:17:02 -0400 Subject: [PATCH 07/22] wip: zen --- packages/console/core/src/workspace.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/console/core/src/workspace.ts b/packages/console/core/src/workspace.ts index f9591632a..32f5bd36c 100644 --- a/packages/console/core/src/workspace.ts +++ b/packages/console/core/src/workspace.ts @@ -12,7 +12,7 @@ import { eq } from "drizzle-orm" export namespace Workspace { export const create = fn( z.object({ - name: z.string(), + name: z.string().min(1), }), async ({ name }) => { const account = Actor.assert("account") From a470859f6f757a749b2f347bcc6949c9b884a9d9 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 6 Oct 2025 17:23:10 -0400 Subject: [PATCH 08/22] wip: zen --- bun.lock | 1 + packages/console/app/package.json | 1 + packages/console/app/src/lib/beta.ts | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/bun.lock b/bun.lock index eef4aa56f..173664dce 100644 --- a/bun.lock +++ b/bun.lock @@ -17,6 +17,7 @@ "@kobalte/core": "catalog:", "@openauthjs/openauth": "0.0.0-20250322224806", "@opencode-ai/console-core": "workspace:*", + "@opencode-ai/console-resource": "workspace:*", "@solidjs/meta": "^0.29.4", "@solidjs/router": "^0.15.0", "@solidjs/start": "^1.1.0", diff --git a/packages/console/app/package.json b/packages/console/app/package.json index cfa272706..875129ea3 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -14,6 +14,7 @@ "@kobalte/core": "catalog:", "@openauthjs/openauth": "0.0.0-20250322224806", "@opencode-ai/console-core": "workspace:*", + "@opencode-ai/console-resource": "workspace:*", "@solidjs/meta": "^0.29.4", "@solidjs/router": "^0.15.0", "@solidjs/start": "^1.1.0", diff --git a/packages/console/app/src/lib/beta.ts b/packages/console/app/src/lib/beta.ts index 910731c54..d60a735ee 100644 --- a/packages/console/app/src/lib/beta.ts +++ b/packages/console/app/src/lib/beta.ts @@ -1,5 +1,5 @@ import { query } from "@solidjs/router" -import { Resource } from "@opencode/console-resource" +import { Resource } from "@opencode-ai/console-resource" export const beta = query(async (workspaceID?: string) => { "use server" From 9a0735de76bd43743073263a13408a1791f26fd1 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 6 Oct 2025 18:50:56 -0400 Subject: [PATCH 09/22] Add session forking functionality and simplify remove logic --- packages/opencode/src/session/index.ts | 44 ++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index cce1cf8c6..c8e6d4ad4 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -17,6 +17,7 @@ import { MessageV2 } from "./message-v2" import { Project } from "../project/project" import { Instance } from "../project/instance" import { SessionPrompt } from "./prompt" +import { fn } from "@/util/fn" export namespace Session { const log = Log.create({ service: "session" }) @@ -100,6 +101,37 @@ export namespace Session { }) } + export const fork = fn( + z.object({ + sessionID: Identifier.schema("session"), + messageID: Identifier.schema("message").optional(), + }), + async (input) => { + const session = await createNext({ + directory: Instance.directory, + }) + const msgs = await messages(input.sessionID) + for (const msg of msgs) { + if (input.messageID && msg.info.id >= input.messageID) break + const cloned = await updateMessage({ + ...msg.info, + sessionID: session.id, + id: Identifier.ascending("message"), + }) + + for (const part of msg.parts) { + await updatePart({ + ...part, + id: Identifier.ascending("part"), + messageID: cloned.id, + sessionID: session.id, + }) + } + } + return session + }, + ) + export async function touch(sessionID: string) { await update(sessionID, (draft) => { draft.time.updated = Date.now() @@ -242,12 +274,12 @@ export namespace Session { return result } - export async function remove(sessionID: string, emitEvent = true) { + export async function remove(sessionID: string) { const project = Instance.project try { const session = await get(sessionID) for (const child of await children(sessionID)) { - await remove(child.id, false) + await remove(child.id) } await unshare(sessionID).catch(() => {}) for (const msg of await Storage.list(["message", sessionID])) { @@ -257,11 +289,9 @@ export namespace Session { await Storage.remove(msg) } await Storage.remove(["session", project.id, sessionID]) - if (emitEvent) { - Bus.publish(Event.Deleted, { - info: session, - }) - } + Bus.publish(Event.Deleted, { + info: session, + }) } catch (e) { log.error(e) } From 6417edf99806b5aa93625622ba1726feae1a11fd Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 6 Oct 2025 18:51:57 -0400 Subject: [PATCH 10/22] Add todo list and session forking API endpoints --- packages/opencode/src/server/server.ts | 60 ++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 7020a2aaa..cba186dd9 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -29,7 +29,9 @@ import { SessionPrompt } from "../session/prompt" import { SessionCompaction } from "../session/compaction" import { SessionRevert } from "../session/revert" import { lazy } from "../util/lazy" +import { Todo } from "../session/todo" import { InstanceBootstrap } from "../project/bootstrap" +import { Identifier } from "@/id/id" const ERRORS = { 400: { @@ -343,6 +345,34 @@ export namespace Server { return c.json(session) }, ) + .get( + "/session/:id/todo", + describeRoute({ + description: "Get the todo list for a session", + operationId: "session.todo", + responses: { + 200: { + description: "Todo list", + content: { + "application/json": { + schema: resolver(Todo.Info.array()), + }, + }, + }, + }, + }), + validator( + "param", + z.object({ + id: z.string().meta({ description: "Session ID" }), + }), + ), + async (c) => { + const sessionID = c.req.valid("param").id + const todos = await Todo.get(sessionID) + return c.json(todos) + }, + ) .post( "/session", describeRoute({ @@ -480,6 +510,36 @@ export namespace Server { return c.json(true) }, ) + .post( + "/session/:id/fork", + describeRoute({ + description: "Fork an existing session at a specific message", + operationId: "session.fork", + responses: { + 200: { + description: "200", + content: { + "application/json": { + schema: resolver(Session.Info), + }, + }, + }, + }, + }), + validator( + "param", + z.object({ + id: Identifier.schema("session").meta({ description: "Session ID" }), + }), + ), + validator("json", Session.fork.schema.omit({ sessionID: true })), + async (c) => { + const sessionID = c.req.valid("param").id + const body = c.req.valid("json") + const result = await Session.fork({ ...body, sessionID }) + return c.json(result) + }, + ) .post( "/session/:id/abort", describeRoute({ From cdd6e98af91c11143c601520d008df5d883bf44c Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 6 Oct 2025 18:53:35 -0400 Subject: [PATCH 11/22] Add missing files and fix type aliases for opentui features --- packages/opencode/src/session/todo.ts | 34 +++++++++++++++++++++++++++ packages/opencode/src/tool/bash.ts | 22 +++++++++-------- packages/opencode/src/tool/test.ts | 11 ++++++--- packages/opencode/src/util/fn.ts | 11 +++++++++ packages/opencode/tsconfig.json | 10 +++++++- 5 files changed, 74 insertions(+), 14 deletions(-) create mode 100644 packages/opencode/src/session/todo.ts create mode 100644 packages/opencode/src/util/fn.ts diff --git a/packages/opencode/src/session/todo.ts b/packages/opencode/src/session/todo.ts new file mode 100644 index 000000000..4f4268540 --- /dev/null +++ b/packages/opencode/src/session/todo.ts @@ -0,0 +1,34 @@ +import z from "zod/v4" +import { Bus } from "../bus" +import { Storage } from "../storage/storage" + +export namespace Todo { + export const Info = z + .object({ + content: z.string().describe("Brief description of the task"), + status: z.string().describe("Current status of the task: pending, in_progress, completed, cancelled"), + priority: z.string().describe("Priority level of the task: high, medium, low"), + id: z.string().describe("Unique identifier for the todo item"), + }) + .meta({ ref: "Todo" }) + export type Info = z.infer + + export const Event = { + Updated: Bus.event( + "todo.updated", + z.object({ + sessionID: z.string(), + todos: z.array(Info), + }), + ), + } + + export async function update(input: { sessionID: string; todos: Info[] }) { + await Storage.write(["todo", input.sessionID], input.todos) + Bus.publish(Event.Updated, input) + } + + export async function get(sessionID: string) { + return Storage.read(["todo", sessionID]) ?? [] + } +} diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index 0e1d37ecf..1946ada1f 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -3,22 +3,17 @@ import { exec } from "child_process" import { Tool } from "./tool" import DESCRIPTION from "./bash.txt" -import { Permission } from "../permission" -import { Filesystem } from "../util/filesystem" import { lazy } from "../util/lazy" import { Log } from "../util/log" -import { Wildcard } from "../util/wildcard" -import { $ } from "bun" import { Instance } from "../project/instance" -import { Agent } from "../agent/agent" const MAX_OUTPUT_LENGTH = 30_000 const DEFAULT_TIMEOUT = 1 * 60 * 1000 const MAX_TIMEOUT = 10 * 60 * 1000 -const log = Log.create({ service: "bash-tool" }) +export const log = Log.create({ service: "bash-tool" }) -const parser = lazy(async () => { +export const parser = lazy(async () => { try { const { default: Parser } = await import("tree-sitter") const Bash = await import("tree-sitter-bash") @@ -26,8 +21,10 @@ const parser = lazy(async () => { p.setLanguage(Bash.language as any) return p } catch (e) { - const { default: Parser } = await import("web-tree-sitter") - const { default: treeWasm } = await import("web-tree-sitter/tree-sitter.wasm" as string, { with: { type: "wasm" } }) + const { Parser, Language } = await import("web-tree-sitter") + const { default: treeWasm } = await import("web-tree-sitter/web-tree-sitter.wasm" as string, { + with: { type: "wasm" }, + }) await Parser.init({ locateFile() { return treeWasm @@ -36,7 +33,7 @@ const parser = lazy(async () => { const { default: bashWasm } = await import("tree-sitter-bash/tree-sitter-bash.wasm" as string, { with: { type: "wasm" }, }) - const bashLanguage = await Parser.Language.load(bashWasm) + const bashLanguage = await Language.load(bashWasm) const p = new Parser() p.setLanguage(bashLanguage) return p @@ -56,7 +53,11 @@ export const BashTool = Tool.define("bash", { }), async execute(params, ctx) { const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT) + /* const tree = await parser().then((p) => p.parse(params.command)) + if (!tree) { + throw new Error("Failed to parse command") + } const permissions = await Agent.get(ctx.agent).then((x) => x.permission.bash) const askPatterns = new Set() @@ -145,6 +146,7 @@ export const BashTool = Tool.define("bash", { }, }) } + */ const process = exec(params.command, { cwd: Instance.directory, diff --git a/packages/opencode/src/tool/test.ts b/packages/opencode/src/tool/test.ts index 138d92fbc..14427c73c 100644 --- a/packages/opencode/src/tool/test.ts +++ b/packages/opencode/src/tool/test.ts @@ -6,8 +6,10 @@ const parser = async () => { p.setLanguage(Bash.language as any) return p } catch (e) { - const { default: Parser } = await import("web-tree-sitter") - const { default: treeWasm } = await import("web-tree-sitter/tree-sitter.wasm" as string, { with: { type: "wasm" } }) + const { Parser, Language } = await import("web-tree-sitter") + const { default: treeWasm } = await import("web-tree-sitter/web-tree-sitter.wasm" as string, { + with: { type: "wasm" }, + }) await Parser.init({ locateFile() { return treeWasm @@ -16,7 +18,7 @@ const parser = async () => { const { default: bashWasm } = await import("tree-sitter-bash/tree-sitter-bash.wasm" as string, { with: { type: "wasm" }, }) - const bashLanguage = await Parser.Language.load(bashWasm) + const bashLanguage = await Language.load(bashWasm) const p = new Parser() p.setLanguage(bashLanguage) return p @@ -62,6 +64,9 @@ function extractCommands(node: any): Array<{ command: string; args: string[] }> // Extract and display commands console.log("Source code: " + sourceCode) +if (!tree) { + throw new Error("Failed to parse command") +} const commands = extractCommands(tree.rootNode) console.log("Extracted commands:") commands.forEach((cmd, index) => { diff --git a/packages/opencode/src/util/fn.ts b/packages/opencode/src/util/fn.ts new file mode 100644 index 000000000..9efe4622f --- /dev/null +++ b/packages/opencode/src/util/fn.ts @@ -0,0 +1,11 @@ +import { z } from "zod" + +export function fn(schema: T, cb: (input: z.infer) => Result) { + const result = (input: z.infer) => { + const parsed = schema.parse(input) + return cb(parsed) + } + result.force = (input: z.infer) => cb(input) + result.schema = schema + return result +} diff --git a/packages/opencode/tsconfig.json b/packages/opencode/tsconfig.json index 8e4f68a03..167d7936c 100644 --- a/packages/opencode/tsconfig.json +++ b/packages/opencode/tsconfig.json @@ -2,6 +2,14 @@ "$schema": "https://json.schemastore.org/tsconfig", "extends": "@tsconfig/bun/tsconfig.json", "compilerOptions": { - "lib": ["ESNext", "DOM", "DOM.Iterable"] + "jsx": "preserve", + "jsxImportSource": "@opentui/solid", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "customConditions": ["browser"], + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + "@tui/*": ["./src/cli/cmd/tui/*"] + } } } From aee240150bb75ba40a069e94e2e707c8bd25ecd7 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 6 Oct 2025 18:54:05 -0400 Subject: [PATCH 12/22] Update todo tool to use centralized Todo module --- packages/opencode/src/tool/todo.ts | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/packages/opencode/src/tool/todo.ts b/packages/opencode/src/tool/todo.ts index 9b4efddb0..63180eb6e 100644 --- a/packages/opencode/src/tool/todo.ts +++ b/packages/opencode/src/tool/todo.ts @@ -1,31 +1,18 @@ import z from "zod/v4" import { Tool } from "./tool" import DESCRIPTION_WRITE from "./todowrite.txt" -import { Instance } from "../project/instance" - -const TodoInfo = z.object({ - content: z.string().describe("Brief description of the task"), - status: z.string().describe("Current status of the task: pending, in_progress, completed, cancelled"), - priority: z.string().describe("Priority level of the task: high, medium, low"), - id: z.string().describe("Unique identifier for the todo item"), -}) -type TodoInfo = z.infer - -const state = Instance.state(() => { - const todos: { - [sessionId: string]: TodoInfo[] - } = {} - return todos -}) +import { Todo } from "../session/todo" export const TodoWriteTool = Tool.define("todowrite", { description: DESCRIPTION_WRITE, parameters: z.object({ - todos: z.array(TodoInfo).describe("The updated todo list"), + todos: z.array(Todo.Info).describe("The updated todo list"), }), async execute(params, opts) { - const todos = state() - todos[opts.sessionID] = params.todos + await Todo.update({ + sessionID: opts.sessionID, + todos: params.todos, + }) return { title: `${params.todos.filter((x) => x.status !== "completed").length} todos`, output: JSON.stringify(params.todos, null, 2), @@ -40,7 +27,7 @@ export const TodoReadTool = Tool.define("todoread", { description: "Use this tool to read your todo list", parameters: z.object({}), async execute(_params, opts) { - const todos = state()[opts.sessionID] ?? [] + const todos = await Todo.get(opts.sessionID) return { title: `${todos.filter((x) => x.status !== "completed").length} todos`, metadata: { From 10998d62b9f0964926d4da967a21889eefe82a87 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 6 Oct 2025 19:37:30 -0400 Subject: [PATCH 13/22] core: improve session API reliability with proper input validation --- packages/opencode/src/cli/cmd/run.ts | 2 +- packages/opencode/src/server/server.ts | 34 +--- packages/opencode/src/session/compaction.ts | 2 +- packages/opencode/src/session/index.ts | 206 +++++++++++--------- packages/opencode/src/session/prompt.ts | 6 +- packages/opencode/src/tool/task.ts | 7 +- 6 files changed, 139 insertions(+), 118 deletions(-) diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index e04ed8103..7d0e68dee 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -106,7 +106,7 @@ export const RunCommand = cmd({ if (args.session) return Session.get(args.session) - return Session.create() + return Session.create({}) })() if (!session) { diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index cba186dd9..26cbb5d71 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -31,7 +31,6 @@ import { SessionRevert } from "../session/revert" import { lazy } from "../util/lazy" import { Todo } from "../session/todo" import { InstanceBootstrap } from "../project/bootstrap" -import { Identifier } from "@/id/id" const ERRORS = { 400: { @@ -308,7 +307,7 @@ export namespace Server { validator( "param", z.object({ - id: z.string(), + id: Session.get.schema, }), ), async (c) => { @@ -336,7 +335,7 @@ export namespace Server { validator( "param", z.object({ - id: z.string(), + id: Session.children.schema, }), ), async (c) => { @@ -390,18 +389,10 @@ export namespace Server { }, }, }), - validator( - "json", - z - .object({ - parentID: z.string().optional(), - title: z.string().optional(), - }) - .optional(), - ), + validator("json", Session.create.schema.optional()), async (c) => { const body = c.req.valid("json") ?? {} - const session = await Session.create(body.parentID, body.title) + const session = await Session.create(body) return c.json(session) }, ) @@ -424,7 +415,7 @@ export namespace Server { validator( "param", z.object({ - id: z.string(), + id: Session.remove.schema, }), ), async (c) => { @@ -495,14 +486,7 @@ export namespace Server { id: z.string().meta({ description: "Session ID" }), }), ), - validator( - "json", - z.object({ - messageID: z.string(), - providerID: z.string(), - modelID: z.string(), - }), - ), + validator("json", Session.initialize.schema.omit({ sessionID: true })), async (c) => { const sessionID = c.req.valid("param").id const body = c.req.valid("json") @@ -529,7 +513,7 @@ export namespace Server { validator( "param", z.object({ - id: Identifier.schema("session").meta({ description: "Session ID" }), + id: Session.fork.schema.shape.sessionID, }), ), validator("json", Session.fork.schema.omit({ sessionID: true })), @@ -614,7 +598,7 @@ export namespace Server { validator( "param", z.object({ - id: z.string(), + id: Session.unshare.schema, }), ), async (c) => { @@ -717,7 +701,7 @@ export namespace Server { ), async (c) => { const params = c.req.valid("param") - const message = await Session.getMessage(params.id, params.messageID) + const message = await Session.getMessage({ sessionID: params.id, messageID: params.messageID }) return c.json(message) }, ) diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index e9b120c96..9282d8243 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -144,7 +144,7 @@ export namespace SessionCompaction { }, ], }) - const usage = Session.getUsage(model.info, generated.usage, generated.providerMetadata) + const usage = Session.getUsage({ model: model.info, usage: generated.usage, metadata: generated.providerMetadata }) msg.cost += usage.cost msg.tokens = usage.tokens msg.summary = true diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index c8e6d4ad4..521dcfe72 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -93,13 +93,21 @@ export namespace Session { ), } - export async function create(parentID?: string, title?: string) { - return createNext({ - parentID, - directory: Instance.directory, - title, - }) - } + export const create = fn( + z + .object({ + parentID: Identifier.schema("session").optional(), + title: z.string().optional(), + }) + .optional(), + async (input) => { + return createNext({ + parentID: input?.parentID, + directory: Instance.directory, + title: input?.title, + }) + }, + ) export const fork = fn( z.object({ @@ -132,11 +140,11 @@ export namespace Session { }, ) - export async function touch(sessionID: string) { + export const touch = fn(Identifier.schema("session"), async (sessionID) => { await update(sessionID, (draft) => { draft.time.updated = Date.now() }) - } + }) export async function createNext(input: { id?: string; title?: string; parentID?: string; directory: string }) { const result: Info = { @@ -170,16 +178,16 @@ export namespace Session { return result } - export async function get(id: string) { + export const get = fn(Identifier.schema("session"), async (id) => { const read = await Storage.read(["session", Instance.project.id, id]) return read as Info - } + }) - export async function getShare(id: string) { + export const getShare = fn(Identifier.schema("session"), async (id) => { return Storage.read(["share", id]) - } + }) - export async function share(id: string) { + export const share = fn(Identifier.schema("session"), async (id) => { const cfg = await Config.get() if (cfg.share === "disabled") { throw new Error("Sharing is disabled in configuration") @@ -202,9 +210,9 @@ export namespace Session { } } return share - } + }) - export async function unshare(id: string) { + export const unshare = fn(Identifier.schema("session"), async (id) => { const share = await getShare(id) if (!share) return await Storage.remove(["share", id]) @@ -212,7 +220,7 @@ export namespace Session { draft.share = undefined }) await Share.remove(id, share.secret) - } + }) export async function update(id: string, editor: (session: Info) => void) { const project = Instance.project @@ -226,7 +234,7 @@ export namespace Session { return result } - export async function messages(sessionID: string) { + export const messages = fn(Identifier.schema("session"), async (sessionID) => { const result = [] as MessageV2.WithParts[] for (const p of await Storage.list(["message", sessionID])) { const read = await Storage.read(p) @@ -237,16 +245,22 @@ export namespace Session { } result.sort((a, b) => (a.info.id > b.info.id ? 1 : -1)) return result - } + }) - export async function getMessage(sessionID: string, messageID: string) { - return { - info: await Storage.read(["message", sessionID, messageID]), - parts: await getParts(messageID), - } - } + export const getMessage = fn( + z.object({ + sessionID: Identifier.schema("session"), + messageID: Identifier.schema("message"), + }), + async (input) => { + return { + info: await Storage.read(["message", input.sessionID, input.messageID]), + parts: await getParts(input.messageID), + } + }, + ) - export async function getParts(messageID: string) { + export const getParts = fn(Identifier.schema("message"), async (messageID) => { const result = [] as MessageV2.Part[] for (const item of await Storage.list(["part", messageID])) { const read = await Storage.read(item) @@ -254,7 +268,7 @@ export namespace Session { } result.sort((a, b) => (a.id > b.id ? 1 : -1)) return result - } + }) export async function* list() { const project = Instance.project @@ -263,7 +277,7 @@ export namespace Session { } } - export async function children(parentID: string) { + export const children = fn(Identifier.schema("session"), async (parentID) => { const project = Instance.project const result = [] as Session.Info[] for (const item of await Storage.list(["session", project.id])) { @@ -272,9 +286,9 @@ export namespace Session { result.push(session) } return result - } + }) - export async function remove(sessionID: string) { + export const remove = fn(Identifier.schema("session"), async (sessionID) => { const project = Instance.project try { const session = await get(sessionID) @@ -295,56 +309,69 @@ export namespace Session { } catch (e) { log.error(e) } - } + }) - export async function updateMessage(msg: MessageV2.Info) { + export const updateMessage = fn(MessageV2.Info, async (msg) => { await Storage.write(["message", msg.sessionID, msg.id], msg) Bus.publish(MessageV2.Event.Updated, { info: msg, }) return msg - } + }) - export async function removeMessage(sessionID: string, messageID: string) { - await Storage.remove(["message", sessionID, messageID]) - Bus.publish(MessageV2.Event.Removed, { - sessionID, - messageID, - }) - return messageID - } + export const removeMessage = fn( + z.object({ + sessionID: Identifier.schema("session"), + messageID: Identifier.schema("message"), + }), + async (input) => { + await Storage.remove(["message", input.sessionID, input.messageID]) + Bus.publish(MessageV2.Event.Removed, { + sessionID: input.sessionID, + messageID: input.messageID, + }) + return input.messageID + }, + ) - export async function updatePart(part: MessageV2.Part) { + export const updatePart = fn(MessageV2.Part, async (part) => { await Storage.write(["part", part.messageID, part.id], part) Bus.publish(MessageV2.Event.PartUpdated, { part, }) return part - } + }) - export function getUsage(model: ModelsDev.Model, usage: LanguageModelUsage, metadata?: ProviderMetadata) { - const tokens = { - input: usage.inputTokens ?? 0, - output: usage.outputTokens ?? 0, - reasoning: usage?.reasoningTokens ?? 0, - cache: { - write: (metadata?.["anthropic"]?.["cacheCreationInputTokens"] ?? - // @ts-expect-error - metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ?? - 0) as number, - read: usage.cachedInputTokens ?? 0, - }, - } - return { - cost: new Decimal(0) - .add(new Decimal(tokens.input).mul(model.cost?.input ?? 0).div(1_000_000)) - .add(new Decimal(tokens.output).mul(model.cost?.output ?? 0).div(1_000_000)) - .add(new Decimal(tokens.cache.read).mul(model.cost?.cache_read ?? 0).div(1_000_000)) - .add(new Decimal(tokens.cache.write).mul(model.cost?.cache_write ?? 0).div(1_000_000)) - .toNumber(), - tokens, - } - } + export const getUsage = fn( + z.object({ + model: z.custom(), + usage: z.custom(), + metadata: z.custom().optional(), + }), + (input) => { + const tokens = { + input: input.usage.inputTokens ?? 0, + output: input.usage.outputTokens ?? 0, + reasoning: input.usage?.reasoningTokens ?? 0, + cache: { + write: (input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ?? + // @ts-expect-error + input.metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ?? + 0) as number, + read: input.usage.cachedInputTokens ?? 0, + }, + } + return { + cost: new Decimal(0) + .add(new Decimal(tokens.input).mul(input.model.cost?.input ?? 0).div(1_000_000)) + .add(new Decimal(tokens.output).mul(input.model.cost?.output ?? 0).div(1_000_000)) + .add(new Decimal(tokens.cache.read).mul(input.model.cost?.cache_read ?? 0).div(1_000_000)) + .add(new Decimal(tokens.cache.write).mul(input.model.cost?.cache_write ?? 0).div(1_000_000)) + .toNumber(), + tokens, + } + }, + ) export class BusyError extends Error { constructor(public readonly sessionID: string) { @@ -352,27 +379,30 @@ export namespace Session { } } - export async function initialize(input: { - sessionID: string - modelID: string - providerID: string - messageID: string - }) { - await SessionPrompt.prompt({ - sessionID: input.sessionID, - messageID: input.messageID, - model: { - providerID: input.providerID, - modelID: input.modelID, - }, - parts: [ - { - id: Identifier.ascending("part"), - type: "text", - text: PROMPT_INITIALIZE.replace("${path}", Instance.worktree), + export const initialize = fn( + z.object({ + sessionID: Identifier.schema("session"), + modelID: z.string(), + providerID: z.string(), + messageID: Identifier.schema("message"), + }), + async (input) => { + await SessionPrompt.prompt({ + sessionID: input.sessionID, + messageID: input.messageID, + model: { + providerID: input.providerID, + modelID: input.modelID, }, - ], - }) - await Project.setInitialized(Instance.project.id) - } + parts: [ + { + id: Identifier.ascending("part"), + type: "text", + text: PROMPT_INITIALIZE.replace("${path}", Instance.worktree), + }, + ], + }) + await Project.setInitialized(Instance.project.id) + }, + ) } diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 474843dd9..9ba06f010 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1031,7 +1031,11 @@ export namespace SessionPrompt { break case "finish-step": - const usage = Session.getUsage(input.model, value.usage, value.providerMetadata) + const usage = Session.getUsage({ + model: input.model, + usage: value.usage, + metadata: value.providerMetadata, + }) assistantMsg.cost += usage.cost assistantMsg.tokens = usage.tokens await Session.updatePart({ diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index 5875722f8..302e0cce3 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -26,8 +26,11 @@ export const TaskTool = Tool.define("task", async () => { async execute(params, ctx) { const agent = await Agent.get(params.subagent_type) if (!agent) throw new Error(`Unknown agent type: ${params.subagent_type} is not a valid agent type`) - const session = await Session.create(ctx.sessionID, params.description + ` (@${agent.name} subagent)`) - const msg = await Session.getMessage(ctx.sessionID, ctx.messageID) + const session = await Session.create({ + parentID: ctx.sessionID, + title: params.description + ` (@${agent.name} subagent)`, + }) + const msg = await Session.getMessage({ sessionID: ctx.sessionID, messageID: ctx.messageID }) if (msg.info.role !== "assistant") throw new Error("Not an assistant message") const messageID = Identifier.ascending("message") const parts: Record = {} From 2bf0e42367a0912d876bd37cfb29ae5a1718dc63 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 6 Oct 2025 23:24:07 -0400 Subject: [PATCH 14/22] core: restore bash command security validation to prevent accidental directory traversal The permission validation that prevents commands from accessing paths outside the project directory was accidentally disabled, which could allow commands like 'cd ../' to escape the workspace. This restores the security check that keeps your commands safely contained within your project boundaries. --- packages/opencode/src/tool/bash.ts | 22 ++++++++++------------ packages/opencode/src/tool/test.ts | 4 ++-- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index 1946ada1f..0e1d37ecf 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -3,17 +3,22 @@ import { exec } from "child_process" import { Tool } from "./tool" import DESCRIPTION from "./bash.txt" +import { Permission } from "../permission" +import { Filesystem } from "../util/filesystem" import { lazy } from "../util/lazy" import { Log } from "../util/log" +import { Wildcard } from "../util/wildcard" +import { $ } from "bun" import { Instance } from "../project/instance" +import { Agent } from "../agent/agent" const MAX_OUTPUT_LENGTH = 30_000 const DEFAULT_TIMEOUT = 1 * 60 * 1000 const MAX_TIMEOUT = 10 * 60 * 1000 -export const log = Log.create({ service: "bash-tool" }) +const log = Log.create({ service: "bash-tool" }) -export const parser = lazy(async () => { +const parser = lazy(async () => { try { const { default: Parser } = await import("tree-sitter") const Bash = await import("tree-sitter-bash") @@ -21,10 +26,8 @@ export const parser = lazy(async () => { p.setLanguage(Bash.language as any) return p } catch (e) { - const { Parser, Language } = await import("web-tree-sitter") - const { default: treeWasm } = await import("web-tree-sitter/web-tree-sitter.wasm" as string, { - with: { type: "wasm" }, - }) + const { default: Parser } = await import("web-tree-sitter") + const { default: treeWasm } = await import("web-tree-sitter/tree-sitter.wasm" as string, { with: { type: "wasm" } }) await Parser.init({ locateFile() { return treeWasm @@ -33,7 +36,7 @@ export const parser = lazy(async () => { const { default: bashWasm } = await import("tree-sitter-bash/tree-sitter-bash.wasm" as string, { with: { type: "wasm" }, }) - const bashLanguage = await Language.load(bashWasm) + const bashLanguage = await Parser.Language.load(bashWasm) const p = new Parser() p.setLanguage(bashLanguage) return p @@ -53,11 +56,7 @@ export const BashTool = Tool.define("bash", { }), async execute(params, ctx) { const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT) - /* const tree = await parser().then((p) => p.parse(params.command)) - if (!tree) { - throw new Error("Failed to parse command") - } const permissions = await Agent.get(ctx.agent).then((x) => x.permission.bash) const askPatterns = new Set() @@ -146,7 +145,6 @@ export const BashTool = Tool.define("bash", { }, }) } - */ const process = exec(params.command, { cwd: Instance.directory, diff --git a/packages/opencode/src/tool/test.ts b/packages/opencode/src/tool/test.ts index 14427c73c..81428ba96 100644 --- a/packages/opencode/src/tool/test.ts +++ b/packages/opencode/src/tool/test.ts @@ -6,7 +6,7 @@ const parser = async () => { p.setLanguage(Bash.language as any) return p } catch (e) { - const { Parser, Language } = await import("web-tree-sitter") + const { default: Parser } = await import("web-tree-sitter") const { default: treeWasm } = await import("web-tree-sitter/web-tree-sitter.wasm" as string, { with: { type: "wasm" }, }) @@ -18,7 +18,7 @@ const parser = async () => { const { default: bashWasm } = await import("tree-sitter-bash/tree-sitter-bash.wasm" as string, { with: { type: "wasm" }, }) - const bashLanguage = await Language.load(bashWasm) + const bashLanguage = await Parser.Language.load(bashWasm) const p = new Parser() p.setLanguage(bashLanguage) return p From a20fc2dfdf0b54fddc6cc839be2db99045a52cbd Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 6 Oct 2025 23:25:01 -0400 Subject: [PATCH 15/22] ignore: --- packages/opencode/src/tool/test.ts | 75 ------------------------------ 1 file changed, 75 deletions(-) delete mode 100644 packages/opencode/src/tool/test.ts diff --git a/packages/opencode/src/tool/test.ts b/packages/opencode/src/tool/test.ts deleted file mode 100644 index 81428ba96..000000000 --- a/packages/opencode/src/tool/test.ts +++ /dev/null @@ -1,75 +0,0 @@ -const parser = async () => { - try { - const { default: Parser } = await import("tree-sitter") - const Bash = await import("tree-sitter-bash") - const p = new Parser() - p.setLanguage(Bash.language as any) - return p - } catch (e) { - const { default: Parser } = await import("web-tree-sitter") - const { default: treeWasm } = await import("web-tree-sitter/web-tree-sitter.wasm" as string, { - with: { type: "wasm" }, - }) - await Parser.init({ - locateFile() { - return treeWasm - }, - }) - const { default: bashWasm } = await import("tree-sitter-bash/tree-sitter-bash.wasm" as string, { - with: { type: "wasm" }, - }) - const bashLanguage = await Parser.Language.load(bashWasm) - const p = new Parser() - p.setLanguage(bashLanguage) - return p - } -} - -const sourceCode = `cd --foo foo/bar && echo "hello" && cd ../baz` - -const tree = await parser().then((p) => p.parse(sourceCode)) - -// Function to extract commands and arguments -function extractCommands(node: any): Array<{ command: string; args: string[] }> { - const commands: Array<{ command: string; args: string[] }> = [] - - function traverse(node: any) { - if (node.type === "command") { - const commandNode = node.child(0) - if (commandNode) { - const command = commandNode.text - const args: string[] = [] - - // Extract arguments - for (let i = 1; i < node.childCount; i++) { - const child = node.child(i) - if (child && child.type === "word") { - args.push(child.text) - } - } - - commands.push({ command, args }) - } - } - - // Traverse children - for (let i = 0; i < node.childCount; i++) { - traverse(node.child(i)) - } - } - - traverse(node) - return commands -} - -// Extract and display commands -console.log("Source code: " + sourceCode) -if (!tree) { - throw new Error("Failed to parse command") -} -const commands = extractCommands(tree.rootNode) -console.log("Extracted commands:") -commands.forEach((cmd, index) => { - console.log(`${index + 1}. Command: ${cmd.command}`) - console.log(` Args: [${cmd.args.join(", ")}]`) -}) From e3f9e7785eb8ee94c52a2c5c05532b022b6bee83 Mon Sep 17 00:00:00 2001 From: opencode Date: Tue, 7 Oct 2025 03:32:10 +0000 Subject: [PATCH 16/22] release: v0.14.4 --- bun.lock | 20 +++---- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/console/scripts/package.json | 2 +- packages/desktop/package.json | 2 +- packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/sdk/js/src/gen/sdk.gen.ts | 28 ++++++++++ packages/sdk/js/src/gen/types.gen.ts | 77 +++++++++++++++++++++++++- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 15 files changed, 125 insertions(+), 24 deletions(-) diff --git a/bun.lock b/bun.lock index 173664dce..12855a175 100644 --- a/bun.lock +++ b/bun.lock @@ -28,7 +28,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "0.14.3", + "version": "0.14.4", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -48,7 +48,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "0.14.3", + "version": "0.14.4", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -68,7 +68,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "0.14.3", + "version": "0.14.4", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -84,7 +84,7 @@ }, "packages/console/scripts": { "name": "@opencode-ai/console-scripts", - "version": "0.14.3", + "version": "0.14.4", "dependencies": { "@opencode-ai/console-core": "workspace:*", "tsx": "4.20.5", @@ -96,7 +96,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "0.14.3", + "version": "0.14.4", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -130,7 +130,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "0.14.3", + "version": "0.14.4", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "22.0.0", @@ -145,7 +145,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "0.14.3", + "version": "0.14.4", "bin": { "opencode": "./bin/opencode", }, @@ -198,7 +198,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "0.14.3", + "version": "0.14.4", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -210,7 +210,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "0.14.3", + "version": "0.14.4", "dependencies": { "@hey-api/openapi-ts": "0.81.0", }, @@ -222,7 +222,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "0.14.3", + "version": "0.14.4", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 875129ea3..8fb644701 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -7,7 +7,7 @@ "dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev", "build": "vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json", "start": "vinxi start", - "version": "0.14.3" + "version": "0.14.4" }, "dependencies": { "@ibm/plex": "6.4.1", diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 72f15529f..76fc56a2e 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "0.14.3", + "version": "0.14.4", "private": true, "type": "module", "dependencies": { diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 490763b01..0ce023182 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "0.14.3", + "version": "0.14.4", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 51106f908..6e0e0128d 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-mail", - "version": "0.14.3", + "version": "0.14.4", "private": true, "type": "module", "dependencies": { diff --git a/packages/console/scripts/package.json b/packages/console/scripts/package.json index 24af8a674..086ce9915 100644 --- a/packages/console/scripts/package.json +++ b/packages/console/scripts/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-scripts", - "version": "0.14.3", + "version": "0.14.4", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 4e1279fd5..96f234e6d 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/desktop", - "version": "0.14.3", + "version": "0.14.4", "description": "", "type": "module", "scripts": { diff --git a/packages/function/package.json b/packages/function/package.json index 26589a290..3019f89a8 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "0.14.3", + "version": "0.14.4", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 179436346..c613b7927 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "0.14.3", + "version": "0.14.4", "name": "opencode", "type": "module", "private": true, diff --git a/packages/plugin/package.json b/packages/plugin/package.json index a8020354f..cc1c2ccce 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "0.14.3", + "version": "0.14.4", "type": "module", "scripts": { "typecheck": "tsc --noEmit", diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 2e13ace51..67007bcf6 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "0.14.3", + "version": "0.14.4", "type": "module", "scripts": { "typecheck": "tsc --noEmit", diff --git a/packages/sdk/js/src/gen/sdk.gen.ts b/packages/sdk/js/src/gen/sdk.gen.ts index ab49e18d2..aea90daec 100644 --- a/packages/sdk/js/src/gen/sdk.gen.ts +++ b/packages/sdk/js/src/gen/sdk.gen.ts @@ -32,8 +32,12 @@ import type { SessionUpdateResponses, SessionChildrenData, SessionChildrenResponses, + SessionTodoData, + SessionTodoResponses, SessionInitData, SessionInitResponses, + SessionForkData, + SessionForkResponses, SessionAbortData, SessionAbortResponses, SessionUnshareData, @@ -292,6 +296,16 @@ class Session extends _HeyApiClient { }) } + /** + * Get the todo list for a session + */ + public todo(options: Options) { + return (options.client ?? this._client).get({ + url: "/session/{id}/todo", + ...options, + }) + } + /** * Analyze the app and create an AGENTS.md file */ @@ -306,6 +320,20 @@ class Session extends _HeyApiClient { }) } + /** + * Fork an existing session at a specific message + */ + public fork(options: Options) { + return (options.client ?? this._client).post({ + url: "/session/{id}/fork", + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers, + }, + }) + } + /** * Abort a session */ diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index b62bc096f..c255cc69c 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -550,6 +550,25 @@ export type Session = { } } +export type Todo = { + /** + * Brief description of the task + */ + content: string + /** + * Current status of the task: pending, in_progress, completed, cancelled + */ + status: string + /** + * Priority level of the task: high, medium, low + */ + priority: string + /** + * Unique identifier for the todo item + */ + id: string +} + export type UserMessage = { id: string sessionID: string @@ -1093,6 +1112,14 @@ export type EventFileWatcherUpdated = { } } +export type EventTodoUpdated = { + type: "todo.updated" + properties: { + sessionID: string + todos: Array + } +} + export type EventSessionIdle = { type: "session.idle" properties: { @@ -1148,6 +1175,7 @@ export type Event = | EventPermissionReplied | EventFileEdited | EventFileWatcherUpdated + | EventTodoUpdated | EventSessionIdle | EventSessionUpdated | EventSessionDeleted @@ -1440,11 +1468,34 @@ export type SessionChildrenResponses = { export type SessionChildrenResponse = SessionChildrenResponses[keyof SessionChildrenResponses] +export type SessionTodoData = { + body?: never + path: { + /** + * Session ID + */ + id: string + } + query?: { + directory?: string + } + url: "/session/{id}/todo" +} + +export type SessionTodoResponses = { + /** + * Todo list + */ + 200: Array +} + +export type SessionTodoResponse = SessionTodoResponses[keyof SessionTodoResponses] + export type SessionInitData = { body?: { - messageID: string - providerID: string modelID: string + providerID: string + messageID: string } path: { /** @@ -1467,6 +1518,28 @@ export type SessionInitResponses = { export type SessionInitResponse = SessionInitResponses[keyof SessionInitResponses] +export type SessionForkData = { + body?: { + messageID?: string + } + path: { + id: string + } + query?: { + directory?: string + } + url: "/session/{id}/fork" +} + +export type SessionForkResponses = { + /** + * 200 + */ + 200: Session +} + +export type SessionForkResponse = SessionForkResponses[keyof SessionForkResponses] + export type SessionAbortData = { body?: never path: { diff --git a/packages/web/package.json b/packages/web/package.json index ec0ced0ca..e7497f7bc 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/web", "type": "module", - "version": "0.14.3", + "version": "0.14.4", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index fbafdf783..c4cca2880 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "0.14.3", + "version": "0.14.4", "publisher": "sst-dev", "repository": { "type": "git", From 4f33594b99b015000d76ee2d4e1353b717c5920e Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 6 Oct 2025 22:24:13 -0400 Subject: [PATCH 17/22] wip: zen --- bun.lock | 5 ----- packages/console/scripts/package.json | 13 ++++--------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/bun.lock b/bun.lock index 12855a175..0853c62f8 100644 --- a/bun.lock +++ b/bun.lock @@ -87,11 +87,6 @@ "version": "0.14.4", "dependencies": { "@opencode-ai/console-core": "workspace:*", - "tsx": "4.20.5", - }, - "devDependencies": { - "@types/node": "catalog:", - "typescript": "catalog:", }, }, "packages/desktop": { diff --git a/packages/console/scripts/package.json b/packages/console/scripts/package.json index 086ce9915..b7633e1e0 100644 --- a/packages/console/scripts/package.json +++ b/packages/console/scripts/package.json @@ -5,16 +5,11 @@ "private": true, "type": "module", "scripts": { - "shell": "sst shell -- bun tsx", - "shell-dev": "sst shell --stage dev -- bun tsx", - "shell-prod": "sst shell --stage production -- bun tsx" + "shell": "sst shell -- bun", + "shell-dev": "sst shell --stage dev -- bun", + "shell-prod": "sst shell --stage production -- bun" }, "dependencies": { - "@opencode-ai/console-core": "workspace:*", - "tsx": "4.20.5" - }, - "devDependencies": { - "@types/node": "catalog:", - "typescript": "catalog:" + "@opencode-ai/console-core": "workspace:*" } } From 0534bc0c0920dd1cb2503caeb9de6c4bbd5dce05 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 6 Oct 2025 23:57:52 -0400 Subject: [PATCH 18/22] wip: zen --- .../core/migrations/0028_careful_cerise.sql | 1 + .../core/migrations/meta/0028_snapshot.json | 709 ++++++++++++++++++ .../core/migrations/meta/_journal.json | 7 + .../console/core/src/schema/workspace.sql.ts | 2 +- packages/console/core/src/workspace.ts | 11 +- 5 files changed, 728 insertions(+), 2 deletions(-) create mode 100644 packages/console/core/migrations/0028_careful_cerise.sql create mode 100644 packages/console/core/migrations/meta/0028_snapshot.json diff --git a/packages/console/core/migrations/0028_careful_cerise.sql b/packages/console/core/migrations/0028_careful_cerise.sql new file mode 100644 index 000000000..ba2bc0203 --- /dev/null +++ b/packages/console/core/migrations/0028_careful_cerise.sql @@ -0,0 +1 @@ +ALTER TABLE `workspace` MODIFY COLUMN `name` varchar(255) NOT NULL; \ No newline at end of file diff --git a/packages/console/core/migrations/meta/0028_snapshot.json b/packages/console/core/migrations/meta/0028_snapshot.json new file mode 100644 index 000000000..8242ae52d --- /dev/null +++ b/packages/console/core/migrations/meta/0028_snapshot.json @@ -0,0 +1,709 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "a331e38c-c2e3-406d-a1ff-b0af7229cd85", + "prevId": "05e873f6-1556-4bcb-8e19-14971e37610a", + "tables": { + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "email": { + "name": "email", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "billing": { + "name": "billing", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_id": { + "name": "customer_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_method_id": { + "name": "payment_method_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_method_last4": { + "name": "payment_method_last4", + "type": "varchar(4)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "balance": { + "name": "balance", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "monthly_limit": { + "name": "monthly_limit", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "monthly_usage": { + "name": "monthly_usage", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_monthly_usage_updated": { + "name": "time_monthly_usage_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reload": { + "name": "reload", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reload_error": { + "name": "reload_error", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_reload_error": { + "name": "time_reload_error", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_reload_locked_till": { + "name": "time_reload_locked_till", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "global_customer_id": { + "name": "global_customer_id", + "columns": [ + "customer_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "billing_workspace_id_id_pk": { + "name": "billing_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "payment": { + "name": "payment", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_id": { + "name": "customer_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoice_id": { + "name": "invoice_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_id": { + "name": "payment_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "amount": { + "name": "amount", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_refunded": { + "name": "time_refunded", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "payment_workspace_id_id_pk": { + "name": "payment_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "usage": { + "name": "usage", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_write_5m_tokens": { + "name": "cache_write_5m_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_write_1h_tokens": { + "name": "cache_write_1h_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost": { + "name": "cost", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "usage_workspace_id_id_pk": { + "name": "usage_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "key": { + "name": "key", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_used": { + "name": "time_used", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "global_key": { + "name": "global_key", + "columns": [ + "key" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "key_workspace_id_id_pk": { + "name": "key_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_seen": { + "name": "time_seen", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "enum('admin','member')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_account_id": { + "name": "user_account_id", + "columns": [ + "workspace_id", + "account_id" + ], + "isUnique": true + }, + "user_email": { + "name": "user_email", + "columns": [ + "workspace_id", + "email" + ], + "isUnique": true + }, + "global_account_id": { + "name": "global_account_id", + "columns": [ + "account_id" + ], + "isUnique": false + }, + "global_email": { + "name": "global_email", + "columns": [ + "email" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "user_workspace_id_id_pk": { + "name": "user_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "workspace": { + "name": "workspace", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "slug": { + "name": "slug", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "workspace_id": { + "name": "workspace_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/console/core/migrations/meta/_journal.json b/packages/console/core/migrations/meta/_journal.json index 58370d4e4..eaa28ddf6 100644 --- a/packages/console/core/migrations/meta/_journal.json +++ b/packages/console/core/migrations/meta/_journal.json @@ -197,6 +197,13 @@ "when": 1759553466608, "tag": "0027_hot_wong", "breakpoints": true + }, + { + "idx": 28, + "version": "5", + "when": 1759805025276, + "tag": "0028_careful_cerise", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/console/core/src/schema/workspace.sql.ts b/packages/console/core/src/schema/workspace.sql.ts index 979255428..269b62a2a 100644 --- a/packages/console/core/src/schema/workspace.sql.ts +++ b/packages/console/core/src/schema/workspace.sql.ts @@ -6,7 +6,7 @@ export const WorkspaceTable = mysqlTable( { id: ulid("id").notNull().primaryKey(), slug: varchar("slug", { length: 255 }), - name: varchar("name", { length: 255 }), + name: varchar("name", { length: 255 }).notNull(), ...timestamps, }, (table) => [uniqueIndex("slug").on(table.slug)], diff --git a/packages/console/core/src/workspace.ts b/packages/console/core/src/workspace.ts index 32f5bd36c..7a742e896 100644 --- a/packages/console/core/src/workspace.ts +++ b/packages/console/core/src/workspace.ts @@ -7,7 +7,7 @@ import { UserTable } from "./schema/user.sql" import { BillingTable } from "./schema/billing.sql" import { WorkspaceTable } from "./schema/workspace.sql" import { Key } from "./key" -import { eq } from "drizzle-orm" +import { eq, sql } from "drizzle-orm" export namespace Workspace { export const create = fn( @@ -63,4 +63,13 @@ export namespace Workspace { ) }, ) + + export const remove = fn(z.void(), async () => { + await Database.use((tx) => + tx + .update(WorkspaceTable) + .set({ timeDeleted: sql`now()` }) + .where(eq(WorkspaceTable.id, Actor.workspace())), + ) + }) } From 06c42093c84798f7740745ead50beb0d63d6e731 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Mon, 6 Oct 2025 23:24:00 -0500 Subject: [PATCH 19/22] tweak: grep tool to handle single file better (#3004) --- packages/opencode/src/tool/grep.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/opencode/src/tool/grep.ts b/packages/opencode/src/tool/grep.ts index 3faeb6fb4..cc654e339 100644 --- a/packages/opencode/src/tool/grep.ts +++ b/packages/opencode/src/tool/grep.ts @@ -20,12 +20,11 @@ export const GrepTool = Tool.define("grep", { const searchPath = params.path || Instance.directory const rgPath = await Ripgrep.filepath() - const args = ["-n", params.pattern] + const args = ["-nH", "--field-match-separator=|", params.pattern] if (params.include) { args.push("--glob", params.include) } args.push(searchPath) - args.push("--field-match-separator=|") const proc = Bun.spawn([rgPath, ...args], { stdout: "pipe", From cd528ae78fd20c8eaa84933bf8afd3e99f4ca8f5 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Tue, 7 Oct 2025 00:45:46 -0500 Subject: [PATCH 20/22] fix: mcp error (#3006) --- packages/opencode/src/session/prompt.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 9ba06f010..b65309d9e 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -528,6 +528,8 @@ export namespace SessionPrompt { ) return { + title: "", + metadata: {}, output, } } From 27c211ef869cae9d18b3fefc36ba787af75ebde0 Mon Sep 17 00:00:00 2001 From: opencode Date: Tue, 7 Oct 2025 06:21:31 +0000 Subject: [PATCH 21/22] release: v0.14.5 --- bun.lock | 20 ++++++++++---------- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/console/scripts/package.json | 2 +- packages/desktop/package.json | 2 +- packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 13 files changed, 22 insertions(+), 22 deletions(-) diff --git a/bun.lock b/bun.lock index 0853c62f8..3022f5b8e 100644 --- a/bun.lock +++ b/bun.lock @@ -28,7 +28,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "0.14.4", + "version": "0.14.5", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -48,7 +48,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "0.14.4", + "version": "0.14.5", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -68,7 +68,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "0.14.4", + "version": "0.14.5", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -84,14 +84,14 @@ }, "packages/console/scripts": { "name": "@opencode-ai/console-scripts", - "version": "0.14.4", + "version": "0.14.5", "dependencies": { "@opencode-ai/console-core": "workspace:*", }, }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "0.14.4", + "version": "0.14.5", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -125,7 +125,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "0.14.4", + "version": "0.14.5", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "22.0.0", @@ -140,7 +140,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "0.14.4", + "version": "0.14.5", "bin": { "opencode": "./bin/opencode", }, @@ -193,7 +193,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "0.14.4", + "version": "0.14.5", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -205,7 +205,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "0.14.4", + "version": "0.14.5", "dependencies": { "@hey-api/openapi-ts": "0.81.0", }, @@ -217,7 +217,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "0.14.4", + "version": "0.14.5", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 8fb644701..30a0f566a 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -7,7 +7,7 @@ "dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev", "build": "vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json", "start": "vinxi start", - "version": "0.14.4" + "version": "0.14.5" }, "dependencies": { "@ibm/plex": "6.4.1", diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 76fc56a2e..74cbec0a8 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "0.14.4", + "version": "0.14.5", "private": true, "type": "module", "dependencies": { diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 0ce023182..5fd8cef6c 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "0.14.4", + "version": "0.14.5", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 6e0e0128d..367e5891f 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-mail", - "version": "0.14.4", + "version": "0.14.5", "private": true, "type": "module", "dependencies": { diff --git a/packages/console/scripts/package.json b/packages/console/scripts/package.json index b7633e1e0..220589e6f 100644 --- a/packages/console/scripts/package.json +++ b/packages/console/scripts/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-scripts", - "version": "0.14.4", + "version": "0.14.5", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 96f234e6d..2ec37e398 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/desktop", - "version": "0.14.4", + "version": "0.14.5", "description": "", "type": "module", "scripts": { diff --git a/packages/function/package.json b/packages/function/package.json index 3019f89a8..6866ab598 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "0.14.4", + "version": "0.14.5", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index c613b7927..16361d179 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "0.14.4", + "version": "0.14.5", "name": "opencode", "type": "module", "private": true, diff --git a/packages/plugin/package.json b/packages/plugin/package.json index cc1c2ccce..937443d2e 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "0.14.4", + "version": "0.14.5", "type": "module", "scripts": { "typecheck": "tsc --noEmit", diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 67007bcf6..40d78be6b 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "0.14.4", + "version": "0.14.5", "type": "module", "scripts": { "typecheck": "tsc --noEmit", diff --git a/packages/web/package.json b/packages/web/package.json index e7497f7bc..16ecc5c22 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/web", "type": "module", - "version": "0.14.4", + "version": "0.14.5", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index c4cca2880..8a38631dc 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "0.14.4", + "version": "0.14.5", "publisher": "sst-dev", "repository": { "type": "git", From a440e09cfe57b955c3cbbdcc43eaf84d33d9c6dc Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 7 Oct 2025 04:04:19 -0400 Subject: [PATCH 22/22] core: improve MCP reliability and add status monitoring - Added 5-second timeout to MCP client verification to prevent hanging connections - New GET /mcp endpoint to monitor server connection status - Automatically removes unresponsive MCP clients during initialization --- packages/opencode/src/mcp/index.ts | 31 +++++++++++++++++++++++++- packages/opencode/src/server/server.ts | 21 +++++++++++++++++ packages/sdk/js/src/gen/sdk.gen.ts | 15 +++++++++++++ packages/sdk/js/src/gen/types.gen.ts | 16 +++++++++++++ 4 files changed, 82 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index dc90dfe5f..dc5bb8b86 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -9,6 +9,7 @@ import z from "zod/v4" import { Session } from "../session" import { Bus } from "../bus" import { Instance } from "../project/instance" +import { withTimeout } from "@/util/timeout" export namespace MCP { const log = Log.create({ service: "mcp" }) @@ -20,11 +21,13 @@ export namespace MCP { }), ) + type MCPClient = Awaited> + const state = Instance.state( async () => { const cfg = await Config.get() const clients: { - [name: string]: Awaited> + [name: string]: MCPClient } = {} for (const [key, mcp] of Object.entries(cfg.mcp ?? {})) { if (mcp.enabled === false) { @@ -128,8 +131,17 @@ export namespace MCP { } } + for (const [key, client] of Object.entries(clients)) { + const result = await withTimeout(client.tools(), 5000).catch(() => {}) + if (!result) { + log.warn("mcp client verification failed, removing client", { key }) + delete clients[key] + } + } + return { clients, + config: cfg.mcp ?? {}, } }, async (state) => { @@ -139,6 +151,23 @@ export namespace MCP { }, ) + export async function status() { + return state().then((state) => { + const result: Record = {} + for (const [key, client] of Object.entries(state.config)) { + if (client.enabled === false) { + result[key] = "disabled" + continue + } + if (state.clients[key]) { + result[key] = "connected" + } + result[key] = "failed" + } + return result + }) + } + export async function clients() { return state().then((state) => state.clients) } diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 26cbb5d71..ee04b1f92 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -31,6 +31,7 @@ import { SessionRevert } from "../session/revert" import { lazy } from "../util/lazy" import { Todo } from "../session/todo" import { InstanceBootstrap } from "../project/bootstrap" +import { MCP } from "../mcp" const ERRORS = { 400: { @@ -1183,6 +1184,26 @@ export namespace Server { return c.json(modes) }, ) + .get( + "/mcp", + describeRoute({ + description: "Get MCP server status", + operationId: "mcp.status", + responses: { + 200: { + description: "MCP server status", + content: { + "application/json": { + schema: resolver(z.any()), + }, + }, + }, + }, + }), + async (c) => { + return c.json(await MCP.status()) + }, + ) .post( "/tui/append-prompt", describeRoute({ diff --git a/packages/sdk/js/src/gen/sdk.gen.ts b/packages/sdk/js/src/gen/sdk.gen.ts index aea90daec..6bb1e115f 100644 --- a/packages/sdk/js/src/gen/sdk.gen.ts +++ b/packages/sdk/js/src/gen/sdk.gen.ts @@ -82,6 +82,8 @@ import type { AppLogResponses, AppAgentsData, AppAgentsResponses, + McpStatusData, + McpStatusResponses, TuiAppendPromptData, TuiAppendPromptResponses, TuiOpenHelpData, @@ -567,6 +569,18 @@ class App extends _HeyApiClient { } } +class Mcp extends _HeyApiClient { + /** + * Get MCP server status + */ + public status(options?: Options) { + return (options?.client ?? this._client).get({ + url: "/mcp", + ...options, + }) + } +} + class Tui extends _HeyApiClient { /** * Append prompt to the TUI @@ -724,6 +738,7 @@ export class OpencodeClient extends _HeyApiClient { find = new Find({ client: this._client }) file = new File({ client: this._client }) app = new App({ client: this._client }) + mcp = new Mcp({ client: this._client }) tui = new Tui({ client: this._client }) auth = new Auth({ client: this._client }) event = new Event({ client: this._client }) diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index c255cc69c..cc94c1f1f 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -2070,6 +2070,22 @@ export type AppAgentsResponses = { export type AppAgentsResponse = AppAgentsResponses[keyof AppAgentsResponses] +export type McpStatusData = { + body?: never + path?: never + query?: { + directory?: string + } + url: "/mcp" +} + +export type McpStatusResponses = { + /** + * MCP server status + */ + 200: unknown +} + export type TuiAppendPromptData = { body?: { text: string