Compare commits
60
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d7501a6a3 | ||
|
|
bf601628db | ||
|
|
00e39d2114 | ||
|
|
46b74e0873 | ||
|
|
aedc4e964f | ||
|
|
e83404367c | ||
|
|
42206da1f8 | ||
|
|
44f38193c0 | ||
|
|
9a6b455bfe | ||
|
|
8063e0b5c6 | ||
|
|
157c5d77f8 | ||
|
|
ce19c051be | ||
|
|
91786d2fc1 | ||
|
|
eca11ca71a | ||
|
|
17bd16667c | ||
|
|
16c60c9ee7 | ||
|
|
0970b102e1 | ||
|
|
04074d3f4a | ||
|
|
b16ee08fd5 | ||
|
|
98874a09f7 | ||
|
|
877be7e8e0 | ||
|
|
eac50f9151 | ||
|
|
1a902b291c | ||
|
|
bbe4a04f9f | ||
|
|
b2f621b897 | ||
|
|
7202b3a325 | ||
|
|
35b44df94a | ||
|
|
10441efad1 | ||
|
|
3199383eef | ||
|
|
9f54115c5d | ||
|
|
2ecc6ae65f | ||
|
|
02b32e1ba7 | ||
|
|
34b9792654 | ||
|
|
537160dbc0 | ||
|
|
b0600664ab | ||
|
|
581a7692ff | ||
|
|
f73e4d5d31 | ||
|
|
a7743e6467 | ||
|
|
5d3dba666c | ||
|
|
bd53b651a3 | ||
|
|
46da801f30 | ||
|
|
58a99916bb | ||
|
|
c29392d085 | ||
|
|
46f243fea7 | ||
|
|
847fc9d268 | ||
|
|
489f57974d | ||
|
|
3fc3974cbc | ||
|
|
ca57248246 | ||
|
|
ee23043d64 | ||
|
|
9c1c061b84 | ||
|
|
d82b163e56 | ||
|
|
cd8e8a9928 | ||
|
|
8bdcc22541 | ||
|
|
2bdd279467 | ||
|
|
51535d8ef3 | ||
|
|
38f8714c09 | ||
|
|
4961d72c0f | ||
|
|
00cb8839ae | ||
|
|
689b1a4b3a | ||
|
|
d98be39344 |
@@ -114,7 +114,7 @@ jobs:
|
|||||||
- build-cli
|
- build-cli
|
||||||
- version
|
- version
|
||||||
runs-on: blacksmith-4vcpu-windows-2025
|
runs-on: blacksmith-4vcpu-windows-2025
|
||||||
if: github.repository == 'anomalyco/opencode'
|
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'beta'
|
||||||
env:
|
env:
|
||||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
@@ -213,6 +213,7 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- build-cli
|
- build-cli
|
||||||
- version
|
- version
|
||||||
|
if: github.ref_name != 'beta'
|
||||||
continue-on-error: false
|
continue-on-error: false
|
||||||
env:
|
env:
|
||||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
@@ -389,6 +390,7 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- build-cli
|
- build-cli
|
||||||
- version
|
- version
|
||||||
|
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'beta'
|
||||||
continue-on-error: false
|
continue-on-error: false
|
||||||
env:
|
env:
|
||||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
@@ -421,7 +423,6 @@ jobs:
|
|||||||
target: aarch64-unknown-linux-gnu
|
target: aarch64-unknown-linux-gnu
|
||||||
platform_flag: --linux
|
platform_flag: --linux
|
||||||
runs-on: ${{ matrix.settings.host }}
|
runs-on: ${{ matrix.settings.host }}
|
||||||
# if: github.ref_name == 'beta'
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
@@ -547,6 +548,7 @@ jobs:
|
|||||||
- sign-cli-windows
|
- sign-cli-windows
|
||||||
- build-tauri
|
- build-tauri
|
||||||
- build-electron
|
- build-electron
|
||||||
|
if: always() && !failure() && !cancelled()
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -589,12 +591,13 @@ jobs:
|
|||||||
path: packages/opencode/dist
|
path: packages/opencode/dist
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
|
if: github.ref_name != 'beta'
|
||||||
with:
|
with:
|
||||||
name: opencode-cli-signed-windows
|
name: opencode-cli-signed-windows
|
||||||
path: packages/opencode/dist
|
path: packages/opencode/dist
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
if: needs.version.outputs.release
|
if: needs.version.outputs.release && github.ref_name != 'beta'
|
||||||
with:
|
with:
|
||||||
pattern: latest-yml-*
|
pattern: latest-yml-*
|
||||||
path: /tmp/latest-yml
|
path: /tmp/latest-yml
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
checks: write
|
checks: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
unit:
|
unit:
|
||||||
name: unit (${{ matrix.settings.name }})
|
name: unit (${{ matrix.settings.name }})
|
||||||
@@ -38,6 +41,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "24"
|
||||||
|
|
||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
uses: ./.github/actions/setup-bun
|
uses: ./.github/actions/setup-bun
|
||||||
|
|
||||||
@@ -102,6 +110,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "24"
|
||||||
|
|
||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
uses: ./.github/actions/setup-bun
|
uses: ./.github/actions/setup-bun
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.4.0",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.4.0",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.4.0",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-sts": "3.782.0",
|
"@aws-sdk/client-sts": "3.782.0",
|
||||||
"@jsx-email/render": "1.1.1",
|
"@jsx-email/render": "1.1.1",
|
||||||
@@ -142,7 +142,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.4.0",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "3.0.64",
|
"@ai-sdk/anthropic": "3.0.64",
|
||||||
"@ai-sdk/openai": "3.0.48",
|
"@ai-sdk/openai": "3.0.48",
|
||||||
@@ -166,7 +166,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.4.0",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
@@ -190,7 +190,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.4.0",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/app": "workspace:*",
|
"@opencode-ai/app": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
@@ -223,14 +223,8 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop-electron": {
|
"packages/desktop-electron": {
|
||||||
"name": "@opencode-ai/desktop-electron",
|
"name": "@opencode-ai/desktop-electron",
|
||||||
"version": "1.4.0",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/app": "workspace:*",
|
|
||||||
"@opencode-ai/ui": "workspace:*",
|
|
||||||
"@solid-primitives/i18n": "2.2.1",
|
|
||||||
"@solid-primitives/storage": "catalog:",
|
|
||||||
"@solidjs/meta": "catalog:",
|
|
||||||
"@solidjs/router": "0.15.4",
|
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"electron-context-menu": "4.1.2",
|
"electron-context-menu": "4.1.2",
|
||||||
"electron-log": "^5",
|
"electron-log": "^5",
|
||||||
@@ -238,24 +232,41 @@
|
|||||||
"electron-updater": "^6",
|
"electron-updater": "^6",
|
||||||
"electron-window-state": "^5.0.3",
|
"electron-window-state": "^5.0.3",
|
||||||
"marked": "^15",
|
"marked": "^15",
|
||||||
"solid-js": "catalog:",
|
|
||||||
"tree-kill": "^1.2.2",
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@actions/artifact": "4.0.0",
|
"@actions/artifact": "4.0.0",
|
||||||
|
"@lydell/node-pty": "catalog:",
|
||||||
|
"@opencode-ai/app": "workspace:*",
|
||||||
|
"@opencode-ai/ui": "workspace:*",
|
||||||
|
"@solid-primitives/i18n": "2.2.1",
|
||||||
|
"@solid-primitives/storage": "catalog:",
|
||||||
|
"@solidjs/meta": "catalog:",
|
||||||
|
"@solidjs/router": "0.15.4",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
|
"@valibot/to-json-schema": "1.6.0",
|
||||||
"electron": "40.4.1",
|
"electron": "40.4.1",
|
||||||
"electron-builder": "^26",
|
"electron-builder": "^26",
|
||||||
"electron-vite": "^5",
|
"electron-vite": "^5",
|
||||||
|
"solid-js": "catalog:",
|
||||||
|
"sury": "11.0.0-alpha.4",
|
||||||
"typescript": "~5.6.2",
|
"typescript": "~5.6.2",
|
||||||
"vite": "catalog:",
|
"vite": "catalog:",
|
||||||
|
"zod-openapi": "5.4.6",
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@lydell/node-pty-darwin-arm64": "1.2.0-beta.10",
|
||||||
|
"@lydell/node-pty-darwin-x64": "1.2.0-beta.10",
|
||||||
|
"@lydell/node-pty-linux-arm64": "1.2.0-beta.10",
|
||||||
|
"@lydell/node-pty-linux-x64": "1.2.0-beta.10",
|
||||||
|
"@lydell/node-pty-win32-arm64": "1.2.0-beta.10",
|
||||||
|
"@lydell/node-pty-win32-x64": "1.2.0-beta.10",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.4.0",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
@@ -284,7 +295,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.4.0",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
@@ -300,7 +311,7 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.4.0",
|
"version": "1.4.3",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -336,7 +347,7 @@
|
|||||||
"@hono/node-ws": "1.3.0",
|
"@hono/node-ws": "1.3.0",
|
||||||
"@hono/standard-validator": "0.1.5",
|
"@hono/standard-validator": "0.1.5",
|
||||||
"@hono/zod-validator": "catalog:",
|
"@hono/zod-validator": "catalog:",
|
||||||
"@lydell/node-pty": "1.2.0-beta.10",
|
"@lydell/node-pty": "catalog:",
|
||||||
"@modelcontextprotocol/sdk": "1.27.1",
|
"@modelcontextprotocol/sdk": "1.27.1",
|
||||||
"@npmcli/arborist": "9.4.0",
|
"@npmcli/arborist": "9.4.0",
|
||||||
"@octokit/graphql": "9.0.2",
|
"@octokit/graphql": "9.0.2",
|
||||||
@@ -367,7 +378,7 @@
|
|||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
"gitlab-ai-provider": "6.0.0",
|
"gitlab-ai-provider": "6.4.2",
|
||||||
"glob": "13.0.5",
|
"glob": "13.0.5",
|
||||||
"google-auth-library": "10.5.0",
|
"google-auth-library": "10.5.0",
|
||||||
"gray-matter": "4.0.3",
|
"gray-matter": "4.0.3",
|
||||||
@@ -436,7 +447,7 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.4.0",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
@@ -470,7 +481,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.4.0",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-spawn": "catalog:",
|
"cross-spawn": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -485,7 +496,7 @@
|
|||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.4.0",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -520,7 +531,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.4.0",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -569,7 +580,7 @@
|
|||||||
},
|
},
|
||||||
"packages/util": {
|
"packages/util": {
|
||||||
"name": "@opencode-ai/util",
|
"name": "@opencode-ai/util",
|
||||||
"version": "1.4.0",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -580,7 +591,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.4.0",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
@@ -633,6 +644,7 @@
|
|||||||
"@effect/platform-node": "4.0.0-beta.43",
|
"@effect/platform-node": "4.0.0-beta.43",
|
||||||
"@hono/zod-validator": "0.4.2",
|
"@hono/zod-validator": "0.4.2",
|
||||||
"@kobalte/core": "0.13.11",
|
"@kobalte/core": "0.13.11",
|
||||||
|
"@lydell/node-pty": "1.2.0-beta.10",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||||
"@pierre/diffs": "1.1.0-beta.18",
|
"@pierre/diffs": "1.1.0-beta.18",
|
||||||
@@ -2313,6 +2325,8 @@
|
|||||||
|
|
||||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||||
|
|
||||||
|
"@valibot/to-json-schema": ["@valibot/to-json-schema@1.6.0", "", { "peerDependencies": { "valibot": "^1.3.0" } }, "sha512-d6rYyK5KVa2XdqamWgZ4/Nr+cXhxjy7lmpe6Iajw15J/jmU+gyxl2IEd1Otg1d7Rl3gOQL5reulnSypzBtYy1A=="],
|
||||||
|
|
||||||
"@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="],
|
"@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="],
|
||||||
|
|
||||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
|
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
|
||||||
@@ -3165,7 +3179,7 @@
|
|||||||
|
|
||||||
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
|
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
|
||||||
|
|
||||||
"gitlab-ai-provider": ["gitlab-ai-provider@6.0.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-683GcJdrer/GhnljkbVcGsndCEhvGB8f9fUdCxQBlkuyt8rzf0G9DpSh+iMBYp9HpcSvYmYG0Qv5ks9dLrNxwQ=="],
|
"gitlab-ai-provider": ["gitlab-ai-provider@6.4.2", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-Wyw6uslCuipBOr/NYwAtpgXEUJj68iJY5aekad2DjePN99JetKVQBqkLgAy9PZp2EA4OuscfRQu9qKIBN/evNw=="],
|
||||||
|
|
||||||
"glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="],
|
"glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="],
|
||||||
|
|
||||||
@@ -4577,6 +4591,8 @@
|
|||||||
|
|
||||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||||
|
|
||||||
|
"sury": ["sury@11.0.0-alpha.4", "", { "peerDependencies": { "rescript": "12.x" }, "optionalPeers": ["rescript"] }, "sha512-oeG/GJWZvQCKtGPpLbu0yCZudfr5LxycDo5kh7SJmKHDPCsEPJssIZL2Eb4Tl7g9aPEvIDuRrkS+L0pybsMEMA=="],
|
||||||
|
|
||||||
"system-architecture": ["system-architecture@0.1.0", "", {}, "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA=="],
|
"system-architecture": ["system-architecture@0.1.0", "", {}, "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA=="],
|
||||||
|
|
||||||
"tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="],
|
"tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="],
|
||||||
@@ -4655,8 +4671,6 @@
|
|||||||
|
|
||||||
"traverse": ["traverse@0.3.9", "", {}, "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ=="],
|
"traverse": ["traverse@0.3.9", "", {}, "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ=="],
|
||||||
|
|
||||||
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
|
|
||||||
|
|
||||||
"tree-sitter-bash": ["tree-sitter-bash@0.25.0", "", { "dependencies": { "node-addon-api": "^8.2.1", "node-gyp-build": "^4.8.2" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-gZtlj9+qFS81qKxpLfD6H0UssQ3QBc/F0nKkPsiFDyfQF2YBqYvglFJUzchrPpVhZe9kLZTrJ9n2J6lmka69Vg=="],
|
"tree-sitter-bash": ["tree-sitter-bash@0.25.0", "", { "dependencies": { "node-addon-api": "^8.2.1", "node-gyp-build": "^4.8.2" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-gZtlj9+qFS81qKxpLfD6H0UssQ3QBc/F0nKkPsiFDyfQF2YBqYvglFJUzchrPpVhZe9kLZTrJ9n2J6lmka69Vg=="],
|
||||||
|
|
||||||
"tree-sitter-powershell": ["tree-sitter-powershell@0.25.10", "", { "dependencies": { "node-addon-api": "^7.1.0", "node-gyp-build": "^4.8.0" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-bEt8QoySpGFnU3aa8WedQyNMaN6aTwy/WUbvIVt0JSKF+BbJoSHNHu+wCbhj7xLMsfB0AuffmiJm+B8gzva8Lg=="],
|
"tree-sitter-powershell": ["tree-sitter-powershell@0.25.10", "", { "dependencies": { "node-addon-api": "^7.1.0", "node-gyp-build": "^4.8.0" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-bEt8QoySpGFnU3aa8WedQyNMaN6aTwy/WUbvIVt0JSKF+BbJoSHNHu+wCbhj7xLMsfB0AuffmiJm+B8gzva8Lg=="],
|
||||||
@@ -4811,6 +4825,8 @@
|
|||||||
|
|
||||||
"uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="],
|
"uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="],
|
||||||
|
|
||||||
|
"valibot": ["valibot@1.3.1", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-sfdRir/QFM0JaF22hqTroPc5xy4DimuGQVKFrzF1YfGwaS1nJot3Y8VqMdLO2Lg27fMzat2yD3pY5PbAYO39Gg=="],
|
||||||
|
|
||||||
"validate-npm-package-name": ["validate-npm-package-name@7.0.2", "", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="],
|
"validate-npm-package-name": ["validate-npm-package-name@7.0.2", "", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="],
|
||||||
|
|
||||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||||
@@ -4967,6 +4983,8 @@
|
|||||||
|
|
||||||
"zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
|
"zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
|
||||||
|
|
||||||
|
"zod-openapi": ["zod-openapi@5.4.6", "", { "peerDependencies": { "zod": "^3.25.74 || ^4.0.0" } }, "sha512-P2jsOOBAq/6hCwUsMCjUATZ8szkMsV5VAwZENfyxp2Hc/XPJQpVwAgevWZc65xZauCwWB9LAn7zYeiCJFAEL+A=="],
|
||||||
|
|
||||||
"zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
|
"zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
|
||||||
|
|
||||||
"zod-to-ts": ["zod-to-ts@1.2.0", "", { "peerDependencies": { "typescript": "^4.9.4 || ^5.0.2", "zod": "^3" } }, "sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA=="],
|
"zod-to-ts": ["zod-to-ts@1.2.0", "", { "peerDependencies": { "typescript": "^4.9.4 || ^5.0.2", "zod": "^3" } }, "sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA=="],
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-85wpU1oCWbthPleNIOj5d5AOuuYZ6rM7gMLZR6YJ2WU=",
|
"x86_64-linux": "sha256-285KZ7rZLRoc6XqCZRHc25NE+mmpGh/BVeMpv8aPQtQ=",
|
||||||
"aarch64-linux": "sha256-C3A56SDQGJquCpIRj2JhIzr4A7N4cc9lxtEjl8bXDeM=",
|
"aarch64-linux": "sha256-qIwmY4TP4CI7R7G6A5OMYRrorVNXjkg25tTtVpIHm2o=",
|
||||||
"aarch64-darwin": "sha256-/Ij3qhGRrcLlMfl9uEacDNnGK5URxhctuQFBW4Njrog=",
|
"aarch64-darwin": "sha256-RwvnZQhdYZ0u7h7evyfxuPLHHX9eO/jXTAxIFc8B+IE=",
|
||||||
"x86_64-darwin": "sha256-10sOPuN4eZ75orw4FI8ztCq1+AKS2e8aAfg3Z6Yn56w="
|
"x86_64-darwin": "sha256-vVj40al+TEeMpbe5XG2GmJEpN+eQAvtr9W0T98l5PBE="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -71,7 +71,8 @@
|
|||||||
"@solidjs/router": "0.15.4",
|
"@solidjs/router": "0.15.4",
|
||||||
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
|
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
|
||||||
"solid-js": "1.9.10",
|
"solid-js": "1.9.10",
|
||||||
"vite-plugin-solid": "2.11.10"
|
"vite-plugin-solid": "2.11.10",
|
||||||
|
"@lydell/node-pty": "1.2.0-beta.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -44,8 +44,12 @@ async function waitForHealth(url: string, probe = "/global/health") {
|
|||||||
throw new Error(`Timed out waiting for backend health at ${url}${probe}${last ? ` (${last})` : ""}`)
|
throw new Error(`Timed out waiting for backend health at ${url}${probe}${last ? ` (${last})` : ""}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function done(proc: ReturnType<typeof spawn>) {
|
||||||
|
return proc.exitCode !== null || proc.signalCode !== null
|
||||||
|
}
|
||||||
|
|
||||||
async function waitExit(proc: ReturnType<typeof spawn>, timeout = 10_000) {
|
async function waitExit(proc: ReturnType<typeof spawn>, timeout = 10_000) {
|
||||||
if (proc.exitCode !== null) return
|
if (done(proc)) return
|
||||||
await Promise.race([
|
await Promise.race([
|
||||||
new Promise<void>((resolve) => proc.once("exit", () => resolve())),
|
new Promise<void>((resolve) => proc.once("exit", () => resolve())),
|
||||||
new Promise<void>((resolve) => setTimeout(resolve, timeout)),
|
new Promise<void>((resolve) => setTimeout(resolve, timeout)),
|
||||||
@@ -123,11 +127,11 @@ export async function startBackend(label: string, input?: { llmUrl?: string }):
|
|||||||
return {
|
return {
|
||||||
url,
|
url,
|
||||||
async stop() {
|
async stop() {
|
||||||
if (proc.exitCode === null) {
|
if (!done(proc)) {
|
||||||
proc.kill("SIGTERM")
|
proc.kill("SIGTERM")
|
||||||
await waitExit(proc)
|
await waitExit(proc)
|
||||||
}
|
}
|
||||||
if (proc.exitCode === null) {
|
if (!done(proc)) {
|
||||||
proc.kill("SIGKILL")
|
proc.kill("SIGKILL")
|
||||||
await waitExit(proc)
|
await waitExit(proc)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.4.0",
|
"version": "1.4.3",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -182,7 +182,6 @@ function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) {
|
|||||||
if (checkMode() === "background" || type === "http") return false
|
if (checkMode() === "background" || type === "http") return false
|
||||||
}
|
}
|
||||||
}).pipe(
|
}).pipe(
|
||||||
effectMinDuration(checkMode() === "blocking" ? "1.2 seconds" : 0),
|
|
||||||
Effect.timeoutOrElse({ duration: "10 seconds", orElse: () => Effect.succeed(false) }),
|
Effect.timeoutOrElse({ duration: "10 seconds", orElse: () => Effect.succeed(false) }),
|
||||||
Effect.ensuring(Effect.sync(() => setCheckMode("background"))),
|
Effect.ensuring(Effect.sync(() => setCheckMode("background"))),
|
||||||
Effect.runPromise,
|
Effect.runPromise,
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
const auth = server.current?.http
|
const auth = server.current?.http
|
||||||
const username = auth?.username ?? "opencode"
|
const username = auth?.username ?? "opencode"
|
||||||
const password = auth?.password ?? ""
|
const password = auth?.password ?? ""
|
||||||
|
const sameOrigin = new URL(url, location.href).origin === location.origin
|
||||||
let container!: HTMLDivElement
|
let container!: HTMLDivElement
|
||||||
const [local, others] = splitProps(props, ["pty", "class", "classList", "autoFocus", "onConnect", "onConnectError"])
|
const [local, others] = splitProps(props, ["pty", "class", "classList", "autoFocus", "onConnect", "onConnectError"])
|
||||||
const id = local.pty.id
|
const id = local.pty.id
|
||||||
@@ -519,8 +520,12 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
next.searchParams.set("directory", directory)
|
next.searchParams.set("directory", directory)
|
||||||
next.searchParams.set("cursor", String(seek))
|
next.searchParams.set("cursor", String(seek))
|
||||||
next.protocol = next.protocol === "https:" ? "wss:" : "ws:"
|
next.protocol = next.protocol === "https:" ? "wss:" : "ws:"
|
||||||
|
if (!sameOrigin && password) {
|
||||||
|
next.searchParams.set("auth_token", btoa(`${username}:${password}`))
|
||||||
|
// For same-origin requests, let the browser reuse the page's existing auth.
|
||||||
next.username = username
|
next.username = username
|
||||||
next.password = password
|
next.password = password
|
||||||
|
}
|
||||||
|
|
||||||
const socket = new WebSocket(next)
|
const socket = new WebSocket(next)
|
||||||
socket.binaryType = "arraybuffer"
|
socket.binaryType = "arraybuffer"
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import type {
|
|||||||
import type { State, VcsCache } from "./types"
|
import type { State, VcsCache } from "./types"
|
||||||
import { trimSessions } from "./session-trim"
|
import { trimSessions } from "./session-trim"
|
||||||
import { dropSessionCaches } from "./session-cache"
|
import { dropSessionCaches } from "./session-cache"
|
||||||
|
import { diffs as list, message as clean } from "@/utils/diffs"
|
||||||
|
|
||||||
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
||||||
|
|
||||||
@@ -162,7 +163,7 @@ export function applyDirectoryEvent(input: {
|
|||||||
}
|
}
|
||||||
case "session.diff": {
|
case "session.diff": {
|
||||||
const props = event.properties as { sessionID: string; diff: SnapshotFileDiff[] }
|
const props = event.properties as { sessionID: string; diff: SnapshotFileDiff[] }
|
||||||
input.setStore("session_diff", props.sessionID, reconcile(props.diff, { key: "file" }))
|
input.setStore("session_diff", props.sessionID, reconcile(list(props.diff), { key: "file" }))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "todo.updated": {
|
case "todo.updated": {
|
||||||
@@ -177,7 +178,7 @@ export function applyDirectoryEvent(input: {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "message.updated": {
|
case "message.updated": {
|
||||||
const info = (event.properties as { info: Message }).info
|
const info = clean((event.properties as { info: Message }).info)
|
||||||
const messages = input.store.message[info.sessionID]
|
const messages = input.store.message[info.sessionID]
|
||||||
if (!messages) {
|
if (!messages) {
|
||||||
input.setStore("message", info.sessionID, [info])
|
input.setStore("message", info.sessionID, [info])
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { useGlobalSync } from "./global-sync"
|
|||||||
import { useSDK } from "./sdk"
|
import { useSDK } from "./sdk"
|
||||||
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
|
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
|
||||||
import { SESSION_CACHE_LIMIT, dropSessionCaches, pickSessionCacheEvictions } from "./global-sync/session-cache"
|
import { SESSION_CACHE_LIMIT, dropSessionCaches, pickSessionCacheEvictions } from "./global-sync/session-cache"
|
||||||
|
import { diffs as list, message as clean } from "@/utils/diffs"
|
||||||
|
|
||||||
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
||||||
|
|
||||||
@@ -300,7 +301,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
input.client.session.messages({ sessionID: input.sessionID, limit: input.limit, before: input.before }),
|
input.client.session.messages({ sessionID: input.sessionID, limit: input.limit, before: input.before }),
|
||||||
)
|
)
|
||||||
const items = (messages.data ?? []).filter((x) => !!x?.info?.id)
|
const items = (messages.data ?? []).filter((x) => !!x?.info?.id)
|
||||||
const session = items.map((x) => x.info).sort((a, b) => cmp(a.id, b.id))
|
const session = items.map((x) => clean(x.info)).sort((a, b) => cmp(a.id, b.id))
|
||||||
const part = items.map((message) => ({ id: message.info.id, part: sortParts(message.parts) }))
|
const part = items.map((message) => ({ id: message.info.id, part: sortParts(message.parts) }))
|
||||||
const cursor = messages.response.headers.get("x-next-cursor") ?? undefined
|
const cursor = messages.response.headers.get("x-next-cursor") ?? undefined
|
||||||
return {
|
return {
|
||||||
@@ -509,7 +510,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
return runInflight(inflightDiff, key, () =>
|
return runInflight(inflightDiff, key, () =>
|
||||||
retry(() => client.session.diff({ sessionID })).then((diff) => {
|
retry(() => client.session.diff({ sessionID })).then((diff) => {
|
||||||
if (!tracked(directory, sessionID)) return
|
if (!tracked(directory, sessionID)) return
|
||||||
setStore("session_diff", sessionID, reconcile(diff.data ?? [], { key: "file" }))
|
setStore("session_diff", sessionID, reconcile(list(diff.data), { key: "file" }))
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ import { TerminalPanel } from "@/pages/session/terminal-panel"
|
|||||||
import { useSessionCommands } from "@/pages/session/use-session-commands"
|
import { useSessionCommands } from "@/pages/session/use-session-commands"
|
||||||
import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
|
import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
|
||||||
import { Identifier } from "@/utils/id"
|
import { Identifier } from "@/utils/id"
|
||||||
|
import { diffs as list } from "@/utils/diffs"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import { extractPromptFromParts } from "@/utils/prompt"
|
import { extractPromptFromParts } from "@/utils/prompt"
|
||||||
import { same } from "@/utils/same"
|
import { same } from "@/utils/same"
|
||||||
@@ -430,7 +431,7 @@ export default function Page() {
|
|||||||
|
|
||||||
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
||||||
const isChildSession = createMemo(() => !!info()?.parentID)
|
const isChildSession = createMemo(() => !!info()?.parentID)
|
||||||
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
|
const diffs = createMemo(() => (params.id ? list(sync.data.session_diff[params.id]) : []))
|
||||||
const sessionCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length))
|
const sessionCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length))
|
||||||
const hasSessionReview = createMemo(() => sessionCount() > 0)
|
const hasSessionReview = createMemo(() => sessionCount() > 0)
|
||||||
const canReview = createMemo(() => !!sync.project)
|
const canReview = createMemo(() => !!sync.project)
|
||||||
@@ -611,7 +612,7 @@ export default function Page() {
|
|||||||
.diff({ mode })
|
.diff({ mode })
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (vcsRun.get(mode) !== run) return
|
if (vcsRun.get(mode) !== run) return
|
||||||
setVcs("diff", mode, result.data ?? [])
|
setVcs("diff", mode, list(result.data))
|
||||||
setVcs("ready", mode, true)
|
setVcs("ready", mode, true)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -649,7 +650,7 @@ export default function Page() {
|
|||||||
return open
|
return open
|
||||||
}, desktopReviewOpen())
|
}, desktopReviewOpen())
|
||||||
|
|
||||||
const turnDiffs = createMemo(() => lastUserMessage()?.summary?.diffs ?? [])
|
const turnDiffs = createMemo(() => list(lastUserMessage()?.summary?.diffs))
|
||||||
const nogit = createMemo(() => !!sync.project && sync.project.vcs !== "git")
|
const nogit = createMemo(() => !!sync.project && sync.project.vcs !== "git")
|
||||||
const changesOptions = createMemo<ChangeMode[]>(() => {
|
const changesOptions = createMemo<ChangeMode[]>(() => {
|
||||||
const list: ChangeMode[] = []
|
const list: ChangeMode[] = []
|
||||||
@@ -669,15 +670,11 @@ export default function Page() {
|
|||||||
if (store.changes === "git" || store.changes === "branch") return store.changes
|
if (store.changes === "git" || store.changes === "branch") return store.changes
|
||||||
})
|
})
|
||||||
const reviewDiffs = createMemo(() => {
|
const reviewDiffs = createMemo(() => {
|
||||||
if (store.changes === "git") return vcs.diff.git
|
if (store.changes === "git") return list(vcs.diff.git)
|
||||||
if (store.changes === "branch") return vcs.diff.branch
|
if (store.changes === "branch") return list(vcs.diff.branch)
|
||||||
return turnDiffs()
|
return turnDiffs()
|
||||||
})
|
})
|
||||||
const reviewCount = createMemo(() => {
|
const reviewCount = createMemo(() => reviewDiffs().length)
|
||||||
if (store.changes === "git") return vcs.diff.git.length
|
|
||||||
if (store.changes === "branch") return vcs.diff.branch.length
|
|
||||||
return turnDiffs().length
|
|
||||||
})
|
|
||||||
const hasReview = createMemo(() => reviewCount() > 0)
|
const hasReview = createMemo(() => reviewCount() > 0)
|
||||||
const reviewReady = createMemo(() => {
|
const reviewReady = createMemo(() => {
|
||||||
if (store.changes === "git") return vcs.ready.git
|
if (store.changes === "git") return vcs.ready.git
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import type { SnapshotFileDiff } from "@opencode-ai/sdk/v2"
|
||||||
|
import type { Message } from "@opencode-ai/sdk/v2/client"
|
||||||
|
import { diffs, message } from "./diffs"
|
||||||
|
|
||||||
|
const item = {
|
||||||
|
file: "src/app.ts",
|
||||||
|
patch: "@@ -1 +1 @@\n-old\n+new\n",
|
||||||
|
additions: 1,
|
||||||
|
deletions: 1,
|
||||||
|
status: "modified",
|
||||||
|
} satisfies SnapshotFileDiff
|
||||||
|
|
||||||
|
describe("diffs", () => {
|
||||||
|
test("keeps valid arrays", () => {
|
||||||
|
expect(diffs([item])).toEqual([item])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("wraps a single diff object", () => {
|
||||||
|
expect(diffs(item)).toEqual([item])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("reads keyed diff objects", () => {
|
||||||
|
expect(diffs({ a: item })).toEqual([item])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("drops invalid entries", () => {
|
||||||
|
expect(
|
||||||
|
diffs([
|
||||||
|
item,
|
||||||
|
{ file: "src/bad.ts", additions: 1, deletions: 1 },
|
||||||
|
{ patch: item.patch, additions: 1, deletions: 1 },
|
||||||
|
]),
|
||||||
|
).toEqual([item])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("message", () => {
|
||||||
|
test("normalizes user summaries with object diffs", () => {
|
||||||
|
const input = {
|
||||||
|
id: "msg_1",
|
||||||
|
sessionID: "ses_1",
|
||||||
|
role: "user",
|
||||||
|
time: { created: 1 },
|
||||||
|
agent: "build",
|
||||||
|
model: { providerID: "openai", modelID: "gpt-5" },
|
||||||
|
summary: {
|
||||||
|
title: "Edit",
|
||||||
|
diffs: { a: item },
|
||||||
|
},
|
||||||
|
} as unknown as Message
|
||||||
|
|
||||||
|
expect(message(input)).toMatchObject({
|
||||||
|
summary: {
|
||||||
|
title: "Edit",
|
||||||
|
diffs: [item],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("drops invalid user summaries", () => {
|
||||||
|
const input = {
|
||||||
|
id: "msg_1",
|
||||||
|
sessionID: "ses_1",
|
||||||
|
role: "user",
|
||||||
|
time: { created: 1 },
|
||||||
|
agent: "build",
|
||||||
|
model: { providerID: "openai", modelID: "gpt-5" },
|
||||||
|
summary: true,
|
||||||
|
} as unknown as Message
|
||||||
|
|
||||||
|
expect(message(input)).toMatchObject({ summary: undefined })
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
|
||||||
|
import type { Message } from "@opencode-ai/sdk/v2/client"
|
||||||
|
|
||||||
|
type Diff = SnapshotFileDiff | VcsFileDiff
|
||||||
|
|
||||||
|
function diff(value: unknown): value is Diff {
|
||||||
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false
|
||||||
|
if (!("file" in value) || typeof value.file !== "string") return false
|
||||||
|
if (!("patch" in value) || typeof value.patch !== "string") return false
|
||||||
|
if (!("additions" in value) || typeof value.additions !== "number") return false
|
||||||
|
if (!("deletions" in value) || typeof value.deletions !== "number") return false
|
||||||
|
if (!("status" in value) || value.status === undefined) return true
|
||||||
|
return value.status === "added" || value.status === "deleted" || value.status === "modified"
|
||||||
|
}
|
||||||
|
|
||||||
|
function object(value: unknown): value is Record<string, unknown> {
|
||||||
|
return !!value && typeof value === "object" && !Array.isArray(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function diffs(value: unknown): Diff[] {
|
||||||
|
if (Array.isArray(value) && value.every(diff)) return value
|
||||||
|
if (Array.isArray(value)) return value.filter(diff)
|
||||||
|
if (diff(value)) return [value]
|
||||||
|
if (!object(value)) return []
|
||||||
|
return Object.values(value).filter(diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function message(value: Message): Message {
|
||||||
|
if (value.role !== "user") return value
|
||||||
|
|
||||||
|
const raw = value.summary as unknown
|
||||||
|
if (raw === undefined) return value
|
||||||
|
if (!object(raw)) return { ...value, summary: undefined }
|
||||||
|
|
||||||
|
const title = typeof raw.title === "string" ? raw.title : undefined
|
||||||
|
const body = typeof raw.body === "string" ? raw.body : undefined
|
||||||
|
const next = diffs(raw.diffs)
|
||||||
|
|
||||||
|
if (title === raw.title && body === raw.body && next === raw.diffs) return value
|
||||||
|
|
||||||
|
return {
|
||||||
|
...value,
|
||||||
|
summary: {
|
||||||
|
...(title === undefined ? {} : { title }),
|
||||||
|
...(body === undefined ? {} : { body }),
|
||||||
|
diffs: next,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.4.0",
|
"version": "1.4.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.4.0",
|
"version": "1.4.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.4.0",
|
"version": "1.4.3",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.4.0",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
|
|||||||
@@ -34,11 +34,6 @@ const getBase = (): Configuration => ({
|
|||||||
},
|
},
|
||||||
files: ["out/**/*", "resources/**/*"],
|
files: ["out/**/*", "resources/**/*"],
|
||||||
extraResources: [
|
extraResources: [
|
||||||
{
|
|
||||||
from: "resources/",
|
|
||||||
to: "",
|
|
||||||
filter: ["opencode-cli*"],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
from: "native/",
|
from: "native/",
|
||||||
to: "native/",
|
to: "native/",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { defineConfig } from "electron-vite"
|
import { defineConfig } from "electron-vite"
|
||||||
import appPlugin from "@opencode-ai/app/vite"
|
import appPlugin from "@opencode-ai/app/vite"
|
||||||
|
import * as fs from "node:fs/promises"
|
||||||
|
|
||||||
const channel = (() => {
|
const channel = (() => {
|
||||||
const raw = process.env.OPENCODE_CHANNEL
|
const raw = process.env.OPENCODE_CHANNEL
|
||||||
@@ -7,6 +8,10 @@ const channel = (() => {
|
|||||||
return "dev"
|
return "dev"
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
const OPENCODE_SERVER_DIST = "../opencode/dist/node"
|
||||||
|
|
||||||
|
const nodePtyPkg = `@lydell/node-pty-${process.platform}-${process.arch}`
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
main: {
|
main: {
|
||||||
define: {
|
define: {
|
||||||
@@ -16,7 +21,33 @@ export default defineConfig({
|
|||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: { index: "src/main/index.ts" },
|
input: { index: "src/main/index.ts" },
|
||||||
},
|
},
|
||||||
|
externalizeDeps: { include: [nodePtyPkg] },
|
||||||
},
|
},
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
name: "opencode:node-pty-narrower",
|
||||||
|
enforce: "pre",
|
||||||
|
resolveId(s) {
|
||||||
|
if (s === "@lydell/node-pty") return nodePtyPkg
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "opencode:virtual-server-module",
|
||||||
|
enforce: "pre",
|
||||||
|
resolveId(id) {
|
||||||
|
if (id === "virtual:opencode-server") return this.resolve(`${OPENCODE_SERVER_DIST}/node.js`)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "opencode:copy-server-assets",
|
||||||
|
async writeBundle() {
|
||||||
|
for (const l of await fs.readdir(OPENCODE_SERVER_DIST)) {
|
||||||
|
if (!l.endsWith(".wasm")) continue
|
||||||
|
await fs.writeFile(`./out/main/chunks/${l}`, await fs.readFile(`${OPENCODE_SERVER_DIST}/${l}`))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
preload: {
|
preload: {
|
||||||
build: {
|
build: {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/desktop-electron",
|
"name": "@opencode-ai/desktop-electron",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.4.0",
|
"version": "1.4.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://opencode.ai",
|
"homepage": "https://opencode.ai",
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"typecheck": "tsgo -b",
|
"typecheck": "tsgo -b",
|
||||||
"predev": "bun ./scripts/predev.ts",
|
"predev": "bun ./scripts/predev.ts",
|
||||||
"dev": "electron-vite dev",
|
"dev": "electron-vite dev",
|
||||||
"prebuild": "bun ./scripts/copy-icons.ts",
|
"prebuild": "bun ./scripts/prebuild.ts",
|
||||||
"build": "electron-vite build",
|
"build": "electron-vite build",
|
||||||
"preview": "electron-vite preview",
|
"preview": "electron-vite preview",
|
||||||
"package": "electron-builder --config electron-builder.config.ts",
|
"package": "electron-builder --config electron-builder.config.ts",
|
||||||
@@ -24,31 +24,42 @@
|
|||||||
},
|
},
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/app": "workspace:*",
|
|
||||||
"@opencode-ai/ui": "workspace:*",
|
|
||||||
"@solid-primitives/i18n": "2.2.1",
|
|
||||||
"@solid-primitives/storage": "catalog:",
|
|
||||||
"@solidjs/meta": "catalog:",
|
|
||||||
"@solidjs/router": "0.15.4",
|
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"electron-context-menu": "4.1.2",
|
"electron-context-menu": "4.1.2",
|
||||||
"electron-log": "^5",
|
"electron-log": "^5",
|
||||||
"electron-store": "^10",
|
"electron-store": "^10",
|
||||||
"electron-updater": "^6",
|
"electron-updater": "^6",
|
||||||
"electron-window-state": "^5.0.3",
|
"electron-window-state": "^5.0.3",
|
||||||
"marked": "^15",
|
"marked": "^15"
|
||||||
"solid-js": "catalog:",
|
|
||||||
"tree-kill": "^1.2.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@actions/artifact": "4.0.0",
|
"@actions/artifact": "4.0.0",
|
||||||
|
"@lydell/node-pty": "catalog:",
|
||||||
|
"@opencode-ai/app": "workspace:*",
|
||||||
|
"@opencode-ai/ui": "workspace:*",
|
||||||
|
"@solid-primitives/i18n": "2.2.1",
|
||||||
|
"@solid-primitives/storage": "catalog:",
|
||||||
|
"@solidjs/meta": "catalog:",
|
||||||
|
"@solidjs/router": "0.15.4",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
|
"@valibot/to-json-schema": "1.6.0",
|
||||||
"electron": "40.4.1",
|
"electron": "40.4.1",
|
||||||
"electron-builder": "^26",
|
"electron-builder": "^26",
|
||||||
"electron-vite": "^5",
|
"electron-vite": "^5",
|
||||||
|
"solid-js": "catalog:",
|
||||||
|
"sury": "11.0.0-alpha.4",
|
||||||
"typescript": "~5.6.2",
|
"typescript": "~5.6.2",
|
||||||
"vite": "catalog:"
|
"vite": "catalog:",
|
||||||
|
"zod-openapi": "5.4.6"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@lydell/node-pty-darwin-arm64": "1.2.0-beta.10",
|
||||||
|
"@lydell/node-pty-darwin-x64": "1.2.0-beta.10",
|
||||||
|
"@lydell/node-pty-linux-arm64": "1.2.0-beta.10",
|
||||||
|
"@lydell/node-pty-linux-x64": "1.2.0-beta.10",
|
||||||
|
"@lydell/node-pty-win32-arm64": "1.2.0-beta.10",
|
||||||
|
"@lydell/node-pty-win32-x64": "1.2.0-beta.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
import { $ } from "bun"
|
||||||
|
|
||||||
|
import { resolveChannel } from "./utils"
|
||||||
|
|
||||||
|
const channel = resolveChannel()
|
||||||
|
await $`bun ./scripts/copy-icons.ts ${channel}`
|
||||||
|
|
||||||
|
await $`cd ../opencode && bun script/build-node.ts`
|
||||||
@@ -1,17 +1,5 @@
|
|||||||
import { $ } from "bun"
|
import { $ } from "bun"
|
||||||
|
|
||||||
import { copyBinaryToSidecarFolder, getCurrentSidecar, windowsify } from "./utils"
|
|
||||||
|
|
||||||
await $`bun ./scripts/copy-icons.ts ${process.env.OPENCODE_CHANNEL ?? "dev"}`
|
await $`bun ./scripts/copy-icons.ts ${process.env.OPENCODE_CHANNEL ?? "dev"}`
|
||||||
|
|
||||||
const RUST_TARGET = Bun.env.RUST_TARGET
|
await $`cd ../opencode && bun script/build-node.ts`
|
||||||
|
|
||||||
const sidecarConfig = getCurrentSidecar(RUST_TARGET)
|
|
||||||
|
|
||||||
const binaryPath = windowsify(`../opencode/dist/${sidecarConfig.ocBinary}/bin/opencode`)
|
|
||||||
|
|
||||||
await (sidecarConfig.ocBinary.includes("-baseline")
|
|
||||||
? $`cd ../opencode && bun run build --single --baseline`
|
|
||||||
: $`cd ../opencode && bun run build --single`)
|
|
||||||
|
|
||||||
await copyBinaryToSidecarFolder(binaryPath, RUST_TARGET)
|
|
||||||
|
|||||||
@@ -1,25 +1,9 @@
|
|||||||
#!/usr/bin/env bun
|
#!/usr/bin/env bun
|
||||||
import { $ } from "bun"
|
|
||||||
|
|
||||||
import { Script } from "@opencode-ai/script"
|
import { Script } from "@opencode-ai/script"
|
||||||
import { copyBinaryToSidecarFolder, getCurrentSidecar, resolveChannel, windowsify } from "./utils"
|
|
||||||
|
|
||||||
const channel = resolveChannel()
|
await import("./prebuild")
|
||||||
await $`bun ./scripts/copy-icons.ts ${channel}`
|
|
||||||
|
|
||||||
const pkg = await Bun.file("./package.json").json()
|
const pkg = await Bun.file("./package.json").json()
|
||||||
pkg.version = Script.version
|
pkg.version = Script.version
|
||||||
await Bun.write("./package.json", JSON.stringify(pkg, null, 2) + "\n")
|
await Bun.write("./package.json", JSON.stringify(pkg, null, 2) + "\n")
|
||||||
console.log(`Updated package.json version to ${Script.version}`)
|
console.log(`Updated package.json version to ${Script.version}`)
|
||||||
|
|
||||||
const sidecarConfig = getCurrentSidecar()
|
|
||||||
const artifact = process.env.OPENCODE_CLI_ARTIFACT ?? "opencode-cli"
|
|
||||||
|
|
||||||
const dir = "resources/opencode-binaries"
|
|
||||||
|
|
||||||
await $`mkdir -p ${dir}`
|
|
||||||
await $`gh run download ${process.env.GITHUB_RUN_ID} -n ${artifact}`.cwd(dir)
|
|
||||||
|
|
||||||
await copyBinaryToSidecarFolder(windowsify(`${dir}/${sidecarConfig.ocBinary}/bin/opencode`))
|
|
||||||
|
|
||||||
await $`rm -rf ${dir}`
|
|
||||||
|
|||||||
@@ -1,283 +0,0 @@
|
|||||||
import { execFileSync, spawn } from "node:child_process"
|
|
||||||
import { EventEmitter } from "node:events"
|
|
||||||
import { chmodSync, readFileSync, unlinkSync, writeFileSync } from "node:fs"
|
|
||||||
import { tmpdir } from "node:os"
|
|
||||||
import { dirname, join } from "node:path"
|
|
||||||
import readline from "node:readline"
|
|
||||||
import { fileURLToPath } from "node:url"
|
|
||||||
import { app } from "electron"
|
|
||||||
import treeKill from "tree-kill"
|
|
||||||
|
|
||||||
import { WSL_ENABLED_KEY } from "./constants"
|
|
||||||
import { getUserShell, loadShellEnv, mergeShellEnv } from "./shell-env"
|
|
||||||
import { store } from "./store"
|
|
||||||
|
|
||||||
const CLI_INSTALL_DIR = ".opencode/bin"
|
|
||||||
const CLI_BINARY_NAME = "opencode"
|
|
||||||
|
|
||||||
export type ServerConfig = {
|
|
||||||
hostname?: string
|
|
||||||
port?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Config = {
|
|
||||||
server?: ServerConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TerminatedPayload = { code: number | null; signal: number | null }
|
|
||||||
|
|
||||||
export type CommandEvent =
|
|
||||||
| { type: "stdout"; value: string }
|
|
||||||
| { type: "stderr"; value: string }
|
|
||||||
| { type: "error"; value: string }
|
|
||||||
| { type: "terminated"; value: TerminatedPayload }
|
|
||||||
| { type: "sqlite"; value: SqliteMigrationProgress }
|
|
||||||
|
|
||||||
export type SqliteMigrationProgress = { type: "InProgress"; value: number } | { type: "Done" }
|
|
||||||
|
|
||||||
export type CommandChild = {
|
|
||||||
pid: number | undefined
|
|
||||||
kill: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const root = dirname(fileURLToPath(import.meta.url))
|
|
||||||
|
|
||||||
export function getSidecarPath() {
|
|
||||||
const suffix = process.platform === "win32" ? ".exe" : ""
|
|
||||||
const path = app.isPackaged
|
|
||||||
? join(process.resourcesPath, `opencode-cli${suffix}`)
|
|
||||||
: join(root, "../../resources", `opencode-cli${suffix}`)
|
|
||||||
console.log(`[cli] Sidecar path resolved: ${path} (isPackaged: ${app.isPackaged})`)
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getConfig(): Promise<Config | null> {
|
|
||||||
const { events } = spawnCommand("debug config", {})
|
|
||||||
let output = ""
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
events.on("stdout", (line: string) => {
|
|
||||||
output += line
|
|
||||||
})
|
|
||||||
events.on("stderr", (line: string) => {
|
|
||||||
output += line
|
|
||||||
})
|
|
||||||
events.on("terminated", () => resolve())
|
|
||||||
events.on("error", () => resolve())
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
return JSON.parse(output) as Config
|
|
||||||
} catch {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function installCli(): Promise<string> {
|
|
||||||
if (process.platform === "win32") {
|
|
||||||
throw new Error("CLI installation is only supported on macOS & Linux")
|
|
||||||
}
|
|
||||||
|
|
||||||
const sidecar = getSidecarPath()
|
|
||||||
const scriptPath = join(app.getAppPath(), "install")
|
|
||||||
const script = readFileSync(scriptPath, "utf8")
|
|
||||||
const tempScript = join(tmpdir(), "opencode-install.sh")
|
|
||||||
|
|
||||||
writeFileSync(tempScript, script, "utf8")
|
|
||||||
chmodSync(tempScript, 0o755)
|
|
||||||
|
|
||||||
const cmd = spawn(tempScript, ["--binary", sidecar], { stdio: "pipe" })
|
|
||||||
return await new Promise<string>((resolve, reject) => {
|
|
||||||
cmd.on("exit", (code: number | null) => {
|
|
||||||
try {
|
|
||||||
unlinkSync(tempScript)
|
|
||||||
} catch {}
|
|
||||||
if (code === 0) {
|
|
||||||
const installPath = getCliInstallPath()
|
|
||||||
if (installPath) return resolve(installPath)
|
|
||||||
return reject(new Error("Could not determine install path"))
|
|
||||||
}
|
|
||||||
reject(new Error("Install script failed"))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function syncCli() {
|
|
||||||
if (!app.isPackaged) return
|
|
||||||
const installPath = getCliInstallPath()
|
|
||||||
if (!installPath) return
|
|
||||||
|
|
||||||
let version = ""
|
|
||||||
try {
|
|
||||||
version = execFileSync(installPath, ["--version"], { windowsHide: true }).toString().trim()
|
|
||||||
} catch {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const cli = parseVersion(version)
|
|
||||||
const appVersion = parseVersion(app.getVersion())
|
|
||||||
if (!cli || !appVersion) return
|
|
||||||
if (compareVersions(cli, appVersion) >= 0) return
|
|
||||||
void installCli().catch(() => undefined)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function serve(hostname: string, port: number, password: string) {
|
|
||||||
const args = `--print-logs --log-level WARN serve --hostname ${hostname} --port ${port}`
|
|
||||||
const env = {
|
|
||||||
OPENCODE_SERVER_USERNAME: "opencode",
|
|
||||||
OPENCODE_SERVER_PASSWORD: password,
|
|
||||||
}
|
|
||||||
|
|
||||||
return spawnCommand(args, env)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function spawnCommand(args: string, extraEnv: Record<string, string>) {
|
|
||||||
console.log(`[cli] Spawning command with args: ${args}`)
|
|
||||||
const base = Object.fromEntries(
|
|
||||||
Object.entries(process.env).filter((entry): entry is [string, string] => typeof entry[1] === "string"),
|
|
||||||
)
|
|
||||||
const env = {
|
|
||||||
...base,
|
|
||||||
OPENCODE_EXPERIMENTAL_ICON_DISCOVERY: "true",
|
|
||||||
OPENCODE_EXPERIMENTAL_FILEWATCHER: "true",
|
|
||||||
OPENCODE_CLIENT: "desktop",
|
|
||||||
XDG_STATE_HOME: app.getPath("userData"),
|
|
||||||
...extraEnv,
|
|
||||||
}
|
|
||||||
const shell = process.platform === "win32" ? null : getUserShell()
|
|
||||||
const envs = shell ? mergeShellEnv(loadShellEnv(shell), env) : env
|
|
||||||
|
|
||||||
const { cmd, cmdArgs } = buildCommand(args, envs, shell)
|
|
||||||
console.log(`[cli] Executing: ${cmd} ${cmdArgs.join(" ")}`)
|
|
||||||
const child = spawn(cmd, cmdArgs, {
|
|
||||||
env: envs,
|
|
||||||
detached: process.platform !== "win32",
|
|
||||||
windowsHide: true,
|
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
|
||||||
})
|
|
||||||
console.log(`[cli] Spawned process with PID: ${child.pid}`)
|
|
||||||
|
|
||||||
const events = new EventEmitter()
|
|
||||||
const exit = new Promise<TerminatedPayload>((resolve) => {
|
|
||||||
child.on("exit", (code: number | null, signal: NodeJS.Signals | null) => {
|
|
||||||
console.log(`[cli] Process exited with code: ${code}, signal: ${signal}`)
|
|
||||||
resolve({ code: code ?? null, signal: null })
|
|
||||||
})
|
|
||||||
child.on("error", (error: Error) => {
|
|
||||||
console.error(`[cli] Process error: ${error.message}`)
|
|
||||||
events.emit("error", error.message)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const stdout = child.stdout
|
|
||||||
const stderr = child.stderr
|
|
||||||
|
|
||||||
if (stdout) {
|
|
||||||
readline.createInterface({ input: stdout }).on("line", (line: string) => {
|
|
||||||
if (handleSqliteProgress(events, line)) return
|
|
||||||
events.emit("stdout", `${line}\n`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stderr) {
|
|
||||||
readline.createInterface({ input: stderr }).on("line", (line: string) => {
|
|
||||||
if (handleSqliteProgress(events, line)) return
|
|
||||||
events.emit("stderr", `${line}\n`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
exit.then((payload) => {
|
|
||||||
events.emit("terminated", payload)
|
|
||||||
})
|
|
||||||
|
|
||||||
const kill = () => {
|
|
||||||
if (!child.pid) return
|
|
||||||
treeKill(child.pid)
|
|
||||||
}
|
|
||||||
|
|
||||||
return { events, child: { pid: child.pid, kill }, exit }
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSqliteProgress(events: EventEmitter, line: string) {
|
|
||||||
const stripped = line.startsWith("sqlite-migration:") ? line.slice("sqlite-migration:".length).trim() : null
|
|
||||||
if (!stripped) return false
|
|
||||||
if (stripped === "done") {
|
|
||||||
events.emit("sqlite", { type: "Done" })
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
const value = Number.parseInt(stripped, 10)
|
|
||||||
if (!Number.isNaN(value)) {
|
|
||||||
events.emit("sqlite", { type: "InProgress", value })
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildCommand(args: string, env: Record<string, string>, shell: string | null) {
|
|
||||||
if (process.platform === "win32" && isWslEnabled()) {
|
|
||||||
console.log(`[cli] Using WSL mode`)
|
|
||||||
const version = app.getVersion()
|
|
||||||
const script = [
|
|
||||||
"set -e",
|
|
||||||
'BIN="$HOME/.opencode/bin/opencode"',
|
|
||||||
'if [ ! -x "$BIN" ]; then',
|
|
||||||
` curl -fsSL https://opencode.ai/install | bash -s -- --version ${shellEscape(version)} --no-modify-path`,
|
|
||||||
"fi",
|
|
||||||
`${envPrefix(env)} exec "$BIN" ${args}`,
|
|
||||||
].join("\n")
|
|
||||||
|
|
||||||
return { cmd: "wsl", cmdArgs: ["-e", "bash", "-lc", script] }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.platform === "win32") {
|
|
||||||
const sidecar = getSidecarPath()
|
|
||||||
console.log(`[cli] Windows direct mode, sidecar: ${sidecar}`)
|
|
||||||
return { cmd: sidecar, cmdArgs: args.split(" ") }
|
|
||||||
}
|
|
||||||
|
|
||||||
const sidecar = getSidecarPath()
|
|
||||||
const user = shell || getUserShell()
|
|
||||||
const line = user.endsWith("/nu") ? `^\"${sidecar}\" ${args}` : `\"${sidecar}\" ${args}`
|
|
||||||
console.log(`[cli] Unix mode, shell: ${user}, command: ${line}`)
|
|
||||||
return { cmd: user, cmdArgs: ["-l", "-c", line] }
|
|
||||||
}
|
|
||||||
|
|
||||||
function envPrefix(env: Record<string, string>) {
|
|
||||||
const entries = Object.entries(env).map(([key, value]) => `${key}=${shellEscape(value)}`)
|
|
||||||
return entries.join(" ")
|
|
||||||
}
|
|
||||||
|
|
||||||
function shellEscape(input: string) {
|
|
||||||
if (!input) return "''"
|
|
||||||
return `'${input.replace(/'/g, `'"'"'`)}'`
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCliInstallPath() {
|
|
||||||
const home = process.env.HOME
|
|
||||||
if (!home) return null
|
|
||||||
return join(home, CLI_INSTALL_DIR, CLI_BINARY_NAME)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isWslEnabled() {
|
|
||||||
return store.get(WSL_ENABLED_KEY) === true
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseVersion(value: string) {
|
|
||||||
const parts = value
|
|
||||||
.replace(/^v/, "")
|
|
||||||
.split(".")
|
|
||||||
.map((part) => Number.parseInt(part, 10))
|
|
||||||
if (parts.some((part) => Number.isNaN(part))) return null
|
|
||||||
return parts
|
|
||||||
}
|
|
||||||
|
|
||||||
function compareVersions(a: number[], b: number[]) {
|
|
||||||
const len = Math.max(a.length, b.length)
|
|
||||||
for (let i = 0; i < len; i += 1) {
|
|
||||||
const left = a[i] ?? 0
|
|
||||||
const right = b[i] ?? 0
|
|
||||||
if (left > right) return 1
|
|
||||||
if (left < right) return -1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
+22
@@ -5,3 +5,25 @@ interface ImportMetaEnv {
|
|||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
readonly env: ImportMetaEnv
|
readonly env: ImportMetaEnv
|
||||||
}
|
}
|
||||||
|
declare module "virtual:opencode-server" {
|
||||||
|
export namespace Server {
|
||||||
|
export const listen: typeof import("../../../opencode/dist/types/src/node").Server.listen
|
||||||
|
export type Listener = import("../../../opencode/dist/types/src/node").Server.Listener
|
||||||
|
}
|
||||||
|
export namespace Config {
|
||||||
|
export const get: typeof import("../../../opencode/dist/types/src/node").Config.get
|
||||||
|
export type Info = import("../../../opencode/dist/types/src/node").Config.Info
|
||||||
|
}
|
||||||
|
export namespace Log {
|
||||||
|
export const init: typeof import("../../../opencode/dist/types/src/node").Log.init
|
||||||
|
}
|
||||||
|
export namespace Database {
|
||||||
|
export const Path: typeof import("../../../opencode/dist/types/src/node").Database.Path
|
||||||
|
export const Client: typeof import("../../../opencode/dist/types/src/node").Database.Client
|
||||||
|
}
|
||||||
|
export namespace JsonMigration {
|
||||||
|
export type Progress = import("../../../opencode/dist/types/src/node").JsonMigration.Progress
|
||||||
|
export const run: typeof import("../../../opencode/dist/types/src/node").JsonMigration.run
|
||||||
|
}
|
||||||
|
export const bootstrap: typeof import("../../../opencode/dist/types/src/node").bootstrap
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import pkg from "electron-updater"
|
|||||||
import contextMenu from "electron-context-menu"
|
import contextMenu from "electron-context-menu"
|
||||||
contextMenu({ showSaveImageAs: true, showLookUpSelection: false, showSearchWithGoogle: false })
|
contextMenu({ showSaveImageAs: true, showLookUpSelection: false, showSearchWithGoogle: false })
|
||||||
|
|
||||||
|
process.env.OPENCODE_DISABLE_EMBEDDED_WEB_UI = "true"
|
||||||
|
|
||||||
const APP_NAMES: Record<string, string> = {
|
const APP_NAMES: Record<string, string> = {
|
||||||
dev: "OpenCode Dev",
|
dev: "OpenCode Dev",
|
||||||
beta: "OpenCode Beta",
|
beta: "OpenCode Beta",
|
||||||
@@ -27,8 +29,6 @@ const { autoUpdater } = pkg
|
|||||||
|
|
||||||
import type { InitStep, ServerReadyData, SqliteMigrationProgress, WslConfig } from "../preload/types"
|
import type { InitStep, ServerReadyData, SqliteMigrationProgress, WslConfig } from "../preload/types"
|
||||||
import { checkAppExists, resolveAppPath, wslPath } from "./apps"
|
import { checkAppExists, resolveAppPath, wslPath } from "./apps"
|
||||||
import type { CommandChild } from "./cli"
|
|
||||||
import { installCli, syncCli } from "./cli"
|
|
||||||
import { CHANNEL, UPDATER_ENABLED } from "./constants"
|
import { CHANNEL, UPDATER_ENABLED } from "./constants"
|
||||||
import { registerIpcHandlers, sendDeepLinks, sendMenuCommand, sendSqliteMigrationProgress } from "./ipc"
|
import { registerIpcHandlers, sendDeepLinks, sendMenuCommand, sendSqliteMigrationProgress } from "./ipc"
|
||||||
import { initLogging } from "./logging"
|
import { initLogging } from "./logging"
|
||||||
@@ -36,12 +36,13 @@ import { parseMarkdown } from "./markdown"
|
|||||||
import { createMenu } from "./menu"
|
import { createMenu } from "./menu"
|
||||||
import { getDefaultServerUrl, getWslConfig, setDefaultServerUrl, setWslConfig, spawnLocalServer } from "./server"
|
import { getDefaultServerUrl, getWslConfig, setDefaultServerUrl, setWslConfig, spawnLocalServer } from "./server"
|
||||||
import { createLoadingWindow, createMainWindow, setBackgroundColor, setDockIcon } from "./windows"
|
import { createLoadingWindow, createMainWindow, setBackgroundColor, setDockIcon } from "./windows"
|
||||||
|
import type { Server } from "virtual:opencode-server"
|
||||||
|
|
||||||
const initEmitter = new EventEmitter()
|
const initEmitter = new EventEmitter()
|
||||||
let initStep: InitStep = { phase: "server_waiting" }
|
let initStep: InitStep = { phase: "server_waiting" }
|
||||||
|
|
||||||
let mainWindow: BrowserWindow | null = null
|
let mainWindow: BrowserWindow | null = null
|
||||||
let sidecar: CommandChild | null = null
|
let server: Server.Listener | null = null
|
||||||
const loadingComplete = defer<void>()
|
const loadingComplete = defer<void>()
|
||||||
|
|
||||||
const pendingDeepLinks: string[] = []
|
const pendingDeepLinks: string[] = []
|
||||||
@@ -96,11 +97,9 @@ function setupApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void app.whenReady().then(async () => {
|
void app.whenReady().then(async () => {
|
||||||
// migrate()
|
|
||||||
app.setAsDefaultProtocolClient("opencode")
|
app.setAsDefaultProtocolClient("opencode")
|
||||||
setDockIcon()
|
setDockIcon()
|
||||||
setupAutoUpdater()
|
setupAutoUpdater()
|
||||||
syncCli()
|
|
||||||
await initialize()
|
await initialize()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -134,8 +133,8 @@ async function initialize() {
|
|||||||
const password = randomUUID()
|
const password = randomUUID()
|
||||||
|
|
||||||
logger.log("spawning sidecar", { url })
|
logger.log("spawning sidecar", { url })
|
||||||
const { child, health, events } = spawnLocalServer(hostname, port, password)
|
const { listener, health } = await spawnLocalServer(hostname, port, password)
|
||||||
sidecar = child
|
server = listener
|
||||||
serverReady.resolve({
|
serverReady.resolve({
|
||||||
url,
|
url,
|
||||||
username: "opencode",
|
username: "opencode",
|
||||||
@@ -145,7 +144,7 @@ async function initialize() {
|
|||||||
const loadingTask = (async () => {
|
const loadingTask = (async () => {
|
||||||
logger.log("sidecar connection started", { url })
|
logger.log("sidecar connection started", { url })
|
||||||
|
|
||||||
events.on("sqlite", (progress: SqliteMigrationProgress) => {
|
initEmitter.on("sqlite", (progress: SqliteMigrationProgress) => {
|
||||||
setInitStep({ phase: "sqlite_waiting" })
|
setInitStep({ phase: "sqlite_waiting" })
|
||||||
if (overlay) sendSqliteMigrationProgress(overlay, progress)
|
if (overlay) sendSqliteMigrationProgress(overlay, progress)
|
||||||
if (mainWindow) sendSqliteMigrationProgress(mainWindow, progress)
|
if (mainWindow) sendSqliteMigrationProgress(mainWindow, progress)
|
||||||
@@ -198,9 +197,6 @@ function wireMenu() {
|
|||||||
if (!mainWindow) return
|
if (!mainWindow) return
|
||||||
createMenu({
|
createMenu({
|
||||||
trigger: (id) => mainWindow && sendMenuCommand(mainWindow, id),
|
trigger: (id) => mainWindow && sendMenuCommand(mainWindow, id),
|
||||||
installCli: () => {
|
|
||||||
void installCli()
|
|
||||||
},
|
|
||||||
checkForUpdates: () => {
|
checkForUpdates: () => {
|
||||||
void checkForUpdates(true)
|
void checkForUpdates(true)
|
||||||
},
|
},
|
||||||
@@ -215,7 +211,6 @@ function wireMenu() {
|
|||||||
|
|
||||||
registerIpcHandlers({
|
registerIpcHandlers({
|
||||||
killSidecar: () => killSidecar(),
|
killSidecar: () => killSidecar(),
|
||||||
installCli: async () => installCli(),
|
|
||||||
awaitInitialization: async (sendStep) => {
|
awaitInitialization: async (sendStep) => {
|
||||||
sendStep(initStep)
|
sendStep(initStep)
|
||||||
const listener = (step: InitStep) => sendStep(step)
|
const listener = (step: InitStep) => sendStep(step)
|
||||||
@@ -247,16 +242,9 @@ registerIpcHandlers({
|
|||||||
})
|
})
|
||||||
|
|
||||||
function killSidecar() {
|
function killSidecar() {
|
||||||
if (!sidecar) return
|
if (!server) return
|
||||||
const pid = sidecar.pid
|
server.stop()
|
||||||
sidecar.kill()
|
server = null
|
||||||
sidecar = null
|
|
||||||
// tree-kill is async; also send process group signal as immediate fallback
|
|
||||||
if (pid && process.platform !== "win32") {
|
|
||||||
try {
|
|
||||||
process.kill(-pid, "SIGTERM")
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureLoopbackNoProxy() {
|
function ensureLoopbackNoProxy() {
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ const pickerFilters = (ext?: string[]) => {
|
|||||||
|
|
||||||
type Deps = {
|
type Deps = {
|
||||||
killSidecar: () => void
|
killSidecar: () => void
|
||||||
installCli: () => Promise<string>
|
|
||||||
awaitInitialization: (sendStep: (step: InitStep) => void) => Promise<ServerReadyData>
|
awaitInitialization: (sendStep: (step: InitStep) => void) => Promise<ServerReadyData>
|
||||||
getDefaultServerUrl: () => Promise<string | null> | string | null
|
getDefaultServerUrl: () => Promise<string | null> | string | null
|
||||||
setDefaultServerUrl: (url: string | null) => Promise<void> | void
|
setDefaultServerUrl: (url: string | null) => Promise<void> | void
|
||||||
@@ -34,7 +33,6 @@ type Deps = {
|
|||||||
|
|
||||||
export function registerIpcHandlers(deps: Deps) {
|
export function registerIpcHandlers(deps: Deps) {
|
||||||
ipcMain.handle("kill-sidecar", () => deps.killSidecar())
|
ipcMain.handle("kill-sidecar", () => deps.killSidecar())
|
||||||
ipcMain.handle("install-cli", () => deps.installCli())
|
|
||||||
ipcMain.handle("await-initialization", (event: IpcMainInvokeEvent) => {
|
ipcMain.handle("await-initialization", (event: IpcMainInvokeEvent) => {
|
||||||
const send = (step: InitStep) => event.sender.send("init-step", step)
|
const send = (step: InitStep) => event.sender.send("init-step", step)
|
||||||
return deps.awaitInitialization(send)
|
return deps.awaitInitialization(send)
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { createMainWindow } from "./windows"
|
|||||||
|
|
||||||
type Deps = {
|
type Deps = {
|
||||||
trigger: (id: string) => void
|
trigger: (id: string) => void
|
||||||
installCli: () => void
|
|
||||||
checkForUpdates: () => void
|
checkForUpdates: () => void
|
||||||
reload: () => void
|
reload: () => void
|
||||||
relaunch: () => void
|
relaunch: () => void
|
||||||
@@ -24,10 +23,6 @@ export function createMenu(deps: Deps) {
|
|||||||
enabled: UPDATER_ENABLED,
|
enabled: UPDATER_ENABLED,
|
||||||
click: () => deps.checkForUpdates(),
|
click: () => deps.checkForUpdates(),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: "Install CLI...",
|
|
||||||
click: () => deps.installCli(),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: "Reload Webview",
|
label: "Reload Webview",
|
||||||
click: () => deps.reload(),
|
click: () => deps.reload(),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { serve, type CommandChild } from "./cli"
|
import { app } from "electron"
|
||||||
import { DEFAULT_SERVER_URL_KEY, WSL_ENABLED_KEY } from "./constants"
|
import { DEFAULT_SERVER_URL_KEY, WSL_ENABLED_KEY } from "./constants"
|
||||||
|
import { getUserShell, loadShellEnv } from "./shell-env"
|
||||||
import { store } from "./store"
|
import { store } from "./store"
|
||||||
|
|
||||||
export type WslConfig = { enabled: boolean }
|
export type WslConfig = { enabled: boolean }
|
||||||
@@ -29,8 +30,16 @@ export function setWslConfig(config: WslConfig) {
|
|||||||
store.set(WSL_ENABLED_KEY, config.enabled)
|
store.set(WSL_ENABLED_KEY, config.enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function spawnLocalServer(hostname: string, port: number, password: string) {
|
export async function spawnLocalServer(hostname: string, port: number, password: string) {
|
||||||
const { child, exit, events } = serve(hostname, port, password)
|
prepareServerEnv(password)
|
||||||
|
const { Log, Server } = await import("virtual:opencode-server")
|
||||||
|
await Log.init({ level: "WARN" })
|
||||||
|
const listener = await Server.listen({
|
||||||
|
port,
|
||||||
|
hostname,
|
||||||
|
username: "opencode",
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
|
||||||
const wait = (async () => {
|
const wait = (async () => {
|
||||||
const url = `http://${hostname}:${port}`
|
const url = `http://${hostname}:${port}`
|
||||||
@@ -42,19 +51,26 @@ export function spawnLocalServer(hostname: string, port: number, password: strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const terminated = async () => {
|
await ready()
|
||||||
const payload = await exit
|
|
||||||
throw new Error(
|
|
||||||
`Sidecar terminated before becoming healthy (code=${payload.code ?? "unknown"} signal=${
|
|
||||||
payload.signal ?? "unknown"
|
|
||||||
})`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.race([ready(), terminated()])
|
|
||||||
})()
|
})()
|
||||||
|
|
||||||
return { child, health: { wait }, events }
|
return { listener, health: { wait } }
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareServerEnv(password: string) {
|
||||||
|
const shell = process.platform === "win32" ? null : getUserShell()
|
||||||
|
const shellEnv = shell ? (loadShellEnv(shell) ?? {}) : {}
|
||||||
|
const env = {
|
||||||
|
...process.env,
|
||||||
|
...shellEnv,
|
||||||
|
OPENCODE_EXPERIMENTAL_ICON_DISCOVERY: "true",
|
||||||
|
OPENCODE_EXPERIMENTAL_FILEWATCHER: "true",
|
||||||
|
OPENCODE_CLIENT: "desktop",
|
||||||
|
OPENCODE_SERVER_USERNAME: "opencode",
|
||||||
|
OPENCODE_SERVER_PASSWORD: password,
|
||||||
|
XDG_STATE_HOME: app.getPath("userData"),
|
||||||
|
}
|
||||||
|
Object.assign(process.env, env)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkHealth(url: string, password?: string | null): Promise<boolean> {
|
export async function checkHealth(url: string, password?: string | null): Promise<boolean> {
|
||||||
@@ -82,5 +98,3 @@ export async function checkHealth(url: string, password?: string | null): Promis
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { CommandChild }
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { spawnSync } from "node:child_process"
|
import { spawnSync } from "node:child_process"
|
||||||
import { basename } from "node:path"
|
import { basename } from "node:path"
|
||||||
|
|
||||||
const SHELL_ENV_TIMEOUT = 5_000
|
const TIMEOUT = 5_000
|
||||||
|
|
||||||
type Probe = { type: "Loaded"; value: Record<string, string> } | { type: "Timeout" } | { type: "Unavailable" }
|
type Probe = { type: "Loaded"; value: Record<string, string> } | { type: "Timeout" } | { type: "Unavailable" }
|
||||||
|
|
||||||
@@ -20,28 +20,28 @@ export function parseShellEnv(out: Buffer) {
|
|||||||
return env
|
return env
|
||||||
}
|
}
|
||||||
|
|
||||||
function probeShellEnv(shell: string, mode: "-il" | "-l"): Probe {
|
function probe(shell: string, mode: "-il" | "-l"): Probe {
|
||||||
const out = spawnSync(shell, [mode, "-c", "env -0"], {
|
const out = spawnSync(shell, [mode, "-c", "env -0"], {
|
||||||
stdio: ["ignore", "pipe", "ignore"],
|
stdio: ["ignore", "pipe", "ignore"],
|
||||||
timeout: SHELL_ENV_TIMEOUT,
|
timeout: TIMEOUT,
|
||||||
windowsHide: true,
|
windowsHide: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const err = out.error as NodeJS.ErrnoException | undefined
|
const err = out.error as NodeJS.ErrnoException | undefined
|
||||||
if (err) {
|
if (err) {
|
||||||
if (err.code === "ETIMEDOUT") return { type: "Timeout" }
|
if (err.code === "ETIMEDOUT") return { type: "Timeout" }
|
||||||
console.log(`[cli] Shell env probe failed for ${shell} ${mode}: ${err.message}`)
|
console.log(`[server] Shell env probe failed for ${shell} ${mode}: ${err.message}`)
|
||||||
return { type: "Unavailable" }
|
return { type: "Unavailable" }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (out.status !== 0) {
|
if (out.status !== 0) {
|
||||||
console.log(`[cli] Shell env probe exited with non-zero status for ${shell} ${mode}`)
|
console.log(`[server] Shell env probe exited with non-zero status for ${shell} ${mode}`)
|
||||||
return { type: "Unavailable" }
|
return { type: "Unavailable" }
|
||||||
}
|
}
|
||||||
|
|
||||||
const env = parseShellEnv(out.stdout)
|
const env = parseShellEnv(out.stdout)
|
||||||
if (Object.keys(env).length === 0) {
|
if (Object.keys(env).length === 0) {
|
||||||
console.log(`[cli] Shell env probe returned empty env for ${shell} ${mode}`)
|
console.log(`[server] Shell env probe returned empty env for ${shell} ${mode}`)
|
||||||
return { type: "Unavailable" }
|
return { type: "Unavailable" }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,27 +56,27 @@ export function isNushell(shell: string) {
|
|||||||
|
|
||||||
export function loadShellEnv(shell: string) {
|
export function loadShellEnv(shell: string) {
|
||||||
if (isNushell(shell)) {
|
if (isNushell(shell)) {
|
||||||
console.log(`[cli] Skipping shell env probe for nushell: ${shell}`)
|
console.log(`[server] Skipping shell env probe for nushell: ${shell}`)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const interactive = probeShellEnv(shell, "-il")
|
const interactive = probe(shell, "-il")
|
||||||
if (interactive.type === "Loaded") {
|
if (interactive.type === "Loaded") {
|
||||||
console.log(`[cli] Loaded shell environment with -il (${Object.keys(interactive.value).length} vars)`)
|
console.log(`[server] Loaded shell environment with -il (${Object.keys(interactive.value).length} vars)`)
|
||||||
return interactive.value
|
return interactive.value
|
||||||
}
|
}
|
||||||
if (interactive.type === "Timeout") {
|
if (interactive.type === "Timeout") {
|
||||||
console.warn(`[cli] Interactive shell env probe timed out: ${shell}`)
|
console.warn(`[server] Interactive shell env probe timed out: ${shell}`)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const login = probeShellEnv(shell, "-l")
|
const login = probe(shell, "-l")
|
||||||
if (login.type === "Loaded") {
|
if (login.type === "Loaded") {
|
||||||
console.log(`[cli] Loaded shell environment with -l (${Object.keys(login.value).length} vars)`)
|
console.log(`[server] Loaded shell environment with -l (${Object.keys(login.value).length} vars)`)
|
||||||
return login.value
|
return login.value
|
||||||
}
|
}
|
||||||
|
|
||||||
console.warn(`[cli] Falling back to app environment: ${shell}`)
|
console.warn(`[server] Falling back to app environment: ${shell}`)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.4.0",
|
"version": "1.4.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const releaseId = process.env.OPENCODE_RELEASE
|
|||||||
if (!releaseId) throw new Error("OPENCODE_RELEASE is required")
|
if (!releaseId) throw new Error("OPENCODE_RELEASE is required")
|
||||||
|
|
||||||
const version = process.env.OPENCODE_VERSION
|
const version = process.env.OPENCODE_VERSION
|
||||||
if (!releaseId) throw new Error("OPENCODE_VERSION is required")
|
if (!version) throw new Error("OPENCODE_VERSION is required")
|
||||||
|
|
||||||
const token = process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN
|
const token = process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN
|
||||||
if (!token) throw new Error("GH_TOKEN or GITHUB_TOKEN is required")
|
if (!token) throw new Error("GH_TOKEN or GITHUB_TOKEN is required")
|
||||||
@@ -54,7 +54,10 @@ const assets = release.assets ?? []
|
|||||||
const assetByName = new Map(assets.map((asset) => [asset.name, asset]))
|
const assetByName = new Map(assets.map((asset) => [asset.name, asset]))
|
||||||
|
|
||||||
const latestAsset = assetByName.get("latest.json")
|
const latestAsset = assetByName.get("latest.json")
|
||||||
if (!latestAsset) throw new Error("latest.json asset not found")
|
if (!latestAsset) {
|
||||||
|
console.log("latest.json not found, skipping tauri finalization")
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
const latestRes = await fetch(latestAsset.url, {
|
const latestRes = await fetch(latestAsset.url, {
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.4.0",
|
"version": "1.4.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
id = "opencode"
|
id = "opencode"
|
||||||
name = "OpenCode"
|
name = "OpenCode"
|
||||||
description = "The open source coding agent."
|
description = "The open source coding agent."
|
||||||
version = "1.4.0"
|
version = "1.4.3"
|
||||||
schema_version = 1
|
schema_version = 1
|
||||||
authors = ["Anomaly"]
|
authors = ["Anomaly"]
|
||||||
repository = "https://github.com/anomalyco/opencode"
|
repository = "https://github.com/anomalyco/opencode"
|
||||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
|||||||
icon = "./icons/opencode.svg"
|
icon = "./icons/opencode.svg"
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-aarch64]
|
[agent_servers.opencode.targets.darwin-aarch64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.0/opencode-darwin-arm64.zip"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.3/opencode-darwin-arm64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-x86_64]
|
[agent_servers.opencode.targets.darwin-x86_64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.0/opencode-darwin-x64.zip"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.3/opencode-darwin-x64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-aarch64]
|
[agent_servers.opencode.targets.linux-aarch64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.0/opencode-linux-arm64.tar.gz"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.3/opencode-linux-arm64.tar.gz"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-x86_64]
|
[agent_servers.opencode.targets.linux-x86_64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.0/opencode-linux-x64.tar.gz"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.3/opencode-linux-x64.tar.gz"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.windows-x86_64]
|
[agent_servers.opencode.targets.windows-x86_64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.0/opencode-windows-x64.zip"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.3/opencode-windows-x64.zip"
|
||||||
cmd = "./opencode.exe"
|
cmd = "./opencode.exe"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.4.0",
|
"version": "1.4.3",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.4.0",
|
"version": "1.4.3",
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
"@hono/node-ws": "1.3.0",
|
"@hono/node-ws": "1.3.0",
|
||||||
"@hono/standard-validator": "0.1.5",
|
"@hono/standard-validator": "0.1.5",
|
||||||
"@hono/zod-validator": "catalog:",
|
"@hono/zod-validator": "catalog:",
|
||||||
"@lydell/node-pty": "1.2.0-beta.10",
|
"@lydell/node-pty": "catalog:",
|
||||||
"@modelcontextprotocol/sdk": "1.27.1",
|
"@modelcontextprotocol/sdk": "1.27.1",
|
||||||
"@npmcli/arborist": "9.4.0",
|
"@npmcli/arborist": "9.4.0",
|
||||||
"@octokit/graphql": "9.0.2",
|
"@octokit/graphql": "9.0.2",
|
||||||
@@ -137,7 +137,7 @@
|
|||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
"gitlab-ai-provider": "6.0.0",
|
"gitlab-ai-provider": "6.4.2",
|
||||||
"glob": "13.0.5",
|
"glob": "13.0.5",
|
||||||
"google-auth-library": "10.5.0",
|
"google-auth-library": "10.5.0",
|
||||||
"gray-matter": "4.0.3",
|
"gray-matter": "4.0.3",
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
#!/usr/bin/env bun
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
import { $ } from "bun"
|
|
||||||
import { Script } from "@opencode-ai/script"
|
import { Script } from "@opencode-ai/script"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
@@ -9,18 +8,11 @@ import { fileURLToPath } from "url"
|
|||||||
const __filename = fileURLToPath(import.meta.url)
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
const __dirname = path.dirname(__filename)
|
const __dirname = path.dirname(__filename)
|
||||||
const dir = path.resolve(__dirname, "..")
|
const dir = path.resolve(__dirname, "..")
|
||||||
const root = path.resolve(dir, "../..")
|
|
||||||
|
|
||||||
function linker(): "hoisted" | "isolated" {
|
|
||||||
// jsonc-parser is only declared in packages/opencode, so its install location
|
|
||||||
// tells us whether Bun used a hoisted or isolated workspace layout.
|
|
||||||
if (fs.existsSync(path.join(dir, "node_modules", "jsonc-parser"))) return "isolated"
|
|
||||||
if (fs.existsSync(path.join(root, "node_modules", "jsonc-parser"))) return "hoisted"
|
|
||||||
throw new Error("Could not detect Bun linker from jsonc-parser")
|
|
||||||
}
|
|
||||||
|
|
||||||
process.chdir(dir)
|
process.chdir(dir)
|
||||||
|
|
||||||
|
await import("./generate.ts")
|
||||||
|
|
||||||
// Load migrations from migration directories
|
// Load migrations from migration directories
|
||||||
const migrationDirs = (
|
const migrationDirs = (
|
||||||
await fs.promises.readdir(path.join(dir, "migration"), {
|
await fs.promises.readdir(path.join(dir, "migration"), {
|
||||||
@@ -51,21 +43,20 @@ const migrations = await Promise.all(
|
|||||||
)
|
)
|
||||||
console.log(`Loaded ${migrations.length} migrations`)
|
console.log(`Loaded ${migrations.length} migrations`)
|
||||||
|
|
||||||
const link = linker()
|
|
||||||
|
|
||||||
await $`bun install --linker=${link} --os="*" --cpu="*" @lydell/node-pty@1.2.0-beta.10`
|
|
||||||
|
|
||||||
await Bun.build({
|
await Bun.build({
|
||||||
target: "node",
|
target: "node",
|
||||||
entrypoints: ["./src/node.ts"],
|
entrypoints: ["./src/node.ts"],
|
||||||
outdir: "./dist",
|
outdir: "./dist/node",
|
||||||
format: "esm",
|
format: "esm",
|
||||||
sourcemap: "linked",
|
sourcemap: "linked",
|
||||||
external: ["jsonc-parser"],
|
external: ["jsonc-parser", "@lydell/node-pty"],
|
||||||
define: {
|
define: {
|
||||||
OPENCODE_MIGRATIONS: JSON.stringify(migrations),
|
OPENCODE_MIGRATIONS: JSON.stringify(migrations),
|
||||||
OPENCODE_CHANNEL: `'${Script.channel}'`,
|
OPENCODE_CHANNEL: `'${Script.channel}'`,
|
||||||
},
|
},
|
||||||
|
files: {
|
||||||
|
"opencode-web-ui.gen.ts": "",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log("Build complete")
|
console.log("Build complete")
|
||||||
|
|||||||
@@ -12,24 +12,11 @@ const dir = path.resolve(__dirname, "..")
|
|||||||
|
|
||||||
process.chdir(dir)
|
process.chdir(dir)
|
||||||
|
|
||||||
|
await import("./generate.ts")
|
||||||
|
|
||||||
import { Script } from "@opencode-ai/script"
|
import { Script } from "@opencode-ai/script"
|
||||||
import pkg from "../package.json"
|
import pkg from "../package.json"
|
||||||
|
|
||||||
const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.dev"
|
|
||||||
// Fetch and generate models.dev snapshot
|
|
||||||
const modelsData = process.env.MODELS_DEV_API_JSON
|
|
||||||
? await Bun.file(process.env.MODELS_DEV_API_JSON).text()
|
|
||||||
: await fetch(`${modelsUrl}/api.json`).then((x) => x.text())
|
|
||||||
await Bun.write(
|
|
||||||
path.join(dir, "src/provider/models-snapshot.js"),
|
|
||||||
`// @ts-nocheck\n// Auto-generated by build.ts - do not edit\nexport const snapshot = ${modelsData}\n`,
|
|
||||||
)
|
|
||||||
await Bun.write(
|
|
||||||
path.join(dir, "src/provider/models-snapshot.d.ts"),
|
|
||||||
`// Auto-generated by build.ts - do not edit\nexport declare const snapshot: Record<string, unknown>\n`,
|
|
||||||
)
|
|
||||||
console.log("Generated models-snapshot.js")
|
|
||||||
|
|
||||||
// Load migrations from migration directories
|
// Load migrations from migration directories
|
||||||
const migrationDirs = (
|
const migrationDirs = (
|
||||||
await fs.promises.readdir(path.join(dir, "migration"), {
|
await fs.promises.readdir(path.join(dir, "migration"), {
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import path from "path"
|
||||||
|
import { fileURLToPath } from "url"
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
|
const __dirname = path.dirname(__filename)
|
||||||
|
const dir = path.resolve(__dirname, "..")
|
||||||
|
|
||||||
|
process.chdir(dir)
|
||||||
|
|
||||||
|
const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.dev"
|
||||||
|
// Fetch and generate models.dev snapshot
|
||||||
|
const modelsData = process.env.MODELS_DEV_API_JSON
|
||||||
|
? await Bun.file(process.env.MODELS_DEV_API_JSON).text()
|
||||||
|
: await fetch(`${modelsUrl}/api.json`).then((x) => x.text())
|
||||||
|
await Bun.write(
|
||||||
|
path.join(dir, "src/provider/models-snapshot.js"),
|
||||||
|
`// @ts-nocheck\n// Auto-generated by build.ts - do not edit\nexport const snapshot = ${modelsData}\n`,
|
||||||
|
)
|
||||||
|
await Bun.write(
|
||||||
|
path.join(dir, "src/provider/models-snapshot.d.ts"),
|
||||||
|
`// Auto-generated by build.ts - do not edit\nexport declare const snapshot: Record<string, unknown>\n`,
|
||||||
|
)
|
||||||
|
console.log("Generated models-snapshot.js")
|
||||||
@@ -461,28 +461,11 @@ export namespace Account {
|
|||||||
return Option.getOrUndefined(await runPromise((service) => service.active()))
|
return Option.getOrUndefined(await runPromise((service) => service.active()))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function list(): Promise<Info[]> {
|
|
||||||
return runPromise((service) => service.list())
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function activeOrg(): Promise<ActiveOrg | undefined> {
|
|
||||||
return Option.getOrUndefined(await runPromise((service) => service.activeOrg()))
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function orgsByAccount(): Promise<readonly AccountOrgs[]> {
|
export async function orgsByAccount(): Promise<readonly AccountOrgs[]> {
|
||||||
return runPromise((service) => service.orgsByAccount())
|
return runPromise((service) => service.orgsByAccount())
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function orgs(accountID: AccountID): Promise<readonly Org[]> {
|
|
||||||
return runPromise((service) => service.orgs(accountID))
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function switchOrg(accountID: AccountID, orgID: OrgID) {
|
export async function switchOrg(accountID: AccountID, orgID: OrgID) {
|
||||||
return runPromise((service) => service.use(accountID, Option.some(orgID)))
|
return runPromise((service) => service.use(accountID, Option.some(orgID)))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function token(accountID: AccountID): Promise<AccessToken | undefined> {
|
|
||||||
const t = await runPromise((service) => service.token(accountID))
|
|
||||||
return Option.getOrUndefined(t)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -341,6 +341,10 @@ export namespace Agent {
|
|||||||
)
|
)
|
||||||
const existing = yield* InstanceState.useEffect(state, (s) => s.list())
|
const existing = yield* InstanceState.useEffect(state, (s) => s.list())
|
||||||
|
|
||||||
|
// TODO: clean this up so provider specific logic doesnt bleed over
|
||||||
|
const authInfo = yield* auth.get(model.providerID).pipe(Effect.orDie)
|
||||||
|
const isOpenaiOauth = model.providerID === "openai" && authInfo?.type === "oauth"
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
experimental_telemetry: {
|
experimental_telemetry: {
|
||||||
isEnabled: cfg.experimental?.openTelemetry,
|
isEnabled: cfg.experimental?.openTelemetry,
|
||||||
@@ -350,12 +354,14 @@ export namespace Agent {
|
|||||||
},
|
},
|
||||||
temperature: 0.3,
|
temperature: 0.3,
|
||||||
messages: [
|
messages: [
|
||||||
...system.map(
|
...(isOpenaiOauth
|
||||||
|
? []
|
||||||
|
: system.map(
|
||||||
(item): ModelMessage => ({
|
(item): ModelMessage => ({
|
||||||
role: "system",
|
role: "system",
|
||||||
content: item,
|
content: item,
|
||||||
}),
|
}),
|
||||||
),
|
)),
|
||||||
{
|
{
|
||||||
role: "user",
|
role: "user",
|
||||||
content: `Create an agent configuration based on this request: \"${input.description}\".\n\nIMPORTANT: The following identifiers already exist and must NOT be used: ${existing.map((i) => i.name).join(", ")}\n Return ONLY the JSON object, no other text, do not wrap in backticks`,
|
content: `Create an agent configuration based on this request: \"${input.description}\".\n\nIMPORTANT: The following identifiers already exist and must NOT be used: ${existing.map((i) => i.name).join(", ")}\n Return ONLY the JSON object, no other text, do not wrap in backticks`,
|
||||||
@@ -369,13 +375,12 @@ export namespace Agent {
|
|||||||
}),
|
}),
|
||||||
} satisfies Parameters<typeof generateObject>[0]
|
} satisfies Parameters<typeof generateObject>[0]
|
||||||
|
|
||||||
// TODO: clean this up so provider specific logic doesnt bleed over
|
if (isOpenaiOauth) {
|
||||||
const authInfo = yield* auth.get(model.providerID).pipe(Effect.orDie)
|
|
||||||
if (model.providerID === "openai" && authInfo?.type === "oauth") {
|
|
||||||
return yield* Effect.promise(async () => {
|
return yield* Effect.promise(async () => {
|
||||||
const result = streamObject({
|
const result = streamObject({
|
||||||
...params,
|
...params,
|
||||||
providerOptions: ProviderTransform.providerOptions(resolved, {
|
providerOptions: ProviderTransform.providerOptions(resolved, {
|
||||||
|
instructions: system.join("\n"),
|
||||||
store: false,
|
store: false,
|
||||||
}),
|
}),
|
||||||
onError: () => {},
|
onError: () => {},
|
||||||
@@ -393,11 +398,13 @@ export namespace Agent {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(
|
export const defaultLayer = Layer.suspend(() =>
|
||||||
|
layer.pipe(
|
||||||
Layer.provide(Provider.defaultLayer),
|
Layer.provide(Provider.defaultLayer),
|
||||||
Layer.provide(Auth.defaultLayer),
|
Layer.provide(Auth.defaultLayer),
|
||||||
Layer.provide(Config.defaultLayer),
|
Layer.provide(Config.defaultLayer),
|
||||||
Layer.provide(Skill.defaultLayer),
|
Layer.provide(Skill.defaultLayer),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ export const GlobalBus = new EventEmitter<{
|
|||||||
event: [
|
event: [
|
||||||
{
|
{
|
||||||
directory?: string
|
directory?: string
|
||||||
|
project?: string
|
||||||
|
workspace?: string
|
||||||
payload: any
|
payload: any
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Effect, Exit, Layer, PubSub, Scope, ServiceMap, Stream } from "effect"
|
import { Effect, Exit, Layer, PubSub, Scope, ServiceMap, Stream } from "effect"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { Instance } from "../project/instance"
|
|
||||||
import { BusEvent } from "./bus-event"
|
import { BusEvent } from "./bus-event"
|
||||||
import { GlobalBus } from "./global"
|
import { GlobalBus } from "./global"
|
||||||
|
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
import { makeRuntime } from "@/effect/run-service"
|
import { makeRuntime } from "@/effect/run-service"
|
||||||
|
|
||||||
@@ -91,8 +91,13 @@ export namespace Bus {
|
|||||||
yield* PubSub.publish(s.wildcard, payload)
|
yield* PubSub.publish(s.wildcard, payload)
|
||||||
|
|
||||||
const dir = yield* InstanceState.directory
|
const dir = yield* InstanceState.directory
|
||||||
|
const context = yield* InstanceState.context
|
||||||
|
const workspace = yield* InstanceState.workspaceID
|
||||||
|
|
||||||
GlobalBus.emit("event", {
|
GlobalBus.emit("event", {
|
||||||
directory: dir,
|
directory: dir,
|
||||||
|
project: context.project.id,
|
||||||
|
workspace,
|
||||||
payload,
|
payload,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Argv } from "yargs"
|
import type { Argv } from "yargs"
|
||||||
import { spawn } from "child_process"
|
import { spawn } from "child_process"
|
||||||
import { Database } from "../../storage/db"
|
import { Database } from "../../storage/db"
|
||||||
|
import { drizzle } from "drizzle-orm/bun-sqlite"
|
||||||
import { Database as BunDatabase } from "bun:sqlite"
|
import { Database as BunDatabase } from "bun:sqlite"
|
||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
@@ -74,7 +75,7 @@ const MigrateCommand = cmd({
|
|||||||
let last = -1
|
let last = -1
|
||||||
if (tty) process.stderr.write("\x1b[?25l")
|
if (tty) process.stderr.write("\x1b[?25l")
|
||||||
try {
|
try {
|
||||||
const stats = await JsonMigration.run(sqlite, {
|
const stats = await JsonMigration.run(drizzle({ client: sqlite }), {
|
||||||
progress: (event) => {
|
progress: (event) => {
|
||||||
const percent = Math.floor((event.current / event.total) * 100)
|
const percent = Math.floor((event.current / event.total) * 100)
|
||||||
if (percent === last) return
|
if (percent === last) return
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { cmd } from "./cmd"
|
|||||||
import { ModelsDev } from "../../provider/models"
|
import { ModelsDev } from "../../provider/models"
|
||||||
import { Instance } from "@/project/instance"
|
import { Instance } from "@/project/instance"
|
||||||
import { bootstrap } from "../bootstrap"
|
import { bootstrap } from "../bootstrap"
|
||||||
|
import { SessionShare } from "@/share/session"
|
||||||
import { Session } from "../../session"
|
import { Session } from "../../session"
|
||||||
import type { SessionID } from "../../session/schema"
|
import type { SessionID } from "../../session/schema"
|
||||||
import { MessageID, PartID } from "../../session/schema"
|
import { MessageID, PartID } from "../../session/schema"
|
||||||
@@ -559,7 +560,7 @@ export const GithubRunCommand = cmd({
|
|||||||
shareId = await (async () => {
|
shareId = await (async () => {
|
||||||
if (share === false) return
|
if (share === false) return
|
||||||
if (!share && repoData.data.private) return
|
if (!share && repoData.data.private) return
|
||||||
await Session.share(session.id)
|
await SessionShare.share(session.id)
|
||||||
return session.id.slice(-8)
|
return session.id.slice(-8)
|
||||||
})()
|
})()
|
||||||
console.log("opencode session", session.id)
|
console.log("opencode session", session.id)
|
||||||
|
|||||||
@@ -688,6 +688,7 @@ export const McpDebugCommand = cmd({
|
|||||||
clientId: oauthConfig?.clientId,
|
clientId: oauthConfig?.clientId,
|
||||||
clientSecret: oauthConfig?.clientSecret,
|
clientSecret: oauthConfig?.clientSecret,
|
||||||
scope: oauthConfig?.scope,
|
scope: oauthConfig?.scope,
|
||||||
|
redirectUri: oauthConfig?.redirectUri,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onRedirect: async () => {},
|
onRedirect: async () => {},
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Flag } from "../../flag/flag"
|
|||||||
import { bootstrap } from "../bootstrap"
|
import { bootstrap } from "../bootstrap"
|
||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { Filesystem } from "../../util/filesystem"
|
import { Filesystem } from "../../util/filesystem"
|
||||||
import { createOpencodeClient, type Message, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2"
|
import { createOpencodeClient, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2"
|
||||||
import { Server } from "../../server/server"
|
import { Server } from "../../server/server"
|
||||||
import { Provider } from "../../provider/provider"
|
import { Provider } from "../../provider/provider"
|
||||||
import { Agent } from "../../agent/agent"
|
import { Agent } from "../../agent/agent"
|
||||||
@@ -680,7 +680,7 @@ export const RunCommand = cmd({
|
|||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||||
const request = new Request(input, init)
|
const request = new Request(input, init)
|
||||||
return Server.Default().fetch(request)
|
return Server.Default().app.fetch(request)
|
||||||
}) as typeof globalThis.fetch
|
}) as typeof globalThis.fetch
|
||||||
const sdk = createOpencodeClient({ baseUrl: "http://opencode.internal", fetch: fetchFn })
|
const sdk = createOpencodeClient({ baseUrl: "http://opencode.internal", fetch: fetchFn })
|
||||||
await execute(sdk)
|
await execute(sdk)
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
batch,
|
batch,
|
||||||
Show,
|
Show,
|
||||||
on,
|
on,
|
||||||
onCleanup,
|
|
||||||
} from "solid-js"
|
} from "solid-js"
|
||||||
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
|
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
|
||||||
import { Flag } from "@/flag/flag"
|
import { Flag } from "@/flag/flag"
|
||||||
@@ -23,6 +22,8 @@ import { DialogProvider, useDialog } from "@tui/ui/dialog"
|
|||||||
import { DialogProvider as DialogProviderList } from "@tui/component/dialog-provider"
|
import { DialogProvider as DialogProviderList } from "@tui/component/dialog-provider"
|
||||||
import { ErrorComponent } from "@tui/component/error-component"
|
import { ErrorComponent } from "@tui/component/error-component"
|
||||||
import { PluginRouteMissing } from "@tui/component/plugin-route-missing"
|
import { PluginRouteMissing } from "@tui/component/plugin-route-missing"
|
||||||
|
import { ProjectProvider } from "@tui/context/project"
|
||||||
|
import { useEvent } from "@tui/context/event"
|
||||||
import { SDKProvider, useSDK } from "@tui/context/sdk"
|
import { SDKProvider, useSDK } from "@tui/context/sdk"
|
||||||
import { StartupLoading } from "@tui/component/startup-loading"
|
import { StartupLoading } from "@tui/component/startup-loading"
|
||||||
import { SyncProvider, useSync } from "@tui/context/sync"
|
import { SyncProvider, useSync } from "@tui/context/sync"
|
||||||
@@ -54,7 +55,6 @@ import { KVProvider, useKV } from "./context/kv"
|
|||||||
import { Provider } from "@/provider/provider"
|
import { Provider } from "@/provider/provider"
|
||||||
import { ArgsProvider, useArgs, type Args } from "./context/args"
|
import { ArgsProvider, useArgs, type Args } from "./context/args"
|
||||||
import open from "open"
|
import open from "open"
|
||||||
import { writeHeapSnapshot } from "v8"
|
|
||||||
import { PromptRefProvider, usePromptRef } from "./context/prompt"
|
import { PromptRefProvider, usePromptRef } from "./context/prompt"
|
||||||
import { TuiConfigProvider, useTuiConfig } from "./context/tui-config"
|
import { TuiConfigProvider, useTuiConfig } from "./context/tui-config"
|
||||||
import { TuiConfig } from "@/config/tui"
|
import { TuiConfig } from "@/config/tui"
|
||||||
@@ -216,6 +216,7 @@ export function tui(input: {
|
|||||||
headers={input.headers}
|
headers={input.headers}
|
||||||
events={input.events}
|
events={input.events}
|
||||||
>
|
>
|
||||||
|
<ProjectProvider>
|
||||||
<SyncProvider>
|
<SyncProvider>
|
||||||
<ThemeProvider mode={mode}>
|
<ThemeProvider mode={mode}>
|
||||||
<LocalProvider>
|
<LocalProvider>
|
||||||
@@ -237,6 +238,7 @@ export function tui(input: {
|
|||||||
</LocalProvider>
|
</LocalProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</SyncProvider>
|
</SyncProvider>
|
||||||
|
</ProjectProvider>
|
||||||
</SDKProvider>
|
</SDKProvider>
|
||||||
</TuiConfigProvider>
|
</TuiConfigProvider>
|
||||||
</RouteProvider>
|
</RouteProvider>
|
||||||
@@ -260,6 +262,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
|||||||
const kv = useKV()
|
const kv = useKV()
|
||||||
const command = useCommandDialog()
|
const command = useCommandDialog()
|
||||||
const keybind = useKeybind()
|
const keybind = useKeybind()
|
||||||
|
const event = useEvent()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const themeState = useTheme()
|
const themeState = useTheme()
|
||||||
@@ -283,6 +286,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
|||||||
route,
|
route,
|
||||||
routes,
|
routes,
|
||||||
bump: () => setRouteRev((x) => x + 1),
|
bump: () => setRouteRev((x) => x + 1),
|
||||||
|
event,
|
||||||
sdk,
|
sdk,
|
||||||
sync,
|
sync,
|
||||||
theme: themeState,
|
theme: themeState,
|
||||||
@@ -491,12 +495,9 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
|||||||
const current = promptRef.current
|
const current = promptRef.current
|
||||||
// Don't require focus - if there's any text, preserve it
|
// Don't require focus - if there's any text, preserve it
|
||||||
const currentPrompt = current?.current?.input ? current.current : undefined
|
const currentPrompt = current?.current?.input ? current.current : undefined
|
||||||
const workspaceID =
|
|
||||||
route.data.type === "session" ? sync.session.get(route.data.sessionID)?.workspaceID : undefined
|
|
||||||
route.navigate({
|
route.navigate({
|
||||||
type: "home",
|
type: "home",
|
||||||
initialPrompt: currentPrompt,
|
initialPrompt: currentPrompt,
|
||||||
workspaceID,
|
|
||||||
})
|
})
|
||||||
dialog.clear()
|
dialog.clear()
|
||||||
},
|
},
|
||||||
@@ -806,11 +807,11 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
sdk.event.on(TuiEvent.CommandExecute.type, (evt) => {
|
event.on(TuiEvent.CommandExecute.type, (evt) => {
|
||||||
command.trigger(evt.properties.command)
|
command.trigger(evt.properties.command)
|
||||||
})
|
})
|
||||||
|
|
||||||
sdk.event.on(TuiEvent.ToastShow.type, (evt) => {
|
event.on(TuiEvent.ToastShow.type, (evt) => {
|
||||||
toast.show({
|
toast.show({
|
||||||
title: evt.properties.title,
|
title: evt.properties.title,
|
||||||
message: evt.properties.message,
|
message: evt.properties.message,
|
||||||
@@ -819,14 +820,14 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
sdk.event.on(TuiEvent.SessionSelect.type, (evt) => {
|
event.on(TuiEvent.SessionSelect.type, (evt) => {
|
||||||
route.navigate({
|
route.navigate({
|
||||||
type: "session",
|
type: "session",
|
||||||
sessionID: evt.properties.sessionID,
|
sessionID: evt.properties.sessionID,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
sdk.event.on("session.deleted", (evt) => {
|
event.on("session.deleted", (evt) => {
|
||||||
if (route.data.type === "session" && route.data.sessionID === evt.properties.info.id) {
|
if (route.data.type === "session" && route.data.sessionID === evt.properties.info.id) {
|
||||||
route.navigate({ type: "home" })
|
route.navigate({ type: "home" })
|
||||||
toast.show({
|
toast.show({
|
||||||
@@ -836,7 +837,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
sdk.event.on("session.error", (evt) => {
|
event.on("session.error", (evt) => {
|
||||||
const error = evt.properties.error
|
const error = evt.properties.error
|
||||||
if (error && typeof error === "object" && error.name === "MessageAbortedError") return
|
if (error && typeof error === "object" && error.name === "MessageAbortedError") return
|
||||||
const message = errorMessage(error)
|
const message = errorMessage(error)
|
||||||
@@ -848,7 +849,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
sdk.event.on("installation.update-available", async (evt) => {
|
event.on("installation.update-available", async (evt) => {
|
||||||
const version = evt.properties.version
|
const version = evt.properties.version
|
||||||
|
|
||||||
const skipped = kv.get("skipped_version")
|
const skipped = kv.get("skipped_version")
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { RGBA, TextAttributes } from "@opentui/core"
|
||||||
|
import { useKeyboard } from "@opentui/solid"
|
||||||
|
import open from "open"
|
||||||
|
import { createSignal } from "solid-js"
|
||||||
|
import { selectedForeground, useTheme } from "@tui/context/theme"
|
||||||
|
import { useDialog, type DialogContext } from "@tui/ui/dialog"
|
||||||
|
import { Link } from "@tui/ui/link"
|
||||||
|
|
||||||
|
const GO_URL = "https://opencode.ai/go"
|
||||||
|
|
||||||
|
export type DialogGoUpsellProps = {
|
||||||
|
onClose?: (dontShowAgain?: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function subscribe(props: DialogGoUpsellProps, dialog: ReturnType<typeof useDialog>) {
|
||||||
|
open(GO_URL).catch(() => {})
|
||||||
|
props.onClose?.()
|
||||||
|
dialog.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismiss(props: DialogGoUpsellProps, dialog: ReturnType<typeof useDialog>) {
|
||||||
|
props.onClose?.(true)
|
||||||
|
dialog.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DialogGoUpsell(props: DialogGoUpsellProps) {
|
||||||
|
const dialog = useDialog()
|
||||||
|
const { theme } = useTheme()
|
||||||
|
const fg = selectedForeground(theme)
|
||||||
|
const [selected, setSelected] = createSignal(0)
|
||||||
|
|
||||||
|
useKeyboard((evt) => {
|
||||||
|
if (evt.name === "left" || evt.name === "right" || evt.name === "tab") {
|
||||||
|
setSelected((s) => (s === 0 ? 1 : 0))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (evt.name !== "return") return
|
||||||
|
if (selected() === 0) subscribe(props, dialog)
|
||||||
|
else dismiss(props, dialog)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box paddingLeft={2} paddingRight={2} gap={1}>
|
||||||
|
<box flexDirection="row" justifyContent="space-between">
|
||||||
|
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
||||||
|
Free limit reached
|
||||||
|
</text>
|
||||||
|
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
|
||||||
|
esc
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
<box gap={1} paddingBottom={1}>
|
||||||
|
<text fg={theme.textMuted}>
|
||||||
|
Subscribe to OpenCode Go to keep going with reliable access to the best open-source models, starting at
|
||||||
|
$5/month.
|
||||||
|
</text>
|
||||||
|
<box flexDirection="row" gap={1}>
|
||||||
|
<Link href={GO_URL} fg={theme.primary} />
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
<box flexDirection="row" justifyContent="flex-end" gap={1} paddingBottom={1}>
|
||||||
|
<box
|
||||||
|
paddingLeft={3}
|
||||||
|
paddingRight={3}
|
||||||
|
backgroundColor={selected() === 0 ? theme.primary : RGBA.fromInts(0, 0, 0, 0)}
|
||||||
|
onMouseOver={() => setSelected(0)}
|
||||||
|
onMouseUp={() => subscribe(props, dialog)}
|
||||||
|
>
|
||||||
|
<text fg={selected() === 0 ? fg : theme.text} attributes={selected() === 0 ? TextAttributes.BOLD : undefined}>
|
||||||
|
subscribe
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
<box
|
||||||
|
paddingLeft={3}
|
||||||
|
paddingRight={3}
|
||||||
|
backgroundColor={selected() === 1 ? theme.primary : RGBA.fromInts(0, 0, 0, 0)}
|
||||||
|
onMouseOver={() => setSelected(1)}
|
||||||
|
onMouseUp={() => dismiss(props, dialog)}
|
||||||
|
>
|
||||||
|
<text
|
||||||
|
fg={selected() === 1 ? fg : theme.textMuted}
|
||||||
|
attributes={selected() === 1 ? TextAttributes.BOLD : undefined}
|
||||||
|
>
|
||||||
|
don't show again
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
DialogGoUpsell.show = (dialog: DialogContext) => {
|
||||||
|
return new Promise<boolean>((resolve) => {
|
||||||
|
dialog.replace(
|
||||||
|
() => <DialogGoUpsell onClose={(dontShow) => resolve(dontShow ?? false)} />,
|
||||||
|
() => resolve(false),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useDialog } from "@tui/ui/dialog"
|
import { useDialog } from "@tui/ui/dialog"
|
||||||
import { DialogSelect } from "@tui/ui/dialog-select"
|
import { DialogSelect } from "@tui/ui/dialog-select"
|
||||||
|
import { useProject } from "@tui/context/project"
|
||||||
import { useRoute } from "@tui/context/route"
|
import { useRoute } from "@tui/context/route"
|
||||||
import { useSync } from "@tui/context/sync"
|
import { useSync } from "@tui/context/sync"
|
||||||
import { createEffect, createMemo, createSignal, onMount } from "solid-js"
|
import { createEffect, createMemo, createSignal, onMount } from "solid-js"
|
||||||
@@ -14,7 +15,7 @@ function scoped(sdk: ReturnType<typeof useSDK>, sync: ReturnType<typeof useSync>
|
|||||||
return createOpencodeClient({
|
return createOpencodeClient({
|
||||||
baseUrl: sdk.url,
|
baseUrl: sdk.url,
|
||||||
fetch: sdk.fetch,
|
fetch: sdk.fetch,
|
||||||
directory: sync.data.path.directory || sdk.directory,
|
directory: sync.path.directory || sdk.directory,
|
||||||
experimental_workspaceID: workspaceID,
|
experimental_workspaceID: workspaceID,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -149,6 +150,7 @@ function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) => Promi
|
|||||||
|
|
||||||
export function DialogWorkspaceList() {
|
export function DialogWorkspaceList() {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
|
const project = useProject()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
@@ -168,8 +170,9 @@ export function DialogWorkspaceList() {
|
|||||||
forceCreate,
|
forceCreate,
|
||||||
})
|
})
|
||||||
|
|
||||||
async function selectWorkspace(workspaceID: string) {
|
async function selectWorkspace(workspaceID: string | null) {
|
||||||
if (workspaceID === "__local__") {
|
if (workspaceID == null) {
|
||||||
|
project.workspace.set(undefined)
|
||||||
if (localCount() > 0) {
|
if (localCount() > 0) {
|
||||||
dialog.replace(() => <DialogSessionList localOnly={true} />)
|
dialog.replace(() => <DialogSessionList localOnly={true} />)
|
||||||
return
|
return
|
||||||
@@ -199,12 +202,7 @@ export function DialogWorkspaceList() {
|
|||||||
await open(workspaceID)
|
await open(workspaceID)
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentWorkspaceID = createMemo(() => {
|
const currentWorkspaceID = createMemo(() => project.workspace.current())
|
||||||
if (route.data.type === "session") {
|
|
||||||
return sync.session.get(route.data.sessionID)?.workspaceID ?? "__local__"
|
|
||||||
}
|
|
||||||
return "__local__"
|
|
||||||
})
|
|
||||||
|
|
||||||
const localCount = createMemo(
|
const localCount = createMemo(
|
||||||
() => sync.data.session.filter((session) => !session.workspaceID && !session.parentID).length,
|
() => sync.data.session.filter((session) => !session.workspaceID && !session.parentID).length,
|
||||||
@@ -234,7 +232,7 @@ export function DialogWorkspaceList() {
|
|||||||
const options = createMemo(() => [
|
const options = createMemo(() => [
|
||||||
{
|
{
|
||||||
title: "Local",
|
title: "Local",
|
||||||
value: "__local__",
|
value: null,
|
||||||
category: "Workspace",
|
category: "Workspace",
|
||||||
description: "Use the local machine",
|
description: "Use the local machine",
|
||||||
footer: `${localCount()} session${localCount() === 1 ? "" : "s"}`,
|
footer: `${localCount()} session${localCount() === 1 ? "" : "s"}`,
|
||||||
@@ -292,7 +290,7 @@ export function DialogWorkspaceList() {
|
|||||||
keybind: keybind.all.session_delete?.[0],
|
keybind: keybind.all.session_delete?.[0],
|
||||||
title: "delete",
|
title: "delete",
|
||||||
onTrigger: async (option) => {
|
onTrigger: async (option) => {
|
||||||
if (option.value === "__create__" || option.value === "__local__") return
|
if (option.value === "__create__" || option.value === null) return
|
||||||
if (toDelete() !== option.value) {
|
if (toDelete() !== option.value) {
|
||||||
setToDelete(option.value)
|
setToDelete(option.value)
|
||||||
return
|
return
|
||||||
@@ -307,6 +305,7 @@ export function DialogWorkspaceList() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (currentWorkspaceID() === option.value) {
|
if (currentWorkspaceID() === option.value) {
|
||||||
|
project.workspace.set(undefined)
|
||||||
route.navigate({
|
route.navigate({
|
||||||
type: "home",
|
type: "home",
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ export function Autocomplete(props: {
|
|||||||
const width = props.anchor().width - 4
|
const width = props.anchor().width - 4
|
||||||
options.push(
|
options.push(
|
||||||
...sortedFiles.map((item): AutocompleteOption => {
|
...sortedFiles.map((item): AutocompleteOption => {
|
||||||
const baseDir = (sync.data.path.directory || process.cwd()).replace(/\/+$/, "")
|
const baseDir = (sync.path.directory || process.cwd()).replace(/\/+$/, "")
|
||||||
const fullPath = `${baseDir}/${item}`
|
const fullPath = `${baseDir}/${item}`
|
||||||
const urlObj = pathToFileURL(fullPath)
|
const urlObj = pathToFileURL(fullPath)
|
||||||
let filename = item
|
let filename = item
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { EmptyBorder, SplitBorder } from "@tui/component/border"
|
|||||||
import { useSDK } from "@tui/context/sdk"
|
import { useSDK } from "@tui/context/sdk"
|
||||||
import { useRoute } from "@tui/context/route"
|
import { useRoute } from "@tui/context/route"
|
||||||
import { useSync } from "@tui/context/sync"
|
import { useSync } from "@tui/context/sync"
|
||||||
|
import { useEvent } from "@tui/context/event"
|
||||||
import { MessageID, PartID } from "@/session/schema"
|
import { MessageID, PartID } from "@/session/schema"
|
||||||
import { createStore, produce } from "solid-js/store"
|
import { createStore, produce } from "solid-js/store"
|
||||||
import { useKeybind } from "@tui/context/keybind"
|
import { useKeybind } from "@tui/context/keybind"
|
||||||
@@ -115,8 +116,9 @@ export function Prompt(props: PromptProps) {
|
|||||||
const agentStyleId = syntax().getStyleId("extmark.agent")!
|
const agentStyleId = syntax().getStyleId("extmark.agent")!
|
||||||
const pasteStyleId = syntax().getStyleId("extmark.paste")!
|
const pasteStyleId = syntax().getStyleId("extmark.paste")!
|
||||||
let promptPartTypeId = 0
|
let promptPartTypeId = 0
|
||||||
|
const event = useEvent()
|
||||||
|
|
||||||
sdk.event.on(TuiEvent.PromptAppend.type, (evt) => {
|
event.on(TuiEvent.PromptAppend.type, (evt) => {
|
||||||
if (!input || input.isDestroyed) return
|
if (!input || input.isDestroyed) return
|
||||||
input.insertText(evt.properties.text)
|
input.insertText(evt.properties.text)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { createMemo } from "solid-js"
|
import { createMemo } from "solid-js"
|
||||||
|
import { useProject } from "./project"
|
||||||
import { useSync } from "./sync"
|
import { useSync } from "./sync"
|
||||||
import { Global } from "@/global"
|
import { Global } from "@/global"
|
||||||
|
|
||||||
export function useDirectory() {
|
export function useDirectory() {
|
||||||
|
const project = useProject()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
return createMemo(() => {
|
return createMemo(() => {
|
||||||
const directory = sync.data.path.directory || process.cwd()
|
const directory = project.instance.path().directory || process.cwd()
|
||||||
const result = directory.replace(Global.Path.home, "~")
|
const result = directory.replace(Global.Path.home, "~")
|
||||||
if (sync.data.vcs?.branch) return result + ":" + sync.data.vcs.branch
|
if (sync.data.vcs?.branch) return result + ":" + sync.data.vcs.branch
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import type { Event } from "@opencode-ai/sdk/v2"
|
||||||
|
import { useProject } from "./project"
|
||||||
|
import { useSDK } from "./sdk"
|
||||||
|
|
||||||
|
export function useEvent() {
|
||||||
|
const project = useProject()
|
||||||
|
const sdk = useSDK()
|
||||||
|
|
||||||
|
function subscribe(handler: (event: Event) => void) {
|
||||||
|
return sdk.event.on("event", (event) => {
|
||||||
|
// Special hack for truly global events
|
||||||
|
if (event.directory === "global") {
|
||||||
|
handler(event.payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (project.workspace.current()) {
|
||||||
|
if (event.workspace === project.workspace.current()) {
|
||||||
|
handler(event.payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.directory === project.instance.directory()) {
|
||||||
|
handler(event.payload)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function on<T extends Event["type"]>(type: T, handler: (event: Extract<Event, { type: T }>) => void) {
|
||||||
|
return subscribe((event) => {
|
||||||
|
if (event.type !== type) return
|
||||||
|
handler(event as Extract<Event, { type: T }>)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
on,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { batch } from "solid-js"
|
||||||
|
import type { Path } from "@opencode-ai/sdk"
|
||||||
|
import { createStore, reconcile } from "solid-js/store"
|
||||||
|
import { createSimpleContext } from "./helper"
|
||||||
|
import { useSDK } from "./sdk"
|
||||||
|
|
||||||
|
export const { use: useProject, provider: ProjectProvider } = createSimpleContext({
|
||||||
|
name: "Project",
|
||||||
|
init: () => {
|
||||||
|
const sdk = useSDK()
|
||||||
|
const [store, setStore] = createStore({
|
||||||
|
project: {
|
||||||
|
id: undefined as string | undefined,
|
||||||
|
},
|
||||||
|
instance: {
|
||||||
|
path: {
|
||||||
|
state: "",
|
||||||
|
config: "",
|
||||||
|
worktree: "",
|
||||||
|
directory: sdk.directory ?? "",
|
||||||
|
} satisfies Path,
|
||||||
|
},
|
||||||
|
workspace: undefined as string | undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
async function sync() {
|
||||||
|
const workspace = store.workspace
|
||||||
|
const [path, project] = await Promise.all([
|
||||||
|
sdk.client.path.get({ workspace }),
|
||||||
|
sdk.client.project.current({ workspace }),
|
||||||
|
])
|
||||||
|
|
||||||
|
batch(() => {
|
||||||
|
setStore("instance", "path", reconcile(path.data!))
|
||||||
|
setStore("project", "id", project.data?.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: store,
|
||||||
|
project() {
|
||||||
|
return store.project.id
|
||||||
|
},
|
||||||
|
instance: {
|
||||||
|
path() {
|
||||||
|
return store.instance.path
|
||||||
|
},
|
||||||
|
directory() {
|
||||||
|
return store.instance.path.directory
|
||||||
|
},
|
||||||
|
},
|
||||||
|
workspace: {
|
||||||
|
current() {
|
||||||
|
return store.workspace
|
||||||
|
},
|
||||||
|
set(next?: string | null) {
|
||||||
|
const workspace = next ?? undefined
|
||||||
|
if (store.workspace === workspace) return
|
||||||
|
setStore("workspace", workspace)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sync,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -5,7 +5,6 @@ import type { PromptInfo } from "../component/prompt/history"
|
|||||||
export type HomeRoute = {
|
export type HomeRoute = {
|
||||||
type: "home"
|
type: "home"
|
||||||
initialPrompt?: PromptInfo
|
initialPrompt?: PromptInfo
|
||||||
workspaceID?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SessionRoute = {
|
export type SessionRoute = {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2"
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
||||||
|
import type { GlobalEvent, Event } from "@opencode-ai/sdk/v2"
|
||||||
import { createSimpleContext } from "./helper"
|
import { createSimpleContext } from "./helper"
|
||||||
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
||||||
import { batch, onCleanup, onMount } from "solid-js"
|
import { batch, onCleanup, onMount } from "solid-js"
|
||||||
|
|
||||||
export type EventSource = {
|
export type EventSource = {
|
||||||
subscribe: (directory: string | undefined, handler: (event: Event) => void) => Promise<() => void>
|
subscribe: (handler: (event: GlobalEvent) => void) => Promise<() => void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||||
@@ -32,10 +33,10 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
|||||||
let sdk = createSDK()
|
let sdk = createSDK()
|
||||||
|
|
||||||
const emitter = createGlobalEmitter<{
|
const emitter = createGlobalEmitter<{
|
||||||
[key in Event["type"]]: Extract<Event, { type: key }>
|
event: GlobalEvent
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
let queue: Event[] = []
|
let queue: GlobalEvent[] = []
|
||||||
let timer: Timer | undefined
|
let timer: Timer | undefined
|
||||||
let last = 0
|
let last = 0
|
||||||
|
|
||||||
@@ -48,12 +49,12 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
|||||||
// Batch all event emissions so all store updates result in a single render
|
// Batch all event emissions so all store updates result in a single render
|
||||||
batch(() => {
|
batch(() => {
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
emitter.emit(event.type, event)
|
emitter.emit("event", event)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEvent = (event: Event) => {
|
const handleEvent = (event: GlobalEvent) => {
|
||||||
queue.push(event)
|
queue.push(event)
|
||||||
const elapsed = Date.now() - last
|
const elapsed = Date.now() - last
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
|||||||
;(async () => {
|
;(async () => {
|
||||||
while (true) {
|
while (true) {
|
||||||
if (abort.signal.aborted || ctrl.signal.aborted) break
|
if (abort.signal.aborted || ctrl.signal.aborted) break
|
||||||
const events = await sdk.event.subscribe({}, { signal: ctrl.signal })
|
const events = await sdk.global.event({ signal: ctrl.signal })
|
||||||
|
|
||||||
for await (const event of events.stream) {
|
for await (const event of events.stream) {
|
||||||
if (ctrl.signal.aborted) break
|
if (ctrl.signal.aborted) break
|
||||||
@@ -89,7 +90,7 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
|||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (props.events) {
|
if (props.events) {
|
||||||
const unsub = await props.events.subscribe(props.directory, handleEvent)
|
const unsub = await props.events.subscribe(handleEvent)
|
||||||
onCleanup(unsub)
|
onCleanup(unsub)
|
||||||
} else {
|
} else {
|
||||||
startSSE()
|
startSSE()
|
||||||
|
|||||||
@@ -17,18 +17,19 @@ import type {
|
|||||||
ProviderListResponse,
|
ProviderListResponse,
|
||||||
ProviderAuthMethod,
|
ProviderAuthMethod,
|
||||||
VcsInfo,
|
VcsInfo,
|
||||||
|
Workspace,
|
||||||
} from "@opencode-ai/sdk/v2"
|
} from "@opencode-ai/sdk/v2"
|
||||||
import { createStore, produce, reconcile } from "solid-js/store"
|
import { createStore, produce, reconcile } from "solid-js/store"
|
||||||
|
import { useProject } from "@tui/context/project"
|
||||||
|
import { useEvent } from "@tui/context/event"
|
||||||
import { useSDK } from "@tui/context/sdk"
|
import { useSDK } from "@tui/context/sdk"
|
||||||
import { Binary } from "@opencode-ai/util/binary"
|
import { Binary } from "@opencode-ai/util/binary"
|
||||||
import { createSimpleContext } from "./helper"
|
import { createSimpleContext } from "./helper"
|
||||||
import type { Snapshot } from "@/snapshot"
|
import type { Snapshot } from "@/snapshot"
|
||||||
import { useExit } from "./exit"
|
import { useExit } from "./exit"
|
||||||
import { useArgs } from "./args"
|
import { useArgs } from "./args"
|
||||||
import { batch, onMount } from "solid-js"
|
import { batch, createEffect, on } from "solid-js"
|
||||||
import { Log } from "@/util/log"
|
import { Log } from "@/util/log"
|
||||||
import type { Path } from "@opencode-ai/sdk"
|
|
||||||
import type { Workspace } from "@opencode-ai/sdk/v2"
|
|
||||||
import { ConsoleState, emptyConsoleState, type ConsoleState as ConsoleStateType } from "@/config/console-state"
|
import { ConsoleState, emptyConsoleState, type ConsoleState as ConsoleStateType } from "@/config/console-state"
|
||||||
|
|
||||||
export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||||
@@ -74,9 +75,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
[key: string]: McpResource
|
[key: string]: McpResource
|
||||||
}
|
}
|
||||||
formatter: FormatterStatus[]
|
formatter: FormatterStatus[]
|
||||||
vcs: VcsInfo | undefined
|
|
||||||
path: Path
|
|
||||||
workspaceList: Workspace[]
|
workspaceList: Workspace[]
|
||||||
|
vcs: VcsInfo | undefined
|
||||||
}>({
|
}>({
|
||||||
provider_next: {
|
provider_next: {
|
||||||
all: [],
|
all: [],
|
||||||
@@ -103,21 +103,25 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
mcp: {},
|
mcp: {},
|
||||||
mcp_resource: {},
|
mcp_resource: {},
|
||||||
formatter: [],
|
formatter: [],
|
||||||
vcs: undefined,
|
|
||||||
path: { state: "", config: "", worktree: "", directory: "" },
|
|
||||||
workspaceList: [],
|
workspaceList: [],
|
||||||
|
vcs: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const event = useEvent()
|
||||||
|
const project = useProject()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
|
|
||||||
async function syncWorkspaces() {
|
async function syncWorkspaces() {
|
||||||
|
const workspace = project.workspace.current()
|
||||||
const result = await sdk.client.experimental.workspace.list().catch(() => undefined)
|
const result = await sdk.client.experimental.workspace.list().catch(() => undefined)
|
||||||
if (!result?.data) return
|
if (!result?.data) return
|
||||||
setStore("workspaceList", reconcile(result.data))
|
setStore("workspaceList", reconcile(result.data))
|
||||||
|
if (!result.data.some((item) => item.id === workspace)) {
|
||||||
|
project.workspace.set(undefined)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sdk.event.listen((e) => {
|
event.subscribe((event) => {
|
||||||
const event = e.details
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case "server.instance.disposed":
|
case "server.instance.disposed":
|
||||||
bootstrap()
|
bootstrap()
|
||||||
@@ -344,7 +348,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "lsp.updated": {
|
case "lsp.updated": {
|
||||||
sdk.client.lsp.status().then((x) => setStore("lsp", x.data!))
|
const workspace = project.workspace.current()
|
||||||
|
sdk.client.lsp.status({ workspace }).then((x) => setStore("lsp", x.data!))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,25 +365,28 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
console.log("bootstrapping")
|
console.log("bootstrapping")
|
||||||
|
const workspace = project.workspace.current()
|
||||||
const start = Date.now() - 30 * 24 * 60 * 60 * 1000
|
const start = Date.now() - 30 * 24 * 60 * 60 * 1000
|
||||||
const sessionListPromise = sdk.client.session
|
const sessionListPromise = sdk.client.session
|
||||||
.list({ start: start })
|
.list({ start: start, workspace })
|
||||||
.then((x) => (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)))
|
.then((x) => (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)))
|
||||||
|
|
||||||
// blocking - include session.list when continuing a session
|
// blocking - include session.list when continuing a session
|
||||||
const providersPromise = sdk.client.config.providers({}, { throwOnError: true })
|
const providersPromise = sdk.client.config.providers({ workspace }, { throwOnError: true })
|
||||||
const providerListPromise = sdk.client.provider.list({}, { throwOnError: true })
|
const providerListPromise = sdk.client.provider.list({ workspace }, { throwOnError: true })
|
||||||
const consoleStatePromise = sdk.client.experimental.console
|
const consoleStatePromise = sdk.client.experimental.console
|
||||||
.get({}, { throwOnError: true })
|
.get({ workspace }, { throwOnError: true })
|
||||||
.then((x) => ConsoleState.parse(x.data))
|
.then((x) => ConsoleState.parse(x.data))
|
||||||
.catch(() => emptyConsoleState)
|
.catch(() => emptyConsoleState)
|
||||||
const agentsPromise = sdk.client.app.agents({}, { throwOnError: true })
|
const agentsPromise = sdk.client.app.agents({ workspace }, { throwOnError: true })
|
||||||
const configPromise = sdk.client.config.get({}, { throwOnError: true })
|
const configPromise = sdk.client.config.get({ workspace }, { throwOnError: true })
|
||||||
|
const projectPromise = project.sync()
|
||||||
const blockingRequests: Promise<unknown>[] = [
|
const blockingRequests: Promise<unknown>[] = [
|
||||||
providersPromise,
|
providersPromise,
|
||||||
providerListPromise,
|
providerListPromise,
|
||||||
agentsPromise,
|
agentsPromise,
|
||||||
configPromise,
|
configPromise,
|
||||||
|
projectPromise,
|
||||||
...(args.continue ? [sessionListPromise] : []),
|
...(args.continue ? [sessionListPromise] : []),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -423,17 +431,18 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
Promise.all([
|
Promise.all([
|
||||||
...(args.continue ? [] : [sessionListPromise.then((sessions) => setStore("session", reconcile(sessions)))]),
|
...(args.continue ? [] : [sessionListPromise.then((sessions) => setStore("session", reconcile(sessions)))]),
|
||||||
consoleStatePromise.then((consoleState) => setStore("console_state", reconcile(consoleState))),
|
consoleStatePromise.then((consoleState) => setStore("console_state", reconcile(consoleState))),
|
||||||
sdk.client.command.list().then((x) => setStore("command", reconcile(x.data ?? []))),
|
sdk.client.command.list({ workspace }).then((x) => setStore("command", reconcile(x.data ?? []))),
|
||||||
sdk.client.lsp.status().then((x) => setStore("lsp", reconcile(x.data!))),
|
sdk.client.lsp.status({ workspace }).then((x) => setStore("lsp", reconcile(x.data!))),
|
||||||
sdk.client.mcp.status().then((x) => setStore("mcp", reconcile(x.data!))),
|
sdk.client.mcp.status({ workspace }).then((x) => setStore("mcp", reconcile(x.data!))),
|
||||||
sdk.client.experimental.resource.list().then((x) => setStore("mcp_resource", reconcile(x.data ?? {}))),
|
sdk.client.experimental.resource
|
||||||
sdk.client.formatter.status().then((x) => setStore("formatter", reconcile(x.data!))),
|
.list({ workspace })
|
||||||
sdk.client.session.status().then((x) => {
|
.then((x) => setStore("mcp_resource", reconcile(x.data ?? {}))),
|
||||||
|
sdk.client.formatter.status({ workspace }).then((x) => setStore("formatter", reconcile(x.data!))),
|
||||||
|
sdk.client.session.status({ workspace }).then((x) => {
|
||||||
setStore("session_status", reconcile(x.data!))
|
setStore("session_status", reconcile(x.data!))
|
||||||
}),
|
}),
|
||||||
sdk.client.provider.auth().then((x) => setStore("provider_auth", reconcile(x.data ?? {}))),
|
sdk.client.provider.auth({ workspace }).then((x) => setStore("provider_auth", reconcile(x.data ?? {}))),
|
||||||
sdk.client.vcs.get().then((x) => setStore("vcs", reconcile(x.data))),
|
sdk.client.vcs.get({ workspace }).then((x) => setStore("vcs", reconcile(x.data))),
|
||||||
sdk.client.path.get().then((x) => setStore("path", reconcile(x.data!))),
|
|
||||||
syncWorkspaces(),
|
syncWorkspaces(),
|
||||||
]).then(() => {
|
]).then(() => {
|
||||||
setStore("status", "complete")
|
setStore("status", "complete")
|
||||||
@@ -449,11 +458,17 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
bootstrap()
|
|
||||||
})
|
|
||||||
|
|
||||||
const fullSyncedSessions = new Set<string>()
|
const fullSyncedSessions = new Set<string>()
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
() => project.workspace.current(),
|
||||||
|
() => {
|
||||||
|
fullSyncedSessions.clear()
|
||||||
|
void bootstrap()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
data: store,
|
data: store,
|
||||||
set: setStore,
|
set: setStore,
|
||||||
@@ -463,6 +478,9 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
get ready() {
|
get ready() {
|
||||||
return store.status !== "loading"
|
return store.status !== "loading"
|
||||||
},
|
},
|
||||||
|
get path() {
|
||||||
|
return project.instance.path()
|
||||||
|
},
|
||||||
session: {
|
session: {
|
||||||
get(sessionID: string) {
|
get(sessionID: string) {
|
||||||
const match = Binary.search(store.session, sessionID, (s) => s.id)
|
const match = Binary.search(store.session, sessionID, (s) => s.id)
|
||||||
@@ -481,11 +499,12 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
},
|
},
|
||||||
async sync(sessionID: string) {
|
async sync(sessionID: string) {
|
||||||
if (fullSyncedSessions.has(sessionID)) return
|
if (fullSyncedSessions.has(sessionID)) return
|
||||||
|
const workspace = project.workspace.current()
|
||||||
const [session, messages, todo, diff] = await Promise.all([
|
const [session, messages, todo, diff] = await Promise.all([
|
||||||
sdk.client.session.get({ sessionID }, { throwOnError: true }),
|
sdk.client.session.get({ sessionID, workspace }, { throwOnError: true }),
|
||||||
sdk.client.session.messages({ sessionID, limit: 100 }),
|
sdk.client.session.messages({ sessionID, limit: 100, workspace }),
|
||||||
sdk.client.session.todo({ sessionID }),
|
sdk.client.session.todo({ sessionID, workspace }),
|
||||||
sdk.client.session.diff({ sessionID }),
|
sdk.client.session.diff({ sessionID, workspace }),
|
||||||
])
|
])
|
||||||
setStore(
|
setStore(
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
@@ -504,8 +523,11 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
workspace: {
|
workspace: {
|
||||||
|
list() {
|
||||||
|
return store.workspaceList
|
||||||
|
},
|
||||||
get(workspaceID: string) {
|
get(workspaceID: string) {
|
||||||
return store.workspaceList.find((workspace) => workspace.id === workspaceID)
|
return store.workspaceList.find((item) => item.id === workspaceID)
|
||||||
},
|
},
|
||||||
sync: syncWorkspaces,
|
sync: syncWorkspaces,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { ParsedKey } from "@opentui/core"
|
import type { ParsedKey } from "@opentui/core"
|
||||||
import type { TuiDialogSelectOption, TuiPluginApi, TuiRouteDefinition, TuiSlotProps } from "@opencode-ai/plugin/tui"
|
import type { TuiDialogSelectOption, TuiPluginApi, TuiRouteDefinition, TuiSlotProps } from "@opencode-ai/plugin/tui"
|
||||||
import type { useCommandDialog } from "@tui/component/dialog-command"
|
import type { useCommandDialog } from "@tui/component/dialog-command"
|
||||||
|
import type { useEvent } from "@tui/context/event"
|
||||||
import type { useKeybind } from "@tui/context/keybind"
|
import type { useKeybind } from "@tui/context/keybind"
|
||||||
import type { useRoute } from "@tui/context/route"
|
import type { useRoute } from "@tui/context/route"
|
||||||
import type { useSDK } from "@tui/context/sdk"
|
import type { useSDK } from "@tui/context/sdk"
|
||||||
@@ -36,6 +37,7 @@ type Input = {
|
|||||||
route: ReturnType<typeof useRoute>
|
route: ReturnType<typeof useRoute>
|
||||||
routes: RouteMap
|
routes: RouteMap
|
||||||
bump: () => void
|
bump: () => void
|
||||||
|
event: ReturnType<typeof useEvent>
|
||||||
sdk: ReturnType<typeof useSDK>
|
sdk: ReturnType<typeof useSDK>
|
||||||
sync: ReturnType<typeof useSync>
|
sync: ReturnType<typeof useSync>
|
||||||
theme: ReturnType<typeof useTheme>
|
theme: ReturnType<typeof useTheme>
|
||||||
@@ -136,7 +138,7 @@ function stateApi(sync: ReturnType<typeof useSync>): TuiPluginApi["state"] {
|
|||||||
return sync.data.provider
|
return sync.data.provider
|
||||||
},
|
},
|
||||||
get path() {
|
get path() {
|
||||||
return sync.data.path
|
return sync.path
|
||||||
},
|
},
|
||||||
get vcs() {
|
get vcs() {
|
||||||
if (!sync.data.vcs) return
|
if (!sync.data.vcs) return
|
||||||
@@ -342,7 +344,7 @@ export function createTuiApi(input: Input): TuiPluginApi {
|
|||||||
get client() {
|
get client() {
|
||||||
return input.sdk.client
|
return input.sdk.client
|
||||||
},
|
},
|
||||||
event: input.sdk.event,
|
event: input.event,
|
||||||
renderer: input.renderer,
|
renderer: input.renderer,
|
||||||
slots: {
|
slots: {
|
||||||
register() {
|
register() {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Prompt, type PromptRef } from "@tui/component/prompt"
|
import { Prompt, type PromptRef } from "@tui/component/prompt"
|
||||||
import { createEffect, createSignal } from "solid-js"
|
import { createEffect, createSignal } from "solid-js"
|
||||||
import { Logo } from "../component/logo"
|
import { Logo } from "../component/logo"
|
||||||
|
import { useProject } from "../context/project"
|
||||||
import { useSync } from "../context/sync"
|
import { useSync } from "../context/sync"
|
||||||
import { Toast } from "../ui/toast"
|
import { Toast } from "../ui/toast"
|
||||||
import { useArgs } from "../context/args"
|
import { useArgs } from "../context/args"
|
||||||
@@ -18,6 +19,7 @@ const placeholder = {
|
|||||||
|
|
||||||
export function Home() {
|
export function Home() {
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
|
const project = useProject()
|
||||||
const route = useRouteData("home")
|
const route = useRouteData("home")
|
||||||
const promptRef = usePromptRef()
|
const promptRef = usePromptRef()
|
||||||
const [ref, setRef] = createSignal<PromptRef | undefined>()
|
const [ref, setRef] = createSignal<PromptRef | undefined>()
|
||||||
@@ -63,11 +65,16 @@ export function Home() {
|
|||||||
</box>
|
</box>
|
||||||
<box height={1} minHeight={0} flexShrink={1} />
|
<box height={1} minHeight={0} flexShrink={1} />
|
||||||
<box width="100%" maxWidth={75} zIndex={1000} paddingTop={1} flexShrink={0}>
|
<box width="100%" maxWidth={75} zIndex={1000} paddingTop={1} flexShrink={0}>
|
||||||
<TuiPluginRuntime.Slot name="home_prompt" mode="replace" workspace_id={route.workspaceID} ref={bind}>
|
<TuiPluginRuntime.Slot
|
||||||
|
name="home_prompt"
|
||||||
|
mode="replace"
|
||||||
|
workspace_id={project.workspace.current()}
|
||||||
|
ref={bind}
|
||||||
|
>
|
||||||
<Prompt
|
<Prompt
|
||||||
ref={bind}
|
ref={bind}
|
||||||
workspaceID={route.workspaceID}
|
workspaceID={project.workspace.current()}
|
||||||
right={<TuiPluginRuntime.Slot name="home_prompt_right" workspace_id={route.workspaceID} />}
|
right={<TuiPluginRuntime.Slot name="home_prompt_right" workspace_id={project.workspace.current()} />}
|
||||||
placeholders={placeholder}
|
placeholders={placeholder}
|
||||||
/>
|
/>
|
||||||
</TuiPluginRuntime.Slot>
|
</TuiPluginRuntime.Slot>
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import {
|
|||||||
import { Dynamic } from "solid-js/web"
|
import { Dynamic } from "solid-js/web"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { useRoute, useRouteData } from "@tui/context/route"
|
import { useRoute, useRouteData } from "@tui/context/route"
|
||||||
|
import { useProject } from "@tui/context/project"
|
||||||
import { useSync } from "@tui/context/sync"
|
import { useSync } from "@tui/context/sync"
|
||||||
|
import { useEvent } from "@tui/context/event"
|
||||||
import { SplitBorder } from "@tui/component/border"
|
import { SplitBorder } from "@tui/component/border"
|
||||||
import { Spinner } from "@tui/component/spinner"
|
import { Spinner } from "@tui/component/spinner"
|
||||||
import { selectedForeground, useTheme } from "@tui/context/theme"
|
import { selectedForeground, useTheme } from "@tui/context/theme"
|
||||||
@@ -83,9 +85,15 @@ import { UI } from "@/cli/ui.ts"
|
|||||||
import { useTuiConfig } from "../../context/tui-config"
|
import { useTuiConfig } from "../../context/tui-config"
|
||||||
import { getScrollAcceleration } from "../../util/scroll"
|
import { getScrollAcceleration } from "../../util/scroll"
|
||||||
import { TuiPluginRuntime } from "../../plugin"
|
import { TuiPluginRuntime } from "../../plugin"
|
||||||
|
import { DialogGoUpsell } from "../../component/dialog-go-upsell"
|
||||||
|
import { SessionRetry } from "@/session/retry"
|
||||||
|
|
||||||
addDefaultParsers(parsers.parsers)
|
addDefaultParsers(parsers.parsers)
|
||||||
|
|
||||||
|
const GO_UPSELL_LAST_SEEN_AT = "go_upsell_last_seen_at"
|
||||||
|
const GO_UPSELL_DONT_SHOW = "go_upsell_dont_show"
|
||||||
|
const GO_UPSELL_WINDOW = 86_400_000 // 24 hrs
|
||||||
|
|
||||||
const context = createContext<{
|
const context = createContext<{
|
||||||
width: number
|
width: number
|
||||||
sessionID: string
|
sessionID: string
|
||||||
@@ -110,6 +118,8 @@ export function Session() {
|
|||||||
const route = useRouteData("session")
|
const route = useRouteData("session")
|
||||||
const { navigate } = useRoute()
|
const { navigate } = useRoute()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
|
const event = useEvent()
|
||||||
|
const project = useProject()
|
||||||
const tuiConfig = useTuiConfig()
|
const tuiConfig = useTuiConfig()
|
||||||
const kv = useKV()
|
const kv = useKV()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
@@ -149,7 +159,7 @@ export function Session() {
|
|||||||
const [timestamps, setTimestamps] = kv.signal<"hide" | "show">("timestamps", "hide")
|
const [timestamps, setTimestamps] = kv.signal<"hide" | "show">("timestamps", "hide")
|
||||||
const [showDetails, setShowDetails] = kv.signal("tool_details_visibility", true)
|
const [showDetails, setShowDetails] = kv.signal("tool_details_visibility", true)
|
||||||
const [showAssistantMetadata, setShowAssistantMetadata] = kv.signal("assistant_metadata_visibility", true)
|
const [showAssistantMetadata, setShowAssistantMetadata] = kv.signal("assistant_metadata_visibility", true)
|
||||||
const [showScrollbar, setShowScrollbar] = kv.signal("scrollbar_visible", true)
|
const [showScrollbar, setShowScrollbar] = kv.signal("scrollbar_visible", false)
|
||||||
const [diffWrapMode] = kv.signal<"word" | "none">("diff_wrap_mode", "word")
|
const [diffWrapMode] = kv.signal<"word" | "none">("diff_wrap_mode", "word")
|
||||||
const [animationsEnabled, setAnimationsEnabled] = kv.signal("animations_enabled", true)
|
const [animationsEnabled, setAnimationsEnabled] = kv.signal("animations_enabled", true)
|
||||||
const [showGenericToolOutput, setShowGenericToolOutput] = kv.signal("generic_tool_output_visibility", false)
|
const [showGenericToolOutput, setShowGenericToolOutput] = kv.signal("generic_tool_output_visibility", false)
|
||||||
@@ -166,10 +176,16 @@ export function Session() {
|
|||||||
const providers = createMemo(() => Model.index(sync.data.provider))
|
const providers = createMemo(() => Model.index(sync.data.provider))
|
||||||
|
|
||||||
const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig))
|
const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig))
|
||||||
|
const toast = useToast()
|
||||||
|
const sdk = useSDK()
|
||||||
|
|
||||||
createEffect(async () => {
|
createEffect(async () => {
|
||||||
await sync.session
|
await sdk.client.session
|
||||||
.sync(route.sessionID)
|
.get({ sessionID: route.sessionID }, { throwOnError: true })
|
||||||
|
.then((x) => {
|
||||||
|
project.workspace.set(x.data?.workspaceID)
|
||||||
|
})
|
||||||
|
.then(() => sync.session.sync(route.sessionID))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (scroll) scroll.scrollBy(100_000)
|
if (scroll) scroll.scrollBy(100_000)
|
||||||
})
|
})
|
||||||
@@ -183,13 +199,10 @@ export function Session() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const toast = useToast()
|
|
||||||
const sdk = useSDK()
|
|
||||||
|
|
||||||
// Handle initial prompt from fork
|
// Handle initial prompt from fork
|
||||||
let seeded = false
|
let seeded = false
|
||||||
let lastSwitch: string | undefined = undefined
|
let lastSwitch: string | undefined = undefined
|
||||||
sdk.event.on("message.part.updated", (evt) => {
|
event.on("message.part.updated", (evt) => {
|
||||||
const part = evt.properties.part
|
const part = evt.properties.part
|
||||||
if (part.type !== "tool") return
|
if (part.type !== "tool") return
|
||||||
if (part.sessionID !== route.sessionID) return
|
if (part.sessionID !== route.sessionID) return
|
||||||
@@ -218,6 +231,23 @@ export function Session() {
|
|||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const renderer = useRenderer()
|
const renderer = useRenderer()
|
||||||
|
|
||||||
|
event.on("session.status", (evt) => {
|
||||||
|
if (evt.properties.sessionID !== route.sessionID) return
|
||||||
|
if (evt.properties.status.type !== "retry") return
|
||||||
|
if (evt.properties.status.message !== SessionRetry.GO_UPSELL_MESSAGE) return
|
||||||
|
if (dialog.stack.length > 0) return
|
||||||
|
|
||||||
|
const seen = kv.get(GO_UPSELL_LAST_SEEN_AT)
|
||||||
|
if (typeof seen === "number" && Date.now() - seen < GO_UPSELL_WINDOW) return
|
||||||
|
|
||||||
|
if (kv.get(GO_UPSELL_DONT_SHOW)) return
|
||||||
|
|
||||||
|
DialogGoUpsell.show(dialog).then((dontShowAgain) => {
|
||||||
|
if (dontShowAgain) kv.set(GO_UPSELL_DONT_SHOW, true)
|
||||||
|
kv.set(GO_UPSELL_LAST_SEEN_AT, Date.now())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// Allow exit when in child session (prompt is hidden)
|
// Allow exit when in child session (prompt is hidden)
|
||||||
const exit = useExit()
|
const exit = useExit()
|
||||||
|
|
||||||
@@ -1768,7 +1798,7 @@ function Bash(props: ToolProps<typeof BashTool>) {
|
|||||||
const workdir = props.input.workdir
|
const workdir = props.input.workdir
|
||||||
if (!workdir || workdir === ".") return undefined
|
if (!workdir || workdir === ".") return undefined
|
||||||
|
|
||||||
const base = sync.data.path.directory
|
const base = sync.path.directory
|
||||||
if (!base) return undefined
|
if (!base) return undefined
|
||||||
|
|
||||||
const absolute = path.resolve(base, workdir)
|
const absolute = path.resolve(base, workdir)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { errorMessage } from "@/util/error"
|
|||||||
import { withTimeout } from "@/util/timeout"
|
import { withTimeout } from "@/util/timeout"
|
||||||
import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network"
|
import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network"
|
||||||
import { Filesystem } from "@/util/filesystem"
|
import { Filesystem } from "@/util/filesystem"
|
||||||
import type { Event } from "@opencode-ai/sdk/v2"
|
import type { GlobalEvent } from "@opencode-ai/sdk/v2"
|
||||||
import type { EventSource } from "./context/sdk"
|
import type { EventSource } from "./context/sdk"
|
||||||
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
|
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
|
||||||
import { TuiConfig } from "@/config/tui"
|
import { TuiConfig } from "@/config/tui"
|
||||||
@@ -43,18 +43,10 @@ function createWorkerFetch(client: RpcClient): typeof fetch {
|
|||||||
|
|
||||||
function createEventSource(client: RpcClient): EventSource {
|
function createEventSource(client: RpcClient): EventSource {
|
||||||
return {
|
return {
|
||||||
subscribe: async (directory, handler) => {
|
subscribe: async (handler) => {
|
||||||
const id = await client.call("subscribe", { directory })
|
return client.on<GlobalEvent>("global.event", (e) => {
|
||||||
const unsub = client.on<{ id: string; event: Event }>("event", (e) => {
|
handler(e)
|
||||||
if (e.id === id) {
|
|
||||||
handler(e.event)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return () => {
|
|
||||||
unsub()
|
|
||||||
client.call("unsubscribe", { id })
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,10 @@ import { InstanceBootstrap } from "@/project/bootstrap"
|
|||||||
import { Rpc } from "@/util/rpc"
|
import { Rpc } from "@/util/rpc"
|
||||||
import { upgrade } from "@/cli/upgrade"
|
import { upgrade } from "@/cli/upgrade"
|
||||||
import { Config } from "@/config/config"
|
import { Config } from "@/config/config"
|
||||||
import { Bus } from "@/bus"
|
|
||||||
import { GlobalBus } from "@/bus/global"
|
import { GlobalBus } from "@/bus/global"
|
||||||
import type { Event } from "@opencode-ai/sdk/v2"
|
import type { GlobalEvent } from "@opencode-ai/sdk/v2"
|
||||||
import { Flag } from "@/flag/flag"
|
import { Flag } from "@/flag/flag"
|
||||||
import { setTimeout as sleep } from "node:timers/promises"
|
|
||||||
import { writeHeapSnapshot } from "node:v8"
|
import { writeHeapSnapshot } from "node:v8"
|
||||||
import { WorkspaceID } from "@/control-plane/schema"
|
|
||||||
import { Heap } from "@/cli/heap"
|
import { Heap } from "@/cli/heap"
|
||||||
|
|
||||||
await Log.init({
|
await Log.init({
|
||||||
@@ -45,87 +42,6 @@ GlobalBus.on("event", (event) => {
|
|||||||
|
|
||||||
let server: Awaited<ReturnType<typeof Server.listen>> | undefined
|
let server: Awaited<ReturnType<typeof Server.listen>> | undefined
|
||||||
|
|
||||||
const eventStreams = new Map<string, AbortController>()
|
|
||||||
|
|
||||||
function startEventStream(directory: string) {
|
|
||||||
const id = crypto.randomUUID()
|
|
||||||
|
|
||||||
const abort = new AbortController()
|
|
||||||
const signal = abort.signal
|
|
||||||
|
|
||||||
eventStreams.set(id, abort)
|
|
||||||
|
|
||||||
async function run() {
|
|
||||||
while (!signal.aborted) {
|
|
||||||
const shouldReconnect = await Instance.provide({
|
|
||||||
directory,
|
|
||||||
init: InstanceBootstrap,
|
|
||||||
fn: () =>
|
|
||||||
new Promise<boolean>((resolve) => {
|
|
||||||
Rpc.emit("event", {
|
|
||||||
type: "server.connected",
|
|
||||||
properties: {},
|
|
||||||
} satisfies Event)
|
|
||||||
|
|
||||||
let settled = false
|
|
||||||
const settle = (value: boolean) => {
|
|
||||||
if (settled) return
|
|
||||||
settled = true
|
|
||||||
signal.removeEventListener("abort", onAbort)
|
|
||||||
unsub()
|
|
||||||
resolve(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const unsub = Bus.subscribeAll((event) => {
|
|
||||||
Rpc.emit("event", {
|
|
||||||
id,
|
|
||||||
event: event as Event,
|
|
||||||
})
|
|
||||||
if (event.type === Bus.InstanceDisposed.type) {
|
|
||||||
settle(true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const onAbort = () => {
|
|
||||||
settle(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
signal.addEventListener("abort", onAbort, { once: true })
|
|
||||||
}),
|
|
||||||
}).catch((error) => {
|
|
||||||
Log.Default.error("event stream subscribe error", {
|
|
||||||
error: error instanceof Error ? error.message : error,
|
|
||||||
})
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!shouldReconnect || signal.aborted) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!signal.aborted) {
|
|
||||||
await sleep(250)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
run().catch((error) => {
|
|
||||||
Log.Default.error("event stream error", {
|
|
||||||
error: error instanceof Error ? error.message : error,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopEventStream(id: string) {
|
|
||||||
const abortController = eventStreams.get(id)
|
|
||||||
if (!abortController) return
|
|
||||||
|
|
||||||
abortController.abort()
|
|
||||||
eventStreams.delete(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const rpc = {
|
export const rpc = {
|
||||||
async fetch(input: { url: string; method: string; headers: Record<string, string>; body?: string }) {
|
async fetch(input: { url: string; method: string; headers: Record<string, string>; body?: string }) {
|
||||||
const headers = { ...input.headers }
|
const headers = { ...input.headers }
|
||||||
@@ -138,7 +54,7 @@ export const rpc = {
|
|||||||
headers,
|
headers,
|
||||||
body: input.body,
|
body: input.body,
|
||||||
})
|
})
|
||||||
const response = await Server.Default().fetch(request)
|
const response = await Server.Default().app.fetch(request)
|
||||||
const body = await response.text()
|
const body = await response.text()
|
||||||
return {
|
return {
|
||||||
status: response.status,
|
status: response.status,
|
||||||
@@ -167,19 +83,9 @@ export const rpc = {
|
|||||||
async reload() {
|
async reload() {
|
||||||
await Config.invalidate(true)
|
await Config.invalidate(true)
|
||||||
},
|
},
|
||||||
async subscribe(input: { directory: string | undefined }) {
|
|
||||||
return startEventStream(input.directory || process.cwd())
|
|
||||||
},
|
|
||||||
async unsubscribe(input: { id: string }) {
|
|
||||||
stopEventStream(input.id)
|
|
||||||
},
|
|
||||||
async shutdown() {
|
async shutdown() {
|
||||||
Log.Default.info("worker shutting down")
|
Log.Default.info("worker shutting down")
|
||||||
|
|
||||||
for (const id of [...eventStreams.keys()]) {
|
|
||||||
stopEventStream(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
await Instance.disposeAll()
|
await Instance.disposeAll()
|
||||||
if (server) await server.stop(true)
|
if (server) await server.stop(true)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -399,6 +399,10 @@ export namespace Config {
|
|||||||
.describe("OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted."),
|
.describe("OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted."),
|
||||||
clientSecret: z.string().optional().describe("OAuth client secret (if required by the authorization server)"),
|
clientSecret: z.string().optional().describe("OAuth client secret (if required by the authorization server)"),
|
||||||
scope: z.string().optional().describe("OAuth scopes to request during authorization"),
|
scope: z.string().optional().describe("OAuth scopes to request during authorization"),
|
||||||
|
redirectUri: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("OAuth redirect URI (default: http://127.0.0.1:19876/mcp/oauth/callback)."),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.meta({
|
.meta({
|
||||||
@@ -786,14 +790,58 @@ export namespace Config {
|
|||||||
})
|
})
|
||||||
export type Layout = z.infer<typeof Layout>
|
export type Layout = z.infer<typeof Layout>
|
||||||
|
|
||||||
export const Provider = ModelsDev.Provider.partial()
|
export const Model = z
|
||||||
.extend({
|
.object({
|
||||||
whitelist: z.array(z.string()).optional(),
|
id: z.string(),
|
||||||
blacklist: z.array(z.string()).optional(),
|
name: z.string(),
|
||||||
models: z
|
family: z.string().optional(),
|
||||||
.record(
|
release_date: z.string(),
|
||||||
z.string(),
|
attachment: z.boolean(),
|
||||||
ModelsDev.Model.partial().extend({
|
reasoning: z.boolean(),
|
||||||
|
temperature: z.boolean(),
|
||||||
|
tool_call: z.boolean(),
|
||||||
|
interleaved: z
|
||||||
|
.union([
|
||||||
|
z.literal(true),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
field: z.enum(["reasoning_content", "reasoning_details"]),
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
|
])
|
||||||
|
.optional(),
|
||||||
|
cost: z
|
||||||
|
.object({
|
||||||
|
input: z.number(),
|
||||||
|
output: z.number(),
|
||||||
|
cache_read: z.number().optional(),
|
||||||
|
cache_write: z.number().optional(),
|
||||||
|
context_over_200k: z
|
||||||
|
.object({
|
||||||
|
input: z.number(),
|
||||||
|
output: z.number(),
|
||||||
|
cache_read: z.number().optional(),
|
||||||
|
cache_write: z.number().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
limit: z.object({
|
||||||
|
context: z.number(),
|
||||||
|
input: z.number().optional(),
|
||||||
|
output: z.number(),
|
||||||
|
}),
|
||||||
|
modalities: z
|
||||||
|
.object({
|
||||||
|
input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
|
||||||
|
output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
experimental: z.boolean().optional(),
|
||||||
|
status: z.enum(["alpha", "beta", "deprecated"]).optional(),
|
||||||
|
provider: z.object({ npm: z.string().optional(), api: z.string().optional() }).optional(),
|
||||||
|
options: z.record(z.string(), z.any()),
|
||||||
|
headers: z.record(z.string(), z.string()).optional(),
|
||||||
variants: z
|
variants: z
|
||||||
.record(
|
.record(
|
||||||
z.string(),
|
z.string(),
|
||||||
@@ -805,9 +853,18 @@ export namespace Config {
|
|||||||
)
|
)
|
||||||
.optional()
|
.optional()
|
||||||
.describe("Variant-specific configuration"),
|
.describe("Variant-specific configuration"),
|
||||||
}),
|
})
|
||||||
)
|
.partial()
|
||||||
.optional(),
|
|
||||||
|
export const Provider = z
|
||||||
|
.object({
|
||||||
|
api: z.string().optional(),
|
||||||
|
name: z.string(),
|
||||||
|
env: z.array(z.string()),
|
||||||
|
id: z.string(),
|
||||||
|
npm: z.string().optional(),
|
||||||
|
whitelist: z.array(z.string()).optional(),
|
||||||
|
blacklist: z.array(z.string()).optional(),
|
||||||
options: z
|
options: z
|
||||||
.object({
|
.object({
|
||||||
apiKey: z.string().optional(),
|
apiKey: z.string().optional(),
|
||||||
@@ -840,11 +897,14 @@ export namespace Config {
|
|||||||
})
|
})
|
||||||
.catchall(z.any())
|
.catchall(z.any())
|
||||||
.optional(),
|
.optional(),
|
||||||
|
models: z.record(z.string(), Model).optional(),
|
||||||
})
|
})
|
||||||
|
.partial()
|
||||||
.strict()
|
.strict()
|
||||||
.meta({
|
.meta({
|
||||||
ref: "ProviderConfig",
|
ref: "ProviderConfig",
|
||||||
})
|
})
|
||||||
|
|
||||||
export type Provider = z.infer<typeof Provider>
|
export type Provider = z.infer<typeof Provider>
|
||||||
|
|
||||||
export const Info = z
|
export const Info = z
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Context } from "../util/context"
|
||||||
|
import type { WorkspaceID } from "../control-plane/schema"
|
||||||
|
|
||||||
|
export interface WorkspaceContext {
|
||||||
|
workspaceID: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = Context.create<WorkspaceContext>("instance")
|
||||||
|
|
||||||
|
export const WorkspaceContext = {
|
||||||
|
async provide<R>(input: { workspaceID: WorkspaceID; fn: () => R }): Promise<R> {
|
||||||
|
return context.provide({ workspaceID: input.workspaceID as string }, () => input.fn())
|
||||||
|
},
|
||||||
|
|
||||||
|
get workspaceID() {
|
||||||
|
try {
|
||||||
|
return context.use().workspaceID
|
||||||
|
} catch (err) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -134,12 +134,12 @@ export namespace Workspace {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
await parseSSE(res.body, stop, (event) => {
|
// await parseSSE(res.body, stop, (event) => {
|
||||||
GlobalBus.emit("event", {
|
// GlobalBus.emit("event", {
|
||||||
directory: space.id,
|
// directory: space.id,
|
||||||
payload: event,
|
// payload: event,
|
||||||
})
|
// })
|
||||||
})
|
// })
|
||||||
|
|
||||||
// Wait 250ms and retry if SSE connection fails
|
// Wait 250ms and retry if SSE connection fails
|
||||||
await sleep(250)
|
await sleep(250)
|
||||||
|
|||||||
@@ -499,4 +499,3 @@ const rt = lazy(async () => {
|
|||||||
|
|
||||||
type RT = Awaited<ReturnType<typeof rt>>
|
type RT = Awaited<ReturnType<typeof rt>>
|
||||||
export const runPromiseExit: RT["runPromiseExit"] = async (...args) => (await rt()).runPromiseExit(...(args as [any]))
|
export const runPromiseExit: RT["runPromiseExit"] = async (...args) => (await rt()).runPromiseExit(...(args as [any]))
|
||||||
export const runPromise: RT["runPromise"] = async (...args) => (await rt()).runPromise(...(args as [any]))
|
|
||||||
|
|||||||
@@ -4,3 +4,7 @@ import type { InstanceContext } from "@/project/instance"
|
|||||||
export const InstanceRef = ServiceMap.Reference<InstanceContext | undefined>("~opencode/InstanceRef", {
|
export const InstanceRef = ServiceMap.Reference<InstanceContext | undefined>("~opencode/InstanceRef", {
|
||||||
defaultValue: () => undefined,
|
defaultValue: () => undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const WorkspaceRef = ServiceMap.Reference<string | undefined>("~opencode/WorkspaceRef", {
|
||||||
|
defaultValue: () => undefined,
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Effect, Fiber, ScopedCache, Scope, ServiceMap } from "effect"
|
import { Effect, Fiber, ScopedCache, Scope, ServiceMap } from "effect"
|
||||||
import { Instance, type InstanceContext } from "@/project/instance"
|
import { Instance, type InstanceContext } from "@/project/instance"
|
||||||
import { Context } from "@/util/context"
|
import { Context } from "@/util/context"
|
||||||
import { InstanceRef } from "./instance-ref"
|
import { InstanceRef, WorkspaceRef } from "./instance-ref"
|
||||||
import { registerDisposer } from "./instance-registry"
|
import { registerDisposer } from "./instance-registry"
|
||||||
|
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
||||||
|
|
||||||
const TypeId = "~opencode/InstanceState"
|
const TypeId = "~opencode/InstanceState"
|
||||||
|
|
||||||
@@ -28,6 +29,10 @@ export namespace InstanceState {
|
|||||||
return (yield* InstanceRef) ?? Instance.current
|
return (yield* InstanceRef) ?? Instance.current
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const workspaceID = Effect.gen(function* () {
|
||||||
|
return (yield* WorkspaceRef) ?? WorkspaceContext.workspaceID
|
||||||
|
})
|
||||||
|
|
||||||
export const directory = Effect.map(context, (ctx) => ctx.directory)
|
export const directory = Effect.map(context, (ctx) => ctx.directory)
|
||||||
|
|
||||||
export const make = <A, E = never, R = never>(
|
export const make = <A, E = never, R = never>(
|
||||||
@@ -73,10 +78,4 @@ export namespace InstanceState {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
return yield* ScopedCache.invalidate(self.cache, yield* directory)
|
return yield* ScopedCache.invalidate(self.cache, yield* directory)
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* Effect finalizers run on the fiber scheduler after the original async
|
|
||||||
* boundary, so ALS reads like Instance.directory can be gone by then.
|
|
||||||
*/
|
|
||||||
export const withALS = <T>(fn: () => T) => Effect.map(context, (ctx) => Instance.restore(ctx, fn))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,17 @@ import { Effect, Layer, ManagedRuntime } from "effect"
|
|||||||
import * as ServiceMap from "effect/ServiceMap"
|
import * as ServiceMap from "effect/ServiceMap"
|
||||||
import { Instance } from "@/project/instance"
|
import { Instance } from "@/project/instance"
|
||||||
import { Context } from "@/util/context"
|
import { Context } from "@/util/context"
|
||||||
import { InstanceRef } from "./instance-ref"
|
import { InstanceRef, WorkspaceRef } from "./instance-ref"
|
||||||
import { Observability } from "./oltp"
|
import { Observability } from "./oltp"
|
||||||
|
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
||||||
|
|
||||||
export const memoMap = Layer.makeMemoMapUnsafe()
|
export const memoMap = Layer.makeMemoMapUnsafe()
|
||||||
|
|
||||||
function attach<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> {
|
function attach<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> {
|
||||||
try {
|
try {
|
||||||
const ctx = Instance.current
|
const ctx = Instance.current
|
||||||
return Effect.provideService(effect, InstanceRef, ctx)
|
const workspaceID = WorkspaceContext.workspaceID
|
||||||
|
return effect.pipe(Effect.provideService(InstanceRef, ctx), Effect.provideService(WorkspaceRef, workspaceID))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!(err instanceof Context.NotFound)) throw err
|
if (!(err instanceof Context.NotFound)) throw err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Cause, Deferred, Effect, Exit, Fiber, Option, Schema, Scope, SynchronizedRef } from "effect"
|
import { Cause, Deferred, Effect, Exit, Fiber, Schema, Scope, SynchronizedRef } from "effect"
|
||||||
|
|
||||||
export interface Runner<A, E = never> {
|
export interface Runner<A, E = never> {
|
||||||
readonly state: Runner.State<A, E>
|
readonly state: Runner.State<A, E>
|
||||||
readonly busy: boolean
|
readonly busy: boolean
|
||||||
readonly ensureRunning: (work: Effect.Effect<A, E>) => Effect.Effect<A, E>
|
readonly ensureRunning: (work: Effect.Effect<A, E>) => Effect.Effect<A, E>
|
||||||
readonly startShell: (work: (signal: AbortSignal) => Effect.Effect<A, E>) => Effect.Effect<A, E>
|
readonly startShell: (work: Effect.Effect<A, E>) => Effect.Effect<A, E>
|
||||||
readonly cancel: Effect.Effect<void>
|
readonly cancel: Effect.Effect<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,7 +20,6 @@ export namespace Runner {
|
|||||||
interface ShellHandle<A, E> {
|
interface ShellHandle<A, E> {
|
||||||
id: number
|
id: number
|
||||||
fiber: Fiber.Fiber<A, E>
|
fiber: Fiber.Fiber<A, E>
|
||||||
abort: AbortController
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PendingHandle<A, E> {
|
interface PendingHandle<A, E> {
|
||||||
@@ -100,13 +99,7 @@ export namespace Runner {
|
|||||||
}),
|
}),
|
||||||
).pipe(Effect.flatten)
|
).pipe(Effect.flatten)
|
||||||
|
|
||||||
const stopShell = (shell: ShellHandle<A, E>) =>
|
const stopShell = (shell: ShellHandle<A, E>) => Fiber.interrupt(shell.fiber)
|
||||||
Effect.gen(function* () {
|
|
||||||
shell.abort.abort()
|
|
||||||
const exit = yield* Fiber.await(shell.fiber).pipe(Effect.timeoutOption("100 millis"))
|
|
||||||
if (Option.isNone(exit)) yield* Fiber.interrupt(shell.fiber)
|
|
||||||
yield* Fiber.await(shell.fiber).pipe(Effect.exit, Effect.asVoid)
|
|
||||||
})
|
|
||||||
|
|
||||||
const ensureRunning = (work: Effect.Effect<A, E>) =>
|
const ensureRunning = (work: Effect.Effect<A, E>) =>
|
||||||
SynchronizedRef.modifyEffect(
|
SynchronizedRef.modifyEffect(
|
||||||
@@ -138,7 +131,7 @@ export namespace Runner {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const startShell = (work: (signal: AbortSignal) => Effect.Effect<A, E>) =>
|
const startShell = (work: Effect.Effect<A, E>) =>
|
||||||
SynchronizedRef.modifyEffect(
|
SynchronizedRef.modifyEffect(
|
||||||
ref,
|
ref,
|
||||||
Effect.fnUntraced(function* (st) {
|
Effect.fnUntraced(function* (st) {
|
||||||
@@ -153,9 +146,8 @@ export namespace Runner {
|
|||||||
}
|
}
|
||||||
yield* busy
|
yield* busy
|
||||||
const id = next()
|
const id = next()
|
||||||
const abort = new AbortController()
|
const fiber = yield* work.pipe(Effect.ensuring(finishShell(id)), Effect.forkChild)
|
||||||
const fiber = yield* work(abort.signal).pipe(Effect.ensuring(finishShell(id)), Effect.forkChild)
|
const shell = { id, fiber } satisfies ShellHandle<A, E>
|
||||||
const shell = { id, fiber, abort } satisfies ShellHandle<A, E>
|
|
||||||
return [
|
return [
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const exit = yield* Fiber.await(fiber)
|
const exit = yield* Fiber.await(fiber)
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import path from "path"
|
|||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
import { Filesystem } from "../util/filesystem"
|
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { Protected } from "./protected"
|
import { Protected } from "./protected"
|
||||||
import { Ripgrep } from "./ripgrep"
|
import { Ripgrep } from "./ripgrep"
|
||||||
@@ -344,6 +343,7 @@ export namespace File {
|
|||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const appFs = yield* AppFileSystem.Service
|
const appFs = yield* AppFileSystem.Service
|
||||||
|
const git = yield* Git.Service
|
||||||
|
|
||||||
const state = yield* InstanceState.make<State>(
|
const state = yield* InstanceState.make<State>(
|
||||||
Effect.fn("File.state")(() =>
|
Effect.fn("File.state")(() =>
|
||||||
@@ -410,6 +410,10 @@ export namespace File {
|
|||||||
cachedScan = yield* Effect.cached(scan().pipe(Effect.catchCause(() => Effect.void)))
|
cachedScan = yield* Effect.cached(scan().pipe(Effect.catchCause(() => Effect.void)))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const gitText = Effect.fnUntraced(function* (args: string[]) {
|
||||||
|
return (yield* git.run(args, { cwd: Instance.directory })).text()
|
||||||
|
})
|
||||||
|
|
||||||
const init = Effect.fn("File.init")(function* () {
|
const init = Effect.fn("File.init")(function* () {
|
||||||
yield* ensure()
|
yield* ensure()
|
||||||
})
|
})
|
||||||
@@ -417,12 +421,15 @@ export namespace File {
|
|||||||
const status = Effect.fn("File.status")(function* () {
|
const status = Effect.fn("File.status")(function* () {
|
||||||
if (Instance.project.vcs !== "git") return []
|
if (Instance.project.vcs !== "git") return []
|
||||||
|
|
||||||
return yield* Effect.promise(async () => {
|
const diffOutput = yield* gitText([
|
||||||
const diffOutput = (
|
"-c",
|
||||||
await Git.run(["-c", "core.fsmonitor=false", "-c", "core.quotepath=false", "diff", "--numstat", "HEAD"], {
|
"core.fsmonitor=false",
|
||||||
cwd: Instance.directory,
|
"-c",
|
||||||
})
|
"core.quotepath=false",
|
||||||
).text()
|
"diff",
|
||||||
|
"--numstat",
|
||||||
|
"HEAD",
|
||||||
|
])
|
||||||
|
|
||||||
const changed: File.Info[] = []
|
const changed: File.Info[] = []
|
||||||
|
|
||||||
@@ -438,9 +445,7 @@ export namespace File {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const untrackedOutput = (
|
const untrackedOutput = yield* gitText([
|
||||||
await Git.run(
|
|
||||||
[
|
|
||||||
"-c",
|
"-c",
|
||||||
"core.fsmonitor=false",
|
"core.fsmonitor=false",
|
||||||
"-c",
|
"-c",
|
||||||
@@ -448,32 +453,24 @@ export namespace File {
|
|||||||
"ls-files",
|
"ls-files",
|
||||||
"--others",
|
"--others",
|
||||||
"--exclude-standard",
|
"--exclude-standard",
|
||||||
],
|
])
|
||||||
{
|
|
||||||
cwd: Instance.directory,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
).text()
|
|
||||||
|
|
||||||
if (untrackedOutput.trim()) {
|
if (untrackedOutput.trim()) {
|
||||||
for (const file of untrackedOutput.trim().split("\n")) {
|
for (const file of untrackedOutput.trim().split("\n")) {
|
||||||
try {
|
const content = yield* appFs
|
||||||
const content = await Filesystem.readText(path.join(Instance.directory, file))
|
.readFileString(path.join(Instance.directory, file))
|
||||||
|
.pipe(Effect.catch(() => Effect.succeed<string | undefined>(undefined)))
|
||||||
|
if (content === undefined) continue
|
||||||
changed.push({
|
changed.push({
|
||||||
path: file,
|
path: file,
|
||||||
added: content.split("\n").length,
|
added: content.split("\n").length,
|
||||||
removed: 0,
|
removed: 0,
|
||||||
status: "added",
|
status: "added",
|
||||||
})
|
})
|
||||||
} catch {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deletedOutput = (
|
const deletedOutput = yield* gitText([
|
||||||
await Git.run(
|
|
||||||
[
|
|
||||||
"-c",
|
"-c",
|
||||||
"core.fsmonitor=false",
|
"core.fsmonitor=false",
|
||||||
"-c",
|
"-c",
|
||||||
@@ -482,12 +479,7 @@ export namespace File {
|
|||||||
"--name-only",
|
"--name-only",
|
||||||
"--diff-filter=D",
|
"--diff-filter=D",
|
||||||
"HEAD",
|
"HEAD",
|
||||||
],
|
])
|
||||||
{
|
|
||||||
cwd: Instance.directory,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
).text()
|
|
||||||
|
|
||||||
if (deletedOutput.trim()) {
|
if (deletedOutput.trim()) {
|
||||||
for (const file of deletedOutput.trim().split("\n")) {
|
for (const file of deletedOutput.trim().split("\n")) {
|
||||||
@@ -508,9 +500,8 @@ export namespace File {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
const read = Effect.fn("File.read")(function* (file: string) {
|
const read: Interface["read"] = Effect.fn("File.read")(function* (file: string) {
|
||||||
using _ = log.time("read", { file })
|
using _ = log.time("read", { file })
|
||||||
const full = path.join(Instance.directory, file)
|
const full = path.join(Instance.directory, file)
|
||||||
|
|
||||||
@@ -558,27 +549,19 @@ export namespace File {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (Instance.project.vcs === "git") {
|
if (Instance.project.vcs === "git") {
|
||||||
return yield* Effect.promise(async (): Promise<File.Content> => {
|
let diff = yield* gitText(["-c", "core.fsmonitor=false", "diff", "--", file])
|
||||||
let diff = (
|
|
||||||
await Git.run(["-c", "core.fsmonitor=false", "diff", "--", file], { cwd: Instance.directory })
|
|
||||||
).text()
|
|
||||||
if (!diff.trim()) {
|
if (!diff.trim()) {
|
||||||
diff = (
|
diff = yield* gitText(["-c", "core.fsmonitor=false", "diff", "--staged", "--", file])
|
||||||
await Git.run(["-c", "core.fsmonitor=false", "diff", "--staged", "--", file], {
|
|
||||||
cwd: Instance.directory,
|
|
||||||
})
|
|
||||||
).text()
|
|
||||||
}
|
}
|
||||||
if (diff.trim()) {
|
if (diff.trim()) {
|
||||||
const original = (await Git.run(["show", `HEAD:${file}`], { cwd: Instance.directory })).text()
|
const original = yield* git.show(Instance.directory, "HEAD", file)
|
||||||
const patch = structuredPatch(file, file, original, content, "old", "new", {
|
const patch = structuredPatch(file, file, original, content, "old", "new", {
|
||||||
context: Infinity,
|
context: Infinity,
|
||||||
ignoreWhitespace: true,
|
ignoreWhitespace: true,
|
||||||
})
|
})
|
||||||
return { type: "text", content, patch, diff: formatPatch(patch) }
|
return { type: "text" as const, content, patch, diff: formatPatch(patch) }
|
||||||
}
|
}
|
||||||
return { type: "text", content }
|
return { type: "text" as const, content }
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { type: "text" as const, content }
|
return { type: "text" as const, content }
|
||||||
@@ -660,7 +643,7 @@ export namespace File {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
|
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Git.defaultLayer))
|
||||||
|
|
||||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export namespace FileTime {
|
|||||||
const disableCheck = yield* Flag.OPENCODE_DISABLE_FILETIME_CHECK
|
const disableCheck = yield* Flag.OPENCODE_DISABLE_FILETIME_CHECK
|
||||||
|
|
||||||
const stamp = Effect.fnUntraced(function* (file: string) {
|
const stamp = Effect.fnUntraced(function* (file: string) {
|
||||||
const info = yield* fsys.stat(file).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
const info = yield* fsys.stat(file).pipe(Effect.catch(() => Effect.void))
|
||||||
return {
|
return {
|
||||||
read: yield* DateTime.nowAsDate,
|
read: yield* DateTime.nowAsDate,
|
||||||
mtime: info ? Option.getOrUndefined(info.mtime)?.getTime() : undefined,
|
mtime: info ? Option.getOrUndefined(info.mtime)?.getTime() : undefined,
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export namespace FileWatcher {
|
|||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
|
const git = yield* Git.Service
|
||||||
|
|
||||||
const state = yield* InstanceState.make(
|
const state = yield* InstanceState.make(
|
||||||
Effect.fn("FileWatcher.state")(
|
Effect.fn("FileWatcher.state")(
|
||||||
@@ -131,11 +132,9 @@ export namespace FileWatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Instance.project.vcs === "git") {
|
if (Instance.project.vcs === "git") {
|
||||||
const result = yield* Effect.promise(() =>
|
const result = yield* git.run(["rev-parse", "--git-dir"], {
|
||||||
Git.run(["rev-parse", "--git-dir"], {
|
|
||||||
cwd: Instance.project.worktree,
|
cwd: Instance.project.worktree,
|
||||||
}),
|
})
|
||||||
)
|
|
||||||
const vcsDir =
|
const vcsDir =
|
||||||
result.exitCode === 0 ? path.resolve(Instance.project.worktree, result.text().trim()) : undefined
|
result.exitCode === 0 ? path.resolve(Instance.project.worktree, result.text().trim()) : undefined
|
||||||
if (vcsDir && !cfgIgnores.includes(".git") && !cfgIgnores.includes(vcsDir)) {
|
if (vcsDir && !cfgIgnores.includes(".git") && !cfgIgnores.includes(vcsDir)) {
|
||||||
@@ -161,7 +160,7 @@ export namespace FileWatcher {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer))
|
export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer), Layer.provide(Git.defaultLayer))
|
||||||
|
|
||||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
||||||
|
|||||||
@@ -265,39 +265,7 @@ export namespace Git {
|
|||||||
return runPromise((git) => git.run(args, opts))
|
return runPromise((git) => git.run(args, opts))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function branch(cwd: string) {
|
|
||||||
return runPromise((git) => git.branch(cwd))
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function prefix(cwd: string) {
|
|
||||||
return runPromise((git) => git.prefix(cwd))
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function defaultBranch(cwd: string) {
|
export async function defaultBranch(cwd: string) {
|
||||||
return runPromise((git) => git.defaultBranch(cwd))
|
return runPromise((git) => git.defaultBranch(cwd))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function hasHead(cwd: string) {
|
|
||||||
return runPromise((git) => git.hasHead(cwd))
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function mergeBase(cwd: string, base: string, head?: string) {
|
|
||||||
return runPromise((git) => git.mergeBase(cwd, base, head))
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function show(cwd: string, ref: string, file: string, prefix?: string) {
|
|
||||||
return runPromise((git) => git.show(cwd, ref, file, prefix))
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function status(cwd: string) {
|
|
||||||
return runPromise((git) => git.status(cwd))
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function diff(cwd: string, ref: string) {
|
|
||||||
return runPromise((git) => git.diff(cwd, ref))
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function stats(cwd: string, ref: string) {
|
|
||||||
return runPromise((git) => git.stats(cwd, ref))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import { Database } from "./storage/db"
|
|||||||
import { errorMessage } from "./util/error"
|
import { errorMessage } from "./util/error"
|
||||||
import { PluginCommand } from "./cli/cmd/plug"
|
import { PluginCommand } from "./cli/cmd/plug"
|
||||||
import { Heap } from "./cli/heap"
|
import { Heap } from "./cli/heap"
|
||||||
|
import { drizzle } from "drizzle-orm/bun-sqlite"
|
||||||
|
|
||||||
process.on("unhandledRejection", (e) => {
|
process.on("unhandledRejection", (e) => {
|
||||||
Log.Default.error("rejection", {
|
Log.Default.error("rejection", {
|
||||||
@@ -119,7 +120,7 @@ const cli = yargs(args)
|
|||||||
let last = -1
|
let last = -1
|
||||||
if (tty) process.stderr.write("\x1b[?25l")
|
if (tty) process.stderr.write("\x1b[?25l")
|
||||||
try {
|
try {
|
||||||
await JsonMigration.run(Database.Client().$client, {
|
await JsonMigration.run(drizzle({ client: Database.Client().$client }), {
|
||||||
progress: (event) => {
|
progress: (event) => {
|
||||||
const percent = Math.floor((event.current / event.total) * 100)
|
const percent = Math.floor((event.current / event.total) * 100)
|
||||||
if (percent === last && event.current !== event.total) return
|
if (percent === last && event.current !== event.total) return
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ export namespace LSP {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!handle) return undefined
|
if (!handle) return undefined
|
||||||
log.info("spawned lsp server", { serverID: server.id })
|
log.info("spawned lsp server", { serverID: server.id, root })
|
||||||
|
|
||||||
const client = await LSPClient.create({
|
const client = await LSPClient.create({
|
||||||
serverID: server.id,
|
serverID: server.id,
|
||||||
|
|||||||
@@ -105,17 +105,7 @@ export namespace LSPServer {
|
|||||||
if (!tsserver) return
|
if (!tsserver) return
|
||||||
const bin = await Npm.which("typescript-language-server")
|
const bin = await Npm.which("typescript-language-server")
|
||||||
if (!bin) return
|
if (!bin) return
|
||||||
|
const proc = spawn(bin, ["--stdio"], {
|
||||||
const args = ["--stdio", "--tsserver-log-verbosity", "off", "--tsserver-path", tsserver]
|
|
||||||
|
|
||||||
if (
|
|
||||||
!(await pathExists(path.join(root, "tsconfig.json"))) &&
|
|
||||||
!(await pathExists(path.join(root, "jsconfig.json")))
|
|
||||||
) {
|
|
||||||
args.push("--ignore-node-modules")
|
|
||||||
}
|
|
||||||
|
|
||||||
const proc = spawn(bin, args, {
|
|
||||||
cwd: root,
|
cwd: root,
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
@@ -867,7 +857,7 @@ export namespace LSPServer {
|
|||||||
|
|
||||||
export const Clangd: Info = {
|
export const Clangd: Info = {
|
||||||
id: "clangd",
|
id: "clangd",
|
||||||
root: NearestRoot(["compile_commands.json", "compile_flags.txt", ".clangd", "CMakeLists.txt", "Makefile"]),
|
root: NearestRoot(["compile_commands.json", "compile_flags.txt", ".clangd"]),
|
||||||
extensions: [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"],
|
extensions: [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"],
|
||||||
async spawn(root) {
|
async spawn(root) {
|
||||||
const args = ["--background-index", "--clang-tidy"]
|
const args = ["--background-index", "--clang-tidy"]
|
||||||
|
|||||||
@@ -286,6 +286,7 @@ export namespace MCP {
|
|||||||
clientId: oauthConfig?.clientId,
|
clientId: oauthConfig?.clientId,
|
||||||
clientSecret: oauthConfig?.clientSecret,
|
clientSecret: oauthConfig?.clientSecret,
|
||||||
scope: oauthConfig?.scope,
|
scope: oauthConfig?.scope,
|
||||||
|
redirectUri: oauthConfig?.redirectUri,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onRedirect: async (url) => {
|
onRedirect: async (url) => {
|
||||||
@@ -501,7 +502,7 @@ export namespace MCP {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = yield* create(key, mcp).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
const result = yield* create(key, mcp).pipe(Effect.catch(() => Effect.void))
|
||||||
if (!result) return
|
if (!result) return
|
||||||
|
|
||||||
s.status[key] = result.status
|
s.status[key] = result.status
|
||||||
@@ -716,13 +717,16 @@ export namespace MCP {
|
|||||||
if (mcpConfig.type !== "remote") throw new Error(`MCP server ${mcpName} is not a remote server`)
|
if (mcpConfig.type !== "remote") throw new Error(`MCP server ${mcpName} is not a remote server`)
|
||||||
if (mcpConfig.oauth === false) throw new Error(`MCP server ${mcpName} has OAuth explicitly disabled`)
|
if (mcpConfig.oauth === false) throw new Error(`MCP server ${mcpName} has OAuth explicitly disabled`)
|
||||||
|
|
||||||
yield* Effect.promise(() => McpOAuthCallback.ensureRunning())
|
// OAuth config is optional - if not provided, we'll use auto-discovery
|
||||||
|
const oauthConfig = typeof mcpConfig.oauth === "object" ? mcpConfig.oauth : undefined
|
||||||
|
|
||||||
|
// Start the callback server with custom redirectUri if configured
|
||||||
|
yield* Effect.promise(() => McpOAuthCallback.ensureRunning(oauthConfig?.redirectUri))
|
||||||
|
|
||||||
const oauthState = Array.from(crypto.getRandomValues(new Uint8Array(32)))
|
const oauthState = Array.from(crypto.getRandomValues(new Uint8Array(32)))
|
||||||
.map((b) => b.toString(16).padStart(2, "0"))
|
.map((b) => b.toString(16).padStart(2, "0"))
|
||||||
.join("")
|
.join("")
|
||||||
yield* auth.updateOAuthState(mcpName, oauthState)
|
yield* auth.updateOAuthState(mcpName, oauthState)
|
||||||
const oauthConfig = typeof mcpConfig.oauth === "object" ? mcpConfig.oauth : undefined
|
|
||||||
let capturedUrl: URL | undefined
|
let capturedUrl: URL | undefined
|
||||||
const authProvider = new McpOAuthProvider(
|
const authProvider = new McpOAuthProvider(
|
||||||
mcpName,
|
mcpName,
|
||||||
@@ -731,6 +735,7 @@ export namespace MCP {
|
|||||||
clientId: oauthConfig?.clientId,
|
clientId: oauthConfig?.clientId,
|
||||||
clientSecret: oauthConfig?.clientSecret,
|
clientSecret: oauthConfig?.clientSecret,
|
||||||
scope: oauthConfig?.scope,
|
scope: oauthConfig?.scope,
|
||||||
|
redirectUri: oauthConfig?.redirectUri,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onRedirect: async (url) => {
|
onRedirect: async (url) => {
|
||||||
@@ -901,9 +906,6 @@ export namespace MCP {
|
|||||||
|
|
||||||
export const disconnect = async (name: string) => runPromise((svc) => svc.disconnect(name))
|
export const disconnect = async (name: string) => runPromise((svc) => svc.disconnect(name))
|
||||||
|
|
||||||
export const getPrompt = async (clientName: string, name: string, args?: Record<string, string>) =>
|
|
||||||
runPromise((svc) => svc.getPrompt(clientName, name, args))
|
|
||||||
|
|
||||||
export const startAuth = async (mcpName: string) => runPromise((svc) => svc.startAuth(mcpName))
|
export const startAuth = async (mcpName: string) => runPromise((svc) => svc.startAuth(mcpName))
|
||||||
|
|
||||||
export const authenticate = async (mcpName: string) => runPromise((svc) => svc.authenticate(mcpName))
|
export const authenticate = async (mcpName: string) => runPromise((svc) => svc.authenticate(mcpName))
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { createConnection } from "net"
|
import { createConnection } from "net"
|
||||||
import { createServer } from "http"
|
import { createServer } from "http"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH } from "./oauth-provider"
|
import { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH, parseRedirectUri } from "./oauth-provider"
|
||||||
|
|
||||||
const log = Log.create({ service: "mcp.oauth-callback" })
|
const log = Log.create({ service: "mcp.oauth-callback" })
|
||||||
|
|
||||||
|
// Current callback server configuration (may differ from defaults if custom redirectUri is used)
|
||||||
|
let currentPort = OAUTH_CALLBACK_PORT
|
||||||
|
let currentPath = OAUTH_CALLBACK_PATH
|
||||||
|
|
||||||
const HTML_SUCCESS = `<!DOCTYPE html>
|
const HTML_SUCCESS = `<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
@@ -71,9 +75,9 @@ export namespace McpOAuthCallback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleRequest(req: import("http").IncomingMessage, res: import("http").ServerResponse) {
|
function handleRequest(req: import("http").IncomingMessage, res: import("http").ServerResponse) {
|
||||||
const url = new URL(req.url || "/", `http://localhost:${OAUTH_CALLBACK_PORT}`)
|
const url = new URL(req.url || "/", `http://localhost:${currentPort}`)
|
||||||
|
|
||||||
if (url.pathname !== OAUTH_CALLBACK_PATH) {
|
if (url.pathname !== currentPath) {
|
||||||
res.writeHead(404)
|
res.writeHead(404)
|
||||||
res.end("Not found")
|
res.end("Not found")
|
||||||
return
|
return
|
||||||
@@ -135,19 +139,31 @@ export namespace McpOAuthCallback {
|
|||||||
res.end(HTML_SUCCESS)
|
res.end(HTML_SUCCESS)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ensureRunning(): Promise<void> {
|
export async function ensureRunning(redirectUri?: string): Promise<void> {
|
||||||
|
// Parse the redirect URI to get port and path (uses defaults if not provided)
|
||||||
|
const { port, path } = parseRedirectUri(redirectUri)
|
||||||
|
|
||||||
|
// If server is running on a different port/path, stop it first
|
||||||
|
if (server && (currentPort !== port || currentPath !== path)) {
|
||||||
|
log.info("stopping oauth callback server to reconfigure", { oldPort: currentPort, newPort: port })
|
||||||
|
await stop()
|
||||||
|
}
|
||||||
|
|
||||||
if (server) return
|
if (server) return
|
||||||
|
|
||||||
const running = await isPortInUse()
|
const running = await isPortInUse(port)
|
||||||
if (running) {
|
if (running) {
|
||||||
log.info("oauth callback server already running on another instance", { port: OAUTH_CALLBACK_PORT })
|
log.info("oauth callback server already running on another instance", { port })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentPort = port
|
||||||
|
currentPath = path
|
||||||
|
|
||||||
server = createServer(handleRequest)
|
server = createServer(handleRequest)
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
server!.listen(OAUTH_CALLBACK_PORT, () => {
|
server!.listen(currentPort, () => {
|
||||||
log.info("oauth callback server started", { port: OAUTH_CALLBACK_PORT })
|
log.info("oauth callback server started", { port: currentPort, path: currentPath })
|
||||||
resolve()
|
resolve()
|
||||||
})
|
})
|
||||||
server!.on("error", reject)
|
server!.on("error", reject)
|
||||||
@@ -182,9 +198,9 @@ export namespace McpOAuthCallback {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function isPortInUse(): Promise<boolean> {
|
export async function isPortInUse(port: number = OAUTH_CALLBACK_PORT): Promise<boolean> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const socket = createConnection(OAUTH_CALLBACK_PORT, "127.0.0.1")
|
const socket = createConnection(port, "127.0.0.1")
|
||||||
socket.on("connect", () => {
|
socket.on("connect", () => {
|
||||||
socket.destroy()
|
socket.destroy()
|
||||||
resolve(true)
|
resolve(true)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export interface McpOAuthConfig {
|
|||||||
clientId?: string
|
clientId?: string
|
||||||
clientSecret?: string
|
clientSecret?: string
|
||||||
scope?: string
|
scope?: string
|
||||||
|
redirectUri?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface McpOAuthCallbacks {
|
export interface McpOAuthCallbacks {
|
||||||
@@ -32,6 +33,9 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
get redirectUrl(): string {
|
get redirectUrl(): string {
|
||||||
|
if (this.config.redirectUri) {
|
||||||
|
return this.config.redirectUri
|
||||||
|
}
|
||||||
return `http://127.0.0.1:${OAUTH_CALLBACK_PORT}${OAUTH_CALLBACK_PATH}`
|
return `http://127.0.0.1:${OAUTH_CALLBACK_PORT}${OAUTH_CALLBACK_PATH}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,3 +187,22 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH }
|
export { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a redirect URI to extract port and path for the callback server.
|
||||||
|
* Returns defaults if the URI can't be parsed.
|
||||||
|
*/
|
||||||
|
export function parseRedirectUri(redirectUri?: string): { port: number; path: string } {
|
||||||
|
if (!redirectUri) {
|
||||||
|
return { port: OAUTH_CALLBACK_PORT, path: OAUTH_CALLBACK_PATH }
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = new URL(redirectUri)
|
||||||
|
const port = url.port ? parseInt(url.port, 10) : url.protocol === "https:" ? 443 : 80
|
||||||
|
const path = url.pathname || OAUTH_CALLBACK_PATH
|
||||||
|
return { port, path }
|
||||||
|
} catch {
|
||||||
|
return { port: OAUTH_CALLBACK_PORT, path: OAUTH_CALLBACK_PATH }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +1,6 @@
|
|||||||
|
export { Config } from "./config/config"
|
||||||
export { Server } from "./server/server"
|
export { Server } from "./server/server"
|
||||||
|
export { bootstrap } from "./cli/bootstrap"
|
||||||
|
export { Log } from "./util/log"
|
||||||
|
export { Database } from "./storage/db"
|
||||||
|
export { JsonMigration } from "./storage/json-migration"
|
||||||
|
|||||||
@@ -376,9 +376,9 @@ export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> {
|
|||||||
"gpt-5.4",
|
"gpt-5.4",
|
||||||
"gpt-5.4-mini",
|
"gpt-5.4-mini",
|
||||||
])
|
])
|
||||||
for (const modelId of Object.keys(provider.models)) {
|
for (const [modelId, model] of Object.entries(provider.models)) {
|
||||||
if (modelId.includes("codex")) continue
|
if (modelId.includes("codex")) continue
|
||||||
if (allowedModels.has(modelId)) continue
|
if (allowedModels.has(model.api.id)) continue
|
||||||
delete provider.models[modelId]
|
delete provider.models[modelId]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export namespace Plugin {
|
|||||||
Authorization: `Basic ${Buffer.from(`${Flag.OPENCODE_SERVER_USERNAME ?? "opencode"}:${Flag.OPENCODE_SERVER_PASSWORD}`).toString("base64")}`,
|
Authorization: `Basic ${Buffer.from(`${Flag.OPENCODE_SERVER_USERNAME ?? "opencode"}:${Flag.OPENCODE_SERVER_PASSWORD}`).toString("base64")}`,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
fetch: async (...args) => Server.Default().fetch(...args),
|
fetch: async (...args) => Server.Default().app.fetch(...args),
|
||||||
})
|
})
|
||||||
const cfg = yield* config.get()
|
const cfg = yield* config.get()
|
||||||
const input: PluginInput = {
|
const input: PluginInput = {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { iife } from "@/util/iife"
|
|||||||
import { Log } from "@/util/log"
|
import { Log } from "@/util/log"
|
||||||
import { Context } from "../util/context"
|
import { Context } from "../util/context"
|
||||||
import { Project } from "./project"
|
import { Project } from "./project"
|
||||||
|
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
||||||
import { State } from "./state"
|
import { State } from "./state"
|
||||||
|
|
||||||
export interface InstanceContext {
|
export interface InstanceContext {
|
||||||
@@ -20,19 +21,9 @@ const disposal = {
|
|||||||
all: undefined as Promise<void> | undefined,
|
all: undefined as Promise<void> | undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
function emit(directory: string) {
|
function emitDisposed(directory: string) {}
|
||||||
GlobalBus.emit("event", {
|
|
||||||
directory,
|
|
||||||
payload: {
|
|
||||||
type: "server.instance.disposed",
|
|
||||||
properties: {
|
|
||||||
directory,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function boot(input: { directory: string; init?: () => Promise<any>; project?: Project.Info; worktree?: string }) {
|
function boot(input: { directory: string; init?: () => Promise<any>; worktree?: string; project?: Project.Info }) {
|
||||||
return iife(async () => {
|
return iife(async () => {
|
||||||
const ctx =
|
const ctx =
|
||||||
input.project && input.worktree
|
input.project && input.worktree
|
||||||
@@ -93,6 +84,7 @@ export const Instance = {
|
|||||||
get project() {
|
get project() {
|
||||||
return context.use().project
|
return context.use().project
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a path is within the project boundary.
|
* Check if a path is within the project boundary.
|
||||||
* Returns true if path is inside Instance.directory OR Instance.worktree.
|
* Returns true if path is inside Instance.directory OR Instance.worktree.
|
||||||
@@ -131,15 +123,39 @@ export const Instance = {
|
|||||||
await Promise.all([State.dispose(directory), disposeInstance(directory)])
|
await Promise.all([State.dispose(directory), disposeInstance(directory)])
|
||||||
cache.delete(directory)
|
cache.delete(directory)
|
||||||
const next = track(directory, boot({ ...input, directory }))
|
const next = track(directory, boot({ ...input, directory }))
|
||||||
emit(directory)
|
|
||||||
|
GlobalBus.emit("event", {
|
||||||
|
directory,
|
||||||
|
project: input.project?.id,
|
||||||
|
workspace: WorkspaceContext.workspaceID,
|
||||||
|
payload: {
|
||||||
|
type: "server.instance.disposed",
|
||||||
|
properties: {
|
||||||
|
directory,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
return await next
|
return await next
|
||||||
},
|
},
|
||||||
async dispose() {
|
async dispose() {
|
||||||
const directory = Instance.directory
|
const directory = Instance.directory
|
||||||
|
const project = Instance.project
|
||||||
Log.Default.info("disposing instance", { directory })
|
Log.Default.info("disposing instance", { directory })
|
||||||
await Promise.all([State.dispose(directory), disposeInstance(directory)])
|
await Promise.all([State.dispose(directory), disposeInstance(directory)])
|
||||||
cache.delete(directory)
|
cache.delete(directory)
|
||||||
emit(directory)
|
|
||||||
|
GlobalBus.emit("event", {
|
||||||
|
directory,
|
||||||
|
project: project.id,
|
||||||
|
workspace: WorkspaceContext.workspaceID,
|
||||||
|
payload: {
|
||||||
|
type: "server.instance.disposed",
|
||||||
|
properties: {
|
||||||
|
directory,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
},
|
},
|
||||||
async disposeAll() {
|
async disposeAll() {
|
||||||
if (disposal.all) return disposal.all
|
if (disposal.all) return disposal.all
|
||||||
|
|||||||
@@ -137,6 +137,8 @@ export namespace Project {
|
|||||||
const emitUpdated = (data: Info) =>
|
const emitUpdated = (data: Info) =>
|
||||||
Effect.sync(() =>
|
Effect.sync(() =>
|
||||||
GlobalBus.emit("event", {
|
GlobalBus.emit("event", {
|
||||||
|
directory: "global",
|
||||||
|
project: data.id,
|
||||||
payload: { type: Event.Updated.type, properties: data },
|
payload: { type: Event.Updated.type, properties: data },
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -158,7 +160,7 @@ export namespace Project {
|
|||||||
return yield* fs.readFileString(pathSvc.join(dir, "opencode")).pipe(
|
return yield* fs.readFileString(pathSvc.join(dir, "opencode")).pipe(
|
||||||
Effect.map((x) => x.trim()),
|
Effect.map((x) => x.trim()),
|
||||||
Effect.map(ProjectID.make),
|
Effect.map(ProjectID.make),
|
||||||
Effect.catch(() => Effect.succeed(undefined)),
|
Effect.catch(() => Effect.void),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -161,8 +161,7 @@ export namespace Vcs {
|
|||||||
const bus = yield* Bus.Service
|
const bus = yield* Bus.Service
|
||||||
|
|
||||||
const state = yield* InstanceState.make<State>(
|
const state = yield* InstanceState.make<State>(
|
||||||
Effect.fn("Vcs.state")((ctx) =>
|
Effect.fn("Vcs.state")(function* (ctx) {
|
||||||
Effect.gen(function* () {
|
|
||||||
if (ctx.project.vcs !== "git") {
|
if (ctx.project.vcs !== "git") {
|
||||||
return { current: undefined, root: undefined }
|
return { current: undefined, root: undefined }
|
||||||
}
|
}
|
||||||
@@ -193,7 +192,6 @@ export namespace Vcs {
|
|||||||
|
|
||||||
return value
|
return value
|
||||||
}),
|
}),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return Service.of({
|
return Service.of({
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// Auto-generated by build.ts - do not edit
|
||||||
|
export declare const snapshot: Record<string, unknown>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,27 @@ export namespace ModelsDev {
|
|||||||
)
|
)
|
||||||
const ttl = 5 * 60 * 1000
|
const ttl = 5 * 60 * 1000
|
||||||
|
|
||||||
|
type JsonValue = string | number | boolean | null | { [key: string]: JsonValue } | JsonValue[]
|
||||||
|
|
||||||
|
const JsonValue: z.ZodType<JsonValue> = z.lazy(() =>
|
||||||
|
z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(JsonValue), z.record(z.string(), JsonValue)]),
|
||||||
|
)
|
||||||
|
|
||||||
|
const Cost = z.object({
|
||||||
|
input: z.number(),
|
||||||
|
output: z.number(),
|
||||||
|
cache_read: z.number().optional(),
|
||||||
|
cache_write: z.number().optional(),
|
||||||
|
context_over_200k: z
|
||||||
|
.object({
|
||||||
|
input: z.number(),
|
||||||
|
output: z.number(),
|
||||||
|
cache_read: z.number().optional(),
|
||||||
|
cache_write: z.number().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
|
||||||
export const Model = z.object({
|
export const Model = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
@@ -41,22 +62,7 @@ export namespace ModelsDev {
|
|||||||
.strict(),
|
.strict(),
|
||||||
])
|
])
|
||||||
.optional(),
|
.optional(),
|
||||||
cost: z
|
cost: Cost.optional(),
|
||||||
.object({
|
|
||||||
input: z.number(),
|
|
||||||
output: z.number(),
|
|
||||||
cache_read: z.number().optional(),
|
|
||||||
cache_write: z.number().optional(),
|
|
||||||
context_over_200k: z
|
|
||||||
.object({
|
|
||||||
input: z.number(),
|
|
||||||
output: z.number(),
|
|
||||||
cache_read: z.number().optional(),
|
|
||||||
cache_write: z.number().optional(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
limit: z.object({
|
limit: z.object({
|
||||||
context: z.number(),
|
context: z.number(),
|
||||||
input: z.number().optional(),
|
input: z.number().optional(),
|
||||||
@@ -68,12 +74,26 @@ export namespace ModelsDev {
|
|||||||
output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
|
output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
experimental: z.boolean().optional(),
|
experimental: z
|
||||||
status: z.enum(["alpha", "beta", "deprecated"]).optional(),
|
.object({
|
||||||
options: z.record(z.string(), z.any()),
|
modes: z
|
||||||
|
.record(
|
||||||
|
z.string(),
|
||||||
|
z.object({
|
||||||
|
cost: Cost.optional(),
|
||||||
|
provider: z
|
||||||
|
.object({
|
||||||
|
body: z.record(z.string(), JsonValue).optional(),
|
||||||
headers: z.record(z.string(), z.string()).optional(),
|
headers: z.record(z.string(), z.string()).optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
status: z.enum(["alpha", "beta", "deprecated"]).optional(),
|
||||||
provider: z.object({ npm: z.string().optional(), api: z.string().optional() }).optional(),
|
provider: z.object({ npm: z.string().optional(), api: z.string().optional() }).optional(),
|
||||||
variants: z.record(z.string(), z.record(z.string(), z.any())).optional(),
|
|
||||||
})
|
})
|
||||||
export type Model = z.infer<typeof Model>
|
export type Model = z.infer<typeof Model>
|
||||||
|
|
||||||
|
|||||||
@@ -574,6 +574,7 @@ export namespace Provider {
|
|||||||
const sdkModelID = isWorkflowModel(modelID) ? modelID : "duo-workflow"
|
const sdkModelID = isWorkflowModel(modelID) ? modelID : "duo-workflow"
|
||||||
const model = sdk.workflowChat(sdkModelID, {
|
const model = sdk.workflowChat(sdkModelID, {
|
||||||
featureFlags,
|
featureFlags,
|
||||||
|
workflowDefinition: options?.workflowDefinition as string | undefined,
|
||||||
})
|
})
|
||||||
if (workflowRef) {
|
if (workflowRef) {
|
||||||
model.selectedModelRef = workflowRef
|
model.selectedModelRef = workflowRef
|
||||||
@@ -925,6 +926,28 @@ export namespace Provider {
|
|||||||
|
|
||||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Provider") {}
|
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Provider") {}
|
||||||
|
|
||||||
|
function cost(c: ModelsDev.Model["cost"]): Model["cost"] {
|
||||||
|
const result: Model["cost"] = {
|
||||||
|
input: c?.input ?? 0,
|
||||||
|
output: c?.output ?? 0,
|
||||||
|
cache: {
|
||||||
|
read: c?.cache_read ?? 0,
|
||||||
|
write: c?.cache_write ?? 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if (c?.context_over_200k) {
|
||||||
|
result.experimentalOver200K = {
|
||||||
|
cache: {
|
||||||
|
read: c.context_over_200k.cache_read ?? 0,
|
||||||
|
write: c.context_over_200k.cache_write ?? 0,
|
||||||
|
},
|
||||||
|
input: c.context_over_200k.input,
|
||||||
|
output: c.context_over_200k.output,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
|
function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
|
||||||
const m: Model = {
|
const m: Model = {
|
||||||
id: ModelID.make(model.id),
|
id: ModelID.make(model.id),
|
||||||
@@ -937,26 +960,9 @@ export namespace Provider {
|
|||||||
npm: model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible",
|
npm: model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible",
|
||||||
},
|
},
|
||||||
status: model.status ?? "active",
|
status: model.status ?? "active",
|
||||||
headers: model.headers ?? {},
|
headers: {},
|
||||||
options: model.options ?? {},
|
options: {},
|
||||||
cost: {
|
cost: cost(model.cost),
|
||||||
input: model.cost?.input ?? 0,
|
|
||||||
output: model.cost?.output ?? 0,
|
|
||||||
cache: {
|
|
||||||
read: model.cost?.cache_read ?? 0,
|
|
||||||
write: model.cost?.cache_write ?? 0,
|
|
||||||
},
|
|
||||||
experimentalOver200K: model.cost?.context_over_200k
|
|
||||||
? {
|
|
||||||
cache: {
|
|
||||||
read: model.cost.context_over_200k.cache_read ?? 0,
|
|
||||||
write: model.cost.context_over_200k.cache_write ?? 0,
|
|
||||||
},
|
|
||||||
input: model.cost.context_over_200k.input,
|
|
||||||
output: model.cost.context_over_200k.output,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
},
|
|
||||||
limit: {
|
limit: {
|
||||||
context: model.limit.context,
|
context: model.limit.context,
|
||||||
input: model.limit.input,
|
input: model.limit.input,
|
||||||
@@ -993,13 +999,31 @@ export namespace Provider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
|
export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
|
||||||
|
const models: Record<string, Model> = {}
|
||||||
|
for (const [key, model] of Object.entries(provider.models)) {
|
||||||
|
models[key] = fromModelsDevModel(provider, model)
|
||||||
|
for (const [mode, opts] of Object.entries(model.experimental?.modes ?? {})) {
|
||||||
|
const id = `${model.id}-${mode}`
|
||||||
|
const m = fromModelsDevModel(provider, model)
|
||||||
|
m.id = ModelID.make(id)
|
||||||
|
m.name = `${model.name} ${mode[0].toUpperCase()}${mode.slice(1)}`
|
||||||
|
if (opts.cost) m.cost = mergeDeep(m.cost, cost(opts.cost))
|
||||||
|
// convert body params to camelCase for ai sdk compatibility
|
||||||
|
if (opts.provider?.body)
|
||||||
|
m.options = Object.fromEntries(
|
||||||
|
Object.entries(opts.provider.body).map(([k, v]) => [k.replace(/_([a-z])/g, (_, c) => c.toUpperCase()), v]),
|
||||||
|
)
|
||||||
|
if (opts.provider?.headers) m.headers = opts.provider.headers
|
||||||
|
models[id] = m
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
id: ProviderID.make(provider.id),
|
id: ProviderID.make(provider.id),
|
||||||
source: "custom",
|
source: "custom",
|
||||||
name: provider.name,
|
name: provider.name,
|
||||||
env: provider.env ?? [],
|
env: provider.env ?? [],
|
||||||
options: {},
|
options: {},
|
||||||
models: mapValues(provider.models, (model) => fromModelsDevModel(provider, model)),
|
models,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -376,7 +376,8 @@ export namespace ProviderTransform {
|
|||||||
id.includes("mistral") ||
|
id.includes("mistral") ||
|
||||||
id.includes("kimi") ||
|
id.includes("kimi") ||
|
||||||
id.includes("k2p5") ||
|
id.includes("k2p5") ||
|
||||||
id.includes("qwen")
|
id.includes("qwen") ||
|
||||||
|
id.includes("big-pickle")
|
||||||
)
|
)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|||||||
@@ -371,10 +371,6 @@ export namespace Pty {
|
|||||||
return runPromise((svc) => svc.get(id))
|
return runPromise((svc) => svc.get(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resize(id: PtyID, cols: number, rows: number) {
|
|
||||||
return runPromise((svc) => svc.resize(id, cols, rows))
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function write(id: PtyID, data: string) {
|
export async function write(id: PtyID, data: string) {
|
||||||
return runPromise((svc) => svc.write(id, data))
|
return runPromise((svc) => svc.write(id, data))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { proxy } from "hono/proxy"
|
|||||||
import type { UpgradeWebSocket } from "hono/ws"
|
import type { UpgradeWebSocket } from "hono/ws"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { createHash } from "node:crypto"
|
import { createHash } from "node:crypto"
|
||||||
|
import * as fs from "node:fs/promises"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { Format } from "../format"
|
import { Format } from "../format"
|
||||||
import { TuiRoutes } from "./routes/tui"
|
import { TuiRoutes } from "./routes/tui"
|
||||||
@@ -28,6 +29,7 @@ import { ExperimentalRoutes } from "./routes/experimental"
|
|||||||
import { ProviderRoutes } from "./routes/provider"
|
import { ProviderRoutes } from "./routes/provider"
|
||||||
import { EventRoutes } from "./routes/event"
|
import { EventRoutes } from "./routes/event"
|
||||||
import { errorHandler } from "./middleware"
|
import { errorHandler } from "./middleware"
|
||||||
|
import { getMimeType } from "hono/utils/mime"
|
||||||
|
|
||||||
const log = Log.create({ service: "server" })
|
const log = Log.create({ service: "server" })
|
||||||
|
|
||||||
@@ -285,13 +287,14 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket, app: Hono = new Hono()
|
|||||||
if (embeddedWebUI) {
|
if (embeddedWebUI) {
|
||||||
const match = embeddedWebUI[path.replace(/^\//, "")] ?? embeddedWebUI["index.html"] ?? null
|
const match = embeddedWebUI[path.replace(/^\//, "")] ?? embeddedWebUI["index.html"] ?? null
|
||||||
if (!match) return c.json({ error: "Not Found" }, 404)
|
if (!match) return c.json({ error: "Not Found" }, 404)
|
||||||
const file = Bun.file(match)
|
|
||||||
if (await file.exists()) {
|
if (await fs.exists(match)) {
|
||||||
c.header("Content-Type", file.type)
|
const mime = getMimeType(match) ?? "text/plain"
|
||||||
if (file.type.startsWith("text/html")) {
|
c.header("Content-Type", mime)
|
||||||
|
if (mime.startsWith("text/html")) {
|
||||||
c.header("Content-Security-Policy", DEFAULT_CSP)
|
c.header("Content-Security-Policy", DEFAULT_CSP)
|
||||||
}
|
}
|
||||||
return c.body(await file.arrayBuffer())
|
return c.body(new Uint8Array(await fs.readFile(match)))
|
||||||
} else {
|
} else {
|
||||||
return c.json({ error: "Not Found" }, 404)
|
return c.json({ error: "Not Found" }, 404)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { Target } from "@/control-plane/types"
|
import type { Target } from "@/control-plane/types"
|
||||||
import { lazy } from "@/util/lazy"
|
|
||||||
import { Hono } from "hono"
|
import { Hono } from "hono"
|
||||||
import { upgradeWebSocket } from "hono/bun"
|
import type { UpgradeWebSocket } from "hono/ws"
|
||||||
|
|
||||||
const hop = new Set([
|
const hop = new Set([
|
||||||
"connection",
|
"connection",
|
||||||
@@ -53,10 +52,10 @@ function send(ws: { send(data: string | ArrayBuffer | Uint8Array): void }, data:
|
|||||||
return ws.send(data)
|
return ws.send(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
const app = lazy(() =>
|
const app = (upgrade: UpgradeWebSocket) =>
|
||||||
new Hono().get(
|
new Hono().get(
|
||||||
"/__workspace_ws",
|
"/__workspace_ws",
|
||||||
upgradeWebSocket((c) => {
|
upgrade((c) => {
|
||||||
const url = c.req.header("x-opencode-proxy-url")
|
const url = c.req.header("x-opencode-proxy-url")
|
||||||
const queue: Msg[] = []
|
const queue: Msg[] = []
|
||||||
let remote: WebSocket | undefined
|
let remote: WebSocket | undefined
|
||||||
@@ -96,7 +95,6 @@ const app = lazy(() =>
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
export namespace ServerProxy {
|
export namespace ServerProxy {
|
||||||
@@ -112,13 +110,18 @@ export namespace ServerProxy {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function websocket(target: Extract<Target, { type: "remote" }>, req: Request, env: unknown) {
|
export function websocket(
|
||||||
|
upgrade: UpgradeWebSocket,
|
||||||
|
target: Extract<Target, { type: "remote" }>,
|
||||||
|
req: Request,
|
||||||
|
env: unknown,
|
||||||
|
) {
|
||||||
const url = new URL(req.url)
|
const url = new URL(req.url)
|
||||||
url.pathname = "/__workspace_ws"
|
url.pathname = "/__workspace_ws"
|
||||||
url.search = ""
|
url.search = ""
|
||||||
const next = new Headers(req.headers)
|
const next = new Headers(req.headers)
|
||||||
next.set("x-opencode-proxy-url", socket(target.url))
|
next.set("x-opencode-proxy-url", socket(target.url))
|
||||||
return app().fetch(
|
return app(upgrade).fetch(
|
||||||
new Request(url, {
|
new Request(url, {
|
||||||
method: req.method,
|
method: req.method,
|
||||||
headers: next,
|
headers: next,
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import { Filesystem } from "@/util/filesystem"
|
|||||||
import { Instance } from "@/project/instance"
|
import { Instance } from "@/project/instance"
|
||||||
import { InstanceBootstrap } from "@/project/bootstrap"
|
import { InstanceBootstrap } from "@/project/bootstrap"
|
||||||
import { InstanceRoutes } from "./instance"
|
import { InstanceRoutes } from "./instance"
|
||||||
|
import { Session } from "@/session"
|
||||||
|
import { SessionID } from "@/session/schema"
|
||||||
|
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
||||||
|
|
||||||
type Rule = { method?: string; path: string; exact?: boolean; action: "local" | "forward" }
|
type Rule = { method?: string; path: string; exact?: boolean; action: "local" | "forward" }
|
||||||
|
|
||||||
@@ -26,6 +29,16 @@ function local(method: string, path: string) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getSessionWorkspace(url: URL) {
|
||||||
|
if (url.pathname === "/session/status") return null
|
||||||
|
|
||||||
|
const id = url.pathname.match(/^\/session\/([^/]+)(?:\/|$)/)?.[1]
|
||||||
|
if (!id) return null
|
||||||
|
|
||||||
|
const session = await Session.get(SessionID.make(id)).catch(() => undefined)
|
||||||
|
return session?.workspaceID
|
||||||
|
}
|
||||||
|
|
||||||
export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): MiddlewareHandler {
|
export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): MiddlewareHandler {
|
||||||
const routes = lazy(() => InstanceRoutes(upgrade))
|
const routes = lazy(() => InstanceRoutes(upgrade))
|
||||||
|
|
||||||
@@ -42,13 +55,12 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
|
|||||||
)
|
)
|
||||||
|
|
||||||
const url = new URL(c.req.url)
|
const url = new URL(c.req.url)
|
||||||
const workspaceParam = url.searchParams.get("workspace") || c.req.header("x-opencode-workspace")
|
|
||||||
|
|
||||||
// TODO: If session is being routed, force it to lookup the
|
const sessionWorkspaceID = await getSessionWorkspace(url)
|
||||||
// project/workspace
|
const workspaceID = sessionWorkspaceID || url.searchParams.get("workspace")
|
||||||
|
|
||||||
// If no workspace is provided we use the "project" workspace
|
// If no workspace is provided we use the project
|
||||||
if (!workspaceParam) {
|
if (!workspaceID) {
|
||||||
return Instance.provide({
|
return Instance.provide({
|
||||||
directory,
|
directory,
|
||||||
init: InstanceBootstrap,
|
init: InstanceBootstrap,
|
||||||
@@ -58,8 +70,7 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspaceID = WorkspaceID.make(workspaceParam)
|
const workspace = await Workspace.get(WorkspaceID.make(workspaceID))
|
||||||
const workspace = await Workspace.get(workspaceID)
|
|
||||||
if (!workspace) {
|
if (!workspace) {
|
||||||
return new Response(`Workspace not found: ${workspaceID}`, {
|
return new Response(`Workspace not found: ${workspaceID}`, {
|
||||||
status: 500,
|
status: 500,
|
||||||
@@ -73,12 +84,16 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
|
|||||||
const target = await adaptor.target(workspace)
|
const target = await adaptor.target(workspace)
|
||||||
|
|
||||||
if (target.type === "local") {
|
if (target.type === "local") {
|
||||||
return Instance.provide({
|
return WorkspaceContext.provide({
|
||||||
|
workspaceID: WorkspaceID.make(workspaceID),
|
||||||
|
fn: () =>
|
||||||
|
Instance.provide({
|
||||||
directory: target.directory,
|
directory: target.directory,
|
||||||
init: InstanceBootstrap,
|
init: InstanceBootstrap,
|
||||||
async fn() {
|
async fn() {
|
||||||
return routes().fetch(c.req.raw, c.env)
|
return routes().fetch(c.req.raw, c.env)
|
||||||
},
|
},
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +104,7 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (c.req.header("upgrade")?.toLowerCase() === "websocket") {
|
if (c.req.header("upgrade")?.toLowerCase() === "websocket") {
|
||||||
return ServerProxy.websocket(target, c.req.raw, c.env)
|
return ServerProxy.websocket(upgrade, target, c.req.raw, c.env)
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = new Headers(c.req.raw.headers)
|
const headers = new Headers(c.req.raw.headers)
|
||||||
|
|||||||
@@ -105,6 +105,8 @@ export const GlobalRoutes = lazy(() =>
|
|||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
directory: z.string(),
|
directory: z.string(),
|
||||||
|
project: z.string().optional(),
|
||||||
|
workspace: z.string().optional(),
|
||||||
payload: BusEvent.payloads(),
|
payload: BusEvent.payloads(),
|
||||||
})
|
})
|
||||||
.meta({
|
.meta({
|
||||||
|
|||||||
@@ -6,13 +6,16 @@ import z from "zod"
|
|||||||
import { Session } from "../../session"
|
import { Session } from "../../session"
|
||||||
import { MessageV2 } from "../../session/message-v2"
|
import { MessageV2 } from "../../session/message-v2"
|
||||||
import { SessionPrompt } from "../../session/prompt"
|
import { SessionPrompt } from "../../session/prompt"
|
||||||
|
import { SessionRunState } from "@/session/run-state"
|
||||||
import { SessionCompaction } from "../../session/compaction"
|
import { SessionCompaction } from "../../session/compaction"
|
||||||
import { SessionRevert } from "../../session/revert"
|
import { SessionRevert } from "../../session/revert"
|
||||||
|
import { SessionShare } from "@/share/session"
|
||||||
import { SessionStatus } from "@/session/status"
|
import { SessionStatus } from "@/session/status"
|
||||||
import { SessionSummary } from "@/session/summary"
|
import { SessionSummary } from "@/session/summary"
|
||||||
import { Todo } from "../../session/todo"
|
import { Todo } from "../../session/todo"
|
||||||
import { Agent } from "../../agent/agent"
|
import { Agent } from "../../agent/agent"
|
||||||
import { Snapshot } from "@/snapshot"
|
import { Snapshot } from "@/snapshot"
|
||||||
|
import { Command } from "../../command"
|
||||||
import { Log } from "../../util/log"
|
import { Log } from "../../util/log"
|
||||||
import { Permission } from "@/permission"
|
import { Permission } from "@/permission"
|
||||||
import { PermissionID } from "@/permission/schema"
|
import { PermissionID } from "@/permission/schema"
|
||||||
@@ -121,7 +124,6 @@ export const SessionRoutes = lazy(() =>
|
|||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const sessionID = c.req.valid("param").sessionID
|
const sessionID = c.req.valid("param").sessionID
|
||||||
log.info("SEARCH", { url: c.req.url })
|
|
||||||
const session = await Session.get(sessionID)
|
const session = await Session.get(sessionID)
|
||||||
return c.json(session)
|
return c.json(session)
|
||||||
},
|
},
|
||||||
@@ -205,10 +207,10 @@ export const SessionRoutes = lazy(() =>
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
validator("json", Session.create.schema.optional()),
|
validator("json", Session.create.schema),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const body = c.req.valid("json") ?? {}
|
const body = c.req.valid("json") ?? {}
|
||||||
const session = await Session.create(body)
|
const session = await SessionShare.create(body)
|
||||||
return c.json(session)
|
return c.json(session)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -292,6 +294,7 @@ export const SessionRoutes = lazy(() =>
|
|||||||
return c.json(session)
|
return c.json(session)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
// TODO(v2): remove this dedicated route and rely on the normal `/init` command flow.
|
||||||
.post(
|
.post(
|
||||||
"/:sessionID/init",
|
"/:sessionID/init",
|
||||||
describeRoute({
|
describeRoute({
|
||||||
@@ -317,11 +320,24 @@ export const SessionRoutes = lazy(() =>
|
|||||||
sessionID: SessionID.zod,
|
sessionID: SessionID.zod,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
validator("json", Session.initialize.schema.omit({ sessionID: true })),
|
validator(
|
||||||
|
"json",
|
||||||
|
z.object({
|
||||||
|
modelID: ModelID.zod,
|
||||||
|
providerID: ProviderID.zod,
|
||||||
|
messageID: MessageID.zod,
|
||||||
|
}),
|
||||||
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const sessionID = c.req.valid("param").sessionID
|
const sessionID = c.req.valid("param").sessionID
|
||||||
const body = c.req.valid("json")
|
const body = c.req.valid("json")
|
||||||
await Session.initialize({ ...body, sessionID })
|
await SessionPrompt.command({
|
||||||
|
sessionID,
|
||||||
|
messageID: body.messageID,
|
||||||
|
model: body.providerID + "/" + body.modelID,
|
||||||
|
command: Command.Default.INIT,
|
||||||
|
arguments: "",
|
||||||
|
})
|
||||||
return c.json(true)
|
return c.json(true)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -411,7 +427,7 @@ export const SessionRoutes = lazy(() =>
|
|||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const sessionID = c.req.valid("param").sessionID
|
const sessionID = c.req.valid("param").sessionID
|
||||||
await Session.share(sessionID)
|
await SessionShare.share(sessionID)
|
||||||
const session = await Session.get(sessionID)
|
const session = await Session.get(sessionID)
|
||||||
return c.json(session)
|
return c.json(session)
|
||||||
},
|
},
|
||||||
@@ -476,12 +492,12 @@ export const SessionRoutes = lazy(() =>
|
|||||||
validator(
|
validator(
|
||||||
"param",
|
"param",
|
||||||
z.object({
|
z.object({
|
||||||
sessionID: Session.unshare.schema,
|
sessionID: SessionID.zod,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const sessionID = c.req.valid("param").sessionID
|
const sessionID = c.req.valid("param").sessionID
|
||||||
await Session.unshare(sessionID)
|
await SessionShare.unshare(sessionID)
|
||||||
const session = await Session.get(sessionID)
|
const session = await Session.get(sessionID)
|
||||||
return c.json(session)
|
return c.json(session)
|
||||||
},
|
},
|
||||||
@@ -699,7 +715,7 @@ export const SessionRoutes = lazy(() =>
|
|||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const params = c.req.valid("param")
|
const params = c.req.valid("param")
|
||||||
await SessionPrompt.assertNotBusy(params.sessionID)
|
await SessionRunState.assertNotBusy(params.sessionID)
|
||||||
await Session.removeMessage({
|
await Session.removeMessage({
|
||||||
sessionID: params.sessionID,
|
sessionID: params.sessionID,
|
||||||
messageID: params.messageID,
|
messageID: params.messageID,
|
||||||
@@ -843,9 +859,6 @@ export const SessionRoutes = lazy(() =>
|
|||||||
),
|
),
|
||||||
validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
|
validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
c.status(204)
|
|
||||||
c.header("Content-Type", "application/json")
|
|
||||||
return stream(c, async () => {
|
|
||||||
const sessionID = c.req.valid("param").sessionID
|
const sessionID = c.req.valid("param").sessionID
|
||||||
const body = c.req.valid("json")
|
const body = c.req.valid("json")
|
||||||
SessionPrompt.prompt({ ...body, sessionID }).catch((err) => {
|
SessionPrompt.prompt({ ...body, sessionID }).catch((err) => {
|
||||||
@@ -855,7 +868,8 @@ export const SessionRoutes = lazy(() =>
|
|||||||
error: new NamedError.Unknown({ message: err instanceof Error ? err.message : String(err) }).toObject(),
|
error: new NamedError.Unknown({ message: err instanceof Error ? err.message : String(err) }).toObject(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
return c.body(null, 204)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.post(
|
.post(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Log } from "../util/log"
|
|||||||
import { describeRoute, generateSpecs, validator, resolver, openAPIRouteHandler } from "hono-openapi"
|
import { describeRoute, generateSpecs, validator, resolver, openAPIRouteHandler } from "hono-openapi"
|
||||||
import { Hono } from "hono"
|
import { Hono } from "hono"
|
||||||
import { compress } from "hono/compress"
|
import { compress } from "hono/compress"
|
||||||
|
import { createNodeWebSocket } from "@hono/node-ws"
|
||||||
import { cors } from "hono/cors"
|
import { cors } from "hono/cors"
|
||||||
import { basicAuth } from "hono/basic-auth"
|
import { basicAuth } from "hono/basic-auth"
|
||||||
import type { UpgradeWebSocket } from "hono/ws"
|
import type { UpgradeWebSocket } from "hono/ws"
|
||||||
@@ -9,8 +10,6 @@ import z from "zod"
|
|||||||
import { Auth } from "../auth"
|
import { Auth } from "../auth"
|
||||||
import { Flag } from "../flag/flag"
|
import { Flag } from "../flag/flag"
|
||||||
import { ProviderID } from "../provider/schema"
|
import { ProviderID } from "../provider/schema"
|
||||||
import { createAdaptorServer, type ServerType } from "@hono/node-server"
|
|
||||||
import { createNodeWebSocket } from "@hono/node-ws"
|
|
||||||
import { WorkspaceRouterMiddleware } from "./router"
|
import { WorkspaceRouterMiddleware } from "./router"
|
||||||
import { errors } from "./error"
|
import { errors } from "./error"
|
||||||
import { GlobalRoutes } from "./routes/global"
|
import { GlobalRoutes } from "./routes/global"
|
||||||
@@ -19,6 +18,7 @@ import { lazy } from "@/util/lazy"
|
|||||||
import { errorHandler } from "./middleware"
|
import { errorHandler } from "./middleware"
|
||||||
import { InstanceRoutes } from "./instance"
|
import { InstanceRoutes } from "./instance"
|
||||||
import { initProjectors } from "./projectors"
|
import { initProjectors } from "./projectors"
|
||||||
|
import { createAdaptorServer, type ServerType } from "@hono/node-server"
|
||||||
|
|
||||||
// @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
|
// @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
|
globalThis.AI_SDK_LOG_WARNINGS = false
|
||||||
@@ -42,7 +42,7 @@ export namespace Server {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Default = lazy(() => create({}).app)
|
export const Default = lazy(() => create({}))
|
||||||
|
|
||||||
export function ControlPlaneRoutes(upgrade: UpgradeWebSocket, app = new Hono(), opts?: { cors?: string[] }): Hono {
|
export function ControlPlaneRoutes(upgrade: UpgradeWebSocket, app = new Hono(), opts?: { cors?: string[] }): Hono {
|
||||||
return app
|
return app
|
||||||
@@ -54,6 +54,9 @@ export namespace Server {
|
|||||||
const password = Flag.OPENCODE_SERVER_PASSWORD
|
const password = Flag.OPENCODE_SERVER_PASSWORD
|
||||||
if (!password) return next()
|
if (!password) return next()
|
||||||
const username = Flag.OPENCODE_SERVER_USERNAME ?? "opencode"
|
const username = Flag.OPENCODE_SERVER_USERNAME ?? "opencode"
|
||||||
|
|
||||||
|
if (c.req.query("auth_token")) c.req.raw.headers.set("authorization", `Basic ${c.req.query("auth_token")}`)
|
||||||
|
|
||||||
return basicAuth({ username, password })(c, next)
|
return basicAuth({ username, password })(c, next)
|
||||||
})
|
})
|
||||||
.use(async (c, next) => {
|
.use(async (c, next) => {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user