mirror of
https://github.com/anomalyco/opencode.git
synced 2026-04-07 22:44:53 +00:00
Compare commits
74 Commits
kit/ci-uni
...
kit/remove
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b4b419b1c | ||
|
|
155eb81ab4 | ||
|
|
bc1840b196 | ||
|
|
095aeba0a7 | ||
|
|
e945436b6f | ||
|
|
6bfa82de65 | ||
|
|
d83fe4b540 | ||
|
|
81bdffc81c | ||
|
|
2549a38a71 | ||
|
|
5d48e7bd44 | ||
|
|
ec8b9810b4 | ||
|
|
65318a80f7 | ||
|
|
90e1e50ada | ||
|
|
234863844d | ||
|
|
f7c9d4f3c7 | ||
|
|
6a5aae9a84 | ||
|
|
1f94c48bdd | ||
|
|
01c5eb679c | ||
|
|
41612b3dbe | ||
|
|
c2d2ca3522 | ||
|
|
3a1ec27feb | ||
|
|
3c96bf8468 | ||
|
|
3ea6413407 | ||
|
|
885df8eb54 | ||
|
|
f4975ef32a | ||
|
|
37883a9f3a | ||
|
|
3c31d04666 | ||
|
|
e64548fb4d | ||
|
|
31f6f43cfc | ||
|
|
090ad8290e | ||
|
|
d1258ac19c | ||
|
|
48c1b6b338 | ||
|
|
40e4cd27a1 | ||
|
|
5a6d10cd53 | ||
|
|
527b51477d | ||
|
|
535343bf56 | ||
|
|
4394e42615 | ||
|
|
2e4c43c1cf | ||
|
|
965c751522 | ||
|
|
24bdd3c9fb | ||
|
|
01f0319192 | ||
|
|
517e6c9aa4 | ||
|
|
a4a9ea4ab0 | ||
|
|
eaa272ef7f | ||
|
|
70b636a360 | ||
|
|
a8fd0159be | ||
|
|
342436dfc4 | ||
|
|
77a462c930 | ||
|
|
9965d385de | ||
|
|
f0f1e51c5c | ||
|
|
4712c18a58 | ||
|
|
9e156ea168 | ||
|
|
68f4aa220e | ||
|
|
3a0e00dd7f | ||
|
|
66b4e5e020 | ||
|
|
8b8d4fa066 | ||
|
|
6253ef0c27 | ||
|
|
c6ebc7ff7c | ||
|
|
985663620f | ||
|
|
c796b9a19e | ||
|
|
6ea108a03b | ||
|
|
280eb16e77 | ||
|
|
930e94a3ea | ||
|
|
629e866ff0 | ||
|
|
c08fa5675f | ||
|
|
cc50b778eb | ||
|
|
00fa68b3a7 | ||
|
|
288eb044cb | ||
|
|
59ca4543d8 | ||
|
|
650d0dbe54 | ||
|
|
a5ec741cff | ||
|
|
fff98636f7 | ||
|
|
c72642dd35 | ||
|
|
f2d4ced8ea |
35
.github/workflows/test.yml
vendored
35
.github/workflows/test.yml
vendored
@@ -15,6 +15,7 @@ concurrency:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write
|
||||
|
||||
jobs:
|
||||
unit:
|
||||
@@ -45,14 +46,40 @@ jobs:
|
||||
git config --global user.email "bot@opencode.ai"
|
||||
git config --global user.name "opencode"
|
||||
|
||||
- name: Cache Turbo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: node_modules/.cache/turbo
|
||||
key: turbo-${{ runner.os }}-${{ hashFiles('turbo.json', '**/package.json') }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
turbo-${{ runner.os }}-${{ hashFiles('turbo.json', '**/package.json') }}-
|
||||
turbo-${{ runner.os }}-
|
||||
|
||||
- name: Run unit tests
|
||||
run: bun turbo test
|
||||
run: bun turbo test:ci
|
||||
env:
|
||||
# Bun 1.3.11 intermittently crashes on Windows during test teardown
|
||||
# inside the native @parcel/watcher binding. Unit CI does not rely on
|
||||
# the live watcher backend there, so disable it for that platform.
|
||||
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }}
|
||||
|
||||
- name: Publish unit reports
|
||||
if: always()
|
||||
uses: mikepenz/action-junit-report@v6
|
||||
with:
|
||||
report_paths: packages/*/.artifacts/unit/junit.xml
|
||||
check_name: "unit results (${{ matrix.settings.name }})"
|
||||
detailed_summary: true
|
||||
include_time_in_summary: true
|
||||
fail_on_failure: false
|
||||
|
||||
- name: Upload unit artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: unit-${{ matrix.settings.name }}-${{ github.run_attempt }}
|
||||
include-hidden-files: true
|
||||
if-no-files-found: ignore
|
||||
retention-days: 7
|
||||
path: packages/*/.artifacts/unit/junit.xml
|
||||
|
||||
e2e:
|
||||
name: e2e (${{ matrix.settings.name }})
|
||||
strategy:
|
||||
|
||||
211
bun.lock
211
bun.lock
@@ -26,7 +26,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.17",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -80,7 +80,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.17",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -114,7 +114,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.17",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -141,7 +141,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.17",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "3.0.64",
|
||||
"@ai-sdk/openai": "3.0.48",
|
||||
@@ -165,7 +165,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.17",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -189,7 +189,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.17",
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -222,7 +222,7 @@
|
||||
},
|
||||
"packages/desktop-electron": {
|
||||
"name": "@opencode-ai/desktop-electron",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.17",
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -254,7 +254,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.17",
|
||||
"dependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
@@ -283,7 +283,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.17",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -299,7 +299,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.17",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -308,7 +308,7 @@
|
||||
"@actions/github": "6.0.1",
|
||||
"@agentclientprotocol/sdk": "0.16.1",
|
||||
"@ai-sdk/amazon-bedrock": "4.0.83",
|
||||
"@ai-sdk/anthropic": "3.0.64",
|
||||
"@ai-sdk/anthropic": "3.0.67",
|
||||
"@ai-sdk/azure": "3.0.49",
|
||||
"@ai-sdk/cerebras": "2.0.41",
|
||||
"@ai-sdk/cohere": "3.0.27",
|
||||
@@ -322,15 +322,20 @@
|
||||
"@ai-sdk/openai-compatible": "2.0.37",
|
||||
"@ai-sdk/perplexity": "3.0.26",
|
||||
"@ai-sdk/provider": "3.0.8",
|
||||
"@ai-sdk/provider-utils": "4.0.21",
|
||||
"@ai-sdk/provider-utils": "4.0.23",
|
||||
"@ai-sdk/togetherai": "2.0.41",
|
||||
"@ai-sdk/vercel": "2.0.39",
|
||||
"@ai-sdk/xai": "3.0.75",
|
||||
"@aws-sdk/credential-providers": "3.993.0",
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@gitlab/gitlab-ai-provider": "3.6.0",
|
||||
"@gitlab/opencode-gitlab-auth": "1.3.3",
|
||||
"@hono/node-server": "1.19.11",
|
||||
"@hono/node-ws": "1.3.0",
|
||||
"@hono/standard-validator": "0.1.5",
|
||||
"@hono/zod-validator": "catalog:",
|
||||
"@lydell/node-pty": "1.2.0-beta.10",
|
||||
"@modelcontextprotocol/sdk": "1.27.1",
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@octokit/graphql": "9.0.2",
|
||||
@@ -340,9 +345,9 @@
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "2.3.3",
|
||||
"@opentui/core": "0.1.96",
|
||||
"@opentui/solid": "0.1.96",
|
||||
"@openrouter/ai-sdk-provider": "2.4.2",
|
||||
"@opentui/core": "0.1.97",
|
||||
"@opentui/solid": "0.1.97",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
@@ -371,6 +376,7 @@
|
||||
"jsonc-parser": "3.3.1",
|
||||
"mime-types": "3.0.2",
|
||||
"minimatch": "10.0.3",
|
||||
"npm-package-arg": "13.0.2",
|
||||
"open": "10.1.2",
|
||||
"opencode-gitlab-auth": "2.0.1",
|
||||
"opencode-poe-auth": "0.0.1",
|
||||
@@ -412,6 +418,7 @@
|
||||
"@types/bun": "catalog:",
|
||||
"@types/cross-spawn": "catalog:",
|
||||
"@types/mime-types": "3.0.1",
|
||||
"@types/npm-package-arg": "6.1.4",
|
||||
"@types/npmcli__arborist": "6.3.3",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/turndown": "5.0.5",
|
||||
@@ -428,22 +435,22 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.17",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"zod": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opentui/core": "0.1.96",
|
||||
"@opentui/solid": "0.1.96",
|
||||
"@opentui/core": "0.1.97",
|
||||
"@opentui/solid": "0.1.97",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentui/core": ">=0.1.96",
|
||||
"@opentui/solid": ">=0.1.96",
|
||||
"@opentui/core": ">=0.1.97",
|
||||
"@opentui/solid": ">=0.1.97",
|
||||
},
|
||||
"optionalPeers": [
|
||||
"@opentui/core",
|
||||
@@ -462,7 +469,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.17",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -477,7 +484,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.17",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -512,7 +519,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.17",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -560,7 +567,7 @@
|
||||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.17",
|
||||
"dependencies": {
|
||||
"zod": "catalog:",
|
||||
},
|
||||
@@ -571,7 +578,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.17",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -614,8 +621,6 @@
|
||||
"patchedDependencies": {
|
||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"@ai-sdk/anthropic@3.0.64": "patches/@ai-sdk%2Fanthropic@3.0.64.patch",
|
||||
"@ai-sdk/provider-utils@4.0.21": "patches/@ai-sdk%2Fprovider-utils@4.0.21.patch",
|
||||
},
|
||||
"overrides": {
|
||||
"@types/bun": "catalog:",
|
||||
@@ -643,7 +648,7 @@
|
||||
"@types/node": "22.13.9",
|
||||
"@types/semver": "7.7.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
||||
"ai": "6.0.138",
|
||||
"ai": "6.0.149",
|
||||
"cross-spawn": "7.0.6",
|
||||
"diff": "8.0.2",
|
||||
"dompurify": "3.3.1",
|
||||
@@ -726,7 +731,7 @@
|
||||
|
||||
"@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
|
||||
|
||||
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.23", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-z8GlDaCmRSDlqkMF2f4/RFgWxdarvIbyuk+m6WXT1LYgsnGiXRJGTD2Z1+SDl3LqtFuRtGX1aghYvQLoHL/9pg=="],
|
||||
|
||||
"@ai-sdk/togetherai": ["@ai-sdk/togetherai@2.0.41", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-k3p9e3k0/gpDDyTtvafsK4HYR4D/aUQW/kzCwWo1+CzdBU84i4L14gWISC/mv6tgSicMXHcEUd521fPufQwNlg=="],
|
||||
|
||||
@@ -1142,6 +1147,10 @@
|
||||
|
||||
"@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="],
|
||||
|
||||
"@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.6.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": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-8LmcIQ86xkMtC7L4P1/QYVEC+yKMTRerfPeniaaQGalnzXKtX6iMHLjLPOL9Rxp55lOXi6ed0WrFuJzZx+fNRg=="],
|
||||
|
||||
"@gitlab/opencode-gitlab-auth": ["@gitlab/opencode-gitlab-auth@1.3.3", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-FT+KsCmAJjtqWr1YAq0MywGgL9kaLQ4apmsoowAXrPqHtoYf2i/nY10/A+L06kNj22EATeEDRpbB1NWXMto/SA=="],
|
||||
|
||||
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
|
||||
|
||||
"@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="],
|
||||
@@ -1154,7 +1163,9 @@
|
||||
|
||||
"@hey-api/types": ["@hey-api/types@0.1.2", "", {}, "sha512-uNNtiVAWL7XNrV/tFXx7GLY9lwaaDazx1173cGW3+UEaw4RUPsHEmiB4DSpcjNxMIcrctfz2sGKLnVx5PBG2RA=="],
|
||||
|
||||
"@hono/node-server": ["@hono/node-server@1.19.12", "", { "peerDependencies": { "hono": "^4" } }, "sha512-txsUW4SQ1iilgE0l9/e9VQWmELXifEFvmdA1j6WFh/aFPj99hIntrSsq/if0UWyGVkmrRPKA1wCeP+UCr1B9Uw=="],
|
||||
"@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="],
|
||||
|
||||
"@hono/node-ws": ["@hono/node-ws@1.3.0", "", { "dependencies": { "ws": "^8.17.0" }, "peerDependencies": { "@hono/node-server": "^1.19.2", "hono": "^4.6.0" } }, "sha512-ju25YbbvLuXdqBCmLZLqnNYu1nbHIQjoyUqA8ApZOeL1k4skuiTcw5SW77/5SUYo2Xi2NVBJoVlfQurnKEp03Q=="],
|
||||
|
||||
"@hono/standard-validator": ["@hono/standard-validator@0.1.5", "", { "peerDependencies": { "@standard-schema/spec": "1.0.0", "hono": ">=3.9.0" } }, "sha512-EIyZPPwkyLn6XKwFj5NBEWHXhXbgmnVh2ceIFo5GO7gKI9WmzTjPDKnppQB0KrqKeAkq3kpoW4SIbu5X1dgx3w=="],
|
||||
|
||||
@@ -1346,6 +1357,20 @@
|
||||
|
||||
"@lukeed/ms": ["@lukeed/ms@2.0.2", "", {}, "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA=="],
|
||||
|
||||
"@lydell/node-pty": ["@lydell/node-pty@1.2.0-beta.10", "", { "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" } }, "sha512-Fv+A3+MZVA8qhkBIZsM1E6dCdHNMyXXz22mAYiMWd03LlyK///F3OH6CKPX9mj4id7LUlxpr45yPzyBVy9aDPw=="],
|
||||
|
||||
"@lydell/node-pty-darwin-arm64": ["@lydell/node-pty-darwin-arm64@1.2.0-beta.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-C+eqDyRNHRYvx7RaHj6VVCx6nCpRBPuuxhTcc3JH3GuBMoxTsYeY4GkWH2XOktrgbAq1BG8e/Y8bu/wNQreCEw=="],
|
||||
|
||||
"@lydell/node-pty-darwin-x64": ["@lydell/node-pty-darwin-x64@1.2.0-beta.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-aZoIK6HtJO5BiT4ELm683U4dyHtt8b7wNgq3NJqYAQwSXrcPv576Z8vY3BIulVxfcFkht/SPLKou9TtdFXdNpg=="],
|
||||
|
||||
"@lydell/node-pty-linux-arm64": ["@lydell/node-pty-linux-arm64@1.2.0-beta.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-0cKX2iMyXFNBE4fGtGK6B7IkdXcDMZajyEDoGMOgQQs/DDtoI5tSPcBcqNY9VitVrsRQA8+gFt6eKYU9Ye/lUA=="],
|
||||
|
||||
"@lydell/node-pty-linux-x64": ["@lydell/node-pty-linux-x64@1.2.0-beta.10", "", { "os": "linux", "cpu": "x64" }, "sha512-J9HnxvSzEeMH748+Ul1VrmCLWMo7iCVJy9EGijRR62+YO/Yk5GaCydUTZ+KzlH0/X5aTrgt5cfiof4vx45tRRg=="],
|
||||
|
||||
"@lydell/node-pty-win32-arm64": ["@lydell/node-pty-win32-arm64@1.2.0-beta.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-PlDJpJX/pnKyy6OmADKzhf+INZDDnzTBGaI0LT4laVNc6NblZNqUSkCMjLFWbeakeuQp0VG37M49WQSN9FDfeA=="],
|
||||
|
||||
"@lydell/node-pty-win32-x64": ["@lydell/node-pty-win32-x64@1.2.0-beta.10", "", { "os": "win32", "cpu": "x64" }, "sha512-ExFgWrzyldNAMi45U9PLIOu+g/RatP+f0c/dZxaooifME6yLW32BoHveH26/TtoAjZyJrc2iL0u48pgnR1fzmg=="],
|
||||
|
||||
"@malept/cross-spawn-promise": ["@malept/cross-spawn-promise@2.0.0", "", { "dependencies": { "cross-spawn": "^7.0.1" } }, "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg=="],
|
||||
|
||||
"@malept/flatpak-bundler": ["@malept/flatpak-bundler@0.4.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.0", "lodash": "^4.17.15", "tmp-promise": "^3.0.2" } }, "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q=="],
|
||||
@@ -1500,25 +1525,25 @@
|
||||
|
||||
"@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"],
|
||||
|
||||
"@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.3.3", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-4fVteGkVedc7fGoA9+qJs4tpYwALezMq14m2Sjub3KmyRlksCbK+WJf67NPdGem8+NZrV2tAN42A1NU3+SiV3w=="],
|
||||
"@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.4.2", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-uRQZ4da77gru1I7/lNGJhKbqEIY7o/sPsLlbCM97VY9muGDjM/TaJzuwqIviqKTtXLzF0WDj5qBAi6FhxjvlSg=="],
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
"@opentui/core": ["@opentui/core@0.1.96", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.96", "@opentui/core-darwin-x64": "0.1.96", "@opentui/core-linux-arm64": "0.1.96", "@opentui/core-linux-x64": "0.1.96", "@opentui/core-win32-arm64": "0.1.96", "@opentui/core-win32-x64": "0.1.96", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-VBO5zRiGM6fhibG3AwTMpf0JgbYWG0sXP5AsSJAYw8tQ18OCPj+EDLXGZ1DFmMnJWEi+glKYjmqnIp4yRCqi+Q=="],
|
||||
"@opentui/core": ["@opentui/core@0.1.97", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.97", "@opentui/core-darwin-x64": "0.1.97", "@opentui/core-linux-arm64": "0.1.97", "@opentui/core-linux-x64": "0.1.97", "@opentui/core-win32-arm64": "0.1.97", "@opentui/core-win32-x64": "0.1.97", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-2ENH0Dc4NUAeHeeQCQhF1lg68RuyntOUP68UvortvDqTz/hqLG0tIwF+DboCKtWi8Nmao4SAQEJ7lfmyQNEDOQ=="],
|
||||
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.96", "", { "os": "darwin", "cpu": "arm64" }, "sha512-909i75uhLmlUFCK3LK4iICaymiA7QaB45X9IDX94KaDyHL3Y1PgYTzoRZLJlqeOfOBjVfEjMAh/zA5XexWDMpA=="],
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.97", "", { "os": "darwin", "cpu": "arm64" }, "sha512-t7oMGEfMPQsqLEx7/rPqv/UGJ+vqhe4RWHRRQRYcuHuLKssZ2S8P9mSS7MBPtDqGcxg4PosCrh5nHYeZ94EXUw=="],
|
||||
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.96", "", { "os": "darwin", "cpu": "x64" }, "sha512-qukQjjScKldZAfgY9qVMPv4ZA6Ko7oXjNBUcSMGDgUiOitH6INT1cJQVUnAIu14DY15yEl08MEQ8soLDaSAHcg=="],
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.97", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZuPWAawlVat6ZHb8vaH/CVUeGwI0pI4vd+6zz1ZocZn95ZWJztfyhzNZOJrq1WjHmUROieJ7cOuYUZfvYNuLrg=="],
|
||||
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.96", "", { "os": "linux", "cpu": "arm64" }, "sha512-9ktmyS24nfSmlFPX0GMWEaEYSjtEPbRn59y4KBhHVhzPsl+YKlzstyHomTBu51IAPu6oL3+t3Lu4gU+k1gFOQQ=="],
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.97", "", { "os": "linux", "cpu": "arm64" }, "sha512-QXxhz654vXgEu2wrFFFFnrSWbyk6/r6nXNnDTcMRWofdMZQLx87NhbcsErNmz9KmFdzoPiQSmlpYubLflKKzqQ=="],
|
||||
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.96", "", { "os": "linux", "cpu": "x64" }, "sha512-m2pVhIdtqFYO+QSMc2VZgSSCNxRGPL+U+aKYYbvJjPzqCnIkHB9eO0ePU4b3t+V7GaWCcCP3vDCy3g1J5/FreA=="],
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.97", "", { "os": "linux", "cpu": "x64" }, "sha512-v3z0QWpRS3p8blE/A7pTu15hcFMtSndeiYhRxhrjp6zAhQ+UlruQs9DAG1ifSuVO1RJJ0pUKklFivdbu0pMzuw=="],
|
||||
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.96", "", { "os": "win32", "cpu": "arm64" }, "sha512-OybZ4jvX6H6RKYyGpZqzy3ZrwKaxaXKWwFsmG6pC2J+GRhf5oCIIEy3Y5573h7zy1cq3T9cb225KzBANq9j5BA=="],
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.97", "", { "os": "win32", "cpu": "arm64" }, "sha512-o/m9mD1dvOCwkxOUUyoEILl+d6tzh/85foJc4uqjXYi71NNcwg8u+Eq3/gdHuSKnlT1pusCPKoS1IDuBvZE24A=="],
|
||||
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.96", "", { "os": "win32", "cpu": "x64" }, "sha512-3YKjg90j14I7dJ94yN0pAYcTf4ogCoohv6ptRdG96XUyzrYhQiDMP398vCIOMjaLBjtMtFmTxSf+W46zm96BCQ=="],
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.97", "", { "os": "win32", "cpu": "x64" }, "sha512-Rwp7JOwrYm4wtzPHY2vv+2l91LXmKSI7CtbmWN1sSUGhBPtPGSvfwux3W5xaAZQa2KPEXicPjaKJZc+pob3YRg=="],
|
||||
|
||||
"@opentui/solid": ["@opentui/solid@0.1.96", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.96", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-NGiVvG1ylswMjF9fzvpSaWLcZKQsPw67KRkIZgsdf4ZIKUZEZ94NktabCA92ti4WVGXhPvyM3SIX5S2+HvnJFg=="],
|
||||
"@opentui/solid": ["@opentui/solid@0.1.97", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.97", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-ma/uihG38F+6oLJVD8yR7z82FWmR8QhfesNV5SBXbN74riMCRyy6kyQ6SI4xs4ykt9BbZOjrKLq+Xt/0Pd0SJQ=="],
|
||||
|
||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||
|
||||
@@ -2344,7 +2369,7 @@
|
||||
|
||||
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
|
||||
|
||||
"ai": ["ai@6.0.138", "", { "dependencies": { "@ai-sdk/gateway": "3.0.80", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-49OfPe0f5uxJ6jUdA5BBXjIinP6+ZdYfAtpF2aEH64GA5wPcxH2rf/TBUQQ0bbamBz/D+TLMV18xilZqOC+zaA=="],
|
||||
"ai": ["ai@6.0.149", "", { "dependencies": { "@ai-sdk/gateway": "3.0.91", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3asRb/m3ZGH7H4+VTuTgj8eQYJZ9IJUmV0ljLslY92mQp6Zj+NVn4SmFj0TBr2Y/wFBWC3xgn++47tSGOXxdbw=="],
|
||||
|
||||
"ai-gateway-provider": ["ai-gateway-provider@3.1.2", "", { "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^4.0.62", "@ai-sdk/anthropic": "^3.0.46", "@ai-sdk/azure": "^3.0.31", "@ai-sdk/cerebras": "^2.0.34", "@ai-sdk/cohere": "^3.0.21", "@ai-sdk/deepgram": "^2.0.20", "@ai-sdk/deepseek": "^2.0.20", "@ai-sdk/elevenlabs": "^2.0.20", "@ai-sdk/fireworks": "^2.0.34", "@ai-sdk/google": "^3.0.30", "@ai-sdk/google-vertex": "^4.0.61", "@ai-sdk/groq": "^3.0.24", "@ai-sdk/mistral": "^3.0.20", "@ai-sdk/openai": "^3.0.30", "@ai-sdk/perplexity": "^3.0.19", "@ai-sdk/xai": "^3.0.57", "@openrouter/ai-sdk-provider": "^2.2.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^2.0.0", "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.0", "ai": "^6.0.0" } }, "sha512-krGNnJSoO/gJ7Hbe5nQDlsBpDUGIBGtMQTRUaW7s1MylsfvLduba0TLWzQaGtOmNRkP0pGhtGlwsnS6FNQMlyw=="],
|
||||
|
||||
@@ -4958,8 +4983,50 @@
|
||||
|
||||
"@actions/http-client/undici": ["undici@6.24.1", "", {}, "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||
|
||||
"@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||
|
||||
"@ai-sdk/azure/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||
|
||||
"@ai-sdk/cerebras/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||
|
||||
"@ai-sdk/cohere/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||
|
||||
"@ai-sdk/deepgram/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||
|
||||
"@ai-sdk/deepinfra/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||
|
||||
"@ai-sdk/deepseek/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||
|
||||
"@ai-sdk/elevenlabs/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||
|
||||
"@ai-sdk/fireworks/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||
|
||||
"@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||
|
||||
"@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||
|
||||
"@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||
|
||||
"@ai-sdk/groq/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||
|
||||
"@ai-sdk/mistral/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||
|
||||
"@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||
|
||||
"@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||
|
||||
"@ai-sdk/perplexity/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||
|
||||
"@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/togetherai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||
|
||||
"@ai-sdk/vercel/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||
|
||||
"@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||
|
||||
"@astrojs/check/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
||||
|
||||
"@astrojs/cloudflare/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
|
||||
@@ -5192,10 +5259,18 @@
|
||||
|
||||
"@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
|
||||
|
||||
"@gitlab/gitlab-ai-provider/openai": ["openai@6.33.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-xAYN1W3YsDXJWA5F277135YfkEk6H7D3D6vWwRhJ3OEkzRgcyK8z/P5P9Gyi/wB4N8kK9kM5ZjprfvyHagKmpw=="],
|
||||
|
||||
"@gitlab/gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"@gitlab/opencode-gitlab-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
|
||||
|
||||
"@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
|
||||
|
||||
"@hey-api/openapi-ts/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"@hono/node-ws/ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="],
|
||||
|
||||
"@hono/zod-validator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"@jimp/core/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="],
|
||||
@@ -5248,6 +5323,8 @@
|
||||
|
||||
"@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
||||
|
||||
"@modelcontextprotocol/sdk/@hono/node-server": ["@hono/node-server@1.19.12", "", { "peerDependencies": { "hono": "^4" } }, "sha512-txsUW4SQ1iilgE0l9/e9VQWmELXifEFvmdA1j6WFh/aFPj99hIntrSsq/if0UWyGVkmrRPKA1wCeP+UCr1B9Uw=="],
|
||||
|
||||
"@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
||||
|
||||
"@modelcontextprotocol/sdk/hono": ["hono@4.12.9", "", {}, "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA=="],
|
||||
@@ -5450,6 +5527,10 @@
|
||||
|
||||
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.91", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-J39Dh6Gyg6HjG3A7OFKnJMp3QyZ3Eex+XDiX8aFBdRwwZm3jGWaMhkCxQPH7yiQ9kRiErZwHXX/Oexx4SyGGGA=="],
|
||||
|
||||
"ai-gateway-provider/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.3.3", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-4fVteGkVedc7fGoA9+qJs4tpYwALezMq14m2Sjub3KmyRlksCbK+WJf67NPdGem8+NZrV2tAN42A1NU3+SiV3w=="],
|
||||
|
||||
"ajv-keywords/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
|
||||
|
||||
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
@@ -5660,6 +5741,8 @@
|
||||
|
||||
"nypm/tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="],
|
||||
|
||||
"opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.67", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-FFX4P5Fd6lcQJc2OLngZQkbbJHa0IDDZi087Edb8qRZx6h90krtM61ArbMUL8us/7ZUwojCXnyJ/wQ2Eflx2jQ=="],
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="],
|
||||
|
||||
"opencontrol/@tsconfig/bun": ["@tsconfig/bun@1.0.7", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="],
|
||||
@@ -5794,6 +5877,8 @@
|
||||
|
||||
"uri-js/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"venice-ai-sdk-provider/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||
|
||||
"vite-plugin-icons-spritesheet/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
|
||||
|
||||
"vitest/@vitest/expect": ["@vitest/expect@4.1.2", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.2", "@vitest/utils": "4.1.2", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ=="],
|
||||
@@ -5840,6 +5925,48 @@
|
||||
|
||||
"@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/cerebras/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/cohere/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/deepgram/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/deepinfra/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/deepseek/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/elevenlabs/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/fireworks/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/gateway/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/google-vertex/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/google/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/groq/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/mistral/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/openai-compatible/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/perplexity/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/togetherai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/vercel/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@ai-sdk/xai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@astrojs/check/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||
|
||||
"@astrojs/check/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
@@ -6082,6 +6209,8 @@
|
||||
|
||||
"@expressive-code/plugin-shiki/shiki/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="],
|
||||
|
||||
"@gitlab/opencode-gitlab-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
|
||||
|
||||
"@jsx-email/cli/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="],
|
||||
|
||||
"@jsx-email/cli/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="],
|
||||
@@ -6426,6 +6555,8 @@
|
||||
|
||||
"type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"venice-ai-sdk-provider/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"vite-plugin-icons-spritesheet/glob/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
|
||||
|
||||
"vitest/@vitest/expect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
@@ -109,6 +109,12 @@ const zenLiteCouponFirstMonth50 = new stripe.Coupon("ZenLiteCouponFirstMonth50",
|
||||
appliesToProducts: [zenLiteProduct.id],
|
||||
duration: "once",
|
||||
})
|
||||
const zenLiteCouponFirstMonth100 = new stripe.Coupon("ZenLiteCouponFirstMonth100", {
|
||||
name: "First month 100% off",
|
||||
percentOff: 100,
|
||||
appliesToProducts: [zenLiteProduct.id],
|
||||
duration: "once",
|
||||
})
|
||||
const zenLitePrice = new stripe.Price("ZenLitePrice", {
|
||||
product: zenLiteProduct.id,
|
||||
currency: "usd",
|
||||
@@ -124,6 +130,7 @@ const ZEN_LITE_PRICE = new sst.Linkable("ZEN_LITE_PRICE", {
|
||||
price: zenLitePrice.id,
|
||||
priceInr: 92900,
|
||||
firstMonth50Coupon: zenLiteCouponFirstMonth50.id,
|
||||
firstMonth100Coupon: zenLiteCouponFirstMonth100.id,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -229,6 +236,7 @@ new sst.cloudflare.x.SolidStart("Console", {
|
||||
SALESFORCE_INSTANCE_URL,
|
||||
ZEN_BLACK_PRICE,
|
||||
ZEN_LITE_PRICE,
|
||||
new sst.Secret("ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES"),
|
||||
new sst.Secret("ZEN_LIMITS"),
|
||||
new sst.Secret("ZEN_SESSION_SECRET"),
|
||||
...ZEN_MODELS,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-0jwPCu2Lod433GPQLHN8eEkhfpPviDFfkFJmuvkRdlE=",
|
||||
"aarch64-linux": "sha256-Qi0IkGkaIBKZsPLTO8kaTbCVL0cEfVOm/Y/6VUVI9TY=",
|
||||
"aarch64-darwin": "sha256-1eZBBLgYVkjg5RYN/etR1Mb5UjU3VelElBB5ug5hQdc=",
|
||||
"x86_64-darwin": "sha256-jdXgA+kZb/foFHR40UiPif6rsA2GDVCCVHnJR3jBUGI="
|
||||
"x86_64-linux": "sha256-r1+AehuOGIOaaxfXkQGracT/6OdFRn5Ub8s7H+MeKFY=",
|
||||
"aarch64-linux": "sha256-WkMSRF/ZJLyzxNBjpiMR459C9G0NVOEw31tm8roPneA=",
|
||||
"aarch64-darwin": "sha256-Z127cxFpTl8Ml7PB3CG9TcCU08oYCPuk0FECK2MQ2CI=",
|
||||
"x86_64-darwin": "sha256-pkRoFtnVjyl+5fm+rrFyRnEwvptxylnFxPAcEv4ZOCg="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
|
||||
"dev:storybook": "bun --cwd packages/storybook storybook",
|
||||
"typecheck": "bun turbo typecheck",
|
||||
"postinstall": "bun run --cwd packages/opencode fix-node-pty",
|
||||
"prepare": "husky",
|
||||
"random": "echo 'Random script'",
|
||||
"hello": "echo 'Hello World!'",
|
||||
@@ -47,7 +48,7 @@
|
||||
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
||||
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
||||
"effect": "4.0.0-beta.43",
|
||||
"ai": "6.0.138",
|
||||
"ai": "6.0.149",
|
||||
"cross-spawn": "7.0.6",
|
||||
"hono": "4.10.7",
|
||||
"hono-openapi": "1.1.2",
|
||||
@@ -103,6 +104,7 @@
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"esbuild",
|
||||
"node-pty",
|
||||
"protobufjs",
|
||||
"tree-sitter",
|
||||
"tree-sitter-bash",
|
||||
@@ -116,8 +118,6 @@
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
||||
"@ai-sdk/provider-utils@4.0.21": "patches/@ai-sdk%2Fprovider-utils@4.0.21.patch",
|
||||
"@ai-sdk/anthropic@3.0.64": "patches/@ai-sdk%2Fanthropic@3.0.64.patch"
|
||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,6 +320,7 @@ export async function createTestProject(input?: { serverUrl?: string }) {
|
||||
execSync("git init", { cwd: root, stdio: "ignore" })
|
||||
await fs.writeFile(path.join(root, ".git", "opencode"), id)
|
||||
execSync("git config core.fsmonitor false", { cwd: root, stdio: "ignore" })
|
||||
execSync("git config commit.gpgsign false", { cwd: root, stdio: "ignore" })
|
||||
execSync("git add -A", { cwd: root, stdio: "ignore" })
|
||||
execSync('git -c user.name="e2e" -c user.email="e2e@example.com" commit -m "init" --allow-empty', {
|
||||
cwd: root,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ToolPart } from "@opencode-ai/sdk/v2/client"
|
||||
import { test, expect } from "../fixtures"
|
||||
import { withSession } from "../actions"
|
||||
import { closeDialog, openSettings, withSession } from "../actions"
|
||||
import { promptModelSelector, promptSelector, promptVariantSelector } from "../selectors"
|
||||
|
||||
const isBash = (part: unknown): part is ToolPart => {
|
||||
@@ -19,12 +19,15 @@ test("shell mode runs a command in the project directory", async ({ page, projec
|
||||
await withSession(project.sdk, `e2e shell ${Date.now()}`, async (session) => {
|
||||
project.trackSession(session.id)
|
||||
await project.gotoSession(session.id)
|
||||
const button = page.locator('[data-action="prompt-permissions"]').first()
|
||||
await expect(button).toBeVisible()
|
||||
if ((await button.getAttribute("aria-pressed")) !== "true") {
|
||||
await button.click()
|
||||
await expect(button).toHaveAttribute("aria-pressed", "true")
|
||||
const dialog = await openSettings(page)
|
||||
const toggle = dialog.locator('[data-action="settings-auto-accept-permissions"]').first()
|
||||
const input = toggle.locator('[data-slot="switch-input"]').first()
|
||||
await expect(toggle).toBeVisible()
|
||||
if ((await input.getAttribute("aria-checked")) !== "true") {
|
||||
await toggle.locator('[data-slot="switch-control"]').click()
|
||||
await expect(input).toHaveAttribute("aria-checked", "true")
|
||||
}
|
||||
await closeDialog(page, dialog)
|
||||
await project.shell(cmd)
|
||||
|
||||
await expect
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { seedSessionTask, withSession } from "../actions"
|
||||
import { test, expect } from "../fixtures"
|
||||
import { inputMatch } from "../prompt/mock"
|
||||
import { promptSelector } from "../selectors"
|
||||
|
||||
test("task tool child-session link does not trigger stale show errors", async ({ page, llm, project }) => {
|
||||
test.setTimeout(120_000)
|
||||
@@ -30,15 +29,33 @@ test("task tool child-session link does not trigger stale show errors", async ({
|
||||
|
||||
await project.gotoSession(session.id)
|
||||
|
||||
const link = page
|
||||
.locator("a.subagent-link")
|
||||
const header = page.locator("[data-session-title]")
|
||||
await expect(header.getByRole("button", { name: "More options" })).toBeVisible({ timeout: 30_000 })
|
||||
|
||||
const card = page
|
||||
.locator('[data-component="task-tool-card"]')
|
||||
.filter({ hasText: /open child session/i })
|
||||
.first()
|
||||
await expect(link).toBeVisible({ timeout: 30_000 })
|
||||
await link.click()
|
||||
await expect(card).toBeVisible({ timeout: 30_000 })
|
||||
await card.click()
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/session/${child.sessionID}(?:[/?#]|$)`), { timeout: 30_000 })
|
||||
await expect(page.locator(promptSelector)).toBeVisible({ timeout: 30_000 })
|
||||
await expect(header.locator('[data-slot="session-title-parent"]')).toHaveText(session.title)
|
||||
await expect(header.locator('[data-slot="session-title-child"]')).toHaveText(taskInput.description)
|
||||
await expect(header.locator('[data-slot="session-title-separator"]')).toHaveText("/")
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
header.locator('[data-slot="session-title-separator"]').evaluate((el) => ({
|
||||
left: getComputedStyle(el).paddingLeft,
|
||||
right: getComputedStyle(el).paddingRight,
|
||||
})),
|
||||
{ timeout: 30_000 },
|
||||
)
|
||||
.toEqual({ left: "8px", right: "8px" })
|
||||
await expect(header.getByRole("button", { name: "More options" })).toHaveCount(0)
|
||||
await expect(page.getByText("Subagent sessions cannot be prompted.")).toBeVisible({ timeout: 30_000 })
|
||||
await expect(page.getByRole("button", { name: "Back to main session." })).toBeVisible({ timeout: 30_000 })
|
||||
await expect.poll(() => errs, { timeout: 5_000 }).toEqual([])
|
||||
})
|
||||
} finally {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
type ComposerProbeState,
|
||||
type ComposerWindow,
|
||||
} from "../../src/testing/session-composer"
|
||||
import { cleanupSession, clearSessionDockSeed, seedSessionQuestion } from "../actions"
|
||||
import { cleanupSession, clearSessionDockSeed, closeDialog, openSettings, seedSessionQuestion } from "../actions"
|
||||
import {
|
||||
permissionDockSelector,
|
||||
promptSelector,
|
||||
@@ -65,12 +65,14 @@ async function clearPermissionDock(page: any, label: RegExp) {
|
||||
}
|
||||
|
||||
async function setAutoAccept(page: any, enabled: boolean) {
|
||||
const button = page.locator('[data-action="prompt-permissions"]').first()
|
||||
await expect(button).toBeVisible()
|
||||
const pressed = (await button.getAttribute("aria-pressed")) === "true"
|
||||
if (pressed === enabled) return
|
||||
await button.click()
|
||||
await expect(button).toHaveAttribute("aria-pressed", enabled ? "true" : "false")
|
||||
const dialog = await openSettings(page)
|
||||
const toggle = dialog.locator('[data-action="settings-auto-accept-permissions"]').first()
|
||||
const input = toggle.locator('[data-slot="switch-input"]').first()
|
||||
await expect(toggle).toBeVisible()
|
||||
const checked = (await input.getAttribute("aria-checked")) === "true"
|
||||
if (checked !== enabled) await toggle.locator('[data-slot="switch-control"]').click()
|
||||
await expect(input).toHaveAttribute("aria-checked", enabled ? "true" : "false")
|
||||
await closeDialog(page, dialog)
|
||||
}
|
||||
|
||||
async function expectQuestionBlocked(page: any) {
|
||||
@@ -277,6 +279,7 @@ test("default dock shows prompt input", async ({ page, project }) => {
|
||||
|
||||
await expect(page.locator(sessionComposerDockSelector)).toBeVisible()
|
||||
await expect(page.locator(promptSelector)).toBeVisible()
|
||||
await expect(page.locator('[data-action="prompt-permissions"]')).toHaveCount(0)
|
||||
await expect(page.locator(questionDockSelector)).toHaveCount(0)
|
||||
await expect(page.locator(permissionDockSelector)).toHaveCount(0)
|
||||
|
||||
@@ -290,10 +293,6 @@ test("default dock shows prompt input", async ({ page, project }) => {
|
||||
test("auto-accept toggle works before first submit", async ({ page, project }) => {
|
||||
await project.open()
|
||||
|
||||
const button = page.locator('[data-action="prompt-permissions"]').first()
|
||||
await expect(button).toBeVisible()
|
||||
await expect(button).toHaveAttribute("aria-pressed", "false")
|
||||
|
||||
await setAutoAccept(page, true)
|
||||
await setAutoAccept(page, false)
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.17",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
@@ -15,6 +15,7 @@
|
||||
"build": "vite build",
|
||||
"serve": "vite preview",
|
||||
"test": "bun run test:unit",
|
||||
"test:ci": "mkdir -p .artifacts/unit && bun test --preload ./happydom.ts ./src --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml",
|
||||
"test:unit": "bun test --preload ./happydom.ts ./src",
|
||||
"test:unit:watch": "bun test --watch --preload ./happydom.ts ./src",
|
||||
"test:e2e": "playwright test",
|
||||
|
||||
@@ -87,7 +87,7 @@ const runnerEnv = {
|
||||
|
||||
let seed: ReturnType<typeof Bun.spawn> | undefined
|
||||
let runner: ReturnType<typeof Bun.spawn> | undefined
|
||||
let server: { stop: () => Promise<void> | void } | undefined
|
||||
let server: { stop: (close?: boolean) => Promise<void> | void } | undefined
|
||||
let inst: { Instance: { disposeAll: () => Promise<void> | void } } | undefined
|
||||
let cleaned = false
|
||||
|
||||
@@ -100,7 +100,7 @@ const cleanup = async () => {
|
||||
|
||||
const jobs = [
|
||||
inst?.Instance.disposeAll(),
|
||||
server?.stop(),
|
||||
typeof server?.stop === "function" ? server.stop() : undefined,
|
||||
keepSandbox ? undefined : fs.rm(sandbox, { recursive: true, force: true }),
|
||||
].filter(Boolean)
|
||||
await Promise.allSettled(jobs)
|
||||
@@ -158,7 +158,7 @@ try {
|
||||
|
||||
const servermod = await import("../../opencode/src/server/server")
|
||||
inst = await import("../../opencode/src/project/instance")
|
||||
server = servermod.Server.listen({ port: serverPort, hostname: "127.0.0.1" })
|
||||
server = await servermod.Server.listen({ port: serverPort, hostname: "127.0.0.1" })
|
||||
console.log(`opencode server listening on http://127.0.0.1:${serverPort}`)
|
||||
|
||||
await waitForHealth(`http://127.0.0.1:${serverPort}/global/health`)
|
||||
|
||||
@@ -1079,17 +1079,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
if (!id) return permission.isAutoAcceptingDirectory(sdk.directory)
|
||||
return permission.isAutoAccepting(id, sdk.directory)
|
||||
})
|
||||
const acceptLabel = createMemo(() =>
|
||||
language.t(accepting() ? "command.permissions.autoaccept.disable" : "command.permissions.autoaccept.enable"),
|
||||
)
|
||||
const toggleAccept = () => {
|
||||
if (!params.id) {
|
||||
permission.toggleAutoAcceptDirectory(sdk.directory)
|
||||
return
|
||||
}
|
||||
|
||||
permission.toggleAutoAccept(params.id, sdk.directory)
|
||||
}
|
||||
|
||||
const { abort, handleSubmit } = createPromptSubmit({
|
||||
info,
|
||||
@@ -1333,11 +1322,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
onMouseDown={(e) => {
|
||||
const target = e.target
|
||||
if (!(target instanceof HTMLElement)) return
|
||||
if (
|
||||
target.closest(
|
||||
'[data-action="prompt-attach"], [data-action="prompt-submit"], [data-action="prompt-permissions"]',
|
||||
)
|
||||
) {
|
||||
if (target.closest('[data-action="prompt-attach"], [data-action="prompt-submit"]')) {
|
||||
return
|
||||
}
|
||||
editorRef?.focus()
|
||||
@@ -1597,28 +1582,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
</Show>
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={8}
|
||||
title={acceptLabel()}
|
||||
keybind={command.keybind("permissions.autoaccept")}
|
||||
>
|
||||
<Button
|
||||
data-action="prompt-permissions"
|
||||
variant="ghost"
|
||||
onClick={toggleAccept}
|
||||
classList={{
|
||||
"h-7 w-7 p-0 shrink-0 flex items-center justify-center": true,
|
||||
"text-text-base": !accepting(),
|
||||
"hover:bg-surface-success-base": accepting(),
|
||||
}}
|
||||
style={control()}
|
||||
aria-label={acceptLabel()}
|
||||
aria-pressed={accepting()}
|
||||
>
|
||||
<Icon name="shield" size="small" classList={{ "text-icon-success-base": accepting() }} />
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Component, For, Show } from "solid-js"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import type { ImageAttachmentPart } from "@/context/prompt"
|
||||
|
||||
type PromptImageAttachmentsProps = {
|
||||
@@ -22,34 +23,36 @@ export const PromptImageAttachments: Component<PromptImageAttachmentsProps> = (p
|
||||
<div class="flex flex-wrap gap-2 px-3 pt-3">
|
||||
<For each={props.attachments}>
|
||||
{(attachment) => (
|
||||
<div class="relative group">
|
||||
<Show
|
||||
when={attachment.mime.startsWith("image/")}
|
||||
fallback={
|
||||
<div class={fallbackClass}>
|
||||
<Icon name="folder" class="size-6 text-text-weak" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={attachment.dataUrl}
|
||||
alt={attachment.filename}
|
||||
class={imageClass}
|
||||
onClick={() => props.onOpen(attachment)}
|
||||
/>
|
||||
</Show>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.onRemove(attachment.id)}
|
||||
class={removeClass}
|
||||
aria-label={props.removeLabel}
|
||||
>
|
||||
<Icon name="close" class="size-3 text-text-weak" />
|
||||
</button>
|
||||
<div class={nameClass}>
|
||||
<span class="text-10-regular text-white truncate block">{attachment.filename}</span>
|
||||
<Tooltip value={attachment.filename} placement="top" contentClass="break-all">
|
||||
<div class="relative group">
|
||||
<Show
|
||||
when={attachment.mime.startsWith("image/")}
|
||||
fallback={
|
||||
<div class={fallbackClass}>
|
||||
<Icon name="folder" class="size-6 text-text-weak" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={attachment.dataUrl}
|
||||
alt={attachment.filename}
|
||||
class={imageClass}
|
||||
onClick={() => props.onOpen(attachment)}
|
||||
/>
|
||||
</Show>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.onRemove(attachment.id)}
|
||||
class={removeClass}
|
||||
aria-label={props.removeLabel}
|
||||
>
|
||||
<Icon name="close" class="size-3 text-text-weak" />
|
||||
</button>
|
||||
<div class={nameClass}>
|
||||
<span class="text-10-regular text-white truncate block">{attachment.filename}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
@@ -146,7 +146,7 @@ beforeAll(async () => {
|
||||
add: (value: {
|
||||
directory?: string
|
||||
sessionID?: string
|
||||
message: { agent: string; model: { providerID: string; modelID: string }; variant?: string }
|
||||
message: { agent: string; model: { providerID: string; modelID: string; variant?: string } }
|
||||
}) => {
|
||||
optimistic.push(value)
|
||||
optimisticSeeded.push(
|
||||
@@ -310,8 +310,7 @@ describe("prompt submit worktree selection", () => {
|
||||
expect(optimistic[0]).toMatchObject({
|
||||
message: {
|
||||
agent: "agent",
|
||||
model: { providerID: "provider", modelID: "model" },
|
||||
variant: "high",
|
||||
model: { providerID: "provider", modelID: "model", variant: "high" },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -121,8 +121,7 @@ export async function sendFollowupDraft(input: FollowupSendInput) {
|
||||
role: "user",
|
||||
time: { created: Date.now() },
|
||||
agent: input.draft.agent,
|
||||
model: input.draft.model,
|
||||
variant: input.draft.variant,
|
||||
model: { ...input.draft.model, variant: input.draft.variant },
|
||||
}
|
||||
|
||||
const add = () =>
|
||||
|
||||
@@ -8,7 +8,9 @@ import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { usePermission } from "@/context/permission"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import {
|
||||
monoDefault,
|
||||
@@ -19,6 +21,7 @@ import {
|
||||
sansInput,
|
||||
useSettings,
|
||||
} from "@/context/settings"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
import { playSoundById, SOUND_OPTIONS } from "@/utils/sound"
|
||||
import { Link } from "./link"
|
||||
import { SettingsList } from "./settings-list"
|
||||
@@ -64,7 +67,9 @@ const playDemoSound = (id: string | undefined) => {
|
||||
export const SettingsGeneral: Component = () => {
|
||||
const theme = useTheme()
|
||||
const language = useLanguage()
|
||||
const permission = usePermission()
|
||||
const platform = usePlatform()
|
||||
const params = useParams()
|
||||
const settings = useSettings()
|
||||
|
||||
onMount(() => {
|
||||
@@ -76,6 +81,31 @@ export const SettingsGeneral: Component = () => {
|
||||
})
|
||||
|
||||
const linux = createMemo(() => platform.platform === "desktop" && platform.os === "linux")
|
||||
const dir = createMemo(() => decode64(params.dir))
|
||||
const accepting = createMemo(() => {
|
||||
const value = dir()
|
||||
if (!value) return false
|
||||
if (!params.id) return permission.isAutoAcceptingDirectory(value)
|
||||
return permission.isAutoAccepting(params.id, value)
|
||||
})
|
||||
|
||||
const toggleAccept = (checked: boolean) => {
|
||||
const value = dir()
|
||||
if (!value) return
|
||||
|
||||
if (!params.id) {
|
||||
if (permission.isAutoAcceptingDirectory(value) === checked) return
|
||||
permission.toggleAutoAcceptDirectory(value)
|
||||
return
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
permission.enableAutoAccept(params.id, value)
|
||||
return
|
||||
}
|
||||
|
||||
permission.disableAutoAccept(params.id, value)
|
||||
}
|
||||
|
||||
const check = () => {
|
||||
if (!platform.checkUpdate) return
|
||||
@@ -201,6 +231,15 @@ export const SettingsGeneral: Component = () => {
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
title={language.t("command.permissions.autoaccept.enable")}
|
||||
description={language.t("toast.permissions.autoaccept.on.description")}
|
||||
>
|
||||
<div data-action="settings-auto-accept-permissions">
|
||||
<Switch checked={accepting()} disabled={!dir()} onChange={toggleAccept} />
|
||||
</div>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow
|
||||
title={language.t("settings.general.row.reasoningSummaries.title")}
|
||||
description={language.t("settings.general.row.reasoningSummaries.description")}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } fro
|
||||
import { useSDK } from "./sdk"
|
||||
import { useSync } from "./sync"
|
||||
|
||||
export type ModelKey = { providerID: string; modelID: string }
|
||||
export type ModelKey = { providerID: string; modelID: string; variant?: string }
|
||||
|
||||
type State = {
|
||||
agent?: string
|
||||
@@ -373,7 +373,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
handoff.set(handoffKey(dir, session), next)
|
||||
setStore("draft", undefined)
|
||||
},
|
||||
restore(msg: { sessionID: string; agent: string; model: ModelKey; variant?: string }) {
|
||||
restore(msg: { sessionID: string; agent: string; model: ModelKey }) {
|
||||
const session = id()
|
||||
if (!session) return
|
||||
if (msg.sessionID !== session) return
|
||||
@@ -383,7 +383,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
setSaved("session", session, {
|
||||
agent: msg.agent,
|
||||
model: msg.model,
|
||||
variant: msg.variant ?? null,
|
||||
variant: msg.model.variant ?? null,
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
@@ -416,8 +416,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
role: "user",
|
||||
time: { created: Date.now() },
|
||||
agent: input.agent,
|
||||
model: input.model,
|
||||
variant: input.variant,
|
||||
model: { ...input.model, variant: input.variant },
|
||||
}
|
||||
const [, setStore] = target()
|
||||
setOptimistic(sdk.directory, input.sessionID, { message, parts: input.parts })
|
||||
|
||||
@@ -238,6 +238,8 @@ export const dict = {
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.normal": "Prompt",
|
||||
"prompt.mode.shell.exit": "esc to exit",
|
||||
"session.child.promptDisabled": "Subagent sessions cannot be prompted.",
|
||||
"session.child.backToParent": "Back to main session.",
|
||||
|
||||
"prompt.example.1": "Fix a TODO in the codebase",
|
||||
"prompt.example.2": "What is the tech stack of this project?",
|
||||
|
||||
@@ -1,6 +1,46 @@
|
||||
@import "@opencode-ai/ui/styles/tailwind";
|
||||
|
||||
@layer components {
|
||||
@keyframes session-progress-whip {
|
||||
0% {
|
||||
clip-path: inset(0 100% 0 0 round 999px);
|
||||
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
|
||||
48% {
|
||||
clip-path: inset(0 0 0 0 round 999px);
|
||||
animation-timing-function: cubic-bezier(0.65, 0, 0.35, 1);
|
||||
}
|
||||
|
||||
100% {
|
||||
clip-path: inset(0 0 0 100% round 999px);
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="session-progress"] {
|
||||
position: absolute;
|
||||
inset: 0 0 auto;
|
||||
height: 2px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
opacity: 1;
|
||||
transition: opacity 220ms ease-out;
|
||||
}
|
||||
|
||||
[data-component="session-progress"][data-state="hiding"] {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
[data-component="session-progress-bar"] {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 999px;
|
||||
background: var(--session-progress-color);
|
||||
clip-path: inset(0 100% 0 0 round 999px);
|
||||
animation: session-progress-whip var(--session-progress-ms, 1800ms) infinite;
|
||||
will-change: clip-path;
|
||||
}
|
||||
|
||||
[data-component="getting-started"] {
|
||||
container-type: inline-size;
|
||||
container-name: getting-started;
|
||||
|
||||
@@ -150,7 +150,6 @@ export default function Layout(props: ParentProps) {
|
||||
const [state, setState] = createStore({
|
||||
autoselect: !initialDirectory,
|
||||
busyWorkspaces: {} as Record<string, boolean>,
|
||||
hoverSession: undefined as string | undefined,
|
||||
hoverProject: undefined as string | undefined,
|
||||
scrollSessionKey: undefined as string | undefined,
|
||||
nav: undefined as HTMLElement | undefined,
|
||||
@@ -194,7 +193,6 @@ export default function Layout(props: ParentProps) {
|
||||
onActivate: (directory) => {
|
||||
globalSync.child(directory)
|
||||
setState("hoverProject", directory)
|
||||
setState("hoverSession", undefined)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -231,7 +229,6 @@ export default function Layout(props: ParentProps) {
|
||||
aim.reset()
|
||||
}
|
||||
const clearHoverProjectSoon = () => queueMicrotask(() => setHoverProject(undefined))
|
||||
const setHoverSession = (id: string | undefined) => setState("hoverSession", id)
|
||||
|
||||
const disarm = () => {
|
||||
if (navLeave.current === undefined) return
|
||||
@@ -241,7 +238,6 @@ export default function Layout(props: ParentProps) {
|
||||
|
||||
const reset = () => {
|
||||
disarm()
|
||||
setState("hoverSession", undefined)
|
||||
setHoverProject(undefined)
|
||||
}
|
||||
|
||||
@@ -252,7 +248,6 @@ export default function Layout(props: ParentProps) {
|
||||
navLeave.current = window.setTimeout(() => {
|
||||
navLeave.current = undefined
|
||||
setHoverProject(undefined)
|
||||
setState("hoverSession", undefined)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
@@ -1972,9 +1967,6 @@ export default function Layout(props: ParentProps) {
|
||||
navList: currentSessions,
|
||||
sidebarExpanded,
|
||||
sidebarHovering,
|
||||
nav: () => state.nav,
|
||||
hoverSession: () => state.hoverSession,
|
||||
setHoverSession,
|
||||
clearHoverProjectSoon,
|
||||
prefetchSession,
|
||||
archiveSession,
|
||||
@@ -2003,7 +1995,6 @@ export default function Layout(props: ParentProps) {
|
||||
sidebarOpened: () => layout.sidebar.opened(),
|
||||
sidebarHovering,
|
||||
hoverProject: () => state.hoverProject,
|
||||
nav: () => state.nav,
|
||||
onProjectMouseEnter: (worktree, event) => aim.enter(worktree, event),
|
||||
onProjectMouseLeave: (worktree) => aim.leave(worktree),
|
||||
onProjectFocus: (worktree) => aim.activate(worktree),
|
||||
@@ -2022,15 +2013,10 @@ export default function Layout(props: ParentProps) {
|
||||
sessionProps: {
|
||||
navList: currentSessions,
|
||||
sidebarExpanded,
|
||||
sidebarHovering,
|
||||
nav: () => state.nav,
|
||||
hoverSession: () => state.hoverSession,
|
||||
setHoverSession,
|
||||
clearHoverProjectSoon,
|
||||
prefetchSession,
|
||||
archiveSession,
|
||||
},
|
||||
setHoverSession,
|
||||
}
|
||||
|
||||
const SidebarPanel = (panelProps: {
|
||||
@@ -2041,7 +2027,6 @@ export default function Layout(props: ParentProps) {
|
||||
const project = panelProps.project
|
||||
const merged = createMemo(() => panelProps.mobile || (panelProps.merged ?? layout.sidebar.opened()))
|
||||
const hover = createMemo(() => !panelProps.mobile && panelProps.merged === false && !layout.sidebar.opened())
|
||||
const popover = createMemo(() => !!panelProps.mobile || panelProps.merged === false || layout.sidebar.opened())
|
||||
const empty = createMemo(() => !params.dir && layout.projects.list().length === 0)
|
||||
const projectName = createMemo(() => {
|
||||
const item = project()
|
||||
@@ -2243,7 +2228,6 @@ export default function Layout(props: ParentProps) {
|
||||
project={project()!}
|
||||
sortNow={sortNow}
|
||||
mobile={panelProps.mobile}
|
||||
popover={popover()}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
@@ -2288,7 +2272,6 @@ export default function Layout(props: ParentProps) {
|
||||
project={project()!}
|
||||
sortNow={sortNow}
|
||||
mobile={panelProps.mobile}
|
||||
popover={popover()}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "./deep-links"
|
||||
import { type Session } from "@opencode-ai/sdk/v2/client"
|
||||
import {
|
||||
childSessionOnPath,
|
||||
displayName,
|
||||
effectiveWorkspaceOrder,
|
||||
errorMessage,
|
||||
@@ -198,6 +199,19 @@ describe("layout workspace helpers", () => {
|
||||
expect(result?.id).toBe("root")
|
||||
})
|
||||
|
||||
test("finds the direct child on the active session path", () => {
|
||||
const list = [
|
||||
session({ id: "root", directory: "/workspace" }),
|
||||
session({ id: "child", directory: "/workspace", parentID: "root" }),
|
||||
session({ id: "leaf", directory: "/workspace", parentID: "child" }),
|
||||
]
|
||||
|
||||
expect(childSessionOnPath(list, "root", "leaf")?.id).toBe("child")
|
||||
expect(childSessionOnPath(list, "child", "leaf")?.id).toBe("leaf")
|
||||
expect(childSessionOnPath(list, "root", "root")).toBeUndefined()
|
||||
expect(childSessionOnPath(list, "root", "other")).toBeUndefined()
|
||||
})
|
||||
|
||||
test("formats fallback project display name", () => {
|
||||
expect(displayName({ worktree: "/tmp/app" })).toBe("app")
|
||||
expect(displayName({ worktree: "/tmp/app", name: "My App" })).toBe("My App")
|
||||
|
||||
@@ -46,18 +46,17 @@ export function hasProjectPermissions<T>(
|
||||
return Object.values(request ?? {}).some((list) => list?.some(include))
|
||||
}
|
||||
|
||||
export const childMapByParent = (sessions: Session[] | undefined) => {
|
||||
const map = new Map<string, string[]>()
|
||||
for (const session of sessions ?? []) {
|
||||
if (!session.parentID) continue
|
||||
const existing = map.get(session.parentID)
|
||||
if (existing) {
|
||||
existing.push(session.id)
|
||||
continue
|
||||
}
|
||||
map.set(session.parentID, [session.id])
|
||||
export const childSessionOnPath = (sessions: Session[] | undefined, rootID: string, activeID?: string) => {
|
||||
if (!activeID || activeID === rootID) return
|
||||
const map = new Map((sessions ?? []).map((session) => [session.id, session]))
|
||||
let id = activeID
|
||||
|
||||
while (id) {
|
||||
const session = map.get(id)
|
||||
if (!session?.parentID) return
|
||||
if (session.parentID === rootID) return session
|
||||
id = session.parentID
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
export const displayName = (project: { name?: string; worktree: string }) =>
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import type { Message, Session, TextPart, UserMessage } from "@opencode-ai/sdk/v2/client"
|
||||
import type { Session } from "@opencode-ai/sdk/v2/client"
|
||||
import { Avatar } from "@opencode-ai/ui/avatar"
|
||||
import { HoverCard } from "@opencode-ai/ui/hover-card"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { MessageNav } from "@opencode-ai/ui/message-nav"
|
||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { base64Encode } from "@opencode-ai/util/encode"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { A, useNavigate, useParams } from "@solidjs/router"
|
||||
import { type Accessor, createMemo, For, type JSX, Match, onCleanup, Show, Switch } from "solid-js"
|
||||
import { A, useParams } from "@solidjs/router"
|
||||
import { type Accessor, createMemo, For, type JSX, Match, Show, Switch } from "solid-js"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { getAvatarColors, type LocalProject, useLayout } from "@/context/layout"
|
||||
@@ -18,7 +15,7 @@ import { usePermission } from "@/context/permission"
|
||||
import { messageAgentColor } from "@/utils/agent"
|
||||
import { sessionTitle } from "@/utils/session-title"
|
||||
import { sessionPermissionRequest } from "../session/composer/session-request-tree"
|
||||
import { hasProjectPermissions } from "./helpers"
|
||||
import { childSessionOnPath, hasProjectPermissions } from "./helpers"
|
||||
|
||||
const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
|
||||
|
||||
@@ -39,6 +36,7 @@ export const ProjectIcon = (props: { project: LocalProject; class?: string; noti
|
||||
)
|
||||
const notify = createMemo(() => props.notify && (hasPermissions() || unseenCount() > 0))
|
||||
const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
|
||||
|
||||
return (
|
||||
<div class={`relative size-8 shrink-0 rounded ${props.class ?? ""}`}>
|
||||
<div class="size-full rounded overflow-clip">
|
||||
@@ -73,13 +71,10 @@ export type SessionItemProps = {
|
||||
slug: string
|
||||
mobile?: boolean
|
||||
dense?: boolean
|
||||
popover?: boolean
|
||||
children: Map<string, string[]>
|
||||
showTooltip?: boolean
|
||||
showChild?: boolean
|
||||
level?: number
|
||||
sidebarExpanded: Accessor<boolean>
|
||||
sidebarHovering: Accessor<boolean>
|
||||
nav: Accessor<HTMLElement | undefined>
|
||||
hoverSession: Accessor<string | undefined>
|
||||
setHoverSession: (id: string | undefined) => void
|
||||
clearHoverProjectSoon: () => void
|
||||
prefetchSession: (session: Session, priority?: "high" | "low") => void
|
||||
archiveSession: (session: Session) => Promise<void>
|
||||
@@ -95,116 +90,52 @@ const SessionRow = (props: {
|
||||
hasPermissions: Accessor<boolean>
|
||||
hasError: Accessor<boolean>
|
||||
unseenCount: Accessor<number>
|
||||
setHoverSession: (id: string | undefined) => void
|
||||
clearHoverProjectSoon: () => void
|
||||
sidebarOpened: Accessor<boolean>
|
||||
warmHover: () => void
|
||||
warmPress: () => void
|
||||
warmFocus: () => void
|
||||
cancelHoverPrefetch: () => void
|
||||
}) => {
|
||||
}): JSX.Element => {
|
||||
const title = () => sessionTitle(props.session.title)
|
||||
|
||||
return (
|
||||
<A
|
||||
href={`/${props.slug}/session/${props.session.id}`}
|
||||
class={`flex items-center gap-1 min-w-0 w-full text-left focus:outline-none ${props.dense ? "py-0.5" : "py-1"}`}
|
||||
class={`flex items-center gap-2 min-w-0 w-full text-left focus:outline-none ${props.dense ? "py-0.5" : "py-1"}`}
|
||||
onPointerDown={props.warmPress}
|
||||
onPointerEnter={props.warmHover}
|
||||
onPointerLeave={props.cancelHoverPrefetch}
|
||||
onFocus={props.warmFocus}
|
||||
onClick={() => {
|
||||
props.setHoverSession(undefined)
|
||||
if (props.sidebarOpened()) return
|
||||
props.clearHoverProjectSoon()
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="shrink-0 size-6 flex items-center justify-center"
|
||||
style={{ color: props.tint() ?? "var(--icon-interactive-base)" }}
|
||||
>
|
||||
<Switch fallback={<Icon name="dash" size="small" class="text-icon-weak" />}>
|
||||
<Match when={props.isWorking()}>
|
||||
<Spinner class="size-[15px]" />
|
||||
</Match>
|
||||
<Match when={props.hasPermissions()}>
|
||||
<div class="size-1.5 rounded-full bg-surface-warning-strong" />
|
||||
</Match>
|
||||
<Match when={props.hasError()}>
|
||||
<div class="size-1.5 rounded-full bg-text-diff-delete-base" />
|
||||
</Match>
|
||||
<Match when={props.unseenCount() > 0}>
|
||||
<div class="size-1.5 rounded-full bg-text-interactive-base" />
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
<Show when={props.isWorking() || props.hasPermissions() || props.hasError() || props.unseenCount() > 0}>
|
||||
<div
|
||||
class="shrink-0 size-6 flex items-center justify-center"
|
||||
style={{ color: props.tint() ?? "var(--icon-interactive-base)" }}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={props.isWorking()}>
|
||||
<Spinner class="size-[15px]" />
|
||||
</Match>
|
||||
<Match when={props.hasPermissions()}>
|
||||
<div class="size-1.5 rounded-full bg-surface-warning-strong" />
|
||||
</Match>
|
||||
<Match when={props.hasError()}>
|
||||
<div class="size-1.5 rounded-full bg-text-diff-delete-base" />
|
||||
</Match>
|
||||
<Match when={props.unseenCount() > 0}>
|
||||
<div class="size-1.5 rounded-full bg-text-interactive-base" />
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</Show>
|
||||
<span class="text-14-regular text-text-strong min-w-0 flex-1 truncate">{title()}</span>
|
||||
</A>
|
||||
)
|
||||
}
|
||||
|
||||
const SessionHoverPreview = (props: {
|
||||
mobile?: boolean
|
||||
nav: Accessor<HTMLElement | undefined>
|
||||
hoverSession: Accessor<string | undefined>
|
||||
session: Session
|
||||
sidebarHovering: Accessor<boolean>
|
||||
hoverReady: Accessor<boolean>
|
||||
hoverMessages: Accessor<UserMessage[] | undefined>
|
||||
language: ReturnType<typeof useLanguage>
|
||||
isActive: Accessor<boolean>
|
||||
slug: string
|
||||
setHoverSession: (id: string | undefined) => void
|
||||
messageLabel: (message: Message) => string | undefined
|
||||
onMessageSelect: (message: Message) => void
|
||||
trigger: JSX.Element
|
||||
}): JSX.Element => {
|
||||
let ref: HTMLDivElement | undefined
|
||||
|
||||
return (
|
||||
<HoverCard
|
||||
openDelay={1000}
|
||||
closeDelay={props.sidebarHovering() ? 600 : 0}
|
||||
placement="right-start"
|
||||
gutter={16}
|
||||
shift={-2}
|
||||
trigger={
|
||||
<div ref={ref} class="min-w-0 w-full">
|
||||
{props.trigger}
|
||||
</div>
|
||||
}
|
||||
open={props.hoverSession() === props.session.id}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
props.setHoverSession(undefined)
|
||||
return
|
||||
}
|
||||
if (!ref?.matches(":hover")) return
|
||||
props.setHoverSession(props.session.id)
|
||||
}}
|
||||
>
|
||||
<Show
|
||||
when={props.hoverReady()}
|
||||
fallback={<div class="text-12-regular text-text-weak">{props.language.t("session.messages.loading")}</div>}
|
||||
>
|
||||
<div class="overflow-y-auto overflow-x-hidden max-h-72 h-full">
|
||||
<MessageNav
|
||||
messages={props.hoverMessages() ?? []}
|
||||
current={undefined}
|
||||
getLabel={props.messageLabel}
|
||||
onMessageSelect={props.onMessageSelect}
|
||||
size="normal"
|
||||
class="w-60"
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</HoverCard>
|
||||
)
|
||||
}
|
||||
|
||||
export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
||||
const params = useParams()
|
||||
const navigate = useNavigate()
|
||||
const layout = useLayout()
|
||||
const language = useLanguage()
|
||||
const notification = useNotification()
|
||||
@@ -234,18 +165,13 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
||||
)
|
||||
})
|
||||
|
||||
const tint = createMemo(() => {
|
||||
return messageAgentColor(sessionStore.message[props.session.id], sessionStore.agent)
|
||||
const tint = createMemo(() => messageAgentColor(sessionStore.message[props.session.id], sessionStore.agent))
|
||||
const tooltip = createMemo(() => props.showTooltip ?? (props.mobile || !props.sidebarExpanded()))
|
||||
const currentChild = createMemo(() => {
|
||||
if (!props.showChild) return
|
||||
return childSessionOnPath(sessionStore.session, props.session.id, params.id)
|
||||
})
|
||||
|
||||
const hoverMessages = createMemo(() =>
|
||||
sessionStore.message[props.session.id]?.filter((message): message is UserMessage => message.role === "user"),
|
||||
)
|
||||
const hoverReady = createMemo(() => hoverMessages() !== undefined)
|
||||
const hoverAllowed = createMemo(() => !props.mobile && props.sidebarExpanded())
|
||||
const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed())
|
||||
const isActive = createMemo(() => props.session.id === params.id)
|
||||
|
||||
const warm = (span: number, priority: "high" | "low") => {
|
||||
const nav = props.navList?.()
|
||||
const list = nav?.some((item) => item.id === props.session.id && item.directory === props.session.directory)
|
||||
@@ -266,30 +192,6 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
||||
}
|
||||
}
|
||||
|
||||
const hoverPrefetch = {
|
||||
current: undefined as ReturnType<typeof setTimeout> | undefined,
|
||||
}
|
||||
const cancelHoverPrefetch = () => {
|
||||
if (hoverPrefetch.current === undefined) return
|
||||
clearTimeout(hoverPrefetch.current)
|
||||
hoverPrefetch.current = undefined
|
||||
}
|
||||
const scheduleHoverPrefetch = () => {
|
||||
warm(1, "high")
|
||||
if (hoverPrefetch.current !== undefined) return
|
||||
hoverPrefetch.current = setTimeout(() => {
|
||||
hoverPrefetch.current = undefined
|
||||
warm(2, "low")
|
||||
}, 80)
|
||||
}
|
||||
|
||||
onCleanup(cancelHoverPrefetch)
|
||||
|
||||
const messageLabel = (message: Message) => {
|
||||
const parts = sessionStore.part[message.id] ?? []
|
||||
const text = parts.find((part): part is TextPart => part?.type === "text" && !part.synthetic && !part.ignored)
|
||||
return text?.text
|
||||
}
|
||||
const item = (
|
||||
<SessionRow
|
||||
session={props.session}
|
||||
@@ -301,86 +203,74 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
||||
hasPermissions={hasPermissions}
|
||||
hasError={hasError}
|
||||
unseenCount={unseenCount}
|
||||
setHoverSession={props.setHoverSession}
|
||||
clearHoverProjectSoon={props.clearHoverProjectSoon}
|
||||
sidebarOpened={layout.sidebar.opened}
|
||||
warmHover={scheduleHoverPrefetch}
|
||||
warmPress={() => warm(2, "high")}
|
||||
warmFocus={() => warm(2, "high")}
|
||||
cancelHoverPrefetch={cancelHoverPrefetch}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
data-session-id={props.session.id}
|
||||
class="group/session relative w-full min-w-0 rounded-md cursor-default pl-2 pr-3 transition-colors
|
||||
hover:bg-surface-raised-base-hover [&:has(:focus-visible)]:bg-surface-raised-base-hover has-[[data-expanded]]:bg-surface-raised-base-hover has-[.active]:bg-surface-base-active"
|
||||
>
|
||||
<div class="flex min-w-0 items-center gap-1">
|
||||
<div class="min-w-0 flex-1">
|
||||
<Show
|
||||
when={hoverEnabled()}
|
||||
fallback={
|
||||
<Tooltip
|
||||
placement={props.mobile ? "bottom" : "right"}
|
||||
value={sessionTitle(props.session.title)}
|
||||
gutter={10}
|
||||
class="min-w-0 w-full"
|
||||
>
|
||||
{item}
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<SessionHoverPreview
|
||||
mobile={props.mobile}
|
||||
nav={props.nav}
|
||||
hoverSession={props.hoverSession}
|
||||
session={props.session}
|
||||
sidebarHovering={props.sidebarHovering}
|
||||
hoverReady={hoverReady}
|
||||
hoverMessages={hoverMessages}
|
||||
language={language}
|
||||
isActive={isActive}
|
||||
slug={props.slug}
|
||||
setHoverSession={props.setHoverSession}
|
||||
messageLabel={messageLabel}
|
||||
onMessageSelect={(message) => {
|
||||
if (!isActive())
|
||||
layout.pendingMessage.set(`${base64Encode(props.session.directory)}/${props.session.id}`, message.id)
|
||||
<>
|
||||
<div
|
||||
data-session-id={props.session.id}
|
||||
class="group/session relative w-full min-w-0 rounded-md cursor-default pr-3 transition-colors hover:bg-surface-raised-base-hover [&:has(:focus-visible)]:bg-surface-raised-base-hover has-[[data-expanded]]:bg-surface-raised-base-hover has-[.active]:bg-surface-base-active"
|
||||
style={{ "padding-left": `${8 + (props.level ?? 0) * 16}px` }}
|
||||
>
|
||||
<div class="flex min-w-0 items-center gap-1">
|
||||
<div class="min-w-0 flex-1">
|
||||
<Show
|
||||
when={!tooltip()}
|
||||
fallback={
|
||||
<Tooltip
|
||||
placement={props.mobile ? "bottom" : "right"}
|
||||
value={sessionTitle(props.session.title)}
|
||||
gutter={10}
|
||||
class="min-w-0 w-full"
|
||||
>
|
||||
{item}
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
{item}
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
navigate(`${props.slug}/session/${props.session.id}#message-${message.id}`)
|
||||
<Show when={!props.level}>
|
||||
<div
|
||||
class="shrink-0 overflow-hidden transition-[width,opacity]"
|
||||
classList={{
|
||||
"w-6 opacity-100 pointer-events-auto": !!props.mobile,
|
||||
"w-0 opacity-0 pointer-events-none": !props.mobile,
|
||||
"group-hover/session:w-6 group-hover/session:opacity-100 group-hover/session:pointer-events-auto": true,
|
||||
"group-focus-within/session:w-6 group-focus-within/session:opacity-100 group-focus-within/session:pointer-events-auto": true,
|
||||
}}
|
||||
trigger={item}
|
||||
/>
|
||||
>
|
||||
<Tooltip value={language.t("common.archive")} placement="top">
|
||||
<IconButton
|
||||
icon="archive"
|
||||
variant="ghost"
|
||||
class="size-6 rounded-md"
|
||||
aria-label={language.t("common.archive")}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
void props.archiveSession(props.session)
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="shrink-0 overflow-hidden transition-[width,opacity]"
|
||||
classList={{
|
||||
"w-6 opacity-100 pointer-events-auto": !!props.mobile,
|
||||
"w-0 opacity-0 pointer-events-none": !props.mobile,
|
||||
"group-hover/session:w-6 group-hover/session:opacity-100 group-hover/session:pointer-events-auto": true,
|
||||
"group-focus-within/session:w-6 group-focus-within/session:opacity-100 group-focus-within/session:pointer-events-auto": true,
|
||||
}}
|
||||
>
|
||||
<Tooltip value={language.t("common.archive")} placement="top">
|
||||
<IconButton
|
||||
icon="archive"
|
||||
variant="ghost"
|
||||
class="size-6 rounded-md"
|
||||
aria-label={language.t("common.archive")}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
void props.archiveSession(props.session)
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={currentChild()}>
|
||||
{(child) => (
|
||||
<div class="w-full">
|
||||
<SessionItem {...props} session={child()} level={(props.level ?? 0) + 1} />
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -390,7 +280,6 @@ export const NewSessionItem = (props: {
|
||||
dense?: boolean
|
||||
sidebarExpanded: Accessor<boolean>
|
||||
clearHoverProjectSoon: () => void
|
||||
setHoverSession: (id: string | undefined) => void
|
||||
}): JSX.Element => {
|
||||
const layout = useLayout()
|
||||
const language = useLanguage()
|
||||
@@ -400,9 +289,8 @@ export const NewSessionItem = (props: {
|
||||
<A
|
||||
href={`/${props.slug}/session`}
|
||||
end
|
||||
class={`flex items-center gap-1 min-w-0 w-full text-left focus:outline-none ${props.dense ? "py-0.5" : "py-1"}`}
|
||||
class={`flex items-center gap-2 min-w-0 w-full text-left focus:outline-none ${props.dense ? "py-0.5" : "py-1"}`}
|
||||
onClick={() => {
|
||||
props.setHoverSession(undefined)
|
||||
if (layout.sidebar.opened()) return
|
||||
props.clearHoverProjectSoon()
|
||||
}}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createEffect, createMemo, For, Show, type Accessor, type JSX } from "solid-js"
|
||||
import { createMemo, For, Show, type Accessor, type JSX } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { base64Encode } from "@opencode-ai/util/encode"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
@@ -11,7 +11,7 @@ import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useNotification } from "@/context/notification"
|
||||
import { ProjectIcon, SessionItem, type SessionItemProps } from "./sidebar-items"
|
||||
import { childMapByParent, displayName, sortedRootSessions } from "./helpers"
|
||||
import { displayName, sortedRootSessions } from "./helpers"
|
||||
|
||||
export type ProjectSidebarContext = {
|
||||
currentDir: Accessor<string>
|
||||
@@ -19,7 +19,6 @@ export type ProjectSidebarContext = {
|
||||
sidebarOpened: Accessor<boolean>
|
||||
sidebarHovering: Accessor<boolean>
|
||||
hoverProject: Accessor<string | undefined>
|
||||
nav: Accessor<HTMLElement | undefined>
|
||||
onProjectMouseEnter: (worktree: string, event: MouseEvent) => void
|
||||
onProjectMouseLeave: (worktree: string) => void
|
||||
onProjectFocus: (worktree: string) => void
|
||||
@@ -32,8 +31,7 @@ export type ProjectSidebarContext = {
|
||||
workspacesEnabled: (project: LocalProject) => boolean
|
||||
workspaceIds: (project: LocalProject) => string[]
|
||||
workspaceLabel: (directory: string, branch?: string, projectId?: string) => string
|
||||
sessionProps: Omit<SessionItemProps, "session" | "list" | "slug" | "children" | "mobile" | "dense" | "popover">
|
||||
setHoverSession: (id: string | undefined) => void
|
||||
sessionProps: Omit<SessionItemProps, "session" | "list" | "slug" | "mobile" | "dense">
|
||||
}
|
||||
|
||||
export const ProjectDragOverlay = (props: {
|
||||
@@ -55,7 +53,6 @@ export const ProjectDragOverlay = (props: {
|
||||
const ProjectTile = (props: {
|
||||
project: LocalProject
|
||||
mobile?: boolean
|
||||
nav: Accessor<HTMLElement | undefined>
|
||||
sidebarHovering: Accessor<boolean>
|
||||
selected: Accessor<boolean>
|
||||
active: Accessor<boolean>
|
||||
@@ -195,9 +192,7 @@ const ProjectPreviewPanel = (props: {
|
||||
workspaces: Accessor<string[]>
|
||||
label: (directory: string) => string
|
||||
projectSessions: Accessor<ReturnType<typeof sortedRootSessions>>
|
||||
projectChildren: Accessor<Map<string, string[]>>
|
||||
workspaceSessions: (directory: string) => ReturnType<typeof sortedRootSessions>
|
||||
workspaceChildren: (directory: string) => Map<string, string[]>
|
||||
ctx: ProjectSidebarContext
|
||||
language: ReturnType<typeof useLanguage>
|
||||
}): JSX.Element => (
|
||||
@@ -218,9 +213,8 @@ const ProjectPreviewPanel = (props: {
|
||||
list={props.projectSessions()}
|
||||
slug={base64Encode(props.project.worktree)}
|
||||
dense
|
||||
showTooltip
|
||||
mobile={props.mobile}
|
||||
popover={false}
|
||||
children={props.projectChildren()}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
@@ -229,7 +223,6 @@ const ProjectPreviewPanel = (props: {
|
||||
<For each={props.workspaces()}>
|
||||
{(directory) => {
|
||||
const sessions = createMemo(() => props.workspaceSessions(directory))
|
||||
const children = createMemo(() => props.workspaceChildren(directory))
|
||||
return (
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="px-2 py-0.5 flex items-center gap-1 min-w-0">
|
||||
@@ -246,9 +239,8 @@ const ProjectPreviewPanel = (props: {
|
||||
list={sessions()}
|
||||
slug={base64Encode(directory)}
|
||||
dense
|
||||
showTooltip
|
||||
mobile={props.mobile}
|
||||
popover={false}
|
||||
children={children()}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
@@ -310,20 +302,14 @@ export const SortableProject = (props: {
|
||||
|
||||
const projectStore = createMemo(() => globalSync.child(props.project.worktree, { bootstrap: false })[0])
|
||||
const projectSessions = createMemo(() => sortedRootSessions(projectStore(), props.sortNow()))
|
||||
const projectChildren = createMemo(() => childMapByParent(projectStore().session))
|
||||
const workspaceSessions = (directory: string) => {
|
||||
const [data] = globalSync.child(directory, { bootstrap: false })
|
||||
return sortedRootSessions(data, props.sortNow())
|
||||
}
|
||||
const workspaceChildren = (directory: string) => {
|
||||
const [data] = globalSync.child(directory, { bootstrap: false })
|
||||
return childMapByParent(data.session)
|
||||
}
|
||||
const tile = () => (
|
||||
<ProjectTile
|
||||
project={props.project}
|
||||
mobile={props.mobile}
|
||||
nav={props.ctx.nav}
|
||||
sidebarHovering={props.ctx.sidebarHovering}
|
||||
selected={selected}
|
||||
active={active}
|
||||
@@ -360,7 +346,6 @@ export const SortableProject = (props: {
|
||||
if (state.menu) return
|
||||
if (value && state.suppressHover) return
|
||||
props.ctx.onHoverOpenChanged(props.project.worktree, value)
|
||||
if (value) props.ctx.setHoverSession(undefined)
|
||||
}}
|
||||
>
|
||||
<ProjectPreviewPanel
|
||||
@@ -371,9 +356,7 @@ export const SortableProject = (props: {
|
||||
workspaces={workspaces}
|
||||
label={label}
|
||||
projectSessions={projectSessions}
|
||||
projectChildren={projectChildren}
|
||||
workspaceSessions={workspaceSessions}
|
||||
workspaceChildren={workspaceChildren}
|
||||
ctx={props.ctx}
|
||||
language={language}
|
||||
/>
|
||||
|
||||
@@ -17,7 +17,7 @@ import { type LocalProject } from "@/context/layout"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items"
|
||||
import { childMapByParent, sortedRootSessions, workspaceKey } from "./helpers"
|
||||
import { sortedRootSessions, workspaceKey } from "./helpers"
|
||||
|
||||
type InlineEditorComponent = (props: {
|
||||
id: string
|
||||
@@ -35,9 +35,6 @@ export type WorkspaceSidebarContext = {
|
||||
navList: Accessor<Session[]>
|
||||
sidebarExpanded: Accessor<boolean>
|
||||
sidebarHovering: Accessor<boolean>
|
||||
nav: Accessor<HTMLElement | undefined>
|
||||
hoverSession: Accessor<string | undefined>
|
||||
setHoverSession: (id: string | undefined) => void
|
||||
clearHoverProjectSoon: () => void
|
||||
prefetchSession: (session: Session, priority?: "high" | "low") => void
|
||||
archiveSession: (session: Session) => Promise<void>
|
||||
@@ -152,7 +149,6 @@ const WorkspaceActions = (props: {
|
||||
showResetWorkspaceDialog: WorkspaceSidebarContext["showResetWorkspaceDialog"]
|
||||
showDeleteWorkspaceDialog: WorkspaceSidebarContext["showDeleteWorkspaceDialog"]
|
||||
root: string
|
||||
setHoverSession: WorkspaceSidebarContext["setHoverSession"]
|
||||
clearHoverProjectSoon: WorkspaceSidebarContext["clearHoverProjectSoon"]
|
||||
navigateToNewSession: () => void
|
||||
}): JSX.Element => (
|
||||
@@ -226,7 +222,6 @@ const WorkspaceActions = (props: {
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
props.setHoverSession(undefined)
|
||||
props.clearHoverProjectSoon()
|
||||
props.navigateToNewSession()
|
||||
}}
|
||||
@@ -239,12 +234,10 @@ const WorkspaceActions = (props: {
|
||||
const WorkspaceSessionList = (props: {
|
||||
slug: Accessor<string>
|
||||
mobile?: boolean
|
||||
popover?: boolean
|
||||
ctx: WorkspaceSidebarContext
|
||||
showNew: Accessor<boolean>
|
||||
loading: Accessor<boolean>
|
||||
sessions: Accessor<Session[]>
|
||||
children: Accessor<Map<string, string[]>>
|
||||
hasMore: Accessor<boolean>
|
||||
loadMore: () => Promise<void>
|
||||
language: ReturnType<typeof useLanguage>
|
||||
@@ -256,7 +249,6 @@ const WorkspaceSessionList = (props: {
|
||||
mobile={props.mobile}
|
||||
sidebarExpanded={props.ctx.sidebarExpanded}
|
||||
clearHoverProjectSoon={props.ctx.clearHoverProjectSoon}
|
||||
setHoverSession={props.ctx.setHoverSession}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={props.loading()}>
|
||||
@@ -270,13 +262,8 @@ const WorkspaceSessionList = (props: {
|
||||
navList={props.ctx.navList}
|
||||
slug={props.slug()}
|
||||
mobile={props.mobile}
|
||||
popover={props.popover}
|
||||
children={props.children()}
|
||||
showChild
|
||||
sidebarExpanded={props.ctx.sidebarExpanded}
|
||||
sidebarHovering={props.ctx.sidebarHovering}
|
||||
nav={props.ctx.nav}
|
||||
hoverSession={props.ctx.hoverSession}
|
||||
setHoverSession={props.ctx.setHoverSession}
|
||||
clearHoverProjectSoon={props.ctx.clearHoverProjectSoon}
|
||||
prefetchSession={props.ctx.prefetchSession}
|
||||
archiveSession={props.ctx.archiveSession}
|
||||
@@ -307,7 +294,6 @@ export const SortableWorkspace = (props: {
|
||||
project: LocalProject
|
||||
sortNow: Accessor<number>
|
||||
mobile?: boolean
|
||||
popover?: boolean
|
||||
}): JSX.Element => {
|
||||
const navigate = useNavigate()
|
||||
const params = useParams()
|
||||
@@ -321,7 +307,6 @@ export const SortableWorkspace = (props: {
|
||||
})
|
||||
const slug = createMemo(() => base64Encode(props.directory))
|
||||
const sessions = createMemo(() => sortedRootSessions(workspaceStore, props.sortNow()))
|
||||
const children = createMemo(() => childMapByParent(workspaceStore.session))
|
||||
const local = createMemo(() => props.directory === props.project.worktree)
|
||||
const active = createMemo(() => workspaceKey(props.ctx.currentDir()) === workspaceKey(props.directory))
|
||||
const workspaceValue = createMemo(() => {
|
||||
@@ -428,7 +413,6 @@ export const SortableWorkspace = (props: {
|
||||
showResetWorkspaceDialog={props.ctx.showResetWorkspaceDialog}
|
||||
showDeleteWorkspaceDialog={props.ctx.showDeleteWorkspaceDialog}
|
||||
root={props.project.worktree}
|
||||
setHoverSession={props.ctx.setHoverSession}
|
||||
clearHoverProjectSoon={props.ctx.clearHoverProjectSoon}
|
||||
navigateToNewSession={() => navigate(`/${slug()}/session`)}
|
||||
/>
|
||||
@@ -440,12 +424,10 @@ export const SortableWorkspace = (props: {
|
||||
<WorkspaceSessionList
|
||||
slug={slug}
|
||||
mobile={props.mobile}
|
||||
popover={props.popover}
|
||||
ctx={props.ctx}
|
||||
showNew={showNew}
|
||||
loading={loading}
|
||||
sessions={sessions}
|
||||
children={children}
|
||||
hasMore={hasMore}
|
||||
loadMore={loadMore}
|
||||
language={language}
|
||||
@@ -461,7 +443,6 @@ export const LocalWorkspace = (props: {
|
||||
project: LocalProject
|
||||
sortNow: Accessor<number>
|
||||
mobile?: boolean
|
||||
popover?: boolean
|
||||
}): JSX.Element => {
|
||||
const globalSync = useGlobalSync()
|
||||
const language = useLanguage()
|
||||
@@ -471,7 +452,6 @@ export const LocalWorkspace = (props: {
|
||||
})
|
||||
const slug = createMemo(() => base64Encode(props.project.worktree))
|
||||
const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow()))
|
||||
const children = createMemo(() => childMapByParent(workspace().store.session))
|
||||
const booted = createMemo((prev) => prev || workspace().store.status === "complete", false)
|
||||
const count = createMemo(() => sessions()?.length ?? 0)
|
||||
const loading = createMemo(() => !booted() && count() === 0)
|
||||
@@ -489,12 +469,10 @@ export const LocalWorkspace = (props: {
|
||||
<WorkspaceSessionList
|
||||
slug={slug}
|
||||
mobile={props.mobile}
|
||||
popover={props.popover}
|
||||
ctx={props.ctx}
|
||||
showNew={() => false}
|
||||
loading={loading}
|
||||
sessions={sessions}
|
||||
children={children}
|
||||
hasMore={hasMore}
|
||||
loadMore={loadMore}
|
||||
language={language}
|
||||
|
||||
@@ -429,6 +429,7 @@ export default function Page() {
|
||||
}
|
||||
|
||||
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
||||
const isChildSession = createMemo(() => !!info()?.parentID)
|
||||
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
|
||||
const sessionCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length))
|
||||
const hasSessionReview = createMemo(() => sessionCount() > 0)
|
||||
@@ -1058,7 +1059,7 @@ export default function Page() {
|
||||
}
|
||||
|
||||
if (event.key.length === 1 && event.key !== "Unidentified" && !(event.ctrlKey || event.metaKey)) {
|
||||
if (composer.blocked()) return
|
||||
if (composer.blocked() || isChildSession()) return
|
||||
inputRef?.focus()
|
||||
}
|
||||
}
|
||||
@@ -1127,7 +1128,10 @@ export default function Page() {
|
||||
setFileTreeTab("all")
|
||||
}
|
||||
|
||||
const focusInput = () => inputRef?.focus()
|
||||
const focusInput = () => {
|
||||
if (isChildSession()) return
|
||||
inputRef?.focus()
|
||||
}
|
||||
|
||||
useSessionCommands({
|
||||
navigateMessageByOffset,
|
||||
@@ -1658,7 +1662,7 @@ export default function Page() {
|
||||
const queueEnabled = createMemo(() => {
|
||||
const id = params.id
|
||||
if (!id) return false
|
||||
return settings.general.followup() === "queue" && busy(id) && !composer.blocked()
|
||||
return settings.general.followup() === "queue" && busy(id) && !composer.blocked() && !isChildSession()
|
||||
})
|
||||
|
||||
const followupText = (item: FollowupDraft) => {
|
||||
@@ -1690,6 +1694,7 @@ export default function Page() {
|
||||
const followupDock = createMemo(() => queuedFollowups().map((item) => ({ id: item.id, text: followupText(item) })))
|
||||
|
||||
const sendFollowup = (sessionID: string, id: string, opts?: { manual?: boolean }) => {
|
||||
if (sync.session.get(sessionID)?.parentID) return Promise.resolve()
|
||||
const item = (followup.items[sessionID] ?? []).find((entry) => entry.id === id)
|
||||
if (!item) return Promise.resolve()
|
||||
if (followupBusy(sessionID)) return Promise.resolve()
|
||||
@@ -1820,6 +1825,7 @@ export default function Page() {
|
||||
if (followupBusy(sessionID)) return
|
||||
if (followup.failed[sessionID] === item.id) return
|
||||
if (followup.paused[sessionID]) return
|
||||
if (isChildSession()) return
|
||||
if (composer.blocked()) return
|
||||
if (busy(sessionID)) return
|
||||
|
||||
@@ -2001,7 +2007,7 @@ export default function Page() {
|
||||
}}
|
||||
onResponseSubmit={resumeScroll}
|
||||
followup={
|
||||
params.id
|
||||
params.id && !isChildSession()
|
||||
? {
|
||||
queue: queueEnabled,
|
||||
items: followupDock(),
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Show, createEffect, createMemo, onCleanup } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useNavigate } from "@solidjs/router"
|
||||
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
||||
import { PromptInput } from "@/components/prompt-input"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { usePrompt } from "@/context/prompt"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { getSessionHandoff, setSessionHandoff } from "@/pages/session/handoff"
|
||||
import { useSessionKey } from "@/pages/session/session-layout"
|
||||
import { SessionPermissionDock } from "@/pages/session/composer/session-permission-dock"
|
||||
@@ -43,11 +45,17 @@ export function SessionComposerRegion(props: {
|
||||
}
|
||||
setPromptDockRef: (el: HTMLDivElement) => void
|
||||
}) {
|
||||
const navigate = useNavigate()
|
||||
const prompt = usePrompt()
|
||||
const language = useLanguage()
|
||||
const route = useSessionKey()
|
||||
const sync = useSync()
|
||||
|
||||
const handoffPrompt = createMemo(() => getSessionHandoff(route.sessionKey())?.prompt)
|
||||
const info = createMemo(() => (route.params.id ? sync.session.get(route.params.id) : undefined))
|
||||
const parentID = createMemo(() => info()?.parentID)
|
||||
const child = createMemo(() => !!parentID())
|
||||
const showComposer = createMemo(() => !props.state.blocked() || child())
|
||||
|
||||
const previewPrompt = () =>
|
||||
prompt
|
||||
@@ -113,6 +121,12 @@ export function SessionComposerRegion(props: {
|
||||
const lift = createMemo(() => (rolled() ? 18 : 36 * value()))
|
||||
const full = createMemo(() => Math.max(78, store.height))
|
||||
|
||||
const openParent = () => {
|
||||
const id = parentID()
|
||||
if (!id) return
|
||||
navigate(`/${route.params.dir}/session/${id}`)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
const el = store.body
|
||||
if (!el) return
|
||||
@@ -156,7 +170,7 @@ export function SessionComposerRegion(props: {
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<Show when={!props.state.blocked()}>
|
||||
<Show when={showComposer()}>
|
||||
<Show
|
||||
when={prompt.ready()}
|
||||
fallback={
|
||||
@@ -232,17 +246,40 @@ export function SessionComposerRegion(props: {
|
||||
onEdit={props.followup!.onEdit}
|
||||
/>
|
||||
</Show>
|
||||
<PromptInput
|
||||
ref={props.inputRef}
|
||||
newSessionWorktree={props.newSessionWorktree}
|
||||
onNewSessionWorktreeReset={props.onNewSessionWorktreeReset}
|
||||
edit={props.followup?.edit}
|
||||
onEditLoaded={props.followup?.onEditLoaded}
|
||||
shouldQueue={props.followup?.queue}
|
||||
onQueue={props.followup?.onQueue}
|
||||
onAbort={props.followup?.onAbort}
|
||||
onSubmit={props.onSubmit}
|
||||
/>
|
||||
<Show
|
||||
when={child()}
|
||||
fallback={
|
||||
<Show when={!props.state.blocked()}>
|
||||
<PromptInput
|
||||
ref={props.inputRef}
|
||||
newSessionWorktree={props.newSessionWorktree}
|
||||
onNewSessionWorktreeReset={props.onNewSessionWorktreeReset}
|
||||
edit={props.followup?.edit}
|
||||
onEditLoaded={props.followup?.onEditLoaded}
|
||||
shouldQueue={props.followup?.queue}
|
||||
onQueue={props.followup?.onQueue}
|
||||
onAbort={props.followup?.onAbort}
|
||||
onSubmit={props.onSubmit}
|
||||
/>
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<div
|
||||
ref={props.inputRef}
|
||||
class="w-full rounded-[12px] border border-border-weak-base bg-background-base p-3 text-16-regular text-text-weak"
|
||||
>
|
||||
<span>{language.t("session.child.promptDisabled")} </span>
|
||||
<Show when={parentID()}>
|
||||
<button
|
||||
type="button"
|
||||
class="text-text-base transition-colors hover:text-text-strong"
|
||||
onClick={openParent}
|
||||
>
|
||||
{language.t("session.child.backToParent")}
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
|
||||
@@ -21,6 +21,7 @@ import { Popover as KobaltePopover } from "@kobalte/core/popover"
|
||||
import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture"
|
||||
import { SessionContextUsage } from "@/components/session-context-usage"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useSessionKey } from "@/pages/session/session-layout"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
@@ -68,6 +69,16 @@ const messageComments = (parts: Part[]): MessageComment[] =>
|
||||
]
|
||||
})
|
||||
|
||||
const taskDescription = (part: Part, sessionID: string) => {
|
||||
if (part.type !== "tool" || part.tool !== "task") return
|
||||
const metadata = "metadata" in part.state ? part.state.metadata : undefined
|
||||
if (metadata?.sessionId !== sessionID) return
|
||||
const value = part.state.input?.description
|
||||
if (typeof value === "string" && value) return value
|
||||
}
|
||||
|
||||
const pace = (width: number) => Math.round(Math.max(1200, Math.min(3200, (Math.max(width, 360) * 2000) / 900)))
|
||||
|
||||
const boundaryTarget = (root: HTMLElement, target: EventTarget | null) => {
|
||||
const current = target instanceof Element ? target : undefined
|
||||
const nested = current?.closest("[data-scrollable]")
|
||||
@@ -295,6 +306,32 @@ export function MessageTimeline(props: {
|
||||
const shareUrl = createMemo(() => info()?.share?.url)
|
||||
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
|
||||
const parentID = createMemo(() => info()?.parentID)
|
||||
const parent = createMemo(() => {
|
||||
const id = parentID()
|
||||
if (!id) return
|
||||
return sync.session.get(id)
|
||||
})
|
||||
const parentMessages = createMemo(() => {
|
||||
const id = parentID()
|
||||
if (!id) return emptyMessages
|
||||
return sync.data.message[id] ?? emptyMessages
|
||||
})
|
||||
const parentTitle = createMemo(() => sessionTitle(parent()?.title) ?? language.t("command.session.new"))
|
||||
const childTaskDescription = createMemo(() => {
|
||||
const id = sessionID()
|
||||
if (!id) return
|
||||
return parentMessages()
|
||||
.flatMap((message) => sync.data.part[message.id] ?? [])
|
||||
.map((part) => taskDescription(part, id))
|
||||
.findLast((value): value is string => !!value)
|
||||
})
|
||||
const childTitle = createMemo(() => {
|
||||
if (!parentID()) return titleLabel() ?? ""
|
||||
if (childTaskDescription()) return childTaskDescription()
|
||||
const value = titleLabel()?.replace(/\s+\(@[^)]+ subagent\)$/, "")
|
||||
if (value) return value
|
||||
return language.t("command.session.new")
|
||||
})
|
||||
const showHeader = createMemo(() => !!(titleValue() || parentID()))
|
||||
const stageCfg = { init: 1, batch: 3 }
|
||||
const staging = createTimelineStaging({
|
||||
@@ -317,8 +354,20 @@ export function MessageTimeline(props: {
|
||||
open: false,
|
||||
dismiss: null as "escape" | "outside" | null,
|
||||
})
|
||||
const [bar, setBar] = createStore({
|
||||
ms: pace(640),
|
||||
})
|
||||
|
||||
let more: HTMLButtonElement | undefined
|
||||
let head: HTMLDivElement | undefined
|
||||
|
||||
createResizeObserver(
|
||||
() => head,
|
||||
() => {
|
||||
if (!head || head.clientWidth <= 0) return
|
||||
setBar("ms", pace(head.clientWidth))
|
||||
},
|
||||
)
|
||||
|
||||
const viewShare = () => {
|
||||
const url = shareUrl()
|
||||
@@ -398,8 +447,20 @@ export function MessageTimeline(props: {
|
||||
),
|
||||
)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => [parentID(), childTaskDescription()] as const,
|
||||
([id, description]) => {
|
||||
if (!id || description) return
|
||||
if (sync.data.message[id] !== undefined) return
|
||||
void sync.session.sync(id)
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
const openTitleEditor = () => {
|
||||
if (!sessionID()) return
|
||||
if (!sessionID() || parentID()) return
|
||||
setTitle({ editing: true, draft: titleLabel() ?? "" })
|
||||
requestAnimationFrame(() => {
|
||||
titleRef?.focus()
|
||||
@@ -577,10 +638,18 @@ export function MessageTimeline(props: {
|
||||
}}
|
||||
>
|
||||
<button
|
||||
class="pointer-events-auto size-8 flex items-center justify-center rounded-full bg-background-base border border-border-base shadow-sm text-text-base hover:bg-background-stronger transition-colors"
|
||||
class="pointer-events-auto flex items-center justify-center w-10 h-8 bg-transparent border-none cursor-pointer p-0 group"
|
||||
onClick={props.onResumeScroll}
|
||||
>
|
||||
<Icon name="arrow-down-to-line" />
|
||||
<div
|
||||
class="flex items-center justify-center w-8 h-6 rounded-[6px] border border-[var(--gray-dark-7)] bg-[color-mix(in_srgb,var(--gray-dark-3)_80%,transparent)] backdrop-blur-[0.75px] transition-colors group-hover:border-[var(--gray-dark-8)] [--icon-base:var(--gray-dark-10)] group-hover:[--icon-base:var(--gray-dark-11)]"
|
||||
style={{
|
||||
"box-shadow":
|
||||
"0 51px 60px 0 rgba(0,0,0,0.13), 0 15.375px 18.088px 0 rgba(0,0,0,0.19), 0 6.386px 7.513px 0 rgba(0,0,0,0.25), 0 2.31px 2.717px 0 rgba(0,0,0,0.38)",
|
||||
}}
|
||||
>
|
||||
<Icon name="arrow-down-to-line" size="small" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<ScrollView
|
||||
@@ -638,27 +707,53 @@ export function MessageTimeline(props: {
|
||||
<div ref={props.setContentRef} class="min-w-0 w-full">
|
||||
<Show when={showHeader()}>
|
||||
<div
|
||||
ref={(el) => {
|
||||
head = el
|
||||
setBar("ms", pace(el.clientWidth))
|
||||
}}
|
||||
data-session-title
|
||||
classList={{
|
||||
"sticky top-0 z-30 bg-[linear-gradient(to_bottom,var(--background-stronger)_48px,transparent)]": true,
|
||||
relative: true,
|
||||
"w-full": true,
|
||||
"pb-4": true,
|
||||
"pl-2 pr-3 md:pl-4 md:pr-3": true,
|
||||
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
|
||||
}}
|
||||
>
|
||||
<Show when={workingStatus() !== "hidden"}>
|
||||
<div
|
||||
data-component="session-progress"
|
||||
data-state={workingStatus()}
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
"--session-progress-color": tint() ?? "var(--icon-interactive-base)",
|
||||
"--session-progress-ms": `${bar.ms}ms`,
|
||||
}}
|
||||
>
|
||||
<div data-component="session-progress-bar" />
|
||||
</div>
|
||||
</Show>
|
||||
<div class="h-12 w-full flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-1 min-w-0 flex-1 pr-3">
|
||||
<Show when={parentID()}>
|
||||
<IconButton
|
||||
tabIndex={-1}
|
||||
icon="arrow-left"
|
||||
variant="ghost"
|
||||
onClick={navigateParent}
|
||||
aria-label={language.t("common.goBack")}
|
||||
/>
|
||||
</Show>
|
||||
<div class="flex items-center min-w-0 grow-1">
|
||||
<Show when={parentID()}>
|
||||
<button
|
||||
type="button"
|
||||
data-slot="session-title-parent"
|
||||
class="min-w-0 max-w-[40%] truncate text-14-medium text-text-weak transition-colors hover:text-text-base"
|
||||
onClick={navigateParent}
|
||||
>
|
||||
{parentTitle()}
|
||||
</button>
|
||||
<span
|
||||
data-slot="session-title-separator"
|
||||
class="px-2 text-14-medium text-text-weak"
|
||||
aria-hidden="true"
|
||||
>
|
||||
/
|
||||
</span>
|
||||
</Show>
|
||||
<div
|
||||
class="shrink-0 flex items-center justify-center overflow-hidden transition-[width,margin] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)]"
|
||||
style={{
|
||||
@@ -676,15 +771,16 @@ export function MessageTimeline(props: {
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={titleLabel() || title.editing}>
|
||||
<Show when={childTitle() || title.editing}>
|
||||
<Show
|
||||
when={title.editing}
|
||||
fallback={
|
||||
<h1
|
||||
data-slot="session-title-child"
|
||||
class="text-14-medium text-text-strong truncate grow-1 min-w-0"
|
||||
onDblClick={openTitleEditor}
|
||||
>
|
||||
{titleLabel()}
|
||||
{childTitle()}
|
||||
</h1>
|
||||
}
|
||||
>
|
||||
@@ -692,6 +788,7 @@ export function MessageTimeline(props: {
|
||||
ref={(el) => {
|
||||
titleRef = el
|
||||
}}
|
||||
data-slot="session-title-child"
|
||||
value={title.draft}
|
||||
disabled={titleMutation.isPending}
|
||||
class="text-14-medium text-text-strong grow-1 min-w-0 rounded-[6px]"
|
||||
@@ -719,177 +816,179 @@ export function MessageTimeline(props: {
|
||||
{(id) => (
|
||||
<div class="shrink-0 flex items-center gap-3">
|
||||
<SessionContextUsage placement="bottom" />
|
||||
<DropdownMenu
|
||||
gutter={4}
|
||||
placement="bottom-end"
|
||||
open={title.menuOpen}
|
||||
onOpenChange={(open) => {
|
||||
setTitle("menuOpen", open)
|
||||
if (open) return
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.Trigger
|
||||
as={IconButton}
|
||||
icon="dot-grid"
|
||||
variant="ghost"
|
||||
class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
|
||||
classList={{
|
||||
"bg-surface-base-active": share.open || title.pendingShare,
|
||||
<Show when={!parentID()}>
|
||||
<DropdownMenu
|
||||
gutter={4}
|
||||
placement="bottom-end"
|
||||
open={title.menuOpen}
|
||||
onOpenChange={(open) => {
|
||||
setTitle("menuOpen", open)
|
||||
if (open) return
|
||||
}}
|
||||
aria-label={language.t("common.moreOptions")}
|
||||
aria-expanded={title.menuOpen || share.open || title.pendingShare}
|
||||
ref={(el: HTMLButtonElement) => {
|
||||
more = el
|
||||
}}
|
||||
/>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
style={{ "min-width": "104px" }}
|
||||
onCloseAutoFocus={(event) => {
|
||||
if (title.pendingRename) {
|
||||
event.preventDefault()
|
||||
setTitle("pendingRename", false)
|
||||
openTitleEditor()
|
||||
return
|
||||
}
|
||||
if (title.pendingShare) {
|
||||
event.preventDefault()
|
||||
requestAnimationFrame(() => {
|
||||
setShare({ open: true, dismiss: null })
|
||||
setTitle("pendingShare", false)
|
||||
})
|
||||
}
|
||||
>
|
||||
<DropdownMenu.Trigger
|
||||
as={IconButton}
|
||||
icon="dot-grid"
|
||||
variant="ghost"
|
||||
class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
|
||||
classList={{
|
||||
"bg-surface-base-active": share.open || title.pendingShare,
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => {
|
||||
setTitle("pendingRename", true)
|
||||
setTitle("menuOpen", false)
|
||||
aria-label={language.t("common.moreOptions")}
|
||||
aria-expanded={title.menuOpen || share.open || title.pendingShare}
|
||||
ref={(el: HTMLButtonElement) => {
|
||||
more = el
|
||||
}}
|
||||
/>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
style={{ "min-width": "104px" }}
|
||||
onCloseAutoFocus={(event) => {
|
||||
if (title.pendingRename) {
|
||||
event.preventDefault()
|
||||
setTitle("pendingRename", false)
|
||||
openTitleEditor()
|
||||
return
|
||||
}
|
||||
if (title.pendingShare) {
|
||||
event.preventDefault()
|
||||
requestAnimationFrame(() => {
|
||||
setShare({ open: true, dismiss: null })
|
||||
setTitle("pendingShare", false)
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<Show when={shareEnabled()}>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => {
|
||||
setTitle({ pendingShare: true, menuOpen: false })
|
||||
setTitle("pendingRename", true)
|
||||
setTitle("menuOpen", false)
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>
|
||||
{language.t("session.share.action.share")}
|
||||
</DropdownMenu.ItemLabel>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</Show>
|
||||
<DropdownMenu.Item onSelect={() => void archiveSession(id())}>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id()} />)}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
|
||||
<KobaltePopover
|
||||
open={share.open}
|
||||
anchorRef={() => more}
|
||||
placement="bottom-end"
|
||||
gutter={4}
|
||||
modal={false}
|
||||
onOpenChange={(open) => {
|
||||
if (open) setShare("dismiss", null)
|
||||
setShare("open", open)
|
||||
}}
|
||||
>
|
||||
<KobaltePopover.Portal>
|
||||
<KobaltePopover.Content
|
||||
data-component="popover-content"
|
||||
style={{ "min-width": "320px" }}
|
||||
onEscapeKeyDown={(event) => {
|
||||
setShare({ dismiss: "escape", open: false })
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onPointerDownOutside={() => {
|
||||
setShare({ dismiss: "outside", open: false })
|
||||
}}
|
||||
onFocusOutside={() => {
|
||||
setShare({ dismiss: "outside", open: false })
|
||||
}}
|
||||
onCloseAutoFocus={(event) => {
|
||||
if (share.dismiss === "outside") event.preventDefault()
|
||||
setShare("dismiss", null)
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col p-3">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-13-medium text-text-strong">
|
||||
{language.t("session.share.popover.title")}
|
||||
</div>
|
||||
<div class="text-12-regular text-text-weak">
|
||||
{shareUrl()
|
||||
? language.t("session.share.popover.description.shared")
|
||||
: language.t("session.share.popover.description.unshared")}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 flex flex-col gap-2">
|
||||
<Show
|
||||
when={shareUrl()}
|
||||
fallback={
|
||||
<Button
|
||||
size="large"
|
||||
variant="primary"
|
||||
class="w-full"
|
||||
onClick={shareSession}
|
||||
disabled={shareMutation.isPending}
|
||||
>
|
||||
{shareMutation.isPending
|
||||
? language.t("session.share.action.publishing")
|
||||
: language.t("session.share.action.publish")}
|
||||
</Button>
|
||||
}
|
||||
<Show when={shareEnabled()}>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => {
|
||||
setTitle({ pendingShare: true, menuOpen: false })
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<TextField
|
||||
value={shareUrl() ?? ""}
|
||||
readOnly
|
||||
copyable
|
||||
copyKind="link"
|
||||
tabIndex={-1}
|
||||
class="w-full"
|
||||
/>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
size="large"
|
||||
variant="secondary"
|
||||
class="w-full shadow-none border border-border-weak-base"
|
||||
onClick={unshareSession}
|
||||
disabled={unshareMutation.isPending}
|
||||
>
|
||||
{unshareMutation.isPending
|
||||
? language.t("session.share.action.unpublishing")
|
||||
: language.t("session.share.action.unpublish")}
|
||||
</Button>
|
||||
<DropdownMenu.ItemLabel>
|
||||
{language.t("session.share.action.share")}
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</Show>
|
||||
<DropdownMenu.Item onSelect={() => void archiveSession(id())}>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id()} />)}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
|
||||
<KobaltePopover
|
||||
open={share.open}
|
||||
anchorRef={() => more}
|
||||
placement="bottom-end"
|
||||
gutter={4}
|
||||
modal={false}
|
||||
onOpenChange={(open) => {
|
||||
if (open) setShare("dismiss", null)
|
||||
setShare("open", open)
|
||||
}}
|
||||
>
|
||||
<KobaltePopover.Portal>
|
||||
<KobaltePopover.Content
|
||||
data-component="popover-content"
|
||||
style={{ "min-width": "320px" }}
|
||||
onEscapeKeyDown={(event) => {
|
||||
setShare({ dismiss: "escape", open: false })
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onPointerDownOutside={() => {
|
||||
setShare({ dismiss: "outside", open: false })
|
||||
}}
|
||||
onFocusOutside={() => {
|
||||
setShare({ dismiss: "outside", open: false })
|
||||
}}
|
||||
onCloseAutoFocus={(event) => {
|
||||
if (share.dismiss === "outside") event.preventDefault()
|
||||
setShare("dismiss", null)
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col p-3">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-13-medium text-text-strong">
|
||||
{language.t("session.share.popover.title")}
|
||||
</div>
|
||||
<div class="text-12-regular text-text-weak">
|
||||
{shareUrl()
|
||||
? language.t("session.share.popover.description.shared")
|
||||
: language.t("session.share.popover.description.unshared")}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 flex flex-col gap-2">
|
||||
<Show
|
||||
when={shareUrl()}
|
||||
fallback={
|
||||
<Button
|
||||
size="large"
|
||||
variant="primary"
|
||||
class="w-full"
|
||||
onClick={viewShare}
|
||||
disabled={unshareMutation.isPending}
|
||||
onClick={shareSession}
|
||||
disabled={shareMutation.isPending}
|
||||
>
|
||||
{language.t("session.share.action.view")}
|
||||
{shareMutation.isPending
|
||||
? language.t("session.share.action.publishing")
|
||||
: language.t("session.share.action.publish")}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<TextField
|
||||
value={shareUrl() ?? ""}
|
||||
readOnly
|
||||
copyable
|
||||
copyKind="link"
|
||||
tabIndex={-1}
|
||||
class="w-full"
|
||||
/>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
size="large"
|
||||
variant="secondary"
|
||||
class="w-full shadow-none border border-border-weak-base"
|
||||
onClick={unshareSession}
|
||||
disabled={unshareMutation.isPending}
|
||||
>
|
||||
{unshareMutation.isPending
|
||||
? language.t("session.share.action.unpublishing")
|
||||
: language.t("session.share.action.unpublish")}
|
||||
</Button>
|
||||
<Button
|
||||
size="large"
|
||||
variant="primary"
|
||||
class="w-full"
|
||||
onClick={viewShare}
|
||||
disabled={unshareMutation.isPending}
|
||||
>
|
||||
{language.t("session.share.action.view")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</KobaltePopover.Content>
|
||||
</KobaltePopover.Portal>
|
||||
</KobaltePopover>
|
||||
</KobaltePopover.Content>
|
||||
</KobaltePopover.Portal>
|
||||
</KobaltePopover>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"
|
||||
import type { UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { resetSessionModel, syncSessionModel } from "./session-model-helpers"
|
||||
|
||||
const message = (input?: Partial<Pick<UserMessage, "agent" | "model" | "variant">>) =>
|
||||
const message = (input?: { agent?: string; model?: UserMessage["model"] }) =>
|
||||
({
|
||||
id: "msg",
|
||||
sessionID: "session",
|
||||
@@ -10,7 +10,6 @@ const message = (input?: Partial<Pick<UserMessage, "agent" | "model" | "variant"
|
||||
time: { created: 1 },
|
||||
agent: input?.agent ?? "build",
|
||||
model: input?.model ?? { providerID: "anthropic", modelID: "claude-sonnet-4" },
|
||||
variant: input?.variant,
|
||||
}) as UserMessage
|
||||
|
||||
describe("syncSessionModel", () => {
|
||||
@@ -26,10 +25,12 @@ describe("syncSessionModel", () => {
|
||||
reset() {},
|
||||
},
|
||||
},
|
||||
message({ variant: "high" }),
|
||||
message({ model: { providerID: "anthropic", modelID: "claude-sonnet-4", variant: "high" } }),
|
||||
)
|
||||
|
||||
expect(calls).toEqual([message({ variant: "high" })])
|
||||
expect(calls).toEqual([
|
||||
message({ model: { providerID: "anthropic", modelID: "claude-sonnet-4", variant: "high" } }),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -5,9 +5,30 @@ const defaults: Record<string, string> = {
|
||||
plan: "var(--icon-agent-plan-base)",
|
||||
}
|
||||
|
||||
const palette = [
|
||||
"var(--icon-agent-ask-base)",
|
||||
"var(--icon-agent-build-base)",
|
||||
"var(--icon-agent-docs-base)",
|
||||
"var(--icon-agent-plan-base)",
|
||||
"var(--syntax-info)",
|
||||
"var(--syntax-success)",
|
||||
"var(--syntax-warning)",
|
||||
"var(--syntax-property)",
|
||||
"var(--syntax-constant)",
|
||||
"var(--text-diff-add-base)",
|
||||
"var(--text-diff-delete-base)",
|
||||
"var(--icon-warning-base)",
|
||||
]
|
||||
|
||||
function tone(name: string) {
|
||||
let hash = 0
|
||||
for (const char of name) hash = (hash * 31 + char.charCodeAt(0)) >>> 0
|
||||
return palette[hash % palette.length]
|
||||
}
|
||||
|
||||
export function agentColor(name: string, custom?: string) {
|
||||
if (custom) return custom
|
||||
return defaults[name] ?? defaults[name.toLowerCase()]
|
||||
return defaults[name] ?? defaults[name.toLowerCase()] ?? tone(name.toLowerCase())
|
||||
}
|
||||
|
||||
export function messageAgentColor(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.17",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -9,8 +9,8 @@ export const config = {
|
||||
github: {
|
||||
repoUrl: "https://github.com/anomalyco/opencode",
|
||||
starsFormatted: {
|
||||
compact: "120K",
|
||||
full: "120,000",
|
||||
compact: "140K",
|
||||
full: "140,000",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -22,8 +22,8 @@ export const config = {
|
||||
|
||||
// Static stats (used on landing page)
|
||||
stats: {
|
||||
contributors: "800",
|
||||
commits: "10,000",
|
||||
monthlyUsers: "5M",
|
||||
contributors: "850",
|
||||
commits: "11,000",
|
||||
monthlyUsers: "6.5M",
|
||||
},
|
||||
} as const
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Stripe } from "stripe"
|
||||
import { Billing } from "@opencode-ai/console-core/billing.js"
|
||||
import type { APIEvent } from "@solidjs/start/server"
|
||||
import { and, Database, eq, sql } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
@@ -111,27 +112,17 @@ export async function POST(input: APIEvent) {
|
||||
const customerID = body.data.object.customer as string
|
||||
const invoiceID = body.data.object.latest_invoice as string
|
||||
const subscriptionID = body.data.object.id as string
|
||||
const paymentMethodID = body.data.object.default_payment_method as string
|
||||
|
||||
if (!workspaceID) throw new Error("Workspace ID not found")
|
||||
if (!userID) throw new Error("User ID not found")
|
||||
if (!customerID) throw new Error("Customer ID not found")
|
||||
if (!invoiceID) throw new Error("Invoice ID not found")
|
||||
if (!subscriptionID) throw new Error("Subscription ID not found")
|
||||
|
||||
// get payment id from invoice
|
||||
const invoice = await Billing.stripe().invoices.retrieve(invoiceID, {
|
||||
expand: ["payments"],
|
||||
})
|
||||
const paymentID = invoice.payments?.data[0].payment.payment_intent as string
|
||||
if (!paymentID) throw new Error("Payment ID not found")
|
||||
if (!paymentMethodID) throw new Error("Payment method ID not found")
|
||||
|
||||
// get payment method for the payment intent
|
||||
const paymentIntent = await Billing.stripe().paymentIntents.retrieve(paymentID, {
|
||||
expand: ["payment_method"],
|
||||
})
|
||||
const paymentMethod = paymentIntent.payment_method
|
||||
if (!paymentMethod || typeof paymentMethod === "string") throw new Error("Payment method not expanded")
|
||||
|
||||
const paymentMethod = await Billing.stripe().paymentMethods.retrieve(paymentMethodID)
|
||||
await Actor.provide("system", { workspaceID }, async () => {
|
||||
// look up current billing
|
||||
const billing = await Billing.get()
|
||||
@@ -200,26 +191,18 @@ export async function POST(input: APIEvent) {
|
||||
const amountInCents = body.data.object.amount_paid
|
||||
const customerID = body.data.object.customer as string
|
||||
const subscriptionID = body.data.object.parent?.subscription_details?.subscription as string
|
||||
const productID = body.data.object.lines?.data[0].pricing?.price_details?.product as string
|
||||
|
||||
if (!customerID) throw new Error("Customer ID not found")
|
||||
if (!invoiceID) throw new Error("Invoice ID not found")
|
||||
if (!subscriptionID) throw new Error("Subscription ID not found")
|
||||
|
||||
// get coupon id from subscription
|
||||
const subscriptionData = await Billing.stripe().subscriptions.retrieve(subscriptionID, {
|
||||
expand: ["discounts"],
|
||||
})
|
||||
const couponID =
|
||||
typeof subscriptionData.discounts[0] === "string"
|
||||
? subscriptionData.discounts[0]
|
||||
: subscriptionData.discounts[0]?.coupon?.id
|
||||
const productID = subscriptionData.items.data[0].price.product as string
|
||||
|
||||
// get payment id from invoice
|
||||
const invoice = await Billing.stripe().invoices.retrieve(invoiceID, {
|
||||
expand: ["payments"],
|
||||
expand: ["discounts", "payments"],
|
||||
})
|
||||
const paymentID = invoice.payments?.data[0].payment.payment_intent as string
|
||||
const paymentID = invoice.payments?.data[0]?.payment.payment_intent as string
|
||||
const couponID = (invoice.discounts[0] as Stripe.Discount).coupon?.id as string
|
||||
if (!paymentID) {
|
||||
// payment id can be undefined when using coupon
|
||||
if (!couponID) throw new Error("Payment ID not found")
|
||||
|
||||
@@ -287,6 +287,8 @@ export function LiteSection() {
|
||||
<ul data-slot="promo-models">
|
||||
<li>Kimi K2.5</li>
|
||||
<li>GLM-5</li>
|
||||
<li>Mimo-V2-Pro</li>
|
||||
<li>Mimo-V2-Omni</li>
|
||||
<li>MiniMax M2.5</li>
|
||||
<li>MiniMax M2.7</li>
|
||||
</ul>
|
||||
|
||||
@@ -90,7 +90,8 @@ export async function handler(
|
||||
const body = await input.request.json()
|
||||
const model = opts.parseModel(url, body)
|
||||
const isStream = opts.parseIsStream(url, body)
|
||||
const ip = input.request.headers.get("x-real-ip") ?? ""
|
||||
const rawIp = input.request.headers.get("x-real-ip") ?? ""
|
||||
const ip = rawIp.includes(":") ? rawIp.split(":").slice(0, 4).join(":") : rawIp
|
||||
const sessionId = input.request.headers.get("x-opencode-session") ?? ""
|
||||
const requestId = input.request.headers.get("x-opencode-request") ?? ""
|
||||
const projectId = input.request.headers.get("x-opencode-project") ?? ""
|
||||
|
||||
@@ -17,9 +17,8 @@ export function createRateLimiter(
|
||||
const dict = i18n(localeFromRequest(request))
|
||||
|
||||
const limits = Subscription.getFreeLimits()
|
||||
const headerExists = request.headers.has(limits.checkHeader)
|
||||
const dailyLimit = !headerExists ? limits.fallbackValue : (rateLimit ?? limits.dailyRequests)
|
||||
const isDefaultModel = headerExists && !rateLimit
|
||||
const dailyLimit = rateLimit ?? limits.dailyRequests
|
||||
const isDefaultModel = !rateLimit
|
||||
|
||||
const ip = !rawIp.length ? "unknown" : rawIp
|
||||
const now = Date.now()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.17",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -254,7 +254,7 @@ export namespace Billing {
|
||||
const createSession = () =>
|
||||
Billing.stripe().checkout.sessions.create({
|
||||
mode: "subscription",
|
||||
discounts: [{ coupon: LiteData.firstMonth50Coupon() }],
|
||||
discounts: [{ coupon: LiteData.firstMonthCoupon(email!) }],
|
||||
...(billing.customerID
|
||||
? {
|
||||
customer: billing.customerID,
|
||||
|
||||
@@ -11,6 +11,11 @@ export namespace LiteData {
|
||||
export const productID = fn(z.void(), () => Resource.ZEN_LITE_PRICE.product)
|
||||
export const priceID = fn(z.void(), () => Resource.ZEN_LITE_PRICE.price)
|
||||
export const priceInr = fn(z.void(), () => Resource.ZEN_LITE_PRICE.priceInr)
|
||||
export const firstMonth50Coupon = fn(z.void(), () => Resource.ZEN_LITE_PRICE.firstMonth50Coupon)
|
||||
export const firstMonthCoupon = fn(z.string(), (email) => {
|
||||
const invitees = Resource.ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES.value.split(",")
|
||||
return invitees.includes(email)
|
||||
? Resource.ZEN_LITE_PRICE.firstMonth100Coupon
|
||||
: Resource.ZEN_LITE_PRICE.firstMonth50Coupon
|
||||
})
|
||||
export const planName = fn(z.void(), () => "lite")
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ export namespace Subscription {
|
||||
free: z.object({
|
||||
promoTokens: z.number().int(),
|
||||
dailyRequests: z.number().int(),
|
||||
checkHeader: z.string(),
|
||||
fallbackValue: z.number().int(),
|
||||
}),
|
||||
lite: z.object({
|
||||
rollingLimit: z.number().int(),
|
||||
|
||||
5
packages/console/core/sst-env.d.ts
vendored
5
packages/console/core/sst-env.d.ts
vendored
@@ -142,7 +142,12 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_LITE_PRICE": {
|
||||
"firstMonth100Coupon": string
|
||||
"firstMonth50Coupon": string
|
||||
"price": string
|
||||
"priceInr": number
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.17",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
5
packages/console/function/sst-env.d.ts
vendored
5
packages/console/function/sst-env.d.ts
vendored
@@ -142,7 +142,12 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_LITE_PRICE": {
|
||||
"firstMonth100Coupon": string
|
||||
"firstMonth50Coupon": string
|
||||
"price": string
|
||||
"priceInr": number
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.17",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
5
packages/console/resource/sst-env.d.ts
vendored
5
packages/console/resource/sst-env.d.ts
vendored
@@ -142,7 +142,12 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_LITE_PRICE": {
|
||||
"firstMonth100Coupon": string
|
||||
"firstMonth50Coupon": string
|
||||
"price": string
|
||||
"priceInr": number
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop-electron",
|
||||
"private": true,
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.17",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://opencode.ai",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.17",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.17",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
5
packages/enterprise/sst-env.d.ts
vendored
5
packages/enterprise/sst-env.d.ts
vendored
@@ -142,7 +142,12 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_LITE_PRICE": {
|
||||
"firstMonth100Coupon": string
|
||||
"firstMonth50Coupon": string
|
||||
"price": string
|
||||
"priceInr": number
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The open source coding agent."
|
||||
version = "1.3.13"
|
||||
version = "1.3.17"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/anomalyco/opencode"
|
||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.3.13/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.3.17/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.3.13/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.3.17/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.3.13/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.3.17/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.3.13/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.3.17/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.3.13/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.3.17/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.17",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
5
packages/function/sst-env.d.ts
vendored
5
packages/function/sst-env.d.ts
vendored
@@ -142,7 +142,12 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_LITE_PRICE": {
|
||||
"firstMonth100Coupon": string
|
||||
"firstMonth50Coupon": string
|
||||
"price": string
|
||||
"priceInr": number
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.17",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
@@ -9,7 +9,9 @@
|
||||
"prepare": "effect-language-service patch || true",
|
||||
"typecheck": "tsgo --noEmit",
|
||||
"test": "bun test --timeout 30000",
|
||||
"test:ci": "mkdir -p .artifacts/unit && bun test --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml",
|
||||
"build": "bun run script/build.ts",
|
||||
"fix-node-pty": "bun run script/fix-node-pty.ts",
|
||||
"upgrade-opentui": "bun run script/upgrade-opentui.ts",
|
||||
"dev": "bun run --conditions=browser ./src/index.ts",
|
||||
"random": "echo 'Random script updated at $(date)' && echo 'Change queued successfully' && echo 'Another change made' && echo 'Yet another change' && echo 'One more change' && echo 'Final change' && echo 'Another final change' && echo 'Yet another final change'",
|
||||
@@ -32,6 +34,11 @@
|
||||
"bun": "./src/storage/db.bun.ts",
|
||||
"node": "./src/storage/db.node.ts",
|
||||
"default": "./src/storage/db.bun.ts"
|
||||
},
|
||||
"#pty": {
|
||||
"bun": "./src/pty/pty.bun.ts",
|
||||
"node": "./src/pty/pty.node.ts",
|
||||
"default": "./src/pty/pty.bun.ts"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -53,6 +60,7 @@
|
||||
"@types/bun": "catalog:",
|
||||
"@types/cross-spawn": "catalog:",
|
||||
"@types/mime-types": "3.0.1",
|
||||
"@types/npm-package-arg": "6.1.4",
|
||||
"@types/npmcli__arborist": "6.3.3",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/turndown": "5.0.5",
|
||||
@@ -71,7 +79,7 @@
|
||||
"@actions/github": "6.0.1",
|
||||
"@agentclientprotocol/sdk": "0.16.1",
|
||||
"@ai-sdk/amazon-bedrock": "4.0.83",
|
||||
"@ai-sdk/anthropic": "3.0.64",
|
||||
"@ai-sdk/anthropic": "3.0.67",
|
||||
"@ai-sdk/azure": "3.0.49",
|
||||
"@ai-sdk/cerebras": "2.0.41",
|
||||
"@ai-sdk/cohere": "3.0.27",
|
||||
@@ -85,15 +93,20 @@
|
||||
"@ai-sdk/openai-compatible": "2.0.37",
|
||||
"@ai-sdk/perplexity": "3.0.26",
|
||||
"@ai-sdk/provider": "3.0.8",
|
||||
"@ai-sdk/provider-utils": "4.0.21",
|
||||
"@ai-sdk/provider-utils": "4.0.23",
|
||||
"@ai-sdk/togetherai": "2.0.41",
|
||||
"@ai-sdk/vercel": "2.0.39",
|
||||
"@ai-sdk/xai": "3.0.75",
|
||||
"@aws-sdk/credential-providers": "3.993.0",
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@gitlab/gitlab-ai-provider": "3.6.0",
|
||||
"@gitlab/opencode-gitlab-auth": "1.3.3",
|
||||
"@hono/node-server": "1.19.11",
|
||||
"@hono/node-ws": "1.3.0",
|
||||
"@hono/standard-validator": "0.1.5",
|
||||
"@hono/zod-validator": "catalog:",
|
||||
"@lydell/node-pty": "1.2.0-beta.10",
|
||||
"@modelcontextprotocol/sdk": "1.27.1",
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@octokit/graphql": "9.0.2",
|
||||
@@ -103,9 +116,9 @@
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "2.3.3",
|
||||
"@opentui/core": "0.1.96",
|
||||
"@opentui/solid": "0.1.96",
|
||||
"@openrouter/ai-sdk-provider": "2.4.2",
|
||||
"@opentui/core": "0.1.97",
|
||||
"@opentui/solid": "0.1.97",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
@@ -134,6 +147,7 @@
|
||||
"jsonc-parser": "3.3.1",
|
||||
"mime-types": "3.0.2",
|
||||
"minimatch": "10.0.3",
|
||||
"npm-package-arg": "13.0.2",
|
||||
"open": "10.1.2",
|
||||
"opencode-gitlab-auth": "2.0.1",
|
||||
"opencode-poe-auth": "0.0.1",
|
||||
|
||||
15
packages/opencode/script/build-node.ts
Normal file → Executable file
15
packages/opencode/script/build-node.ts
Normal file → Executable file
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { $ } from "bun"
|
||||
import { Script } from "@opencode-ai/script"
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
@@ -8,6 +9,15 @@ import { fileURLToPath } from "url"
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
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)
|
||||
|
||||
@@ -41,11 +51,16 @@ const migrations = await Promise.all(
|
||||
)
|
||||
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({
|
||||
target: "node",
|
||||
entrypoints: ["./src/node.ts"],
|
||||
outdir: "./dist",
|
||||
format: "esm",
|
||||
sourcemap: "linked",
|
||||
external: ["jsonc-parser"],
|
||||
define: {
|
||||
OPENCODE_MIGRATIONS: JSON.stringify(migrations),
|
||||
|
||||
@@ -209,6 +209,7 @@ for (const item of targets) {
|
||||
conditions: ["browser"],
|
||||
tsconfig: "./tsconfig.json",
|
||||
plugins: [plugin],
|
||||
external: ["node-gyp"],
|
||||
compile: {
|
||||
autoloadBunfig: false,
|
||||
autoloadDotenv: false,
|
||||
|
||||
28
packages/opencode/script/fix-node-pty.ts
Executable file
28
packages/opencode/script/fix-node-pty.ts
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
const dir = path.resolve(__dirname, "..")
|
||||
|
||||
if (process.platform !== "win32") {
|
||||
const root = path.join(dir, "node_modules", "node-pty", "prebuilds")
|
||||
const dirs = await fs.readdir(root, { withFileTypes: true }).catch(() => [])
|
||||
const files = dirs.filter((x) => x.isDirectory()).map((x) => path.join(root, x.name, "spawn-helper"))
|
||||
const result = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const stat = await fs.stat(file).catch(() => undefined)
|
||||
if (!stat) return
|
||||
if ((stat.mode & 0o111) === 0o111) return
|
||||
await fs.chmod(file, stat.mode | 0o755)
|
||||
return file
|
||||
}),
|
||||
)
|
||||
const fixed = result.filter(Boolean)
|
||||
if (fixed.length) {
|
||||
console.log(`fixed node-pty permissions for ${fixed.length} helper${fixed.length === 1 ? "" : "s"}`)
|
||||
}
|
||||
}
|
||||
@@ -235,11 +235,27 @@ Once individual tools are effectified, change `Tool.Info` (`tool/tool.ts`) so `i
|
||||
2. Update `Tool.define()` factory to work with Effects
|
||||
3. Update `SessionPrompt` to `yield*` tool results instead of `await`ing
|
||||
|
||||
### Tool migration details
|
||||
|
||||
Until the tool interface itself returns `Effect`, use this transitional pattern for migrated tools:
|
||||
|
||||
- `Tool.defineEffect(...)` should `yield*` the services the tool depends on and close over them in the returned tool definition.
|
||||
- Keep the bridge at the Promise boundary only. Prefer a single `Effect.runPromise(...)` in the temporary `async execute(...)` implementation, and move the inner logic into `Effect.fn(...)` helpers instead of scattering `runPromise` islands through the tool body.
|
||||
- If a tool starts requiring new services, wire them into `ToolRegistry.defaultLayer` so production callers resolve the same dependencies as tests.
|
||||
|
||||
Tool tests should use the existing Effect helpers in `packages/opencode/test/lib/effect.ts`:
|
||||
|
||||
- Use `testEffect(...)` / `it.live(...)` instead of creating fake local wrappers around effectful tools.
|
||||
- Yield the real tool export, then initialize it: `const info = yield* ReadTool`, `const tool = yield* Effect.promise(() => info.init())`.
|
||||
- Run tests inside a real instance with `provideTmpdirInstance(...)` or `provideInstance(tmpdirScoped(...))` so instance-scoped services resolve exactly as they do in production.
|
||||
|
||||
This keeps migrated tool tests aligned with the production service graph today, and makes the eventual `Tool.Info` → `Effect` cleanup mostly mechanical later.
|
||||
|
||||
Individual tools, ordered by value:
|
||||
|
||||
- [ ] `apply_patch.ts` — HIGH: multi-step orchestration, error accumulation, Bus events
|
||||
- [ ] `bash.ts` — HIGH: shell orchestration, quoting, timeout handling, output capture
|
||||
- [ ] `read.ts` — HIGH: streaming I/O, readline, binary detection → FileSystem + Stream
|
||||
- [x] `read.ts` — HIGH: streaming I/O, readline, binary detection → FileSystem + Stream
|
||||
- [ ] `edit.ts` — HIGH: multi-step diff/format/publish pipeline, FileWatcher lock
|
||||
- [ ] `grep.ts` — MEDIUM: spawns ripgrep → ChildProcessSpawner, timeout handling
|
||||
- [ ] `write.ts` — MEDIUM: permission checks, diagnostics polling, Bus events
|
||||
|
||||
14
packages/opencode/specs/v2.md
Normal file
14
packages/opencode/specs/v2.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# 2.0
|
||||
|
||||
What we would change if we could
|
||||
|
||||
## Keybindings vs. Keymappings
|
||||
|
||||
Make it `keymappings`, closer to neovim. Can be layered like `<leader>abc`. Commands don't define their binding, but have an id that a key can be mapped to like
|
||||
|
||||
```ts
|
||||
{ key: "ctrl+w", cmd: string | function, description }
|
||||
```
|
||||
|
||||
_Why_
|
||||
Currently its keybindings that have an `id` like `message_redo` and then a command can use that or define it's own binding. While some keybindings are just used with `.match` in arbitrary key handlers and there is no info what the key is used for, except the binding id maybe. It also is unknown in which context/scope what binding is active, so a plugin like `which-key` is nearly impossible to get right.
|
||||
@@ -1,9 +1,16 @@
|
||||
import { Cache, Clock, Duration, Effect, Layer, Option, Schema, SchemaGetter, ServiceMap } from "effect"
|
||||
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||
import {
|
||||
FetchHttpClient,
|
||||
HttpClient,
|
||||
HttpClientError,
|
||||
HttpClientRequest,
|
||||
HttpClientResponse,
|
||||
} from "effect/unstable/http"
|
||||
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { withTransientReadRetry } from "@/util/effect-http-client"
|
||||
import { AccountRepo, type AccountRow } from "./repo"
|
||||
import { normalizeServerUrl } from "./url"
|
||||
import {
|
||||
type AccountError,
|
||||
AccessToken,
|
||||
@@ -12,6 +19,7 @@ import {
|
||||
Info,
|
||||
RefreshToken,
|
||||
AccountServiceError,
|
||||
AccountTransportError,
|
||||
Login,
|
||||
Org,
|
||||
OrgID,
|
||||
@@ -30,6 +38,7 @@ export {
|
||||
type AccountError,
|
||||
AccountRepoError,
|
||||
AccountServiceError,
|
||||
AccountTransportError,
|
||||
AccessToken,
|
||||
RefreshToken,
|
||||
DeviceCode,
|
||||
@@ -52,6 +61,11 @@ export type AccountOrgs = {
|
||||
orgs: readonly Org[]
|
||||
}
|
||||
|
||||
export type ActiveOrg = {
|
||||
account: Info
|
||||
org: Org
|
||||
}
|
||||
|
||||
class RemoteConfig extends Schema.Class<RemoteConfig>("RemoteConfig")({
|
||||
config: Schema.Record(Schema.String, Schema.Json),
|
||||
}) {}
|
||||
@@ -127,16 +141,32 @@ const isTokenFresh = (tokenExpiry: number | null, now: number) =>
|
||||
|
||||
const mapAccountServiceError =
|
||||
(message = "Account service operation failed") =>
|
||||
<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, AccountServiceError, R> =>
|
||||
effect.pipe(
|
||||
Effect.mapError((cause) =>
|
||||
cause instanceof AccountServiceError ? cause : new AccountServiceError({ message, cause }),
|
||||
),
|
||||
)
|
||||
<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, AccountError, R> =>
|
||||
effect.pipe(Effect.mapError((cause) => accountErrorFromCause(cause, message)))
|
||||
|
||||
const accountErrorFromCause = (cause: unknown, message: string): AccountError => {
|
||||
if (cause instanceof AccountServiceError || cause instanceof AccountTransportError) {
|
||||
return cause
|
||||
}
|
||||
|
||||
if (HttpClientError.isHttpClientError(cause)) {
|
||||
switch (cause.reason._tag) {
|
||||
case "TransportError": {
|
||||
return AccountTransportError.fromHttpClientError(cause.reason)
|
||||
}
|
||||
default: {
|
||||
return new AccountServiceError({ message, cause })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new AccountServiceError({ message, cause })
|
||||
}
|
||||
|
||||
export namespace Account {
|
||||
export interface Interface {
|
||||
readonly active: () => Effect.Effect<Option.Option<Info>, AccountError>
|
||||
readonly activeOrg: () => Effect.Effect<Option.Option<ActiveOrg>, AccountError>
|
||||
readonly list: () => Effect.Effect<Info[], AccountError>
|
||||
readonly orgsByAccount: () => Effect.Effect<readonly AccountOrgs[], AccountError>
|
||||
readonly remove: (accountID: AccountID) => Effect.Effect<void, AccountError>
|
||||
@@ -279,19 +309,31 @@ export namespace Account {
|
||||
resolveAccess(accountID).pipe(Effect.map(Option.map((r) => r.accessToken))),
|
||||
)
|
||||
|
||||
const activeOrg = Effect.fn("Account.activeOrg")(function* () {
|
||||
const activeAccount = yield* repo.active()
|
||||
if (Option.isNone(activeAccount)) return Option.none<ActiveOrg>()
|
||||
|
||||
const account = activeAccount.value
|
||||
if (!account.active_org_id) return Option.none<ActiveOrg>()
|
||||
|
||||
const accountOrgs = yield* orgs(account.id)
|
||||
const org = accountOrgs.find((item) => item.id === account.active_org_id)
|
||||
if (!org) return Option.none<ActiveOrg>()
|
||||
|
||||
return Option.some({ account, org })
|
||||
})
|
||||
|
||||
const orgsByAccount = Effect.fn("Account.orgsByAccount")(function* () {
|
||||
const accounts = yield* repo.list()
|
||||
const [errors, results] = yield* Effect.partition(
|
||||
return yield* Effect.forEach(
|
||||
accounts,
|
||||
(account) => orgs(account.id).pipe(Effect.map((orgs) => ({ account, orgs }))),
|
||||
(account) =>
|
||||
orgs(account.id).pipe(
|
||||
Effect.catch(() => Effect.succeed([] as readonly Org[])),
|
||||
Effect.map((orgs) => ({ account, orgs })),
|
||||
),
|
||||
{ concurrency: 3 },
|
||||
)
|
||||
for (const error of errors) {
|
||||
yield* Effect.logWarning("failed to fetch orgs for account").pipe(
|
||||
Effect.annotateLogs({ error: String(error) }),
|
||||
)
|
||||
}
|
||||
return results
|
||||
})
|
||||
|
||||
const orgs = Effect.fn("Account.orgs")(function* (accountID: AccountID) {
|
||||
@@ -328,8 +370,9 @@ export namespace Account {
|
||||
})
|
||||
|
||||
const login = Effect.fn("Account.login")(function* (server: string) {
|
||||
const normalizedServer = normalizeServerUrl(server)
|
||||
const response = yield* executeEffectOk(
|
||||
HttpClientRequest.post(`${server}/auth/device/code`).pipe(
|
||||
HttpClientRequest.post(`${normalizedServer}/auth/device/code`).pipe(
|
||||
HttpClientRequest.acceptJson,
|
||||
HttpClientRequest.schemaBodyJson(ClientId)(new ClientId({ client_id: clientId })),
|
||||
),
|
||||
@@ -341,8 +384,8 @@ export namespace Account {
|
||||
return new Login({
|
||||
code: parsed.device_code,
|
||||
user: parsed.user_code,
|
||||
url: `${server}${parsed.verification_uri_complete}`,
|
||||
server,
|
||||
url: `${normalizedServer}${parsed.verification_uri_complete}`,
|
||||
server: normalizedServer,
|
||||
expiry: parsed.expires_in,
|
||||
interval: parsed.interval,
|
||||
})
|
||||
@@ -396,6 +439,7 @@ export namespace Account {
|
||||
|
||||
return Service.of({
|
||||
active: repo.active,
|
||||
activeOrg,
|
||||
list: repo.list,
|
||||
orgsByAccount,
|
||||
remove: repo.remove,
|
||||
@@ -417,6 +461,26 @@ export namespace Account {
|
||||
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[]> {
|
||||
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) {
|
||||
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)
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Effect, Layer, Option, Schema, ServiceMap } from "effect"
|
||||
import { Database } from "@/storage/db"
|
||||
import { AccountStateTable, AccountTable } from "./account.sql"
|
||||
import { AccessToken, AccountID, AccountRepoError, Info, OrgID, RefreshToken } from "./schema"
|
||||
import { normalizeServerUrl } from "./url"
|
||||
|
||||
export type AccountRow = (typeof AccountTable)["$inferSelect"]
|
||||
|
||||
@@ -125,11 +126,13 @@ export class AccountRepo extends ServiceMap.Service<AccountRepo, AccountRepo.Ser
|
||||
|
||||
const persistAccount = Effect.fn("AccountRepo.persistAccount")((input) =>
|
||||
tx((db) => {
|
||||
const url = normalizeServerUrl(input.url)
|
||||
|
||||
db.insert(AccountTable)
|
||||
.values({
|
||||
id: input.id,
|
||||
email: input.email,
|
||||
url: input.url,
|
||||
url,
|
||||
access_token: input.accessToken,
|
||||
refresh_token: input.refreshToken,
|
||||
token_expiry: input.expiry,
|
||||
@@ -138,7 +141,7 @@ export class AccountRepo extends ServiceMap.Service<AccountRepo, AccountRepo.Ser
|
||||
target: AccountTable.id,
|
||||
set: {
|
||||
email: input.email,
|
||||
url: input.url,
|
||||
url,
|
||||
access_token: input.accessToken,
|
||||
refresh_token: input.refreshToken,
|
||||
token_expiry: input.expiry,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Schema } from "effect"
|
||||
import type * as HttpClientError from "effect/unstable/http/HttpClientError"
|
||||
|
||||
import { withStatics } from "@/util/schema"
|
||||
|
||||
@@ -60,7 +61,34 @@ export class AccountServiceError extends Schema.TaggedErrorClass<AccountServiceE
|
||||
cause: Schema.optional(Schema.Defect),
|
||||
}) {}
|
||||
|
||||
export type AccountError = AccountRepoError | AccountServiceError
|
||||
export class AccountTransportError extends Schema.TaggedErrorClass<AccountTransportError>()("AccountTransportError", {
|
||||
method: Schema.String,
|
||||
url: Schema.String,
|
||||
description: Schema.optional(Schema.String),
|
||||
cause: Schema.optional(Schema.Defect),
|
||||
}) {
|
||||
static fromHttpClientError(error: HttpClientError.TransportError): AccountTransportError {
|
||||
return new AccountTransportError({
|
||||
method: error.request.method,
|
||||
url: error.request.url,
|
||||
description: error.description,
|
||||
cause: error.cause,
|
||||
})
|
||||
}
|
||||
|
||||
override get message(): string {
|
||||
return [
|
||||
`Could not reach ${this.method} ${this.url}.`,
|
||||
`This failed before the server returned an HTTP response.`,
|
||||
this.description,
|
||||
`Check your network, proxy, or VPN configuration and try again.`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
export type AccountError = AccountRepoError | AccountServiceError | AccountTransportError
|
||||
|
||||
export class Login extends Schema.Class<Login>("Login")({
|
||||
code: DeviceCode,
|
||||
|
||||
8
packages/opencode/src/account/url.ts
Normal file
8
packages/opencode/src/account/url.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const normalizeServerUrl = (input: string): string => {
|
||||
const url = new URL(input)
|
||||
url.search = ""
|
||||
url.hash = ""
|
||||
|
||||
const pathname = url.pathname.replace(/\/+$/, "")
|
||||
return pathname.length === 0 ? url.origin : `${url.origin}${pathname}`
|
||||
}
|
||||
@@ -21,6 +21,9 @@ import {
|
||||
type Role,
|
||||
type SessionInfo,
|
||||
type SetSessionModelRequest,
|
||||
type SessionConfigOption,
|
||||
type SetSessionConfigOptionRequest,
|
||||
type SetSessionConfigOptionResponse,
|
||||
type SetSessionModeRequest,
|
||||
type SetSessionModeResponse,
|
||||
type ToolCallContent,
|
||||
@@ -601,6 +604,7 @@ export namespace ACP {
|
||||
|
||||
return {
|
||||
sessionId,
|
||||
configOptions: load.configOptions,
|
||||
models: load.models,
|
||||
modes: load.modes,
|
||||
_meta: load._meta,
|
||||
@@ -660,6 +664,11 @@ export namespace ACP {
|
||||
result.modes.currentModeId = lastUser.agent
|
||||
this.sessionManager.setMode(sessionId, lastUser.agent)
|
||||
}
|
||||
result.configOptions = buildConfigOptions({
|
||||
currentModelId: result.models.currentModelId,
|
||||
availableModels: result.models.availableModels,
|
||||
modes: result.modes,
|
||||
})
|
||||
}
|
||||
|
||||
for (const msg of messages ?? []) {
|
||||
@@ -1266,6 +1275,11 @@ export namespace ACP {
|
||||
availableModels,
|
||||
},
|
||||
modes,
|
||||
configOptions: buildConfigOptions({
|
||||
currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, true),
|
||||
availableModels,
|
||||
modes,
|
||||
}),
|
||||
_meta: buildVariantMeta({
|
||||
model,
|
||||
variant: this.sessionManager.getVariant(sessionId),
|
||||
@@ -1305,6 +1319,44 @@ export namespace ACP {
|
||||
this.sessionManager.setMode(params.sessionId, params.modeId)
|
||||
}
|
||||
|
||||
async setSessionConfigOption(params: SetSessionConfigOptionRequest): Promise<SetSessionConfigOptionResponse> {
|
||||
const session = this.sessionManager.get(params.sessionId)
|
||||
const providers = await this.sdk.config
|
||||
.providers({ directory: session.cwd }, { throwOnError: true })
|
||||
.then((x) => x.data!.providers)
|
||||
const entries = sortProvidersByName(providers)
|
||||
|
||||
if (params.configId === "model") {
|
||||
if (typeof params.value !== "string") throw RequestError.invalidParams("model value must be a string")
|
||||
const selection = parseModelSelection(params.value, providers)
|
||||
this.sessionManager.setModel(session.id, selection.model)
|
||||
this.sessionManager.setVariant(session.id, selection.variant)
|
||||
} else if (params.configId === "mode") {
|
||||
if (typeof params.value !== "string") throw RequestError.invalidParams("mode value must be a string")
|
||||
const availableModes = await this.loadAvailableModes(session.cwd)
|
||||
if (!availableModes.some((mode) => mode.id === params.value)) {
|
||||
throw RequestError.invalidParams(JSON.stringify({ error: `Mode not found: ${params.value}` }))
|
||||
}
|
||||
this.sessionManager.setMode(session.id, params.value)
|
||||
} else {
|
||||
throw RequestError.invalidParams(JSON.stringify({ error: `Unknown config option: ${params.configId}` }))
|
||||
}
|
||||
|
||||
const updatedSession = this.sessionManager.get(session.id)
|
||||
const model = updatedSession.model ?? (await defaultModel(this.config, session.cwd))
|
||||
const availableVariants = modelVariantsFromProviders(entries, model)
|
||||
const currentModelId = formatModelIdWithVariant(model, updatedSession.variant, availableVariants, true)
|
||||
const availableModels = buildAvailableModels(entries, { includeVariants: true })
|
||||
const modeState = await this.resolveModeState(session.cwd, session.id)
|
||||
const modes = modeState.currentModeId
|
||||
? { availableModes: modeState.availableModes, currentModeId: modeState.currentModeId }
|
||||
: undefined
|
||||
|
||||
return {
|
||||
configOptions: buildConfigOptions({ currentModelId, availableModels, modes }),
|
||||
}
|
||||
}
|
||||
|
||||
async prompt(params: PromptRequest) {
|
||||
const sessionID = params.sessionId
|
||||
const session = this.sessionManager.get(sessionID)
|
||||
@@ -1760,4 +1812,36 @@ export namespace ACP {
|
||||
|
||||
return { model: parsed, variant: undefined }
|
||||
}
|
||||
|
||||
function buildConfigOptions(input: {
|
||||
currentModelId: string
|
||||
availableModels: ModelOption[]
|
||||
modes?: { availableModes: ModeOption[]; currentModeId: string } | undefined
|
||||
}): SessionConfigOption[] {
|
||||
const options: SessionConfigOption[] = [
|
||||
{
|
||||
id: "model",
|
||||
name: "Model",
|
||||
category: "model",
|
||||
type: "select",
|
||||
currentValue: input.currentModelId,
|
||||
options: input.availableModels.map((m) => ({ value: m.modelId, name: m.name })),
|
||||
},
|
||||
]
|
||||
if (input.modes) {
|
||||
options.push({
|
||||
id: "mode",
|
||||
name: "Session Mode",
|
||||
category: "mode",
|
||||
type: "select",
|
||||
currentValue: input.modes.currentModeId,
|
||||
options: input.modes.availableModes.map((m) => ({
|
||||
value: m.id,
|
||||
name: m.name,
|
||||
...(m.description ? { description: m.description } : {}),
|
||||
})),
|
||||
})
|
||||
}
|
||||
return options
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ export namespace Auth {
|
||||
export class Api extends Schema.Class<Api>("ApiAuth")({
|
||||
type: Schema.Literal("api"),
|
||||
key: Schema.String,
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||
}) {}
|
||||
|
||||
export class WellKnown extends Schema.Class<WellKnown>("WellKnownAuth")({
|
||||
|
||||
@@ -23,7 +23,7 @@ export const AcpCommand = cmd({
|
||||
process.env.OPENCODE_CLIENT = "acp"
|
||||
await bootstrap(process.cwd(), async () => {
|
||||
const opts = await resolveNetworkOptions(args)
|
||||
const server = Server.listen(opts)
|
||||
const server = await Server.listen(opts)
|
||||
|
||||
const sdk = createOpencodeClient({
|
||||
baseUrl: `http://${server.hostname}:${server.port}`,
|
||||
|
||||
@@ -302,6 +302,11 @@ export const RunCommand = cmd({
|
||||
describe: "show thinking blocks",
|
||||
default: false,
|
||||
})
|
||||
.option("dangerously-skip-permissions", {
|
||||
type: "boolean",
|
||||
describe: "auto-approve permissions that are not explicitly denied (dangerous!)",
|
||||
default: false,
|
||||
})
|
||||
},
|
||||
handler: async (args) => {
|
||||
let message = [...args.message, ...(args["--"] || [])]
|
||||
@@ -544,15 +549,23 @@ export const RunCommand = cmd({
|
||||
if (event.type === "permission.asked") {
|
||||
const permission = event.properties
|
||||
if (permission.sessionID !== sessionID) continue
|
||||
UI.println(
|
||||
UI.Style.TEXT_WARNING_BOLD + "!",
|
||||
UI.Style.TEXT_NORMAL +
|
||||
`permission requested: ${permission.permission} (${permission.patterns.join(", ")}); auto-rejecting`,
|
||||
)
|
||||
await sdk.permission.reply({
|
||||
requestID: permission.id,
|
||||
reply: "reject",
|
||||
})
|
||||
|
||||
if (args["dangerously-skip-permissions"]) {
|
||||
await sdk.permission.reply({
|
||||
requestID: permission.id,
|
||||
reply: "once",
|
||||
})
|
||||
} else {
|
||||
UI.println(
|
||||
UI.Style.TEXT_WARNING_BOLD + "!",
|
||||
UI.Style.TEXT_NORMAL +
|
||||
`permission requested: ${permission.permission} (${permission.patterns.join(", ")}); auto-rejecting`,
|
||||
)
|
||||
await sdk.permission.reply({
|
||||
requestID: permission.id,
|
||||
reply: "reject",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export const ServeCommand = cmd({
|
||||
console.log("Warning: OPENCODE_SERVER_PASSWORD is not set; server is unsecured.")
|
||||
}
|
||||
const opts = await resolveNetworkOptions(args)
|
||||
const server = Server.listen(opts)
|
||||
const server = await Server.listen(opts)
|
||||
console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
|
||||
|
||||
await new Promise(() => {})
|
||||
|
||||
@@ -36,6 +36,7 @@ import { CommandProvider, useCommandDialog } from "@tui/component/dialog-command
|
||||
import { DialogAgent } from "@tui/component/dialog-agent"
|
||||
import { DialogSessionList } from "@tui/component/dialog-session-list"
|
||||
import { DialogWorkspaceList } from "@tui/component/dialog-workspace-list"
|
||||
import { DialogConsoleOrg } from "@tui/component/dialog-console-org"
|
||||
import { KeybindProvider, useKeybind } from "@tui/context/keybind"
|
||||
import { ThemeProvider, useTheme } from "@tui/context/theme"
|
||||
import { Home } from "@tui/routes/home"
|
||||
@@ -124,14 +125,17 @@ import type { EventSource } from "./context/sdk"
|
||||
import { DialogVariant } from "./component/dialog-variant"
|
||||
|
||||
function rendererConfig(_config: TuiConfig.Info): CliRendererConfig {
|
||||
const mouseEnabled = !Flag.OPENCODE_DISABLE_MOUSE && (_config.mouse ?? true)
|
||||
|
||||
return {
|
||||
externalOutputMode: "passthrough",
|
||||
targetFps: 60,
|
||||
gatherStats: false,
|
||||
exitOnCtrlC: false,
|
||||
useKittyKeyboard: { events: process.platform === "win32" },
|
||||
useKittyKeyboard: {},
|
||||
autoFocus: false,
|
||||
openConsoleOnError: false,
|
||||
useMouse: mouseEnabled,
|
||||
consoleOptions: {
|
||||
keyBindings: [{ name: "y", ctrl: true, action: "copy-selection" }],
|
||||
onCopySelection: (text) => {
|
||||
@@ -285,9 +289,6 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
||||
toast,
|
||||
renderer,
|
||||
})
|
||||
onCleanup(() => {
|
||||
api.dispose()
|
||||
})
|
||||
const [ready, setReady] = createSignal(false)
|
||||
TuiPluginRuntime.init(api)
|
||||
.catch((error) => {
|
||||
@@ -598,6 +599,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
||||
{
|
||||
title: "Switch model variant",
|
||||
value: "variant.list",
|
||||
keybind: "variant_list",
|
||||
category: "Agent",
|
||||
hidden: local.model.variant.list().length === 0,
|
||||
slash: {
|
||||
@@ -629,6 +631,23 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
||||
},
|
||||
category: "Provider",
|
||||
},
|
||||
...(sync.data.console_state.switchableOrgCount > 1
|
||||
? [
|
||||
{
|
||||
title: "Switch org",
|
||||
value: "console.org.switch",
|
||||
suggested: Boolean(sync.data.console_state.activeOrgName),
|
||||
slash: {
|
||||
name: "org",
|
||||
aliases: ["orgs", "switch-org"],
|
||||
},
|
||||
onSelect: () => {
|
||||
dialog.replace(() => <DialogConsoleOrg />)
|
||||
},
|
||||
category: "Provider",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: "View status",
|
||||
keybind: "status_view",
|
||||
@@ -654,7 +673,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
||||
category: "System",
|
||||
},
|
||||
{
|
||||
title: "Toggle Theme Mode",
|
||||
title: "Toggle theme mode",
|
||||
value: "theme.switch_mode",
|
||||
onSelect: (dialog) => {
|
||||
setMode(mode() === "dark" ? "light" : "dark")
|
||||
@@ -663,7 +682,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
||||
category: "System",
|
||||
},
|
||||
{
|
||||
title: locked() ? "Unlock Theme Mode" : "Lock Theme Mode",
|
||||
title: locked() ? "Unlock theme mode" : "Lock theme mode",
|
||||
value: "theme.mode.lock",
|
||||
onSelect: (dialog) => {
|
||||
if (locked()) unlock()
|
||||
@@ -740,6 +759,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
||||
keybind: "terminal_suspend",
|
||||
category: "System",
|
||||
hidden: true,
|
||||
enabled: tuiConfig.keybinds?.terminal_suspend !== "none",
|
||||
onSelect: () => {
|
||||
process.once("SIGCONT", () => {
|
||||
renderer.resume()
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { createResource, createMemo } from "solid-js"
|
||||
import { DialogSelect } from "@tui/ui/dialog-select"
|
||||
import { useSDK } from "@tui/context/sdk"
|
||||
import { useDialog } from "@tui/ui/dialog"
|
||||
import { useToast } from "@tui/ui/toast"
|
||||
import { useTheme } from "@tui/context/theme"
|
||||
import type { ExperimentalConsoleListOrgsResponse } from "@opencode-ai/sdk/v2"
|
||||
|
||||
type OrgOption = ExperimentalConsoleListOrgsResponse["orgs"][number]
|
||||
|
||||
const accountHost = (url: string) => {
|
||||
try {
|
||||
return new URL(url).host
|
||||
} catch {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
const accountLabel = (item: Pick<OrgOption, "accountEmail" | "accountUrl">) =>
|
||||
`${item.accountEmail} ${accountHost(item.accountUrl)}`
|
||||
|
||||
export function DialogConsoleOrg() {
|
||||
const sdk = useSDK()
|
||||
const dialog = useDialog()
|
||||
const toast = useToast()
|
||||
const { theme } = useTheme()
|
||||
|
||||
const [orgs] = createResource(async () => {
|
||||
const result = await sdk.client.experimental.console.listOrgs({}, { throwOnError: true })
|
||||
return result.data?.orgs ?? []
|
||||
})
|
||||
|
||||
const current = createMemo(() => orgs()?.find((item) => item.active))
|
||||
|
||||
const options = createMemo(() => {
|
||||
const listed = orgs()
|
||||
if (listed === undefined) {
|
||||
return [
|
||||
{
|
||||
title: "Loading orgs...",
|
||||
value: "loading",
|
||||
onSelect: () => {},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
if (listed.length === 0) {
|
||||
return [
|
||||
{
|
||||
title: "No orgs found",
|
||||
value: "empty",
|
||||
onSelect: () => {},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
return listed
|
||||
.toSorted((a, b) => {
|
||||
const activeAccountA = a.active ? 0 : 1
|
||||
const activeAccountB = b.active ? 0 : 1
|
||||
if (activeAccountA !== activeAccountB) return activeAccountA - activeAccountB
|
||||
|
||||
const accountCompare = accountLabel(a).localeCompare(accountLabel(b))
|
||||
if (accountCompare !== 0) return accountCompare
|
||||
|
||||
return a.orgName.localeCompare(b.orgName)
|
||||
})
|
||||
.map((item) => ({
|
||||
title: item.orgName,
|
||||
value: item,
|
||||
category: accountLabel(item),
|
||||
categoryView: (
|
||||
<box flexDirection="row" gap={2}>
|
||||
<text fg={theme.accent}>{item.accountEmail}</text>
|
||||
<text fg={theme.textMuted}>{accountHost(item.accountUrl)}</text>
|
||||
</box>
|
||||
),
|
||||
onSelect: async () => {
|
||||
if (item.active) {
|
||||
dialog.clear()
|
||||
return
|
||||
}
|
||||
|
||||
await sdk.client.experimental.console.switchOrg(
|
||||
{
|
||||
accountID: item.accountID,
|
||||
orgID: item.orgID,
|
||||
},
|
||||
{ throwOnError: true },
|
||||
)
|
||||
|
||||
await sdk.client.instance.dispose()
|
||||
toast.show({
|
||||
message: `Switched to ${item.orgName}`,
|
||||
variant: "info",
|
||||
})
|
||||
dialog.clear()
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
return <DialogSelect<string | OrgOption> title="Switch org" options={options()} current={current()} />
|
||||
}
|
||||
@@ -132,7 +132,11 @@ export function DialogModel(props: { providerID?: string }) {
|
||||
props.providerID ? sync.data.provider.find((x) => x.id === props.providerID) : null,
|
||||
)
|
||||
|
||||
const title = createMemo(() => provider()?.name ?? "Select model")
|
||||
const title = createMemo(() => {
|
||||
const value = provider()
|
||||
if (!value) return "Select model"
|
||||
return value.name
|
||||
})
|
||||
|
||||
function onSelect(providerID: string, modelID: string) {
|
||||
local.model.set({ providerID, modelID }, { recent: true })
|
||||
|
||||
@@ -13,6 +13,7 @@ import { DialogModel } from "./dialog-model"
|
||||
import { useKeyboard } from "@opentui/solid"
|
||||
import { Clipboard } from "@tui/util/clipboard"
|
||||
import { useToast } from "../ui/toast"
|
||||
import { isConsoleManagedProvider } from "@tui/util/provider-origin"
|
||||
|
||||
const PROVIDER_PRIORITY: Record<string, number> = {
|
||||
opencode: 0,
|
||||
@@ -28,87 +29,115 @@ export function createDialogProviderOptions() {
|
||||
const dialog = useDialog()
|
||||
const sdk = useSDK()
|
||||
const toast = useToast()
|
||||
const { theme } = useTheme()
|
||||
const options = createMemo(() => {
|
||||
return pipe(
|
||||
sync.data.provider_next.all,
|
||||
sortBy((x) => PROVIDER_PRIORITY[x.id] ?? 99),
|
||||
map((provider) => ({
|
||||
title: provider.name,
|
||||
value: provider.id,
|
||||
description: {
|
||||
opencode: "(Recommended)",
|
||||
anthropic: "(API key)",
|
||||
openai: "(ChatGPT Plus/Pro or API key)",
|
||||
"opencode-go": "Low cost subscription for everyone",
|
||||
}[provider.id],
|
||||
category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other",
|
||||
async onSelect() {
|
||||
const methods = sync.data.provider_auth[provider.id] ?? [
|
||||
{
|
||||
type: "api",
|
||||
label: "API key",
|
||||
},
|
||||
]
|
||||
let index: number | null = 0
|
||||
if (methods.length > 1) {
|
||||
index = await new Promise<number | null>((resolve) => {
|
||||
dialog.replace(
|
||||
() => (
|
||||
<DialogSelect
|
||||
title="Select auth method"
|
||||
options={methods.map((x, index) => ({
|
||||
title: x.label,
|
||||
value: index,
|
||||
}))}
|
||||
onSelect={(option) => resolve(option.value)}
|
||||
/>
|
||||
),
|
||||
() => resolve(null),
|
||||
)
|
||||
})
|
||||
}
|
||||
if (index == null) return
|
||||
const method = methods[index]
|
||||
if (method.type === "oauth") {
|
||||
let inputs: Record<string, string> | undefined
|
||||
if (method.prompts?.length) {
|
||||
const value = await PromptsMethod({
|
||||
dialog,
|
||||
prompts: method.prompts,
|
||||
})
|
||||
if (!value) return
|
||||
inputs = value
|
||||
}
|
||||
map((provider) => {
|
||||
const consoleManaged = isConsoleManagedProvider(sync.data.console_state.consoleManagedProviders, provider.id)
|
||||
const connected = sync.data.provider_next.connected.includes(provider.id)
|
||||
|
||||
const result = await sdk.client.provider.oauth.authorize({
|
||||
providerID: provider.id,
|
||||
method: index,
|
||||
inputs,
|
||||
})
|
||||
if (result.error) {
|
||||
toast.show({
|
||||
variant: "error",
|
||||
message: JSON.stringify(result.error),
|
||||
return {
|
||||
title: provider.name,
|
||||
value: provider.id,
|
||||
description: {
|
||||
opencode: "(Recommended)",
|
||||
anthropic: "(API key)",
|
||||
openai: "(ChatGPT Plus/Pro or API key)",
|
||||
"opencode-go": "Low cost subscription for everyone",
|
||||
}[provider.id],
|
||||
footer: consoleManaged ? sync.data.console_state.activeOrgName : undefined,
|
||||
category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other",
|
||||
gutter: connected ? <text fg={theme.success}>✓</text> : undefined,
|
||||
async onSelect() {
|
||||
if (consoleManaged) return
|
||||
|
||||
const methods = sync.data.provider_auth[provider.id] ?? [
|
||||
{
|
||||
type: "api",
|
||||
label: "API key",
|
||||
},
|
||||
]
|
||||
let index: number | null = 0
|
||||
if (methods.length > 1) {
|
||||
index = await new Promise<number | null>((resolve) => {
|
||||
dialog.replace(
|
||||
() => (
|
||||
<DialogSelect
|
||||
title="Select auth method"
|
||||
options={methods.map((x, index) => ({
|
||||
title: x.label,
|
||||
value: index,
|
||||
}))}
|
||||
onSelect={(option) => resolve(option.value)}
|
||||
/>
|
||||
),
|
||||
() => resolve(null),
|
||||
)
|
||||
})
|
||||
dialog.clear()
|
||||
return
|
||||
}
|
||||
if (result.data?.method === "code") {
|
||||
dialog.replace(() => (
|
||||
<CodeMethod providerID={provider.id} title={method.label} index={index} authorization={result.data!} />
|
||||
if (index == null) return
|
||||
const method = methods[index]
|
||||
if (method.type === "oauth") {
|
||||
let inputs: Record<string, string> | undefined
|
||||
if (method.prompts?.length) {
|
||||
const value = await PromptsMethod({
|
||||
dialog,
|
||||
prompts: method.prompts,
|
||||
})
|
||||
if (!value) return
|
||||
inputs = value
|
||||
}
|
||||
|
||||
const result = await sdk.client.provider.oauth.authorize({
|
||||
providerID: provider.id,
|
||||
method: index,
|
||||
inputs,
|
||||
})
|
||||
if (result.error) {
|
||||
toast.show({
|
||||
variant: "error",
|
||||
message: JSON.stringify(result.error),
|
||||
})
|
||||
dialog.clear()
|
||||
return
|
||||
}
|
||||
if (result.data?.method === "code") {
|
||||
dialog.replace(() => (
|
||||
<CodeMethod
|
||||
providerID={provider.id}
|
||||
title={method.label}
|
||||
index={index}
|
||||
authorization={result.data!}
|
||||
/>
|
||||
))
|
||||
}
|
||||
if (result.data?.method === "auto") {
|
||||
dialog.replace(() => (
|
||||
<AutoMethod
|
||||
providerID={provider.id}
|
||||
title={method.label}
|
||||
index={index}
|
||||
authorization={result.data!}
|
||||
/>
|
||||
))
|
||||
}
|
||||
}
|
||||
if (method.type === "api") {
|
||||
let metadata: Record<string, string> | undefined
|
||||
if (method.prompts?.length) {
|
||||
const value = await PromptsMethod({ dialog, prompts: method.prompts })
|
||||
if (!value) return
|
||||
metadata = value
|
||||
}
|
||||
return dialog.replace(() => (
|
||||
<ApiMethod providerID={provider.id} title={method.label} metadata={metadata} />
|
||||
))
|
||||
}
|
||||
if (result.data?.method === "auto") {
|
||||
dialog.replace(() => (
|
||||
<AutoMethod providerID={provider.id} title={method.label} index={index} authorization={result.data!} />
|
||||
))
|
||||
}
|
||||
}
|
||||
if (method.type === "api") {
|
||||
return dialog.replace(() => <ApiMethod providerID={provider.id} title={method.label} />)
|
||||
}
|
||||
},
|
||||
})),
|
||||
},
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
return options
|
||||
@@ -224,6 +253,7 @@ function CodeMethod(props: CodeMethodProps) {
|
||||
interface ApiMethodProps {
|
||||
providerID: string
|
||||
title: string
|
||||
metadata?: Record<string, string>
|
||||
}
|
||||
function ApiMethod(props: ApiMethodProps) {
|
||||
const dialog = useDialog()
|
||||
@@ -268,6 +298,7 @@ function ApiMethod(props: ApiMethodProps) {
|
||||
auth: {
|
||||
type: "api",
|
||||
key: value,
|
||||
...(props.metadata ? { metadata: props.metadata } : {}),
|
||||
},
|
||||
})
|
||||
await sdk.client.instance.dispose()
|
||||
|
||||
@@ -2,6 +2,7 @@ import { BoxRenderable, TextareaRenderable, MouseEvent, PasteEvent, decodePasteB
|
||||
import { createEffect, createMemo, onMount, createSignal, onCleanup, on, Show, Switch, Match } from "solid-js"
|
||||
import "opentui-spinner/solid"
|
||||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { useLocal } from "@tui/context/local"
|
||||
import { useTheme } from "@tui/context/theme"
|
||||
@@ -18,11 +19,11 @@ import { usePromptStash } from "./stash"
|
||||
import { DialogStash } from "../dialog-stash"
|
||||
import { type AutocompleteRef, Autocomplete } from "./autocomplete"
|
||||
import { useCommandDialog } from "../dialog-command"
|
||||
import { useKeyboard, useRenderer, type JSX } from "@opentui/solid"
|
||||
import { useRenderer, type JSX } from "@opentui/solid"
|
||||
import { Editor } from "@tui/util/editor"
|
||||
import { useExit } from "../../context/exit"
|
||||
import { Clipboard } from "../../util/clipboard"
|
||||
import type { AssistantMessage, FilePart } from "@opencode-ai/sdk/v2"
|
||||
import type { AssistantMessage, FilePart, UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { TuiEvent } from "../../event"
|
||||
import { iife } from "@/util/iife"
|
||||
import { Locale } from "@/util/locale"
|
||||
@@ -94,6 +95,8 @@ export function Prompt(props: PromptProps) {
|
||||
const list = createMemo(() => props.placeholders?.normal ?? [])
|
||||
const shell = createMemo(() => props.placeholders?.shell ?? [])
|
||||
const [auto, setAuto] = createSignal<AutocompleteRef>()
|
||||
const currentProviderLabel = createMemo(() => local.model.parsed().provider)
|
||||
const hasRightContent = createMemo(() => Boolean(props.right))
|
||||
|
||||
function promptModelWarning() {
|
||||
toast.show({
|
||||
@@ -134,7 +137,7 @@ export function Prompt(props: PromptProps) {
|
||||
if (!props.sessionID) return undefined
|
||||
const messages = sync.data.message[props.sessionID]
|
||||
if (!messages) return undefined
|
||||
return messages.findLast((m) => m.role === "user")
|
||||
return messages.findLast((m): m is UserMessage => m.role === "user")
|
||||
})
|
||||
|
||||
const usage = createMemo(() => {
|
||||
@@ -198,8 +201,10 @@ export function Prompt(props: PromptProps) {
|
||||
const isPrimaryAgent = local.agent.list().some((x) => x.name === msg.agent)
|
||||
if (msg.agent && isPrimaryAgent) {
|
||||
local.agent.set(msg.agent)
|
||||
if (msg.model) local.model.set(msg.model)
|
||||
if (msg.variant) local.model.variant.set(msg.variant)
|
||||
if (msg.model) {
|
||||
local.model.set(msg.model)
|
||||
local.model.variant.set(msg.model.variant)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -238,7 +243,7 @@ export function Prompt(props: PromptProps) {
|
||||
onSelect: async () => {
|
||||
const content = await Clipboard.read()
|
||||
if (content?.mime.startsWith("image/")) {
|
||||
await pasteImage({
|
||||
await pasteAttachment({
|
||||
filename: "clipboard",
|
||||
mime: content.mime,
|
||||
content: content.data,
|
||||
@@ -390,20 +395,6 @@ export function Prompt(props: PromptProps) {
|
||||
]
|
||||
})
|
||||
|
||||
// Windows Terminal 1.25+ handles Ctrl+V on keydown when kitty events are
|
||||
// enabled, but still reports the kitty key-release event. Probe on release.
|
||||
if (process.platform === "win32") {
|
||||
useKeyboard(
|
||||
(evt) => {
|
||||
if (!input.focused) return
|
||||
if (evt.name === "v" && evt.ctrl && evt.eventType === "release") {
|
||||
command.trigger("prompt.paste")
|
||||
}
|
||||
},
|
||||
{ release: true },
|
||||
)
|
||||
}
|
||||
|
||||
const ref: PromptRef = {
|
||||
get focused() {
|
||||
return input.focused
|
||||
@@ -775,11 +766,16 @@ export function Prompt(props: PromptProps) {
|
||||
)
|
||||
}
|
||||
|
||||
async function pasteImage(file: { filename?: string; content: string; mime: string }) {
|
||||
async function pasteAttachment(file: { filename?: string; filepath?: string; content: string; mime: string }) {
|
||||
const currentOffset = input.visualCursor.offset
|
||||
const extmarkStart = currentOffset
|
||||
const count = store.prompt.parts.filter((x) => x.type === "file" && x.mime.startsWith("image/")).length
|
||||
const virtualText = `[Image ${count + 1}]`
|
||||
const pdf = file.mime === "application/pdf"
|
||||
const count = store.prompt.parts.filter((x) => {
|
||||
if (x.type !== "file") return false
|
||||
if (pdf) return x.mime === "application/pdf"
|
||||
return x.mime.startsWith("image/")
|
||||
}).length
|
||||
const virtualText = pdf ? `[PDF ${count + 1}]` : `[Image ${count + 1}]`
|
||||
const extmarkEnd = extmarkStart + virtualText.length
|
||||
const textToInsert = virtualText + " "
|
||||
|
||||
@@ -800,7 +796,7 @@ export function Prompt(props: PromptProps) {
|
||||
url: `data:${file.mime};base64,${file.content}`,
|
||||
source: {
|
||||
type: "file",
|
||||
path: file.filename ?? "",
|
||||
path: file.filepath ?? file.filename ?? "",
|
||||
text: {
|
||||
start: extmarkStart,
|
||||
end: extmarkEnd,
|
||||
@@ -930,7 +926,7 @@ export function Prompt(props: PromptProps) {
|
||||
const content = await Clipboard.read()
|
||||
if (content?.mime.startsWith("image/")) {
|
||||
e.preventDefault()
|
||||
await pasteImage({
|
||||
await pasteAttachment({
|
||||
filename: "clipboard",
|
||||
mime: content.mime,
|
||||
content: content.data,
|
||||
@@ -1016,9 +1012,16 @@ export function Prompt(props: PromptProps) {
|
||||
return
|
||||
}
|
||||
|
||||
// trim ' from the beginning and end of the pasted content. just
|
||||
// ' and nothing else
|
||||
const filepath = pastedContent.replace(/^'+|'+$/g, "").replace(/\\ /g, " ")
|
||||
const filepath = iife(() => {
|
||||
const raw = pastedContent.replace(/^['"]+|['"]+$/g, "")
|
||||
if (raw.startsWith("file://")) {
|
||||
try {
|
||||
return fileURLToPath(raw)
|
||||
} catch {}
|
||||
}
|
||||
if (process.platform === "win32") return raw
|
||||
return raw.replace(/\\(.)/g, "$1")
|
||||
})
|
||||
const isUrl = /^(https?):\/\//.test(filepath)
|
||||
if (!isUrl) {
|
||||
try {
|
||||
@@ -1033,14 +1036,15 @@ export function Prompt(props: PromptProps) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if (mime.startsWith("image/")) {
|
||||
if (mime.startsWith("image/") || mime === "application/pdf") {
|
||||
event.preventDefault()
|
||||
const content = await Filesystem.readArrayBuffer(filepath)
|
||||
.then((buffer) => Buffer.from(buffer).toString("base64"))
|
||||
.catch(() => {})
|
||||
if (content) {
|
||||
await pasteImage({
|
||||
await pasteAttachment({
|
||||
filename,
|
||||
filepath,
|
||||
mime,
|
||||
content,
|
||||
})
|
||||
@@ -1095,7 +1099,7 @@ export function Prompt(props: PromptProps) {
|
||||
<text flexShrink={0} fg={keybind.leader ? theme.textMuted : theme.text}>
|
||||
{local.model.parsed().model}
|
||||
</text>
|
||||
<text fg={theme.textMuted}>{local.model.parsed().provider}</text>
|
||||
<text fg={theme.textMuted}>{currentProviderLabel()}</text>
|
||||
<Show when={showVariant()}>
|
||||
<text fg={theme.textMuted}>·</text>
|
||||
<text>
|
||||
@@ -1105,7 +1109,11 @@ export function Prompt(props: PromptProps) {
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
{props.right}
|
||||
<Show when={hasRightContent()}>
|
||||
<box flexDirection="row" gap={1} alignItems="center">
|
||||
{props.right}
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
</box>
|
||||
</box>
|
||||
@@ -1135,7 +1143,7 @@ export function Prompt(props: PromptProps) {
|
||||
}
|
||||
/>
|
||||
</box>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<box width="100%" flexDirection="row" justifyContent="space-between">
|
||||
<Show when={status().type !== "idle"} fallback={props.hint ?? <text />}>
|
||||
<box
|
||||
flexDirection="row"
|
||||
|
||||
@@ -4,8 +4,7 @@ import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
||||
import { batch, onCleanup, onMount } from "solid-js"
|
||||
|
||||
export type EventSource = {
|
||||
on: (handler: (event: Event) => void) => () => void
|
||||
setWorkspace?: (workspaceID?: string) => void
|
||||
subscribe: (directory: string | undefined, handler: (event: Event) => void) => Promise<() => void>
|
||||
}
|
||||
|
||||
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||
@@ -18,7 +17,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||
events?: EventSource
|
||||
}) => {
|
||||
const abort = new AbortController()
|
||||
let workspaceID: string | undefined
|
||||
let sse: AbortController | undefined
|
||||
|
||||
function createSDK() {
|
||||
@@ -28,7 +26,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||
directory: props.directory,
|
||||
fetch: props.fetch,
|
||||
headers: props.headers,
|
||||
experimental_workspaceID: workspaceID,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -90,9 +87,9 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||
})().catch(() => {})
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
onMount(async () => {
|
||||
if (props.events) {
|
||||
const unsub = props.events.on(handleEvent)
|
||||
const unsub = await props.events.subscribe(props.directory, handleEvent)
|
||||
onCleanup(unsub)
|
||||
} else {
|
||||
startSSE()
|
||||
@@ -109,19 +106,9 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
||||
get client() {
|
||||
return sdk
|
||||
},
|
||||
get workspaceID() {
|
||||
return workspaceID
|
||||
},
|
||||
directory: props.directory,
|
||||
event: emitter,
|
||||
fetch: props.fetch ?? fetch,
|
||||
setWorkspace(next?: string) {
|
||||
if (workspaceID === next) return
|
||||
workspaceID = next
|
||||
sdk = createSDK()
|
||||
props.events?.setWorkspace?.(next)
|
||||
if (!props.events) startSSE()
|
||||
},
|
||||
url: props.url,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -29,6 +29,7 @@ import { batch, onMount } from "solid-js"
|
||||
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"
|
||||
|
||||
export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
name: "Sync",
|
||||
@@ -38,6 +39,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
provider: Provider[]
|
||||
provider_default: Record<string, string>
|
||||
provider_next: ProviderListResponse
|
||||
console_state: ConsoleStateType
|
||||
provider_auth: Record<string, ProviderAuthMethod[]>
|
||||
agent: Agent[]
|
||||
command: Command[]
|
||||
@@ -81,6 +83,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
default: {},
|
||||
connected: [],
|
||||
},
|
||||
console_state: emptyConsoleState,
|
||||
provider_auth: {},
|
||||
config: {},
|
||||
status: "loading",
|
||||
@@ -365,6 +368,10 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
// blocking - include session.list when continuing a session
|
||||
const providersPromise = sdk.client.config.providers({}, { throwOnError: true })
|
||||
const providerListPromise = sdk.client.provider.list({}, { throwOnError: true })
|
||||
const consoleStatePromise = sdk.client.experimental.console
|
||||
.get({}, { throwOnError: true })
|
||||
.then((x) => ConsoleState.parse(x.data))
|
||||
.catch(() => emptyConsoleState)
|
||||
const agentsPromise = sdk.client.app.agents({}, { throwOnError: true })
|
||||
const configPromise = sdk.client.config.get({}, { throwOnError: true })
|
||||
const blockingRequests: Promise<unknown>[] = [
|
||||
@@ -379,6 +386,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
.then(() => {
|
||||
const providersResponse = providersPromise.then((x) => x.data!)
|
||||
const providerListResponse = providerListPromise.then((x) => x.data!)
|
||||
const consoleStateResponse = consoleStatePromise
|
||||
const agentsResponse = agentsPromise.then((x) => x.data ?? [])
|
||||
const configResponse = configPromise.then((x) => x.data!)
|
||||
const sessionListResponse = args.continue ? sessionListPromise : undefined
|
||||
@@ -386,20 +394,23 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
return Promise.all([
|
||||
providersResponse,
|
||||
providerListResponse,
|
||||
consoleStateResponse,
|
||||
agentsResponse,
|
||||
configResponse,
|
||||
...(sessionListResponse ? [sessionListResponse] : []),
|
||||
]).then((responses) => {
|
||||
const providers = responses[0]
|
||||
const providerList = responses[1]
|
||||
const agents = responses[2]
|
||||
const config = responses[3]
|
||||
const sessions = responses[4]
|
||||
const consoleState = responses[2]
|
||||
const agents = responses[3]
|
||||
const config = responses[4]
|
||||
const sessions = responses[5]
|
||||
|
||||
batch(() => {
|
||||
setStore("provider", reconcile(providers.providers))
|
||||
setStore("provider_default", reconcile(providers.default))
|
||||
setStore("provider_next", reconcile(providerList))
|
||||
setStore("console_state", reconcile(consoleState))
|
||||
setStore("agent", reconcile(agents))
|
||||
setStore("config", reconcile(config))
|
||||
if (sessions !== undefined) setStore("session", reconcile(sessions))
|
||||
@@ -411,6 +422,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
// non-blocking
|
||||
Promise.all([
|
||||
...(args.continue ? [] : [sessionListPromise.then((sessions) => setStore("session", reconcile(sessions)))]),
|
||||
consoleStatePromise.then((consoleState) => setStore("console_state", reconcile(consoleState))),
|
||||
sdk.client.command.list().then((x) => setStore("command", reconcile(x.data ?? []))),
|
||||
sdk.client.lsp.status().then((x) => setStore("lsp", reconcile(x.data!))),
|
||||
sdk.client.mcp.status().then((x) => setStore("mcp", reconcile(x.data!))),
|
||||
|
||||
@@ -55,7 +55,7 @@ const TIPS = [
|
||||
"Use {highlight}/undo{/highlight} to revert the last message and file changes",
|
||||
"Use {highlight}/redo{/highlight} to restore previously undone messages and file changes",
|
||||
"Run {highlight}/share{/highlight} to create a public link to your conversation at opencode.ai",
|
||||
"Drag and drop images into the terminal to add them as context",
|
||||
"Drag and drop images or PDFs into the terminal to add them as context",
|
||||
"Press {highlight}Ctrl+V{/highlight} to paste images from your clipboard into the prompt",
|
||||
"Press {highlight}Ctrl+X E{/highlight} or {highlight}/editor{/highlight} to compose messages in your external editor",
|
||||
"Run {highlight}/init{/highlight} to auto-generate project rules based on your codebase",
|
||||
@@ -148,5 +148,7 @@ const TIPS = [
|
||||
"Use {highlight}/review{/highlight} to review uncommitted changes, branches, or PRs",
|
||||
"Run {highlight}/help{/highlight} or {highlight}Ctrl+X H{/highlight} to show the help dialog",
|
||||
"Use {highlight}/rename{/highlight} to rename the current session",
|
||||
"Press {highlight}Ctrl+Z{/highlight} to suspend the terminal and return to your shell",
|
||||
...(process.platform === "win32"
|
||||
? ["Press {highlight}Ctrl+Z{/highlight} to undo changes in your prompt"]
|
||||
: ["Press {highlight}Ctrl+Z{/highlight} to suspend the terminal and return to your shell"]),
|
||||
]
|
||||
|
||||
@@ -18,7 +18,7 @@ import { Prompt } from "../component/prompt"
|
||||
import { Slot as HostSlot } from "./slots"
|
||||
import type { useToast } from "../ui/toast"
|
||||
import { Installation } from "@/installation"
|
||||
import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk/v2"
|
||||
import { type OpencodeClient } from "@opencode-ai/sdk/v2"
|
||||
|
||||
type RouteEntry = {
|
||||
key: symbol
|
||||
@@ -43,11 +43,6 @@ type Input = {
|
||||
renderer: TuiPluginApi["renderer"]
|
||||
}
|
||||
|
||||
type TuiHostPluginApi = TuiPluginApi & {
|
||||
map: Map<string | undefined, OpencodeClient>
|
||||
dispose: () => void
|
||||
}
|
||||
|
||||
function routeRegister(routes: RouteMap, list: TuiRouteDefinition[], bump: () => void) {
|
||||
const key = Symbol()
|
||||
for (const item of list) {
|
||||
@@ -206,29 +201,7 @@ function appApi(): TuiPluginApi["app"] {
|
||||
}
|
||||
}
|
||||
|
||||
export function createTuiApi(input: Input): TuiHostPluginApi {
|
||||
const map = new Map<string | undefined, OpencodeClient>()
|
||||
const scoped: TuiPluginApi["scopedClient"] = (workspaceID) => {
|
||||
const hit = map.get(workspaceID)
|
||||
if (hit) return hit
|
||||
|
||||
const next = createOpencodeClient({
|
||||
baseUrl: input.sdk.url,
|
||||
fetch: input.sdk.fetch,
|
||||
directory: input.sync.data.path.directory || input.sdk.directory,
|
||||
experimental_workspaceID: workspaceID,
|
||||
})
|
||||
map.set(workspaceID, next)
|
||||
return next
|
||||
}
|
||||
const workspace: TuiPluginApi["workspace"] = {
|
||||
current() {
|
||||
return input.sdk.workspaceID
|
||||
},
|
||||
set(workspaceID) {
|
||||
input.sdk.setWorkspace(workspaceID)
|
||||
},
|
||||
}
|
||||
export function createTuiApi(input: Input): TuiPluginApi {
|
||||
const lifecycle: TuiPluginApi["lifecycle"] = {
|
||||
signal: new AbortController().signal,
|
||||
onDispose() {
|
||||
@@ -369,8 +342,6 @@ export function createTuiApi(input: Input): TuiHostPluginApi {
|
||||
get client() {
|
||||
return input.sdk.client
|
||||
},
|
||||
scopedClient: scoped,
|
||||
workspace,
|
||||
event: input.sdk.event,
|
||||
renderer: input.renderer,
|
||||
slots: {
|
||||
@@ -422,9 +393,5 @@ export function createTuiApi(input: Input): TuiHostPluginApi {
|
||||
return input.theme.ready
|
||||
},
|
||||
},
|
||||
map,
|
||||
dispose() {
|
||||
map.clear()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -543,8 +543,6 @@ function pluginApi(runtime: RuntimeState, plugin: PluginEntry, scope: PluginScop
|
||||
get client() {
|
||||
return api.client
|
||||
},
|
||||
scopedClient: api.scopedClient,
|
||||
workspace: api.workspace,
|
||||
event,
|
||||
renderer: api.renderer,
|
||||
slots,
|
||||
|
||||
@@ -167,12 +167,6 @@ export function Session() {
|
||||
|
||||
const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig))
|
||||
|
||||
createEffect(() => {
|
||||
if (session()?.workspaceID) {
|
||||
sdk.setWorkspace(session()?.workspaceID)
|
||||
}
|
||||
})
|
||||
|
||||
createEffect(async () => {
|
||||
await sync.session
|
||||
.sync(route.sessionID)
|
||||
|
||||
@@ -43,9 +43,18 @@ function createWorkerFetch(client: RpcClient): typeof fetch {
|
||||
|
||||
function createEventSource(client: RpcClient): EventSource {
|
||||
return {
|
||||
on: (handler) => client.on<Event>("event", handler),
|
||||
setWorkspace: (workspaceID) => {
|
||||
void client.call("setWorkspace", { workspaceID })
|
||||
subscribe: async (directory, handler) => {
|
||||
const id = await client.call("subscribe", { directory })
|
||||
const unsub = client.on<{ id: string; event: Event }>("event", (e) => {
|
||||
if (e.id === id) {
|
||||
handler(e.event)
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
unsub()
|
||||
client.call("unsubscribe", { id })
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ export interface DialogSelectOption<T = any> {
|
||||
description?: string
|
||||
footer?: JSX.Element | string
|
||||
category?: string
|
||||
categoryView?: JSX.Element
|
||||
disabled?: boolean
|
||||
bg?: RGBA
|
||||
gutter?: JSX.Element
|
||||
@@ -291,9 +292,16 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
<>
|
||||
<Show when={category}>
|
||||
<box paddingTop={index() > 0 ? 1 : 0} paddingLeft={3}>
|
||||
<text fg={theme.accent} attributes={TextAttributes.BOLD}>
|
||||
{category}
|
||||
</text>
|
||||
<Show
|
||||
when={options[0]?.categoryView}
|
||||
fallback={
|
||||
<text fg={theme.accent} attributes={TextAttributes.BOLD}>
|
||||
{category}
|
||||
</text>
|
||||
}
|
||||
>
|
||||
{options[0]?.categoryView}
|
||||
</Show>
|
||||
</box>
|
||||
</Show>
|
||||
<For each={options}>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
const contains = (consoleManagedProviders: string[] | ReadonlySet<string>, providerID: string) =>
|
||||
Array.isArray(consoleManagedProviders)
|
||||
? consoleManagedProviders.includes(providerID)
|
||||
: consoleManagedProviders.has(providerID)
|
||||
|
||||
export const isConsoleManagedProvider = (consoleManagedProviders: string[] | ReadonlySet<string>, providerID: string) =>
|
||||
contains(consoleManagedProviders, providerID)
|
||||
@@ -45,20 +45,20 @@ GlobalBus.on("event", (event) => {
|
||||
|
||||
let server: Awaited<ReturnType<typeof Server.listen>> | undefined
|
||||
|
||||
const eventStream = {
|
||||
abort: undefined as AbortController | undefined,
|
||||
}
|
||||
const eventStreams = new Map<string, AbortController>()
|
||||
|
||||
function startEventStream(directory: string) {
|
||||
const id = crypto.randomUUID()
|
||||
|
||||
const startEventStream = (input: { directory: string; workspaceID?: string }) => {
|
||||
if (eventStream.abort) eventStream.abort.abort()
|
||||
const abort = new AbortController()
|
||||
eventStream.abort = abort
|
||||
const signal = abort.signal
|
||||
|
||||
;(async () => {
|
||||
eventStreams.set(id, abort)
|
||||
|
||||
async function run() {
|
||||
while (!signal.aborted) {
|
||||
const shouldReconnect = await Instance.provide({
|
||||
directory: input.directory,
|
||||
directory,
|
||||
init: InstanceBootstrap,
|
||||
fn: () =>
|
||||
new Promise<boolean>((resolve) => {
|
||||
@@ -77,7 +77,10 @@ const startEventStream = (input: { directory: string; workspaceID?: string }) =>
|
||||
}
|
||||
|
||||
const unsub = Bus.subscribeAll((event) => {
|
||||
Rpc.emit("event", event as Event)
|
||||
Rpc.emit("event", {
|
||||
id,
|
||||
event: event as Event,
|
||||
})
|
||||
if (event.type === Bus.InstanceDisposed.type) {
|
||||
settle(true)
|
||||
}
|
||||
@@ -104,14 +107,24 @@ const startEventStream = (input: { directory: string; workspaceID?: string }) =>
|
||||
await sleep(250)
|
||||
}
|
||||
}
|
||||
})().catch((error) => {
|
||||
}
|
||||
|
||||
run().catch((error) => {
|
||||
Log.Default.error("event stream error", {
|
||||
error: error instanceof Error ? error.message : error,
|
||||
})
|
||||
})
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
startEventStream({ directory: process.cwd() })
|
||||
function stopEventStream(id: string) {
|
||||
const abortController = eventStreams.get(id)
|
||||
if (!abortController) return
|
||||
|
||||
abortController.abort()
|
||||
eventStreams.delete(id)
|
||||
}
|
||||
|
||||
export const rpc = {
|
||||
async fetch(input: { url: string; method: string; headers: Record<string, string>; body?: string }) {
|
||||
@@ -154,12 +167,19 @@ export const rpc = {
|
||||
async reload() {
|
||||
await Config.invalidate(true)
|
||||
},
|
||||
async setWorkspace(input: { workspaceID?: string }) {
|
||||
startEventStream({ directory: process.cwd(), workspaceID: input.workspaceID })
|
||||
async subscribe(input: { directory: string | undefined }) {
|
||||
return startEventStream(input.directory || process.cwd())
|
||||
},
|
||||
async unsubscribe(input: { id: string }) {
|
||||
stopEventStream(input.id)
|
||||
},
|
||||
async shutdown() {
|
||||
Log.Default.info("worker shutting down")
|
||||
if (eventStream.abort) eventStream.abort.abort()
|
||||
|
||||
for (const id of [...eventStreams.keys()]) {
|
||||
stopEventStream(id)
|
||||
}
|
||||
|
||||
await Instance.disposeAll()
|
||||
if (server) await server.stop(true)
|
||||
},
|
||||
|
||||
@@ -37,7 +37,7 @@ export const WebCommand = cmd({
|
||||
UI.println(UI.Style.TEXT_WARNING_BOLD + "! " + "OPENCODE_SERVER_PASSWORD is not set; server is unsecured.")
|
||||
}
|
||||
const opts = await resolveNetworkOptions(args)
|
||||
const server = Server.listen(opts)
|
||||
const server = await Server.listen(opts)
|
||||
UI.empty()
|
||||
UI.println(UI.logo(" "))
|
||||
UI.empty()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AccountServiceError, AccountTransportError } from "@/account"
|
||||
import { ConfigMarkdown } from "@/config/markdown"
|
||||
import { errorFormat } from "@/util/error"
|
||||
import { Config } from "../config/config"
|
||||
@@ -8,6 +9,9 @@ import { UI } from "./ui"
|
||||
export function FormatError(input: unknown) {
|
||||
if (MCP.Failed.isInstance(input))
|
||||
return `MCP server "${input.data.name}" failed. Note, opencode does not support MCP authentication yet.`
|
||||
if (input instanceof AccountTransportError || input instanceof AccountServiceError) {
|
||||
return input.message
|
||||
}
|
||||
if (Provider.ModelNotFoundError.isInstance(input)) {
|
||||
const { providerID, modelID, suggestions } = input.data
|
||||
return [
|
||||
|
||||
@@ -33,6 +33,7 @@ import { Account } from "@/account"
|
||||
import { isRecord } from "@/util/record"
|
||||
import { ConfigPaths } from "./paths"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import type { ConsoleState } from "./console-state"
|
||||
import { AppFileSystem } from "@/filesystem"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
@@ -668,6 +669,7 @@ export namespace Config {
|
||||
agent_cycle: z.string().optional().default("tab").describe("Next agent"),
|
||||
agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"),
|
||||
variant_cycle: z.string().optional().default("ctrl+t").describe("Cycle model variants"),
|
||||
variant_list: z.string().optional().default("none").describe("List model variants"),
|
||||
input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
|
||||
input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
|
||||
input_submit: z.string().optional().default("return").describe("Submit input"),
|
||||
@@ -1050,11 +1052,13 @@ export namespace Config {
|
||||
config: Info
|
||||
directories: string[]
|
||||
deps: Promise<void>[]
|
||||
consoleState: ConsoleState
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly get: () => Effect.Effect<Info>
|
||||
readonly getGlobal: () => Effect.Effect<Info>
|
||||
readonly getConsoleState: () => Effect.Effect<ConsoleState>
|
||||
readonly update: (config: Info) => Effect.Effect<void>
|
||||
readonly updateGlobal: (config: Info) => Effect.Effect<Info>
|
||||
readonly invalidate: (wait?: boolean) => Effect.Effect<void>
|
||||
@@ -1260,6 +1264,8 @@ export namespace Config {
|
||||
const auth = yield* authSvc.all().pipe(Effect.orDie)
|
||||
|
||||
let result: Info = {}
|
||||
const consoleManagedProviders = new Set<string>()
|
||||
let activeOrgName: string | undefined
|
||||
|
||||
const scope = (source: string): PluginScope => {
|
||||
if (source.startsWith("http://") || source.startsWith("https://")) return "global"
|
||||
@@ -1371,26 +1377,31 @@ export namespace Config {
|
||||
log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT")
|
||||
}
|
||||
|
||||
const active = Option.getOrUndefined(yield* accountSvc.active().pipe(Effect.orDie))
|
||||
if (active?.active_org_id) {
|
||||
const activeOrg = Option.getOrUndefined(
|
||||
yield* accountSvc.activeOrg().pipe(Effect.catch(() => Effect.succeed(Option.none()))),
|
||||
)
|
||||
if (activeOrg) {
|
||||
yield* Effect.gen(function* () {
|
||||
const [configOpt, tokenOpt] = yield* Effect.all(
|
||||
[accountSvc.config(active.id, active.active_org_id!), accountSvc.token(active.id)],
|
||||
[accountSvc.config(activeOrg.account.id, activeOrg.org.id), accountSvc.token(activeOrg.account.id)],
|
||||
{ concurrency: 2 },
|
||||
)
|
||||
const token = Option.getOrUndefined(tokenOpt)
|
||||
if (token) {
|
||||
process.env["OPENCODE_CONSOLE_TOKEN"] = token
|
||||
Env.set("OPENCODE_CONSOLE_TOKEN", token)
|
||||
if (Option.isSome(tokenOpt)) {
|
||||
process.env["OPENCODE_CONSOLE_TOKEN"] = tokenOpt.value
|
||||
Env.set("OPENCODE_CONSOLE_TOKEN", tokenOpt.value)
|
||||
}
|
||||
|
||||
const config = Option.getOrUndefined(configOpt)
|
||||
if (config) {
|
||||
const source = `${active.url}/api/config`
|
||||
const next = yield* loadConfig(JSON.stringify(config), {
|
||||
activeOrgName = activeOrg.org.name
|
||||
|
||||
if (Option.isSome(configOpt)) {
|
||||
const source = `${activeOrg.account.url}/api/config`
|
||||
const next = yield* loadConfig(JSON.stringify(configOpt.value), {
|
||||
dir: path.dirname(source),
|
||||
source,
|
||||
})
|
||||
for (const providerID of Object.keys(next.provider ?? {})) {
|
||||
consoleManagedProviders.add(providerID)
|
||||
}
|
||||
merge(source, next, "global")
|
||||
}
|
||||
}).pipe(
|
||||
@@ -1456,6 +1467,11 @@ export namespace Config {
|
||||
config: result,
|
||||
directories,
|
||||
deps,
|
||||
consoleState: {
|
||||
consoleManagedProviders: Array.from(consoleManagedProviders),
|
||||
activeOrgName,
|
||||
switchableOrgCount: 0,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1473,6 +1489,10 @@ export namespace Config {
|
||||
return yield* InstanceState.use(state, (s) => s.directories)
|
||||
})
|
||||
|
||||
const getConsoleState = Effect.fn("Config.getConsoleState")(function* () {
|
||||
return yield* InstanceState.use(state, (s) => s.consoleState)
|
||||
})
|
||||
|
||||
const waitForDependencies = Effect.fn("Config.waitForDependencies")(function* () {
|
||||
yield* InstanceState.useEffect(state, (s) => Effect.promise(() => Promise.all(s.deps).then(() => undefined)))
|
||||
})
|
||||
@@ -1528,6 +1548,7 @@ export namespace Config {
|
||||
return Service.of({
|
||||
get,
|
||||
getGlobal,
|
||||
getConsoleState,
|
||||
update,
|
||||
updateGlobal,
|
||||
invalidate,
|
||||
@@ -1553,6 +1574,10 @@ export namespace Config {
|
||||
return runPromise((svc) => svc.getGlobal())
|
||||
}
|
||||
|
||||
export async function getConsoleState() {
|
||||
return runPromise((svc) => svc.getConsoleState())
|
||||
}
|
||||
|
||||
export async function update(config: Info) {
|
||||
return runPromise((svc) => svc.update(config))
|
||||
}
|
||||
|
||||
15
packages/opencode/src/config/console-state.ts
Normal file
15
packages/opencode/src/config/console-state.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import z from "zod"
|
||||
|
||||
export const ConsoleState = z.object({
|
||||
consoleManagedProviders: z.array(z.string()),
|
||||
activeOrgName: z.string().optional(),
|
||||
switchableOrgCount: z.number().int().nonnegative(),
|
||||
})
|
||||
|
||||
export type ConsoleState = z.infer<typeof ConsoleState>
|
||||
|
||||
export const emptyConsoleState: ConsoleState = {
|
||||
consoleManagedProviders: [],
|
||||
activeOrgName: undefined,
|
||||
switchableOrgCount: 0,
|
||||
}
|
||||
@@ -22,6 +22,7 @@ export const TuiOptions = z.object({
|
||||
.enum(["auto", "stacked"])
|
||||
.optional()
|
||||
.describe("Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column"),
|
||||
mouse: z.boolean().optional().describe("Enable or disable mouse capture (default: true)"),
|
||||
})
|
||||
|
||||
export const TuiInfo = z
|
||||
|
||||
@@ -111,7 +111,15 @@ export namespace TuiConfig {
|
||||
}
|
||||
}
|
||||
|
||||
acc.result.keybinds = Config.Keybinds.parse(acc.result.keybinds ?? {})
|
||||
const keybinds = { ...(acc.result.keybinds ?? {}) }
|
||||
if (process.platform === "win32") {
|
||||
// Native Windows terminals do not support POSIX suspend, so prefer prompt undo.
|
||||
keybinds.terminal_suspend = "none"
|
||||
keybinds.input_undo ??= unique(["ctrl+z", ...Config.Keybinds.shape.input_undo.parse(undefined).split(",")]).join(
|
||||
",",
|
||||
)
|
||||
}
|
||||
acc.result.keybinds = Config.Keybinds.parse(keybinds)
|
||||
|
||||
const deps: Promise<void>[] = []
|
||||
if (acc.result.plugin?.length) {
|
||||
|
||||
@@ -32,7 +32,11 @@ export const WorktreeAdaptor: Adaptor = {
|
||||
const config = Config.parse(info)
|
||||
await Worktree.remove({ directory: config.directory })
|
||||
},
|
||||
async fetch(_info, _input: RequestInfo | URL, _init?: RequestInit) {
|
||||
throw new Error("fetch not implemented")
|
||||
target(info) {
|
||||
const config = Config.parse(info)
|
||||
return {
|
||||
type: "local",
|
||||
directory: config.directory,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -13,9 +13,20 @@ export const WorkspaceInfo = z.object({
|
||||
})
|
||||
export type WorkspaceInfo = z.infer<typeof WorkspaceInfo>
|
||||
|
||||
export type Target =
|
||||
| {
|
||||
type: "local"
|
||||
directory: string
|
||||
}
|
||||
| {
|
||||
type: "remote"
|
||||
url: string | URL
|
||||
headers?: HeadersInit
|
||||
}
|
||||
|
||||
export type Adaptor = {
|
||||
configure(input: WorkspaceInfo): WorkspaceInfo | Promise<WorkspaceInfo>
|
||||
create(input: WorkspaceInfo, from?: WorkspaceInfo): Promise<void>
|
||||
create(config: WorkspaceInfo, from?: WorkspaceInfo): Promise<void>
|
||||
remove(config: WorkspaceInfo): Promise<void>
|
||||
fetch(config: WorkspaceInfo, input: RequestInfo | URL, init?: RequestInit): Promise<Response>
|
||||
target(config: WorkspaceInfo): Target | Promise<Target>
|
||||
}
|
||||
|
||||
@@ -116,17 +116,31 @@ export namespace Workspace {
|
||||
async function workspaceEventLoop(space: Info, stop: AbortSignal) {
|
||||
while (!stop.aborted) {
|
||||
const adaptor = await getAdaptor(space.type)
|
||||
const res = await adaptor.fetch(space, "/event", { method: "GET", signal: stop }).catch(() => undefined)
|
||||
if (!res || !res.ok || !res.body) {
|
||||
const target = await Promise.resolve(adaptor.target(space))
|
||||
|
||||
if (target.type === "local") {
|
||||
return
|
||||
}
|
||||
|
||||
const baseURL = String(target.url).replace(/\/?$/, "/")
|
||||
|
||||
const res = await fetch(new URL(baseURL + "/event"), {
|
||||
method: "GET",
|
||||
signal: stop,
|
||||
})
|
||||
|
||||
if (!res.ok || !res.body) {
|
||||
await sleep(1000)
|
||||
continue
|
||||
}
|
||||
|
||||
await parseSSE(res.body, stop, (event) => {
|
||||
GlobalBus.emit("event", {
|
||||
directory: space.id,
|
||||
payload: event,
|
||||
})
|
||||
})
|
||||
|
||||
// Wait 250ms and retry if SSE connection fails
|
||||
await sleep(250)
|
||||
}
|
||||
|
||||
@@ -188,13 +188,23 @@ export namespace AppFileSystem {
|
||||
|
||||
export function normalizePath(p: string): string {
|
||||
if (process.platform !== "win32") return p
|
||||
const resolved = pathResolve(windowsPath(p))
|
||||
try {
|
||||
return realpathSync.native(p)
|
||||
return realpathSync.native(resolved)
|
||||
} catch {
|
||||
return p
|
||||
return resolved
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizePathPattern(p: string): string {
|
||||
if (process.platform !== "win32") return p
|
||||
if (p === "*") return p
|
||||
const match = p.match(/^(.*)[\\/]\*$/)
|
||||
if (!match) return normalizePath(p)
|
||||
const dir = /^[A-Za-z]:$/.test(match[1]) ? match[1] + "\\" : match[1]
|
||||
return join(normalizePath(dir), "*")
|
||||
}
|
||||
|
||||
export function resolve(p: string): string {
|
||||
const resolved = pathResolve(windowsPath(p))
|
||||
try {
|
||||
|
||||
@@ -31,6 +31,7 @@ export namespace Flag {
|
||||
export const OPENCODE_ENABLE_EXPERIMENTAL_MODELS = truthy("OPENCODE_ENABLE_EXPERIMENTAL_MODELS")
|
||||
export const OPENCODE_DISABLE_AUTOCOMPACT = truthy("OPENCODE_DISABLE_AUTOCOMPACT")
|
||||
export const OPENCODE_DISABLE_MODELS_FETCH = truthy("OPENCODE_DISABLE_MODELS_FETCH")
|
||||
export const OPENCODE_DISABLE_MOUSE = truthy("OPENCODE_DISABLE_MOUSE")
|
||||
export const OPENCODE_DISABLE_CLAUDE_CODE = truthy("OPENCODE_DISABLE_CLAUDE_CODE")
|
||||
export const OPENCODE_DISABLE_CLAUDE_CODE_PROMPT =
|
||||
OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_PROMPT")
|
||||
|
||||
@@ -105,7 +105,17 @@ export namespace LSPServer {
|
||||
if (!tsserver) return
|
||||
const bin = await Npm.which("typescript-language-server")
|
||||
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,
|
||||
env: {
|
||||
...process.env,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createConnection } from "net"
|
||||
import { createServer } from "http"
|
||||
import { Log } from "../util/log"
|
||||
import { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH } from "./oauth-provider"
|
||||
|
||||
@@ -52,7 +53,7 @@ interface PendingAuth {
|
||||
}
|
||||
|
||||
export namespace McpOAuthCallback {
|
||||
let server: ReturnType<typeof Bun.serve> | undefined
|
||||
let server: ReturnType<typeof createServer> | undefined
|
||||
const pendingAuths = new Map<string, PendingAuth>()
|
||||
// Reverse index: mcpName → oauthState, so cancelPending(mcpName) can
|
||||
// find the right entry in pendingAuths (which is keyed by oauthState).
|
||||
@@ -60,6 +61,80 @@ export namespace McpOAuthCallback {
|
||||
|
||||
const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000 // 5 minutes
|
||||
|
||||
function cleanupStateIndex(oauthState: string) {
|
||||
for (const [name, state] of mcpNameToState) {
|
||||
if (state === oauthState) {
|
||||
mcpNameToState.delete(name)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleRequest(req: import("http").IncomingMessage, res: import("http").ServerResponse) {
|
||||
const url = new URL(req.url || "/", `http://localhost:${OAUTH_CALLBACK_PORT}`)
|
||||
|
||||
if (url.pathname !== OAUTH_CALLBACK_PATH) {
|
||||
res.writeHead(404)
|
||||
res.end("Not found")
|
||||
return
|
||||
}
|
||||
|
||||
const code = url.searchParams.get("code")
|
||||
const state = url.searchParams.get("state")
|
||||
const error = url.searchParams.get("error")
|
||||
const errorDescription = url.searchParams.get("error_description")
|
||||
|
||||
log.info("received oauth callback", { hasCode: !!code, state, error })
|
||||
|
||||
// Enforce state parameter presence
|
||||
if (!state) {
|
||||
const errorMsg = "Missing required state parameter - potential CSRF attack"
|
||||
log.error("oauth callback missing state parameter", { url: url.toString() })
|
||||
res.writeHead(400, { "Content-Type": "text/html" })
|
||||
res.end(HTML_ERROR(errorMsg))
|
||||
return
|
||||
}
|
||||
|
||||
if (error) {
|
||||
const errorMsg = errorDescription || error
|
||||
if (pendingAuths.has(state)) {
|
||||
const pending = pendingAuths.get(state)!
|
||||
clearTimeout(pending.timeout)
|
||||
pendingAuths.delete(state)
|
||||
cleanupStateIndex(state)
|
||||
pending.reject(new Error(errorMsg))
|
||||
}
|
||||
res.writeHead(200, { "Content-Type": "text/html" })
|
||||
res.end(HTML_ERROR(errorMsg))
|
||||
return
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
res.writeHead(400, { "Content-Type": "text/html" })
|
||||
res.end(HTML_ERROR("No authorization code provided"))
|
||||
return
|
||||
}
|
||||
|
||||
// Validate state parameter
|
||||
if (!pendingAuths.has(state)) {
|
||||
const errorMsg = "Invalid or expired state parameter - potential CSRF attack"
|
||||
log.error("oauth callback with invalid state", { state, pendingStates: Array.from(pendingAuths.keys()) })
|
||||
res.writeHead(400, { "Content-Type": "text/html" })
|
||||
res.end(HTML_ERROR(errorMsg))
|
||||
return
|
||||
}
|
||||
|
||||
const pending = pendingAuths.get(state)!
|
||||
|
||||
clearTimeout(pending.timeout)
|
||||
pendingAuths.delete(state)
|
||||
cleanupStateIndex(state)
|
||||
pending.resolve(code)
|
||||
|
||||
res.writeHead(200, { "Content-Type": "text/html" })
|
||||
res.end(HTML_SUCCESS)
|
||||
}
|
||||
|
||||
export async function ensureRunning(): Promise<void> {
|
||||
if (server) return
|
||||
|
||||
@@ -69,88 +144,14 @@ export namespace McpOAuthCallback {
|
||||
return
|
||||
}
|
||||
|
||||
server = Bun.serve({
|
||||
port: OAUTH_CALLBACK_PORT,
|
||||
fetch(req) {
|
||||
const url = new URL(req.url)
|
||||
|
||||
if (url.pathname !== OAUTH_CALLBACK_PATH) {
|
||||
return new Response("Not found", { status: 404 })
|
||||
}
|
||||
|
||||
const code = url.searchParams.get("code")
|
||||
const state = url.searchParams.get("state")
|
||||
const error = url.searchParams.get("error")
|
||||
const errorDescription = url.searchParams.get("error_description")
|
||||
|
||||
log.info("received oauth callback", { hasCode: !!code, state, error })
|
||||
|
||||
// Enforce state parameter presence
|
||||
if (!state) {
|
||||
const errorMsg = "Missing required state parameter - potential CSRF attack"
|
||||
log.error("oauth callback missing state parameter", { url: url.toString() })
|
||||
return new Response(HTML_ERROR(errorMsg), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "text/html" },
|
||||
})
|
||||
}
|
||||
|
||||
if (error) {
|
||||
const errorMsg = errorDescription || error
|
||||
if (pendingAuths.has(state)) {
|
||||
const pending = pendingAuths.get(state)!
|
||||
clearTimeout(pending.timeout)
|
||||
pendingAuths.delete(state)
|
||||
for (const [name, s] of mcpNameToState) {
|
||||
if (s === state) {
|
||||
mcpNameToState.delete(name)
|
||||
break
|
||||
}
|
||||
}
|
||||
pending.reject(new Error(errorMsg))
|
||||
}
|
||||
return new Response(HTML_ERROR(errorMsg), {
|
||||
headers: { "Content-Type": "text/html" },
|
||||
})
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
return new Response(HTML_ERROR("No authorization code provided"), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "text/html" },
|
||||
})
|
||||
}
|
||||
|
||||
// Validate state parameter
|
||||
if (!pendingAuths.has(state)) {
|
||||
const errorMsg = "Invalid or expired state parameter - potential CSRF attack"
|
||||
log.error("oauth callback with invalid state", { state, pendingStates: Array.from(pendingAuths.keys()) })
|
||||
return new Response(HTML_ERROR(errorMsg), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "text/html" },
|
||||
})
|
||||
}
|
||||
|
||||
const pending = pendingAuths.get(state)!
|
||||
|
||||
clearTimeout(pending.timeout)
|
||||
pendingAuths.delete(state)
|
||||
// Clean up reverse index
|
||||
for (const [name, s] of mcpNameToState) {
|
||||
if (s === state) {
|
||||
mcpNameToState.delete(name)
|
||||
break
|
||||
}
|
||||
}
|
||||
pending.resolve(code)
|
||||
|
||||
return new Response(HTML_SUCCESS, {
|
||||
headers: { "Content-Type": "text/html" },
|
||||
})
|
||||
},
|
||||
server = createServer(handleRequest)
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server!.listen(OAUTH_CALLBACK_PORT, () => {
|
||||
log.info("oauth callback server started", { port: OAUTH_CALLBACK_PORT })
|
||||
resolve()
|
||||
})
|
||||
server!.on("error", reject)
|
||||
})
|
||||
|
||||
log.info("oauth callback server started", { port: OAUTH_CALLBACK_PORT })
|
||||
}
|
||||
|
||||
export function waitForCallback(oauthState: string, mcpName?: string): Promise<string> {
|
||||
@@ -196,7 +197,7 @@ export namespace McpOAuthCallback {
|
||||
|
||||
export async function stop(): Promise<void> {
|
||||
if (server) {
|
||||
server.stop()
|
||||
await new Promise<void>((resolve) => server!.close(() => resolve()))
|
||||
server = undefined
|
||||
log.info("oauth callback server stopped")
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Arborist } from "@npmcli/arborist"
|
||||
|
||||
export namespace Npm {
|
||||
const log = Log.create({ service: "npm" })
|
||||
const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined
|
||||
|
||||
export const InstallFailedError = NamedError.create(
|
||||
"NpmInstallFailedError",
|
||||
@@ -19,8 +20,13 @@ export namespace Npm {
|
||||
}),
|
||||
)
|
||||
|
||||
export function sanitize(pkg: string) {
|
||||
if (!illegal) return pkg
|
||||
return Array.from(pkg, (char) => (illegal.has(char) || char.charCodeAt(0) < 32 ? "_" : char)).join("")
|
||||
}
|
||||
|
||||
function directory(pkg: string) {
|
||||
return path.join(Global.Path.cache, "packages", pkg)
|
||||
return path.join(Global.Path.cache, "packages", sanitize(pkg))
|
||||
}
|
||||
|
||||
function resolveEntryPoint(name: string, dir: string) {
|
||||
@@ -67,6 +73,7 @@ export namespace Npm {
|
||||
binLinks: true,
|
||||
progress: false,
|
||||
savePrefix: "",
|
||||
ignoreScripts: true,
|
||||
})
|
||||
const tree = await arborist.loadVirtual().catch(() => {})
|
||||
if (tree) {
|
||||
@@ -106,6 +113,7 @@ export namespace Npm {
|
||||
binLinks: true,
|
||||
progress: false,
|
||||
savePrefix: "",
|
||||
ignoreScripts: true,
|
||||
})
|
||||
await arb.reify().catch(() => {})
|
||||
}
|
||||
|
||||
67
packages/opencode/src/plugin/cloudflare.ts
Normal file
67
packages/opencode/src/plugin/cloudflare.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { Hooks, PluginInput } from "@opencode-ai/plugin"
|
||||
|
||||
export async function CloudflareWorkersAuthPlugin(_input: PluginInput): Promise<Hooks> {
|
||||
const prompts = [
|
||||
...(!process.env.CLOUDFLARE_ACCOUNT_ID
|
||||
? [
|
||||
{
|
||||
type: "text" as const,
|
||||
key: "accountId",
|
||||
message: "Enter your Cloudflare Account ID",
|
||||
placeholder: "e.g. 1234567890abcdef1234567890abcdef",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]
|
||||
|
||||
return {
|
||||
auth: {
|
||||
provider: "cloudflare-workers-ai",
|
||||
methods: [
|
||||
{
|
||||
type: "api",
|
||||
label: "API key",
|
||||
prompts,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export async function CloudflareAIGatewayAuthPlugin(_input: PluginInput): Promise<Hooks> {
|
||||
const prompts = [
|
||||
...(!process.env.CLOUDFLARE_ACCOUNT_ID
|
||||
? [
|
||||
{
|
||||
type: "text" as const,
|
||||
key: "accountId",
|
||||
message: "Enter your Cloudflare Account ID",
|
||||
placeholder: "e.g. 1234567890abcdef1234567890abcdef",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(!process.env.CLOUDFLARE_GATEWAY_ID
|
||||
? [
|
||||
{
|
||||
type: "text" as const,
|
||||
key: "gatewayId",
|
||||
message: "Enter your Cloudflare AI Gateway ID",
|
||||
placeholder: "e.g. my-gateway",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]
|
||||
|
||||
return {
|
||||
auth: {
|
||||
provider: "cloudflare-ai-gateway",
|
||||
methods: [
|
||||
{
|
||||
type: "api",
|
||||
label: "Gateway API token",
|
||||
prompts,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import os from "os"
|
||||
import { ProviderTransform } from "@/provider/transform"
|
||||
import { ModelID, ProviderID } from "@/provider/schema"
|
||||
import { setTimeout as sleep } from "node:timers/promises"
|
||||
import { createServer } from "http"
|
||||
|
||||
const log = Log.create({ service: "plugin.codex" })
|
||||
|
||||
@@ -241,7 +242,7 @@ interface PendingOAuth {
|
||||
reject: (error: Error) => void
|
||||
}
|
||||
|
||||
let oauthServer: ReturnType<typeof Bun.serve> | undefined
|
||||
let oauthServer: ReturnType<typeof createServer> | undefined
|
||||
let pendingOAuth: PendingOAuth | undefined
|
||||
|
||||
async function startOAuthServer(): Promise<{ port: number; redirectUri: string }> {
|
||||
@@ -249,77 +250,83 @@ async function startOAuthServer(): Promise<{ port: number; redirectUri: string }
|
||||
return { port: OAUTH_PORT, redirectUri: `http://localhost:${OAUTH_PORT}/auth/callback` }
|
||||
}
|
||||
|
||||
oauthServer = Bun.serve({
|
||||
port: OAUTH_PORT,
|
||||
fetch(req) {
|
||||
const url = new URL(req.url)
|
||||
oauthServer = createServer((req, res) => {
|
||||
const url = new URL(req.url || "/", `http://localhost:${OAUTH_PORT}`)
|
||||
|
||||
if (url.pathname === "/auth/callback") {
|
||||
const code = url.searchParams.get("code")
|
||||
const state = url.searchParams.get("state")
|
||||
const error = url.searchParams.get("error")
|
||||
const errorDescription = url.searchParams.get("error_description")
|
||||
if (url.pathname === "/auth/callback") {
|
||||
const code = url.searchParams.get("code")
|
||||
const state = url.searchParams.get("state")
|
||||
const error = url.searchParams.get("error")
|
||||
const errorDescription = url.searchParams.get("error_description")
|
||||
|
||||
if (error) {
|
||||
const errorMsg = errorDescription || error
|
||||
pendingOAuth?.reject(new Error(errorMsg))
|
||||
pendingOAuth = undefined
|
||||
return new Response(HTML_ERROR(errorMsg), {
|
||||
headers: { "Content-Type": "text/html" },
|
||||
})
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
const errorMsg = "Missing authorization code"
|
||||
pendingOAuth?.reject(new Error(errorMsg))
|
||||
pendingOAuth = undefined
|
||||
return new Response(HTML_ERROR(errorMsg), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "text/html" },
|
||||
})
|
||||
}
|
||||
|
||||
if (!pendingOAuth || state !== pendingOAuth.state) {
|
||||
const errorMsg = "Invalid state - potential CSRF attack"
|
||||
pendingOAuth?.reject(new Error(errorMsg))
|
||||
pendingOAuth = undefined
|
||||
return new Response(HTML_ERROR(errorMsg), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "text/html" },
|
||||
})
|
||||
}
|
||||
|
||||
const current = pendingOAuth
|
||||
if (error) {
|
||||
const errorMsg = errorDescription || error
|
||||
pendingOAuth?.reject(new Error(errorMsg))
|
||||
pendingOAuth = undefined
|
||||
|
||||
exchangeCodeForTokens(code, `http://localhost:${OAUTH_PORT}/auth/callback`, current.pkce)
|
||||
.then((tokens) => current.resolve(tokens))
|
||||
.catch((err) => current.reject(err))
|
||||
|
||||
return new Response(HTML_SUCCESS, {
|
||||
headers: { "Content-Type": "text/html" },
|
||||
})
|
||||
res.writeHead(200, { "Content-Type": "text/html" })
|
||||
res.end(HTML_ERROR(errorMsg))
|
||||
return
|
||||
}
|
||||
|
||||
if (url.pathname === "/cancel") {
|
||||
pendingOAuth?.reject(new Error("Login cancelled"))
|
||||
if (!code) {
|
||||
const errorMsg = "Missing authorization code"
|
||||
pendingOAuth?.reject(new Error(errorMsg))
|
||||
pendingOAuth = undefined
|
||||
return new Response("Login cancelled", { status: 200 })
|
||||
res.writeHead(400, { "Content-Type": "text/html" })
|
||||
res.end(HTML_ERROR(errorMsg))
|
||||
return
|
||||
}
|
||||
|
||||
return new Response("Not found", { status: 404 })
|
||||
},
|
||||
if (!pendingOAuth || state !== pendingOAuth.state) {
|
||||
const errorMsg = "Invalid state - potential CSRF attack"
|
||||
pendingOAuth?.reject(new Error(errorMsg))
|
||||
pendingOAuth = undefined
|
||||
res.writeHead(400, { "Content-Type": "text/html" })
|
||||
res.end(HTML_ERROR(errorMsg))
|
||||
return
|
||||
}
|
||||
|
||||
const current = pendingOAuth
|
||||
pendingOAuth = undefined
|
||||
|
||||
exchangeCodeForTokens(code, `http://localhost:${OAUTH_PORT}/auth/callback`, current.pkce)
|
||||
.then((tokens) => current.resolve(tokens))
|
||||
.catch((err) => current.reject(err))
|
||||
|
||||
res.writeHead(200, { "Content-Type": "text/html" })
|
||||
res.end(HTML_SUCCESS)
|
||||
return
|
||||
}
|
||||
|
||||
if (url.pathname === "/cancel") {
|
||||
pendingOAuth?.reject(new Error("Login cancelled"))
|
||||
pendingOAuth = undefined
|
||||
res.writeHead(200)
|
||||
res.end("Login cancelled")
|
||||
return
|
||||
}
|
||||
|
||||
res.writeHead(404)
|
||||
res.end("Not found")
|
||||
})
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
oauthServer!.listen(OAUTH_PORT, () => {
|
||||
log.info("codex oauth server started", { port: OAUTH_PORT })
|
||||
resolve()
|
||||
})
|
||||
oauthServer!.on("error", reject)
|
||||
})
|
||||
|
||||
log.info("codex oauth server started", { port: OAUTH_PORT })
|
||||
return { port: OAUTH_PORT, redirectUri: `http://localhost:${OAUTH_PORT}/auth/callback` }
|
||||
}
|
||||
|
||||
function stopOAuthServer() {
|
||||
if (oauthServer) {
|
||||
oauthServer.stop()
|
||||
oauthServer.close(() => {
|
||||
log.info("codex oauth server stopped")
|
||||
})
|
||||
oauthServer = undefined
|
||||
log.info("codex oauth server stopped")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -592,5 +599,10 @@ export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> {
|
||||
output.headers["User-Agent"] = `opencode/${Installation.VERSION} (${os.platform()} ${os.release()}; ${os.arch()})`
|
||||
output.headers.session_id = input.sessionID
|
||||
},
|
||||
"chat.params": async (input, output) => {
|
||||
if (input.model.providerID !== "openai") return
|
||||
// Match codex cli
|
||||
output.maxOutputTokens = undefined
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,6 +309,14 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
|
||||
},
|
||||
],
|
||||
},
|
||||
"chat.params": async (incoming, output) => {
|
||||
if (!incoming.model.providerID.includes("github-copilot")) return
|
||||
|
||||
// Match github copilot cli, omit maxOutputTokens for gpt models
|
||||
if (incoming.model.api.id.includes("gpt")) {
|
||||
output.maxOutputTokens = undefined
|
||||
}
|
||||
},
|
||||
"chat.headers": async (incoming, output) => {
|
||||
if (!incoming.model.providerID.includes("github-copilot")) return
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user