mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-28 14:11:49 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4778e6e031 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@opencode-ai/cli": patch
|
||||
---
|
||||
|
||||
Expose a TUI plugin slot at the top of the session view.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
"@opencode-ai/client": patch
|
||||
"@opencode-ai/plugin": patch
|
||||
"@opencode-ai/protocol": patch
|
||||
---
|
||||
|
||||
Expose transient, read-only session generation through the HTTP API, generated clients, and V2 plugin session context.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@opencode-ai/cli": patch
|
||||
---
|
||||
|
||||
Expose a TUI plugin slot above the session composer.
|
||||
@@ -121,55 +121,6 @@ jobs:
|
||||
outputs:
|
||||
version: ${{ needs.version.outputs.version }}
|
||||
|
||||
build-node-cli:
|
||||
needs: version
|
||||
if: github.repository == 'anomalyco/opencode'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
settings:
|
||||
- target: linux-arm64
|
||||
host: blacksmith-4vcpu-ubuntu-2404-arm
|
||||
- target: linux-x64
|
||||
host: blacksmith-4vcpu-ubuntu-2404
|
||||
- target: darwin-arm64
|
||||
host: macos-26
|
||||
- target: windows-arm64
|
||||
host: blacksmith-4vcpu-windows-2025
|
||||
- target: windows-x64
|
||||
host: blacksmith-4vcpu-windows-2025
|
||||
runs-on: ${{ matrix.settings.host }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
with:
|
||||
install-flags: --os=* --cpu=*
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: "26.4.0"
|
||||
|
||||
- name: Build
|
||||
run: bun packages/cli/script/build-node.ts --target=${{ matrix.settings.target }} --skip-install --outdir=dist/node
|
||||
env:
|
||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
|
||||
|
||||
- name: Verify service lifecycle
|
||||
if: matrix.settings.target != 'windows-arm64'
|
||||
working-directory: packages/cli
|
||||
run: bun run script/service-smoke.ts --node
|
||||
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: opencode-node-cli-${{ matrix.settings.target }}
|
||||
path: packages/cli/dist/node/cli-node-*
|
||||
if-no-files-found: error
|
||||
|
||||
sign-cli-windows:
|
||||
needs:
|
||||
- build-cli
|
||||
@@ -462,7 +413,6 @@ jobs:
|
||||
needs:
|
||||
- version
|
||||
- build-cli
|
||||
- build-node-cli
|
||||
- sign-cli-windows
|
||||
- build-electron
|
||||
if: always() && !failure() && !cancelled()
|
||||
@@ -511,12 +461,6 @@ jobs:
|
||||
name: opencode-preview-cli
|
||||
path: packages/cli/dist
|
||||
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
pattern: opencode-node-cli-*
|
||||
path: packages/cli/dist/node
|
||||
merge-multiple: true
|
||||
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
if: needs.version.outputs.release
|
||||
with:
|
||||
|
||||
@@ -78,20 +78,6 @@ jobs:
|
||||
bun run script/build.ts --single --skip-install
|
||||
bun run script/service-smoke.ts
|
||||
|
||||
- name: Setup Node build runtime
|
||||
if: always()
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: "26.4.0"
|
||||
|
||||
- name: Verify Node build
|
||||
if: always()
|
||||
timeout-minutes: 15
|
||||
working-directory: packages/cli
|
||||
run: |
|
||||
bun run script/build-node.ts --single --skip-install --outdir=dist/node
|
||||
bun run script/service-smoke.ts --node
|
||||
|
||||
- name: Check generated client
|
||||
if: runner.os == 'Linux'
|
||||
working-directory: packages/client
|
||||
|
||||
@@ -11,7 +11,6 @@ node_modules
|
||||
playground
|
||||
tmp
|
||||
dist
|
||||
dist-node
|
||||
ts-dist
|
||||
.turbo
|
||||
.typecheck-profiles
|
||||
@@ -26,7 +25,6 @@ Session.vim
|
||||
a.out
|
||||
target
|
||||
.scripts
|
||||
.cache
|
||||
.direnv/
|
||||
|
||||
# Local dev files
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.18.3",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@corvu/drawer": "catalog:",
|
||||
"@dnd-kit/abstract": "0.5.0",
|
||||
@@ -119,7 +119,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@opencode-ai/cli",
|
||||
"version": "1.18.3",
|
||||
"version": "1.18.2",
|
||||
"bin": {
|
||||
"opencode2": "./bin/opencode2.cjs",
|
||||
},
|
||||
@@ -145,28 +145,14 @@
|
||||
"solid-js": "catalog:",
|
||||
"strip-ansi": "7.1.2",
|
||||
"uqr": "0.1.3",
|
||||
"ws": "8.21.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lydell/node-pty-darwin-arm64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-darwin-x64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-linux-arm64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-linux-x64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-win32-arm64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-win32-x64": "1.2.0-beta.12",
|
||||
"@opencode-ai/protocol": "workspace:*",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-glibc": "2.5.1",
|
||||
"@parcel/watcher-win32-arm64": "2.5.1",
|
||||
"@parcel/watcher-win32-x64": "2.5.1",
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/semver": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"vite": "catalog:",
|
||||
"vite-plugin-solid": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/client": {
|
||||
@@ -185,7 +171,7 @@
|
||||
"effect": "catalog:",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"effect": "4.0.0-beta.98",
|
||||
"effect": "4.0.0-beta.83",
|
||||
},
|
||||
"optionalPeers": [
|
||||
"effect",
|
||||
@@ -193,7 +179,7 @@
|
||||
},
|
||||
"packages/codemode": {
|
||||
"name": "@opencode-ai/codemode",
|
||||
"version": "1.18.3",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"acorn": "8.15.0",
|
||||
"effect": "catalog:",
|
||||
@@ -207,7 +193,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.18.3",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -243,7 +229,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.18.3",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -270,7 +256,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.18.3",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "3.0.82",
|
||||
"@ai-sdk/openai": "3.0.48",
|
||||
@@ -292,7 +278,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.18.3",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -316,7 +302,7 @@
|
||||
},
|
||||
"packages/console/support": {
|
||||
"name": "@opencode-ai/console-support",
|
||||
"version": "1.18.3",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@opencode-ai/console-core": "workspace:*",
|
||||
@@ -336,7 +322,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode-ai/core",
|
||||
"version": "1.18.3",
|
||||
"version": "1.18.2",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -401,7 +387,6 @@
|
||||
"mime-types": "3.0.2",
|
||||
"minimatch": "10.2.5",
|
||||
"npm-package-arg": "13.0.2",
|
||||
"resolve.exports": "catalog:",
|
||||
"semver": "^7.6.3",
|
||||
"turndown": "7.2.0",
|
||||
"venice-ai-sdk-provider": "2.1.1",
|
||||
@@ -433,7 +418,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.18.3",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"effect": "catalog:",
|
||||
@@ -493,7 +478,7 @@
|
||||
},
|
||||
"packages/effect-drizzle-sqlite": {
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"version": "1.18.3",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -507,7 +492,7 @@
|
||||
},
|
||||
"packages/effect-sqlite-node": {
|
||||
"name": "@opencode-ai/effect-sqlite-node",
|
||||
"version": "1.18.3",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"effect": "catalog:",
|
||||
},
|
||||
@@ -519,7 +504,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.18.3",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@hono/standard-validator": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -551,7 +536,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.18.3",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -567,9 +552,9 @@
|
||||
},
|
||||
"packages/http-recorder": {
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"version": "1.18.3",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@effect/platform-node-shared": "4.0.0-beta.98",
|
||||
"@effect/platform-node-shared": "4.0.0-beta.83",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -599,7 +584,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.18.3",
|
||||
"version": "1.18.2",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -732,7 +717,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.18.3",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "3.0.8",
|
||||
"@opencode-ai/ai": "workspace:*",
|
||||
@@ -754,9 +739,9 @@
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentui/core": ">=0.4.5",
|
||||
"@opentui/keymap": ">=0.4.5",
|
||||
"@opentui/solid": ">=0.4.5",
|
||||
"@opentui/core": ">=0.4.3",
|
||||
"@opentui/keymap": ">=0.4.3",
|
||||
"@opentui/solid": ">=0.4.3",
|
||||
},
|
||||
"optionalPeers": [
|
||||
"@opentui/core",
|
||||
@@ -821,7 +806,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.18.3",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -836,7 +821,7 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@opencode-ai/server",
|
||||
"version": "1.18.3",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -853,7 +838,7 @@
|
||||
},
|
||||
"packages/session-ui": {
|
||||
"name": "@opencode-ai/session-ui",
|
||||
"version": "1.18.3",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -914,7 +899,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.18.3",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -927,7 +912,7 @@
|
||||
},
|
||||
"packages/stats/app": {
|
||||
"name": "@opencode-ai/stats-app",
|
||||
"version": "1.18.3",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@ibm/plex": "6.4.1",
|
||||
"@kobalte/core": "catalog:",
|
||||
@@ -961,7 +946,7 @@
|
||||
},
|
||||
"packages/stats/core": {
|
||||
"name": "@opencode-ai/stats-core",
|
||||
"version": "1.18.3",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-athena": "3.933.0",
|
||||
"@planetscale/database": "1.19.0",
|
||||
@@ -980,7 +965,7 @@
|
||||
},
|
||||
"packages/stats/server": {
|
||||
"name": "@opencode-ai/stats-server",
|
||||
"version": "1.18.3",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-firehose": "3.933.0",
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -1021,7 +1006,7 @@
|
||||
},
|
||||
"packages/tui": {
|
||||
"name": "@opencode-ai/tui",
|
||||
"version": "1.18.3",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@opencode-ai/client": "workspace:*",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -1036,12 +1021,10 @@
|
||||
"diff": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"fuzzysort": "catalog:",
|
||||
"get-east-asian-width": "catalog:",
|
||||
"open": "10.1.2",
|
||||
"opentui-spinner": "catalog:",
|
||||
"remeda": "catalog:",
|
||||
"solid-js": "catalog:",
|
||||
"string-width": "catalog:",
|
||||
"strip-ansi": "7.1.2",
|
||||
"uqr": "0.1.3",
|
||||
},
|
||||
@@ -1053,7 +1036,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.18.3",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@pierre/diffs": "catalog:",
|
||||
@@ -1104,7 +1087,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.18.3",
|
||||
"version": "1.18.2",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -1180,12 +1163,12 @@
|
||||
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
||||
"gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch",
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch",
|
||||
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
|
||||
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
||||
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
|
||||
"pacote@21.5.0": "patches/pacote@21.5.0.patch",
|
||||
"effect@4.0.0-beta.98": "patches/effect@4.0.0-beta.98.patch",
|
||||
},
|
||||
"overrides": {
|
||||
"@opentui/core": "catalog:",
|
||||
@@ -1197,9 +1180,9 @@
|
||||
"catalog": {
|
||||
"@cloudflare/workers-types": "4.20251008.0",
|
||||
"@corvu/drawer": "0.2.4",
|
||||
"@effect/opentelemetry": "4.0.0-beta.98",
|
||||
"@effect/platform-node": "4.0.0-beta.98",
|
||||
"@effect/sql-sqlite-bun": "4.0.0-beta.98",
|
||||
"@effect/opentelemetry": "4.0.0-beta.83",
|
||||
"@effect/platform-node": "4.0.0-beta.83",
|
||||
"@effect/sql-sqlite-bun": "4.0.0-beta.83",
|
||||
"@hono/standard-validator": "0.2.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@kobalte/core": "0.13.11",
|
||||
@@ -1207,9 +1190,9 @@
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@opentui/core": "0.4.5",
|
||||
"@opentui/keymap": "0.4.5",
|
||||
"@opentui/solid": "0.4.5",
|
||||
"@opentui/core": "0.4.3",
|
||||
"@opentui/keymap": "0.4.3",
|
||||
"@opentui/solid": "0.4.3",
|
||||
"@pierre/diffs": "1.2.10",
|
||||
"@playwright/test": "1.59.1",
|
||||
"@sentry/solid": "10.36.0",
|
||||
@@ -1235,9 +1218,8 @@
|
||||
"dompurify": "3.3.1",
|
||||
"drizzle-kit": "1.0.0-rc.2",
|
||||
"drizzle-orm": "1.0.0-rc.2",
|
||||
"effect": "4.0.0-beta.98",
|
||||
"effect": "4.0.0-beta.83",
|
||||
"fuzzysort": "3.1.0",
|
||||
"get-east-asian-width": "1.6.0",
|
||||
"hono": "4.10.7",
|
||||
"hono-openapi": "1.1.2",
|
||||
"luxon": "3.6.1",
|
||||
@@ -1246,13 +1228,11 @@
|
||||
"opentui-spinner": "0.0.7",
|
||||
"remeda": "2.26.0",
|
||||
"remend": "1.3.0",
|
||||
"resolve.exports": "2.0.3",
|
||||
"semver": "7.7.4",
|
||||
"shiki": "4.2.0",
|
||||
"solid-js": "1.9.10",
|
||||
"solid-list": "0.3.0",
|
||||
"sst": "4.13.1",
|
||||
"string-width": "7.2.0",
|
||||
"tailwindcss": "4.1.11",
|
||||
"typescript": "5.8.2",
|
||||
"ulid": "3.0.1",
|
||||
@@ -1639,13 +1619,13 @@
|
||||
|
||||
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="],
|
||||
|
||||
"@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.98", "", { "peerDependencies": { "@opentelemetry/api": "^1.9", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^4.0.0-beta.98" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-ITfK8xhcl+9GXOvPwzADWkOQ+dgUGZrJNefT3r2+uLFmzjyKRLtHzhLOl6lZaLSsf5io13+nmt8adfMRQPq+oA=="],
|
||||
"@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.83", "", { "peerDependencies": { "@opentelemetry/api": "^1.9", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^4.0.0-beta.83" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-cPfCfp/ghu0itbX6Dqjdr4N0rbjng5ON4sUpnLHV5JJySG8zZpWmuOZLWIrfrNKT2ctYR1BYmp1aYCgkItaJLw=="],
|
||||
|
||||
"@effect/platform-node": ["@effect/platform-node@4.0.0-beta.98", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.98", "mime": "^4.1.0", "undici": "^8.7.0" }, "peerDependencies": { "effect": "^4.0.0-beta.98", "ioredis": "^5.7.0" } }, "sha512-IQu1TiLXQEDSGkDBllyYjVadf+UqdjptryqX4mmktVTTbGDq7X4uVxe7cSgXuqZvyfG6kagTzwj2lfynxOaKQg=="],
|
||||
"@effect/platform-node": ["@effect/platform-node@4.0.0-beta.83", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.83", "mime": "^4.1.0", "undici": "^8.2.0" }, "peerDependencies": { "effect": "^4.0.0-beta.83", "ioredis": "^5.7.0" } }, "sha512-RmpVGu/+X/Bif3/g1Rzj8oFzTOknoVB3yHCa0b179vytPpKe+Kj9ZwKNcAnKWqHUDkbSPBq1Ca60mvOHr2/+LQ=="],
|
||||
|
||||
"@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.98", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.0" }, "peerDependencies": { "effect": "^4.0.0-beta.98" } }, "sha512-iySXaffnCJX1sNAIp79ghhIeui9E5qwUQyqd1VLPkB9UNO4vdpd9B5fTEXwe7S/GusL4jsk9vSvX38XJgRFG1w=="],
|
||||
"@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.83", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.20.0" }, "peerDependencies": { "effect": "^4.0.0-beta.83" } }, "sha512-+yr/+PJmKTgmJq1QOINSBPgLu7Cjc4CZcotBXnGjyDEizOmimFgTkN2B8PBJAKIKUWYWfobjXqC+58/VhhPKAw=="],
|
||||
|
||||
"@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.98", "", { "peerDependencies": { "effect": "^4.0.0-beta.98" } }, "sha512-cc41uLhYBqexdbTNu4dlui+31E8hcVLEapLySa0C8d60FmBY8IEAV/RD3oF+6pqPslKEZ9p1+XVLdDm0iflw5Q=="],
|
||||
"@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.83", "", { "peerDependencies": { "effect": "^4.0.0-beta.83" } }, "sha512-6OaxLsWffxkh9pXYUSyj/AxjVb9URY2rG9U6atjxClWy30Jx77R9Pm3Rrc7cQ63kQurePavEw1bQbzQ/SILiQQ=="],
|
||||
|
||||
"@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
|
||||
|
||||
@@ -2293,27 +2273,27 @@
|
||||
|
||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="],
|
||||
|
||||
"@opentui/core": ["@opentui/core@0.4.5", "", { "dependencies": { "bun-ffi-structs": "0.2.4", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.4.5", "@opentui/core-darwin-x64": "0.4.5", "@opentui/core-linux-arm64": "0.4.5", "@opentui/core-linux-arm64-musl": "0.4.5", "@opentui/core-linux-x64": "0.4.5", "@opentui/core-linux-x64-musl": "0.4.5", "@opentui/core-win32-arm64": "0.4.5", "@opentui/core-win32-x64": "0.4.5" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-JsgRTPkA6e+Vxmumxai6SElOSlRQkbzNKHlCfemlArRiLhfC1IZ9RXJo2QH4xSu+uBOWAM90uss73/pPlkdEig=="],
|
||||
"@opentui/core": ["@opentui/core@0.4.3", "", { "dependencies": { "bun-ffi-structs": "0.2.4", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.4.3", "@opentui/core-darwin-x64": "0.4.3", "@opentui/core-linux-arm64": "0.4.3", "@opentui/core-linux-arm64-musl": "0.4.3", "@opentui/core-linux-x64": "0.4.3", "@opentui/core-linux-x64-musl": "0.4.3", "@opentui/core-win32-arm64": "0.4.3", "@opentui/core-win32-x64": "0.4.3" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-rrJfAk13tALDqldYjhc78eWQ+aKq1iknJgffIOg3OwyZoqQo+p6gtuqyhmWvXIfQzlNUbpgpCPcxbXlhMnlaHQ=="],
|
||||
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.4.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-8KUG0oRidnR+oW1RSZJ72/PhZLl+qRRMk5U/mieF4c0SJ5V3tYACpBZAKzQfHNd1f7QzD8FHZct1lPpQgtmkWg=="],
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.4.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-p5+7AAxpxGuDGagyQfewKtmTFnN7THvTVY4FyKqUtJomNaHdQXPHztapNNzMx0DGWbwOUbVKzpL+yc3CZY3chQ=="],
|
||||
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.4.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-R2bocsg55gwjOqCp/MWFgFYzRmsduKegB6nzgFAPCvAD/L5Jf30xpWJWFlSg3x8vxe1L9WJ84dfqa4M7mZZ3wA=="],
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.4.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-+fh0vEUE0lwVC7RW5ijYLRlTLp5NfvCRj8SzxDVd7IL2j2ssB6YXcfIbXq2EW7UGnrejwPRXf1tgUrIXW9KmOw=="],
|
||||
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-R4MZ25a4CzOAGVjW9aj1hUfzQGVfCJwrwBDbNs2SXaIvzcZqkxCVtU4FoQ5LsaD0j/BdNQVg2CIfFkFsm1fDuQ=="],
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.4.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-gl6qA5QJy6u8Cbt7gOtHbhhfMZ4qQDb0kEwFXHcMGmbnKzz4OHoq74D6tNjyvSQB9saoC7C6C0tvn2DcJOuNog=="],
|
||||
|
||||
"@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ieqdyKI6EIYPalYAETB2wsdP83hr5Ifi+dFnBFUmdEEFHsoKwBmn2S7bsTOYlX7Bg03F4/YPIg+IvRpeC+cUJw=="],
|
||||
"@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.4.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-8p8g8/AEq/xFGpQ7XcIFKcAqjc0QwsZcv+Ll9RbCDpUA56FGH6jfLDir0KYTNTgYXJTIrBIENI9K46VuxMUMQA=="],
|
||||
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-SNyuQoxMKI1vuJhgxSSW96adWM6LqFl2SoS3GM4tGeneGOanVVG2Y06PvlytXvF4cKik97t0rqkVMRetmOs93w=="],
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.4.3", "", { "os": "linux", "cpu": "x64" }, "sha512-dXpJitiZdYE3hq2Pvx6e9I0uPQSOcnaLLp1pDgWAHv+3kvKSHEX//9Yr/pV/Ua6qqT7p+2D/K4vXNap/NKVo2w=="],
|
||||
|
||||
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-mKVKcIcPiSVVZZsdPSBoWwoa2/TCeQAaMDeHF7PFw2kt5bTXZPP7xxWfRQLCNIcA1eaGl59UuwUWHDR2Ve548Q=="],
|
||||
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.4.3", "", { "os": "linux", "cpu": "x64" }, "sha512-/QiFpCrpU2O7vy8QYmLIQYbvAtKDgmqcVjR7dGtqSzkiQk3ktNJoo5RozG7ueXnjung1Wp0nKldKxo2Csg/OrA=="],
|
||||
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.4.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-GHTTsqeR45q2Iek9Rb7ty+x/hAKn2jZ1ujlCgPR8LBKyF7h0E1dNFryoZ7ehMc3kJndP1sKn836IemKFqxuDdQ=="],
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.4.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-Mx2zuOjrhm/z2SDS6RExIyjP/SnN/8QhhagxURUw0jQi/NssGSeAllu1cBAFFnhobJL5QLTE4FU4CRhUK9svgg=="],
|
||||
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.4.5", "", { "os": "win32", "cpu": "x64" }, "sha512-Y8T/yXCDGagRGiQrtmuB6AhRcPucKFs/Dre3v8kJwNYqDccI4FzUPKclZ7djfmRZNjl7JUqPhZZP/PwDpQocMg=="],
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.4.3", "", { "os": "win32", "cpu": "x64" }, "sha512-NuoqvWKGXaYnmlqvu7Gg2lLI6yVMnS9OfWBvxp+7Q+McSgHFSTQmYBXaPpvQ8HikpQXE1nCeMPtuSG4PdZHe2w=="],
|
||||
|
||||
"@opentui/keymap": ["@opentui/keymap@0.4.5", "", { "dependencies": { "@opentui/core": "0.4.5" }, "peerDependencies": { "@opentui/react": "0.4.5", "@opentui/solid": "0.4.5", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-S1wzKHhF70zT6bH+VBFY+lSeTImLcIFW28JNQiME8MoPcy6KGPs7rKFSHrb/U7P8rsTJeRfW5A4d1Cy6PKodDg=="],
|
||||
"@opentui/keymap": ["@opentui/keymap@0.4.3", "", { "dependencies": { "@opentui/core": "0.4.3" }, "peerDependencies": { "@opentui/react": "0.4.3", "@opentui/solid": "0.4.3", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-sinX0pyQBRrEvo89PSSUbSUDIYpL3xWo81VEfec58VFoVRB5FG48/deAtvRTQfJ8w1kgbzN8hzdOXdSm61zBmw=="],
|
||||
|
||||
"@opentui/solid": ["@opentui/solid@0.4.5", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.4.5", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-B0RSkXnrtPVfEJOX+Hj+axjLJ3lzbG1BZw5I7Pvb9OPp48Vzg2cW2a3cSa86/q48ndLt647i/XwFPIw/jqnI5g=="],
|
||||
"@opentui/solid": ["@opentui/solid@0.4.3", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.4.3", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-RcV0+S8HMdXOASyr7HmJUBuTUIaFPzAxMDa44VftS5C2JUgrmAuWo0Njv1q3TWRB1owjHnyKhEfWGKq7A82wxw=="],
|
||||
|
||||
"@orama/orama": ["@orama/orama@3.1.18", "", {}, "sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA=="],
|
||||
|
||||
@@ -4037,7 +4017,7 @@
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"effect": ["effect@4.0.0-beta.98", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.4", "multipasta": "^0.2.8", "toml": "^4.1.2", "uuid": "^14.0.1", "yaml": "^2.9.0" } }, "sha512-oz+bsG5h+6RNrw4t5GMfQrk/xBS8ROoqkYsuvRhBr5O7mCOrpvH/hbw+QrDzvKIpX4HJClwm86F94c87W0sJxg=="],
|
||||
"effect": ["effect@4.0.0-beta.83", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-0wsak8RtgGAr9UWSbVDgJHZcUqMSvicHcvaZv1MbMM7MCGgW4Rn/137J1MHQbwYPcwYGxT/IqehFd+UbYuj78w=="],
|
||||
|
||||
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
||||
|
||||
@@ -4213,7 +4193,7 @@
|
||||
|
||||
"extsprintf": ["extsprintf@1.4.1", "", {}, "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA=="],
|
||||
|
||||
"fast-check": ["fast-check@4.9.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg=="],
|
||||
"fast-check": ["fast-check@4.8.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg=="],
|
||||
|
||||
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
|
||||
|
||||
@@ -5077,7 +5057,7 @@
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"msgpackr": ["msgpackr@2.0.4", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA=="],
|
||||
"msgpackr": ["msgpackr@2.0.2", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-c5hYOXFbP79Slh6Dzd2wzk+jnV7mX1UxfMYtilnY1NmalXPqG8DGb5cYCMBrW4AsH3zekBBZd4QrKz9NhtvYLQ=="],
|
||||
|
||||
"msgpackr-extract": ["msgpackr-extract@3.0.4", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw=="],
|
||||
|
||||
@@ -5085,7 +5065,7 @@
|
||||
|
||||
"multicast-dns": ["multicast-dns@7.2.5", "", { "dependencies": { "dns-packet": "^5.2.2", "thunky": "^1.0.2" }, "bin": { "multicast-dns": "cli.js" } }, "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg=="],
|
||||
|
||||
"multipasta": ["multipasta@0.2.8", "", {}, "sha512-ZPWuMKyv0cSO29f7hozp+k6+crZbQijV8ipMvxNxRf2SwtYGTX1ZX89Kd20VV4H9Znonx+EQn+iy1wGQsJ+b+Q=="],
|
||||
"multipasta": ["multipasta@0.2.7", "", {}, "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA=="],
|
||||
|
||||
"mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="],
|
||||
|
||||
@@ -5615,8 +5595,6 @@
|
||||
|
||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||
|
||||
"resolve.exports": ["resolve.exports@2.0.3", "", {}, "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A=="],
|
||||
|
||||
"responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="],
|
||||
|
||||
"restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="],
|
||||
@@ -5985,7 +5963,7 @@
|
||||
|
||||
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||
|
||||
"toml": ["toml@4.3.0", "", {}, "sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A=="],
|
||||
"toml": ["toml@4.1.1", "", {}, "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw=="],
|
||||
|
||||
"toolbeam-docs-theme": ["toolbeam-docs-theme@0.4.8", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-b+5ynEFp4Woe5a22hzNQm42lD23t13ZMihVxHbzjA50zdcM9aOSJTIjdJ0PDSd4/50HbBXcpHiQsz6rM4N88ww=="],
|
||||
|
||||
@@ -6071,7 +6049,7 @@
|
||||
|
||||
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
|
||||
|
||||
"undici": ["undici@8.7.0", "", {}, "sha512-N7iQtfyLhIMOFgQubvmLV26svHpO0bqKnAiWotTQCVKCmWrcGbBotPuW1x+xwYZ2VHdSTVUfPQQnlEt1/LouTQ=="],
|
||||
"undici": ["undici@8.3.0", "", {}, "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
@@ -6153,7 +6131,7 @@
|
||||
|
||||
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
|
||||
|
||||
"uuid": ["uuid@14.0.1", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew=="],
|
||||
"uuid": ["uuid@14.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg=="],
|
||||
|
||||
"valibot": ["valibot@1.4.1", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-klCmFTz2jeDluy9RwX+F884TCiogtdBJ/YaxSx1EOBYXa3NXNWj8kR1jjN8rzluwojJVWWaHJ4r1U5LfICnM3g=="],
|
||||
|
||||
@@ -8363,26 +8341,6 @@
|
||||
|
||||
"@solidjs/start/shiki/@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="],
|
||||
|
||||
"@standard-community/standard-json/effect/fast-check": ["fast-check@4.8.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg=="],
|
||||
|
||||
"@standard-community/standard-json/effect/msgpackr": ["msgpackr@2.0.2", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-c5hYOXFbP79Slh6Dzd2wzk+jnV7mX1UxfMYtilnY1NmalXPqG8DGb5cYCMBrW4AsH3zekBBZd4QrKz9NhtvYLQ=="],
|
||||
|
||||
"@standard-community/standard-json/effect/multipasta": ["multipasta@0.2.7", "", {}, "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA=="],
|
||||
|
||||
"@standard-community/standard-json/effect/toml": ["toml@4.1.1", "", {}, "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw=="],
|
||||
|
||||
"@standard-community/standard-json/effect/uuid": ["uuid@14.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg=="],
|
||||
|
||||
"@standard-community/standard-openapi/effect/fast-check": ["fast-check@4.8.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg=="],
|
||||
|
||||
"@standard-community/standard-openapi/effect/msgpackr": ["msgpackr@2.0.2", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-c5hYOXFbP79Slh6Dzd2wzk+jnV7mX1UxfMYtilnY1NmalXPqG8DGb5cYCMBrW4AsH3zekBBZd4QrKz9NhtvYLQ=="],
|
||||
|
||||
"@standard-community/standard-openapi/effect/multipasta": ["multipasta@0.2.7", "", {}, "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA=="],
|
||||
|
||||
"@standard-community/standard-openapi/effect/toml": ["toml@4.1.1", "", {}, "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw=="],
|
||||
|
||||
"@standard-community/standard-openapi/effect/uuid": ["uuid@14.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg=="],
|
||||
|
||||
"@stoplight/spectral-core/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="],
|
||||
|
||||
"@storybook/addon-docs/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
||||
|
||||
+8
-11
@@ -37,18 +37,18 @@
|
||||
"packages/slack"
|
||||
],
|
||||
"catalog": {
|
||||
"@effect/opentelemetry": "4.0.0-beta.98",
|
||||
"@effect/platform-node": "4.0.0-beta.98",
|
||||
"@effect/sql-sqlite-bun": "4.0.0-beta.98",
|
||||
"@effect/opentelemetry": "4.0.0-beta.83",
|
||||
"@effect/platform-node": "4.0.0-beta.83",
|
||||
"@effect/sql-sqlite-bun": "4.0.0-beta.83",
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@types/bun": "1.3.13",
|
||||
"@types/cross-spawn": "6.0.6",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@hono/standard-validator": "0.2.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@opentui/core": "0.4.5",
|
||||
"@opentui/keymap": "0.4.5",
|
||||
"@opentui/solid": "0.4.5",
|
||||
"@opentui/core": "0.4.3",
|
||||
"@opentui/keymap": "0.4.3",
|
||||
"@opentui/solid": "0.4.3",
|
||||
"@tanstack/solid-virtual": "3.13.32",
|
||||
"@shikijs/stream": "4.2.0",
|
||||
"ulid": "3.0.1",
|
||||
@@ -69,13 +69,12 @@
|
||||
"dompurify": "3.3.1",
|
||||
"drizzle-kit": "1.0.0-rc.2",
|
||||
"drizzle-orm": "1.0.0-rc.2",
|
||||
"effect": "4.0.0-beta.98",
|
||||
"effect": "4.0.0-beta.83",
|
||||
"ai": "6.0.168",
|
||||
"cross-spawn": "7.0.6",
|
||||
"hono": "4.10.7",
|
||||
"hono-openapi": "1.1.2",
|
||||
"fuzzysort": "3.1.0",
|
||||
"get-east-asian-width": "1.6.0",
|
||||
"luxon": "3.6.1",
|
||||
"marked": "17.0.6",
|
||||
"marked-shiki": "1.2.1",
|
||||
@@ -86,11 +85,9 @@
|
||||
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
||||
"zod": "4.1.8",
|
||||
"remeda": "2.26.0",
|
||||
"resolve.exports": "2.0.3",
|
||||
"sst": "4.13.1",
|
||||
"shiki": "4.2.0",
|
||||
"solid-list": "0.3.0",
|
||||
"string-width": "7.2.0",
|
||||
"tailwindcss": "4.1.11",
|
||||
"vite": "7.1.4",
|
||||
"@solidjs/meta": "0.29.4",
|
||||
@@ -166,7 +163,7 @@
|
||||
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
|
||||
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
|
||||
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
||||
"effect@4.0.0-beta.98": "patches/effect@4.0.0-beta.98.patch",
|
||||
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch",
|
||||
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.18.3",
|
||||
"version": "1.18.2",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
import { getFilename } from "@opencode-ai/core/util/path"
|
||||
import type { GlobalSession, Project } from "@opencode-ai/sdk/v2/client"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useNavigate } from "@solidjs/router"
|
||||
import { createMemo, onCleanup } from "solid-js"
|
||||
import { commandPaletteOptions, useCommand, type CommandOption } from "@/context/command"
|
||||
import { useCommand, type CommandOption } from "@/context/command"
|
||||
import { useFile } from "@/context/file"
|
||||
import { useGlobal } from "@/context/global"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useLayout, type LocalProject } from "@/context/layout"
|
||||
import { ServerConnection } from "@/context/server"
|
||||
import { useServerSDK } from "@/context/server-sdk"
|
||||
import { useTabs } from "@/context/tabs"
|
||||
import { displayName, projectForSession } from "@/pages/layout/helpers"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useServerSDK, type ServerSDK } from "@/context/server-sdk"
|
||||
import { useServerSync } from "@/context/server-sync"
|
||||
import { createSessionTabs } from "@/pages/session/helpers"
|
||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
|
||||
export type CommandPaletteEntry = {
|
||||
id: string
|
||||
@@ -25,8 +24,6 @@ export type CommandPaletteEntry = {
|
||||
path?: string
|
||||
directory?: string
|
||||
sessionID?: string
|
||||
server?: ServerConnection.Key
|
||||
project?: LocalProject
|
||||
archived?: number
|
||||
updated?: number
|
||||
}
|
||||
@@ -78,25 +75,28 @@ export function createCommandPaletteFileOpener(onOpenFile?: (path: string) => vo
|
||||
|
||||
export function createCommandPaletteModel(props: { filesOnly?: () => boolean; onOpenFile?: (path: string) => void }) {
|
||||
const command = useCommand()
|
||||
const global = useGlobal()
|
||||
const language = useLanguage()
|
||||
const layout = useLayout()
|
||||
const file = useFile()
|
||||
const dialog = useDialog()
|
||||
const navigate = useNavigate()
|
||||
const serverSDK = useServerSDK()()
|
||||
const serverCtx = global.ensureServerCtx(serverSDK.server)
|
||||
const appTabs = useTabs()
|
||||
const { tabs: sessionTabs } = useSessionLayout()
|
||||
const serverSync = useServerSync()
|
||||
const { params, tabs } = useSessionLayout()
|
||||
const openFile = createCommandPaletteFileOpener(props.onOpenFile)
|
||||
const state = { cleanup: undefined as (() => void) | void, committed: false }
|
||||
const filesOnly = () => props.filesOnly?.() ?? false
|
||||
|
||||
const allowedCommands = createMemo(() => {
|
||||
if (filesOnly()) return []
|
||||
return commandPaletteOptions(command.options)
|
||||
return command.options.filter(
|
||||
(option) =>
|
||||
!option.disabled && !option.hidden && !option.id.startsWith("suggested.") && option.id !== "file.open",
|
||||
)
|
||||
})
|
||||
const commandEntries = createMemo(() => {
|
||||
const category = language.t("palette.group.commands")
|
||||
return allowedCommands().map((option) => createCommandPaletteCommandEntry(option, category))
|
||||
return allowedCommands().map((option) => createCommandEntry(option, category))
|
||||
})
|
||||
const preferredCommandEntries = createMemo(() => {
|
||||
const all = allowedCommands()
|
||||
@@ -105,11 +105,11 @@ export function createCommandPaletteModel(props: { filesOnly?: () => boolean; on
|
||||
const base = picked.length ? picked : all.slice(0, ENTRY_LIMIT)
|
||||
const sorted = picked.length ? [...base].sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0)) : base
|
||||
const category = language.t("palette.group.commands")
|
||||
return sorted.map((option) => createCommandPaletteCommandEntry(option, category))
|
||||
return sorted.map((option) => createCommandEntry(option, category))
|
||||
})
|
||||
|
||||
const tabState = createSessionTabs({
|
||||
tabs: sessionTabs,
|
||||
tabs,
|
||||
pathFromTab: file.pathFromTab,
|
||||
normalizeTab: (tab) => (tab.startsWith("file://") ? file.tab(tab) : tab),
|
||||
})
|
||||
@@ -140,12 +140,36 @@ export function createCommandPaletteModel(props: { filesOnly?: () => boolean; on
|
||||
.map((path) => createCommandPaletteFileEntry(path, category))
|
||||
})
|
||||
|
||||
const sessions = createServerSessionEntries({
|
||||
server: ServerConnection.key(serverSDK.server),
|
||||
opened: serverCtx.projects.list,
|
||||
stored: () => serverCtx.sync.data.project,
|
||||
load: (search, signal) =>
|
||||
serverSDK.client.experimental.session.list({ roots: true, search, limit: 50 }, { signal }),
|
||||
const projectDirectory = createMemo(() => decode64(params.dir) ?? "")
|
||||
const project = createMemo(() => {
|
||||
const directory = projectDirectory()
|
||||
if (!directory) return undefined
|
||||
return layout.projects.list().find((item) => item.worktree === directory || item.sandboxes?.includes(directory))
|
||||
})
|
||||
const workspaces = createMemo(() => {
|
||||
const directory = projectDirectory()
|
||||
const current = project()
|
||||
if (!current) return directory ? [directory] : []
|
||||
const dirs = [current.worktree, ...(current.sandboxes ?? [])]
|
||||
if (directory && !dirs.includes(directory)) return [...dirs, directory]
|
||||
return dirs
|
||||
})
|
||||
const homedir = createMemo(() => serverSync().data.path.home)
|
||||
const sessions = createSessionEntries({
|
||||
workspaces,
|
||||
label: (directory) => {
|
||||
const current = project()
|
||||
const kind =
|
||||
current && directory === current.worktree
|
||||
? language.t("workspace.type.local")
|
||||
: language.t("workspace.type.sandbox")
|
||||
const [store] = serverSync().child(directory, { bootstrap: false })
|
||||
const home = homedir()
|
||||
const path = home ? directory.replace(home, "~") : directory
|
||||
const name = store.vcs?.branch ?? getFilename(directory)
|
||||
return `${kind} : ${name || path}`
|
||||
},
|
||||
load: (directory) => serverSDK.client.session.list({ directory, roots: true }),
|
||||
untitled: () => language.t("command.session.new"),
|
||||
category: () => language.t("command.category.session"),
|
||||
})
|
||||
@@ -167,17 +191,8 @@ export function createCommandPaletteModel(props: { filesOnly?: () => boolean; on
|
||||
return
|
||||
}
|
||||
if (item.type === "session") {
|
||||
if (!item.sessionID || !item.server) return
|
||||
const directory = item.project?.worktree ?? item.directory
|
||||
if (directory) {
|
||||
serverCtx.projects.open(directory)
|
||||
serverCtx.projects.touch(directory)
|
||||
}
|
||||
const tab = appTabs.addSessionTab({
|
||||
server: item.server,
|
||||
sessionId: item.sessionID,
|
||||
})
|
||||
appTabs.select(tab)
|
||||
if (!item.directory || !item.sessionID) return
|
||||
navigate(`/${base64Encode(item.directory)}/session/${item.sessionID}`)
|
||||
return
|
||||
}
|
||||
if (!item.path) return
|
||||
@@ -203,7 +218,7 @@ export function createCommandPaletteModel(props: { filesOnly?: () => boolean; on
|
||||
}
|
||||
}
|
||||
|
||||
export function createCommandPaletteCommandEntry(option: CommandOption, category: string): CommandPaletteEntry {
|
||||
function createCommandEntry(option: CommandOption, category: string): CommandPaletteEntry {
|
||||
return {
|
||||
id: "command:" + option.id,
|
||||
type: "command",
|
||||
@@ -215,65 +230,96 @@ export function createCommandPaletteCommandEntry(option: CommandOption, category
|
||||
}
|
||||
}
|
||||
|
||||
export function createServerSessionEntries(props: {
|
||||
server: ServerConnection.Key
|
||||
opened: () => LocalProject[]
|
||||
stored: () => Project[]
|
||||
load: (search: string, signal: AbortSignal) => Promise<{ data?: GlobalSession[] }>
|
||||
function createSessionEntries(props: {
|
||||
workspaces: () => string[]
|
||||
label: (directory: string) => string
|
||||
load: (directory: string) => ReturnType<ServerSDK["client"]["session"]["list"]>
|
||||
untitled: () => string
|
||||
category: () => string
|
||||
}) {
|
||||
let abort: AbortController | undefined
|
||||
const state: {
|
||||
token: number
|
||||
inflight: Promise<CommandPaletteEntry[]> | undefined
|
||||
cached: CommandPaletteEntry[] | undefined
|
||||
} = { token: 0, inflight: undefined, cached: undefined }
|
||||
|
||||
onCleanup(() => abort?.abort())
|
||||
|
||||
return async (text: string): Promise<CommandPaletteEntry[]> => {
|
||||
const search = text.trim()
|
||||
if (!search) {
|
||||
abort?.abort()
|
||||
return []
|
||||
return (text: string) => {
|
||||
if (!text.trim()) {
|
||||
state.token += 1
|
||||
state.inflight = undefined
|
||||
state.cached = undefined
|
||||
return [] as CommandPaletteEntry[]
|
||||
}
|
||||
abort?.abort()
|
||||
const current = new AbortController()
|
||||
abort = current
|
||||
await new Promise<void>((resolve) => {
|
||||
const timer = setTimeout(resolve, 100)
|
||||
current.signal.addEventListener(
|
||||
"abort",
|
||||
() => {
|
||||
clearTimeout(timer)
|
||||
resolve()
|
||||
},
|
||||
{ once: true },
|
||||
)
|
||||
})
|
||||
if (current.signal.aborted) return []
|
||||
const opened = props.opened()
|
||||
const openedByID = new Map(opened.flatMap((project) => (project.id ? [[project.id, project] as const] : [])))
|
||||
const stored = props.stored().map((project) => ({ ...project, expanded: false }))
|
||||
const storedByID = new Map(stored.map((project) => [project.id, project] as const))
|
||||
return props
|
||||
.load(search, current.signal)
|
||||
.then((result) =>
|
||||
(result.data ?? [])
|
||||
.filter((session) => !session.time.archived)
|
||||
.map((session) => {
|
||||
const project =
|
||||
projectForSession(session, opened, openedByID) ?? projectForSession(session, stored, storedByID)
|
||||
return {
|
||||
id: `session:${props.server}:${session.id}`,
|
||||
type: "session" as const,
|
||||
title: session.title || props.untitled(),
|
||||
description: project ? displayName(project) : session.project?.name || getFilename(session.directory),
|
||||
category: props.category(),
|
||||
directory: session.directory,
|
||||
sessionID: session.id,
|
||||
server: props.server,
|
||||
project,
|
||||
updated: session.time.updated,
|
||||
}
|
||||
}),
|
||||
)
|
||||
if (state.cached) return state.cached
|
||||
if (state.inflight) return state.inflight
|
||||
|
||||
const current = state.token
|
||||
const dirs = props.workspaces()
|
||||
if (dirs.length === 0) return [] as CommandPaletteEntry[]
|
||||
|
||||
state.inflight = Promise.all(
|
||||
dirs.map((directory) => {
|
||||
const description = props.label(directory)
|
||||
return props
|
||||
.load(directory)
|
||||
.then((result) =>
|
||||
(result.data ?? [])
|
||||
.filter((session) => !!session?.id)
|
||||
.map((session) => ({
|
||||
id: session.id,
|
||||
title: session.title ?? props.untitled(),
|
||||
description,
|
||||
directory,
|
||||
archived: session.time?.archived,
|
||||
updated: session.time?.updated,
|
||||
})),
|
||||
)
|
||||
.catch(() => [] as SessionEntryInput[])
|
||||
}),
|
||||
)
|
||||
.then((results) => {
|
||||
if (state.token !== current) return [] as CommandPaletteEntry[]
|
||||
const seen = new Set<string>()
|
||||
const next = results
|
||||
.flat()
|
||||
.filter((item) => {
|
||||
const key = `${item.directory}:${item.id}`
|
||||
if (seen.has(key)) return false
|
||||
seen.add(key)
|
||||
return true
|
||||
})
|
||||
.map((item) => createSessionEntry(item, props.category()))
|
||||
state.cached = next
|
||||
return next
|
||||
})
|
||||
.catch(() => [] as CommandPaletteEntry[])
|
||||
.finally(() => {
|
||||
state.inflight = undefined
|
||||
})
|
||||
|
||||
return state.inflight
|
||||
}
|
||||
}
|
||||
|
||||
type SessionEntryInput = {
|
||||
directory: string
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
archived?: number
|
||||
updated?: number
|
||||
}
|
||||
|
||||
function createSessionEntry(input: SessionEntryInput, category: string): CommandPaletteEntry {
|
||||
return {
|
||||
id: `session:${input.directory}:${input.id}`,
|
||||
type: "session",
|
||||
title: input.title,
|
||||
description: input.description,
|
||||
category,
|
||||
directory: input.directory,
|
||||
sessionID: input.id,
|
||||
archived: input.archived,
|
||||
updated: input.updated,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,20 +5,13 @@ import { Dialog, DialogBody } from "@opencode-ai/ui/v2/dialog-v2"
|
||||
import { Icon } from "@opencode-ai/ui/v2/icon"
|
||||
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
|
||||
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { createEffect, createMemo, createResource, createSignal, For, Match, onCleanup, Show, Switch } from "solid-js"
|
||||
import { commandPaletteOptions, formatKeybindParts, useCommand } from "@/context/command"
|
||||
import { useGlobal } from "@/context/global"
|
||||
import { createEffect, createMemo, createResource, createSignal, For, Match, Show, Switch } from "solid-js"
|
||||
import { formatKeybindParts } from "@/context/command"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { ServerConnection } from "@/context/server"
|
||||
import { useTabs } from "@/context/tabs"
|
||||
import { SessionTabAvatar } from "@/pages/layout/session-tab-avatar"
|
||||
import { getRelativeTime } from "@/utils/time"
|
||||
import {
|
||||
createCommandPaletteCommandEntry,
|
||||
createCommandPaletteFileEntry,
|
||||
createCommandPaletteModel,
|
||||
createServerSessionEntries,
|
||||
uniqueCommandPaletteEntries,
|
||||
type CommandPaletteEntry,
|
||||
} from "./command-palette"
|
||||
@@ -37,6 +30,9 @@ function matchesEntry(entry: CommandPaletteEntry, query: string) {
|
||||
|
||||
export function DialogCommandPaletteV2(props: { onOpenFile?: (path: string) => void }) {
|
||||
const palette = createCommandPaletteModel(props)
|
||||
const [query, setQuery] = createSignal("")
|
||||
const [active, setActive] = createSignal(0)
|
||||
|
||||
const loadItems = async (text: string) => {
|
||||
const q = text.trim()
|
||||
if (!q) return [...palette.preferredCommandEntries(), ...palette.recentFileEntries()]
|
||||
@@ -45,105 +41,16 @@ export function DialogCommandPaletteV2(props: { onOpenFile?: (path: string) => v
|
||||
const category = palette.language.t("palette.group.files")
|
||||
return [
|
||||
...palette.commandEntries().filter((entry) => matchesEntry(entry, q)),
|
||||
...nextSessions,
|
||||
...nextSessions.filter((entry) => matchesEntry(entry, q)),
|
||||
...files.map((path) => createCommandPaletteFileEntry(path, category)),
|
||||
]
|
||||
}
|
||||
|
||||
return (
|
||||
<CommandPaletteView
|
||||
placeholder={palette.language.t("palette.search.placeholder")}
|
||||
loadItems={loadItems}
|
||||
highlight={palette.highlight}
|
||||
select={palette.select}
|
||||
close={palette.close}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function DialogHomeCommandPaletteV2(props: {
|
||||
server: ServerConnection.Any
|
||||
onSelectSession: (entry: CommandPaletteEntry) => void
|
||||
}) {
|
||||
const command = useCommand()
|
||||
const dialog = useDialog()
|
||||
const global = useGlobal()
|
||||
const language = useLanguage()
|
||||
const serverCtx = global.ensureServerCtx(props.server)
|
||||
const state = { cleanup: undefined as (() => void) | void, committed: false }
|
||||
const commandEntries = createMemo(() => {
|
||||
const category = language.t("palette.group.commands")
|
||||
return commandPaletteOptions(command.options).map((option) => createCommandPaletteCommandEntry(option, category))
|
||||
})
|
||||
const sessions = createServerSessionEntries({
|
||||
server: ServerConnection.key(props.server),
|
||||
opened: serverCtx.projects.list,
|
||||
stored: () => serverCtx.sync.data.project,
|
||||
load: (search, signal) =>
|
||||
serverCtx.sdk.client.experimental.session.list({ roots: true, search, limit: 50 }, { signal }),
|
||||
untitled: () => language.t("command.session.new"),
|
||||
category: () => language.t("command.category.session"),
|
||||
})
|
||||
|
||||
const highlight = (item: CommandPaletteEntry | undefined) => {
|
||||
state.cleanup?.()
|
||||
state.cleanup = undefined
|
||||
if (item?.type !== "command") return
|
||||
state.cleanup = item.option?.onHighlight?.()
|
||||
}
|
||||
const select = (item: CommandPaletteEntry | undefined) => {
|
||||
if (!item) return
|
||||
state.committed = true
|
||||
state.cleanup = undefined
|
||||
dialog.close()
|
||||
if (item.type === "command") {
|
||||
item.option?.onSelect?.("palette")
|
||||
return
|
||||
}
|
||||
if (item.type === "session") props.onSelectSession(item)
|
||||
}
|
||||
const loadItems = async (text: string) => {
|
||||
const query = text.trim()
|
||||
if (!query) return commandEntries().slice(0, 5)
|
||||
return [...commandEntries().filter((entry) => matchesEntry(entry, query)), ...(await sessions(query))]
|
||||
}
|
||||
|
||||
onCleanup(() => {
|
||||
if (state.committed) return
|
||||
state.cleanup?.()
|
||||
})
|
||||
|
||||
return (
|
||||
<CommandPaletteView
|
||||
placeholder={language.t("palette.search.placeholder.home")}
|
||||
loadItems={loadItems}
|
||||
highlight={highlight}
|
||||
select={select}
|
||||
close={() => dialog.close()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandPaletteView(props: {
|
||||
placeholder: string
|
||||
loadItems: (text: string) => CommandPaletteEntry[] | Promise<CommandPaletteEntry[]>
|
||||
highlight: (item: CommandPaletteEntry | undefined) => void
|
||||
select: (item: CommandPaletteEntry | undefined) => void
|
||||
close: () => void
|
||||
}) {
|
||||
const language = useLanguage()
|
||||
const tabs = useTabs()
|
||||
const [query, setQuery] = createSignal("")
|
||||
const [active, setActive] = createSignal(0)
|
||||
|
||||
const [entries] = createResource(query, props.loadItems, { initialValue: [] as CommandPaletteEntry[] })
|
||||
const [entries] = createResource(query, loadItems, { initialValue: [] as CommandPaletteEntry[] })
|
||||
// Render stale results while a new query loads to avoid flashing "Loading" per keystroke.
|
||||
const visibleEntries = createMemo(() => uniqueCommandPaletteEntries(entries.latest ?? []))
|
||||
const groupedEntries = createMemo(() => groups(visibleEntries()))
|
||||
const activeEntry = createMemo(() => visibleEntries()[active()])
|
||||
const openSessions = createMemo(
|
||||
() => new Set(tabs.store.flatMap((tab) => (tab.type === "session" ? [`${tab.server}\0${tab.sessionId}`] : []))),
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
query()
|
||||
@@ -152,7 +59,7 @@ function CommandPaletteView(props: {
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
props.highlight(activeEntry())
|
||||
palette.highlight(activeEntry())
|
||||
})
|
||||
|
||||
let resultsRef: HTMLDivElement | undefined
|
||||
@@ -179,12 +86,12 @@ function CommandPaletteView(props: {
|
||||
}
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault()
|
||||
props.select(activeEntry())
|
||||
palette.select(activeEntry())
|
||||
return
|
||||
}
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault()
|
||||
props.close()
|
||||
palette.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +105,7 @@ function CommandPaletteView(props: {
|
||||
autocomplete="off"
|
||||
spellcheck={false}
|
||||
appearance="large"
|
||||
placeholder={props.placeholder}
|
||||
placeholder={palette.language.t("palette.search.placeholder")}
|
||||
leadingIcon={<Icon name="magnifying-glass" />}
|
||||
onInput={(event) => setQuery(event.currentTarget.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
@@ -210,7 +117,7 @@ function CommandPaletteView(props: {
|
||||
when={visibleEntries().length > 0}
|
||||
fallback={
|
||||
<div class="command-palette-v2-state">
|
||||
{entries.loading ? language.t("common.loading") : language.t("palette.empty")}
|
||||
{entries.loading ? palette.language.t("common.loading") : palette.language.t("palette.empty")}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
@@ -225,14 +132,9 @@ function CommandPaletteView(props: {
|
||||
<PaletteRow
|
||||
item={item}
|
||||
active={activeEntry()?.id === item.id}
|
||||
language={language}
|
||||
sessionOpen={
|
||||
item.server && item.sessionID
|
||||
? openSessions().has(`${item.server}\0${item.sessionID}`)
|
||||
: false
|
||||
}
|
||||
language={palette.language}
|
||||
onActive={() => setActive(visibleEntries().findIndex((entry) => entry.id === item.id))}
|
||||
onSelect={() => props.select(item)}
|
||||
onSelect={() => palette.select(item)}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
@@ -251,19 +153,13 @@ function PaletteRow(props: {
|
||||
item: CommandPaletteEntry
|
||||
active: boolean
|
||||
language: ReturnType<typeof useLanguage>
|
||||
sessionOpen: boolean
|
||||
onActive: () => void
|
||||
onSelect: () => void
|
||||
}) {
|
||||
const session = () =>
|
||||
props.item.server && props.item.directory && props.item.sessionID
|
||||
? { server: props.item.server, directory: props.item.directory, sessionID: props.item.sessionID }
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
class="command-palette-v2-row group"
|
||||
class="command-palette-v2-row"
|
||||
role="option"
|
||||
aria-selected={props.active}
|
||||
data-active={props.active ? "" : undefined}
|
||||
@@ -301,25 +197,7 @@ function PaletteRow(props: {
|
||||
</Match>
|
||||
<Match when={props.item.type === "session"}>
|
||||
<div class="command-palette-v2-row-main">
|
||||
<div class="relative shrink-0">
|
||||
<Show when={props.sessionOpen}>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="pointer-events-none absolute top-1/2 h-3 w-0.5 -translate-y-1/2 rounded-[2px] bg-v2-background-bg-layer-04"
|
||||
style={{ right: "calc(100% + 4px)" }}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={session()}>
|
||||
{(session) => (
|
||||
<SessionTabAvatar
|
||||
project={props.item.project}
|
||||
directory={session().directory}
|
||||
sessionId={session().sessionID}
|
||||
server={session().server}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
<Icon name="status" class="command-palette-v2-row-icon" />
|
||||
<div class="command-palette-v2-row-text">
|
||||
<span class="command-palette-v2-title" classList={{ "opacity-70": !!props.item.archived }}>
|
||||
{props.item.title}
|
||||
|
||||
@@ -8,9 +8,49 @@ import introducingTabsVideo from "@/assets/help/introducing-tabs.mp4"
|
||||
import homeImage from "@/assets/help/home.png"
|
||||
import tabsImage from "@/assets/help/tabs.png"
|
||||
|
||||
const helpIcon = (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
data-slot="icon-svg"
|
||||
>
|
||||
<path
|
||||
d="M6.94235 10.5714V10.4854C6.94617 9.76302 7.01879 9.18777 7.16022 8.75968C7.30546 8.33158 7.50804 7.98567 7.76796 7.72193C8.02787 7.45819 8.34321 7.21548 8.71397 6.99379C8.93948 6.85619 9.14206 6.69374 9.32171 6.50645C9.50518 6.31916 9.64851 6.10511 9.75171 5.86431C9.85874 5.62351 9.91225 5.35404 9.91225 5.0559C9.91225 4.69661 9.82625 4.38509 9.65424 4.12136C9.48607 3.85762 9.26055 3.65504 8.9777 3.51362C8.69486 3.36837 8.38143 3.29575 8.03743 3.29575C7.73165 3.29575 7.43733 3.35882 7.15448 3.48495C6.87546 3.61108 6.6423 3.80984 6.45501 4.08122C6.26772 4.3526 6.15878 4.70425 6.12821 5.13617H4.56299C4.59357 4.47109 4.76557 3.9054 5.07899 3.43908C5.39242 2.96894 5.80522 2.61156 6.31741 2.36694C6.83341 2.12231 7.40675 2 8.03743 2C8.72161 2 9.31789 2.13378 9.82625 2.40134C10.3384 2.66507 10.734 3.0301 11.0131 3.49642C11.2959 3.96273 11.4373 4.49976 11.4373 5.1075C11.4373 5.53177 11.3724 5.914 11.2424 6.25418C11.1124 6.59436 10.9251 6.89823 10.6805 7.16579C10.4397 7.43335 10.1492 7.67033 9.80905 7.87673C9.48033 8.08313 9.21468 8.301 9.0121 8.53034C8.80952 8.75585 8.66237 9.02341 8.57063 9.33302C8.4789 9.64262 8.42921 10.0268 8.42156 10.4854V10.5714H6.94235ZM7.72782 14C7.43351 14 7.17933 13.8949 6.96528 13.6847C6.75506 13.4744 6.64994 13.2203 6.64994 12.9221C6.64994 12.6278 6.75506 12.3755 6.96528 12.1653C7.17933 11.9551 7.43351 11.85 7.72782 11.85C8.02214 11.85 8.27441 11.9551 8.48463 12.1653C8.69868 12.3755 8.8057 12.6278 8.8057 12.9221C8.8057 13.1209 8.75601 13.3024 8.65663 13.4668C8.55726 13.6273 8.4273 13.7573 8.26676 13.8567C8.10623 13.9522 7.92658 14 7.72782 14Z"
|
||||
fill="var(--v2-icon-icon-base)"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
const triggerClass =
|
||||
"size-7 !rounded-full shrink-0 bg-v2-background-bg-base shadow-[var(--v2-elevation-button-neutral)]"
|
||||
|
||||
// TODO: wire to changelog / seen-state when available
|
||||
const showPopover = () => true
|
||||
|
||||
export function HelpButton() {
|
||||
const platform = usePlatform()
|
||||
|
||||
return (
|
||||
<a
|
||||
href="https://opencode.ai"
|
||||
aria-label="Open the OpenCode website"
|
||||
data-component="icon-button-v2"
|
||||
data-size="large"
|
||||
class={`${triggerClass} fixed bottom-5 right-5 z-50 flex items-center justify-center`}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
platform.openLink(event.currentTarget.href)
|
||||
}}
|
||||
>
|
||||
{helpIcon}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
// can remove this after the tabs rollout has been out for a while
|
||||
export function TabsInfoPopup() {
|
||||
const settings = useSettings()
|
||||
@@ -21,7 +61,7 @@ export function TabsInfoPopup() {
|
||||
<Drawer open={drawerOpen()} onOpenChange={setDrawerOpen} side="right">
|
||||
<Show when={settings.general.shouldDisplayTabsToast()}>
|
||||
<div
|
||||
class="fixed bottom-5 right-5 z-50 h-[240px] w-[192px] rounded-[8px] bg-v2-background-bg-base p-1 shadow-[var(--v2-elevation-floating)]"
|
||||
class="fixed bottom-14 right-5 z-50 h-[240px] w-[192px] rounded-[8px] bg-v2-background-bg-base p-1 shadow-[var(--v2-elevation-floating)]"
|
||||
aria-label="Introducing Tabs. Organize your work and active sessions with tabs"
|
||||
>
|
||||
<button
|
||||
|
||||
@@ -10,7 +10,7 @@ import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
||||
import { showToast } from "@/utils/toast"
|
||||
import fuzzysort from "fuzzysort"
|
||||
import { DEFAULT_PALETTE_KEYBIND, formatKeybind, parseKeybind, useCommand } from "@/context/command"
|
||||
import { formatKeybind, parseKeybind, useCommand } from "@/context/command"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useSettings } from "@/context/settings"
|
||||
import { SettingsList } from "./settings-list"
|
||||
@@ -20,6 +20,7 @@ const IconV2 = lazy(() => import("@opencode-ai/ui/v2/icon").then((module) => ({
|
||||
|
||||
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
|
||||
const PALETTE_ID = "command.palette"
|
||||
const DEFAULT_PALETTE_KEYBIND = "mod+shift+p"
|
||||
|
||||
type KeybindGroup = "General" | "Session" | "Navigation" | "Model and agent" | "Terminal" | "Prompt"
|
||||
|
||||
|
||||
@@ -1,19 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { commandPaletteOptions, resolveKeybindOption, upsertCommandRegistration, type CommandOption } from "./command"
|
||||
|
||||
const paletteOptions: CommandOption[] = [
|
||||
{ id: "settings.open", title: "Open settings" },
|
||||
{ id: "session.undo", title: "Undo" },
|
||||
{ id: "file.open", title: "Open file" },
|
||||
{ id: "hidden", title: "Hidden", hidden: true },
|
||||
{ id: "disabled", title: "Disabled", disabled: true },
|
||||
]
|
||||
|
||||
describe("commandPaletteOptions", () => {
|
||||
test("keeps visible enabled commands", () => {
|
||||
expect(commandPaletteOptions(paletteOptions).map((option) => option.id)).toEqual(["settings.open", "session.undo"])
|
||||
})
|
||||
})
|
||||
import { resolveKeybindOption, upsertCommandRegistration } from "./command"
|
||||
|
||||
describe("upsertCommandRegistration", () => {
|
||||
test("replaces keyed registrations", () => {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Persist, persisted } from "@/utils/persist"
|
||||
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
|
||||
|
||||
const PALETTE_ID = "command.palette"
|
||||
export const DEFAULT_PALETTE_KEYBIND = "mod+k,mod+shift+p"
|
||||
const DEFAULT_PALETTE_KEYBIND = "mod+shift+p"
|
||||
const SUGGESTED_PREFIX = "suggested."
|
||||
const EDITABLE_KEYBIND_IDS = new Set(["terminal.toggle", "terminal.new", "file.attach"])
|
||||
|
||||
@@ -87,13 +87,6 @@ export interface CommandOption {
|
||||
onHighlight?: () => (() => void) | void
|
||||
}
|
||||
|
||||
export function commandPaletteOptions(options: CommandOption[]) {
|
||||
return options.filter(
|
||||
(option) =>
|
||||
!option.disabled && !option.hidden && !option.id.startsWith(SUGGESTED_PREFIX) && option.id !== "file.open",
|
||||
)
|
||||
}
|
||||
|
||||
export function resolveKeybindOption(candidates: CommandOption[] | undefined, event: KeyboardEvent) {
|
||||
return candidates?.find((option) => option.when?.(event)) ?? candidates?.find((option) => !option.when)
|
||||
}
|
||||
@@ -382,7 +375,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
||||
}
|
||||
|
||||
const showPalette = () => {
|
||||
run(PALETTE_ID, "palette")
|
||||
run("file.open", "palette")
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { hasCustomAgent, resolveAgent } from "./local-agent"
|
||||
|
||||
describe("hasCustomAgent", () => {
|
||||
test("detects explicitly custom agents", () => {
|
||||
expect(hasCustomAgent([{ native: true }, { native: false }])).toBe(true)
|
||||
})
|
||||
|
||||
test("ignores built-in and unclassified agents", () => {
|
||||
expect(hasCustomAgent([{ native: true }, {}])).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("resolveAgent", () => {
|
||||
const agents = [{ name: "plan" }, { name: "build" }, { name: "custom" }]
|
||||
|
||||
test("uses the requested available agent", () => {
|
||||
expect(resolveAgent(agents, "custom")?.name).toBe("custom")
|
||||
})
|
||||
|
||||
test("defaults to build", () => {
|
||||
expect(resolveAgent(agents)?.name).toBe("build")
|
||||
expect(resolveAgent(agents, "missing")?.name).toBe("build")
|
||||
})
|
||||
|
||||
test("uses the first agent when build is unavailable", () => {
|
||||
expect(resolveAgent([{ name: "custom" }], "missing")?.name).toBe("custom")
|
||||
})
|
||||
})
|
||||
@@ -1,7 +0,0 @@
|
||||
export function hasCustomAgent(items: Array<{ native?: boolean }>) {
|
||||
return items.some((item) => item.native === false)
|
||||
}
|
||||
|
||||
export function resolveAgent<T extends { name: string }>(items: T[], name?: string) {
|
||||
return items.find((item) => item.name === name) ?? items.find((item) => item.name === "build") ?? items[0]
|
||||
}
|
||||
@@ -4,10 +4,8 @@ import { useParams } from "@solidjs/router"
|
||||
import { batch, createEffect, createMemo, startTransition } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useModels } from "@/context/models"
|
||||
import { useSettings } from "@/context/settings"
|
||||
import { useProviders } from "@/hooks/use-providers"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { hasCustomAgent, resolveAgent } from "./local-agent"
|
||||
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
|
||||
import { useSDK } from "./sdk"
|
||||
import { useSync } from "./sync"
|
||||
@@ -64,11 +62,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
const serverSDK = useServerSDK()
|
||||
const providers = useProviders(() => sdk().directory)
|
||||
const models = useModels()
|
||||
const settings = useSettings()
|
||||
|
||||
const id = createMemo(() => params.id || undefined)
|
||||
const list = createMemo(() => sync().data.agent.filter((item) => item.mode !== "subagent" && !item.hidden))
|
||||
const agentsVisible = createMemo(() => settings.visibility.customAgents() || hasCustomAgent(list()))
|
||||
const connected = createMemo(() => new Set(providers.connected().map((item) => item.id)))
|
||||
|
||||
const [saved, setSaved, , savedReady] = persisted(
|
||||
@@ -111,7 +107,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
}
|
||||
|
||||
const pickAgent = (name: string | undefined) => {
|
||||
return resolveAgent(list(), name)
|
||||
const items = list()
|
||||
if (items.length === 0) return
|
||||
return items.find((item) => item.name === name) ?? items[0]
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
@@ -182,9 +180,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
|
||||
const agent = {
|
||||
list,
|
||||
visible: agentsVisible,
|
||||
current() {
|
||||
return pickAgent(agentsVisible() ? (scope()?.agent ?? store.current) : "build")
|
||||
return pickAgent(scope()?.agent ?? store.current)
|
||||
},
|
||||
set(name: string | undefined) {
|
||||
const item = pickAgent(name)
|
||||
|
||||
@@ -86,7 +86,6 @@ export const dict = {
|
||||
"command.session.unshare": "إلغاء مشاركة الجلسة",
|
||||
"command.session.unshare.description": "إيقاف مشاركة هذه الجلسة",
|
||||
"palette.search.placeholder": "البحث في الملفات والأوامر والجلسات",
|
||||
"palette.search.placeholder.home": "البحث في الأوامر والجلسات",
|
||||
"palette.empty": "لا توجد نتائج",
|
||||
"palette.group.commands": "الأوامر",
|
||||
"palette.group.files": "الملفات",
|
||||
|
||||
@@ -86,7 +86,6 @@ export const dict = {
|
||||
"command.session.unshare": "Parar de compartilhar sessão",
|
||||
"command.session.unshare.description": "Parar de compartilhar esta sessão",
|
||||
"palette.search.placeholder": "Buscar arquivos, comandos e sessões",
|
||||
"palette.search.placeholder.home": "Buscar comandos e sessões",
|
||||
"palette.empty": "Nenhum resultado encontrado",
|
||||
"palette.group.commands": "Comandos",
|
||||
"palette.group.files": "Arquivos",
|
||||
|
||||
@@ -93,7 +93,6 @@ export const dict = {
|
||||
"command.session.unshare.description": "Zaustavi dijeljenje ove sesije",
|
||||
|
||||
"palette.search.placeholder": "Pretraži datoteke, komande i sesije",
|
||||
"palette.search.placeholder.home": "Pretraži komande i sesije",
|
||||
"palette.empty": "Nema rezultata",
|
||||
"palette.group.commands": "Komande",
|
||||
"palette.group.files": "Datoteke",
|
||||
|
||||
@@ -93,7 +93,6 @@ export const dict = {
|
||||
"command.session.unshare.description": "Stop med at dele denne session",
|
||||
|
||||
"palette.search.placeholder": "Søg i filer, kommandoer og sessioner",
|
||||
"palette.search.placeholder.home": "Søg i kommandoer og sessioner",
|
||||
"palette.empty": "Ingen resultater fundet",
|
||||
"palette.group.commands": "Kommandoer",
|
||||
"palette.group.files": "Filer",
|
||||
|
||||
@@ -90,7 +90,6 @@ export const dict = {
|
||||
"command.session.unshare": "Teilen der Sitzung aufheben",
|
||||
"command.session.unshare.description": "Teilen dieser Sitzung beenden",
|
||||
"palette.search.placeholder": "Dateien, Befehle und Sitzungen durchsuchen",
|
||||
"palette.search.placeholder.home": "Befehle und Sitzungen durchsuchen",
|
||||
"palette.empty": "Keine Ergebnisse gefunden",
|
||||
"palette.group.commands": "Befehle",
|
||||
"palette.group.files": "Dateien",
|
||||
|
||||
@@ -93,7 +93,6 @@ export const dict = {
|
||||
"command.session.unshare.description": "Stop sharing this session",
|
||||
|
||||
"palette.search.placeholder": "Search files, commands, and sessions",
|
||||
"palette.search.placeholder.home": "Search commands and sessions",
|
||||
"palette.empty": "No results found",
|
||||
"palette.group.commands": "Commands",
|
||||
"palette.group.files": "Files",
|
||||
|
||||
@@ -93,7 +93,6 @@ export const dict = {
|
||||
"command.session.unshare.description": "Dejar de compartir esta sesión",
|
||||
|
||||
"palette.search.placeholder": "Buscar archivos, comandos y sesiones",
|
||||
"palette.search.placeholder.home": "Buscar comandos y sesiones",
|
||||
"palette.empty": "No se encontraron resultados",
|
||||
"palette.group.commands": "Comandos",
|
||||
"palette.group.files": "Archivos",
|
||||
|
||||
@@ -86,7 +86,6 @@ export const dict = {
|
||||
"command.session.unshare": "Ne plus partager la session",
|
||||
"command.session.unshare.description": "Arrêter de partager cette session",
|
||||
"palette.search.placeholder": "Rechercher des fichiers, des commandes et des sessions",
|
||||
"palette.search.placeholder.home": "Rechercher des commandes et des sessions",
|
||||
"palette.empty": "Aucun résultat trouvé",
|
||||
"palette.group.commands": "Commandes",
|
||||
"palette.group.files": "Fichiers",
|
||||
|
||||
@@ -86,7 +86,6 @@ export const dict = {
|
||||
"command.session.unshare": "セッションの共有を停止",
|
||||
"command.session.unshare.description": "このセッションの共有を停止",
|
||||
"palette.search.placeholder": "ファイル、コマンド、セッションを検索",
|
||||
"palette.search.placeholder.home": "コマンドとセッションを検索",
|
||||
"palette.empty": "結果が見つかりません",
|
||||
"palette.group.commands": "コマンド",
|
||||
"palette.group.files": "ファイル",
|
||||
|
||||
@@ -82,7 +82,6 @@ export const dict = {
|
||||
"command.session.unshare": "세션 공유 중지",
|
||||
"command.session.unshare.description": "이 세션 공유 중지",
|
||||
"palette.search.placeholder": "파일, 명령어 및 세션 검색",
|
||||
"palette.search.placeholder.home": "명령어 및 세션 검색",
|
||||
"palette.empty": "결과 없음",
|
||||
"palette.group.commands": "명령어",
|
||||
"palette.group.files": "파일",
|
||||
|
||||
@@ -92,7 +92,6 @@ export const dict = {
|
||||
"command.session.unshare.description": "Slutt å dele denne sesjonen",
|
||||
|
||||
"palette.search.placeholder": "Søk i filer, kommandoer og sesjoner",
|
||||
"palette.search.placeholder.home": "Søk i kommandoer og sesjoner",
|
||||
"palette.empty": "Ingen resultater funnet",
|
||||
"palette.group.commands": "Kommandoer",
|
||||
"palette.group.files": "Filer",
|
||||
|
||||
@@ -86,7 +86,6 @@ export const dict = {
|
||||
"command.session.unshare": "Przestań udostępniać sesję",
|
||||
"command.session.unshare.description": "Zatrzymaj udostępnianie tej sesji",
|
||||
"palette.search.placeholder": "Szukaj plików, poleceń i sesji",
|
||||
"palette.search.placeholder.home": "Szukaj poleceń i sesji",
|
||||
"palette.empty": "Brak wyników",
|
||||
"palette.group.commands": "Polecenia",
|
||||
"palette.group.files": "Pliki",
|
||||
|
||||
@@ -93,7 +93,6 @@ export const dict = {
|
||||
"command.session.unshare.description": "Прекратить публикацию сессии",
|
||||
|
||||
"palette.search.placeholder": "Поиск файлов, команд и сессий",
|
||||
"palette.search.placeholder.home": "Поиск команд и сессий",
|
||||
"palette.empty": "Ничего не найдено",
|
||||
"palette.group.commands": "Команды",
|
||||
"palette.group.files": "Файлы",
|
||||
|
||||
@@ -93,7 +93,6 @@ export const dict = {
|
||||
"command.session.unshare.description": "หยุดการแชร์เซสชันนี้",
|
||||
|
||||
"palette.search.placeholder": "ค้นหาไฟล์ คำสั่ง และเซสชัน",
|
||||
"palette.search.placeholder.home": "ค้นหาคำสั่งและเซสชัน",
|
||||
"palette.empty": "ไม่พบผลลัพธ์",
|
||||
"palette.group.commands": "คำสั่ง",
|
||||
"palette.group.files": "ไฟล์",
|
||||
|
||||
@@ -97,7 +97,6 @@ export const dict = {
|
||||
"command.session.unshare.description": "Bu oturumun paylaşımını durdur",
|
||||
|
||||
"palette.search.placeholder": "Dosya, komut ve oturum ara",
|
||||
"palette.search.placeholder.home": "Komut ve oturum ara",
|
||||
"palette.empty": "Sonuç bulunamadı",
|
||||
"palette.group.commands": "Komutlar",
|
||||
"palette.group.files": "Dosyalar",
|
||||
|
||||
@@ -93,7 +93,6 @@ export const dict = {
|
||||
"command.session.unshare.description": "Припинити поширення цієї сесії",
|
||||
|
||||
"palette.search.placeholder": "Пошук файлів, команд і сесій",
|
||||
"palette.search.placeholder.home": "Пошук команд і сесій",
|
||||
"palette.empty": "Результатів не знайдено",
|
||||
"palette.group.commands": "Команди",
|
||||
"palette.group.files": "Файли",
|
||||
|
||||
@@ -120,7 +120,6 @@ export const dict = {
|
||||
"command.session.unshare.description": "停止分享此会话",
|
||||
|
||||
"palette.search.placeholder": "搜索文件、命令和会话",
|
||||
"palette.search.placeholder.home": "搜索命令和会话",
|
||||
"palette.empty": "未找到结果",
|
||||
"palette.group.commands": "命令",
|
||||
"palette.group.files": "文件",
|
||||
|
||||
@@ -97,7 +97,6 @@ export const dict = {
|
||||
"command.session.unshare.description": "停止分享此工作階段",
|
||||
|
||||
"palette.search.placeholder": "搜尋檔案、命令和工作階段",
|
||||
"palette.search.placeholder.home": "搜尋命令和工作階段",
|
||||
"palette.empty": "找不到結果",
|
||||
"palette.group.commands": "命令",
|
||||
"palette.group.files": "檔案",
|
||||
|
||||
+119
-212
@@ -5,7 +5,6 @@ import {
|
||||
createMemo,
|
||||
createResource,
|
||||
createRoot,
|
||||
createSignal,
|
||||
For,
|
||||
Match,
|
||||
on,
|
||||
@@ -75,23 +74,6 @@ const HOME_SESSION_LIMIT = 64
|
||||
const HOME_SESSION_HEADER_STICKY_TOP = 12
|
||||
const HOME_SESSION_HEADER_TEXT_HEIGHT = 16
|
||||
const HOME_SESSION_HEADER_FADE_DISTANCE = 16
|
||||
|
||||
function containHomeWheel(event: WheelEvent, viewport: HTMLElement) {
|
||||
if (event.defaultPrevented || event.ctrlKey || !event.deltaY) return
|
||||
if (!(event.target instanceof Element)) return
|
||||
|
||||
const scrollable = event.target.closest<HTMLElement>("[data-scrollable]")
|
||||
if (
|
||||
scrollable !== viewport &&
|
||||
scrollable &&
|
||||
(event.deltaY < 0
|
||||
? scrollable.scrollTop > 0
|
||||
: scrollable.scrollTop < scrollable.scrollHeight - scrollable.clientHeight)
|
||||
)
|
||||
return
|
||||
|
||||
event.preventDefault()
|
||||
}
|
||||
const SHOW_HOME_SESSION_ARCHIVE = false
|
||||
const HOME_ROW_LAYOUT =
|
||||
"flex min-w-0 w-full shrink-0 cursor-default items-center rounded-[6px] bg-transparent text-left transition-[background-color,color,box-shadow] duration-[120ms] ease-in-out focus-visible:outline-none"
|
||||
@@ -162,7 +144,6 @@ function useHomeSessionHeaderOpacity(groups: () => HomeSessionGroup[]) {
|
||||
let content: HTMLDivElement | undefined
|
||||
let positionFrame: number | undefined
|
||||
let resizeObserver: ResizeObserver | undefined
|
||||
let stickyTop = HOME_SESSION_HEADER_STICKY_TOP
|
||||
const headerRefs = new Map<HomeSessionGroup["id"], HTMLDivElement>()
|
||||
const headerOffsets = new Map<HomeSessionGroup["id"], number>()
|
||||
const [state, setState] = createStore({
|
||||
@@ -221,13 +202,6 @@ function useHomeSessionHeaderOpacity(groups: () => HomeSessionGroup[]) {
|
||||
|
||||
function updatePositionCache() {
|
||||
if (!viewport) return
|
||||
const header = groups()
|
||||
.map((group) => headerRefs.get(group.id))
|
||||
.find((el) => el !== undefined)
|
||||
if (header && typeof getComputedStyle === "function") {
|
||||
const top = Number.parseFloat(getComputedStyle(header).top)
|
||||
if (Number.isFinite(top)) stickyTop = top
|
||||
}
|
||||
groups().forEach((group) => {
|
||||
const el = headerRefs.get(group.id)
|
||||
if (!el) return
|
||||
@@ -243,7 +217,7 @@ function useHomeSessionHeaderOpacity(groups: () => HomeSessionGroup[]) {
|
||||
.slice(index + 1)
|
||||
.map((item) => headerOffsets.get(item.id))
|
||||
.find((offset) => offset !== undefined)
|
||||
const fadeEnd = stickyTop + HOME_SESSION_HEADER_TEXT_HEIGHT
|
||||
const fadeEnd = HOME_SESSION_HEADER_STICKY_TOP + HOME_SESSION_HEADER_TEXT_HEIGHT
|
||||
const nextTop = nextOffset === undefined ? undefined : nextOffset - scrollTop
|
||||
const opacity =
|
||||
nextTop === undefined ? 1 : Math.max(0, Math.min(1, (nextTop - fadeEnd) / HOME_SESSION_HEADER_FADE_DISTANCE))
|
||||
@@ -297,9 +271,6 @@ export function NewHome() {
|
||||
const marked = useMarked()
|
||||
const openSettings = useSettingsCommand()
|
||||
let focusSessionSearch: (() => void) | undefined
|
||||
let sessionViewport: HTMLDivElement | undefined
|
||||
const [sessionThumbTrack, setSessionThumbTrack] = createSignal<HTMLDivElement>()
|
||||
const [sessionHoverTarget, setSessionHoverTarget] = createSignal<HTMLElement>()
|
||||
const [state, setState] = createStore({
|
||||
search: "",
|
||||
searchFocused: false,
|
||||
@@ -431,34 +402,6 @@ export function NewHome() {
|
||||
}
|
||||
|
||||
command.register("home", () => [
|
||||
{
|
||||
id: "command.palette",
|
||||
title: language.t("command.palette"),
|
||||
hidden: true,
|
||||
onSelect: async () => {
|
||||
const conn = focusedServer()
|
||||
if (!conn) return
|
||||
const ctx = global.ensureServerCtx(conn)
|
||||
const { DialogHomeCommandPaletteV2 } = await import("@/components/dialog-command-palette-v2")
|
||||
void dialog.show(() => (
|
||||
<DialogHomeCommandPaletteV2
|
||||
server={conn}
|
||||
onSelectSession={(entry) => {
|
||||
if (!entry.sessionID || !entry.directory || !entry.server) return
|
||||
const sessionID = entry.sessionID
|
||||
const server = entry.server
|
||||
const directory = entry.project?.worktree ?? entry.directory
|
||||
ctx.projects.open(directory)
|
||||
ctx.projects.touch(directory)
|
||||
void startTransition(() => {
|
||||
const tab = tabs.addSessionTab({ server, sessionId: sessionID })
|
||||
tabs.select(tab)
|
||||
})
|
||||
}}
|
||||
/>
|
||||
))
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "home.sessions.search.focus",
|
||||
title: searchPlaceholder(),
|
||||
@@ -598,158 +541,127 @@ export function NewHome() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="rounded-[10px] shadow-[var(--v2-elevation-raised)] m-2 min-h-0 overflow-hidden bg-v2-background-bg-base self-stretch flex-1">
|
||||
<ScrollView
|
||||
class="h-full [container-type:size]"
|
||||
thumbContainer={sessionThumbTrack}
|
||||
thumbHoverTarget={sessionHoverTarget}
|
||||
viewportRef={(el) => {
|
||||
sessionViewport = el
|
||||
sessionHeaderOpacity.setViewport(el)
|
||||
}}
|
||||
onScroll={(event) => sessionHeaderOpacity.update(event.currentTarget.scrollTop)}
|
||||
onWheel={(event) => {
|
||||
if (!sessionViewport) return
|
||||
if (event.target instanceof Node && sessionViewport.contains(event.target)) return
|
||||
containHomeWheel(event, sessionViewport)
|
||||
}}
|
||||
>
|
||||
<div class="mx-auto grid min-h-full w-full max-w-[1080px] grid-rows-[auto_minmax(0,1fr)_auto] gap-4 px-3 lg:grid-cols-[280px_minmax(0,720px)] lg:grid-rows-1 lg:gap-8 lg:px-6">
|
||||
<HomeProjectColumn
|
||||
projects={projects()}
|
||||
recentlyClosed={recentlyClosed()}
|
||||
homedir={homedir()}
|
||||
selected={selection()}
|
||||
focusServer={focusServer}
|
||||
selectProject={selectProject}
|
||||
openNewSession={openProjectNewSession}
|
||||
openRecentProject={(conn, directory) => addProjects(conn, [directory])}
|
||||
chooseProject={(conn) => void chooseProject(conn)}
|
||||
editProject={editProject}
|
||||
closeProject={(conn, directory) => {
|
||||
const next = closeHomeProject(
|
||||
selection(),
|
||||
ServerConnection.key(conn),
|
||||
global.ensureServerCtx(conn).projects,
|
||||
directory,
|
||||
)
|
||||
if (next) setSelection(next)
|
||||
}}
|
||||
clearNotifications={clearNotifications}
|
||||
unseenCount={unseenCount}
|
||||
openSettings={openSettings}
|
||||
openHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
|
||||
language={language}
|
||||
onWheel={(event) => {
|
||||
if (sessionViewport) containHomeWheel(event, sessionViewport)
|
||||
}}
|
||||
/>
|
||||
<div class="rounded-[10px] shadow-[var(--v2-elevation-raised)] m-2 min-h-0 lg:overflow-hidden bg-v2-background-bg-base self-stretch flex-1">
|
||||
<div class="mx-auto grid h-full w-full max-w-[1080px] grid-rows-[auto_minmax(0,1fr)_auto] gap-4 px-3 lg:grid-cols-[280px_minmax(0,720px)] lg:grid-rows-1 lg:gap-8 lg:px-6">
|
||||
<HomeProjectColumn
|
||||
projects={projects()}
|
||||
recentlyClosed={recentlyClosed()}
|
||||
homedir={homedir()}
|
||||
selected={selection()}
|
||||
focusServer={focusServer}
|
||||
selectProject={selectProject}
|
||||
openNewSession={openProjectNewSession}
|
||||
openRecentProject={(conn, directory) => addProjects(conn, [directory])}
|
||||
chooseProject={(conn) => void chooseProject(conn)}
|
||||
editProject={editProject}
|
||||
closeProject={(conn, directory) => {
|
||||
const next = closeHomeProject(
|
||||
selection(),
|
||||
ServerConnection.key(conn),
|
||||
global.ensureServerCtx(conn).projects,
|
||||
directory,
|
||||
)
|
||||
if (next) setSelection(next)
|
||||
}}
|
||||
clearNotifications={clearNotifications}
|
||||
unseenCount={unseenCount}
|
||||
openSettings={openSettings}
|
||||
openHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
|
||||
language={language}
|
||||
/>
|
||||
|
||||
<section
|
||||
ref={setSessionHoverTarget}
|
||||
class="min-h-0 min-w-0 flex-1 flex flex-col"
|
||||
aria-label={language.t("sidebar.project.recentSessions")}
|
||||
<section
|
||||
class="min-h-0 min-w-0 flex-1 flex flex-col pt-6 lg:pt-12 relative"
|
||||
aria-label={language.t("sidebar.project.recentSessions")}
|
||||
>
|
||||
<HomeSessionSearch
|
||||
value={state.search}
|
||||
placeholder={searchPlaceholder()}
|
||||
open={searchOpen()}
|
||||
loading={sessionLoad.isLoading}
|
||||
results={searchResults()}
|
||||
showProjectName={!selectedProject()}
|
||||
server={selection().server}
|
||||
noResultsLabel={language.t("home.sessions.search.noResults", { query: search() })}
|
||||
bindFocus={(focus) => {
|
||||
focusSessionSearch = focus
|
||||
}}
|
||||
onInput={(value) => setState("search", value)}
|
||||
onFocus={() => setState("searchFocused", true)}
|
||||
onClose={closeSearch}
|
||||
onSelect={selectSearchSession}
|
||||
/>
|
||||
<ScrollView
|
||||
class="mt-3 -mr-3 min-h-0 flex-1 relative"
|
||||
viewportRef={sessionHeaderOpacity.setViewport}
|
||||
onScroll={(event) => sessionHeaderOpacity.update(event.currentTarget.scrollTop)}
|
||||
>
|
||||
<div
|
||||
class="sticky top-0 z-30 shrink-0 bg-v2-background-bg-base pb-3 pt-6 lg:pt-12"
|
||||
onWheel={(event) => {
|
||||
if (sessionViewport) containHomeWheel(event, sessionViewport)
|
||||
}}
|
||||
<Show when={groups().length > 0 && newSessionProject()}>
|
||||
<div class="pointer-events-none absolute top-3 right-3 z-20 flex">
|
||||
<ButtonV2
|
||||
data-action="home-new-session"
|
||||
variant="ghost-muted"
|
||||
size="normal"
|
||||
icon="edit"
|
||||
class="pointer-events-auto h-7 px-2 [font-weight:530]"
|
||||
onClick={openNewSession}
|
||||
>
|
||||
{language.t("command.session.new")}
|
||||
</ButtonV2>
|
||||
</div>
|
||||
</Show>
|
||||
<Show
|
||||
when={!sessionLoad.isLoading}
|
||||
fallback={
|
||||
<div class="pt-3">
|
||||
<HomeSessionSkeleton label={language.t("common.loading")} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<HomeSessionSearch
|
||||
value={state.search}
|
||||
placeholder={searchPlaceholder()}
|
||||
open={searchOpen()}
|
||||
loading={sessionLoad.isLoading}
|
||||
results={searchResults()}
|
||||
showProjectName={!selectedProject()}
|
||||
server={selection().server}
|
||||
noResultsLabel={language.t("home.sessions.search.noResults", { query: search() })}
|
||||
bindFocus={(focus) => {
|
||||
focusSessionSearch = focus
|
||||
}}
|
||||
onInput={(value) => setState("search", value)}
|
||||
onFocus={() => setState("searchFocused", true)}
|
||||
onClose={closeSearch}
|
||||
onSelect={selectSearchSession}
|
||||
/>
|
||||
<Show when={groups().length > 0 && newSessionProject()}>
|
||||
<div class="pointer-events-none absolute right-0 top-[84px] z-20 flex lg:top-[108px]">
|
||||
<ButtonV2
|
||||
data-action="home-new-session"
|
||||
variant="ghost-muted"
|
||||
size="normal"
|
||||
icon="edit"
|
||||
class="pointer-events-auto h-7 px-2 [font-weight:530]"
|
||||
onClick={openNewSession}
|
||||
>
|
||||
{language.t("command.session.new")}
|
||||
</ButtonV2>
|
||||
<Show
|
||||
when={groups().length > 0}
|
||||
fallback={<HomeSessionsEmpty onNewSession={newSessionProject() ? openNewSession : undefined} />}
|
||||
>
|
||||
<div ref={sessionHeaderOpacity.setContentRef} class="flex flex-col pt-3 pr-3 pb-16">
|
||||
<For each={groups()}>
|
||||
{(group, index) => (
|
||||
<>
|
||||
<HomeSessionGroupHeader
|
||||
title={group.title}
|
||||
titleOpacity={sessionHeaderOpacity.titleOpacity(group.id)}
|
||||
ref={(el) => sessionHeaderOpacity.setHeaderRef(group.id, el)}
|
||||
elevated={index() === 0}
|
||||
/>
|
||||
<div
|
||||
class={`flex min-w-0 flex-col gap-px pt-4 ${index() === groups().length - 1 ? "" : "mb-6"}`}
|
||||
>
|
||||
<For each={group.sessions}>
|
||||
{(record) => (
|
||||
<HomeSessionRow
|
||||
record={record}
|
||||
showProjectName={!selectedProject()}
|
||||
server={selection().server}
|
||||
openSession={openSession}
|
||||
archiveSession={archiveSession}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
{/* Sticky chrome for the portaled session scrollbar — matches old sessions ScrollView bounds */}
|
||||
<div class="pointer-events-none sticky top-[84px] z-40 h-0 -mr-3 lg:top-[108px]">
|
||||
<div
|
||||
ref={setSessionThumbTrack}
|
||||
data-component="home-session-scroll-track"
|
||||
class="relative ml-auto h-[calc(100cqh-84px)] w-3 lg:h-[calc(100cqh-108px)]"
|
||||
/>
|
||||
</div>
|
||||
<div class="-mr-3 min-h-[calc(100cqh-72px)] lg:min-h-[calc(100cqh-96px)]">
|
||||
<Show
|
||||
when={!sessionLoad.isLoading}
|
||||
fallback={
|
||||
<div class="pt-3">
|
||||
<HomeSessionSkeleton label={language.t("common.loading")} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Show
|
||||
when={groups().length > 0}
|
||||
fallback={<HomeSessionsEmpty onNewSession={newSessionProject() ? openNewSession : undefined} />}
|
||||
>
|
||||
<div ref={sessionHeaderOpacity.setContentRef} class="flex flex-col pt-3 pr-3 pb-16">
|
||||
<For each={groups()}>
|
||||
{(group, index) => (
|
||||
<>
|
||||
<HomeSessionGroupHeader
|
||||
title={group.title}
|
||||
titleOpacity={sessionHeaderOpacity.titleOpacity(group.id)}
|
||||
ref={(el) => sessionHeaderOpacity.setHeaderRef(group.id, el)}
|
||||
elevated={index() === 0}
|
||||
/>
|
||||
<div
|
||||
class={`flex min-w-0 flex-col gap-px pt-4 ${index() === groups().length - 1 ? "" : "mb-6"}`}
|
||||
>
|
||||
<For each={group.sessions}>
|
||||
{(record) => (
|
||||
<HomeSessionRow
|
||||
record={record}
|
||||
showProjectName={!selectedProject()}
|
||||
server={selection().server}
|
||||
openSession={openSession}
|
||||
archiveSession={archiveSession}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
</section>
|
||||
<HomeUtilityNav
|
||||
class="flex lg:hidden"
|
||||
openSettings={openSettings}
|
||||
openHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
|
||||
language={language}
|
||||
/>
|
||||
</div>
|
||||
</ScrollView>
|
||||
</Show>
|
||||
</ScrollView>
|
||||
</section>
|
||||
<HomeUtilityNav
|
||||
class="flex lg:hidden"
|
||||
openSettings={openSettings}
|
||||
openHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
|
||||
language={language}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -771,7 +683,6 @@ function HomeProjectColumn(props: {
|
||||
openSettings: () => void
|
||||
openHelp: () => void
|
||||
language: ReturnType<typeof useLanguage>
|
||||
onWheel: (event: WheelEvent) => void
|
||||
}) {
|
||||
const global = useGlobal()
|
||||
const dialog = useDialog()
|
||||
@@ -788,12 +699,8 @@ function HomeProjectColumn(props: {
|
||||
|
||||
return (
|
||||
<aside
|
||||
class="mt-6 flex min-h-0 min-w-0 flex-col gap-4 overflow-hidden lg:sticky lg:top-14 lg:mt-14 lg:h-[calc(100cqh-56px)] lg:self-start lg:pt-[52px]"
|
||||
class="mt-6 flex min-h-0 min-w-0 flex-col gap-4 overflow-hidden lg:mt-14 lg:pt-[52px]"
|
||||
aria-label={props.language.t("home.projects")}
|
||||
onWheel={(event) => {
|
||||
if (event.target === event.currentTarget) return
|
||||
props.onWheel(event)
|
||||
}}
|
||||
>
|
||||
<div class="flex h-7 min-w-0 shrink-0 items-center justify-between pl-1.5 pr-3">
|
||||
<div class="text-v2-text-text-muted [font-weight:530]">{props.language.t("home.projects")}</div>
|
||||
@@ -1511,7 +1418,7 @@ function HomeSessionGroupHeader(props: {
|
||||
return (
|
||||
<div
|
||||
ref={props.ref}
|
||||
class={`pointer-events-none sticky top-[84px] lg:top-[108px] flex h-7 min-w-0 items-center justify-between pl-3 bg-v2-background-bg-base ${props.elevated ? "home-session-group-header z-[5]" : "z-10"}`}
|
||||
class={`pointer-events-none sticky top-3 flex h-7 min-w-0 items-center justify-between pl-3 bg-v2-background-bg-base ${props.elevated ? "home-session-group-header z-[5]" : "z-10"}`}
|
||||
>
|
||||
<div class={HOME_SECTION_LABEL} style={{ opacity: props.titleOpacity }}>
|
||||
{props.title}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createEffect, Suspense, type ParentProps } from "solid-js"
|
||||
import { useNavigate } from "@solidjs/router"
|
||||
import { DebugBar } from "@/components/debug-bar"
|
||||
import { TabsInfoPopup } from "@/components/help-button"
|
||||
import { HelpButton, TabsInfoPopup } from "@/components/help-button"
|
||||
import { Titlebar, type TitlebarUpdate } from "@/components/titlebar"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { setNavigate } from "@/utils/notification-click"
|
||||
@@ -38,6 +38,7 @@ export default function NewLayout(props: ParentProps) {
|
||||
</main>
|
||||
{import.meta.env.DEV && <DebugBar inline />}
|
||||
<TabsInfoPopup />
|
||||
<HelpButton />
|
||||
<ToastRegion v2 />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -54,7 +54,7 @@ import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context"
|
||||
import { useCommand, type CommandOption } from "@/context/command"
|
||||
import { ConstrainDragXAxis, getDraggableId } from "@/utils/solid-dnd"
|
||||
import { DebugBar } from "@/components/debug-bar"
|
||||
import { TabsInfoPopup } from "@/components/help-button"
|
||||
import { HelpButton, TabsInfoPopup } from "@/components/help-button"
|
||||
import { Titlebar, type TitlebarUpdate } from "@/components/titlebar"
|
||||
import { useDirectoryPicker } from "@/components/directory-picker"
|
||||
import { ServerConnection, useServer } from "@/context/server"
|
||||
@@ -2396,6 +2396,7 @@ export default function LegacyLayout(props: ParentProps) {
|
||||
{import.meta.env.DEV && import.meta.env.VITE_DISABLE_DEBUG_BAR !== "1" && <DebugBar />}
|
||||
</div>
|
||||
<TabsInfoPopup />
|
||||
<HelpButton />
|
||||
<ToastRegion v2={false} />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -3,7 +3,6 @@ import { createStore } from "solid-js/store"
|
||||
import { Portal } from "solid-js/web"
|
||||
import { useSearchParams } from "@solidjs/router"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||
import { NewSessionDesignView } from "@/components/session"
|
||||
@@ -52,7 +51,6 @@ export default function NewSessionPage() {
|
||||
const comments = useComments()
|
||||
const language = useLanguage()
|
||||
const settings = useSettings()
|
||||
const dialog = useDialog()
|
||||
const command = useCommand()
|
||||
const providers = useProviders(() => sdk().directory)
|
||||
const openProviderSettings = useSettingsDialog("providers")
|
||||
@@ -79,15 +77,6 @@ export default function NewSessionPage() {
|
||||
})
|
||||
|
||||
command.register("new-session", () => [
|
||||
{
|
||||
id: "command.palette",
|
||||
title: language.t("command.palette"),
|
||||
hidden: true,
|
||||
onSelect: async () => {
|
||||
const { DialogSelectFile } = await import("@/components/dialog-select-file")
|
||||
void dialog.show(() => <DialogSelectFile />)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "input.focus",
|
||||
title: language.t("command.input.focus"),
|
||||
|
||||
@@ -41,7 +41,6 @@ import { useLocation, useNavigate, useParams, useSearchParams } from "@solidjs/r
|
||||
import { NewSessionView, SessionHeader } from "@/components/session"
|
||||
import { ErrorPage } from "@/pages/error"
|
||||
import { CommentsProvider, useComments } from "@/context/comments"
|
||||
import { useCommand } from "@/context/command"
|
||||
import { DirectoryDataProvider } from "@/pages/directory-layout"
|
||||
import { useServerSync } from "@/context/server-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
@@ -363,7 +362,6 @@ export default function Page() {
|
||||
const platform = usePlatform()
|
||||
const prompt = usePrompt()
|
||||
const comments = useComments()
|
||||
const command = useCommand()
|
||||
const terminal = useTerminal()
|
||||
const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>()
|
||||
const location = useLocation()
|
||||
@@ -1100,14 +1098,6 @@ export default function Page() {
|
||||
review: reviewTab,
|
||||
fileBrowser: () => newSessionDesign() && isDesktop() && !!params.id,
|
||||
})
|
||||
command.register("session-palette", () => [
|
||||
{
|
||||
id: "command.palette",
|
||||
title: language.t("command.palette"),
|
||||
hidden: true,
|
||||
onSelect: () => command.trigger("file.open", "palette"),
|
||||
},
|
||||
])
|
||||
|
||||
const openReviewFile = createOpenReviewFile({
|
||||
showAllFiles,
|
||||
|
||||
@@ -41,7 +41,7 @@ export function createPromptInputController(input: {
|
||||
options: local.agent.list().map((agent) => agent.name),
|
||||
current: local.agent.current()?.name ?? "",
|
||||
loading: agentsQuery.isLoading,
|
||||
visible: local.agent.visible(),
|
||||
visible: settings.visibility.customAgents(),
|
||||
select: local.agent.set,
|
||||
},
|
||||
model: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCommand, type CommandOption } from "@/context/command"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useLocal, type ModelSelection } from "@/context/local"
|
||||
import { useSettings } from "@/context/settings"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { getCursorPosition, setCursorPosition } from "@/components/prompt-input/editor-dom"
|
||||
import { useSessionLayout } from "./session-layout"
|
||||
@@ -18,6 +19,7 @@ export const useComposerCommands = (input: { model?: ModelSelection } = {}) => {
|
||||
const dialog = useDialog()
|
||||
const language = useLanguage()
|
||||
const local = useLocal()
|
||||
const settings = useSettings()
|
||||
const { sessionKey } = useSessionLayout()
|
||||
const sessionOwnership = createSessionOwnership(sessionKey)
|
||||
const model = input.model ?? local.model
|
||||
@@ -68,7 +70,7 @@ export const useComposerCommands = (input: { model?: ModelSelection } = {}) => {
|
||||
description: language.t("command.agent.cycle.description"),
|
||||
keybind: "mod+.",
|
||||
slash: "agent",
|
||||
disabled: !local.agent.visible(),
|
||||
disabled: !settings.visibility.customAgents(),
|
||||
onSelect: () => local.agent.move(1),
|
||||
}),
|
||||
agentCommand({
|
||||
@@ -76,7 +78,7 @@ export const useComposerCommands = (input: { model?: ModelSelection } = {}) => {
|
||||
title: language.t("command.agent.cycle.reverse"),
|
||||
description: language.t("command.agent.cycle.reverse.description"),
|
||||
keybind: "shift+mod+.",
|
||||
disabled: !local.agent.visible(),
|
||||
disabled: !settings.visibility.customAgents(),
|
||||
onSelect: () => local.agent.move(-1),
|
||||
}),
|
||||
])
|
||||
|
||||
@@ -468,7 +468,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
id: "file.open",
|
||||
title: language.t("command.file.open"),
|
||||
description: language.t("palette.search.placeholder"),
|
||||
keybind: "mod+p",
|
||||
keybind: "mod+k,mod+p",
|
||||
slash: "open",
|
||||
onSelect: openFile,
|
||||
}),
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type { GlobalSession, Project } from "@opencode-ai/sdk/v2/client"
|
||||
import { createRoot } from "solid-js"
|
||||
import { createServerSessionEntries } from "@/components/command-palette"
|
||||
import type { LocalProject } from "@/context/layout"
|
||||
import { ServerConnection } from "@/context/server"
|
||||
import { getProjectAvatarSource } from "@/pages/layout/helpers"
|
||||
|
||||
const stored: Project = {
|
||||
id: "project-1",
|
||||
name: "Palette project",
|
||||
worktree: "/workspace/project",
|
||||
sandboxes: [],
|
||||
time: { created: 1, updated: 1 },
|
||||
}
|
||||
|
||||
const session: GlobalSession = {
|
||||
id: "session-1",
|
||||
slug: "session-1",
|
||||
projectID: stored.id,
|
||||
directory: stored.worktree,
|
||||
title: "Palette session",
|
||||
version: "1",
|
||||
time: { created: 1, updated: 2 },
|
||||
project: { id: stored.id, name: stored.name, worktree: stored.worktree },
|
||||
}
|
||||
|
||||
describe("command palette sessions", () => {
|
||||
test("uses the home project avatar and cancels superseded searches", async () => {
|
||||
const server = ServerConnection.Key.make("selected-server")
|
||||
const opened: LocalProject = {
|
||||
...stored,
|
||||
icon: { override: "home-project-avatar" },
|
||||
expanded: true,
|
||||
}
|
||||
const searches: string[] = []
|
||||
const result = await new Promise<Awaited<ReturnType<ReturnType<typeof createServerSessionEntries>>>>(
|
||||
(resolve, reject) => {
|
||||
createRoot((dispose) => {
|
||||
const search = createServerSessionEntries({
|
||||
server,
|
||||
opened: () => [opened],
|
||||
stored: () => [{ ...stored, icon: { url: "stored-project-avatar" } }],
|
||||
load: async (text) => {
|
||||
searches.push(text)
|
||||
return {
|
||||
data: [session, { ...session, id: "archived-session", time: { ...session.time, archived: 3 } }],
|
||||
}
|
||||
},
|
||||
untitled: () => "Untitled",
|
||||
category: () => "Sessions",
|
||||
})
|
||||
const first = search("palette")
|
||||
const second = search("palette session")
|
||||
Promise.all([first, second])
|
||||
.then(([cancelled, entries]) => {
|
||||
expect(cancelled).toEqual([])
|
||||
resolve(entries)
|
||||
})
|
||||
.catch(reject)
|
||||
.finally(dispose)
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
expect(searches).toEqual(["palette session"])
|
||||
expect(result).toHaveLength(1)
|
||||
expect(getProjectAvatarSource(result[0]?.project?.id, result[0]?.project?.icon)).toBe("home-project-avatar")
|
||||
expect(result[0]).toMatchObject({
|
||||
server,
|
||||
sessionID: session.id,
|
||||
description: stored.name,
|
||||
project: { id: stored.id, icon: opened.icon },
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -31,13 +31,11 @@ function run(target) {
|
||||
|
||||
const envPath = process.env.OPENCODE_BIN_PATH
|
||||
const scriptDir = path.dirname(fs.realpathSync(__filename))
|
||||
const command = path.basename(__filename).replace(/\.cjs$/, "")
|
||||
const nodeBuild = command === "opencode2-node"
|
||||
const cached = path.join(scriptDir, `.${command}`)
|
||||
const cached = path.join(scriptDir, ".opencode2")
|
||||
const platform = { darwin: "darwin", linux: "linux", win32: "windows" }[os.platform()] || os.platform()
|
||||
const arch = { x64: "x64", arm64: "arm64", arm: "arm" }[os.arch()] || os.arch()
|
||||
const base = `@opencode-ai/cli${nodeBuild ? "-node" : ""}-` + platform + "-" + arch
|
||||
const binary = platform === "windows" ? `${command}.exe` : command
|
||||
const base = "@opencode-ai/cli-" + platform + "-" + arch
|
||||
const binary = platform === "windows" ? "opencode2.exe" : "opencode2"
|
||||
|
||||
function supportsAvx2() {
|
||||
if (arch !== "x64") return false
|
||||
@@ -79,7 +77,6 @@ function supportsAvx2() {
|
||||
}
|
||||
|
||||
const names = (() => {
|
||||
if (nodeBuild) return [base]
|
||||
const baseline = arch === "x64" && !supportsAvx2()
|
||||
if (platform === "linux") {
|
||||
const musl = (() => {
|
||||
@@ -124,7 +121,7 @@ function findBinary(startDir) {
|
||||
const resolved = envPath || (fs.existsSync(cached) ? cached : findBinary(scriptDir))
|
||||
if (!resolved) {
|
||||
console.error(
|
||||
`It seems that your package manager failed to install the right ${command} CLI package. Try manually installing ` +
|
||||
"It seems that your package manager failed to install the right opencode2 CLI package. Try manually installing " +
|
||||
names.map((name) => `"${name}"`).join(" or ") +
|
||||
" package",
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/cli",
|
||||
"version": "1.18.3",
|
||||
"version": "1.18.2",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -26,7 +26,6 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "bun run script/build.ts",
|
||||
"build:node": "bun run script/build-node.ts",
|
||||
"dev": "bun run src/index.ts",
|
||||
"test": "bun test --timeout 30000 --only-failures",
|
||||
"typecheck": "tsgo --noEmit"
|
||||
@@ -52,8 +51,7 @@
|
||||
"semver": "catalog:",
|
||||
"solid-js": "catalog:",
|
||||
"strip-ansi": "7.1.2",
|
||||
"uqr": "0.1.3",
|
||||
"ws": "8.21.0"
|
||||
"uqr": "0.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
@@ -61,19 +59,6 @@
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/semver": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"@lydell/node-pty-darwin-arm64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-darwin-x64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-linux-arm64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-linux-x64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-win32-arm64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-win32-x64": "1.2.0-beta.12",
|
||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-glibc": "2.5.1",
|
||||
"@parcel/watcher-win32-arm64": "2.5.1",
|
||||
"@parcel/watcher-win32-x64": "2.5.1",
|
||||
"vite": "catalog:",
|
||||
"vite-plugin-solid": "catalog:"
|
||||
"@typescript/native-preview": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { spawnSync } from "node:child_process"
|
||||
import { createHash } from "node:crypto"
|
||||
import { chmod, copyFile, mkdir, mkdtemp, realpath, rename, rm, stat, writeFile } from "node:fs/promises"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import { build } from "vite"
|
||||
import { Script } from "@opencode-ai/script"
|
||||
import pkg from "../package.json"
|
||||
import { modelsData } from "./generate"
|
||||
import { collectNodeAssets, copyNodeAssets, hashNodeAssets, seaAssetMap } from "./node-assets"
|
||||
import { mainConfig } from "../vite.node.config"
|
||||
import { nodeExecArgv, nodeTarget, type NodeTarget } from "../src/node/target"
|
||||
|
||||
const NODE_VERSION = "26.4.0"
|
||||
const dir = path.resolve(import.meta.dirname, "..")
|
||||
const outdir = path.resolve(
|
||||
dir,
|
||||
process.argv.find((arg) => arg.startsWith("--outdir="))?.slice("--outdir=".length) ?? "dist",
|
||||
)
|
||||
if (outdir === dir) throw new Error("--outdir must not be the package directory")
|
||||
if (outdir === path.join(dir, "dist-node")) {
|
||||
throw new Error("--outdir must not be dist-node because it contains temporary files")
|
||||
}
|
||||
const bundleOnly = process.argv.includes("--bundle-only")
|
||||
const single = process.argv.includes("--single")
|
||||
const skipInstall = process.argv.includes("--skip-install")
|
||||
const requested = process.argv.find((arg) => arg.startsWith("--target="))?.slice("--target=".length)
|
||||
const allTargets = [
|
||||
nodeTarget("linux", "arm64"),
|
||||
nodeTarget("linux", "x64"),
|
||||
nodeTarget("darwin", "arm64"),
|
||||
nodeTarget("win32", "arm64"),
|
||||
nodeTarget("win32", "x64"),
|
||||
]
|
||||
const targets = requested
|
||||
? allTargets.filter((target) => targetName(target) === requested)
|
||||
: single || bundleOnly
|
||||
? [nodeTarget(process.platform, process.arch)]
|
||||
: allTargets
|
||||
|
||||
if (targets.length === 0) {
|
||||
if (requested === "darwin-x64") throw new Error("Node 26.4 SEA does not support macOS x64")
|
||||
throw new Error(`Unknown Node target: ${requested}`)
|
||||
}
|
||||
if (!bundleOnly && targets.some((target) => target.platform === "darwin" && target.arch === "x64")) {
|
||||
throw new Error("Node 26.4 SEA does not support macOS x64")
|
||||
}
|
||||
|
||||
process.chdir(dir)
|
||||
if (!skipInstall) run(process.execPath, ["install", "--os=*", "--cpu=*"])
|
||||
if (!bundleOnly) await rm(outdir, { recursive: true, force: true })
|
||||
const builder =
|
||||
!bundleOnly || targets.some((target) => target.platform === process.platform && target.arch === process.arch)
|
||||
? await resolveHostNode()
|
||||
: undefined
|
||||
|
||||
for (const target of targets) {
|
||||
console.log(`building cli-node-${targetName(target)}`)
|
||||
const assets = await collectNodeAssets(target)
|
||||
await rm("dist-node", { recursive: true, force: true })
|
||||
const assetHash = await hashNodeAssets(assets)
|
||||
const input = { version: Script.version, channel: Script.channel, models: modelsData, assetHash, target }
|
||||
await build(mainConfig(input))
|
||||
await copyNodeAssets(assets)
|
||||
|
||||
const host = target.platform === process.platform && target.arch === process.arch
|
||||
if (host) {
|
||||
if (!builder) throw new Error("Node SEA builder is unavailable")
|
||||
run(builder, [...nodeExecArgv, "dist-node/opencode.mjs", "--version"])
|
||||
run(builder, [...nodeExecArgv, "dist-node/opencode.mjs", "--help"])
|
||||
}
|
||||
if (bundleOnly) continue
|
||||
|
||||
const name = `cli-node-${targetName(target)}`
|
||||
const binary = target.platform === "win32" ? "opencode2-node.exe" : "opencode2-node"
|
||||
const output = path.join(outdir, name, "bin", binary)
|
||||
if (!builder) throw new Error("Node SEA builder is unavailable")
|
||||
await mkdir(path.dirname(output), { recursive: true })
|
||||
const config = {
|
||||
main: "dist-node/opencode.mjs",
|
||||
mainFormat: "module",
|
||||
executable: await resolveTargetNode(target, builder),
|
||||
output: path.relative(dir, output),
|
||||
disableExperimentalSEAWarning: true,
|
||||
useSnapshot: false,
|
||||
useCodeCache: false,
|
||||
execArgv: nodeExecArgv,
|
||||
execArgvExtension: "none",
|
||||
assets: await seaAssetMap(),
|
||||
}
|
||||
await writeFile("dist-node/sea.json", `${JSON.stringify(config, null, 2)}\n`)
|
||||
run(builder, ["--build-sea", "dist-node/sea.json"])
|
||||
if (target.platform !== "win32") await chmod(output, 0o755)
|
||||
if (target.platform === "darwin" && process.platform === "darwin") run("codesign", ["--sign", "-", output])
|
||||
if (target.platform === "darwin" && process.platform !== "darwin") {
|
||||
console.warn(`${output} must be signed on macOS before it can run`)
|
||||
}
|
||||
await writeFile(
|
||||
path.join(outdir, name, "package.json"),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
name: `@opencode-ai/${name}`,
|
||||
version: Script.version,
|
||||
license: pkg.license,
|
||||
repository: { type: "git", url: "git+https://github.com/anomalyco/opencode.git" },
|
||||
os: [target.platform],
|
||||
cpu: [target.arch],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
)
|
||||
if (host) await smoke(output)
|
||||
}
|
||||
|
||||
async function resolveHostNode() {
|
||||
const candidates = [process.env.NODE_BIN, "node"].filter((item): item is string => Boolean(item))
|
||||
for (const candidate of candidates) {
|
||||
const result = spawnSync(
|
||||
candidate,
|
||||
["-p", "JSON.stringify({version:process.versions.node,path:process.execPath})"],
|
||||
{
|
||||
encoding: "utf8",
|
||||
},
|
||||
)
|
||||
if (result.status !== 0) continue
|
||||
const info = JSON.parse(result.stdout) as { version: string; path: string }
|
||||
if (info.version === NODE_VERSION) return realpath(info.path)
|
||||
}
|
||||
return resolveTargetNode(nodeTarget(process.platform, process.arch))
|
||||
}
|
||||
|
||||
async function resolveTargetNode(target: NodeTarget, host?: string) {
|
||||
if (host && target.platform === process.platform && target.arch === process.arch) return host
|
||||
const cache = path.resolve(dir, ".cache", "node")
|
||||
const platform = target.platform === "win32" ? "win" : target.platform
|
||||
const archiveName = `node-v${NODE_VERSION}-${platform}-${target.arch}`
|
||||
const targetDirectory = path.join(cache, archiveName)
|
||||
const executable = path.join(targetDirectory, target.platform === "win32" ? "node.exe" : "bin/node")
|
||||
if (
|
||||
(await stat(executable).then(
|
||||
() => true,
|
||||
() => false,
|
||||
)) &&
|
||||
(await stat(path.join(targetDirectory, ".verified")).then(
|
||||
() => true,
|
||||
() => false,
|
||||
))
|
||||
)
|
||||
return realpath(executable)
|
||||
await mkdir(cache, { recursive: true })
|
||||
const extension = target.platform === "win32" ? "zip" : "tar.gz"
|
||||
const filename = `${archiveName}.${extension}`
|
||||
const archive = path.join(cache, filename)
|
||||
const base = `https://nodejs.org/dist/v${NODE_VERSION}`
|
||||
const [response, sums] = await Promise.all([fetch(`${base}/${filename}`), fetch(`${base}/SHASUMS256.txt`)])
|
||||
if (!response.ok) throw new Error(`Failed to download Node ${NODE_VERSION}: ${response.status}`)
|
||||
if (!sums.ok) throw new Error(`Failed to download Node ${NODE_VERSION} checksums: ${sums.status}`)
|
||||
const data = new Uint8Array(await response.arrayBuffer())
|
||||
const expected = (await sums.text())
|
||||
.split("\n")
|
||||
.find((line) => line.endsWith(` ${filename}`))
|
||||
?.split(/\s+/)[0]
|
||||
if (!expected) throw new Error(`Missing checksum for ${filename}`)
|
||||
if (createHash("sha256").update(data).digest("hex") !== expected) throw new Error(`Checksum mismatch for ${filename}`)
|
||||
await writeFile(archive, data)
|
||||
const temporary = path.join(cache, `${archiveName}.${process.pid}.tmp`)
|
||||
await rm(temporary, { recursive: true, force: true })
|
||||
await mkdir(temporary)
|
||||
if (target.platform !== "win32") run("tar", ["-xzf", archive, "-C", temporary])
|
||||
if (target.platform === "win32" && process.platform === "win32") {
|
||||
run(path.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "tar.exe"), ["-xf", archive, "-C", temporary])
|
||||
}
|
||||
if (target.platform === "win32" && process.platform !== "win32") run("unzip", ["-q", archive, "-d", temporary])
|
||||
await rm(targetDirectory, { recursive: true, force: true })
|
||||
await rename(path.join(temporary, archiveName), targetDirectory)
|
||||
await writeFile(path.join(targetDirectory, ".verified"), `${expected}\n`)
|
||||
await rm(temporary, { recursive: true, force: true })
|
||||
await rm(archive, { force: true })
|
||||
return realpath(executable)
|
||||
}
|
||||
|
||||
async function smoke(output: string) {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "opencode-node-smoke-"))
|
||||
const executable = path.join(root, path.basename(output))
|
||||
await copyFile(output, executable)
|
||||
if (process.platform !== "win32") await chmod(executable, 0o755)
|
||||
run(executable, ["--version"], root)
|
||||
run(executable, ["--help"], root)
|
||||
await rm(root, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
function targetName(target: NodeTarget) {
|
||||
return `${target.platform === "win32" ? "windows" : target.platform}-${target.arch}`
|
||||
}
|
||||
|
||||
function run(command: string, args: readonly string[], cwd = dir) {
|
||||
const result = spawnSync(command, args, { cwd, stdio: "inherit", env: process.env })
|
||||
if (result.error) throw result.error
|
||||
if (result.status !== 0) throw new Error(`${command} exited with status ${result.status ?? "unknown"}`)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { $ } from "bun"
|
||||
import fs from "fs"
|
||||
import { rm } from "fs/promises"
|
||||
import path from "path"
|
||||
import { Script } from "@opencode-ai/script"
|
||||
@@ -10,14 +11,9 @@ import { modelsData } from "./generate"
|
||||
|
||||
const dir = path.resolve(import.meta.dirname, "..")
|
||||
const binary = "opencode2"
|
||||
const outdir = path.resolve(
|
||||
dir,
|
||||
process.argv.find((arg) => arg.startsWith("--outdir="))?.slice("--outdir=".length) ?? "dist",
|
||||
)
|
||||
if (outdir === dir) throw new Error("--outdir must not be the package directory")
|
||||
process.chdir(dir)
|
||||
|
||||
await rm(outdir, { recursive: true, force: true })
|
||||
await rm("dist", { recursive: true, force: true })
|
||||
|
||||
const singleFlag = process.argv.includes("--single")
|
||||
const baselineFlag = process.argv.includes("--baseline")
|
||||
@@ -54,6 +50,10 @@ const targets = singleFlag
|
||||
|
||||
if (!skipInstall) await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}`
|
||||
|
||||
const localParserWorker = path.resolve(dir, "node_modules/@opentui/core/parser.worker.js")
|
||||
const rootParserWorker = path.resolve(dir, "../../node_modules/@opentui/core/parser.worker.js")
|
||||
const parserWorker = fs.realpathSync(fs.existsSync(localParserWorker) ? localParserWorker : rootParserWorker)
|
||||
|
||||
for (const item of targets) {
|
||||
const target = [
|
||||
binary,
|
||||
@@ -67,7 +67,7 @@ for (const item of targets) {
|
||||
const name = target.replace(binary, "cli")
|
||||
console.log(`building ${name}`)
|
||||
const result = await Bun.build({
|
||||
entrypoints: ["./src/index.ts"],
|
||||
entrypoints: ["./src/index.ts", parserWorker],
|
||||
tsconfig: "./tsconfig.json",
|
||||
plugins: [plugin],
|
||||
external: ["node-gyp"],
|
||||
@@ -81,7 +81,7 @@ for (const item of targets) {
|
||||
autoloadTsconfig: true,
|
||||
autoloadPackageJson: true,
|
||||
target: target.replace(binary, "bun") as Bun.Build.CompileTarget,
|
||||
outfile: path.join(outdir, name, "bin", binary),
|
||||
outfile: `./dist/${name}/bin/${binary}`,
|
||||
execArgv: [`--user-agent=${binary}/${Script.version}`, "--use-system-ca", "--"],
|
||||
windows: {},
|
||||
},
|
||||
@@ -93,6 +93,10 @@ for (const item of targets) {
|
||||
OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "undefined",
|
||||
// FFF_LIBC selects the fff native lib variant: "musl" or "gnu".
|
||||
FFF_LIBC: item.os === "linux" ? `'${item.abi ?? "gnu"}'` : "undefined",
|
||||
OTUI_TREE_SITTER_WORKER_PATH:
|
||||
(item.os === "win32" ? '"B:/~BUN/root/' : '"/$bunfs/root/') +
|
||||
path.relative(dir, parserWorker).replaceAll("\\", "/") +
|
||||
'"',
|
||||
...(item.os === "linux" ? { "process.env.OPENTUI_LIBC": JSON.stringify(item.abi ?? "glibc") } : {}),
|
||||
},
|
||||
})
|
||||
@@ -103,7 +107,7 @@ for (const item of targets) {
|
||||
}
|
||||
|
||||
await Bun.write(
|
||||
path.join(outdir, name, "package.json"),
|
||||
`./dist/${name}/package.json`,
|
||||
JSON.stringify(
|
||||
{
|
||||
name: `@opencode-ai/${name}`,
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { readFile } from "node:fs/promises"
|
||||
|
||||
const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.dev"
|
||||
|
||||
export const modelsData = process.env.MODELS_DEV_API_JSON
|
||||
? await readFile(process.env.MODELS_DEV_API_JSON, "utf8")
|
||||
? await Bun.file(process.env.MODELS_DEV_API_JSON).text()
|
||||
: await fetch(`${modelsUrl}/api.json`).then((response) => response.text())
|
||||
|
||||
console.log("Loaded models.dev snapshot")
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import { createHash } from "node:crypto"
|
||||
import { copyFile, mkdir, readdir, readFile, stat } from "node:fs/promises"
|
||||
import { createRequire } from "node:module"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { getNodeAssets } from "@opentui/core/node-assets"
|
||||
import { attentionSoundAssets, type NodeTarget, photonWasmAsset } from "../src/node/target"
|
||||
|
||||
const dir = path.resolve(import.meta.dirname, "..")
|
||||
|
||||
// Bun's compiler discovers file imports and embeds them in its virtual filesystem. Vite only bundles the JavaScript
|
||||
// portion of the Node executable, while SEA embeds only the assets explicitly listed in its build configuration.
|
||||
// Collect and stage those files under stable keys so the SEA prelude can extract them to real paths at startup;
|
||||
// native addons, helper executables, and other path-based consumers cannot use assets directly from SEA memory.
|
||||
export type NodeAsset = {
|
||||
readonly key: string
|
||||
readonly source: string
|
||||
}
|
||||
|
||||
async function files(root: string, current = root): Promise<string[]> {
|
||||
return (
|
||||
await Promise.all(
|
||||
(await readdir(current, { withFileTypes: true })).map((entry) => {
|
||||
const target = path.join(current, entry.name)
|
||||
return entry.isDirectory() ? files(root, target) : [path.relative(root, target)]
|
||||
}),
|
||||
)
|
||||
).flat()
|
||||
}
|
||||
|
||||
export async function collectNodeAssets(target: NodeTarget) {
|
||||
const ptyEntry = fileURLToPath(import.meta.resolve(target.nodePtyPackage))
|
||||
const ptyRoot = path.resolve(path.dirname(ptyEntry), "..")
|
||||
const assets: NodeAsset[] = [
|
||||
...getNodeAssets({
|
||||
platform: target.platform,
|
||||
arch: target.arch,
|
||||
...(target.platform === "linux" ? { libc: "glibc" as const } : {}),
|
||||
}),
|
||||
{ key: target.parcelWatcherAsset, source: fileURLToPath(import.meta.resolve(target.parcelWatcherPackage)) },
|
||||
{
|
||||
key: photonWasmAsset,
|
||||
source: createRequire(path.resolve(dir, "../core/package.json")).resolve(photonWasmAsset),
|
||||
},
|
||||
...attentionSoundAssets.map((key) => ({
|
||||
key,
|
||||
source: path.resolve(dir, "../ui/src/assets/audio", path.basename(key)),
|
||||
})),
|
||||
...(await files(ptyRoot))
|
||||
.filter((relative) => !relative.endsWith(".map") && !relative.endsWith(".pdb"))
|
||||
.map((relative) => ({
|
||||
key: `${target.nodePtyPackage}/${relative}`,
|
||||
source: path.join(ptyRoot, relative),
|
||||
})),
|
||||
]
|
||||
await Promise.all(assets.map((asset) => stat(asset.source)))
|
||||
return assets
|
||||
}
|
||||
|
||||
export async function hashNodeAssets(assets: readonly NodeAsset[]) {
|
||||
const hash = createHash("sha256")
|
||||
for (const asset of assets.toSorted((left, right) => left.key.localeCompare(right.key))) {
|
||||
hash.update(asset.key)
|
||||
hash.update(await readFile(asset.source))
|
||||
}
|
||||
return hash.digest("hex").slice(0, 16)
|
||||
}
|
||||
|
||||
export async function copyNodeAssets(assets: readonly NodeAsset[]) {
|
||||
const root = path.join(dir, "dist-node", "assets")
|
||||
await Promise.all(
|
||||
assets.map(async (asset) => {
|
||||
const target = path.join(root, asset.key)
|
||||
await mkdir(path.dirname(target), { recursive: true })
|
||||
await copyFile(asset.source, target)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export async function seaAssetMap() {
|
||||
const root = path.join(dir, "dist-node", "assets")
|
||||
return Object.fromEntries((await files(root)).map((key) => [key.replaceAll(path.sep, "/"), path.join(root, key)]))
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import childProcess from "node:child_process"
|
||||
import fs from "node:fs"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import { createRequire } from "node:module"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
const directory = path.dirname(fileURLToPath(import.meta.url))
|
||||
const require = createRequire(import.meta.url)
|
||||
const packageJson = JSON.parse(fs.readFileSync(path.join(directory, "package.json"), "utf8"))
|
||||
const command = Object.keys(packageJson.bin ?? {})[0]
|
||||
if (!command) throw new Error("OpenCode package does not declare a binary")
|
||||
|
||||
const platform = { darwin: "darwin", linux: "linux", win32: "windows" }[os.platform()] ?? os.platform()
|
||||
const arch = { x64: "x64", arm64: "arm64", arm: "arm" }[os.arch()] ?? os.arch()
|
||||
const sourceBinary = platform === "windows" ? `${command}.exe` : command
|
||||
const targetBinary = path.resolve(directory, packageJson.bin[command])
|
||||
const dependencies = packageJson.optionalDependencies ?? {}
|
||||
const base = Object.keys(dependencies).find((name) => name.endsWith(`-${platform}-${arch}`))
|
||||
if (!base) throw new Error(`OpenCode does not provide a binary for ${platform}-${arch}`)
|
||||
|
||||
function supportsAvx2() {
|
||||
if (arch !== "x64") return false
|
||||
if (platform === "linux") {
|
||||
try {
|
||||
return /(^|\s)avx2(\s|$)/i.test(fs.readFileSync("/proc/cpuinfo", "utf8"))
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (platform === "darwin") {
|
||||
try {
|
||||
const result = childProcess.spawnSync("sysctl", ["-n", "hw.optional.avx2_0"], {
|
||||
encoding: "utf8",
|
||||
timeout: 1500,
|
||||
})
|
||||
return result.status === 0 && (result.stdout || "").trim() === "1"
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (platform === "windows") {
|
||||
const script =
|
||||
'(Add-Type -MemberDefinition "[DllImport(""kernel32.dll"")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);" -Name Kernel32 -Namespace Win32 -PassThru)::IsProcessorFeaturePresent(40)'
|
||||
for (const executable of ["powershell.exe", "pwsh.exe", "pwsh", "powershell"]) {
|
||||
try {
|
||||
const result = childProcess.spawnSync(executable, ["-NoProfile", "-NonInteractive", "-Command", script], {
|
||||
encoding: "utf8",
|
||||
timeout: 3000,
|
||||
windowsHide: true,
|
||||
})
|
||||
if (result.status !== 0) continue
|
||||
const output = (result.stdout || "").trim().toLowerCase()
|
||||
if (output === "true" || output === "1") return true
|
||||
if (output === "false" || output === "0") return false
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function isMusl() {
|
||||
if (platform !== "linux") return false
|
||||
try {
|
||||
if (fs.existsSync("/etc/alpine-release")) return true
|
||||
const result = childProcess.spawnSync("ldd", ["--version"], { encoding: "utf8" })
|
||||
return `${result.stdout || ""}${result.stderr || ""}`.toLowerCase().includes("musl")
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function packageNames() {
|
||||
const baseline = arch === "x64" && !supportsAvx2()
|
||||
const names =
|
||||
platform === "linux"
|
||||
? isMusl()
|
||||
? arch === "x64"
|
||||
? baseline
|
||||
? [`${base}-baseline-musl`, `${base}-musl`, `${base}-baseline`, base]
|
||||
: [`${base}-musl`, `${base}-baseline-musl`, base, `${base}-baseline`]
|
||||
: [`${base}-musl`, base]
|
||||
: arch === "x64"
|
||||
? baseline
|
||||
? [`${base}-baseline`, base, `${base}-baseline-musl`, `${base}-musl`]
|
||||
: [base, `${base}-baseline`, `${base}-musl`, `${base}-baseline-musl`]
|
||||
: [base, `${base}-musl`]
|
||||
: arch === "x64"
|
||||
? baseline
|
||||
? [`${base}-baseline`, base]
|
||||
: [base, `${base}-baseline`]
|
||||
: [base]
|
||||
return names.filter((name) => dependencies[name])
|
||||
}
|
||||
|
||||
function copyBinary(source) {
|
||||
if (!fs.existsSync(source)) throw new Error(`Binary not found at ${source}`)
|
||||
fs.mkdirSync(path.dirname(targetBinary), { recursive: true })
|
||||
if (fs.existsSync(targetBinary)) fs.unlinkSync(targetBinary)
|
||||
try {
|
||||
fs.linkSync(source, targetBinary)
|
||||
} catch {
|
||||
fs.copyFileSync(source, targetBinary)
|
||||
}
|
||||
fs.chmodSync(targetBinary, 0o755)
|
||||
}
|
||||
|
||||
function resolveBinary(name) {
|
||||
const packagePath = require.resolve(`${name}/package.json`)
|
||||
return path.join(path.dirname(packagePath), "bin", sourceBinary)
|
||||
}
|
||||
|
||||
function installPackage(name) {
|
||||
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-install-"))
|
||||
try {
|
||||
const result = childProcess.spawnSync(
|
||||
"npm",
|
||||
["install", "--ignore-scripts", "--no-save", "--loglevel=error", "--prefix", temp, `${name}@${dependencies[name]}`],
|
||||
{ stdio: "inherit", windowsHide: true },
|
||||
)
|
||||
if (result.status !== 0) return false
|
||||
copyBinary(path.join(temp, "node_modules", name, "bin", sourceBinary))
|
||||
return true
|
||||
} finally {
|
||||
fs.rmSync(temp, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
function verifyBinary() {
|
||||
return (
|
||||
childProcess.spawnSync(targetBinary, ["--version"], {
|
||||
stdio: "ignore",
|
||||
windowsHide: true,
|
||||
}).status === 0
|
||||
)
|
||||
}
|
||||
|
||||
function main() {
|
||||
const names = packageNames()
|
||||
for (const name of names) {
|
||||
try {
|
||||
copyBinary(resolveBinary(name))
|
||||
if (verifyBinary()) return
|
||||
} catch {
|
||||
if (installPackage(name) && verifyBinary()) return
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Failed to install OpenCode. Try manually installing ${names.map((name) => JSON.stringify(name)).join(" or ")}.`)
|
||||
}
|
||||
|
||||
try {
|
||||
main()
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : String(error))
|
||||
process.exit(1)
|
||||
}
|
||||
@@ -18,66 +18,37 @@ async function publish(dir: string, name: string, version: string) {
|
||||
await $`npm publish *.tgz --access public --tag ${Script.channel}`.cwd(dir)
|
||||
}
|
||||
|
||||
async function publishDistribution(input: { root: string; name: string; binary: string; packagePrefix: string }) {
|
||||
const binaries: Record<string, string> = {}
|
||||
for (const filepath of new Bun.Glob("*/package.json").scanSync({ cwd: input.root })) {
|
||||
const item = await Bun.file(`${input.root}/${filepath}`).json()
|
||||
if (!item.name.startsWith(input.packagePrefix)) continue
|
||||
binaries[item.name] = item.version
|
||||
}
|
||||
console.log(input.name, "binaries", binaries)
|
||||
const versions = new Set(Object.values(binaries))
|
||||
if (versions.size > 1) throw new Error(`Binary package versions do not match for ${input.name}`)
|
||||
const version = versions.values().next().value
|
||||
if (!version) throw new Error(`No binary packages found for ${input.name}`)
|
||||
|
||||
await $`mkdir -p ${input.root}/${input.name}/bin`
|
||||
await $`cp ./script/postinstall.mjs ${input.root}/${input.name}/postinstall.mjs`
|
||||
await Bun.file(`${input.root}/${input.name}/bin/${input.binary}.exe`).write(
|
||||
[
|
||||
`echo "Error: ${input.name}'s postinstall script was not run." >&2`,
|
||||
'echo "" >&2',
|
||||
'echo "This occurs when installation scripts are disabled." >&2',
|
||||
'echo "Run the package postinstall script or reinstall with scripts enabled." >&2',
|
||||
"exit 1",
|
||||
"",
|
||||
].join("\n"),
|
||||
)
|
||||
await Bun.file(`${input.root}/${input.name}/package.json`).write(
|
||||
JSON.stringify(
|
||||
{
|
||||
name: input.name,
|
||||
bin: { [input.binary]: `./bin/${input.binary}.exe` },
|
||||
scripts: { postinstall: "node ./postinstall.mjs" },
|
||||
version,
|
||||
license: pkg.license,
|
||||
repository: { type: "git", url: "git+https://github.com/anomalyco/opencode.git" },
|
||||
os: ["darwin", "linux", "win32"],
|
||||
cpu: ["arm64", "x64"],
|
||||
optionalDependencies: binaries,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(binaries).map(([name, version]) =>
|
||||
publish(`${input.root}/${name.replace("@opencode-ai/", "")}`, name, version),
|
||||
),
|
||||
)
|
||||
await publish(`${input.root}/${input.name}`, input.name, version)
|
||||
const binaries: Record<string, string> = {}
|
||||
for (const filepath of new Bun.Glob("*/package.json").scanSync({ cwd: "./dist" })) {
|
||||
const item = await Bun.file(`./dist/${filepath}`).json()
|
||||
binaries[item.name] = item.version
|
||||
}
|
||||
console.log("binaries", binaries)
|
||||
const version = Object.values(binaries)[0]
|
||||
const name = pkg.name
|
||||
|
||||
await publishDistribution({
|
||||
root: "./dist",
|
||||
name: pkg.name,
|
||||
binary: "opencode2",
|
||||
packagePrefix: "@opencode-ai/cli-",
|
||||
})
|
||||
await publishDistribution({
|
||||
root: "./dist/node",
|
||||
name: "opencode-node",
|
||||
binary: "opencode2-node",
|
||||
packagePrefix: "@opencode-ai/cli-node-",
|
||||
})
|
||||
await $`mkdir -p ./dist/${name}/bin`
|
||||
await $`cp ./bin/opencode2.cjs ./dist/${name}/bin/opencode2`
|
||||
await Bun.file(`./dist/${name}/package.json`).write(
|
||||
JSON.stringify(
|
||||
{
|
||||
name,
|
||||
bin: { opencode2: "./bin/opencode2" },
|
||||
version,
|
||||
license: pkg.license,
|
||||
repository: { type: "git", url: "git+https://github.com/anomalyco/opencode.git" },
|
||||
os: ["darwin", "linux", "win32"],
|
||||
cpu: ["arm64", "x64"],
|
||||
optionalDependencies: binaries,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(binaries).map(([name, version]) =>
|
||||
publish(`./dist/${name.replace("@opencode-ai/", "")}`, name, version),
|
||||
),
|
||||
)
|
||||
await publish(`./dist/${name}`, name, version)
|
||||
|
||||
@@ -7,10 +7,9 @@ import fs from "node:fs/promises"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
|
||||
const nodeBuild = process.argv.includes("--node")
|
||||
const target = `cli${nodeBuild ? "-node" : ""}-${process.platform === "win32" ? "windows" : process.platform}-${process.arch}`
|
||||
const directory = path.join(import.meta.dir, "..", "dist", ...(nodeBuild ? ["node"] : []), target, "bin")
|
||||
const binary = path.join(directory, `opencode2${nodeBuild ? "-node" : ""}${process.platform === "win32" ? ".exe" : ""}`)
|
||||
const target = `cli-${process.platform === "win32" ? "windows" : process.platform}-${process.arch}`
|
||||
const directory = path.join(import.meta.dir, "..", "dist", target, "bin")
|
||||
const binary = path.join(directory, `opencode2${process.platform === "win32" ? ".exe" : ""}`)
|
||||
if (!(await Bun.file(binary).exists())) throw new Error(`Missing compiled CLI in ${directory}`)
|
||||
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-smoke-"))
|
||||
|
||||
@@ -114,10 +114,6 @@ export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCO
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Spec.make("plugin", {
|
||||
description: "Manage plugins",
|
||||
commands: [Spec.make("list", { description: "List active plugins" })],
|
||||
}),
|
||||
Spec.make("migrate", { description: "Migrate v1 data to v2" }),
|
||||
Spec.make("mini", {
|
||||
description: "Start the minimal interactive interface",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { run } from "@opencode-ai/tui"
|
||||
import { Commands } from "../commands"
|
||||
@@ -75,6 +75,6 @@ export default Runtime.handler(Commands, (input) =>
|
||||
: Effect.logInfo(message, tags)
|
||||
runFork(effect)
|
||||
},
|
||||
}).pipe(Effect.provide(LayerNode.compile(Global.node)))
|
||||
}).pipe(Effect.provide(AppNodeBuilder.build(Global.node)))
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { EOL } from "node:os"
|
||||
import path from "node:path"
|
||||
import { readFile, stat, writeFile } from "node:fs/promises"
|
||||
import { Effect, Option } from "effect"
|
||||
import { applyEdits, modify } from "jsonc-parser"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
@@ -36,7 +35,7 @@ export default Runtime.handler(
|
||||
}),
|
||||
)
|
||||
|
||||
export async function resolveConfigPath(directory: string) {
|
||||
async function resolveConfigPath(directory: string) {
|
||||
const candidates = [
|
||||
path.join(directory, "opencode.json"),
|
||||
path.join(directory, "opencode.jsonc"),
|
||||
@@ -44,24 +43,16 @@ export async function resolveConfigPath(directory: string) {
|
||||
path.join(directory, ".opencode", "opencode.jsonc"),
|
||||
]
|
||||
for (const candidate of candidates) {
|
||||
if (
|
||||
await stat(candidate).then(
|
||||
(info) => info.isFile(),
|
||||
() => false,
|
||||
)
|
||||
)
|
||||
return candidate
|
||||
if (await Bun.file(candidate).exists()) return candidate
|
||||
}
|
||||
return candidates[0]
|
||||
}
|
||||
|
||||
async function write(configPath: string, name: string, server: unknown) {
|
||||
const text = await readFile(configPath, "utf8").catch((error) => {
|
||||
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return "{}"
|
||||
throw error
|
||||
})
|
||||
const file = Bun.file(configPath)
|
||||
const text = (await file.exists()) ? await file.text() : "{}"
|
||||
const edits = modify(text, ["mcp", "servers", name], server, {
|
||||
formattingOptions: { tabSize: 2, insertSpaces: true },
|
||||
})
|
||||
await writeFile(configPath, applyEdits(text, edits))
|
||||
await Bun.write(configPath, applyEdits(text, edits))
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { EOL } from "node:os"
|
||||
import { Effect } from "effect"
|
||||
import { OpenCode } from "@opencode-ai/client"
|
||||
import { Service } from "@opencode-ai/client/effect/service"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { ServiceConfig } from "../../../services/service-config"
|
||||
|
||||
export default Runtime.handler(
|
||||
Commands.commands.plugin.commands.list,
|
||||
Effect.fn("cli.plugin.list")(function* () {
|
||||
const options = yield* ServiceConfig.options()
|
||||
const found = yield* Service.discover(options)
|
||||
const endpoint = found ?? (yield* Service.ensure(options))
|
||||
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
|
||||
const response = yield* Effect.promise(() => client.plugin.list({ location: { directory: process.cwd() } }))
|
||||
const plugins = response.data.toSorted((a, b) => a.id.localeCompare(b.id))
|
||||
if (plugins.length === 0) {
|
||||
process.stdout.write("No plugins loaded" + EOL)
|
||||
return
|
||||
}
|
||||
process.stdout.write(plugins.map((plugin) => plugin.id).join(EOL) + EOL)
|
||||
}),
|
||||
)
|
||||
@@ -5,7 +5,7 @@ import { ServerProcess } from "../../server-process"
|
||||
|
||||
export default Runtime.handler(
|
||||
Commands.commands.serve,
|
||||
Effect.fnUntraced(function* (input) {
|
||||
Effect.fn("cli.serve")(function* (input) {
|
||||
if (input.service && input.stdio) return yield* Effect.fail(new Error("--service and --stdio cannot be combined"))
|
||||
return yield* ServerProcess.run({
|
||||
mode: input.service ? "service" : input.stdio ? "stdio" : "default",
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Runtime } from "./framework/runtime"
|
||||
import { Observability } from "@opencode-ai/core/observability"
|
||||
import { Updater } from "./services/updater"
|
||||
import { InstallationChannel, InstallationVersion, InstallationLocal } from "@opencode-ai/core/installation/version"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { AppProcess } from "@opencode-ai/core/process"
|
||||
@@ -31,9 +32,6 @@ const Handlers = Runtime.handlers(Commands, {
|
||||
auth: () => import("./commands/handlers/mcp/auth"),
|
||||
logout: () => import("./commands/handlers/mcp/logout"),
|
||||
},
|
||||
plugin: {
|
||||
list: () => import("./commands/handlers/plugin/list"),
|
||||
},
|
||||
migrate: () => import("./commands/handlers/migrate"),
|
||||
mini: () => import("./commands/handlers/mini"),
|
||||
run: () => import("./commands/handlers/run"),
|
||||
@@ -60,7 +58,7 @@ Effect.logInfo("cli starting", {
|
||||
Effect.annotateLogs({ role: "cli" }),
|
||||
Effect.provide(Config.layer),
|
||||
Effect.provide(Updater.layer),
|
||||
Effect.provide(LayerNode.compile(LayerNode.group([Global.node, AppProcess.node, Npm.node]))),
|
||||
Effect.provide(AppNodeBuilder.build(LayerNode.group([Global.node, AppProcess.node, Npm.node]))),
|
||||
Effect.provide(Observability.layer),
|
||||
Effect.provide(NodeServices.layer),
|
||||
Effect.scoped,
|
||||
|
||||
@@ -633,12 +633,6 @@ export function RunSubagentSelectBody(props: {
|
||||
return
|
||||
}
|
||||
|
||||
if (event.name.toLowerCase() === "up" && menu.selected() === 0) {
|
||||
event.preventDefault()
|
||||
props.onClose()
|
||||
return
|
||||
}
|
||||
|
||||
handleKey({ event, menu, field: () => field, setQuery, select, close: props.onClose })
|
||||
})
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useTerminalDimensions } from "@opentui/solid"
|
||||
import { createEffect, createMemo, createSignal, type Accessor } from "solid-js"
|
||||
import { transparent, type RunFooterTheme } from "./theme"
|
||||
import { Locale } from "@opencode-ai/tui/util/locale"
|
||||
import { stringWidth } from "@opencode-ai/tui/util/string-width"
|
||||
|
||||
export const FOOTER_MENU_ROWS = 8
|
||||
|
||||
@@ -197,7 +196,7 @@ export function RunFooterMenu(props: {
|
||||
...props
|
||||
.items()
|
||||
.filter((item) => item.description)
|
||||
.map((item) => stringWidth(item.display)),
|
||||
.map((item) => Bun.stringWidth(item.display)),
|
||||
)
|
||||
return width === 0 ? 0 : width + 2
|
||||
})
|
||||
@@ -206,14 +205,14 @@ export function RunFooterMenu(props: {
|
||||
return ""
|
||||
}
|
||||
|
||||
return " ".repeat(Math.max(1, descriptionColumn() - stringWidth(item.display)))
|
||||
return " ".repeat(Math.max(1, descriptionColumn() - Bun.stringWidth(item.display)))
|
||||
}
|
||||
const descriptionText = (item: RunFooterMenuItem) => {
|
||||
if (!item.description) {
|
||||
return
|
||||
}
|
||||
|
||||
const footerWidth = item.footer ? stringWidth(item.footer) + 1 : 0
|
||||
const footerWidth = item.footer ? Bun.stringWidth(item.footer) + 1 : 0
|
||||
const available =
|
||||
term().width -
|
||||
(border() ? 1 : 0) -
|
||||
|
||||
@@ -5,15 +5,14 @@
|
||||
// It produces a PromptState that RunPromptBody renders as a slim single-line
|
||||
// composer while the footer view renders any active menus below it.
|
||||
/** @jsxImportSource @opentui/solid */
|
||||
import { pathToFileURL } from "bun"
|
||||
import { StyledText, fg, type ColorInput, type KeyEvent, type TextareaRenderable } from "@opentui/core"
|
||||
import { useRenderer } from "@opentui/solid"
|
||||
import { normalizePromptContent } from "@opencode-ai/tui/prompt/content"
|
||||
import fuzzysort from "fuzzysort"
|
||||
import path from "path"
|
||||
import { pathToFileURL } from "node:url"
|
||||
import { createEffect, createMemo, createResource, createSignal, onCleanup, onMount, type Accessor } from "solid-js"
|
||||
import { Locale } from "@opencode-ai/tui/util/locale"
|
||||
import { stringWidth } from "@opencode-ai/tui/util/string-width"
|
||||
import {
|
||||
createPromptHistory,
|
||||
displayCharAt,
|
||||
@@ -24,7 +23,7 @@ import {
|
||||
movePromptHistory,
|
||||
pushPromptHistory,
|
||||
} from "./prompt.shared"
|
||||
import { Keymap } from "@opencode-ai/tui/context/keymap"
|
||||
import { OPENCODE_BASE_MODE, useBindings } from "@opencode-ai/tui/keymap"
|
||||
import { realignEditorPromptParts, resolveEditorSlashValue } from "./prompt.editor"
|
||||
import { FOOTER_MENU_ROWS, createFooterMenuState, type RunFooterMenuItem } from "./footer.menu"
|
||||
import type { RunFooterTheme } from "./theme"
|
||||
@@ -603,7 +602,7 @@ export function createPromptState(input: PromptInput): PromptState {
|
||||
})
|
||||
}
|
||||
|
||||
const restore = (value: RunPrompt, cursor = stringWidth(value.text)) => {
|
||||
const restore = (value: RunPrompt, cursor = Bun.stringWidth(value.text)) => {
|
||||
draft = clonePrompt(value)
|
||||
setShell(value.mode === "shell")
|
||||
if (!area || area.isDestroyed) {
|
||||
@@ -613,7 +612,7 @@ export function createPromptState(input: PromptInput): PromptState {
|
||||
hide()
|
||||
area.setText(value.text)
|
||||
restoreParts(value.parts)
|
||||
area.cursorOffset = Math.min(cursor, stringWidth(area.plainText))
|
||||
area.cursorOffset = Math.min(cursor, Bun.stringWidth(area.plainText))
|
||||
scheduleRows()
|
||||
area.focus()
|
||||
}
|
||||
@@ -644,7 +643,7 @@ export function createPromptState(input: PromptInput): PromptState {
|
||||
area.setText(text)
|
||||
clearParts()
|
||||
draft = shell() ? { text: area.plainText, parts: [], mode: "shell" } : { text: area.plainText, parts: [] }
|
||||
area.cursorOffset = Math.min(stringWidth(text), stringWidth(area.plainText))
|
||||
area.cursorOffset = Math.min(Bun.stringWidth(text), Bun.stringWidth(area.plainText))
|
||||
scheduleRows()
|
||||
area.focus()
|
||||
}
|
||||
@@ -778,7 +777,7 @@ export function createPromptState(input: PromptInput): PromptState {
|
||||
if (move(dir, event)) return
|
||||
if (!area || area.isDestroyed) return false
|
||||
|
||||
const endOffset = stringWidth(area.plainText)
|
||||
const endOffset = Bun.stringWidth(area.plainText)
|
||||
if (dir === -1) {
|
||||
if (area.cursorOffset === 0) return false
|
||||
if (area.visualCursor.visualRow === 0) {
|
||||
@@ -887,12 +886,16 @@ export function createPromptState(input: PromptInput): PromptState {
|
||||
area.cursorOffset = 0
|
||||
const start = area.logicalCursor
|
||||
area.cursorOffset =
|
||||
shell() || !head ? cursor : local ? stringWidth(area.plainText) : stringWidth(area.plainText.slice(0, head.end))
|
||||
shell() || !head
|
||||
? cursor
|
||||
: local
|
||||
? Bun.stringWidth(area.plainText)
|
||||
: Bun.stringWidth(area.plainText.slice(0, head.end))
|
||||
const end = area.logicalCursor
|
||||
|
||||
area.deleteRange(start.row, start.col, end.row, end.col)
|
||||
area.insertText(text)
|
||||
area.cursorOffset = stringWidth(text)
|
||||
area.cursorOffset = Bun.stringWidth(text)
|
||||
hide()
|
||||
syncDraft()
|
||||
if (!shell()) {
|
||||
@@ -917,7 +920,7 @@ export function createPromptState(input: PromptInput): PromptState {
|
||||
|
||||
const text = "@" + next.value
|
||||
const startOffset = at()
|
||||
const endOffset = startOffset + stringWidth(text)
|
||||
const endOffset = startOffset + Bun.stringWidth(text)
|
||||
const part = structuredClone(next.part)
|
||||
if (part.type === "agent") {
|
||||
part.source = {
|
||||
@@ -990,83 +993,93 @@ export function createPromptState(input: PromptInput): PromptState {
|
||||
return true
|
||||
}
|
||||
|
||||
Keymap.createLayer(() => ({
|
||||
useBindings(() => ({
|
||||
mode: OPENCODE_BASE_MODE,
|
||||
enabled: baseBindingsEnabled(),
|
||||
commands: [
|
||||
{
|
||||
id: "prompt.clear",
|
||||
name: "prompt.clear",
|
||||
title: "Clear prompt or exit",
|
||||
group: "Prompt",
|
||||
category: "Prompt",
|
||||
run() {
|
||||
if (requestExit()) return
|
||||
return false
|
||||
},
|
||||
},
|
||||
],
|
||||
bindings: input.tuiConfig.keybinds.get("prompt.clear"),
|
||||
}))
|
||||
|
||||
Keymap.createLayer(() => ({
|
||||
useBindings(() => ({
|
||||
mode: OPENCODE_BASE_MODE,
|
||||
enabled: input.prompt(),
|
||||
commands: [
|
||||
{
|
||||
id: "session.interrupt",
|
||||
name: "session.interrupt",
|
||||
title: "Interrupt session",
|
||||
group: "Session",
|
||||
category: "Session",
|
||||
run() {
|
||||
if (input.onInterrupt()) return
|
||||
return false
|
||||
},
|
||||
},
|
||||
],
|
||||
bindings: input.tuiConfig.keybinds.get("session.interrupt"),
|
||||
}))
|
||||
|
||||
Keymap.createLayer(() => ({
|
||||
useBindings(() => ({
|
||||
mode: OPENCODE_BASE_MODE,
|
||||
enabled: input.prompt() && !visible(),
|
||||
commands: [
|
||||
{
|
||||
id: "prompt.editor",
|
||||
name: "prompt.editor",
|
||||
title: "Open editor",
|
||||
group: "Prompt",
|
||||
category: "Prompt",
|
||||
run() {
|
||||
void openEditor()
|
||||
},
|
||||
},
|
||||
],
|
||||
bindings: input.tuiConfig.keybinds.get("prompt.editor"),
|
||||
}))
|
||||
|
||||
Keymap.createLayer(() => ({
|
||||
useBindings(() => ({
|
||||
priority: 1,
|
||||
mode: OPENCODE_BASE_MODE,
|
||||
enabled: input.prompt() && !visible(),
|
||||
commands: [
|
||||
{
|
||||
id: "prompt.history.previous",
|
||||
name: "prompt.history.previous",
|
||||
title: "Previous prompt history",
|
||||
group: "Prompt",
|
||||
run(_input: string | undefined, event?: KeyEvent) {
|
||||
if (!event) return false
|
||||
return historyCommand(-1, event)
|
||||
category: "Prompt",
|
||||
run(ctx: { event: KeyEvent }) {
|
||||
return historyCommand(-1, ctx.event)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "prompt.history.next",
|
||||
name: "prompt.history.next",
|
||||
title: "Next prompt history",
|
||||
group: "Prompt",
|
||||
run(_input: string | undefined, event?: KeyEvent) {
|
||||
if (!event) return false
|
||||
return historyCommand(1, event)
|
||||
category: "Prompt",
|
||||
run(ctx: { event: KeyEvent }) {
|
||||
return historyCommand(1, ctx.event)
|
||||
},
|
||||
},
|
||||
],
|
||||
bindings: [
|
||||
...input.tuiConfig.keybinds.get("prompt.history.previous"),
|
||||
...input.tuiConfig.keybinds.get("prompt.history.next"),
|
||||
],
|
||||
}))
|
||||
|
||||
Keymap.createLayer(() => ({
|
||||
useBindings(() => ({
|
||||
mode: OPENCODE_BASE_MODE,
|
||||
enabled: input.prompt() && !visible(),
|
||||
commands: [
|
||||
bindings: [
|
||||
{
|
||||
bind: "!",
|
||||
title: "Shell mode",
|
||||
key: "!",
|
||||
desc: "Shell mode",
|
||||
group: "Prompt",
|
||||
run() {
|
||||
cmd() {
|
||||
if (shell()) return false
|
||||
if (!area || area.isDestroyed) return false
|
||||
if (area.cursorOffset !== 0) return false
|
||||
@@ -1076,20 +1089,21 @@ export function createPromptState(input: PromptInput): PromptState {
|
||||
],
|
||||
}))
|
||||
|
||||
Keymap.createLayer(() => ({
|
||||
useBindings(() => ({
|
||||
mode: OPENCODE_BASE_MODE,
|
||||
enabled: input.prompt() && shell() && !visible(),
|
||||
commands: [
|
||||
bindings: [
|
||||
{
|
||||
bind: "escape",
|
||||
title: "Exit shell mode",
|
||||
key: "escape",
|
||||
desc: "Exit shell mode",
|
||||
group: "Prompt",
|
||||
run: () => setShellMode(false),
|
||||
cmd: () => setShellMode(false),
|
||||
},
|
||||
{
|
||||
bind: "backspace",
|
||||
title: "Exit shell mode",
|
||||
key: "backspace",
|
||||
desc: "Exit shell mode",
|
||||
group: "Prompt",
|
||||
run() {
|
||||
cmd() {
|
||||
if (!area || area.isDestroyed) return false
|
||||
if (area.cursorOffset !== 0) return false
|
||||
setShellMode(false)
|
||||
@@ -1098,31 +1112,32 @@ export function createPromptState(input: PromptInput): PromptState {
|
||||
],
|
||||
}))
|
||||
|
||||
Keymap.createLayer(() => ({
|
||||
useBindings(() => ({
|
||||
mode: OPENCODE_BASE_MODE,
|
||||
enabled: input.prompt() && visible(),
|
||||
commands: [
|
||||
{
|
||||
id: "prompt.autocomplete.prev",
|
||||
name: "prompt.autocomplete.prev",
|
||||
title: "Previous autocomplete item",
|
||||
group: "Autocomplete",
|
||||
category: "Autocomplete",
|
||||
run: () => menu.move(-1),
|
||||
},
|
||||
{
|
||||
id: "prompt.autocomplete.next",
|
||||
name: "prompt.autocomplete.next",
|
||||
title: "Next autocomplete item",
|
||||
group: "Autocomplete",
|
||||
category: "Autocomplete",
|
||||
run: () => menu.move(1),
|
||||
},
|
||||
{
|
||||
id: "prompt.autocomplete.hide",
|
||||
name: "prompt.autocomplete.hide",
|
||||
title: "Hide autocomplete",
|
||||
group: "Autocomplete",
|
||||
category: "Autocomplete",
|
||||
run: cancelAutocomplete,
|
||||
},
|
||||
{
|
||||
id: "prompt.autocomplete.select",
|
||||
name: "prompt.autocomplete.select",
|
||||
title: "Select autocomplete item",
|
||||
group: "Autocomplete",
|
||||
category: "Autocomplete",
|
||||
run() {
|
||||
if (mode() === "slash" && options().length === 0) {
|
||||
hide()
|
||||
@@ -1132,9 +1147,9 @@ export function createPromptState(input: PromptInput): PromptState {
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "prompt.autocomplete.complete",
|
||||
name: "prompt.autocomplete.complete",
|
||||
title: "Complete autocomplete item",
|
||||
group: "Autocomplete",
|
||||
category: "Autocomplete",
|
||||
run() {
|
||||
if (mode() === "slash" && options().length === 0) {
|
||||
hide()
|
||||
@@ -1149,6 +1164,13 @@ export function createPromptState(input: PromptInput): PromptState {
|
||||
},
|
||||
},
|
||||
],
|
||||
bindings: [
|
||||
"prompt.autocomplete.prev",
|
||||
"prompt.autocomplete.next",
|
||||
"prompt.autocomplete.hide",
|
||||
"prompt.autocomplete.select",
|
||||
"prompt.autocomplete.complete",
|
||||
].flatMap((command) => input.tuiConfig.keybinds.get(command)),
|
||||
}))
|
||||
|
||||
const onKeyDown = (event: KeyEvent) => {
|
||||
|
||||
@@ -24,11 +24,12 @@
|
||||
// Ctrl-c clears a live prompt draft first; otherwise interrupt and exit use a
|
||||
// two-press pattern where the first press shows a hint and the second press
|
||||
// within 5 seconds actually fires the action.
|
||||
import { CliRenderEvents, type CliRenderer, type TreeSitterClient } from "@opentui/core"
|
||||
import { CliRenderEvents, type CliRenderer, type KeyEvent, type Renderable, type TreeSitterClient } from "@opentui/core"
|
||||
import type { Keymap } from "@opentui/keymap"
|
||||
import { render } from "@opentui/solid"
|
||||
import { createComponent, createSignal, type Accessor, type Setter } from "solid-js"
|
||||
import { createStore, reconcile } from "solid-js/store"
|
||||
import { Keymap } from "@opencode-ai/tui/context/keymap"
|
||||
import { OpencodeKeymapProvider } from "@opencode-ai/tui/keymap"
|
||||
import { RUN_COMMAND_PANEL_ROWS, RUN_SUBAGENT_PANEL_ROWS } from "./footer.command"
|
||||
import { SUBAGENT_INSPECTOR_ROWS } from "./footer.subagent"
|
||||
import { PROMPT_MAX_ROWS, TEXTAREA_MIN_ROWS } from "./footer.prompt"
|
||||
@@ -81,6 +82,7 @@ type RunFooterOptions = {
|
||||
first: boolean
|
||||
history?: RunPrompt[]
|
||||
theme: RunTheme
|
||||
keymap: Keymap<Renderable, KeyEvent>
|
||||
tuiConfig: RunTuiConfig
|
||||
diffStyle: RunDiffStyle
|
||||
onPermissionReply: (input: PermissionReply) => void | Promise<void>
|
||||
@@ -303,8 +305,8 @@ export class RunFooter implements FooterApi {
|
||||
const footer = this
|
||||
void render(
|
||||
() =>
|
||||
createComponent(Keymap.Provider, {
|
||||
config: options.tuiConfig,
|
||||
createComponent(OpencodeKeymapProvider, {
|
||||
keymap: options.keymap,
|
||||
get children() {
|
||||
return createComponent(RunFooterView, {
|
||||
directory: options.directory,
|
||||
|
||||
@@ -27,8 +27,14 @@ import { RunPromptBody, createPromptState } from "./footer.prompt"
|
||||
import { RunPermissionBody } from "./footer.permission"
|
||||
import { RunQuestionBody } from "./footer.question"
|
||||
import { footerWidthPolicy } from "./footer.width"
|
||||
import { Keymap } from "@opencode-ai/tui/context/keymap"
|
||||
|
||||
import {
|
||||
OPENCODE_BASE_MODE,
|
||||
formatKeyBindings,
|
||||
formatKeySequence,
|
||||
useBindings,
|
||||
useKeymapSelector,
|
||||
type OpenTuiKeymap,
|
||||
} from "@opencode-ai/tui/keymap"
|
||||
import type {
|
||||
FooterPromptRoute,
|
||||
FooterQueuedPrompt,
|
||||
@@ -171,15 +177,75 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||
const current = route()
|
||||
return current.type === "subagent" ? subagent().details[current.sessionID] : undefined
|
||||
})
|
||||
const shortcuts = Keymap.useShortcuts()
|
||||
const command = () => shortcuts.get("command.palette.show") ?? ""
|
||||
const subagentShortcut = () => shortcuts.get("session.child.first") ?? ""
|
||||
const queuedShortcut = () => shortcuts.get("session.queued_prompts") ?? ""
|
||||
const backgroundShortcut = () => shortcuts.get("session.background") ?? ""
|
||||
const subagentInterruptShortcut = () => shortcuts.get("subagent.interrupt") ?? ""
|
||||
const interrupt = () => shortcuts.get("session.interrupt") ?? ""
|
||||
const variantCycle = () => shortcuts.all("variant.cycle") ?? ""
|
||||
const clearShortcut = () => shortcuts.get("prompt.clear") ?? ""
|
||||
const command = useKeymapSelector(
|
||||
(keymap: OpenTuiKeymap) =>
|
||||
formatKeySequence(
|
||||
keymap
|
||||
.getCommandBindings({ visibility: "registered", commands: ["command.palette.show"] })
|
||||
.get("command.palette.show")?.[0]?.sequence,
|
||||
props.tuiConfig,
|
||||
) ?? "",
|
||||
)
|
||||
const subagentShortcut = useKeymapSelector(
|
||||
(keymap: OpenTuiKeymap) =>
|
||||
formatKeySequence(
|
||||
keymap
|
||||
.getCommandBindings({ visibility: "registered", commands: ["session.child.first"] })
|
||||
.get("session.child.first")?.[0]?.sequence,
|
||||
props.tuiConfig,
|
||||
) ?? "",
|
||||
)
|
||||
const queuedShortcut = useKeymapSelector(
|
||||
(keymap: OpenTuiKeymap) =>
|
||||
formatKeySequence(
|
||||
keymap
|
||||
.getCommandBindings({ visibility: "registered", commands: ["session.queued_prompts"] })
|
||||
.get("session.queued_prompts")?.[0]?.sequence,
|
||||
props.tuiConfig,
|
||||
) ?? "",
|
||||
)
|
||||
const backgroundShortcut = useKeymapSelector(
|
||||
(keymap: OpenTuiKeymap) =>
|
||||
formatKeySequence(
|
||||
keymap
|
||||
.getCommandBindings({ visibility: "registered", commands: ["session.background"] })
|
||||
.get("session.background")?.[0]?.sequence,
|
||||
props.tuiConfig,
|
||||
) ?? "",
|
||||
)
|
||||
const subagentInterruptShortcut = useKeymapSelector(
|
||||
(keymap: OpenTuiKeymap) =>
|
||||
formatKeySequence(
|
||||
keymap
|
||||
.getCommandBindings({ visibility: "registered", commands: ["subagent.interrupt"] })
|
||||
.get("subagent.interrupt")?.[0]?.sequence,
|
||||
props.tuiConfig,
|
||||
) ?? "",
|
||||
)
|
||||
const interrupt = useKeymapSelector(
|
||||
(keymap: OpenTuiKeymap) =>
|
||||
formatKeySequence(
|
||||
keymap
|
||||
.getCommandBindings({ visibility: "registered", commands: ["session.interrupt"] })
|
||||
.get("session.interrupt")?.[0]?.sequence,
|
||||
props.tuiConfig,
|
||||
) ?? "",
|
||||
)
|
||||
const variantCycle = useKeymapSelector(
|
||||
(keymap: OpenTuiKeymap) =>
|
||||
formatKeyBindings(
|
||||
keymap.getCommandBindings({ visibility: "registered", commands: ["variant.cycle"] }).get("variant.cycle"),
|
||||
props.tuiConfig,
|
||||
) ?? "",
|
||||
)
|
||||
const clearShortcut = useKeymapSelector(
|
||||
(keymap: OpenTuiKeymap) =>
|
||||
formatKeySequence(
|
||||
keymap.getCommandBindings({ visibility: "registered", commands: ["prompt.clear"] }).get("prompt.clear")?.[0]
|
||||
?.sequence,
|
||||
props.tuiConfig,
|
||||
) ?? "",
|
||||
)
|
||||
const busy = createMemo(() => props.state().phase === "running")
|
||||
const armed = createMemo(() => props.state().interrupt > 0)
|
||||
const exiting = createMemo(() => props.state().exit > 0)
|
||||
@@ -438,62 +504,74 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||
props.onRequestExit?.(undefined)
|
||||
})
|
||||
|
||||
Keymap.createLayer(() => ({
|
||||
useBindings(() => ({
|
||||
mode: OPENCODE_BASE_MODE,
|
||||
enabled: active().type === "prompt" && route().type === "composer" && !composer.visible(),
|
||||
commands: [
|
||||
{
|
||||
id: "command.palette.show",
|
||||
name: "command.palette.show",
|
||||
title: "Open command palette",
|
||||
group: "Prompt",
|
||||
category: "Prompt",
|
||||
run: openCommand,
|
||||
},
|
||||
{
|
||||
id: "variant.cycle",
|
||||
name: "variant.cycle",
|
||||
title: "Cycle model variant",
|
||||
group: "Model",
|
||||
category: "Model",
|
||||
run: props.onCycle,
|
||||
},
|
||||
],
|
||||
bindings: [
|
||||
...props.tuiConfig.keybinds.get("command.palette.show"),
|
||||
...props.tuiConfig.keybinds.get("variant.cycle"),
|
||||
],
|
||||
}))
|
||||
|
||||
Keymap.createLayer(() => ({
|
||||
useBindings(() => ({
|
||||
mode: OPENCODE_BASE_MODE,
|
||||
enabled: active().type === "prompt" && route().type === "composer" && foregroundSubagents() && !!props.onBackground,
|
||||
priority: 1,
|
||||
commands: [
|
||||
{
|
||||
id: "session.background",
|
||||
name: "session.background",
|
||||
title: "Background subagents",
|
||||
group: "Session",
|
||||
category: "Session",
|
||||
run: () => props.onBackground?.(),
|
||||
},
|
||||
],
|
||||
bindings: props.tuiConfig.keybinds.get("session.background"),
|
||||
}))
|
||||
|
||||
Keymap.createLayer(() => ({
|
||||
useBindings(() => ({
|
||||
mode: OPENCODE_BASE_MODE,
|
||||
enabled: active().type === "prompt" && route().type === "composer" && tabs().length > 0,
|
||||
commands: [
|
||||
{
|
||||
id: "session.child.first",
|
||||
name: "session.child.first",
|
||||
title: "View subagents",
|
||||
group: "Session",
|
||||
category: "Session",
|
||||
run: openSubagentMenu,
|
||||
},
|
||||
],
|
||||
bindings: props.tuiConfig.keybinds.get("session.child.first"),
|
||||
}))
|
||||
|
||||
Keymap.createLayer(() => ({
|
||||
useBindings(() => ({
|
||||
mode: OPENCODE_BASE_MODE,
|
||||
enabled: active().type === "prompt" && route().type === "composer" && queuedPrompts().length > 0,
|
||||
commands: [
|
||||
{
|
||||
id: "session.queued_prompts",
|
||||
name: "session.queued_prompts",
|
||||
title: "Manage queued prompts",
|
||||
group: "Session",
|
||||
category: "Session",
|
||||
run: openQueuedMenu,
|
||||
},
|
||||
],
|
||||
bindings: props.tuiConfig.keybinds.get("session.queued_prompts"),
|
||||
}))
|
||||
|
||||
Keymap.createLayer(() => ({
|
||||
useBindings(() => ({
|
||||
mode: OPENCODE_BASE_MODE,
|
||||
enabled:
|
||||
active().type === "prompt" &&
|
||||
route().type === "subagent" &&
|
||||
@@ -502,10 +580,9 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||
priority: 1,
|
||||
commands: [
|
||||
{
|
||||
id: "subagent.interrupt",
|
||||
name: "subagent.interrupt",
|
||||
title: "Interrupt subagent",
|
||||
group: "Session",
|
||||
bind: "ctrl+d",
|
||||
category: "Session",
|
||||
run: () => {
|
||||
const current = selectedTab()
|
||||
if (current?.status !== "running") {
|
||||
@@ -516,6 +593,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||
},
|
||||
},
|
||||
],
|
||||
bindings: [{ key: "ctrl+d", desc: "Interrupt subagent", group: "Subagents", cmd: "subagent.interrupt" }],
|
||||
}))
|
||||
|
||||
createEffect(() => {
|
||||
|
||||
@@ -4,8 +4,6 @@ import { ServerConnection } from "../services/server-connection"
|
||||
import { waitForCatalogReady } from "./catalog.shared"
|
||||
import { INTERACTIVE_INPUT_ERROR, resolveInteractiveStdin } from "./runtime.stdin"
|
||||
import type { RunInput, RunTuiConfig } from "./types"
|
||||
import { readStdin } from "../util/io"
|
||||
import { setTimeout } from "node:timers/promises"
|
||||
|
||||
export type MiniCommandInput = {
|
||||
server: ServerConnection.Resolved
|
||||
@@ -24,7 +22,7 @@ export type MiniCommandInput = {
|
||||
type Session = Awaited<ReturnType<OpenCodeClient["session"]["get"]>>
|
||||
export async function runMini(input: MiniCommandInput) {
|
||||
validate(input)
|
||||
const initialInput = mergeInput(process.stdin.isTTY ? undefined : await readStdin(), input.prompt)
|
||||
const initialInput = mergeInput(process.stdin.isTTY ? undefined : await Bun.stdin.text(), input.prompt)
|
||||
const runtimeTask = import("./runtime")
|
||||
const directory = localDirectory()
|
||||
|
||||
@@ -125,7 +123,7 @@ async function validateAgent(sdk: OpenCodeClient, directory: string, name?: stri
|
||||
return
|
||||
}
|
||||
if (agent) return name
|
||||
await setTimeout(25)
|
||||
await Bun.sleep(25)
|
||||
}
|
||||
if (!agents) {
|
||||
warning("failed to list agents. Falling back to default agent")
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { EventSubscribeOutput, OpenCodeClient } from "@opencode-ai/client/promise"
|
||||
import { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||
import { EOL } from "node:os"
|
||||
import { readFile } from "node:fs/promises"
|
||||
import { UI } from "./ui"
|
||||
import type { MiniToolPart } from "./types"
|
||||
|
||||
@@ -479,11 +478,11 @@ async function prepareFile(file: File) {
|
||||
if (file.mime !== "text/plain") {
|
||||
const uri = file.url.startsWith("data:")
|
||||
? file.url
|
||||
: `data:${file.mime};base64,${(await readFile(new URL(file.url))).toString("base64")}`
|
||||
: `data:${file.mime};base64,${Buffer.from(await Bun.file(new URL(file.url)).arrayBuffer()).toString("base64")}`
|
||||
return { attachment: { uri, mime: file.mime, name: file.filename } }
|
||||
}
|
||||
const content = file.url.startsWith("data:")
|
||||
? Buffer.from(file.url.slice(file.url.indexOf(",") + 1), "base64").toString("utf8")
|
||||
: await readFile(new URL(file.url), "utf8")
|
||||
: await Bun.file(new URL(file.url)).text()
|
||||
return { text: `<file name="${file.filename}">\n${content}\n</file>` }
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
// the current draft is saved and history begins. Arrowing past the end
|
||||
// restores the draft.
|
||||
export { displayCharAt, displaySlice, mentionTriggerIndex } from "@opencode-ai/tui/prompt/display"
|
||||
import { stringWidth } from "@opencode-ai/tui/util/string-width"
|
||||
import type { RunPrompt } from "./types"
|
||||
|
||||
const HISTORY_LIMIT = 200
|
||||
@@ -103,7 +102,7 @@ export function movePromptHistory(state: PromptHistoryState, dir: -1 | 1, text:
|
||||
return { state, apply: false }
|
||||
}
|
||||
|
||||
if (dir === 1 && cursor !== stringWidth(text)) {
|
||||
if (dir === 1 && cursor !== Bun.stringWidth(text)) {
|
||||
return { state, apply: false }
|
||||
}
|
||||
|
||||
@@ -137,7 +136,7 @@ export function movePromptHistory(state: PromptHistoryState, dir: -1 | 1, text:
|
||||
index: null,
|
||||
},
|
||||
text: state.draft,
|
||||
cursor: stringWidth(state.draft),
|
||||
cursor: Bun.stringWidth(state.draft),
|
||||
apply: true,
|
||||
}
|
||||
}
|
||||
@@ -148,7 +147,7 @@ export function movePromptHistory(state: PromptHistoryState, dir: -1 | 1, text:
|
||||
index: idx,
|
||||
},
|
||||
text: state.items[idx].text,
|
||||
cursor: dir === -1 ? 0 : stringWidth(state.items[idx].text),
|
||||
cursor: dir === -1 ? 0 : Bun.stringWidth(state.items[idx].text),
|
||||
apply: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { Model } from "@opencode-ai/schema/model"
|
||||
import { open } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { readStdin } from "../util/io"
|
||||
import { ServerConnection } from "../services/server-connection"
|
||||
import { loadRunAgents, waitForCatalogReady } from "./catalog.shared"
|
||||
import { runNonInteractivePrompt } from "./noninteractive"
|
||||
@@ -49,7 +48,7 @@ async function run(input: RunCommandInput) {
|
||||
if (input.fork && !input.continue && !input.session) fail("--fork requires --continue or --session")
|
||||
const root = process.env.PWD ?? process.cwd()
|
||||
const directory = localDirectory(root)
|
||||
const message = mergeInput(formatMessage(input.message), process.stdin.isTTY ? undefined : await readStdin())
|
||||
const message = mergeInput(formatMessage(input.message), process.stdin.isTTY ? undefined : await Bun.stdin.text())
|
||||
if (!message?.trim()) fail("You must provide a message")
|
||||
const files = await Promise.all(input.file.map((file) => prepareFile(file, root)))
|
||||
const prepared = { directory, message, files }
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
// none block each other.
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { resolve } from "@opencode-ai/tui/config/v1"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { makeGlobalNode } from "@opencode-ai/core/effect/app-node"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { makeRuntime } from "@opencode-ai/core/effect/runtime"
|
||||
import { loadRunProviders } from "./catalog.shared"
|
||||
import { resolveCurrentSession, sessionHistory } from "./session.shared"
|
||||
@@ -130,7 +130,7 @@ const layer = Layer.effect(
|
||||
)
|
||||
|
||||
const node = makeGlobalNode({ service: Service, layer, deps: [] })
|
||||
const runtime = makeRuntime(Service, LayerNode.compile(node))
|
||||
const runtime = makeRuntime(Service, AppNodeBuilder.build(node))
|
||||
|
||||
// Fetches available variants and context limits for every provider/model pair.
|
||||
export async function resolveModelInfo(
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
// back to the usual two-press exit sequence through RunFooter.requestExit().
|
||||
import path from "path"
|
||||
import { CliRenderEvents, createCliRenderer, type CliRenderer, type ScrollbackWriter } from "@opentui/core"
|
||||
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { registerOpencodeKeymap } from "@opencode-ai/tui/keymap"
|
||||
import { isDefaultTitle } from "@opencode-ai/tui/util/session"
|
||||
import { Locale } from "@opencode-ai/tui/util/locale"
|
||||
import { resolveInteractiveStdin } from "./runtime.stdin"
|
||||
@@ -165,6 +167,8 @@ function queueSplash(
|
||||
export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lifecycle> {
|
||||
const source = resolveInteractiveStdin()
|
||||
const footerTask = import("./footer")
|
||||
let unregisterKeymap: (() => void) | undefined
|
||||
|
||||
try {
|
||||
const renderer = await createCliRenderer({
|
||||
stdin: source.stdin,
|
||||
@@ -183,6 +187,8 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
|
||||
})
|
||||
const [theme, tuiConfig] = await Promise.all([resolveRunTheme(renderer), input.tuiConfig])
|
||||
renderer.setBackgroundColor(theme.background)
|
||||
const keymap = createDefaultOpenTuiKeymap(renderer)
|
||||
unregisterKeymap = registerOpencodeKeymap(keymap, renderer, tuiConfig)
|
||||
const state: SplashState = {
|
||||
entry: false,
|
||||
exit: false,
|
||||
@@ -227,6 +233,7 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
|
||||
history: input.history,
|
||||
theme,
|
||||
wrote,
|
||||
keymap,
|
||||
tuiConfig,
|
||||
diffStyle: tuiConfig.diff_style ?? "auto",
|
||||
onPermissionReply: input.onPermissionReply,
|
||||
@@ -326,6 +333,7 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
|
||||
footer.close()
|
||||
await footer.idle().catch(() => {})
|
||||
footer.destroy()
|
||||
unregisterKeymap?.()
|
||||
shutdown(renderer)
|
||||
if (!wroteExit) {
|
||||
process.stdout.write("\n")
|
||||
@@ -383,6 +391,7 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
|
||||
close,
|
||||
}
|
||||
} catch (error) {
|
||||
unregisterKeymap?.()
|
||||
source.cleanup?.()
|
||||
throw error
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { readFile } from "node:fs/promises"
|
||||
import type {
|
||||
EventSubscribeOutput,
|
||||
OpenCodeClient,
|
||||
@@ -162,7 +161,7 @@ async function prepareFile(file: RunFilePart) {
|
||||
if (file.mime !== "text/plain") return { attachment: { uri: file.url, name: file.filename } }
|
||||
const content = file.url.startsWith("data:")
|
||||
? Buffer.from(file.url.slice(file.url.indexOf(",") + 1), "base64").toString("utf8")
|
||||
: await readFile(new URL(file.url), "utf8")
|
||||
: await Bun.file(new URL(file.url)).text()
|
||||
return { text: `<file name="${file.filename}">\n${content}\n</file>` }
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
// variant and the persisted file.
|
||||
import path from "path"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { makeGlobalNode } from "@opencode-ai/core/effect/app-node"
|
||||
@@ -202,7 +203,7 @@ const node = makeGlobalNode({ service: Service, layer, deps: [FSUtil.node] })
|
||||
|
||||
/** @internal Exported for testing. */
|
||||
export function createVariantRuntime(replacements?: readonly LayerNode.Replacement[]): VariantRuntime {
|
||||
const runtime = makeRuntime(Service, LayerNode.compile(node, replacements))
|
||||
const runtime = makeRuntime(Service, AppNodeBuilder.build(node, replacements))
|
||||
return {
|
||||
resolveSavedVariant: (model) => runtime.runPromise((svc) => svc.resolveSavedVariant(model)).catch(() => undefined),
|
||||
saveVariant: (model, variant) => runtime.runPromise((svc) => svc.saveVariant(model, variant)).catch(() => {}),
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import "./plugin-runtime.promise"
|
||||
import "./plugin-runtime.effect"
|
||||
|
||||
process.stdout.on("error", (error) => {
|
||||
if ("code" in error && error.code === "EPIPE") return
|
||||
throw error
|
||||
})
|
||||
|
||||
await import("../index")
|
||||
@@ -1,28 +0,0 @@
|
||||
import {
|
||||
Agent,
|
||||
Command,
|
||||
Connection,
|
||||
Credential,
|
||||
Integration,
|
||||
Model,
|
||||
Plugin,
|
||||
Provider,
|
||||
Reference,
|
||||
Skill,
|
||||
} from "@opencode-ai/plugin/v2/effect"
|
||||
import { Tool } from "@opencode-ai/plugin/v2/effect/tool"
|
||||
|
||||
const key = Symbol.for("opencode.plugin.v2.effect")
|
||||
;(globalThis as typeof globalThis & { [key]?: unknown })[key] = {
|
||||
Agent,
|
||||
Command,
|
||||
Connection,
|
||||
Credential,
|
||||
Integration,
|
||||
Model,
|
||||
Plugin,
|
||||
Provider,
|
||||
Reference,
|
||||
Skill,
|
||||
Tool,
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import {
|
||||
Agent,
|
||||
Command,
|
||||
Connection,
|
||||
Credential,
|
||||
Integration,
|
||||
Model,
|
||||
Plugin,
|
||||
Provider,
|
||||
Reference,
|
||||
Skill,
|
||||
} from "@opencode-ai/plugin/v2"
|
||||
|
||||
const key = Symbol.for("opencode.plugin.v2.promise")
|
||||
;(globalThis as typeof globalThis & { [key]?: unknown })[key] = {
|
||||
Agent,
|
||||
Command,
|
||||
Connection,
|
||||
Credential,
|
||||
Integration,
|
||||
Model,
|
||||
Plugin,
|
||||
Provider,
|
||||
Reference,
|
||||
Skill,
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
const platforms = ["darwin", "linux", "win32"] as const
|
||||
|
||||
export type NodeTarget = ReturnType<typeof nodeTarget>
|
||||
|
||||
export function nodeTarget(platform: string, arch: string) {
|
||||
if (!platforms.includes(platform as (typeof platforms)[number]) || (arch !== "arm64" && arch !== "x64")) {
|
||||
throw new Error(`Unsupported Node executable target: ${platform}-${arch}`)
|
||||
}
|
||||
|
||||
const targetPlatform = platform as (typeof platforms)[number]
|
||||
const targetArch = arch as "arm64" | "x64"
|
||||
const nodePtyPackage = `@lydell/node-pty-${targetPlatform}-${targetArch}`
|
||||
const parcelWatcherPackage = `@parcel/watcher-${targetPlatform}-${targetArch}${targetPlatform === "linux" ? "-glibc" : ""}`
|
||||
|
||||
return {
|
||||
platform: targetPlatform,
|
||||
arch: targetArch,
|
||||
nodePtyPackage,
|
||||
nodePtyEntryAsset: `${nodePtyPackage}/lib/index.js`,
|
||||
parcelWatcherPackage,
|
||||
parcelWatcherAsset: `${parcelWatcherPackage}/watcher.node`,
|
||||
}
|
||||
}
|
||||
|
||||
export const photonWasmAsset = "@silvia-odwyer/photon-node/photon_rs_bg.wasm"
|
||||
export const nodeExecArgv = ["--experimental-ffi", "--use-system-ca", "--disable-warning=ExperimentalWarning"] as const
|
||||
|
||||
export const attentionSoundAssets = [
|
||||
"@opencode-ai/ui/audio/bip-bop-01.mp3",
|
||||
"@opencode-ai/ui/audio/bip-bop-03.mp3",
|
||||
"@opencode-ai/ui/audio/staplebops-06.mp3",
|
||||
"@opencode-ai/ui/audio/nope-03.mp3",
|
||||
"@opencode-ai/ui/audio/yup-01.mp3",
|
||||
] as const
|
||||
@@ -1,11 +1,13 @@
|
||||
export * as ServerProcess from "./server-process"
|
||||
|
||||
import { NodeServices } from "@effect/platform-node"
|
||||
import { Service, type DiscoverOptions, type Info } from "@opencode-ai/client/effect/service"
|
||||
import { Service } from "@opencode-ai/client/effect/service"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import { AppProcess } from "@opencode-ai/core/process"
|
||||
import { ProcessLock } from "@opencode-ai/core/util/process-lock"
|
||||
import { randomBytes, randomUUID } from "node:crypto"
|
||||
import path from "node:path"
|
||||
import { Effect, FileSystem, Logger, Option, Redacted, Schedule, Schema } from "effect"
|
||||
@@ -22,29 +24,27 @@ export type Options = {
|
||||
readonly port?: number
|
||||
}
|
||||
|
||||
// The process effect lives until server shutdown; tracing it would parent every request to one process-lifetime trace.
|
||||
export const run = Effect.fnUntraced(function* (options: Options) {
|
||||
return yield* processEffect(options).pipe(
|
||||
export const run = Effect.fn("cli.server-process.run")((options: Options) =>
|
||||
processEffect(options).pipe(
|
||||
Effect.provide(Updater.layer),
|
||||
Effect.provide(LayerNode.compile(LayerNode.group([Global.node, AppProcess.node]))),
|
||||
Effect.provide(AppNodeBuilder.build(LayerNode.group([Global.node, AppProcess.node]))),
|
||||
Effect.provide(NodeServices.layer),
|
||||
)
|
||||
})
|
||||
),
|
||||
)
|
||||
|
||||
const processEffect = Effect.fnUntraced(function* (options: Options) {
|
||||
if (options.mode === "service") yield* Effect.sync(() => process.chdir(Global.Path.home))
|
||||
return yield* Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
const serviceOptions = options.mode === "service" ? yield* ServiceConfig.options() : undefined
|
||||
const config = options.mode === "service" ? yield* ServiceConfig.read() : {}
|
||||
const hostname = options.hostname ?? config.hostname ?? "127.0.0.1"
|
||||
const port = options.port ?? config.port ?? (options.mode === "service" ? ServiceConfig.defaultPort() : undefined)
|
||||
if (
|
||||
serviceOptions !== undefined &&
|
||||
port !== undefined &&
|
||||
(yield* Service.incumbent({ ...serviceOptions, url: serviceURL(hostname, port) })) !== undefined
|
||||
)
|
||||
return
|
||||
if (serviceOptions !== undefined) {
|
||||
const acquired = yield* ProcessLock.acquire(serviceOptions.file + ".lock").pipe(
|
||||
Effect.as(true),
|
||||
Effect.catchTag("ProcessLockHeldError", () => Effect.succeed(false)),
|
||||
)
|
||||
if (!acquired) return yield* Effect.void
|
||||
if ((yield* Service.discover(serviceOptions)) !== undefined) return yield* Effect.void
|
||||
}
|
||||
const { start } = yield* Effect.promise(() => import("@opencode-ai/server/process"))
|
||||
const environmentPassword = yield* Env.password
|
||||
// Keep the lease credential out of the environment inherited by tools.
|
||||
@@ -52,6 +52,7 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
|
||||
delete process.env.OPENCODE_PASSWORD
|
||||
delete process.env.OPENCODE_SERVER_PASSWORD
|
||||
}
|
||||
const config = options.mode === "service" ? yield* ServiceConfig.read() : {}
|
||||
const password =
|
||||
options.mode === "service"
|
||||
? yield* ServiceConfig.password()
|
||||
@@ -61,36 +62,15 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
|
||||
if (!password) return yield* Effect.fail(new Error("Missing server password"))
|
||||
const instanceID = randomUUID()
|
||||
const server = yield* start({
|
||||
hostname,
|
||||
port: Option.fromNullishOr(port),
|
||||
hostname: options.hostname ?? config.hostname ?? "127.0.0.1",
|
||||
port: Option.fromNullishOr(options.port ?? config.port),
|
||||
password,
|
||||
instanceID,
|
||||
service:
|
||||
serviceOptions === undefined
|
||||
? undefined
|
||||
: {
|
||||
onListen: (address, shutdown) => register(address, password, instanceID, serviceOptions.file, shutdown),
|
||||
},
|
||||
}).pipe(
|
||||
Effect.provide(Logger.layer([], { mergeWithExisting: false })),
|
||||
Effect.catch((error) => {
|
||||
if (serviceOptions === undefined || port === undefined || !addressInUse(error)) return Effect.fail(error)
|
||||
return recognizeIncumbent(serviceOptions, hostname, port).pipe(
|
||||
Effect.flatMap((found) =>
|
||||
found
|
||||
? Effect.void
|
||||
: Effect.fail(
|
||||
new Error(
|
||||
`Managed service port ${port} on ${hostname} is already in use by another process. ` +
|
||||
"Configure another port with `opencode service set port <port>` and start the service again.",
|
||||
{ cause: error },
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
if (server === undefined) return
|
||||
: { onListen: (address) => register(address, password, instanceID, serviceOptions.file) },
|
||||
}).pipe(Effect.provide(Logger.layer([], { mergeWithExisting: false })))
|
||||
const url = HttpServer.formatAddress(server.address)
|
||||
console.log(options.mode === "stdio" ? JSON.stringify({ url }) : `server listening on ${url}`)
|
||||
if (options.mode === "default" && !environmentPassword) console.log(`server password ${password}`)
|
||||
@@ -114,7 +94,6 @@ const register = Effect.fnUntraced(function* (
|
||||
password: string,
|
||||
id: string,
|
||||
file: string,
|
||||
shutdown: Effect.Effect<void>,
|
||||
) {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const temp = file + "." + id + ".tmp"
|
||||
@@ -127,49 +106,38 @@ const register = Effect.fnUntraced(function* (
|
||||
password,
|
||||
}
|
||||
const encoded = yield* encodeInfo(info)
|
||||
const publish = fs.writeFileString(temp, encoded, { mode: 0o600 }).pipe(Effect.andThen(fs.rename(temp, file)))
|
||||
yield* publish
|
||||
const current = fs.readFileString(file).pipe(
|
||||
Effect.flatMap(decodeInfo),
|
||||
Effect.orElseSucceed(() => undefined),
|
||||
)
|
||||
const owns = (found: Info | undefined) =>
|
||||
found?.id === info.id &&
|
||||
found.version === info.version &&
|
||||
found.url === info.url &&
|
||||
found.pid === info.pid &&
|
||||
found.password === info.password
|
||||
yield* fs.writeFileString(temp, encoded, { mode: 0o600 }).pipe(Effect.andThen(fs.rename(temp, file)))
|
||||
yield* current.pipe(
|
||||
Effect.filterOrFail(owns),
|
||||
const assertRegistration = Effect.gen(function* () {
|
||||
const found = yield* current
|
||||
if (
|
||||
found !== undefined &&
|
||||
found.id === info.id &&
|
||||
found.version === info.version &&
|
||||
found.url === info.url &&
|
||||
found.pid === info.pid &&
|
||||
found.password === info.password
|
||||
)
|
||||
return
|
||||
yield* publish
|
||||
})
|
||||
yield* Effect.addFinalizer(() =>
|
||||
current.pipe(
|
||||
Effect.flatMap((current) => (current?.id === id ? fs.remove(file) : Effect.void)),
|
||||
Effect.ignore,
|
||||
),
|
||||
)
|
||||
yield* assertRegistration.pipe(
|
||||
Effect.catchCause((cause) => Effect.logWarning("failed to reassert service registration", { cause })),
|
||||
Effect.repeat(Schedule.spaced("5 seconds")),
|
||||
Effect.ignore,
|
||||
Effect.andThen(shutdown),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
return current.pipe(
|
||||
Effect.flatMap((found) => (owns(found) ? fs.remove(file) : Effect.void)),
|
||||
Effect.ignore,
|
||||
)
|
||||
})
|
||||
|
||||
const recognizeIncumbent = Effect.fnUntraced(function* (options: DiscoverOptions, hostname: string, port: number) {
|
||||
const found = yield* Service.incumbent({ ...options, url: serviceURL(hostname, port) }).pipe(
|
||||
Effect.filterOrFail((value) => value !== undefined),
|
||||
Effect.retry(Schedule.max([Schedule.spaced("100 millis"), Schedule.recurs(60)])),
|
||||
Effect.option,
|
||||
)
|
||||
return Option.isSome(found)
|
||||
})
|
||||
|
||||
function serviceURL(hostname: string, port: number) {
|
||||
return `http://${hostname.includes(":") ? `[${hostname}]` : hostname}:${port}`
|
||||
}
|
||||
|
||||
function addressInUse(error: unknown): boolean {
|
||||
if (typeof error !== "object" || error === null) return false
|
||||
if ("code" in error && error.code === "EADDRINUSE") return true
|
||||
return "cause" in error && addressInUse(error.cause)
|
||||
}
|
||||
|
||||
function waitForStdinClose() {
|
||||
return Effect.callback<void>((resume) => {
|
||||
const close = () => resume(Effect.void)
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Service } from "@opencode-ai/client/effect/service"
|
||||
import { Effect, FileSystem, Option, Schema } from "effect"
|
||||
import { randomBytes } from "crypto"
|
||||
import path from "path"
|
||||
import { selfCommand } from "../util/process"
|
||||
|
||||
// The CLI's service configuration file, plus the Service.EnsureOptions binding that
|
||||
// points the client package's service operations at this CLI: which
|
||||
@@ -30,12 +29,6 @@ export function filename(channel = InstallationChannel) {
|
||||
return `service-${Hash.fast(channel)}.json`
|
||||
}
|
||||
|
||||
export function defaultPort(channel = InstallationChannel) {
|
||||
if (channel === "latest") return 0xc0de
|
||||
if (channel === "local") return 0xc0df
|
||||
return 10_000 + (Number.parseInt(Hash.fast(channel).slice(0, 8), 16) % 50_000)
|
||||
}
|
||||
|
||||
export function versionBelongsToChannel(
|
||||
version: string | undefined,
|
||||
channel = InstallationChannel,
|
||||
@@ -85,10 +78,13 @@ const paths = Effect.gen(function* () {
|
||||
export const options = Effect.fnUntraced(function* () {
|
||||
const { file, legacyFile } = yield* paths
|
||||
yield* migrateRegistration(legacyFile, file)
|
||||
const compiled = path.basename(process.execPath).replace(/\.exe$/, "") !== "bun"
|
||||
const entrypoint = compiled ? undefined : process.argv[1]
|
||||
if (!compiled && entrypoint === undefined) return yield* Effect.fail(new Error("Failed to resolve CLI entrypoint"))
|
||||
return {
|
||||
file,
|
||||
version: InstallationVersion,
|
||||
command: [...selfCommand(), "serve", "--service"],
|
||||
command: [process.execPath, ...(entrypoint ? [entrypoint] : []), "serve", "--service"],
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Service, type Endpoint } from "@opencode-ai/client/effect/service"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { Deferred, Effect, Schema, Stream } from "effect"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { Effect, Schema, Stream } from "effect"
|
||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||
import { randomBytes } from "node:crypto"
|
||||
import { selfCommand } from "../util/process"
|
||||
import path from "node:path"
|
||||
|
||||
const Ready = Schema.Struct({ url: Schema.String })
|
||||
const decodeReady = Schema.decodeUnknownPromise(Schema.fromJsonString(Ready))
|
||||
@@ -14,7 +14,10 @@ type Options = {
|
||||
}
|
||||
|
||||
function command(password: string, options: Options) {
|
||||
const [executable, ...args] = options.command ?? [...selfCommand(), "serve"]
|
||||
const compiled = path.basename(process.execPath).replace(/\.exe$/, "") !== "bun"
|
||||
const entrypoint = compiled ? [] : process.argv[1] ? [process.argv[1]] : []
|
||||
if (!compiled && entrypoint.length === 0) throw new Error("Failed to resolve CLI entrypoint")
|
||||
const [executable, ...args] = options.command ?? [process.execPath, ...entrypoint, "serve"]
|
||||
if (!executable) throw new Error("Failed to resolve standalone server command")
|
||||
return ChildProcess.make(executable, [...args, "--stdio", "--port", "0"], {
|
||||
cwd: process.cwd(),
|
||||
@@ -36,16 +39,8 @@ const makeEndpoint = Effect.fn("cli.standalone.endpoint")(
|
||||
const password = randomBytes(32).toString("base64url")
|
||||
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
||||
const proc = yield* spawner.spawn(command(password, options))
|
||||
const readyLine = yield* Deferred.make<string, Error>()
|
||||
// Keep draining stdout after readiness so later server writes cannot hit EPIPE.
|
||||
yield* proc.stdout.pipe(
|
||||
Stream.decodeText(),
|
||||
Stream.splitLines,
|
||||
Stream.runForEach((line) => Deferred.succeed(readyLine, line)),
|
||||
Effect.ensuring(Deferred.fail(readyLine, new Error("Standalone server exited before reporting readiness"))),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
const output = yield* Deferred.await(readyLine)
|
||||
const output = yield* proc.stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.take(1), Stream.mkString)
|
||||
if (!output) return yield* Effect.fail(new Error("Standalone server exited before reporting readiness"))
|
||||
const ready = yield* Effect.tryPromise(() => decodeReady(output))
|
||||
return {
|
||||
url: ready.url,
|
||||
@@ -53,7 +48,7 @@ const makeEndpoint = Effect.fn("cli.standalone.endpoint")(
|
||||
pid: proc.pid,
|
||||
} satisfies Endpoint & { readonly pid: number }
|
||||
},
|
||||
Effect.provide(LayerNode.compile(CrossSpawnSpawner.node)),
|
||||
Effect.provide(AppNodeBuilder.build(CrossSpawnSpawner.node)),
|
||||
)
|
||||
|
||||
export function start(options: Options = {}) {
|
||||
|
||||
@@ -7,7 +7,6 @@ import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import { registerOpencodeSpinner } from "@opencode-ai/tui/component/register-spinner"
|
||||
import { SPINNER_FRAMES } from "@opencode-ai/tui/component/spinner"
|
||||
import { go } from "@opencode-ai/tui/logo"
|
||||
import { setTimeout } from "node:timers/promises"
|
||||
import {
|
||||
batch,
|
||||
createEffect,
|
||||
@@ -124,7 +123,7 @@ async function open(from?: string): Promise<Session> {
|
||||
let shownAt = performance.now()
|
||||
const waitForStage = async () => {
|
||||
const remaining = stageFloor - (performance.now() - shownAt)
|
||||
if (remaining > 0) await setTimeout(remaining)
|
||||
if (remaining > 0) await Bun.sleep(remaining)
|
||||
}
|
||||
const advance = async (stage: number) => {
|
||||
await waitForStage()
|
||||
@@ -141,11 +140,11 @@ async function open(from?: string): Promise<Session> {
|
||||
setOutcome(next)
|
||||
const completed = await Promise.race([
|
||||
settled.promise.then(() => true),
|
||||
setTimeout(transitionDuration + 500).then(() => false),
|
||||
Bun.sleep(transitionDuration + 500).then(() => false),
|
||||
])
|
||||
resolveOutcome = undefined
|
||||
setAnimating(false)
|
||||
if (completed) await setTimeout(hold)
|
||||
if (completed) await Bun.sleep(hold)
|
||||
}
|
||||
let closing: Promise<void> | undefined
|
||||
let transferred = false
|
||||
@@ -155,7 +154,7 @@ async function open(from?: string): Promise<Session> {
|
||||
setAnimating(false)
|
||||
if (renderer.isDestroyed) return
|
||||
renderer.pause()
|
||||
await Promise.race([renderer.idle(), setTimeout(500)])
|
||||
await Promise.race([renderer.idle(), Bun.sleep(500)])
|
||||
renderer.destroy()
|
||||
})())
|
||||
let loading: Promise<void> | undefined
|
||||
@@ -179,7 +178,7 @@ async function open(from?: string): Promise<Session> {
|
||||
renderer.screenMode = "alternate-screen"
|
||||
renderer.consoleMode = "console-overlay"
|
||||
renderer.requestRender()
|
||||
await Promise.race([renderer.idle(), setTimeout(500)])
|
||||
await Promise.race([renderer.idle(), Bun.sleep(500)])
|
||||
transferred = true
|
||||
return {
|
||||
renderer,
|
||||
|
||||
@@ -12,16 +12,11 @@ import { parse, type ParseError } from "jsonc-parser"
|
||||
import path from "node:path"
|
||||
import semver from "semver"
|
||||
|
||||
declare const OPENCODE_CLI_NAME: string | undefined
|
||||
|
||||
export type Policy = boolean | "notify"
|
||||
export type Action = "none" | "upgrade"
|
||||
type Method = "npm" | "pnpm" | "bun" | "yarn"
|
||||
|
||||
const packageName =
|
||||
typeof OPENCODE_CLI_NAME === "string" && OPENCODE_CLI_NAME === "opencode2-node"
|
||||
? OPENCODE_CLI_NAME
|
||||
: "@opencode-ai/cli"
|
||||
const packageName = "@opencode-ai/cli"
|
||||
|
||||
export interface Interface {
|
||||
readonly check: () => Effect.Effect<void>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { text } from "node:stream/consumers"
|
||||
|
||||
export function readStdin() {
|
||||
return text(process.stdin)
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import path from "node:path"
|
||||
|
||||
export function selfCommand() {
|
||||
const runtime = path.basename(process.execPath, path.extname(process.execPath)).toLowerCase()
|
||||
if (runtime !== "bun" && runtime !== "node" && runtime !== "nodejs") return [process.execPath]
|
||||
if (!process.argv[1]) throw new Error("Failed to resolve CLI entrypoint")
|
||||
if (runtime === "node" || runtime === "nodejs") return [process.execPath, ...nodeFlags(), process.argv[1]]
|
||||
return [process.execPath, process.argv[1]]
|
||||
}
|
||||
|
||||
function nodeFlags() {
|
||||
return process.execArgv.flatMap((arg, index, args) => {
|
||||
if (index > 0 && args[index - 1] === "--conditions") return []
|
||||
if (arg === "--conditions") return args[index + 1] ? [arg, args[index + 1]] : []
|
||||
if (arg.startsWith("--conditions=")) return [arg]
|
||||
if (
|
||||
arg === "--experimental-ffi" ||
|
||||
arg === "--use-system-ca" ||
|
||||
arg === "--enable-source-maps" ||
|
||||
arg === "--no-addons"
|
||||
)
|
||||
return [arg]
|
||||
if (arg === "--no-warnings" || arg.startsWith("--disable-warning=")) return [arg]
|
||||
return []
|
||||
})
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
/** @jsxImportSource @opentui/solid */
|
||||
import { testRender } from "@opentui/solid"
|
||||
import { Keymap } from "@opencode-ai/tui/context/keymap"
|
||||
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
|
||||
import { testRender, useRenderer } from "@opentui/solid"
|
||||
import { OpencodeKeymapProvider, registerOpencodeKeymap } from "@opencode-ai/tui/keymap"
|
||||
import { resolve } from "@opencode-ai/tui/config/v1"
|
||||
import { expect, test } from "bun:test"
|
||||
import { createSignal } from "solid-js"
|
||||
import { createComponent, createSignal } from "solid-js"
|
||||
import { RunFooterView } from "../src/mini/footer.view"
|
||||
import { RUN_THEME_FALLBACK } from "../src/mini/theme"
|
||||
import type { FooterState, FooterSubagentState, FooterView } from "../src/mini/types"
|
||||
@@ -41,43 +42,52 @@ test("down opens subagents from an empty prompt", async () => {
|
||||
{ keybinds: { editor_open: "none", session_queued_prompts: "none" } },
|
||||
{ terminalSuspend: true },
|
||||
)
|
||||
let offKeymap: (() => void) | undefined
|
||||
|
||||
function Harness() {
|
||||
return (
|
||||
<Keymap.Provider config={config}>
|
||||
<RunFooterView
|
||||
directory="/tmp"
|
||||
findFiles={async () => []}
|
||||
agents={() => []}
|
||||
references={() => []}
|
||||
commands={() => []}
|
||||
providers={() => undefined}
|
||||
currentModel={() => undefined}
|
||||
variants={() => []}
|
||||
currentVariant={() => undefined}
|
||||
state={state}
|
||||
view={view}
|
||||
subagent={subagents}
|
||||
theme={() => RUN_THEME_FALLBACK}
|
||||
tuiConfig={config}
|
||||
agent="opencode"
|
||||
onSubmit={() => true}
|
||||
onPermissionReply={() => {}}
|
||||
onQuestionReply={() => {}}
|
||||
onQuestionReject={() => {}}
|
||||
onCycle={() => {}}
|
||||
onInterrupt={() => false}
|
||||
onEditorOpen={async () => undefined}
|
||||
onInputClear={() => {}}
|
||||
onExit={() => {}}
|
||||
onModelSelect={() => {}}
|
||||
onVariantSelect={() => {}}
|
||||
onRows={() => {}}
|
||||
onLayout={() => {}}
|
||||
onStatus={() => {}}
|
||||
onQueuedRemove={async () => true}
|
||||
/>
|
||||
</Keymap.Provider>
|
||||
)
|
||||
const renderer = useRenderer()
|
||||
const keymap = createDefaultOpenTuiKeymap(renderer)
|
||||
offKeymap = registerOpencodeKeymap(keymap, renderer, config)
|
||||
|
||||
return createComponent(OpencodeKeymapProvider, {
|
||||
keymap,
|
||||
get children() {
|
||||
return (
|
||||
<RunFooterView
|
||||
directory="/tmp"
|
||||
findFiles={async () => []}
|
||||
agents={() => []}
|
||||
references={() => []}
|
||||
commands={() => []}
|
||||
providers={() => undefined}
|
||||
currentModel={() => undefined}
|
||||
variants={() => []}
|
||||
currentVariant={() => undefined}
|
||||
state={state}
|
||||
view={view}
|
||||
subagent={subagents}
|
||||
theme={() => RUN_THEME_FALLBACK}
|
||||
tuiConfig={config}
|
||||
agent="opencode"
|
||||
onSubmit={() => true}
|
||||
onPermissionReply={() => {}}
|
||||
onQuestionReply={() => {}}
|
||||
onQuestionReject={() => {}}
|
||||
onCycle={() => {}}
|
||||
onInterrupt={() => false}
|
||||
onEditorOpen={async () => undefined}
|
||||
onInputClear={() => {}}
|
||||
onExit={() => {}}
|
||||
onModelSelect={() => {}}
|
||||
onVariantSelect={() => {}}
|
||||
onRows={() => {}}
|
||||
onLayout={() => {}}
|
||||
onStatus={() => {}}
|
||||
onQueuedRemove={async () => true}
|
||||
/>
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const app = await testRender(() => <Harness />, { width: 100, height: 8, kittyKeyboard: true })
|
||||
@@ -90,6 +100,7 @@ test("down opens subagents from an empty prompt", async () => {
|
||||
} finally {
|
||||
app.renderer.currentFocusedRenderable?.blur()
|
||||
app.renderer.currentFocusedEditor?.blur()
|
||||
offKeymap?.()
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { Service, type Info } from "@opencode-ai/client/effect/service"
|
||||
import { Service } from "@opencode-ai/client/effect/service"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { EventTable } from "@opencode-ai/core/event/sql"
|
||||
@@ -17,13 +17,6 @@ import os from "node:os"
|
||||
import path from "node:path"
|
||||
import { ServiceConfig } from "../src/services/service-config"
|
||||
|
||||
test("managed service ports are stable per installation channel", () => {
|
||||
expect(ServiceConfig.defaultPort("latest")).toBe(0xc0de)
|
||||
expect(ServiceConfig.defaultPort("local")).toBe(0xc0df)
|
||||
expect(ServiceConfig.defaultPort("preview-a")).toBe(ServiceConfig.defaultPort("preview-a"))
|
||||
expect(ServiceConfig.defaultPort("preview-a")).not.toBe(ServiceConfig.defaultPort("preview-b"))
|
||||
})
|
||||
|
||||
test("local channel stores service config with the local service filename", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-"))
|
||||
try {
|
||||
@@ -97,80 +90,6 @@ test("preview registration migration never moves stable discovery", async () =>
|
||||
}
|
||||
})
|
||||
|
||||
test("managed service writes its registration once", async () => {
|
||||
const service = await startManagedService("opencode-service-once-")
|
||||
try {
|
||||
const before = await fs.stat(service.registration)
|
||||
await Bun.sleep(6_000)
|
||||
const after = await fs.stat(service.registration)
|
||||
expect(after.ino).toBe(before.ino)
|
||||
expect(after.mtimeMs).toBe(before.mtimeMs)
|
||||
expect(await Bun.file(service.registration).json()).toEqual(service.info)
|
||||
} finally {
|
||||
await stopManagedService(service)
|
||||
}
|
||||
}, 30_000)
|
||||
|
||||
test("deleting a managed service registration stops its owner", async () => {
|
||||
const service = await startManagedService("opencode-service-delete-")
|
||||
try {
|
||||
await fs.rm(service.registration)
|
||||
expect(await waitForExit(service.owner)).toBe(true)
|
||||
expect(await Bun.file(service.registration).exists()).toBe(false)
|
||||
await expectPortAvailable(service.port)
|
||||
} finally {
|
||||
await stopManagedService(service)
|
||||
}
|
||||
}, 30_000)
|
||||
|
||||
test("deleting a failed service registration stops its owner", async () => {
|
||||
const service = await startManagedService("opencode-service-failed-delete-", true)
|
||||
try {
|
||||
await waitForFailed(service.info)
|
||||
await fs.rm(service.registration)
|
||||
expect(await waitForExit(service.owner)).toBe(true)
|
||||
await expectPortAvailable(service.port)
|
||||
} finally {
|
||||
await stopManagedService(service)
|
||||
}
|
||||
}, 30_000)
|
||||
|
||||
test("corrupting a managed service registration stops its owner", async () => {
|
||||
const service = await startManagedService("opencode-service-corrupt-")
|
||||
try {
|
||||
await fs.writeFile(service.registration, "not-json")
|
||||
expect(await waitForExit(service.owner)).toBe(true)
|
||||
expect(await Bun.file(service.registration).text()).toBe("not-json")
|
||||
await expectPortAvailable(service.port)
|
||||
} finally {
|
||||
await stopManagedService(service)
|
||||
}
|
||||
}, 30_000)
|
||||
|
||||
test("replacing a managed service registration stops its owner and preserves the foreign owner", async () => {
|
||||
const service = await startManagedService("opencode-service-foreign-")
|
||||
const foreign = { ...service.info, id: "foreign-owner", pid: process.pid }
|
||||
try {
|
||||
await fs.writeFile(service.registration, JSON.stringify(foreign))
|
||||
expect(await waitForExit(service.owner)).toBe(true)
|
||||
expect(await Bun.file(service.registration).json()).toEqual(foreign)
|
||||
await expectPortAvailable(service.port)
|
||||
} finally {
|
||||
await stopManagedService(service)
|
||||
}
|
||||
}, 30_000)
|
||||
|
||||
test("clean managed service shutdown removes its registration", async () => {
|
||||
const service = await startManagedService("opencode-service-clean-")
|
||||
try {
|
||||
await Effect.runPromise(Service.stop({ file: service.registration }).pipe(Effect.provide(NodeFileSystem.layer)))
|
||||
expect(await waitForExit(service.owner)).toBe(true)
|
||||
expect(await Bun.file(service.registration).exists()).toBe(false)
|
||||
} finally {
|
||||
await stopManagedService(service)
|
||||
}
|
||||
}, 30_000)
|
||||
|
||||
test("concurrent service processes elect one server", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-election-"))
|
||||
const database = path.join(root, "opencode.db")
|
||||
@@ -211,9 +130,6 @@ test("concurrent service processes elect one server", async () => {
|
||||
)
|
||||
const command = [process.execPath, path.join(import.meta.dir, "../src/index.ts"), "serve", "--service"]
|
||||
const registration = path.join(root, "state", "opencode", "service-local.json")
|
||||
const port = await availablePort()
|
||||
await fs.mkdir(path.join(root, "config", "opencode"), { recursive: true })
|
||||
await fs.writeFile(path.join(root, "config", "opencode", "service-local.json"), JSON.stringify({ port }))
|
||||
const processes = Array.from({ length: 10 }, () => Bun.spawn(command, { env, stderr: "pipe", stdout: "ignore" }))
|
||||
|
||||
try {
|
||||
@@ -221,13 +137,11 @@ test("concurrent service processes elect one server", async () => {
|
||||
const winner = processes.find((process) => process.pid === info.pid)
|
||||
const losers = processes.filter((process) => process.pid !== info.pid)
|
||||
const exited = await Promise.all(
|
||||
losers.map((process) => Promise.race([process.exited.then(() => true), Bun.sleep(60_000).then(() => false)])),
|
||||
losers.map((process) => Promise.race([process.exited.then(() => true), Bun.sleep(10_000).then(() => false)])),
|
||||
)
|
||||
|
||||
expect(exited).toEqual(losers.map(() => true))
|
||||
expect(winner?.exitCode).toBe(null)
|
||||
expect(new URL(info.url).port).toBe(String(port))
|
||||
expect(await Bun.file(registration + ".lock").exists()).toBe(false)
|
||||
expect(
|
||||
await fetch(new URL("/api/health", info.url), {
|
||||
headers: { authorization: "Basic " + btoa(`opencode:${info.password}`) },
|
||||
@@ -237,6 +151,20 @@ test("concurrent service processes elect one server", async () => {
|
||||
version: info.version,
|
||||
pid: info.pid,
|
||||
})
|
||||
const blockedTemp = registration + "." + info.id + ".tmp"
|
||||
await fs.mkdir(blockedTemp)
|
||||
await fs.rm(registration)
|
||||
await Bun.sleep(6_000)
|
||||
expect(await Bun.file(registration).exists()).toBe(false)
|
||||
await fs.rm(blockedTemp, { recursive: true })
|
||||
const restored = await waitForInfo(registration)
|
||||
expect(restored.id).toBe(info.id)
|
||||
expect(restored.pid).toBe(info.pid)
|
||||
await fs.writeFile(registration, "not-json")
|
||||
const repaired = await waitForInfo(registration)
|
||||
expect(repaired.id).toBe(info.id)
|
||||
expect(repaired.pid).toBe(info.pid)
|
||||
|
||||
const contender = Bun.spawn(command, { env, stderr: "pipe", stdout: "ignore" })
|
||||
try {
|
||||
const contenderExited = await Promise.race([
|
||||
@@ -263,7 +191,9 @@ test("concurrent service processes elect one server", async () => {
|
||||
),
|
||||
).toEqual({ timeSuspended: null })
|
||||
expect(await waitForExecutionStart(database, sessionID)).toBe(1)
|
||||
await Effect.runPromise(Service.stop({ file: registration }).pipe(Effect.provide(NodeFileSystem.layer)))
|
||||
await Effect.runPromise(
|
||||
Service.stop({ file: registration }).pipe(Effect.provide(NodeFileSystem.layer)),
|
||||
)
|
||||
await winner?.exited
|
||||
} finally {
|
||||
processes.forEach((process) => process.kill("SIGTERM"))
|
||||
@@ -274,88 +204,9 @@ test("concurrent service processes elect one server", async () => {
|
||||
await fs.rm(root, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
}, 120_000)
|
||||
}, 60_000)
|
||||
|
||||
test("configured managed service port overrides the channel default", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-port-"))
|
||||
const port = await availablePort()
|
||||
const env = serviceEnv(root)
|
||||
const registration = path.join(root, "state", "opencode", "service-local.json")
|
||||
await fs.mkdir(path.join(root, "config", "opencode"), { recursive: true })
|
||||
await fs.writeFile(path.join(root, "config", "opencode", "service-local.json"), JSON.stringify({ port }))
|
||||
const owner = Bun.spawn([process.execPath, path.join(import.meta.dir, "../src/index.ts"), "serve", "--service"], {
|
||||
env,
|
||||
stderr: "pipe",
|
||||
stdout: "ignore",
|
||||
})
|
||||
try {
|
||||
const info = await waitForInfo(registration)
|
||||
expect(new URL(info.url).port).toBe(String(port))
|
||||
await Effect.runPromise(Service.stop({ file: registration }).pipe(Effect.provide(NodeFileSystem.layer)))
|
||||
await owner.exited
|
||||
} finally {
|
||||
owner.kill("SIGTERM")
|
||||
await owner.exited
|
||||
await fs.rm(root, { recursive: true, force: true })
|
||||
}
|
||||
}, 30_000)
|
||||
|
||||
test("unrelated managed port occupancy reports an actionable conflict", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-conflict-"))
|
||||
const listener = Bun.serve({ port: 0, fetch: () => new Response("unrelated") })
|
||||
const port = listener.port
|
||||
const registration = path.join(root, "state", "opencode", "service-local.json")
|
||||
await fs.mkdir(path.join(root, "config", "opencode"), { recursive: true })
|
||||
await fs.writeFile(path.join(root, "config", "opencode", "service-local.json"), JSON.stringify({ port }))
|
||||
const contender = Bun.spawn([process.execPath, path.join(import.meta.dir, "../src/index.ts"), "serve", "--service"], {
|
||||
env: serviceEnv(root),
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
})
|
||||
try {
|
||||
expect(await contender.exited).not.toBe(0)
|
||||
const output = (await new Response(contender.stdout).text()) + (await new Response(contender.stderr).text())
|
||||
expect(output).toContain(`Managed service port ${port} on 127.0.0.1 is already in use by another process`)
|
||||
expect(output).toContain("opencode service set port <port>")
|
||||
expect(await Bun.file(registration).exists()).toBe(false)
|
||||
} finally {
|
||||
listener.stop(true)
|
||||
contender.kill("SIGTERM")
|
||||
await contender.exited
|
||||
await fs.rm(root, { recursive: true, force: true })
|
||||
}
|
||||
}, 30_000)
|
||||
|
||||
test("stale dead registration is replaced after binding the selected port", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-stale-"))
|
||||
const port = await availablePort()
|
||||
const registration = path.join(root, "state", "opencode", "service-local.json")
|
||||
await fs.mkdir(path.join(root, "config", "opencode"), { recursive: true })
|
||||
await fs.mkdir(path.dirname(registration), { recursive: true })
|
||||
await fs.writeFile(path.join(root, "config", "opencode", "service-local.json"), JSON.stringify({ port }))
|
||||
await fs.writeFile(
|
||||
registration,
|
||||
JSON.stringify({ id: "dead", version: "dead", url: `http://127.0.0.1:${port}`, pid: 2_147_483_647 }),
|
||||
)
|
||||
const owner = Bun.spawn([process.execPath, path.join(import.meta.dir, "../src/index.ts"), "serve", "--service"], {
|
||||
env: serviceEnv(root),
|
||||
stderr: "pipe",
|
||||
stdout: "ignore",
|
||||
})
|
||||
try {
|
||||
const info = await waitForInfo(registration, (value) => value.id !== "dead")
|
||||
expect(new URL(info.url).port).toBe(String(port))
|
||||
expect(info.pid).toBe(owner.pid)
|
||||
await Effect.runPromise(Service.stop({ file: registration }).pipe(Effect.provide(NodeFileSystem.layer)))
|
||||
await owner.exited
|
||||
} finally {
|
||||
owner.kill("SIGTERM")
|
||||
await owner.exited
|
||||
await fs.rm(root, { recursive: true, force: true })
|
||||
}
|
||||
}, 30_000)
|
||||
|
||||
test("a failed service stays registered and owns the selected port until stopped", async () => {
|
||||
test("a failed service stays registered and owns the lock until stopped", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-failed-"))
|
||||
const database = path.join(root, "database")
|
||||
await fs.mkdir(database)
|
||||
@@ -420,92 +271,19 @@ function waitForExecutionStart(file: string, sessionID: SessionV2.ID) {
|
||||
),
|
||||
Effect.filterOrFail((rows) => rows.length > 0),
|
||||
Effect.map((rows) => rows.length),
|
||||
Effect.retry(Schedule.max([Schedule.spaced("50 millis"), Schedule.recurs(200)])),
|
||||
Effect.retry(Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(200)))),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async function waitForInfo(file: string, accept: (info: Info) => boolean = () => true) {
|
||||
async function waitForInfo(file: string) {
|
||||
for (let attempt = 0; attempt < 400; attempt++) {
|
||||
const value = await Bun.file(file)
|
||||
.json()
|
||||
.catch(() => undefined)
|
||||
if (value !== undefined) {
|
||||
const info = await Schema.decodeUnknownPromise(Service.Info)(value)
|
||||
if (accept(info)) return info
|
||||
}
|
||||
if (value !== undefined) return Schema.decodeUnknownPromise(Service.Info)(value)
|
||||
await Bun.sleep(50)
|
||||
}
|
||||
throw new Error("Timed out waiting for service registration")
|
||||
}
|
||||
|
||||
async function waitForFailed(info: Info) {
|
||||
for (let attempt = 0; attempt < 400; attempt++) {
|
||||
const status = await fetch(new URL("/api/health", info.url), {
|
||||
headers: { authorization: "Basic " + btoa(`opencode:${info.password}`) },
|
||||
})
|
||||
.then((response) => response.status)
|
||||
.catch(() => undefined)
|
||||
if (status === 500) return
|
||||
await Bun.sleep(50)
|
||||
}
|
||||
throw new Error("Timed out waiting for service boot failure")
|
||||
}
|
||||
|
||||
async function availablePort() {
|
||||
const server = Bun.serve({ port: 0, fetch: () => new Response() })
|
||||
const port = server.port
|
||||
await server.stop(true)
|
||||
if (port === undefined) throw new Error("Server did not bind a port")
|
||||
return port
|
||||
}
|
||||
|
||||
function serviceEnv(root: string) {
|
||||
return {
|
||||
...process.env,
|
||||
HOME: root,
|
||||
OPENCODE_DB: path.join(root, "opencode.db"),
|
||||
OPENCODE_TEST_HOME: root,
|
||||
XDG_CACHE_HOME: path.join(root, "cache"),
|
||||
XDG_CONFIG_HOME: path.join(root, "config"),
|
||||
XDG_DATA_HOME: path.join(root, "data"),
|
||||
XDG_STATE_HOME: path.join(root, "state"),
|
||||
}
|
||||
}
|
||||
|
||||
async function startManagedService(prefix: string, failBoot = false) {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), prefix))
|
||||
const port = await availablePort()
|
||||
const registration = path.join(root, "state", "opencode", "service-local.json")
|
||||
await fs.mkdir(path.join(root, "config", "opencode"), { recursive: true })
|
||||
if (failBoot) await fs.mkdir(path.join(root, "database"))
|
||||
await fs.writeFile(path.join(root, "config", "opencode", "service-local.json"), JSON.stringify({ port }))
|
||||
const owner = Bun.spawn([process.execPath, path.join(import.meta.dir, "../src/index.ts"), "serve", "--service"], {
|
||||
env: failBoot ? { ...serviceEnv(root), OPENCODE_DB: path.join(root, "database") } : serviceEnv(root),
|
||||
stderr: "pipe",
|
||||
stdout: "ignore",
|
||||
})
|
||||
const info = await waitForInfo(registration).catch(async (cause) => {
|
||||
owner.kill("SIGTERM")
|
||||
await owner.exited
|
||||
await fs.rm(root, { recursive: true, force: true })
|
||||
throw cause
|
||||
})
|
||||
return { root, port, registration, owner, info }
|
||||
}
|
||||
|
||||
async function stopManagedService(service: Awaited<ReturnType<typeof startManagedService>>) {
|
||||
service.owner.kill("SIGTERM")
|
||||
await service.owner.exited
|
||||
await fs.rm(service.root, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
function waitForExit(process: Bun.Subprocess, timeout = 10_000) {
|
||||
return Promise.race([process.exited.then(() => true), Bun.sleep(timeout).then(() => false)])
|
||||
}
|
||||
|
||||
async function expectPortAvailable(port: number) {
|
||||
const server = Bun.serve({ hostname: "127.0.0.1", port, fetch: () => new Response() })
|
||||
await server.stop(true)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,5 @@
|
||||
"jsxImportSource": "@opentui/solid",
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"noUncheckedIndexedAccess": false
|
||||
},
|
||||
"exclude": ["dist", "dist-node"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
import path from "node:path"
|
||||
import { readFile } from "node:fs/promises"
|
||||
import { createRequire } from "node:module"
|
||||
import { defineConfig, type Plugin, type UserConfig } from "vite"
|
||||
import solid from "vite-plugin-solid"
|
||||
import { nodeExecArgv, nodeTarget, type NodeTarget, photonWasmAsset } from "./src/node/target"
|
||||
|
||||
const dir = import.meta.dirname
|
||||
|
||||
function rawTextPlugin(): Plugin {
|
||||
return {
|
||||
name: "opencode:raw-text",
|
||||
async load(id) {
|
||||
if (!id.endsWith(".md")) return
|
||||
return `export default ${JSON.stringify(await readFile(id, "utf8"))}`
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function runtimeRequirePlugin(): Plugin {
|
||||
return {
|
||||
name: "opencode:runtime-require",
|
||||
enforce: "pre",
|
||||
transform(code, id) {
|
||||
if (!id.endsWith("turndown/lib/turndown.es.js")) return
|
||||
const transformed = code.replace(" var domino = require('@mixmark-io/domino');", "")
|
||||
if (transformed === code) this.error("Failed to rewrite Turndown's Domino require")
|
||||
return `import domino from "@mixmark-io/domino"\n${transformed}`
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const resolve = {
|
||||
alias: [
|
||||
{ find: /^solid-js\/store$/, replacement: "solid-js/store/dist/store.js" },
|
||||
{ find: /^solid-js$/, replacement: "solid-js/dist/solid.js" },
|
||||
{
|
||||
find: /^ws$/,
|
||||
replacement: path.join(path.dirname(createRequire(import.meta.url).resolve("ws/package.json")), "wrapper.mjs"),
|
||||
},
|
||||
],
|
||||
conditions: ["node"],
|
||||
}
|
||||
|
||||
const output = (entryFileNames: string, banner?: string) => ({
|
||||
format: "esm" as const,
|
||||
entryFileNames,
|
||||
inlineDynamicImports: true,
|
||||
banner,
|
||||
})
|
||||
|
||||
function nodePrelude(input: NodeBuildInput) {
|
||||
const nodePtySpawnHelper =
|
||||
input.target.platform === "darwin"
|
||||
? `${input.target.nodePtyPackage}/prebuilds/darwin-${input.target.arch}/spawn-helper`
|
||||
: undefined
|
||||
const promiseModule = `const sdk = globalThis[Symbol.for("opencode.plugin.v2.promise")]
|
||||
if (!sdk) throw new Error("OpenCode Promise plugin SDK is unavailable")
|
||||
export const Agent = sdk.Agent
|
||||
export const Command = sdk.Command
|
||||
export const Connection = sdk.Connection
|
||||
export const Credential = sdk.Credential
|
||||
export const Integration = sdk.Integration
|
||||
export const Model = sdk.Model
|
||||
export const Plugin = sdk.Plugin
|
||||
export const Provider = sdk.Provider
|
||||
export const Reference = sdk.Reference
|
||||
export const Skill = sdk.Skill`
|
||||
const effectModule = promiseModule
|
||||
.replace("opencode.plugin.v2.promise", "opencode.plugin.v2.effect")
|
||||
.replace("Promise plugin", "Effect plugin")
|
||||
const promisePluginModule = `const sdk = globalThis[Symbol.for("opencode.plugin.v2.promise")]
|
||||
if (!sdk) throw new Error("OpenCode Promise plugin SDK is unavailable")
|
||||
export const define = sdk.Plugin.define`
|
||||
const effectPluginModule = promisePluginModule
|
||||
.replace("opencode.plugin.v2.promise", "opencode.plugin.v2.effect")
|
||||
.replace("Promise plugin", "Effect plugin")
|
||||
const effectToolModule = `const sdk = globalThis[Symbol.for("opencode.plugin.v2.effect")]
|
||||
if (!sdk) throw new Error("OpenCode Effect plugin SDK is unavailable")
|
||||
export const Tool = sdk.Tool
|
||||
export const Failure = sdk.Tool.Failure
|
||||
export const RegistrationError = sdk.Tool.RegistrationError
|
||||
export const make = sdk.Tool.make
|
||||
export const validateName = sdk.Tool.validateName
|
||||
export const registrationEntries = sdk.Tool.registrationEntries
|
||||
export const withPermission = sdk.Tool.withPermission
|
||||
export const permission = sdk.Tool.permission
|
||||
export const definition = sdk.Tool.definition
|
||||
export const settle = sdk.Tool.settle`
|
||||
return `#!/usr/bin/env -S node ${nodeExecArgv.join(" ")}
|
||||
import __cjs_mod__ from "node:module"
|
||||
import { chmodSync as __ocChmod, existsSync as __ocExists, lstatSync as __ocLstat, mkdirSync as __ocMkdir, renameSync as __ocRename, rmSync as __ocRm, writeFileSync as __ocWrite } from "node:fs"
|
||||
import { tmpdir as __ocTmpdir } from "node:os"
|
||||
import __ocPath from "node:path"
|
||||
import { getAssetKeys as __ocAssetKeys, getRawAsset as __ocRawAsset, isSea as __ocIsSea } from "node:sea"
|
||||
import { fileURLToPath as __ocFileURLToPath } from "node:url"
|
||||
const __filename = import.meta.filename
|
||||
const __dirname = import.meta.dirname
|
||||
const require = __cjs_mod__.createRequire(import.meta.url)
|
||||
const __ocPluginModules = ${JSON.stringify({
|
||||
"@opencode-ai/plugin/v2": "opencode:plugin-v2",
|
||||
"@opencode-ai/plugin/v2/plugin": "opencode:plugin-v2-plugin",
|
||||
"@opencode-ai/plugin/v2/effect": "opencode:plugin-v2-effect",
|
||||
"@opencode-ai/plugin/v2/effect/plugin": "opencode:plugin-v2-effect-plugin",
|
||||
"@opencode-ai/plugin/v2/effect/tool": "opencode:plugin-v2-effect-tool",
|
||||
})}
|
||||
const __ocPluginSources = ${JSON.stringify({
|
||||
"opencode:plugin-v2": promiseModule,
|
||||
"opencode:plugin-v2-plugin": promisePluginModule,
|
||||
"opencode:plugin-v2-effect": effectModule,
|
||||
"opencode:plugin-v2-effect-plugin": effectPluginModule,
|
||||
"opencode:plugin-v2-effect-tool": effectToolModule,
|
||||
})}
|
||||
__cjs_mod__.registerHooks({
|
||||
resolve(__ocSpecifier, __ocContext, __ocNextResolve) {
|
||||
const __ocUrl = __ocPluginModules[__ocSpecifier]
|
||||
return __ocUrl ? { url: __ocUrl, shortCircuit: true } : __ocNextResolve(__ocSpecifier, __ocContext)
|
||||
},
|
||||
load(__ocUrl, __ocContext, __ocNextLoad) {
|
||||
const __ocSource = __ocPluginSources[__ocUrl]
|
||||
return __ocSource
|
||||
? { format: "module", source: __ocSource, shortCircuit: true }
|
||||
: __ocNextLoad(__ocUrl, __ocContext)
|
||||
},
|
||||
})
|
||||
const __ocUid = typeof process.getuid === "function" ? process.getuid() : undefined
|
||||
const __ocCacheRoot = __ocPath.join(__ocTmpdir(), \`opencode-node-\${__ocUid ?? "user"}\`)
|
||||
if (__ocIsSea()) {
|
||||
try {
|
||||
__ocMkdir(__ocCacheRoot, { mode: 0o700 })
|
||||
} catch (__ocError) {
|
||||
if (!__ocExists(__ocCacheRoot)) throw __ocError
|
||||
}
|
||||
const __ocCacheInfo = __ocLstat(__ocCacheRoot)
|
||||
if (!__ocCacheInfo.isDirectory() || __ocCacheInfo.isSymbolicLink()) throw new Error("Unsafe Node asset cache path")
|
||||
if (__ocUid !== undefined && __ocCacheInfo.uid !== __ocUid) throw new Error("Node asset cache is owned by another user")
|
||||
if (__ocUid !== undefined) __ocChmod(__ocCacheRoot, 0o700)
|
||||
}
|
||||
const __ocAssetRoot = __ocIsSea()
|
||||
? __ocPath.join(__ocCacheRoot, ${JSON.stringify(`${input.assetHash}-${input.target.platform}-${input.target.arch}`)})
|
||||
: __ocFileURLToPath(new URL("./assets/", import.meta.url))
|
||||
if (__ocIsSea()) {
|
||||
for (const __ocKey of __ocAssetKeys()) {
|
||||
const __ocTarget = __ocPath.join(__ocAssetRoot, __ocKey)
|
||||
if (__ocExists(__ocTarget)) continue
|
||||
__ocMkdir(__ocPath.dirname(__ocTarget), { recursive: true })
|
||||
const __ocTemporary = \`${"${__ocTarget}"}.${"${process.pid}"}.${"${crypto.randomUUID()}"}.tmp\`
|
||||
__ocWrite(__ocTemporary, new Uint8Array(__ocRawAsset(__ocKey)))
|
||||
try {
|
||||
__ocRename(__ocTemporary, __ocTarget)
|
||||
} catch (__ocError) {
|
||||
__ocRm(__ocTemporary, { force: true })
|
||||
if (!__ocExists(__ocTarget)) throw __ocError
|
||||
}
|
||||
}
|
||||
const __ocPtySpawnHelper = ${JSON.stringify(nodePtySpawnHelper)}
|
||||
if (__ocPtySpawnHelper) __ocChmod(__ocPath.join(__ocAssetRoot, __ocPtySpawnHelper), 0o755)
|
||||
}
|
||||
process.env.OPENCODE_NODE_ASSETS_DIR = __ocAssetRoot
|
||||
process.env.OTUI_ASSET_ROOT = __ocAssetRoot
|
||||
process.env.OPENCODE_NODE_PTY_PATH = __ocPath.join(__ocAssetRoot, ${JSON.stringify(input.target.nodePtyEntryAsset)})
|
||||
process.env.OPENCODE_PARCEL_WATCHER_PATH = __ocPath.join(__ocAssetRoot, ${JSON.stringify(input.target.parcelWatcherAsset)})
|
||||
process.env.OPENCODE_PHOTON_WASM_PATH = __ocPath.join(__ocAssetRoot, ${JSON.stringify(photonWasmAsset)})
|
||||
globalThis.__OPENCODE_PHOTON_WASM_PATH = process.env.OPENCODE_PHOTON_WASM_PATH
|
||||
if (process.platform === "linux") process.env.OPENTUI_LIBC = "glibc"`
|
||||
}
|
||||
|
||||
export type NodeBuildInput = {
|
||||
readonly version: string
|
||||
readonly channel: string
|
||||
readonly models: string
|
||||
readonly assetHash: string
|
||||
readonly target: NodeTarget
|
||||
}
|
||||
|
||||
export function mainConfig(input: NodeBuildInput): UserConfig {
|
||||
return defineConfig({
|
||||
root: dir,
|
||||
plugins: [
|
||||
rawTextPlugin(),
|
||||
runtimeRequirePlugin(),
|
||||
solid({
|
||||
solid: {
|
||||
generate: "universal",
|
||||
moduleName: "@opentui/solid",
|
||||
},
|
||||
}),
|
||||
],
|
||||
resolve,
|
||||
esbuild: { jsx: "automatic" },
|
||||
define: {
|
||||
OPENCODE_VERSION: JSON.stringify(input.version),
|
||||
OPENCODE_CLI_NAME: JSON.stringify("opencode2-node"),
|
||||
OPENCODE_MODELS_DEV: input.models,
|
||||
OPENCODE_CHANNEL: JSON.stringify(input.channel),
|
||||
OPENCODE_LIBC: input.target.platform === "linux" ? JSON.stringify("glibc") : "undefined",
|
||||
FFF_LIBC: input.target.platform === "linux" ? JSON.stringify("gnu") : "undefined",
|
||||
},
|
||||
ssr: { noExternal: true },
|
||||
build: {
|
||||
ssr: "src/node/index.ts",
|
||||
target: "node26",
|
||||
outDir: "dist-node",
|
||||
emptyOutDir: false,
|
||||
minify: true,
|
||||
rollupOptions: {
|
||||
external: [/^@opencode-ai\/simulation(?:\/|$)/],
|
||||
output: output("opencode.mjs", nodePrelude(input)),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export default mainConfig({
|
||||
version: process.env.OPENCODE_VERSION ?? "local",
|
||||
channel: process.env.OPENCODE_CHANNEL ?? "local",
|
||||
models: "undefined",
|
||||
assetHash: "local",
|
||||
target: nodeTarget(process.platform, process.arch),
|
||||
})
|
||||
@@ -36,7 +36,7 @@
|
||||
"@opencode-ai/protocol": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"effect": "4.0.0-beta.98"
|
||||
"effect": "4.0.0-beta.83"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"effect": {
|
||||
|
||||
@@ -269,40 +269,32 @@ export type SessionInstructionsEntryRemoveOperation<E = never> = (
|
||||
input: Endpoint5_24Input,
|
||||
) => Effect.Effect<Endpoint5_24Output, E>
|
||||
|
||||
type Endpoint5_25Request = Parameters<RawClient["server.session"]["session.generate"]>[0]
|
||||
type Endpoint5_25Request = Parameters<RawClient["server.session"]["session.log"]>[0]
|
||||
export type Endpoint5_25Input = {
|
||||
readonly sessionID: Endpoint5_25Request["params"]["sessionID"]
|
||||
readonly prompt: Endpoint5_25Request["payload"]["prompt"]
|
||||
readonly after?: Endpoint5_25Request["query"]["after"]
|
||||
readonly follow?: Endpoint5_25Request["query"]["follow"]
|
||||
}
|
||||
export type Endpoint5_25Output = EffectValue<ReturnType<RawClient["server.session"]["session.generate"]>>["data"]
|
||||
export type SessionGenerateOperation<E = never> = (input: Endpoint5_25Input) => Effect.Effect<Endpoint5_25Output, E>
|
||||
export type Endpoint5_25Output = StreamValue<EffectValue<ReturnType<RawClient["server.session"]["session.log"]>>>
|
||||
export type SessionLogOperation<E = never> = (input: Endpoint5_25Input) => Stream.Stream<Endpoint5_25Output, E>
|
||||
|
||||
type Endpoint5_26Request = Parameters<RawClient["server.session"]["session.log"]>[0]
|
||||
export type Endpoint5_26Input = {
|
||||
readonly sessionID: Endpoint5_26Request["params"]["sessionID"]
|
||||
readonly after?: Endpoint5_26Request["query"]["after"]
|
||||
readonly follow?: Endpoint5_26Request["query"]["follow"]
|
||||
}
|
||||
export type Endpoint5_26Output = StreamValue<EffectValue<ReturnType<RawClient["server.session"]["session.log"]>>>
|
||||
export type SessionLogOperation<E = never> = (input: Endpoint5_26Input) => Stream.Stream<Endpoint5_26Output, E>
|
||||
type Endpoint5_26Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
||||
export type Endpoint5_26Input = { readonly sessionID: Endpoint5_26Request["params"]["sessionID"] }
|
||||
export type Endpoint5_26Output = EffectValue<ReturnType<RawClient["server.session"]["session.interrupt"]>>
|
||||
export type SessionInterruptOperation<E = never> = (input: Endpoint5_26Input) => Effect.Effect<Endpoint5_26Output, E>
|
||||
|
||||
type Endpoint5_27Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
||||
type Endpoint5_27Request = Parameters<RawClient["server.session"]["session.background"]>[0]
|
||||
export type Endpoint5_27Input = { readonly sessionID: Endpoint5_27Request["params"]["sessionID"] }
|
||||
export type Endpoint5_27Output = EffectValue<ReturnType<RawClient["server.session"]["session.interrupt"]>>
|
||||
export type SessionInterruptOperation<E = never> = (input: Endpoint5_27Input) => Effect.Effect<Endpoint5_27Output, E>
|
||||
export type Endpoint5_27Output = EffectValue<ReturnType<RawClient["server.session"]["session.background"]>>
|
||||
export type SessionBackgroundOperation<E = never> = (input: Endpoint5_27Input) => Effect.Effect<Endpoint5_27Output, E>
|
||||
|
||||
type Endpoint5_28Request = Parameters<RawClient["server.session"]["session.background"]>[0]
|
||||
export type Endpoint5_28Input = { readonly sessionID: Endpoint5_28Request["params"]["sessionID"] }
|
||||
export type Endpoint5_28Output = EffectValue<ReturnType<RawClient["server.session"]["session.background"]>>
|
||||
export type SessionBackgroundOperation<E = never> = (input: Endpoint5_28Input) => Effect.Effect<Endpoint5_28Output, E>
|
||||
|
||||
type Endpoint5_29Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
||||
export type Endpoint5_29Input = {
|
||||
readonly sessionID: Endpoint5_29Request["params"]["sessionID"]
|
||||
readonly messageID: Endpoint5_29Request["params"]["messageID"]
|
||||
type Endpoint5_28Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
||||
export type Endpoint5_28Input = {
|
||||
readonly sessionID: Endpoint5_28Request["params"]["sessionID"]
|
||||
readonly messageID: Endpoint5_28Request["params"]["messageID"]
|
||||
}
|
||||
export type Endpoint5_29Output = EffectValue<ReturnType<RawClient["server.session"]["session.message"]>>["data"]
|
||||
export type SessionMessageOperation<E = never> = (input: Endpoint5_29Input) => Effect.Effect<Endpoint5_29Output, E>
|
||||
export type Endpoint5_28Output = EffectValue<ReturnType<RawClient["server.session"]["session.message"]>>["data"]
|
||||
export type SessionMessageOperation<E = never> = (input: Endpoint5_28Input) => Effect.Effect<Endpoint5_28Output, E>
|
||||
|
||||
export interface SessionApi<E = never> {
|
||||
readonly list: SessionListOperation<E>
|
||||
@@ -336,7 +328,6 @@ export interface SessionApi<E = never> {
|
||||
readonly remove: SessionInstructionsEntryRemoveOperation<E>
|
||||
}
|
||||
}
|
||||
readonly generate: SessionGenerateOperation<E>
|
||||
readonly log: SessionLogOperation<E>
|
||||
readonly interrupt: SessionInterruptOperation<E>
|
||||
readonly background: SessionBackgroundOperation<E>
|
||||
|
||||
@@ -361,24 +361,13 @@ const Endpoint5_24 = (raw: RawClient["server.session"]) => (input: Endpoint5_24I
|
||||
Effect.mapError(mapClientError),
|
||||
)
|
||||
|
||||
type Endpoint5_25Request = Parameters<RawClient["server.session"]["session.generate"]>[0]
|
||||
type Endpoint5_25Request = Parameters<RawClient["server.session"]["session.log"]>[0]
|
||||
type Endpoint5_25Input = {
|
||||
readonly sessionID: Endpoint5_25Request["params"]["sessionID"]
|
||||
readonly prompt: Endpoint5_25Request["payload"]["prompt"]
|
||||
readonly after?: Endpoint5_25Request["query"]["after"]
|
||||
readonly follow?: Endpoint5_25Request["query"]["follow"]
|
||||
}
|
||||
const Endpoint5_25 = (raw: RawClient["server.session"]) => (input: Endpoint5_25Input) =>
|
||||
raw["session.generate"]({ params: { sessionID: input["sessionID"] }, payload: { prompt: input["prompt"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint5_26Request = Parameters<RawClient["server.session"]["session.log"]>[0]
|
||||
type Endpoint5_26Input = {
|
||||
readonly sessionID: Endpoint5_26Request["params"]["sessionID"]
|
||||
readonly after?: Endpoint5_26Request["query"]["after"]
|
||||
readonly follow?: Endpoint5_26Request["query"]["follow"]
|
||||
}
|
||||
const Endpoint5_26 = (raw: RawClient["server.session"]) => (input: Endpoint5_26Input) =>
|
||||
Stream.unwrap(
|
||||
raw["session.log"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
@@ -389,22 +378,22 @@ const Endpoint5_26 = (raw: RawClient["server.session"]) => (input: Endpoint5_26I
|
||||
),
|
||||
)
|
||||
|
||||
type Endpoint5_27Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
||||
type Endpoint5_27Input = { readonly sessionID: Endpoint5_27Request["params"]["sessionID"] }
|
||||
const Endpoint5_27 = (raw: RawClient["server.session"]) => (input: Endpoint5_27Input) =>
|
||||
type Endpoint5_26Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
||||
type Endpoint5_26Input = { readonly sessionID: Endpoint5_26Request["params"]["sessionID"] }
|
||||
const Endpoint5_26 = (raw: RawClient["server.session"]) => (input: Endpoint5_26Input) =>
|
||||
raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint5_28Request = Parameters<RawClient["server.session"]["session.background"]>[0]
|
||||
type Endpoint5_28Input = { readonly sessionID: Endpoint5_28Request["params"]["sessionID"] }
|
||||
const Endpoint5_28 = (raw: RawClient["server.session"]) => (input: Endpoint5_28Input) =>
|
||||
type Endpoint5_27Request = Parameters<RawClient["server.session"]["session.background"]>[0]
|
||||
type Endpoint5_27Input = { readonly sessionID: Endpoint5_27Request["params"]["sessionID"] }
|
||||
const Endpoint5_27 = (raw: RawClient["server.session"]) => (input: Endpoint5_27Input) =>
|
||||
raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint5_29Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
||||
type Endpoint5_29Input = {
|
||||
readonly sessionID: Endpoint5_29Request["params"]["sessionID"]
|
||||
readonly messageID: Endpoint5_29Request["params"]["messageID"]
|
||||
type Endpoint5_28Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
||||
type Endpoint5_28Input = {
|
||||
readonly sessionID: Endpoint5_28Request["params"]["sessionID"]
|
||||
readonly messageID: Endpoint5_28Request["params"]["messageID"]
|
||||
}
|
||||
const Endpoint5_29 = (raw: RawClient["server.session"]) => (input: Endpoint5_29Input) =>
|
||||
const Endpoint5_28 = (raw: RawClient["server.session"]) => (input: Endpoint5_28Input) =>
|
||||
raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
@@ -432,11 +421,10 @@ const adaptGroup5 = (raw: RawClient["server.session"]) => ({
|
||||
context: Endpoint5_20(raw),
|
||||
pending: { list: Endpoint5_21(raw) },
|
||||
instructions: { entry: { list: Endpoint5_22(raw), put: Endpoint5_23(raw), remove: Endpoint5_24(raw) } },
|
||||
generate: Endpoint5_25(raw),
|
||||
log: Endpoint5_26(raw),
|
||||
interrupt: Endpoint5_27(raw),
|
||||
background: Endpoint5_28(raw),
|
||||
message: Endpoint5_29(raw),
|
||||
log: Endpoint5_25(raw),
|
||||
interrupt: Endpoint5_26(raw),
|
||||
background: Endpoint5_27(raw),
|
||||
message: Endpoint5_28(raw),
|
||||
})
|
||||
|
||||
type Endpoint6_0Request = Parameters<RawClient["server.message"]["session.messages"]>[0]
|
||||
|
||||
@@ -3,7 +3,12 @@ import { Effect, FileSystem, Option, Schedule, Schema } from "effect"
|
||||
import { spawn, type ChildProcess } from "node:child_process"
|
||||
import { homedir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import type { DiscoverOptions, Endpoint, EnsureOptions, StopOptions } from "../service.js"
|
||||
import type {
|
||||
DiscoverOptions,
|
||||
Endpoint,
|
||||
EnsureOptions,
|
||||
StopOptions,
|
||||
} from "../service.js"
|
||||
|
||||
export * from "../service.js"
|
||||
/** Contents of the local service registration file. */
|
||||
@@ -29,17 +34,6 @@ export const discover = Effect.fn("service.discover")(function* (options: Discov
|
||||
return (yield* discoverLocal(options))?.endpoint
|
||||
})
|
||||
|
||||
/** Recognize an authenticated compatible service bound to an expected URL, including while it starts or fails. */
|
||||
export const incumbent = Effect.fn("service.incumbent")(function* (
|
||||
options: DiscoverOptions & { readonly url: string },
|
||||
) {
|
||||
const info = yield* read(options.file)
|
||||
const found = info === undefined ? undefined : yield* probe({ ...info, url: options.url })
|
||||
if (found === undefined || found.legacy) return undefined
|
||||
if (options.version !== undefined && found.version !== options.version) return undefined
|
||||
return { endpoint: found.endpoint, state: found.state }
|
||||
})
|
||||
|
||||
const discoverLocal = Effect.fnUntraced(function* (options: DiscoverOptions) {
|
||||
const found = (yield* registered(options.file)).service
|
||||
if (found?.state !== "ready") return undefined
|
||||
@@ -88,8 +82,7 @@ export const ensure = Effect.fn("service.ensure")(function* (options: EnsureOpti
|
||||
spawnDelay = 5_000
|
||||
const compatible = !service.legacy && (options.version === undefined || service.version === options.version)
|
||||
if (compatible && service.state === "ready") return Option.some(service)
|
||||
if (compatible && service.state === "failed")
|
||||
return yield* Effect.fail(new Error("Background service failed to start"))
|
||||
if (compatible && service.state === "failed") return yield* Effect.fail(new Error("Background service failed to start"))
|
||||
if (compatible) return Option.none<LocalService>()
|
||||
yield* announce("version-mismatch", service.version)
|
||||
yield* kill(service, options).pipe(Effect.ignore)
|
||||
@@ -112,15 +105,8 @@ export const ensure = Effect.fn("service.ensure")(function* (options: EnsureOpti
|
||||
lastSpawn = Date.now()
|
||||
}
|
||||
return Option.none<LocalService>()
|
||||
}).pipe(
|
||||
Effect.repeat({
|
||||
until: Option.isSome,
|
||||
schedule: Schedule.max([Schedule.spaced("1 second"), Schedule.recurs(120)]),
|
||||
}),
|
||||
)
|
||||
if (Option.isNone(found))
|
||||
return yield* Effect.fail(new Error("Timed out waiting for the background service to start"))
|
||||
return found.value.endpoint
|
||||
}).pipe(Effect.repeat({ until: Option.isSome, schedule: Schedule.spaced("1 second") }))
|
||||
return Option.getOrThrow(found).endpoint
|
||||
})
|
||||
|
||||
function contenderFailure(contender: Contender) {
|
||||
@@ -235,7 +221,7 @@ const find = Effect.fnUntraced(function* (options: { readonly file?: string }) {
|
||||
|
||||
// 50ms cadence bounded at ~5s, shared by stop escalation and each ensure
|
||||
// discovery window.
|
||||
const poll = Schedule.max([Schedule.spaced("50 millis"), Schedule.recurs(100)])
|
||||
const poll = Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(100)))
|
||||
|
||||
const signal = (pid: number, name: NodeJS.Signals) =>
|
||||
Effect.try({ try: () => process.kill(pid, name), catch: (cause) => cause }).pipe(Effect.ignore)
|
||||
@@ -252,7 +238,10 @@ function same(left: Info, right: Info) {
|
||||
return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid
|
||||
}
|
||||
|
||||
const kill = Effect.fnUntraced(function* (service: LocalService, options: { readonly file?: string }) {
|
||||
const kill = Effect.fnUntraced(function* (
|
||||
service: LocalService,
|
||||
options: { readonly file?: string },
|
||||
) {
|
||||
const requested = yield* requestStop(service)
|
||||
if (requested === "rejected") return
|
||||
if (requested === "unsupported") {
|
||||
@@ -291,4 +280,4 @@ const requestStop = Effect.fnUntraced(function* (service: LocalService) {
|
||||
})
|
||||
|
||||
/** Effect-based local service lifecycle operations. */
|
||||
export const Service = { discover, incumbent, ensure, stop, headers, Info }
|
||||
export const Service = { discover, ensure, stop, headers, Info }
|
||||
|
||||
@@ -58,8 +58,6 @@ import type {
|
||||
SessionInstructionsEntryPutOutput,
|
||||
SessionInstructionsEntryRemoveInput,
|
||||
SessionInstructionsEntryRemoveOutput,
|
||||
SessionGenerateInput,
|
||||
SessionGenerateOutput,
|
||||
SessionLogInput,
|
||||
SessionLogOutput,
|
||||
SessionInterruptInput,
|
||||
@@ -745,18 +743,6 @@ export function make(options: ClientOptions) {
|
||||
),
|
||||
},
|
||||
},
|
||||
generate: (input: SessionGenerateInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: SessionGenerateOutput }>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/generate`,
|
||||
body: { prompt: input["prompt"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [404, 503, 400, 401],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
log: (input: SessionLogInput, requestOptions?: RequestOptions): AsyncIterable<SessionLogOutput> =>
|
||||
sse<SessionLogOutput>(
|
||||
{
|
||||
|
||||
@@ -134,8 +134,6 @@ export type SessionMessageCompactionCompleted = {
|
||||
|
||||
export type InstructionEntryKey = string
|
||||
|
||||
export type SessionGenerateResponse = { data: { text: string } }
|
||||
|
||||
export type SessionPendingSyntheticData1 = { text: string; description?: string; metadata?: { [x: string]: any } }
|
||||
|
||||
export type ShellInfo = {
|
||||
@@ -805,16 +803,6 @@ export type SessionRevertCommitted = {
|
||||
data: { sessionID: string; to: string }
|
||||
}
|
||||
|
||||
export type SessionUsageRecorded = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.usage.recorded"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; source: "title" | "compaction"; cost: MoneyUSD; tokens: TokenUsageInfo }
|
||||
}
|
||||
|
||||
export type ModelsDevRefreshed = {
|
||||
id: string
|
||||
created: number
|
||||
@@ -2228,7 +2216,6 @@ export type SessionEventDurable =
|
||||
| SessionRevertStaged
|
||||
| SessionRevertCleared
|
||||
| SessionRevertCommitted
|
||||
| SessionUsageRecorded
|
||||
|
||||
export type SessionMessagesResponse = {
|
||||
data: Array<SessionMessageInfo>
|
||||
@@ -3189,13 +3176,6 @@ export type SessionInstructionsEntryRemoveInput = {
|
||||
|
||||
export type SessionInstructionsEntryRemoveOutput = void
|
||||
|
||||
export type SessionGenerateInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
readonly prompt: { readonly prompt: string }["prompt"]
|
||||
}
|
||||
|
||||
export type SessionGenerateOutput = SessionGenerateResponse["data"]
|
||||
|
||||
export type SessionLogInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
readonly after?: { readonly after?: number | undefined; readonly follow?: boolean | undefined }["after"]
|
||||
|
||||
@@ -2,7 +2,13 @@ import { readFile } from "node:fs/promises"
|
||||
import { spawn, type ChildProcess } from "node:child_process"
|
||||
import { homedir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import type { DiscoverOptions, Endpoint, Info, EnsureOptions, StopOptions } from "../service.js"
|
||||
import type {
|
||||
DiscoverOptions,
|
||||
Endpoint,
|
||||
Info,
|
||||
EnsureOptions,
|
||||
StopOptions,
|
||||
} from "../service.js"
|
||||
import type { ServiceHealth, ServiceStopResponse } from "./generated/types.js"
|
||||
|
||||
export * from "../service.js"
|
||||
@@ -32,7 +38,6 @@ async function discoverLocal(options: DiscoverOptions) {
|
||||
|
||||
/** Ensure a healthy, compatible local service is running. */
|
||||
export async function ensure(options: EnsureOptions = {}): Promise<Endpoint> {
|
||||
const deadline = Date.now() + 120_000
|
||||
const contenders = new Set<Contender>()
|
||||
let announced = false
|
||||
let lastSpawn = 0
|
||||
@@ -61,7 +66,6 @@ export async function ensure(options: EnsureOptions = {}): Promise<Endpoint> {
|
||||
}
|
||||
|
||||
while (true) {
|
||||
if (Date.now() >= deadline) throw new Error("Timed out waiting for the background service to start")
|
||||
const registration = await registered(options.file, true)
|
||||
|
||||
if (registration.service !== undefined) {
|
||||
@@ -124,9 +128,7 @@ function fallback() {
|
||||
/** Create HTTP authentication headers for a service endpoint. */
|
||||
export function headers(endpoint: Endpoint) {
|
||||
if (endpoint.auth === undefined) return undefined
|
||||
return {
|
||||
authorization: "Basic " + Buffer.from(endpoint.auth.username + ":" + endpoint.auth.password).toString("base64"),
|
||||
}
|
||||
return { authorization: "Basic " + Buffer.from(endpoint.auth.username + ":" + endpoint.auth.password).toString("base64") }
|
||||
}
|
||||
|
||||
async function read(file?: string) {
|
||||
@@ -225,8 +227,7 @@ async function kill(service: LocalService, options: { readonly file?: string })
|
||||
const latest = await find(options)
|
||||
if (latest === undefined || !same(latest.info, service.info)) return
|
||||
signal(service.info.pid, "SIGKILL")
|
||||
if (!(await waitUntilStopped(service.info.pid)))
|
||||
throw new Error(`Server process ${service.info.pid} is still running`)
|
||||
if (!(await waitUntilStopped(service.info.pid))) throw new Error(`Server process ${service.info.pid} is still running`)
|
||||
}
|
||||
|
||||
async function requestStop(service: LocalService) {
|
||||
|
||||
@@ -379,7 +379,6 @@ test("session methods use the public HTTP contract", async () => {
|
||||
})
|
||||
}
|
||||
if (url.includes("/prompt")) return Response.json(admission)
|
||||
if (url.includes("/generate")) return Response.json({ data: { text: "A transient answer" } })
|
||||
if (url.includes("/synthetic")) return Response.json(syntheticAdmission)
|
||||
if (url.endsWith("/compact")) return Response.json(compactionAdmission)
|
||||
if (url.includes("/context")) return Response.json({ data: [] })
|
||||
@@ -404,7 +403,6 @@ test("session methods use the public HTTP contract", async () => {
|
||||
text: "Hello",
|
||||
resume: false,
|
||||
})
|
||||
const generated = await client.session.generate({ sessionID: "ses_test", prompt: "Summarize this session" })
|
||||
const synthetic = await client.session.synthetic({
|
||||
sessionID: "ses_test",
|
||||
text: "Completed",
|
||||
@@ -423,7 +421,6 @@ test("session methods use the public HTTP contract", async () => {
|
||||
expect(active).toEqual({ ses_test: { type: "running" } })
|
||||
expect(created.id).toBe("ses_test")
|
||||
expect(admitted.id).toBe("msg_test")
|
||||
expect(generated.text).toBe("A transient answer")
|
||||
expect(synthetic).toMatchObject({ type: "synthetic", data: { text: "Completed" }, delivery: "queue" })
|
||||
expect(context).toEqual([])
|
||||
expect(log).toEqual([modelSwitchedEvent, synced])
|
||||
@@ -435,7 +432,6 @@ test("session methods use the public HTTP contract", async () => {
|
||||
["POST", "http://localhost:3000/api/session/ses_test/agent"],
|
||||
["POST", "http://localhost:3000/api/session/ses_test/model"],
|
||||
["POST", "http://localhost:3000/api/session/ses_test/prompt"],
|
||||
["POST", "http://localhost:3000/api/session/ses_test/generate"],
|
||||
["POST", "http://localhost:3000/api/session/ses_test/synthetic"],
|
||||
["POST", "http://localhost:3000/api/session/ses_test/compact"],
|
||||
["POST", "http://localhost:3000/api/session/ses_test/wait"],
|
||||
|
||||
@@ -108,9 +108,7 @@ const runtime = CodeMode.make({ tools: { opencode: api.tools } })
|
||||
|
||||
It is synchronous and returns `{ tools, skipped }`: operations with unsupported encodings, non-JSON bodies, binary
|
||||
responses, or streaming land in `skipped` instead of producing broken tools. Auth is resolved host-side and never
|
||||
model-visible; generated tools require `HttpClient.HttpClient` in the environment. `readOnly` properties are omitted
|
||||
from request signatures and `writeOnly` properties from response signatures. These JSON Schemas are model-facing, not
|
||||
runtime filters: nested value bodies and server responses pass through unchanged. See the option docstrings in
|
||||
model-visible; generated tools require `HttpClient.HttpClient` in the environment. See the option docstrings in
|
||||
`src/openapi/types.ts` for full semantics.
|
||||
|
||||
## Outputs
|
||||
|
||||
@@ -50,7 +50,7 @@ ultimate source of truth.
|
||||
- [x] Direct function declarations are hoisted in program and block statement lists.
|
||||
- [x] Parameter defaults observe a temporal dead zone for later parameters.
|
||||
- [ ] JavaScript-correct function scoping, hoisting, and redeclaration for accepted `var` declarations.
|
||||
- [x] Predeclare `let` and `const` bindings in every lexical scope, including program/block bodies, switch bodies, and
|
||||
- [ ] Predeclare `let` and `const` bindings in every lexical scope, including program/block bodies, switch bodies, and
|
||||
loop headers, so reads before initialization and self- or cross-referential initializers observe the JavaScript
|
||||
temporal dead zone.
|
||||
- [ ] Hoist function declarations accepted directly in switch cases.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/codemode",
|
||||
"version": "1.18.3",
|
||||
"version": "1.18.2",
|
||||
"description": "Effect-native confined code execution over schema-described tools",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -144,20 +144,6 @@ const collectPatternNames = (pattern: AstNode, out: Array<string> = []): Array<s
|
||||
return out
|
||||
}
|
||||
|
||||
const loopDeclaration = (left: AstNode, statement: "for...of" | "for...in") => {
|
||||
if (left.type !== "VariableDeclaration") return undefined
|
||||
const declarations = getArray(left, "declarations")
|
||||
if (declarations.length !== 1) {
|
||||
throw new InterpreterRuntimeError(`${statement} supports one declared binding.`, left)
|
||||
}
|
||||
const kind = getString(left, "kind")
|
||||
return {
|
||||
pattern: getNode(asNode(declarations[0], "declarations[0]"), "id"),
|
||||
mutable: kind !== "const",
|
||||
lexical: kind !== "var",
|
||||
}
|
||||
}
|
||||
|
||||
export class Interpreter<R> {
|
||||
private scopes: ScopeStack
|
||||
private readonly invokeTool: (path: ReadonlyArray<string>, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>
|
||||
@@ -221,7 +207,6 @@ export class Interpreter<R> {
|
||||
// Keep top-level declarations separate so they can shadow builtins.
|
||||
this.scopes.push()
|
||||
return Effect.gen(function* () {
|
||||
self.predeclareLexical(program.body)
|
||||
self.hoistFunctions(program.body)
|
||||
let value: unknown = undefined
|
||||
for (const [index, statement] of program.body.entries()) {
|
||||
@@ -318,7 +303,6 @@ export class Interpreter<R> {
|
||||
const self = this
|
||||
return Effect.gen(function* () {
|
||||
const body = getArray(node, "body")
|
||||
self.predeclareLexical(body)
|
||||
self.hoistFunctions(body)
|
||||
|
||||
for (const statementValue of body) {
|
||||
@@ -359,25 +343,6 @@ export class Interpreter<R> {
|
||||
}
|
||||
}
|
||||
|
||||
private predeclareLexical(statements: Array<unknown>): void {
|
||||
for (const statementValue of statements) {
|
||||
if (!isRecord(statementValue) || statementValue.type !== "VariableDeclaration") continue
|
||||
const statement = statementValue as AstNode
|
||||
const kind = getString(statement, "kind")
|
||||
if (kind === "var") continue
|
||||
for (const declarationValue of getArray(statement, "declarations")) {
|
||||
const declaration = asNode(declarationValue, "declarations")
|
||||
for (const name of collectPatternNames(getNode(declaration, "id"))) {
|
||||
this.scopes.reserve(name, kind !== "const", declaration)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private predeclarePattern(pattern: AstNode, mutable: boolean, node: AstNode): void {
|
||||
for (const name of collectPatternNames(pattern)) this.scopes.reserve(name, mutable, node)
|
||||
}
|
||||
|
||||
private evaluateIfStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
|
||||
const testNode = getNode(node, "test")
|
||||
const consequentNode = getNode(node, "consequent")
|
||||
@@ -394,6 +359,7 @@ export class Interpreter<R> {
|
||||
|
||||
private evaluateSwitchStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
|
||||
const self = this
|
||||
this.scopes.push()
|
||||
return Effect.gen(function* () {
|
||||
const discriminant = yield* self.evaluateExpression(getNode(node, "discriminant"))
|
||||
if (containsOpaqueReference(discriminant)) {
|
||||
@@ -403,43 +369,39 @@ export class Interpreter<R> {
|
||||
"InvalidDataValue",
|
||||
)
|
||||
}
|
||||
self.scopes.push()
|
||||
return yield* Effect.gen(function* () {
|
||||
const cases = getArray(node, "cases").map((value, index) => asNode(value, `cases[${index}]`))
|
||||
self.predeclareLexical(cases.flatMap((branch) => getArray(branch, "consequent")))
|
||||
let defaultIndex: number | undefined
|
||||
let selected: number | undefined
|
||||
for (const [index, branch] of cases.entries()) {
|
||||
const test = getOptionalNode(branch, "test")
|
||||
if (!test) {
|
||||
defaultIndex = index
|
||||
continue
|
||||
}
|
||||
const candidate = yield* self.evaluateExpression(test)
|
||||
if (containsOpaqueReference(candidate)) {
|
||||
throw new InterpreterRuntimeError(
|
||||
"Switch case values must be data values in CodeMode.",
|
||||
test,
|
||||
"InvalidDataValue",
|
||||
)
|
||||
}
|
||||
if (candidate === discriminant) {
|
||||
selected = index
|
||||
break
|
||||
}
|
||||
const cases = getArray(node, "cases").map((value, index) => asNode(value, `cases[${index}]`))
|
||||
let defaultIndex: number | undefined
|
||||
let selected: number | undefined
|
||||
for (const [index, branch] of cases.entries()) {
|
||||
const test = getOptionalNode(branch, "test")
|
||||
if (!test) {
|
||||
defaultIndex = index
|
||||
continue
|
||||
}
|
||||
const start = selected ?? defaultIndex
|
||||
if (start === undefined) return { kind: "none" } satisfies StatementResult
|
||||
for (let index = start; index < cases.length; index += 1) {
|
||||
for (const statementValue of getArray(cases[index]!, "consequent")) {
|
||||
const result = yield* self.evaluateStatement(asNode(statementValue, "consequent"))
|
||||
if (result.kind === "break") return { kind: "none" } satisfies StatementResult
|
||||
if (result.kind === "return" || result.kind === "continue") return result
|
||||
}
|
||||
const candidate = yield* self.evaluateExpression(test)
|
||||
if (containsOpaqueReference(candidate)) {
|
||||
throw new InterpreterRuntimeError(
|
||||
"Switch case values must be data values in CodeMode.",
|
||||
test,
|
||||
"InvalidDataValue",
|
||||
)
|
||||
}
|
||||
return { kind: "none" } satisfies StatementResult
|
||||
}).pipe(Effect.ensuring(Effect.sync(() => self.scopes.pop())))
|
||||
})
|
||||
if (candidate === discriminant) {
|
||||
selected = index
|
||||
break
|
||||
}
|
||||
}
|
||||
const start = selected ?? defaultIndex
|
||||
if (start === undefined) return { kind: "none" } satisfies StatementResult
|
||||
for (let index = start; index < cases.length; index += 1) {
|
||||
for (const statementValue of getArray(cases[index]!, "consequent")) {
|
||||
const result = yield* self.evaluateStatement(asNode(statementValue, "consequent"))
|
||||
if (result.kind === "break") return { kind: "none" } satisfies StatementResult
|
||||
if (result.kind === "return" || result.kind === "continue") return result
|
||||
}
|
||||
}
|
||||
return { kind: "none" } satisfies StatementResult
|
||||
}).pipe(Effect.ensuring(Effect.sync(() => self.scopes.pop())))
|
||||
}
|
||||
|
||||
private evaluateWhileStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
|
||||
@@ -503,10 +465,6 @@ export class Interpreter<R> {
|
||||
const updateNode = getOptionalNode(node, "update")
|
||||
const bodyNode = getNode(node, "body")
|
||||
|
||||
if (initNode?.type === "VariableDeclaration" && getString(initNode, "kind") !== "var") {
|
||||
self.predeclareLexical([initNode])
|
||||
}
|
||||
|
||||
if (initNode) {
|
||||
if (initNode.type === "VariableDeclaration") {
|
||||
yield* self.evaluateVariableDeclaration(initNode)
|
||||
@@ -520,18 +478,21 @@ export class Interpreter<R> {
|
||||
? Array.from(self.scopes.current().keys())
|
||||
: []
|
||||
|
||||
const nextIteration = () => {
|
||||
if (perIterationBindings.length === 0) return
|
||||
const current = self.scopes.current()
|
||||
self.scopes.pop()
|
||||
self.scopes.push(
|
||||
new Map(perIterationBindings.map((name): [string, Binding] => [name, { ...current.get(name)! }])),
|
||||
)
|
||||
}
|
||||
nextIteration()
|
||||
|
||||
while (testNode ? yield* self.evaluateExpression(testNode) : true) {
|
||||
const result = yield* self.evaluateStatement(bodyNode)
|
||||
const iterationScope =
|
||||
perIterationBindings.length > 0
|
||||
? new Map(
|
||||
perIterationBindings.map((name): [string, Binding] => [name, { ...self.scopes.current().get(name)! }]),
|
||||
)
|
||||
: undefined
|
||||
if (iterationScope) self.scopes.push(iterationScope)
|
||||
const result = yield* self.evaluateStatement(bodyNode).pipe(
|
||||
Effect.ensuring(
|
||||
Effect.sync(() => {
|
||||
if (iterationScope) self.scopes.pop()
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
if (result.kind === "return") {
|
||||
return result
|
||||
@@ -541,7 +502,13 @@ export class Interpreter<R> {
|
||||
return { kind: "none" } satisfies StatementResult
|
||||
}
|
||||
|
||||
nextIteration()
|
||||
if (iterationScope) {
|
||||
const loopScope = self.scopes.current()
|
||||
for (const name of perIterationBindings) {
|
||||
loopScope.set(name, { ...iterationScope.get(name)! })
|
||||
}
|
||||
}
|
||||
|
||||
if (updateNode) {
|
||||
yield* self.evaluateExpression(updateNode)
|
||||
}
|
||||
@@ -560,13 +527,9 @@ export class Interpreter<R> {
|
||||
throw new InterpreterRuntimeError("for await...of is not supported.", node)
|
||||
}
|
||||
|
||||
const left = getNode(node, "left")
|
||||
const declared = loopDeclaration(left, "for...of")
|
||||
if (declared?.lexical) this.scopes.push()
|
||||
|
||||
const self = this
|
||||
return Effect.gen(function* () {
|
||||
if (declared?.lexical) self.predeclarePattern(declared.pattern, declared.mutable, left)
|
||||
const left = getNode(node, "left")
|
||||
const right = yield* self.evaluateExpression(getNode(node, "right"))
|
||||
const body = getNode(node, "body")
|
||||
|
||||
@@ -575,34 +538,40 @@ export class Interpreter<R> {
|
||||
throw new InterpreterRuntimeError("for...of requires an array, string, Map, or Set value in CodeMode.", node)
|
||||
}
|
||||
|
||||
let declaration: { readonly pattern: AstNode; readonly mutable: boolean } | undefined
|
||||
let assignment: AstNode | undefined
|
||||
|
||||
if (
|
||||
left.type !== "VariableDeclaration" &&
|
||||
(left.type === "Identifier" ||
|
||||
left.type === "MemberExpression" ||
|
||||
left.type === "ArrayPattern" ||
|
||||
left.type === "ObjectPattern")
|
||||
if (left.type === "VariableDeclaration") {
|
||||
const declarations = getArray(left, "declarations")
|
||||
if (declarations.length !== 1) {
|
||||
throw new InterpreterRuntimeError("for...of supports one declared binding.", left)
|
||||
}
|
||||
|
||||
const declarator = asNode(declarations[0], "declarations[0]")
|
||||
declaration = { pattern: getNode(declarator, "id"), mutable: getString(left, "kind") !== "const" }
|
||||
} else if (
|
||||
left.type === "Identifier" ||
|
||||
left.type === "MemberExpression" ||
|
||||
left.type === "ArrayPattern" ||
|
||||
left.type === "ObjectPattern"
|
||||
) {
|
||||
assignment = left
|
||||
} else if (left.type !== "VariableDeclaration") {
|
||||
} else {
|
||||
throw new InterpreterRuntimeError("Unsupported for...of binding.", left)
|
||||
}
|
||||
|
||||
for (const value of iterable) {
|
||||
const result = yield* Effect.gen(function* () {
|
||||
if (declared) {
|
||||
self.scopes.push()
|
||||
if (declared.lexical) self.predeclarePattern(declared.pattern, declared.mutable, left)
|
||||
yield* self.declarePattern(declared.pattern, value, declared.mutable, left, declared.lexical)
|
||||
} else if (assignment) {
|
||||
yield* self.assignPattern(assignment, value, left)
|
||||
}
|
||||
return yield* self.evaluateStatement(body)
|
||||
}).pipe(
|
||||
if (declaration) {
|
||||
self.scopes.push()
|
||||
yield* self.declarePattern(declaration.pattern, value, declaration.mutable, left)
|
||||
} else if (assignment) {
|
||||
yield* self.assignPattern(assignment, value, left)
|
||||
}
|
||||
|
||||
const result = yield* self.evaluateStatement(body).pipe(
|
||||
Effect.ensuring(
|
||||
Effect.sync(() => {
|
||||
if (declared) self.scopes.pop()
|
||||
if (declaration) self.scopes.pop()
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -612,7 +581,7 @@ export class Interpreter<R> {
|
||||
}
|
||||
|
||||
if (result.kind === "break") {
|
||||
return { kind: "none" } satisfies StatementResult
|
||||
return { kind: "none" }
|
||||
}
|
||||
|
||||
if (result.kind === "continue") {
|
||||
@@ -620,14 +589,8 @@ export class Interpreter<R> {
|
||||
}
|
||||
}
|
||||
|
||||
return { kind: "none" } satisfies StatementResult
|
||||
}).pipe(
|
||||
Effect.ensuring(
|
||||
Effect.sync(() => {
|
||||
if (declared?.lexical) self.scopes.pop()
|
||||
}),
|
||||
),
|
||||
)
|
||||
return { kind: "none" }
|
||||
})
|
||||
}
|
||||
|
||||
private enumerableKeys(value: unknown): Array<string> | undefined {
|
||||
@@ -644,13 +607,9 @@ export class Interpreter<R> {
|
||||
}
|
||||
|
||||
private evaluateForInStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
|
||||
const left = getNode(node, "left")
|
||||
const declared = loopDeclaration(left, "for...in")
|
||||
if (declared?.lexical) this.scopes.push()
|
||||
|
||||
const self = this
|
||||
return Effect.gen(function* () {
|
||||
if (declared?.lexical) self.predeclarePattern(declared.pattern, declared.mutable, left)
|
||||
const left = getNode(node, "left")
|
||||
const right = yield* self.evaluateExpression(getNode(node, "right"))
|
||||
const body = getNode(node, "body")
|
||||
|
||||
@@ -662,28 +621,35 @@ export class Interpreter<R> {
|
||||
)
|
||||
}
|
||||
|
||||
let declaration: { readonly pattern: AstNode; readonly mutable: boolean } | undefined
|
||||
let assignmentName: string | undefined
|
||||
|
||||
if (left.type === "Identifier") {
|
||||
if (left.type === "VariableDeclaration") {
|
||||
const declarations = getArray(left, "declarations")
|
||||
if (declarations.length !== 1) {
|
||||
throw new InterpreterRuntimeError("for...in supports one declared binding.", left)
|
||||
}
|
||||
|
||||
const declarator = asNode(declarations[0], "declarations[0]")
|
||||
declaration = { pattern: getNode(declarator, "id"), mutable: getString(left, "kind") !== "const" }
|
||||
} else if (left.type === "Identifier") {
|
||||
assignmentName = getString(left, "name")
|
||||
} else if (left.type !== "VariableDeclaration") {
|
||||
} else {
|
||||
throw new InterpreterRuntimeError("Unsupported for...in binding.", left)
|
||||
}
|
||||
|
||||
for (const key of keys) {
|
||||
const result = yield* Effect.gen(function* () {
|
||||
if (declared) {
|
||||
self.scopes.push()
|
||||
if (declared.lexical) self.predeclarePattern(declared.pattern, declared.mutable, left)
|
||||
yield* self.declarePattern(declared.pattern, key, declared.mutable, left, declared.lexical)
|
||||
} else if (assignmentName) {
|
||||
self.scopes.set(assignmentName, key, left)
|
||||
}
|
||||
return yield* self.evaluateStatement(body)
|
||||
}).pipe(
|
||||
if (declaration) {
|
||||
self.scopes.push()
|
||||
yield* self.declarePattern(declaration.pattern, key, declaration.mutable, left)
|
||||
} else if (assignmentName) {
|
||||
self.scopes.set(assignmentName, key, left)
|
||||
}
|
||||
|
||||
const result = yield* self.evaluateStatement(body).pipe(
|
||||
Effect.ensuring(
|
||||
Effect.sync(() => {
|
||||
if (declared) self.scopes.pop()
|
||||
if (declaration) self.scopes.pop()
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -693,7 +659,7 @@ export class Interpreter<R> {
|
||||
}
|
||||
|
||||
if (result.kind === "break") {
|
||||
return { kind: "none" } satisfies StatementResult
|
||||
return { kind: "none" }
|
||||
}
|
||||
|
||||
if (result.kind === "continue") {
|
||||
@@ -701,14 +667,8 @@ export class Interpreter<R> {
|
||||
}
|
||||
}
|
||||
|
||||
return { kind: "none" } satisfies StatementResult
|
||||
}).pipe(
|
||||
Effect.ensuring(
|
||||
Effect.sync(() => {
|
||||
if (declared?.lexical) self.scopes.pop()
|
||||
}),
|
||||
),
|
||||
)
|
||||
return { kind: "none" }
|
||||
})
|
||||
}
|
||||
|
||||
private evaluateBreakStatement(node: AstNode): StatementResult {
|
||||
@@ -792,7 +752,7 @@ export class Interpreter<R> {
|
||||
|
||||
const init = getOptionalNode(declaration, "init")
|
||||
const value = init ? yield* self.evaluateExpression(init) : undefined
|
||||
yield* self.declarePattern(getNode(declaration, "id"), value, kind !== "const", declaration, kind !== "var")
|
||||
yield* self.declarePattern(getNode(declaration, "id"), value, kind !== "const", declaration)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -802,20 +762,17 @@ export class Interpreter<R> {
|
||||
value: unknown,
|
||||
mutable: boolean,
|
||||
node: AstNode,
|
||||
initialize = false,
|
||||
): Effect.Effect<void, unknown, R> {
|
||||
const self = this
|
||||
return Effect.gen(function* () {
|
||||
if (pattern.type === "Identifier") {
|
||||
const name = getString(pattern, "name")
|
||||
if (initialize) self.scopes.initialize(name, value, node)
|
||||
else self.scopes.declare(name, value, mutable, node)
|
||||
self.scopes.declare(getString(pattern, "name"), value, mutable, node)
|
||||
return
|
||||
}
|
||||
|
||||
if (pattern.type === "AssignmentPattern") {
|
||||
const resolved = value === undefined ? yield* self.evaluateExpression(getNode(pattern, "right")) : value
|
||||
yield* self.declarePattern(getNode(pattern, "left"), resolved, mutable, node, initialize)
|
||||
yield* self.declarePattern(getNode(pattern, "left"), resolved, mutable, node)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -837,7 +794,7 @@ export class Interpreter<R> {
|
||||
for (const [key, item] of Object.entries(value as SafeObject)) {
|
||||
if (!consumed.has(key) && !isBlockedMember(key)) rest[key] = item
|
||||
}
|
||||
yield* self.declarePattern(getNode(property, "argument"), rest, mutable, property, initialize)
|
||||
yield* self.declarePattern(getNode(property, "argument"), rest, mutable, property)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -851,7 +808,6 @@ export class Interpreter<R> {
|
||||
self.destructuringPropertyValue(value as SafeObject | Array<unknown>, key),
|
||||
mutable,
|
||||
property,
|
||||
initialize,
|
||||
)
|
||||
}
|
||||
return
|
||||
@@ -867,10 +823,10 @@ export class Interpreter<R> {
|
||||
if (item === null) continue
|
||||
const element = asNode(item, `elements[${index}]`)
|
||||
if (element.type === "RestElement") {
|
||||
yield* self.declarePattern(getNode(element, "argument"), items.slice(index), mutable, element, initialize)
|
||||
yield* self.declarePattern(getNode(element, "argument"), items.slice(index), mutable, element)
|
||||
break
|
||||
}
|
||||
yield* self.declarePattern(element, items[index], mutable, pattern, initialize)
|
||||
yield* self.declarePattern(element, items[index], mutable, pattern)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1621,10 +1577,10 @@ export class Interpreter<R> {
|
||||
}
|
||||
for (const [index, parameter] of fn.parameters.entries()) {
|
||||
if (parameter.type === "RestElement") {
|
||||
yield* invocation.declarePattern(getNode(parameter, "argument"), args.slice(index), true, parameter, true)
|
||||
yield* invocation.declarePattern(getNode(parameter, "argument"), args.slice(index), true, parameter)
|
||||
break
|
||||
}
|
||||
yield* invocation.declarePattern(parameter, args[index], true, parameter, true)
|
||||
yield* invocation.declarePattern(parameter, args[index], true, parameter)
|
||||
}
|
||||
|
||||
if (fn.body.type === "BlockStatement") {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user