diff --git a/.github/workflows/close-issues.yml b/.github/workflows/close-issues.yml
new file mode 100644
index 000000000..04b6ae7ac
--- /dev/null
+++ b/.github/workflows/close-issues.yml
@@ -0,0 +1,24 @@
+name: close-issues
+
+on:
+ schedule:
+ - cron: "0 2 * * *" # Daily at 2:00 AM
+ workflow_dispatch:
+
+jobs:
+ close:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ issues: write
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: latest
+
+ - name: Close stale issues
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ run: bun script/github/close-issues.ts
diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml
deleted file mode 100644
index a4b8583f9..000000000
--- a/.github/workflows/stale-issues.yml
+++ /dev/null
@@ -1,33 +0,0 @@
-name: stale-issues
-
-on:
- schedule:
- - cron: "30 1 * * *" # Daily at 1:30 AM
- workflow_dispatch:
-
-env:
- DAYS_BEFORE_STALE: 90
- DAYS_BEFORE_CLOSE: 7
-
-jobs:
- stale:
- runs-on: ubuntu-latest
- permissions:
- issues: write
- steps:
- - uses: actions/stale@v10
- with:
- days-before-stale: ${{ env.DAYS_BEFORE_STALE }}
- days-before-close: ${{ env.DAYS_BEFORE_CLOSE }}
- stale-issue-label: "stale"
- close-issue-message: |
- [automated] Closing due to ${{ env.DAYS_BEFORE_STALE }}+ days of inactivity.
-
- Feel free to reopen if you still need this!
- stale-issue-message: |
- [automated] This issue has had no activity for ${{ env.DAYS_BEFORE_STALE }} days.
-
- It will be closed in ${{ env.DAYS_BEFORE_CLOSE }} days if there's no new activity.
- remove-stale-when-updated: true
- exempt-issue-labels: "pinned,security,feature-request,on-hold"
- start-date: "2025-12-27"
diff --git a/bun.lock b/bun.lock
index 0b00d830f..82a2c2a8a 100644
--- a/bun.lock
+++ b/bun.lock
@@ -330,7 +330,7 @@
"@ff-labs/fff-node": "0.4.2",
"@hono/standard-validator": "0.1.5",
"@hono/zod-validator": "catalog:",
- "@modelcontextprotocol/sdk": "1.25.2",
+ "@modelcontextprotocol/sdk": "1.27.1",
"@octokit/graphql": "9.0.2",
"@octokit/rest": "catalog:",
"@openauthjs/openauth": "catalog:",
@@ -1343,7 +1343,7 @@
"@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="],
- "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.2", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww=="],
+ "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="],
"@motionone/animation": ["@motionone/animation@10.18.0", "", { "dependencies": { "@motionone/easing": "^10.18.0", "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw=="],
@@ -2927,7 +2927,7 @@
"express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="],
- "express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="],
+ "express-rate-limit": ["express-rate-limit@8.3.1", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw=="],
"expressive-code": ["expressive-code@0.41.7", "", { "dependencies": { "@expressive-code/core": "^0.41.7", "@expressive-code/plugin-frames": "^0.41.7", "@expressive-code/plugin-shiki": "^0.41.7", "@expressive-code/plugin-text-markers": "^0.41.7" } }, "sha512-2wZjC8OQ3TaVEMcBtYY4Va3lo6J+Ai9jf3d4dbhURMJcU4Pbqe6EcHe424MIZI0VHUA1bR6xdpoHYi3yxokWqA=="],
@@ -5169,6 +5169,8 @@
"@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
+ "@modelcontextprotocol/sdk/hono": ["hono@4.12.9", "", {}, "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA=="],
+
"@modelcontextprotocol/sdk/jose": ["jose@6.2.1", "", {}, "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw=="],
"@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
@@ -6351,6 +6353,8 @@
"opencontrol/@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
+ "opencontrol/@modelcontextprotocol/sdk/express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="],
+
"opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="],
"opencontrol/@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
diff --git a/packages/app/src/context/command-keybind.test.ts b/packages/app/src/context/command-keybind.test.ts
index d804195c4..c8e2dbb5d 100644
--- a/packages/app/src/context/command-keybind.test.ts
+++ b/packages/app/src/context/command-keybind.test.ts
@@ -32,6 +32,25 @@ describe("command keybind helpers", () => {
expect(matchKeybind(keybinds, new KeyboardEvent("keydown", { key: ",", ctrlKey: true, altKey: true }))).toBe(false)
})
+ test("matchKeybind supports bracket keys", () => {
+ const keybinds = parseKeybind("mod+alt+[, mod+alt+]")
+ const prev = keybinds[0]
+ const next = keybinds[1]
+
+ expect(
+ matchKeybind(
+ keybinds,
+ new KeyboardEvent("keydown", { key: "[", ctrlKey: prev?.ctrl, metaKey: prev?.meta, altKey: true }),
+ ),
+ ).toBe(true)
+ expect(
+ matchKeybind(
+ keybinds,
+ new KeyboardEvent("keydown", { key: "]", ctrlKey: next?.ctrl, metaKey: next?.meta, altKey: true }),
+ ),
+ ).toBe(true)
+ })
+
test("formatKeybind returns human readable output", () => {
const display = formatKeybind("ctrl+alt+arrowup")
diff --git a/packages/app/src/entry.tsx b/packages/app/src/entry.tsx
index da22c5552..b5cbed6e7 100644
--- a/packages/app/src/entry.tsx
+++ b/packages/app/src/entry.tsx
@@ -97,15 +97,10 @@ if (!(root instanceof HTMLElement) && import.meta.env.DEV) {
throw new Error(getRootNotFoundError())
}
-const localUrl = () =>
- `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`
-
-const isLocalHost = () => ["localhost", "127.0.0.1", "0.0.0.0"].includes(location.hostname)
-
const getCurrentUrl = () => {
- if (location.hostname.includes("opencode.ai")) return localUrl()
- if (import.meta.env.DEV) return localUrl()
- if (isLocalHost()) return localUrl()
+ if (location.hostname.includes("opencode.ai")) return "http://localhost:4096"
+ if (import.meta.env.DEV)
+ return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`
return location.origin
}
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx
index 731f0fe5b..b5a96110f 100644
--- a/packages/app/src/pages/layout.tsx
+++ b/packages/app/src/pages/layout.tsx
@@ -965,6 +965,8 @@ export default function Layout(props: ParentProps) {
: projects[(index + offset + projects.length) % projects.length]
if (!target) return
+ // warm up child store to prevent flicker
+ globalSync.child(target.worktree)
openProject(target.worktree)
}
diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx
index 5fef41a55..33437ce9c 100644
--- a/packages/app/src/pages/session/message-timeline.tsx
+++ b/packages/app/src/pages/session/message-timeline.tsx
@@ -896,7 +896,8 @@ export function MessageTimeline(props: {
{
id: "message.previous",
title: language.t("command.message.previous"),
description: language.t("command.message.previous.description"),
- keybind: "mod+arrowup",
+ keybind: "mod+alt+[",
disabled: !params.id,
onSelect: () => navigateMessageByOffset(-1),
}),
@@ -341,7 +341,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
id: "message.next",
title: language.t("command.message.next"),
description: language.t("command.message.next.description"),
- keybind: "mod+arrowdown",
+ keybind: "mod+alt+]",
disabled: !params.id,
onSelect: () => navigateMessageByOffset(1),
}),
diff --git a/packages/desktop-electron/src/main/ipc.ts b/packages/desktop-electron/src/main/ipc.ts
index 543f857a5..d2cfc2524 100644
--- a/packages/desktop-electron/src/main/ipc.ts
+++ b/packages/desktop-electron/src/main/ipc.ts
@@ -88,7 +88,7 @@ export function registerIpcHandlers(deps: Deps) {
"open-directory-picker",
async (_event: IpcMainInvokeEvent, opts?: { multiple?: boolean; title?: string; defaultPath?: string }) => {
const result = await dialog.showOpenDialog({
- properties: ["openDirectory", ...(opts?.multiple ? ["multiSelections" as const] : [])],
+ properties: ["openDirectory", ...(opts?.multiple ? ["multiSelections" as const] : []), "createDirectory"],
title: opts?.title ?? "Choose a folder",
defaultPath: opts?.defaultPath,
})
diff --git a/packages/desktop-electron/src/main/store.ts b/packages/desktop-electron/src/main/store.ts
index fa1c5682e..cf2d25b11 100644
--- a/packages/desktop-electron/src/main/store.ts
+++ b/packages/desktop-electron/src/main/store.ts
@@ -7,7 +7,7 @@ const cache = new Map
()
export function getStore(name = SETTINGS_STORE) {
const cached = cache.get(name)
if (cached) return cached
- const next = new Store({ name })
+ const next = new Store({ name, fileExtension: "" })
cache.set(name, next)
return next
}
diff --git a/packages/opencode/migration/20260323234822_events/migration.sql b/packages/opencode/migration/20260323234822_events/migration.sql
new file mode 100644
index 000000000..b0fe7e4e6
--- /dev/null
+++ b/packages/opencode/migration/20260323234822_events/migration.sql
@@ -0,0 +1,13 @@
+CREATE TABLE `event_sequence` (
+ `aggregate_id` text PRIMARY KEY,
+ `seq` integer NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE `event` (
+ `id` text PRIMARY KEY,
+ `aggregate_id` text NOT NULL,
+ `seq` integer NOT NULL,
+ `type` text NOT NULL,
+ `data` text NOT NULL,
+ CONSTRAINT `fk_event_aggregate_id_event_sequence_aggregate_id_fk` FOREIGN KEY (`aggregate_id`) REFERENCES `event_sequence`(`aggregate_id`) ON DELETE CASCADE
+);
diff --git a/packages/opencode/migration/20260323234822_events/snapshot.json b/packages/opencode/migration/20260323234822_events/snapshot.json
new file mode 100644
index 000000000..07519aab7
--- /dev/null
+++ b/packages/opencode/migration/20260323234822_events/snapshot.json
@@ -0,0 +1,1271 @@
+{
+ "version": "7",
+ "dialect": "sqlite",
+ "id": "f13dfa58-7fb4-47a2-8f6b-dc70258e14ed",
+ "prevIds": ["37e1554d-af4c-43f2-aa7c-307fb49a315e"],
+ "ddl": [
+ {
+ "name": "account_state",
+ "entityType": "tables"
+ },
+ {
+ "name": "account",
+ "entityType": "tables"
+ },
+ {
+ "name": "control_account",
+ "entityType": "tables"
+ },
+ {
+ "name": "workspace",
+ "entityType": "tables"
+ },
+ {
+ "name": "project",
+ "entityType": "tables"
+ },
+ {
+ "name": "message",
+ "entityType": "tables"
+ },
+ {
+ "name": "part",
+ "entityType": "tables"
+ },
+ {
+ "name": "permission",
+ "entityType": "tables"
+ },
+ {
+ "name": "session",
+ "entityType": "tables"
+ },
+ {
+ "name": "todo",
+ "entityType": "tables"
+ },
+ {
+ "name": "session_share",
+ "entityType": "tables"
+ },
+ {
+ "name": "event_sequence",
+ "entityType": "tables"
+ },
+ {
+ "name": "event",
+ "entityType": "tables"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "account_state"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "active_account_id",
+ "entityType": "columns",
+ "table": "account_state"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "active_org_id",
+ "entityType": "columns",
+ "table": "account_state"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "email",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "url",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "access_token",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "refresh_token",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "token_expiry",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_created",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_updated",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "email",
+ "entityType": "columns",
+ "table": "control_account"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "url",
+ "entityType": "columns",
+ "table": "control_account"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "access_token",
+ "entityType": "columns",
+ "table": "control_account"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "refresh_token",
+ "entityType": "columns",
+ "table": "control_account"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "token_expiry",
+ "entityType": "columns",
+ "table": "control_account"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "active",
+ "entityType": "columns",
+ "table": "control_account"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_created",
+ "entityType": "columns",
+ "table": "control_account"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_updated",
+ "entityType": "columns",
+ "table": "control_account"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "workspace"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "type",
+ "entityType": "columns",
+ "table": "workspace"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "branch",
+ "entityType": "columns",
+ "table": "workspace"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "name",
+ "entityType": "columns",
+ "table": "workspace"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "directory",
+ "entityType": "columns",
+ "table": "workspace"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "extra",
+ "entityType": "columns",
+ "table": "workspace"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "project_id",
+ "entityType": "columns",
+ "table": "workspace"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "project"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "worktree",
+ "entityType": "columns",
+ "table": "project"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "vcs",
+ "entityType": "columns",
+ "table": "project"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "name",
+ "entityType": "columns",
+ "table": "project"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "icon_url",
+ "entityType": "columns",
+ "table": "project"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "icon_color",
+ "entityType": "columns",
+ "table": "project"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_created",
+ "entityType": "columns",
+ "table": "project"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_updated",
+ "entityType": "columns",
+ "table": "project"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_initialized",
+ "entityType": "columns",
+ "table": "project"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "sandboxes",
+ "entityType": "columns",
+ "table": "project"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "commands",
+ "entityType": "columns",
+ "table": "project"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "message"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "session_id",
+ "entityType": "columns",
+ "table": "message"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_created",
+ "entityType": "columns",
+ "table": "message"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_updated",
+ "entityType": "columns",
+ "table": "message"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "data",
+ "entityType": "columns",
+ "table": "message"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "part"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "message_id",
+ "entityType": "columns",
+ "table": "part"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "session_id",
+ "entityType": "columns",
+ "table": "part"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_created",
+ "entityType": "columns",
+ "table": "part"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_updated",
+ "entityType": "columns",
+ "table": "part"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "data",
+ "entityType": "columns",
+ "table": "part"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "project_id",
+ "entityType": "columns",
+ "table": "permission"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_created",
+ "entityType": "columns",
+ "table": "permission"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_updated",
+ "entityType": "columns",
+ "table": "permission"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "data",
+ "entityType": "columns",
+ "table": "permission"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "project_id",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "workspace_id",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "parent_id",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "slug",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "directory",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "title",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "version",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "share_url",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "summary_additions",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "summary_deletions",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "summary_files",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "summary_diffs",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "revert",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "permission",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_created",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_updated",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_compacting",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_archived",
+ "entityType": "columns",
+ "table": "session"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "session_id",
+ "entityType": "columns",
+ "table": "todo"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "content",
+ "entityType": "columns",
+ "table": "todo"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "status",
+ "entityType": "columns",
+ "table": "todo"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "priority",
+ "entityType": "columns",
+ "table": "todo"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "position",
+ "entityType": "columns",
+ "table": "todo"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_created",
+ "entityType": "columns",
+ "table": "todo"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_updated",
+ "entityType": "columns",
+ "table": "todo"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "session_id",
+ "entityType": "columns",
+ "table": "session_share"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "session_share"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "secret",
+ "entityType": "columns",
+ "table": "session_share"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "url",
+ "entityType": "columns",
+ "table": "session_share"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_created",
+ "entityType": "columns",
+ "table": "session_share"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "time_updated",
+ "entityType": "columns",
+ "table": "session_share"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "aggregate_id",
+ "entityType": "columns",
+ "table": "event_sequence"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "seq",
+ "entityType": "columns",
+ "table": "event_sequence"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "event"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "aggregate_id",
+ "entityType": "columns",
+ "table": "event"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "seq",
+ "entityType": "columns",
+ "table": "event"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "type",
+ "entityType": "columns",
+ "table": "event"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "data",
+ "entityType": "columns",
+ "table": "event"
+ },
+ {
+ "columns": ["active_account_id"],
+ "tableTo": "account",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "SET NULL",
+ "nameExplicit": false,
+ "name": "fk_account_state_active_account_id_account_id_fk",
+ "entityType": "fks",
+ "table": "account_state"
+ },
+ {
+ "columns": ["project_id"],
+ "tableTo": "project",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_workspace_project_id_project_id_fk",
+ "entityType": "fks",
+ "table": "workspace"
+ },
+ {
+ "columns": ["session_id"],
+ "tableTo": "session",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_message_session_id_session_id_fk",
+ "entityType": "fks",
+ "table": "message"
+ },
+ {
+ "columns": ["message_id"],
+ "tableTo": "message",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_part_message_id_message_id_fk",
+ "entityType": "fks",
+ "table": "part"
+ },
+ {
+ "columns": ["project_id"],
+ "tableTo": "project",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_permission_project_id_project_id_fk",
+ "entityType": "fks",
+ "table": "permission"
+ },
+ {
+ "columns": ["project_id"],
+ "tableTo": "project",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_session_project_id_project_id_fk",
+ "entityType": "fks",
+ "table": "session"
+ },
+ {
+ "columns": ["session_id"],
+ "tableTo": "session",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_todo_session_id_session_id_fk",
+ "entityType": "fks",
+ "table": "todo"
+ },
+ {
+ "columns": ["session_id"],
+ "tableTo": "session",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_session_share_session_id_session_id_fk",
+ "entityType": "fks",
+ "table": "session_share"
+ },
+ {
+ "columns": ["aggregate_id"],
+ "tableTo": "event_sequence",
+ "columnsTo": ["aggregate_id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk",
+ "entityType": "fks",
+ "table": "event"
+ },
+ {
+ "columns": ["email", "url"],
+ "nameExplicit": false,
+ "name": "control_account_pk",
+ "entityType": "pks",
+ "table": "control_account"
+ },
+ {
+ "columns": ["session_id", "position"],
+ "nameExplicit": false,
+ "name": "todo_pk",
+ "entityType": "pks",
+ "table": "todo"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "account_state_pk",
+ "table": "account_state",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "account_pk",
+ "table": "account",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "workspace_pk",
+ "table": "workspace",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "project_pk",
+ "table": "project",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "message_pk",
+ "table": "message",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "part_pk",
+ "table": "part",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["project_id"],
+ "nameExplicit": false,
+ "name": "permission_pk",
+ "table": "permission",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "session_pk",
+ "table": "session",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["session_id"],
+ "nameExplicit": false,
+ "name": "session_share_pk",
+ "table": "session_share",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["aggregate_id"],
+ "nameExplicit": false,
+ "name": "event_sequence_pk",
+ "table": "event_sequence",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "event_pk",
+ "table": "event",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ {
+ "value": "session_id",
+ "isExpression": false
+ },
+ {
+ "value": "time_created",
+ "isExpression": false
+ },
+ {
+ "value": "id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "message_session_time_created_id_idx",
+ "entityType": "indexes",
+ "table": "message"
+ },
+ {
+ "columns": [
+ {
+ "value": "message_id",
+ "isExpression": false
+ },
+ {
+ "value": "id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "part_message_id_id_idx",
+ "entityType": "indexes",
+ "table": "part"
+ },
+ {
+ "columns": [
+ {
+ "value": "session_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "part_session_idx",
+ "entityType": "indexes",
+ "table": "part"
+ },
+ {
+ "columns": [
+ {
+ "value": "project_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "session_project_idx",
+ "entityType": "indexes",
+ "table": "session"
+ },
+ {
+ "columns": [
+ {
+ "value": "workspace_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "session_workspace_idx",
+ "entityType": "indexes",
+ "table": "session"
+ },
+ {
+ "columns": [
+ {
+ "value": "parent_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "session_parent_idx",
+ "entityType": "indexes",
+ "table": "session"
+ },
+ {
+ "columns": [
+ {
+ "value": "session_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "todo_session_idx",
+ "entityType": "indexes",
+ "table": "todo"
+ }
+ ],
+ "renames": []
+}
diff --git a/packages/opencode/package.json b/packages/opencode/package.json
index f15530c38..f1bbc6d50 100644
--- a/packages/opencode/package.json
+++ b/packages/opencode/package.json
@@ -93,7 +93,7 @@
"@ff-labs/fff-node": "0.4.2",
"@hono/standard-validator": "0.1.5",
"@hono/zod-validator": "catalog:",
- "@modelcontextprotocol/sdk": "1.25.2",
+ "@modelcontextprotocol/sdk": "1.27.1",
"@octokit/graphql": "9.0.2",
"@octokit/rest": "catalog:",
"@openauthjs/openauth": "catalog:",
@@ -133,9 +133,9 @@
"minimatch": "10.0.3",
"open": "10.1.2",
"opencode-gitlab-auth": "2.0.0",
+ "opencode-poe-auth": "0.0.1",
"opentui-spinner": "0.0.6",
"partial-json": "0.1.7",
- "opencode-poe-auth": "0.0.1",
"remeda": "catalog:",
"semver": "^7.6.3",
"solid-js": "catalog:",
diff --git a/packages/opencode/specs/effect-migration.md b/packages/opencode/specs/effect-migration.md
index cf217871d..073da7b32 100644
--- a/packages/opencode/specs/effect-migration.md
+++ b/packages/opencode/specs/effect-migration.md
@@ -164,7 +164,7 @@ Still open and likely worth migrating:
- [x] `Plugin`
- [x] `ToolRegistry`
- [ ] `Pty`
-- [ ] `Worktree`
+- [x] `Worktree`
- [ ] `Bus`
- [x] `Command`
- [ ] `Config`
diff --git a/packages/opencode/src/account/repo.ts b/packages/opencode/src/account/repo.ts
index 96f980cda..d02cf1b63 100644
--- a/packages/opencode/src/account/repo.ts
+++ b/packages/opencode/src/account/repo.ts
@@ -8,6 +8,7 @@ import { AccessToken, AccountID, AccountRepoError, Info, OrgID, RefreshToken } f
export type AccountRow = (typeof AccountTable)["$inferSelect"]
type DbClient = Parameters[0] extends (db: infer T) => unknown ? T : never
+type DbTransactionCallback = Parameters>[0]
const ACCOUNT_STATE_ID = 1
@@ -42,13 +43,13 @@ export class AccountRepo extends ServiceMap.Service(f: (db: DbClient) => A) =>
+ const query = (f: DbTransactionCallback) =>
Effect.try({
try: () => Database.use(f),
catch: (cause) => new AccountRepoError({ message: "Database operation failed", cause }),
})
- const tx = (f: (db: DbClient) => A) =>
+ const tx = (f: DbTransactionCallback) =>
Effect.try({
try: () => Database.transaction(f),
catch: (cause) => new AccountRepoError({ message: "Database operation failed", cause }),
diff --git a/packages/opencode/src/bus/bus-event.ts b/packages/opencode/src/bus/bus-event.ts
index 7fe13833c..d97922290 100644
--- a/packages/opencode/src/bus/bus-event.ts
+++ b/packages/opencode/src/bus/bus-event.ts
@@ -1,10 +1,7 @@
import z from "zod"
import type { ZodType } from "zod"
-import { Log } from "../util/log"
export namespace BusEvent {
- const log = Log.create({ service: "event" })
-
export type Definition = ReturnType
const registry = new Map()
diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts
index edd9d7561..abb5af7f6 100644
--- a/packages/opencode/src/cli/cmd/github.ts
+++ b/packages/opencode/src/cli/cmd/github.ts
@@ -890,7 +890,7 @@ export const GithubRunCommand = cmd({
}
let text = ""
- Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => {
+ Bus.subscribe(MessageV2.Event.PartUpdated, (evt) => {
if (evt.properties.part.sessionID !== session.id) return
//if (evt.properties.part.messageID === messageID) return
const part = evt.properties.part
diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx
index 8aabafd08..84050d220 100644
--- a/packages/opencode/src/cli/cmd/tui/app.tsx
+++ b/packages/opencode/src/cli/cmd/tui/app.tsx
@@ -710,7 +710,7 @@ function App(props: { onSnapshot?: () => Promise }) {
})
})
- sdk.event.on(SessionApi.Event.Deleted.type, (evt) => {
+ sdk.event.on("session.deleted", (evt) => {
if (route.data.type === "session" && route.data.sessionID === evt.properties.info.id) {
route.navigate({ type: "home" })
toast.show({
@@ -720,7 +720,7 @@ function App(props: { onSnapshot?: () => Promise }) {
}
})
- sdk.event.on(SessionApi.Event.Error.type, (evt) => {
+ sdk.event.on("session.error", (evt) => {
const error = evt.properties.error
if (error && typeof error === "object" && error.name === "MessageAbortedError") return
const message = (() => {
diff --git a/packages/opencode/src/control-plane/adaptors/worktree.ts b/packages/opencode/src/control-plane/adaptors/worktree.ts
index f84890950..ff2d92e19 100644
--- a/packages/opencode/src/control-plane/adaptors/worktree.ts
+++ b/packages/opencode/src/control-plane/adaptors/worktree.ts
@@ -22,12 +22,11 @@ export const WorktreeAdaptor: Adaptor = {
},
async create(info) {
const config = Config.parse(info)
- const bootstrap = await Worktree.createFromInfo({
+ await Worktree.createFromInfo({
name: config.name,
directory: config.directory,
branch: config.branch,
})
- return bootstrap()
},
async remove(info) {
const config = Config.parse(info)
diff --git a/packages/opencode/src/id/id.ts b/packages/opencode/src/id/id.ts
index 6673297cb..9e324962b 100644
--- a/packages/opencode/src/id/id.ts
+++ b/packages/opencode/src/id/id.ts
@@ -3,6 +3,7 @@ import { randomBytes } from "crypto"
export namespace Identifier {
const prefixes = {
+ event: "evt",
session: "ses",
message: "msg",
permission: "per",
diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts
index 2eb1ad93e..41a650afd 100644
--- a/packages/opencode/src/lsp/index.ts
+++ b/packages/opencode/src/lsp/index.ts
@@ -177,6 +177,12 @@ export namespace LSP {
async function getClients(file: string) {
const s = await state()
+
+ // Only spawn LSP clients for files within the instance directory
+ if (!Instance.containsPath(file)) {
+ return []
+ }
+
const extension = path.parse(file).ext || file
const result: LSPClient.Info[] = []
diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts
index 5cd379078..09e991c5a 100644
--- a/packages/opencode/src/plugin/index.ts
+++ b/packages/opencode/src/plugin/index.ts
@@ -3,7 +3,6 @@ import { Config } from "../config/config"
import { Bus } from "../bus"
import { Log } from "../util/log"
import { createOpencodeClient } from "@opencode-ai/sdk"
-import { Server } from "../server/server"
import { BunProc } from "../bun"
import { Flag } from "../flag/flag"
import { CodexAuthPlugin } from "./codex"
@@ -58,6 +57,8 @@ export namespace Plugin {
const hooks: Hooks[] = []
yield* Effect.promise(async () => {
+ const { Server } = await import("../server/server")
+
const client = createOpencodeClient({
baseUrl: "http://localhost:4096",
directory: ctx.directory,
diff --git a/packages/opencode/src/server/projectors.ts b/packages/opencode/src/server/projectors.ts
new file mode 100644
index 000000000..eb85a8017
--- /dev/null
+++ b/packages/opencode/src/server/projectors.ts
@@ -0,0 +1,28 @@
+import z from "zod"
+import sessionProjectors from "../session/projectors"
+import { SyncEvent } from "@/sync"
+import { Session } from "@/session"
+import { SessionTable } from "@/session/session.sql"
+import { Database, eq } from "@/storage/db"
+
+export function initProjectors() {
+ SyncEvent.init({
+ projectors: sessionProjectors,
+ convertEvent: (type, data) => {
+ if (type === "session.updated") {
+ const id = (data as z.infer).sessionID
+ const row = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, id)).get())
+
+ if (!row) return data
+
+ return {
+ sessionID: id,
+ info: Session.fromRow(row),
+ }
+ }
+ return data
+ },
+ })
+}
+
+initProjectors()
diff --git a/packages/opencode/src/server/routes/event.ts b/packages/opencode/src/server/routes/event.ts
index f34ff0566..178da5994 100644
--- a/packages/opencode/src/server/routes/event.ts
+++ b/packages/opencode/src/server/routes/event.ts
@@ -6,7 +6,6 @@ import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import { lazy } from "../../util/lazy"
import { AsyncQueue } from "../../util/queue"
-import { Instance } from "@/project/instance"
const log = Log.create({ service: "server" })
@@ -53,13 +52,6 @@ export const EventRoutes = lazy(() =>
)
}, 10_000)
- const unsub = Bus.subscribeAll((event) => {
- q.push(JSON.stringify(event))
- if (event.type === Bus.InstanceDisposed.type) {
- stop()
- }
- })
-
const stop = () => {
if (done) return
done = true
@@ -69,6 +61,13 @@ export const EventRoutes = lazy(() =>
log.info("event disconnected")
}
+ const unsub = Bus.subscribeAll((event) => {
+ q.push(JSON.stringify(event))
+ if (event.type === Bus.InstanceDisposed.type) {
+ stop()
+ }
+ })
+
stream.onAbort(stop)
try {
diff --git a/packages/opencode/src/server/routes/experimental.ts b/packages/opencode/src/server/routes/experimental.ts
index 43be6f245..a41b21a1f 100644
--- a/packages/opencode/src/server/routes/experimental.ts
+++ b/packages/opencode/src/server/routes/experimental.ts
@@ -108,7 +108,7 @@ export const ExperimentalRoutes = lazy(() =>
...errors(400),
},
}),
- validator("json", Worktree.create.schema),
+ validator("json", Worktree.CreateInput.optional()),
async (c) => {
const body = c.req.valid("json")
const worktree = await Worktree.create(body)
@@ -155,7 +155,7 @@ export const ExperimentalRoutes = lazy(() =>
...errors(400),
},
}),
- validator("json", Worktree.remove.schema),
+ validator("json", Worktree.RemoveInput),
async (c) => {
const body = c.req.valid("json")
await Worktree.remove(body)
@@ -181,7 +181,7 @@ export const ExperimentalRoutes = lazy(() =>
...errors(400),
},
}),
- validator("json", Worktree.reset.schema),
+ validator("json", Worktree.ResetInput),
async (c) => {
const body = c.req.valid("json")
await Worktree.reset(body)
diff --git a/packages/opencode/src/server/routes/global.ts b/packages/opencode/src/server/routes/global.ts
index 4dd30db2a..88f54f844 100644
--- a/packages/opencode/src/server/routes/global.ts
+++ b/packages/opencode/src/server/routes/global.ts
@@ -1,9 +1,9 @@
-import { Hono } from "hono"
-import { describeRoute, validator, resolver } from "hono-openapi"
+import { Hono, type Context } from "hono"
+import { describeRoute, resolver, validator } from "hono-openapi"
import { streamSSE } from "hono/streaming"
import z from "zod"
-import { Bus } from "../../bus"
import { BusEvent } from "@/bus/bus-event"
+import { SyncEvent } from "@/sync"
import { GlobalBus } from "@/bus/global"
import { AsyncQueue } from "@/util/queue"
import { Instance } from "../../project/instance"
@@ -17,6 +17,56 @@ const log = Log.create({ service: "server" })
export const GlobalDisposedEvent = BusEvent.define("global.disposed", z.object({}))
+async function streamEvents(c: Context, subscribe: (q: AsyncQueue) => () => void) {
+ return streamSSE(c, async (stream) => {
+ const q = new AsyncQueue()
+ let done = false
+
+ q.push(
+ JSON.stringify({
+ payload: {
+ type: "server.connected",
+ properties: {},
+ },
+ }),
+ )
+
+ // Send heartbeat every 10s to prevent stalled proxy streams.
+ const heartbeat = setInterval(() => {
+ q.push(
+ JSON.stringify({
+ payload: {
+ type: "server.heartbeat",
+ properties: {},
+ },
+ }),
+ )
+ }, 10_000)
+
+ const stop = () => {
+ if (done) return
+ done = true
+ clearInterval(heartbeat)
+ unsub()
+ q.push(null)
+ log.info("global event disconnected")
+ }
+
+ const unsub = subscribe(q)
+
+ stream.onAbort(stop)
+
+ try {
+ for await (const data of q) {
+ if (data === null) return
+ await stream.writeSSE({ data })
+ }
+ } finally {
+ stop()
+ }
+ })
+}
+
export const GlobalRoutes = lazy(() =>
new Hono()
.get(
@@ -70,55 +120,58 @@ export const GlobalRoutes = lazy(() =>
log.info("global event connected")
c.header("X-Accel-Buffering", "no")
c.header("X-Content-Type-Options", "nosniff")
- return streamSSE(c, async (stream) => {
- const q = new AsyncQueue()
- let done = false
-
- q.push(
- JSON.stringify({
- payload: {
- type: "server.connected",
- properties: {},
- },
- }),
- )
-
- // Send heartbeat every 10s to prevent stalled proxy streams.
- const heartbeat = setInterval(() => {
- q.push(
- JSON.stringify({
- payload: {
- type: "server.heartbeat",
- properties: {},
- },
- }),
- )
- }, 10_000)
+ return streamEvents(c, (q) => {
async function handler(event: any) {
q.push(JSON.stringify(event))
}
GlobalBus.on("event", handler)
-
- const stop = () => {
- if (done) return
- done = true
- clearInterval(heartbeat)
- GlobalBus.off("event", handler)
- q.push(null)
- log.info("event disconnected")
- }
-
- stream.onAbort(stop)
-
- try {
- for await (const data of q) {
- if (data === null) return
- await stream.writeSSE({ data })
- }
- } finally {
- stop()
- }
+ return () => GlobalBus.off("event", handler)
+ })
+ },
+ )
+ .get(
+ "/sync-event",
+ describeRoute({
+ summary: "Subscribe to global sync events",
+ description: "Get global sync events",
+ operationId: "global.sync-event.subscribe",
+ responses: {
+ 200: {
+ description: "Event stream",
+ content: {
+ "text/event-stream": {
+ schema: resolver(
+ z
+ .object({
+ payload: SyncEvent.payloads(),
+ })
+ .meta({
+ ref: "SyncEvent",
+ }),
+ ),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ log.info("global sync event connected")
+ c.header("X-Accel-Buffering", "no")
+ c.header("X-Content-Type-Options", "nosniff")
+ return streamEvents(c, (q) => {
+ return SyncEvent.subscribeAll(({ def, event }) => {
+ // TODO: don't pass def, just pass the type (and it should
+ // be versioned)
+ q.push(
+ JSON.stringify({
+ payload: {
+ ...event,
+ type: SyncEvent.versionedType(def.type, def.version),
+ },
+ }),
+ )
+ })
})
},
)
diff --git a/packages/opencode/src/server/routes/session.ts b/packages/opencode/src/server/routes/session.ts
index 3c9ebfdc5..d499e5a1e 100644
--- a/packages/opencode/src/server/routes/session.ts
+++ b/packages/opencode/src/server/routes/session.ts
@@ -281,14 +281,14 @@ export const SessionRoutes = lazy(() =>
const sessionID = c.req.valid("param").sessionID
const updates = c.req.valid("json")
- let session = await Session.get(sessionID)
if (updates.title !== undefined) {
- session = await Session.setTitle({ sessionID, title: updates.title })
+ await Session.setTitle({ sessionID, title: updates.title })
}
if (updates.time?.archived !== undefined) {
- session = await Session.setArchived({ sessionID, time: updates.time.archived })
+ await Session.setArchived({ sessionID, time: updates.time.archived })
}
+ const session = await Session.get(sessionID)
return c.json(session)
},
)
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index 7ead4df8a..d260714d4 100644
--- a/packages/opencode/src/server/server.ts
+++ b/packages/opencode/src/server/server.ts
@@ -1,3 +1,4 @@
+import { createHash } from "node:crypto"
import { Log } from "../util/log"
import { describeRoute, generateSpecs, validator, resolver, openAPIRouteHandler } from "hono-openapi"
import { Hono } from "hono"
@@ -43,10 +44,16 @@ import { PermissionRoutes } from "./routes/permission"
import { GlobalRoutes } from "./routes/global"
import { MDNS } from "./mdns"
import { lazy } from "@/util/lazy"
+import { initProjectors } from "./projectors"
// @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
globalThis.AI_SDK_LOG_WARNINGS = false
+const csp = (hash = "") =>
+ `default-src 'self'; script-src 'self' 'wasm-unsafe-eval'${hash ? ` 'sha256-${hash}'` : ""}; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:`
+
+initProjectors()
+
export namespace Server {
const log = Log.create({ service: "server" })
@@ -506,10 +513,13 @@ export namespace Server {
host: "app.opencode.ai",
},
})
- response.headers.set(
- "Content-Security-Policy",
- "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:",
- )
+ const match = response.headers.get("content-type")?.includes("text/html")
+ ? (await response.clone().text()).match(
+ /