mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-15 17:08:21 -04:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 00ad2bed67 | |||
| c4830c147d | |||
| 22c23cb44c | |||
| 60ce33cde8 | |||
| 6ea8247e0f | |||
| 987242b3e8 | |||
| fafb04ed59 | |||
| 87d5b27668 | |||
| c7a7900ff2 | |||
| f2a4011371 | |||
| 4bc8faa01c | |||
| 5d5b33f195 | |||
| 3fc06accbd | |||
| 677526d72e | |||
| bce9c639d1 | |||
| 48adb9521c | |||
| 9b3a4655f1 | |||
| bd79e24842 | |||
| d67e035c18 | |||
| f8470404e3 | |||
| 651751405d | |||
| 44f7bb71c1 | |||
| b26d948b76 | |||
| 2180fb8e7b | |||
| 3db4458e6c | |||
| 42e2dde739 | |||
| a1b274e6f8 | |||
| 27e1692848 | |||
| 73567f3570 | |||
| 91238441a6 | |||
| faf964691b | |||
| 4a7f760d25 | |||
| f76e18201a | |||
| 331533deff | |||
| 309860558d | |||
| b4a4ef0b3c | |||
| e916b99742 | |||
| 88b8883122 | |||
| ac1b802820 | |||
| 5ee4c1082a | |||
| 8a70d70006 | |||
| 91b4634363 | |||
| 4f60bde502 | |||
| 0d3b6d430e |
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@opencode-ai/cli": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Expose a TUI plugin slot at the top of the session view.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@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.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@opencode-ai/cli": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Expose a TUI plugin slot above the session composer.
|
||||||
@@ -121,6 +121,55 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
version: ${{ needs.version.outputs.version }}
|
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:
|
sign-cli-windows:
|
||||||
needs:
|
needs:
|
||||||
- build-cli
|
- build-cli
|
||||||
@@ -413,6 +462,7 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- version
|
- version
|
||||||
- build-cli
|
- build-cli
|
||||||
|
- build-node-cli
|
||||||
- sign-cli-windows
|
- sign-cli-windows
|
||||||
- build-electron
|
- build-electron
|
||||||
if: always() && !failure() && !cancelled()
|
if: always() && !failure() && !cancelled()
|
||||||
@@ -461,6 +511,12 @@ jobs:
|
|||||||
name: opencode-preview-cli
|
name: opencode-preview-cli
|
||||||
path: packages/cli/dist
|
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
|
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||||
if: needs.version.outputs.release
|
if: needs.version.outputs.release
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -78,6 +78,20 @@ jobs:
|
|||||||
bun run script/build.ts --single --skip-install
|
bun run script/build.ts --single --skip-install
|
||||||
bun run script/service-smoke.ts
|
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
|
- name: Check generated client
|
||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
working-directory: packages/client
|
working-directory: packages/client
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ node_modules
|
|||||||
playground
|
playground
|
||||||
tmp
|
tmp
|
||||||
dist
|
dist
|
||||||
|
dist-node
|
||||||
ts-dist
|
ts-dist
|
||||||
.turbo
|
.turbo
|
||||||
.typecheck-profiles
|
.typecheck-profiles
|
||||||
@@ -25,6 +26,7 @@ Session.vim
|
|||||||
a.out
|
a.out
|
||||||
target
|
target
|
||||||
.scripts
|
.scripts
|
||||||
|
.cache
|
||||||
.direnv/
|
.direnv/
|
||||||
|
|
||||||
# Local dev files
|
# Local dev files
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@corvu/drawer": "catalog:",
|
"@corvu/drawer": "catalog:",
|
||||||
"@dnd-kit/abstract": "0.5.0",
|
"@dnd-kit/abstract": "0.5.0",
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
},
|
},
|
||||||
"packages/cli": {
|
"packages/cli": {
|
||||||
"name": "@opencode-ai/cli",
|
"name": "@opencode-ai/cli",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode2": "./bin/opencode2.cjs",
|
"opencode2": "./bin/opencode2.cjs",
|
||||||
},
|
},
|
||||||
@@ -145,14 +145,28 @@
|
|||||||
"solid-js": "catalog:",
|
"solid-js": "catalog:",
|
||||||
"strip-ansi": "7.1.2",
|
"strip-ansi": "7.1.2",
|
||||||
"uqr": "0.1.3",
|
"uqr": "0.1.3",
|
||||||
|
"ws": "8.21.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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/protocol": "workspace:*",
|
||||||
"@opencode-ai/script": "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:",
|
"@tsconfig/bun": "catalog:",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
"@types/semver": "catalog:",
|
"@types/semver": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
|
"vite": "catalog:",
|
||||||
|
"vite-plugin-solid": "catalog:",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/client": {
|
"packages/client": {
|
||||||
@@ -171,7 +185,7 @@
|
|||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"effect": "4.0.0-beta.83",
|
"effect": "4.0.0-beta.98",
|
||||||
},
|
},
|
||||||
"optionalPeers": [
|
"optionalPeers": [
|
||||||
"effect",
|
"effect",
|
||||||
@@ -179,7 +193,7 @@
|
|||||||
},
|
},
|
||||||
"packages/codemode": {
|
"packages/codemode": {
|
||||||
"name": "@opencode-ai/codemode",
|
"name": "@opencode-ai/codemode",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"acorn": "8.15.0",
|
"acorn": "8.15.0",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -193,7 +207,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
@@ -229,7 +243,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-sts": "3.782.0",
|
"@aws-sdk/client-sts": "3.782.0",
|
||||||
"@jsx-email/render": "1.1.1",
|
"@jsx-email/render": "1.1.1",
|
||||||
@@ -256,7 +270,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "3.0.82",
|
"@ai-sdk/anthropic": "3.0.82",
|
||||||
"@ai-sdk/openai": "3.0.48",
|
"@ai-sdk/openai": "3.0.48",
|
||||||
@@ -278,7 +292,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
@@ -302,7 +316,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/support": {
|
"packages/console/support": {
|
||||||
"name": "@opencode-ai/console-support",
|
"name": "@opencode-ai/console-support",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@opencode-ai/console-core": "workspace:*",
|
"@opencode-ai/console-core": "workspace:*",
|
||||||
@@ -322,7 +336,7 @@
|
|||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@opencode-ai/core",
|
"name": "@opencode-ai/core",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -387,6 +401,7 @@
|
|||||||
"mime-types": "3.0.2",
|
"mime-types": "3.0.2",
|
||||||
"minimatch": "10.2.5",
|
"minimatch": "10.2.5",
|
||||||
"npm-package-arg": "13.0.2",
|
"npm-package-arg": "13.0.2",
|
||||||
|
"resolve.exports": "catalog:",
|
||||||
"semver": "^7.6.3",
|
"semver": "^7.6.3",
|
||||||
"turndown": "7.2.0",
|
"turndown": "7.2.0",
|
||||||
"venice-ai-sdk-provider": "2.1.1",
|
"venice-ai-sdk-provider": "2.1.1",
|
||||||
@@ -418,7 +433,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@zip.js/zip.js": "2.7.62",
|
"@zip.js/zip.js": "2.7.62",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -478,7 +493,7 @@
|
|||||||
},
|
},
|
||||||
"packages/effect-drizzle-sqlite": {
|
"packages/effect-drizzle-sqlite": {
|
||||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
@@ -492,7 +507,7 @@
|
|||||||
},
|
},
|
||||||
"packages/effect-sqlite-node": {
|
"packages/effect-sqlite-node": {
|
||||||
"name": "@opencode-ai/effect-sqlite-node",
|
"name": "@opencode-ai/effect-sqlite-node",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -504,7 +519,7 @@
|
|||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hono/standard-validator": "catalog:",
|
"@hono/standard-validator": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
@@ -536,7 +551,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
@@ -552,9 +567,9 @@
|
|||||||
},
|
},
|
||||||
"packages/http-recorder": {
|
"packages/http-recorder": {
|
||||||
"name": "@opencode-ai/http-recorder",
|
"name": "@opencode-ai/http-recorder",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform-node-shared": "4.0.0-beta.83",
|
"@effect/platform-node-shared": "4.0.0-beta.98",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
@@ -584,7 +599,7 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -717,7 +732,7 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "3.0.8",
|
"@ai-sdk/provider": "3.0.8",
|
||||||
"@opencode-ai/ai": "workspace:*",
|
"@opencode-ai/ai": "workspace:*",
|
||||||
@@ -739,9 +754,9 @@
|
|||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@opentui/core": ">=0.4.3",
|
"@opentui/core": ">=0.4.5",
|
||||||
"@opentui/keymap": ">=0.4.3",
|
"@opentui/keymap": ">=0.4.5",
|
||||||
"@opentui/solid": ">=0.4.3",
|
"@opentui/solid": ">=0.4.5",
|
||||||
},
|
},
|
||||||
"optionalPeers": [
|
"optionalPeers": [
|
||||||
"@opentui/core",
|
"@opentui/core",
|
||||||
@@ -806,7 +821,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-spawn": "catalog:",
|
"cross-spawn": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -821,7 +836,7 @@
|
|||||||
},
|
},
|
||||||
"packages/server": {
|
"packages/server": {
|
||||||
"name": "@opencode-ai/server",
|
"name": "@opencode-ai/server",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
@@ -838,7 +853,7 @@
|
|||||||
},
|
},
|
||||||
"packages/session-ui": {
|
"packages/session-ui": {
|
||||||
"name": "@opencode-ai/session-ui",
|
"name": "@opencode-ai/session-ui",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
@@ -899,7 +914,7 @@
|
|||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -912,7 +927,7 @@
|
|||||||
},
|
},
|
||||||
"packages/stats/app": {
|
"packages/stats/app": {
|
||||||
"name": "@opencode-ai/stats-app",
|
"name": "@opencode-ai/stats-app",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
@@ -946,7 +961,7 @@
|
|||||||
},
|
},
|
||||||
"packages/stats/core": {
|
"packages/stats/core": {
|
||||||
"name": "@opencode-ai/stats-core",
|
"name": "@opencode-ai/stats-core",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-athena": "3.933.0",
|
"@aws-sdk/client-athena": "3.933.0",
|
||||||
"@planetscale/database": "1.19.0",
|
"@planetscale/database": "1.19.0",
|
||||||
@@ -965,7 +980,7 @@
|
|||||||
},
|
},
|
||||||
"packages/stats/server": {
|
"packages/stats/server": {
|
||||||
"name": "@opencode-ai/stats-server",
|
"name": "@opencode-ai/stats-server",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-firehose": "3.933.0",
|
"@aws-sdk/client-firehose": "3.933.0",
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
@@ -1006,7 +1021,7 @@
|
|||||||
},
|
},
|
||||||
"packages/tui": {
|
"packages/tui": {
|
||||||
"name": "@opencode-ai/tui",
|
"name": "@opencode-ai/tui",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/client": "workspace:*",
|
"@opencode-ai/client": "workspace:*",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
@@ -1021,10 +1036,12 @@
|
|||||||
"diff": "catalog:",
|
"diff": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"fuzzysort": "catalog:",
|
"fuzzysort": "catalog:",
|
||||||
|
"get-east-asian-width": "catalog:",
|
||||||
"open": "10.1.2",
|
"open": "10.1.2",
|
||||||
"opentui-spinner": "catalog:",
|
"opentui-spinner": "catalog:",
|
||||||
"remeda": "catalog:",
|
"remeda": "catalog:",
|
||||||
"solid-js": "catalog:",
|
"solid-js": "catalog:",
|
||||||
|
"string-width": "catalog:",
|
||||||
"strip-ansi": "7.1.2",
|
"strip-ansi": "7.1.2",
|
||||||
"uqr": "0.1.3",
|
"uqr": "0.1.3",
|
||||||
},
|
},
|
||||||
@@ -1036,7 +1053,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
@@ -1087,7 +1104,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
@@ -1163,12 +1180,12 @@
|
|||||||
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
||||||
"gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.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",
|
"@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",
|
"@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",
|
"@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",
|
"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",
|
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
|
||||||
"pacote@21.5.0": "patches/pacote@21.5.0.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": {
|
"overrides": {
|
||||||
"@opentui/core": "catalog:",
|
"@opentui/core": "catalog:",
|
||||||
@@ -1180,9 +1197,9 @@
|
|||||||
"catalog": {
|
"catalog": {
|
||||||
"@cloudflare/workers-types": "4.20251008.0",
|
"@cloudflare/workers-types": "4.20251008.0",
|
||||||
"@corvu/drawer": "0.2.4",
|
"@corvu/drawer": "0.2.4",
|
||||||
"@effect/opentelemetry": "4.0.0-beta.83",
|
"@effect/opentelemetry": "4.0.0-beta.98",
|
||||||
"@effect/platform-node": "4.0.0-beta.83",
|
"@effect/platform-node": "4.0.0-beta.98",
|
||||||
"@effect/sql-sqlite-bun": "4.0.0-beta.83",
|
"@effect/sql-sqlite-bun": "4.0.0-beta.98",
|
||||||
"@hono/standard-validator": "0.2.0",
|
"@hono/standard-validator": "0.2.0",
|
||||||
"@hono/zod-validator": "0.4.2",
|
"@hono/zod-validator": "0.4.2",
|
||||||
"@kobalte/core": "0.13.11",
|
"@kobalte/core": "0.13.11",
|
||||||
@@ -1190,9 +1207,9 @@
|
|||||||
"@npmcli/arborist": "9.4.0",
|
"@npmcli/arborist": "9.4.0",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||||
"@opentui/core": "0.4.3",
|
"@opentui/core": "0.4.5",
|
||||||
"@opentui/keymap": "0.4.3",
|
"@opentui/keymap": "0.4.5",
|
||||||
"@opentui/solid": "0.4.3",
|
"@opentui/solid": "0.4.5",
|
||||||
"@pierre/diffs": "1.2.10",
|
"@pierre/diffs": "1.2.10",
|
||||||
"@playwright/test": "1.59.1",
|
"@playwright/test": "1.59.1",
|
||||||
"@sentry/solid": "10.36.0",
|
"@sentry/solid": "10.36.0",
|
||||||
@@ -1218,8 +1235,9 @@
|
|||||||
"dompurify": "3.3.1",
|
"dompurify": "3.3.1",
|
||||||
"drizzle-kit": "1.0.0-rc.2",
|
"drizzle-kit": "1.0.0-rc.2",
|
||||||
"drizzle-orm": "1.0.0-rc.2",
|
"drizzle-orm": "1.0.0-rc.2",
|
||||||
"effect": "4.0.0-beta.83",
|
"effect": "4.0.0-beta.98",
|
||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
|
"get-east-asian-width": "1.6.0",
|
||||||
"hono": "4.10.7",
|
"hono": "4.10.7",
|
||||||
"hono-openapi": "1.1.2",
|
"hono-openapi": "1.1.2",
|
||||||
"luxon": "3.6.1",
|
"luxon": "3.6.1",
|
||||||
@@ -1228,11 +1246,13 @@
|
|||||||
"opentui-spinner": "0.0.7",
|
"opentui-spinner": "0.0.7",
|
||||||
"remeda": "2.26.0",
|
"remeda": "2.26.0",
|
||||||
"remend": "1.3.0",
|
"remend": "1.3.0",
|
||||||
|
"resolve.exports": "2.0.3",
|
||||||
"semver": "7.7.4",
|
"semver": "7.7.4",
|
||||||
"shiki": "4.2.0",
|
"shiki": "4.2.0",
|
||||||
"solid-js": "1.9.10",
|
"solid-js": "1.9.10",
|
||||||
"solid-list": "0.3.0",
|
"solid-list": "0.3.0",
|
||||||
"sst": "4.13.1",
|
"sst": "4.13.1",
|
||||||
|
"string-width": "7.2.0",
|
||||||
"tailwindcss": "4.1.11",
|
"tailwindcss": "4.1.11",
|
||||||
"typescript": "5.8.2",
|
"typescript": "5.8.2",
|
||||||
"ulid": "3.0.1",
|
"ulid": "3.0.1",
|
||||||
@@ -1619,13 +1639,13 @@
|
|||||||
|
|
||||||
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="],
|
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="],
|
||||||
|
|
||||||
"@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/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/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": ["@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-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/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/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.83", "", { "peerDependencies": { "effect": "^4.0.0-beta.83" } }, "sha512-6OaxLsWffxkh9pXYUSyj/AxjVb9URY2rG9U6atjxClWy30Jx77R9Pm3Rrc7cQ63kQurePavEw1bQbzQ/SILiQQ=="],
|
"@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=="],
|
||||||
|
|
||||||
"@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=="],
|
"@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=="],
|
||||||
|
|
||||||
@@ -2273,27 +2293,27 @@
|
|||||||
|
|
||||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="],
|
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="],
|
||||||
|
|
||||||
"@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": ["@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-darwin-arm64": ["@opentui/core-darwin-arm64@0.4.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-p5+7AAxpxGuDGagyQfewKtmTFnN7THvTVY4FyKqUtJomNaHdQXPHztapNNzMx0DGWbwOUbVKzpL+yc3CZY3chQ=="],
|
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.4.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-8KUG0oRidnR+oW1RSZJ72/PhZLl+qRRMk5U/mieF4c0SJ5V3tYACpBZAKzQfHNd1f7QzD8FHZct1lPpQgtmkWg=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.4.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-+fh0vEUE0lwVC7RW5ijYLRlTLp5NfvCRj8SzxDVd7IL2j2ssB6YXcfIbXq2EW7UGnrejwPRXf1tgUrIXW9KmOw=="],
|
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.4.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-R2bocsg55gwjOqCp/MWFgFYzRmsduKegB6nzgFAPCvAD/L5Jf30xpWJWFlSg3x8vxe1L9WJ84dfqa4M7mZZ3wA=="],
|
||||||
|
|
||||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.4.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-gl6qA5QJy6u8Cbt7gOtHbhhfMZ4qQDb0kEwFXHcMGmbnKzz4OHoq74D6tNjyvSQB9saoC7C6C0tvn2DcJOuNog=="],
|
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-R4MZ25a4CzOAGVjW9aj1hUfzQGVfCJwrwBDbNs2SXaIvzcZqkxCVtU4FoQ5LsaD0j/BdNQVg2CIfFkFsm1fDuQ=="],
|
||||||
|
|
||||||
"@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-arm64-musl": ["@opentui/core-linux-arm64-musl@0.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ieqdyKI6EIYPalYAETB2wsdP83hr5Ifi+dFnBFUmdEEFHsoKwBmn2S7bsTOYlX7Bg03F4/YPIg+IvRpeC+cUJw=="],
|
||||||
|
|
||||||
"@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": ["@opentui/core-linux-x64@0.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-SNyuQoxMKI1vuJhgxSSW96adWM6LqFl2SoS3GM4tGeneGOanVVG2Y06PvlytXvF4cKik97t0rqkVMRetmOs93w=="],
|
||||||
|
|
||||||
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.4.3", "", { "os": "linux", "cpu": "x64" }, "sha512-/QiFpCrpU2O7vy8QYmLIQYbvAtKDgmqcVjR7dGtqSzkiQk3ktNJoo5RozG7ueXnjung1Wp0nKldKxo2Csg/OrA=="],
|
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-mKVKcIcPiSVVZZsdPSBoWwoa2/TCeQAaMDeHF7PFw2kt5bTXZPP7xxWfRQLCNIcA1eaGl59UuwUWHDR2Ve548Q=="],
|
||||||
|
|
||||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.4.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-Mx2zuOjrhm/z2SDS6RExIyjP/SnN/8QhhagxURUw0jQi/NssGSeAllu1cBAFFnhobJL5QLTE4FU4CRhUK9svgg=="],
|
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.4.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-GHTTsqeR45q2Iek9Rb7ty+x/hAKn2jZ1ujlCgPR8LBKyF7h0E1dNFryoZ7ehMc3kJndP1sKn836IemKFqxuDdQ=="],
|
||||||
|
|
||||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.4.3", "", { "os": "win32", "cpu": "x64" }, "sha512-NuoqvWKGXaYnmlqvu7Gg2lLI6yVMnS9OfWBvxp+7Q+McSgHFSTQmYBXaPpvQ8HikpQXE1nCeMPtuSG4PdZHe2w=="],
|
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.4.5", "", { "os": "win32", "cpu": "x64" }, "sha512-Y8T/yXCDGagRGiQrtmuB6AhRcPucKFs/Dre3v8kJwNYqDccI4FzUPKclZ7djfmRZNjl7JUqPhZZP/PwDpQocMg=="],
|
||||||
|
|
||||||
"@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/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/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=="],
|
"@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=="],
|
||||||
|
|
||||||
"@orama/orama": ["@orama/orama@3.1.18", "", {}, "sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA=="],
|
"@orama/orama": ["@orama/orama@3.1.18", "", {}, "sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA=="],
|
||||||
|
|
||||||
@@ -4017,7 +4037,7 @@
|
|||||||
|
|
||||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||||
|
|
||||||
"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=="],
|
"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=="],
|
||||||
|
|
||||||
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
||||||
|
|
||||||
@@ -4193,7 +4213,7 @@
|
|||||||
|
|
||||||
"extsprintf": ["extsprintf@1.4.1", "", {}, "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA=="],
|
"extsprintf": ["extsprintf@1.4.1", "", {}, "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA=="],
|
||||||
|
|
||||||
"fast-check": ["fast-check@4.8.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg=="],
|
"fast-check": ["fast-check@4.9.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg=="],
|
||||||
|
|
||||||
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
|
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
|
||||||
|
|
||||||
@@ -5057,7 +5077,7 @@
|
|||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
"msgpackr": ["msgpackr@2.0.2", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-c5hYOXFbP79Slh6Dzd2wzk+jnV7mX1UxfMYtilnY1NmalXPqG8DGb5cYCMBrW4AsH3zekBBZd4QrKz9NhtvYLQ=="],
|
"msgpackr": ["msgpackr@2.0.4", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA=="],
|
||||||
|
|
||||||
"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=="],
|
"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=="],
|
||||||
|
|
||||||
@@ -5065,7 +5085,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=="],
|
"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.7", "", {}, "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA=="],
|
"multipasta": ["multipasta@0.2.8", "", {}, "sha512-ZPWuMKyv0cSO29f7hozp+k6+crZbQijV8ipMvxNxRf2SwtYGTX1ZX89Kd20VV4H9Znonx+EQn+iy1wGQsJ+b+Q=="],
|
||||||
|
|
||||||
"mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="],
|
"mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="],
|
||||||
|
|
||||||
@@ -5595,6 +5615,8 @@
|
|||||||
|
|
||||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
"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=="],
|
"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=="],
|
"restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="],
|
||||||
@@ -5963,7 +5985,7 @@
|
|||||||
|
|
||||||
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||||
|
|
||||||
"toml": ["toml@4.1.1", "", {}, "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw=="],
|
"toml": ["toml@4.3.0", "", {}, "sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A=="],
|
||||||
|
|
||||||
"toolbeam-docs-theme": ["toolbeam-docs-theme@0.4.8", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-b+5ynEFp4Woe5a22hzNQm42lD23t13ZMihVxHbzjA50zdcM9aOSJTIjdJ0PDSd4/50HbBXcpHiQsz6rM4N88ww=="],
|
"toolbeam-docs-theme": ["toolbeam-docs-theme@0.4.8", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-b+5ynEFp4Woe5a22hzNQm42lD23t13ZMihVxHbzjA50zdcM9aOSJTIjdJ0PDSd4/50HbBXcpHiQsz6rM4N88ww=="],
|
||||||
|
|
||||||
@@ -6049,7 +6071,7 @@
|
|||||||
|
|
||||||
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
|
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
|
||||||
|
|
||||||
"undici": ["undici@8.3.0", "", {}, "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q=="],
|
"undici": ["undici@8.7.0", "", {}, "sha512-N7iQtfyLhIMOFgQubvmLV26svHpO0bqKnAiWotTQCVKCmWrcGbBotPuW1x+xwYZ2VHdSTVUfPQQnlEt1/LouTQ=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||||
|
|
||||||
@@ -6131,7 +6153,7 @@
|
|||||||
|
|
||||||
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
|
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
|
||||||
|
|
||||||
"uuid": ["uuid@14.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg=="],
|
"uuid": ["uuid@14.0.1", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew=="],
|
||||||
|
|
||||||
"valibot": ["valibot@1.4.1", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-klCmFTz2jeDluy9RwX+F884TCiogtdBJ/YaxSx1EOBYXa3NXNWj8kR1jjN8rzluwojJVWWaHJ4r1U5LfICnM3g=="],
|
"valibot": ["valibot@1.4.1", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-klCmFTz2jeDluy9RwX+F884TCiogtdBJ/YaxSx1EOBYXa3NXNWj8kR1jjN8rzluwojJVWWaHJ4r1U5LfICnM3g=="],
|
||||||
|
|
||||||
@@ -8341,6 +8363,26 @@
|
|||||||
|
|
||||||
"@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=="],
|
"@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=="],
|
"@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=="],
|
"@storybook/addon-docs/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
||||||
|
|||||||
+11
-8
@@ -37,18 +37,18 @@
|
|||||||
"packages/slack"
|
"packages/slack"
|
||||||
],
|
],
|
||||||
"catalog": {
|
"catalog": {
|
||||||
"@effect/opentelemetry": "4.0.0-beta.83",
|
"@effect/opentelemetry": "4.0.0-beta.98",
|
||||||
"@effect/platform-node": "4.0.0-beta.83",
|
"@effect/platform-node": "4.0.0-beta.98",
|
||||||
"@effect/sql-sqlite-bun": "4.0.0-beta.83",
|
"@effect/sql-sqlite-bun": "4.0.0-beta.98",
|
||||||
"@npmcli/arborist": "9.4.0",
|
"@npmcli/arborist": "9.4.0",
|
||||||
"@types/bun": "1.3.13",
|
"@types/bun": "1.3.13",
|
||||||
"@types/cross-spawn": "6.0.6",
|
"@types/cross-spawn": "6.0.6",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
"@hono/standard-validator": "0.2.0",
|
"@hono/standard-validator": "0.2.0",
|
||||||
"@hono/zod-validator": "0.4.2",
|
"@hono/zod-validator": "0.4.2",
|
||||||
"@opentui/core": "0.4.3",
|
"@opentui/core": "0.4.5",
|
||||||
"@opentui/keymap": "0.4.3",
|
"@opentui/keymap": "0.4.5",
|
||||||
"@opentui/solid": "0.4.3",
|
"@opentui/solid": "0.4.5",
|
||||||
"@tanstack/solid-virtual": "3.13.32",
|
"@tanstack/solid-virtual": "3.13.32",
|
||||||
"@shikijs/stream": "4.2.0",
|
"@shikijs/stream": "4.2.0",
|
||||||
"ulid": "3.0.1",
|
"ulid": "3.0.1",
|
||||||
@@ -69,12 +69,13 @@
|
|||||||
"dompurify": "3.3.1",
|
"dompurify": "3.3.1",
|
||||||
"drizzle-kit": "1.0.0-rc.2",
|
"drizzle-kit": "1.0.0-rc.2",
|
||||||
"drizzle-orm": "1.0.0-rc.2",
|
"drizzle-orm": "1.0.0-rc.2",
|
||||||
"effect": "4.0.0-beta.83",
|
"effect": "4.0.0-beta.98",
|
||||||
"ai": "6.0.168",
|
"ai": "6.0.168",
|
||||||
"cross-spawn": "7.0.6",
|
"cross-spawn": "7.0.6",
|
||||||
"hono": "4.10.7",
|
"hono": "4.10.7",
|
||||||
"hono-openapi": "1.1.2",
|
"hono-openapi": "1.1.2",
|
||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
|
"get-east-asian-width": "1.6.0",
|
||||||
"luxon": "3.6.1",
|
"luxon": "3.6.1",
|
||||||
"marked": "17.0.6",
|
"marked": "17.0.6",
|
||||||
"marked-shiki": "1.2.1",
|
"marked-shiki": "1.2.1",
|
||||||
@@ -85,9 +86,11 @@
|
|||||||
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
||||||
"zod": "4.1.8",
|
"zod": "4.1.8",
|
||||||
"remeda": "2.26.0",
|
"remeda": "2.26.0",
|
||||||
|
"resolve.exports": "2.0.3",
|
||||||
"sst": "4.13.1",
|
"sst": "4.13.1",
|
||||||
"shiki": "4.2.0",
|
"shiki": "4.2.0",
|
||||||
"solid-list": "0.3.0",
|
"solid-list": "0.3.0",
|
||||||
|
"string-width": "7.2.0",
|
||||||
"tailwindcss": "4.1.11",
|
"tailwindcss": "4.1.11",
|
||||||
"vite": "7.1.4",
|
"vite": "7.1.4",
|
||||||
"@solidjs/meta": "0.29.4",
|
"@solidjs/meta": "0.29.4",
|
||||||
@@ -163,7 +166,7 @@
|
|||||||
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
|
"@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",
|
"@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",
|
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
||||||
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch",
|
"effect@4.0.0-beta.98": "patches/effect@4.0.0-beta.98.patch",
|
||||||
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch"
|
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
|
||||||
import { getFilename } from "@opencode-ai/core/util/path"
|
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 { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { useNavigate } from "@solidjs/router"
|
|
||||||
import { createMemo, onCleanup } from "solid-js"
|
import { createMemo, onCleanup } from "solid-js"
|
||||||
import { useCommand, type CommandOption } from "@/context/command"
|
import { commandPaletteOptions, useCommand, type CommandOption } from "@/context/command"
|
||||||
import { useFile } from "@/context/file"
|
import { useFile } from "@/context/file"
|
||||||
|
import { useGlobal } from "@/context/global"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout, type LocalProject } from "@/context/layout"
|
||||||
import { useServerSDK, type ServerSDK } from "@/context/server-sdk"
|
import { ServerConnection } from "@/context/server"
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useServerSDK } from "@/context/server-sdk"
|
||||||
|
import { useTabs } from "@/context/tabs"
|
||||||
|
import { displayName, projectForSession } from "@/pages/layout/helpers"
|
||||||
import { createSessionTabs } from "@/pages/session/helpers"
|
import { createSessionTabs } from "@/pages/session/helpers"
|
||||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||||
import { decode64 } from "@/utils/base64"
|
|
||||||
|
|
||||||
export type CommandPaletteEntry = {
|
export type CommandPaletteEntry = {
|
||||||
id: string
|
id: string
|
||||||
@@ -24,6 +25,8 @@ export type CommandPaletteEntry = {
|
|||||||
path?: string
|
path?: string
|
||||||
directory?: string
|
directory?: string
|
||||||
sessionID?: string
|
sessionID?: string
|
||||||
|
server?: ServerConnection.Key
|
||||||
|
project?: LocalProject
|
||||||
archived?: number
|
archived?: number
|
||||||
updated?: number
|
updated?: number
|
||||||
}
|
}
|
||||||
@@ -75,28 +78,25 @@ export function createCommandPaletteFileOpener(onOpenFile?: (path: string) => vo
|
|||||||
|
|
||||||
export function createCommandPaletteModel(props: { filesOnly?: () => boolean; onOpenFile?: (path: string) => void }) {
|
export function createCommandPaletteModel(props: { filesOnly?: () => boolean; onOpenFile?: (path: string) => void }) {
|
||||||
const command = useCommand()
|
const command = useCommand()
|
||||||
|
const global = useGlobal()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const layout = useLayout()
|
|
||||||
const file = useFile()
|
const file = useFile()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const navigate = useNavigate()
|
|
||||||
const serverSDK = useServerSDK()()
|
const serverSDK = useServerSDK()()
|
||||||
const serverSync = useServerSync()
|
const serverCtx = global.ensureServerCtx(serverSDK.server)
|
||||||
const { params, tabs } = useSessionLayout()
|
const appTabs = useTabs()
|
||||||
|
const { tabs: sessionTabs } = useSessionLayout()
|
||||||
const openFile = createCommandPaletteFileOpener(props.onOpenFile)
|
const openFile = createCommandPaletteFileOpener(props.onOpenFile)
|
||||||
const state = { cleanup: undefined as (() => void) | void, committed: false }
|
const state = { cleanup: undefined as (() => void) | void, committed: false }
|
||||||
const filesOnly = () => props.filesOnly?.() ?? false
|
const filesOnly = () => props.filesOnly?.() ?? false
|
||||||
|
|
||||||
const allowedCommands = createMemo(() => {
|
const allowedCommands = createMemo(() => {
|
||||||
if (filesOnly()) return []
|
if (filesOnly()) return []
|
||||||
return command.options.filter(
|
return commandPaletteOptions(command.options)
|
||||||
(option) =>
|
|
||||||
!option.disabled && !option.hidden && !option.id.startsWith("suggested.") && option.id !== "file.open",
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
const commandEntries = createMemo(() => {
|
const commandEntries = createMemo(() => {
|
||||||
const category = language.t("palette.group.commands")
|
const category = language.t("palette.group.commands")
|
||||||
return allowedCommands().map((option) => createCommandEntry(option, category))
|
return allowedCommands().map((option) => createCommandPaletteCommandEntry(option, category))
|
||||||
})
|
})
|
||||||
const preferredCommandEntries = createMemo(() => {
|
const preferredCommandEntries = createMemo(() => {
|
||||||
const all = allowedCommands()
|
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 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 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")
|
const category = language.t("palette.group.commands")
|
||||||
return sorted.map((option) => createCommandEntry(option, category))
|
return sorted.map((option) => createCommandPaletteCommandEntry(option, category))
|
||||||
})
|
})
|
||||||
|
|
||||||
const tabState = createSessionTabs({
|
const tabState = createSessionTabs({
|
||||||
tabs,
|
tabs: sessionTabs,
|
||||||
pathFromTab: file.pathFromTab,
|
pathFromTab: file.pathFromTab,
|
||||||
normalizeTab: (tab) => (tab.startsWith("file://") ? file.tab(tab) : tab),
|
normalizeTab: (tab) => (tab.startsWith("file://") ? file.tab(tab) : tab),
|
||||||
})
|
})
|
||||||
@@ -140,36 +140,12 @@ export function createCommandPaletteModel(props: { filesOnly?: () => boolean; on
|
|||||||
.map((path) => createCommandPaletteFileEntry(path, category))
|
.map((path) => createCommandPaletteFileEntry(path, category))
|
||||||
})
|
})
|
||||||
|
|
||||||
const projectDirectory = createMemo(() => decode64(params.dir) ?? "")
|
const sessions = createServerSessionEntries({
|
||||||
const project = createMemo(() => {
|
server: ServerConnection.key(serverSDK.server),
|
||||||
const directory = projectDirectory()
|
opened: serverCtx.projects.list,
|
||||||
if (!directory) return undefined
|
stored: () => serverCtx.sync.data.project,
|
||||||
return layout.projects.list().find((item) => item.worktree === directory || item.sandboxes?.includes(directory))
|
load: (search, signal) =>
|
||||||
})
|
serverSDK.client.experimental.session.list({ roots: true, search, limit: 50 }, { signal }),
|
||||||
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"),
|
untitled: () => language.t("command.session.new"),
|
||||||
category: () => language.t("command.category.session"),
|
category: () => language.t("command.category.session"),
|
||||||
})
|
})
|
||||||
@@ -191,8 +167,17 @@ export function createCommandPaletteModel(props: { filesOnly?: () => boolean; on
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (item.type === "session") {
|
if (item.type === "session") {
|
||||||
if (!item.directory || !item.sessionID) return
|
if (!item.sessionID || !item.server) return
|
||||||
navigate(`/${base64Encode(item.directory)}/session/${item.sessionID}`)
|
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)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!item.path) return
|
if (!item.path) return
|
||||||
@@ -218,7 +203,7 @@ export function createCommandPaletteModel(props: { filesOnly?: () => boolean; on
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createCommandEntry(option: CommandOption, category: string): CommandPaletteEntry {
|
export function createCommandPaletteCommandEntry(option: CommandOption, category: string): CommandPaletteEntry {
|
||||||
return {
|
return {
|
||||||
id: "command:" + option.id,
|
id: "command:" + option.id,
|
||||||
type: "command",
|
type: "command",
|
||||||
@@ -230,96 +215,65 @@ function createCommandEntry(option: CommandOption, category: string): CommandPal
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSessionEntries(props: {
|
export function createServerSessionEntries(props: {
|
||||||
workspaces: () => string[]
|
server: ServerConnection.Key
|
||||||
label: (directory: string) => string
|
opened: () => LocalProject[]
|
||||||
load: (directory: string) => ReturnType<ServerSDK["client"]["session"]["list"]>
|
stored: () => Project[]
|
||||||
|
load: (search: string, signal: AbortSignal) => Promise<{ data?: GlobalSession[] }>
|
||||||
untitled: () => string
|
untitled: () => string
|
||||||
category: () => string
|
category: () => string
|
||||||
}) {
|
}) {
|
||||||
const state: {
|
let abort: AbortController | undefined
|
||||||
token: number
|
|
||||||
inflight: Promise<CommandPaletteEntry[]> | undefined
|
|
||||||
cached: CommandPaletteEntry[] | undefined
|
|
||||||
} = { token: 0, inflight: undefined, cached: undefined }
|
|
||||||
|
|
||||||
return (text: string) => {
|
onCleanup(() => abort?.abort())
|
||||||
if (!text.trim()) {
|
|
||||||
state.token += 1
|
return async (text: string): Promise<CommandPaletteEntry[]> => {
|
||||||
state.inflight = undefined
|
const search = text.trim()
|
||||||
state.cached = undefined
|
if (!search) {
|
||||||
return [] as CommandPaletteEntry[]
|
abort?.abort()
|
||||||
|
return []
|
||||||
}
|
}
|
||||||
if (state.cached) return state.cached
|
abort?.abort()
|
||||||
if (state.inflight) return state.inflight
|
const current = new AbortController()
|
||||||
|
abort = current
|
||||||
const current = state.token
|
await new Promise<void>((resolve) => {
|
||||||
const dirs = props.workspaces()
|
const timer = setTimeout(resolve, 100)
|
||||||
if (dirs.length === 0) return [] as CommandPaletteEntry[]
|
current.signal.addEventListener(
|
||||||
|
"abort",
|
||||||
state.inflight = Promise.all(
|
() => {
|
||||||
dirs.map((directory) => {
|
clearTimeout(timer)
|
||||||
const description = props.label(directory)
|
resolve()
|
||||||
return props
|
},
|
||||||
.load(directory)
|
{ once: true },
|
||||||
.then((result) =>
|
)
|
||||||
(result.data ?? [])
|
})
|
||||||
.filter((session) => !!session?.id)
|
if (current.signal.aborted) return []
|
||||||
.map((session) => ({
|
const opened = props.opened()
|
||||||
id: session.id,
|
const openedByID = new Map(opened.flatMap((project) => (project.id ? [[project.id, project] as const] : [])))
|
||||||
title: session.title ?? props.untitled(),
|
const stored = props.stored().map((project) => ({ ...project, expanded: false }))
|
||||||
description,
|
const storedByID = new Map(stored.map((project) => [project.id, project] as const))
|
||||||
directory,
|
return props
|
||||||
archived: session.time?.archived,
|
.load(search, current.signal)
|
||||||
updated: session.time?.updated,
|
.then((result) =>
|
||||||
})),
|
(result.data ?? [])
|
||||||
)
|
.filter((session) => !session.time.archived)
|
||||||
.catch(() => [] as SessionEntryInput[])
|
.map((session) => {
|
||||||
}),
|
const project =
|
||||||
)
|
projectForSession(session, opened, openedByID) ?? projectForSession(session, stored, storedByID)
|
||||||
.then((results) => {
|
return {
|
||||||
if (state.token !== current) return [] as CommandPaletteEntry[]
|
id: `session:${props.server}:${session.id}`,
|
||||||
const seen = new Set<string>()
|
type: "session" as const,
|
||||||
const next = results
|
title: session.title || props.untitled(),
|
||||||
.flat()
|
description: project ? displayName(project) : session.project?.name || getFilename(session.directory),
|
||||||
.filter((item) => {
|
category: props.category(),
|
||||||
const key = `${item.directory}:${item.id}`
|
directory: session.directory,
|
||||||
if (seen.has(key)) return false
|
sessionID: session.id,
|
||||||
seen.add(key)
|
server: props.server,
|
||||||
return true
|
project,
|
||||||
})
|
updated: session.time.updated,
|
||||||
.map((item) => createSessionEntry(item, props.category()))
|
}
|
||||||
state.cached = next
|
}),
|
||||||
return next
|
)
|
||||||
})
|
|
||||||
.catch(() => [] as CommandPaletteEntry[])
|
.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,13 +5,20 @@ import { Dialog, DialogBody } from "@opencode-ai/ui/v2/dialog-v2"
|
|||||||
import { Icon } from "@opencode-ai/ui/v2/icon"
|
import { Icon } from "@opencode-ai/ui/v2/icon"
|
||||||
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
|
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
|
||||||
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
||||||
import { createEffect, createMemo, createResource, createSignal, For, Match, Show, Switch } from "solid-js"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { formatKeybindParts } from "@/context/command"
|
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 { useLanguage } from "@/context/language"
|
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 { getRelativeTime } from "@/utils/time"
|
||||||
import {
|
import {
|
||||||
|
createCommandPaletteCommandEntry,
|
||||||
createCommandPaletteFileEntry,
|
createCommandPaletteFileEntry,
|
||||||
createCommandPaletteModel,
|
createCommandPaletteModel,
|
||||||
|
createServerSessionEntries,
|
||||||
uniqueCommandPaletteEntries,
|
uniqueCommandPaletteEntries,
|
||||||
type CommandPaletteEntry,
|
type CommandPaletteEntry,
|
||||||
} from "./command-palette"
|
} from "./command-palette"
|
||||||
@@ -30,9 +37,6 @@ function matchesEntry(entry: CommandPaletteEntry, query: string) {
|
|||||||
|
|
||||||
export function DialogCommandPaletteV2(props: { onOpenFile?: (path: string) => void }) {
|
export function DialogCommandPaletteV2(props: { onOpenFile?: (path: string) => void }) {
|
||||||
const palette = createCommandPaletteModel(props)
|
const palette = createCommandPaletteModel(props)
|
||||||
const [query, setQuery] = createSignal("")
|
|
||||||
const [active, setActive] = createSignal(0)
|
|
||||||
|
|
||||||
const loadItems = async (text: string) => {
|
const loadItems = async (text: string) => {
|
||||||
const q = text.trim()
|
const q = text.trim()
|
||||||
if (!q) return [...palette.preferredCommandEntries(), ...palette.recentFileEntries()]
|
if (!q) return [...palette.preferredCommandEntries(), ...palette.recentFileEntries()]
|
||||||
@@ -41,16 +45,105 @@ export function DialogCommandPaletteV2(props: { onOpenFile?: (path: string) => v
|
|||||||
const category = palette.language.t("palette.group.files")
|
const category = palette.language.t("palette.group.files")
|
||||||
return [
|
return [
|
||||||
...palette.commandEntries().filter((entry) => matchesEntry(entry, q)),
|
...palette.commandEntries().filter((entry) => matchesEntry(entry, q)),
|
||||||
...nextSessions.filter((entry) => matchesEntry(entry, q)),
|
...nextSessions,
|
||||||
...files.map((path) => createCommandPaletteFileEntry(path, category)),
|
...files.map((path) => createCommandPaletteFileEntry(path, category)),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const [entries] = createResource(query, loadItems, { initialValue: [] as CommandPaletteEntry[] })
|
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[] })
|
||||||
// Render stale results while a new query loads to avoid flashing "Loading" per keystroke.
|
// Render stale results while a new query loads to avoid flashing "Loading" per keystroke.
|
||||||
const visibleEntries = createMemo(() => uniqueCommandPaletteEntries(entries.latest ?? []))
|
const visibleEntries = createMemo(() => uniqueCommandPaletteEntries(entries.latest ?? []))
|
||||||
const groupedEntries = createMemo(() => groups(visibleEntries()))
|
const groupedEntries = createMemo(() => groups(visibleEntries()))
|
||||||
const activeEntry = createMemo(() => visibleEntries()[active()])
|
const activeEntry = createMemo(() => visibleEntries()[active()])
|
||||||
|
const openSessions = createMemo(
|
||||||
|
() => new Set(tabs.store.flatMap((tab) => (tab.type === "session" ? [`${tab.server}\0${tab.sessionId}`] : []))),
|
||||||
|
)
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
query()
|
query()
|
||||||
@@ -59,7 +152,7 @@ export function DialogCommandPaletteV2(props: { onOpenFile?: (path: string) => v
|
|||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
palette.highlight(activeEntry())
|
props.highlight(activeEntry())
|
||||||
})
|
})
|
||||||
|
|
||||||
let resultsRef: HTMLDivElement | undefined
|
let resultsRef: HTMLDivElement | undefined
|
||||||
@@ -86,12 +179,12 @@ export function DialogCommandPaletteV2(props: { onOpenFile?: (path: string) => v
|
|||||||
}
|
}
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
palette.select(activeEntry())
|
props.select(activeEntry())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
palette.close()
|
props.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +198,7 @@ export function DialogCommandPaletteV2(props: { onOpenFile?: (path: string) => v
|
|||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
spellcheck={false}
|
spellcheck={false}
|
||||||
appearance="large"
|
appearance="large"
|
||||||
placeholder={palette.language.t("palette.search.placeholder")}
|
placeholder={props.placeholder}
|
||||||
leadingIcon={<Icon name="magnifying-glass" />}
|
leadingIcon={<Icon name="magnifying-glass" />}
|
||||||
onInput={(event) => setQuery(event.currentTarget.value)}
|
onInput={(event) => setQuery(event.currentTarget.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
@@ -117,7 +210,7 @@ export function DialogCommandPaletteV2(props: { onOpenFile?: (path: string) => v
|
|||||||
when={visibleEntries().length > 0}
|
when={visibleEntries().length > 0}
|
||||||
fallback={
|
fallback={
|
||||||
<div class="command-palette-v2-state">
|
<div class="command-palette-v2-state">
|
||||||
{entries.loading ? palette.language.t("common.loading") : palette.language.t("palette.empty")}
|
{entries.loading ? language.t("common.loading") : language.t("palette.empty")}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -132,9 +225,14 @@ export function DialogCommandPaletteV2(props: { onOpenFile?: (path: string) => v
|
|||||||
<PaletteRow
|
<PaletteRow
|
||||||
item={item}
|
item={item}
|
||||||
active={activeEntry()?.id === item.id}
|
active={activeEntry()?.id === item.id}
|
||||||
language={palette.language}
|
language={language}
|
||||||
|
sessionOpen={
|
||||||
|
item.server && item.sessionID
|
||||||
|
? openSessions().has(`${item.server}\0${item.sessionID}`)
|
||||||
|
: false
|
||||||
|
}
|
||||||
onActive={() => setActive(visibleEntries().findIndex((entry) => entry.id === item.id))}
|
onActive={() => setActive(visibleEntries().findIndex((entry) => entry.id === item.id))}
|
||||||
onSelect={() => palette.select(item)}
|
onSelect={() => props.select(item)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
@@ -153,13 +251,19 @@ function PaletteRow(props: {
|
|||||||
item: CommandPaletteEntry
|
item: CommandPaletteEntry
|
||||||
active: boolean
|
active: boolean
|
||||||
language: ReturnType<typeof useLanguage>
|
language: ReturnType<typeof useLanguage>
|
||||||
|
sessionOpen: boolean
|
||||||
onActive: () => void
|
onActive: () => void
|
||||||
onSelect: () => 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 (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="command-palette-v2-row"
|
class="command-palette-v2-row group"
|
||||||
role="option"
|
role="option"
|
||||||
aria-selected={props.active}
|
aria-selected={props.active}
|
||||||
data-active={props.active ? "" : undefined}
|
data-active={props.active ? "" : undefined}
|
||||||
@@ -197,7 +301,25 @@ function PaletteRow(props: {
|
|||||||
</Match>
|
</Match>
|
||||||
<Match when={props.item.type === "session"}>
|
<Match when={props.item.type === "session"}>
|
||||||
<div class="command-palette-v2-row-main">
|
<div class="command-palette-v2-row-main">
|
||||||
<Icon name="status" class="command-palette-v2-row-icon" />
|
<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>
|
||||||
<div class="command-palette-v2-row-text">
|
<div class="command-palette-v2-row-text">
|
||||||
<span class="command-palette-v2-title" classList={{ "opacity-70": !!props.item.archived }}>
|
<span class="command-palette-v2-title" classList={{ "opacity-70": !!props.item.archived }}>
|
||||||
{props.item.title}
|
{props.item.title}
|
||||||
|
|||||||
@@ -8,49 +8,9 @@ import introducingTabsVideo from "@/assets/help/introducing-tabs.mp4"
|
|||||||
import homeImage from "@/assets/help/home.png"
|
import homeImage from "@/assets/help/home.png"
|
||||||
import tabsImage from "@/assets/help/tabs.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
|
// TODO: wire to changelog / seen-state when available
|
||||||
const showPopover = () => true
|
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
|
// can remove this after the tabs rollout has been out for a while
|
||||||
export function TabsInfoPopup() {
|
export function TabsInfoPopup() {
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
@@ -61,7 +21,7 @@ export function TabsInfoPopup() {
|
|||||||
<Drawer open={drawerOpen()} onOpenChange={setDrawerOpen} side="right">
|
<Drawer open={drawerOpen()} onOpenChange={setDrawerOpen} side="right">
|
||||||
<Show when={settings.general.shouldDisplayTabsToast()}>
|
<Show when={settings.general.shouldDisplayTabsToast()}>
|
||||||
<div
|
<div
|
||||||
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)]"
|
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)]"
|
||||||
aria-label="Introducing Tabs. Organize your work and active sessions with tabs"
|
aria-label="Introducing Tabs. Organize your work and active sessions with tabs"
|
||||||
>
|
>
|
||||||
<button
|
<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 { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
|
||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import fuzzysort from "fuzzysort"
|
import fuzzysort from "fuzzysort"
|
||||||
import { formatKeybind, parseKeybind, useCommand } from "@/context/command"
|
import { DEFAULT_PALETTE_KEYBIND, formatKeybind, parseKeybind, useCommand } from "@/context/command"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { SettingsList } from "./settings-list"
|
import { SettingsList } from "./settings-list"
|
||||||
@@ -20,7 +20,6 @@ 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 IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
|
||||||
const PALETTE_ID = "command.palette"
|
const PALETTE_ID = "command.palette"
|
||||||
const DEFAULT_PALETTE_KEYBIND = "mod+shift+p"
|
|
||||||
|
|
||||||
type KeybindGroup = "General" | "Session" | "Navigation" | "Model and agent" | "Terminal" | "Prompt"
|
type KeybindGroup = "General" | "Session" | "Navigation" | "Model and agent" | "Terminal" | "Prompt"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { resolveKeybindOption, upsertCommandRegistration } from "./command"
|
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"])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("upsertCommandRegistration", () => {
|
describe("upsertCommandRegistration", () => {
|
||||||
test("replaces keyed registrations", () => {
|
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 IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
|
||||||
|
|
||||||
const PALETTE_ID = "command.palette"
|
const PALETTE_ID = "command.palette"
|
||||||
const DEFAULT_PALETTE_KEYBIND = "mod+shift+p"
|
export const DEFAULT_PALETTE_KEYBIND = "mod+k,mod+shift+p"
|
||||||
const SUGGESTED_PREFIX = "suggested."
|
const SUGGESTED_PREFIX = "suggested."
|
||||||
const EDITABLE_KEYBIND_IDS = new Set(["terminal.toggle", "terminal.new", "file.attach"])
|
const EDITABLE_KEYBIND_IDS = new Set(["terminal.toggle", "terminal.new", "file.attach"])
|
||||||
|
|
||||||
@@ -87,6 +87,13 @@ export interface CommandOption {
|
|||||||
onHighlight?: () => (() => void) | void
|
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) {
|
export function resolveKeybindOption(candidates: CommandOption[] | undefined, event: KeyboardEvent) {
|
||||||
return candidates?.find((option) => option.when?.(event)) ?? candidates?.find((option) => !option.when)
|
return candidates?.find((option) => option.when?.(event)) ?? candidates?.find((option) => !option.when)
|
||||||
}
|
}
|
||||||
@@ -375,7 +382,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
|||||||
}
|
}
|
||||||
|
|
||||||
const showPalette = () => {
|
const showPalette = () => {
|
||||||
run("file.open", "palette")
|
run(PALETTE_ID, "palette")
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
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")
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
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,8 +4,10 @@ import { useParams } from "@solidjs/router"
|
|||||||
import { batch, createEffect, createMemo, startTransition } from "solid-js"
|
import { batch, createEffect, createMemo, startTransition } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { useModels } from "@/context/models"
|
import { useModels } from "@/context/models"
|
||||||
|
import { useSettings } from "@/context/settings"
|
||||||
import { useProviders } from "@/hooks/use-providers"
|
import { useProviders } from "@/hooks/use-providers"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
|
import { hasCustomAgent, resolveAgent } from "./local-agent"
|
||||||
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
|
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
|
||||||
import { useSDK } from "./sdk"
|
import { useSDK } from "./sdk"
|
||||||
import { useSync } from "./sync"
|
import { useSync } from "./sync"
|
||||||
@@ -62,9 +64,11 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
const serverSDK = useServerSDK()
|
const serverSDK = useServerSDK()
|
||||||
const providers = useProviders(() => sdk().directory)
|
const providers = useProviders(() => sdk().directory)
|
||||||
const models = useModels()
|
const models = useModels()
|
||||||
|
const settings = useSettings()
|
||||||
|
|
||||||
const id = createMemo(() => params.id || undefined)
|
const id = createMemo(() => params.id || undefined)
|
||||||
const list = createMemo(() => sync().data.agent.filter((item) => item.mode !== "subagent" && !item.hidden))
|
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 connected = createMemo(() => new Set(providers.connected().map((item) => item.id)))
|
||||||
|
|
||||||
const [saved, setSaved, , savedReady] = persisted(
|
const [saved, setSaved, , savedReady] = persisted(
|
||||||
@@ -107,9 +111,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pickAgent = (name: string | undefined) => {
|
const pickAgent = (name: string | undefined) => {
|
||||||
const items = list()
|
return resolveAgent(list(), name)
|
||||||
if (items.length === 0) return
|
|
||||||
return items.find((item) => item.name === name) ?? items[0]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
@@ -180,8 +182,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
|
|
||||||
const agent = {
|
const agent = {
|
||||||
list,
|
list,
|
||||||
|
visible: agentsVisible,
|
||||||
current() {
|
current() {
|
||||||
return pickAgent(scope()?.agent ?? store.current)
|
return pickAgent(agentsVisible() ? (scope()?.agent ?? store.current) : "build")
|
||||||
},
|
},
|
||||||
set(name: string | undefined) {
|
set(name: string | undefined) {
|
||||||
const item = pickAgent(name)
|
const item = pickAgent(name)
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ export const dict = {
|
|||||||
"command.session.unshare": "إلغاء مشاركة الجلسة",
|
"command.session.unshare": "إلغاء مشاركة الجلسة",
|
||||||
"command.session.unshare.description": "إيقاف مشاركة هذه الجلسة",
|
"command.session.unshare.description": "إيقاف مشاركة هذه الجلسة",
|
||||||
"palette.search.placeholder": "البحث في الملفات والأوامر والجلسات",
|
"palette.search.placeholder": "البحث في الملفات والأوامر والجلسات",
|
||||||
|
"palette.search.placeholder.home": "البحث في الأوامر والجلسات",
|
||||||
"palette.empty": "لا توجد نتائج",
|
"palette.empty": "لا توجد نتائج",
|
||||||
"palette.group.commands": "الأوامر",
|
"palette.group.commands": "الأوامر",
|
||||||
"palette.group.files": "الملفات",
|
"palette.group.files": "الملفات",
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ export const dict = {
|
|||||||
"command.session.unshare": "Parar de compartilhar sessão",
|
"command.session.unshare": "Parar de compartilhar sessão",
|
||||||
"command.session.unshare.description": "Parar de compartilhar esta sessão",
|
"command.session.unshare.description": "Parar de compartilhar esta sessão",
|
||||||
"palette.search.placeholder": "Buscar arquivos, comandos e sessões",
|
"palette.search.placeholder": "Buscar arquivos, comandos e sessões",
|
||||||
|
"palette.search.placeholder.home": "Buscar comandos e sessões",
|
||||||
"palette.empty": "Nenhum resultado encontrado",
|
"palette.empty": "Nenhum resultado encontrado",
|
||||||
"palette.group.commands": "Comandos",
|
"palette.group.commands": "Comandos",
|
||||||
"palette.group.files": "Arquivos",
|
"palette.group.files": "Arquivos",
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export const dict = {
|
|||||||
"command.session.unshare.description": "Zaustavi dijeljenje ove sesije",
|
"command.session.unshare.description": "Zaustavi dijeljenje ove sesije",
|
||||||
|
|
||||||
"palette.search.placeholder": "Pretraži datoteke, komande i sesije",
|
"palette.search.placeholder": "Pretraži datoteke, komande i sesije",
|
||||||
|
"palette.search.placeholder.home": "Pretraži komande i sesije",
|
||||||
"palette.empty": "Nema rezultata",
|
"palette.empty": "Nema rezultata",
|
||||||
"palette.group.commands": "Komande",
|
"palette.group.commands": "Komande",
|
||||||
"palette.group.files": "Datoteke",
|
"palette.group.files": "Datoteke",
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export const dict = {
|
|||||||
"command.session.unshare.description": "Stop med at dele denne session",
|
"command.session.unshare.description": "Stop med at dele denne session",
|
||||||
|
|
||||||
"palette.search.placeholder": "Søg i filer, kommandoer og sessioner",
|
"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.empty": "Ingen resultater fundet",
|
||||||
"palette.group.commands": "Kommandoer",
|
"palette.group.commands": "Kommandoer",
|
||||||
"palette.group.files": "Filer",
|
"palette.group.files": "Filer",
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export const dict = {
|
|||||||
"command.session.unshare": "Teilen der Sitzung aufheben",
|
"command.session.unshare": "Teilen der Sitzung aufheben",
|
||||||
"command.session.unshare.description": "Teilen dieser Sitzung beenden",
|
"command.session.unshare.description": "Teilen dieser Sitzung beenden",
|
||||||
"palette.search.placeholder": "Dateien, Befehle und Sitzungen durchsuchen",
|
"palette.search.placeholder": "Dateien, Befehle und Sitzungen durchsuchen",
|
||||||
|
"palette.search.placeholder.home": "Befehle und Sitzungen durchsuchen",
|
||||||
"palette.empty": "Keine Ergebnisse gefunden",
|
"palette.empty": "Keine Ergebnisse gefunden",
|
||||||
"palette.group.commands": "Befehle",
|
"palette.group.commands": "Befehle",
|
||||||
"palette.group.files": "Dateien",
|
"palette.group.files": "Dateien",
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export const dict = {
|
|||||||
"command.session.unshare.description": "Stop sharing this session",
|
"command.session.unshare.description": "Stop sharing this session",
|
||||||
|
|
||||||
"palette.search.placeholder": "Search files, commands, and sessions",
|
"palette.search.placeholder": "Search files, commands, and sessions",
|
||||||
|
"palette.search.placeholder.home": "Search commands and sessions",
|
||||||
"palette.empty": "No results found",
|
"palette.empty": "No results found",
|
||||||
"palette.group.commands": "Commands",
|
"palette.group.commands": "Commands",
|
||||||
"palette.group.files": "Files",
|
"palette.group.files": "Files",
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export const dict = {
|
|||||||
"command.session.unshare.description": "Dejar de compartir esta sesión",
|
"command.session.unshare.description": "Dejar de compartir esta sesión",
|
||||||
|
|
||||||
"palette.search.placeholder": "Buscar archivos, comandos y sesiones",
|
"palette.search.placeholder": "Buscar archivos, comandos y sesiones",
|
||||||
|
"palette.search.placeholder.home": "Buscar comandos y sesiones",
|
||||||
"palette.empty": "No se encontraron resultados",
|
"palette.empty": "No se encontraron resultados",
|
||||||
"palette.group.commands": "Comandos",
|
"palette.group.commands": "Comandos",
|
||||||
"palette.group.files": "Archivos",
|
"palette.group.files": "Archivos",
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ export const dict = {
|
|||||||
"command.session.unshare": "Ne plus partager la session",
|
"command.session.unshare": "Ne plus partager la session",
|
||||||
"command.session.unshare.description": "Arrêter de partager cette 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": "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.empty": "Aucun résultat trouvé",
|
||||||
"palette.group.commands": "Commandes",
|
"palette.group.commands": "Commandes",
|
||||||
"palette.group.files": "Fichiers",
|
"palette.group.files": "Fichiers",
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ export const dict = {
|
|||||||
"command.session.unshare": "セッションの共有を停止",
|
"command.session.unshare": "セッションの共有を停止",
|
||||||
"command.session.unshare.description": "このセッションの共有を停止",
|
"command.session.unshare.description": "このセッションの共有を停止",
|
||||||
"palette.search.placeholder": "ファイル、コマンド、セッションを検索",
|
"palette.search.placeholder": "ファイル、コマンド、セッションを検索",
|
||||||
|
"palette.search.placeholder.home": "コマンドとセッションを検索",
|
||||||
"palette.empty": "結果が見つかりません",
|
"palette.empty": "結果が見つかりません",
|
||||||
"palette.group.commands": "コマンド",
|
"palette.group.commands": "コマンド",
|
||||||
"palette.group.files": "ファイル",
|
"palette.group.files": "ファイル",
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ export const dict = {
|
|||||||
"command.session.unshare": "세션 공유 중지",
|
"command.session.unshare": "세션 공유 중지",
|
||||||
"command.session.unshare.description": "이 세션 공유 중지",
|
"command.session.unshare.description": "이 세션 공유 중지",
|
||||||
"palette.search.placeholder": "파일, 명령어 및 세션 검색",
|
"palette.search.placeholder": "파일, 명령어 및 세션 검색",
|
||||||
|
"palette.search.placeholder.home": "명령어 및 세션 검색",
|
||||||
"palette.empty": "결과 없음",
|
"palette.empty": "결과 없음",
|
||||||
"palette.group.commands": "명령어",
|
"palette.group.commands": "명령어",
|
||||||
"palette.group.files": "파일",
|
"palette.group.files": "파일",
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ export const dict = {
|
|||||||
"command.session.unshare.description": "Slutt å dele denne sesjonen",
|
"command.session.unshare.description": "Slutt å dele denne sesjonen",
|
||||||
|
|
||||||
"palette.search.placeholder": "Søk i filer, kommandoer og sesjoner",
|
"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.empty": "Ingen resultater funnet",
|
||||||
"palette.group.commands": "Kommandoer",
|
"palette.group.commands": "Kommandoer",
|
||||||
"palette.group.files": "Filer",
|
"palette.group.files": "Filer",
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ export const dict = {
|
|||||||
"command.session.unshare": "Przestań udostępniać sesję",
|
"command.session.unshare": "Przestań udostępniać sesję",
|
||||||
"command.session.unshare.description": "Zatrzymaj udostępnianie tej sesji",
|
"command.session.unshare.description": "Zatrzymaj udostępnianie tej sesji",
|
||||||
"palette.search.placeholder": "Szukaj plików, poleceń i sesji",
|
"palette.search.placeholder": "Szukaj plików, poleceń i sesji",
|
||||||
|
"palette.search.placeholder.home": "Szukaj poleceń i sesji",
|
||||||
"palette.empty": "Brak wyników",
|
"palette.empty": "Brak wyników",
|
||||||
"palette.group.commands": "Polecenia",
|
"palette.group.commands": "Polecenia",
|
||||||
"palette.group.files": "Pliki",
|
"palette.group.files": "Pliki",
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export const dict = {
|
|||||||
"command.session.unshare.description": "Прекратить публикацию сессии",
|
"command.session.unshare.description": "Прекратить публикацию сессии",
|
||||||
|
|
||||||
"palette.search.placeholder": "Поиск файлов, команд и сессий",
|
"palette.search.placeholder": "Поиск файлов, команд и сессий",
|
||||||
|
"palette.search.placeholder.home": "Поиск команд и сессий",
|
||||||
"palette.empty": "Ничего не найдено",
|
"palette.empty": "Ничего не найдено",
|
||||||
"palette.group.commands": "Команды",
|
"palette.group.commands": "Команды",
|
||||||
"palette.group.files": "Файлы",
|
"palette.group.files": "Файлы",
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export const dict = {
|
|||||||
"command.session.unshare.description": "หยุดการแชร์เซสชันนี้",
|
"command.session.unshare.description": "หยุดการแชร์เซสชันนี้",
|
||||||
|
|
||||||
"palette.search.placeholder": "ค้นหาไฟล์ คำสั่ง และเซสชัน",
|
"palette.search.placeholder": "ค้นหาไฟล์ คำสั่ง และเซสชัน",
|
||||||
|
"palette.search.placeholder.home": "ค้นหาคำสั่งและเซสชัน",
|
||||||
"palette.empty": "ไม่พบผลลัพธ์",
|
"palette.empty": "ไม่พบผลลัพธ์",
|
||||||
"palette.group.commands": "คำสั่ง",
|
"palette.group.commands": "คำสั่ง",
|
||||||
"palette.group.files": "ไฟล์",
|
"palette.group.files": "ไฟล์",
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ export const dict = {
|
|||||||
"command.session.unshare.description": "Bu oturumun paylaşımını durdur",
|
"command.session.unshare.description": "Bu oturumun paylaşımını durdur",
|
||||||
|
|
||||||
"palette.search.placeholder": "Dosya, komut ve oturum ara",
|
"palette.search.placeholder": "Dosya, komut ve oturum ara",
|
||||||
|
"palette.search.placeholder.home": "Komut ve oturum ara",
|
||||||
"palette.empty": "Sonuç bulunamadı",
|
"palette.empty": "Sonuç bulunamadı",
|
||||||
"palette.group.commands": "Komutlar",
|
"palette.group.commands": "Komutlar",
|
||||||
"palette.group.files": "Dosyalar",
|
"palette.group.files": "Dosyalar",
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export const dict = {
|
|||||||
"command.session.unshare.description": "Припинити поширення цієї сесії",
|
"command.session.unshare.description": "Припинити поширення цієї сесії",
|
||||||
|
|
||||||
"palette.search.placeholder": "Пошук файлів, команд і сесій",
|
"palette.search.placeholder": "Пошук файлів, команд і сесій",
|
||||||
|
"palette.search.placeholder.home": "Пошук команд і сесій",
|
||||||
"palette.empty": "Результатів не знайдено",
|
"palette.empty": "Результатів не знайдено",
|
||||||
"palette.group.commands": "Команди",
|
"palette.group.commands": "Команди",
|
||||||
"palette.group.files": "Файли",
|
"palette.group.files": "Файли",
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ export const dict = {
|
|||||||
"command.session.unshare.description": "停止分享此会话",
|
"command.session.unshare.description": "停止分享此会话",
|
||||||
|
|
||||||
"palette.search.placeholder": "搜索文件、命令和会话",
|
"palette.search.placeholder": "搜索文件、命令和会话",
|
||||||
|
"palette.search.placeholder.home": "搜索命令和会话",
|
||||||
"palette.empty": "未找到结果",
|
"palette.empty": "未找到结果",
|
||||||
"palette.group.commands": "命令",
|
"palette.group.commands": "命令",
|
||||||
"palette.group.files": "文件",
|
"palette.group.files": "文件",
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ export const dict = {
|
|||||||
"command.session.unshare.description": "停止分享此工作階段",
|
"command.session.unshare.description": "停止分享此工作階段",
|
||||||
|
|
||||||
"palette.search.placeholder": "搜尋檔案、命令和工作階段",
|
"palette.search.placeholder": "搜尋檔案、命令和工作階段",
|
||||||
|
"palette.search.placeholder.home": "搜尋命令和工作階段",
|
||||||
"palette.empty": "找不到結果",
|
"palette.empty": "找不到結果",
|
||||||
"palette.group.commands": "命令",
|
"palette.group.commands": "命令",
|
||||||
"palette.group.files": "檔案",
|
"palette.group.files": "檔案",
|
||||||
|
|||||||
+211
-118
@@ -5,6 +5,7 @@ import {
|
|||||||
createMemo,
|
createMemo,
|
||||||
createResource,
|
createResource,
|
||||||
createRoot,
|
createRoot,
|
||||||
|
createSignal,
|
||||||
For,
|
For,
|
||||||
Match,
|
Match,
|
||||||
on,
|
on,
|
||||||
@@ -74,6 +75,23 @@ const HOME_SESSION_LIMIT = 64
|
|||||||
const HOME_SESSION_HEADER_STICKY_TOP = 12
|
const HOME_SESSION_HEADER_STICKY_TOP = 12
|
||||||
const HOME_SESSION_HEADER_TEXT_HEIGHT = 16
|
const HOME_SESSION_HEADER_TEXT_HEIGHT = 16
|
||||||
const HOME_SESSION_HEADER_FADE_DISTANCE = 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 SHOW_HOME_SESSION_ARCHIVE = false
|
||||||
const HOME_ROW_LAYOUT =
|
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"
|
"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"
|
||||||
@@ -144,6 +162,7 @@ function useHomeSessionHeaderOpacity(groups: () => HomeSessionGroup[]) {
|
|||||||
let content: HTMLDivElement | undefined
|
let content: HTMLDivElement | undefined
|
||||||
let positionFrame: number | undefined
|
let positionFrame: number | undefined
|
||||||
let resizeObserver: ResizeObserver | undefined
|
let resizeObserver: ResizeObserver | undefined
|
||||||
|
let stickyTop = HOME_SESSION_HEADER_STICKY_TOP
|
||||||
const headerRefs = new Map<HomeSessionGroup["id"], HTMLDivElement>()
|
const headerRefs = new Map<HomeSessionGroup["id"], HTMLDivElement>()
|
||||||
const headerOffsets = new Map<HomeSessionGroup["id"], number>()
|
const headerOffsets = new Map<HomeSessionGroup["id"], number>()
|
||||||
const [state, setState] = createStore({
|
const [state, setState] = createStore({
|
||||||
@@ -202,6 +221,13 @@ function useHomeSessionHeaderOpacity(groups: () => HomeSessionGroup[]) {
|
|||||||
|
|
||||||
function updatePositionCache() {
|
function updatePositionCache() {
|
||||||
if (!viewport) return
|
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) => {
|
groups().forEach((group) => {
|
||||||
const el = headerRefs.get(group.id)
|
const el = headerRefs.get(group.id)
|
||||||
if (!el) return
|
if (!el) return
|
||||||
@@ -217,7 +243,7 @@ function useHomeSessionHeaderOpacity(groups: () => HomeSessionGroup[]) {
|
|||||||
.slice(index + 1)
|
.slice(index + 1)
|
||||||
.map((item) => headerOffsets.get(item.id))
|
.map((item) => headerOffsets.get(item.id))
|
||||||
.find((offset) => offset !== undefined)
|
.find((offset) => offset !== undefined)
|
||||||
const fadeEnd = HOME_SESSION_HEADER_STICKY_TOP + HOME_SESSION_HEADER_TEXT_HEIGHT
|
const fadeEnd = stickyTop + HOME_SESSION_HEADER_TEXT_HEIGHT
|
||||||
const nextTop = nextOffset === undefined ? undefined : nextOffset - scrollTop
|
const nextTop = nextOffset === undefined ? undefined : nextOffset - scrollTop
|
||||||
const opacity =
|
const opacity =
|
||||||
nextTop === undefined ? 1 : Math.max(0, Math.min(1, (nextTop - fadeEnd) / HOME_SESSION_HEADER_FADE_DISTANCE))
|
nextTop === undefined ? 1 : Math.max(0, Math.min(1, (nextTop - fadeEnd) / HOME_SESSION_HEADER_FADE_DISTANCE))
|
||||||
@@ -271,6 +297,9 @@ export function NewHome() {
|
|||||||
const marked = useMarked()
|
const marked = useMarked()
|
||||||
const openSettings = useSettingsCommand()
|
const openSettings = useSettingsCommand()
|
||||||
let focusSessionSearch: (() => void) | undefined
|
let focusSessionSearch: (() => void) | undefined
|
||||||
|
let sessionViewport: HTMLDivElement | undefined
|
||||||
|
const [sessionThumbTrack, setSessionThumbTrack] = createSignal<HTMLDivElement>()
|
||||||
|
const [sessionHoverTarget, setSessionHoverTarget] = createSignal<HTMLElement>()
|
||||||
const [state, setState] = createStore({
|
const [state, setState] = createStore({
|
||||||
search: "",
|
search: "",
|
||||||
searchFocused: false,
|
searchFocused: false,
|
||||||
@@ -402,6 +431,34 @@ export function NewHome() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
command.register("home", () => [
|
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",
|
id: "home.sessions.search.focus",
|
||||||
title: searchPlaceholder(),
|
title: searchPlaceholder(),
|
||||||
@@ -541,127 +598,158 @@ export function NewHome() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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="rounded-[10px] shadow-[var(--v2-elevation-raised)] m-2 min-h-0 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">
|
<ScrollView
|
||||||
<HomeProjectColumn
|
class="h-full [container-type:size]"
|
||||||
projects={projects()}
|
thumbContainer={sessionThumbTrack}
|
||||||
recentlyClosed={recentlyClosed()}
|
thumbHoverTarget={sessionHoverTarget}
|
||||||
homedir={homedir()}
|
viewportRef={(el) => {
|
||||||
selected={selection()}
|
sessionViewport = el
|
||||||
focusServer={focusServer}
|
sessionHeaderOpacity.setViewport(el)
|
||||||
selectProject={selectProject}
|
}}
|
||||||
openNewSession={openProjectNewSession}
|
onScroll={(event) => sessionHeaderOpacity.update(event.currentTarget.scrollTop)}
|
||||||
openRecentProject={(conn, directory) => addProjects(conn, [directory])}
|
onWheel={(event) => {
|
||||||
chooseProject={(conn) => void chooseProject(conn)}
|
if (!sessionViewport) return
|
||||||
editProject={editProject}
|
if (event.target instanceof Node && sessionViewport.contains(event.target)) return
|
||||||
closeProject={(conn, directory) => {
|
containHomeWheel(event, sessionViewport)
|
||||||
const next = closeHomeProject(
|
}}
|
||||||
selection(),
|
>
|
||||||
ServerConnection.key(conn),
|
<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">
|
||||||
global.ensureServerCtx(conn).projects,
|
<HomeProjectColumn
|
||||||
directory,
|
projects={projects()}
|
||||||
)
|
recentlyClosed={recentlyClosed()}
|
||||||
if (next) setSelection(next)
|
homedir={homedir()}
|
||||||
}}
|
selected={selection()}
|
||||||
clearNotifications={clearNotifications}
|
focusServer={focusServer}
|
||||||
unseenCount={unseenCount}
|
selectProject={selectProject}
|
||||||
openSettings={openSettings}
|
openNewSession={openProjectNewSession}
|
||||||
openHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
|
openRecentProject={(conn, directory) => addProjects(conn, [directory])}
|
||||||
language={language}
|
chooseProject={(conn) => void chooseProject(conn)}
|
||||||
/>
|
editProject={editProject}
|
||||||
|
closeProject={(conn, directory) => {
|
||||||
<section
|
const next = closeHomeProject(
|
||||||
class="min-h-0 min-w-0 flex-1 flex flex-col pt-6 lg:pt-12 relative"
|
selection(),
|
||||||
aria-label={language.t("sidebar.project.recentSessions")}
|
ServerConnection.key(conn),
|
||||||
>
|
global.ensureServerCtx(conn).projects,
|
||||||
<HomeSessionSearch
|
directory,
|
||||||
value={state.search}
|
)
|
||||||
placeholder={searchPlaceholder()}
|
if (next) setSelection(next)
|
||||||
open={searchOpen()}
|
}}
|
||||||
loading={sessionLoad.isLoading}
|
clearNotifications={clearNotifications}
|
||||||
results={searchResults()}
|
unseenCount={unseenCount}
|
||||||
showProjectName={!selectedProject()}
|
openSettings={openSettings}
|
||||||
server={selection().server}
|
openHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
|
||||||
noResultsLabel={language.t("home.sessions.search.noResults", { query: search() })}
|
language={language}
|
||||||
bindFocus={(focus) => {
|
onWheel={(event) => {
|
||||||
focusSessionSearch = focus
|
if (sessionViewport) containHomeWheel(event, sessionViewport)
|
||||||
}}
|
}}
|
||||||
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"
|
<section
|
||||||
viewportRef={sessionHeaderOpacity.setViewport}
|
ref={setSessionHoverTarget}
|
||||||
onScroll={(event) => sessionHeaderOpacity.update(event.currentTarget.scrollTop)}
|
class="min-h-0 min-w-0 flex-1 flex flex-col"
|
||||||
|
aria-label={language.t("sidebar.project.recentSessions")}
|
||||||
>
|
>
|
||||||
<Show when={groups().length > 0 && newSessionProject()}>
|
<div
|
||||||
<div class="pointer-events-none absolute top-3 right-3 z-20 flex">
|
class="sticky top-0 z-30 shrink-0 bg-v2-background-bg-base pb-3 pt-6 lg:pt-12"
|
||||||
<ButtonV2
|
onWheel={(event) => {
|
||||||
data-action="home-new-session"
|
if (sessionViewport) containHomeWheel(event, sessionViewport)
|
||||||
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>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Show
|
<HomeSessionSearch
|
||||||
when={groups().length > 0}
|
value={state.search}
|
||||||
fallback={<HomeSessionsEmpty onNewSession={newSessionProject() ? openNewSession : undefined} />}
|
placeholder={searchPlaceholder()}
|
||||||
>
|
open={searchOpen()}
|
||||||
<div ref={sessionHeaderOpacity.setContentRef} class="flex flex-col pt-3 pr-3 pb-16">
|
loading={sessionLoad.isLoading}
|
||||||
<For each={groups()}>
|
results={searchResults()}
|
||||||
{(group, index) => (
|
showProjectName={!selectedProject()}
|
||||||
<>
|
server={selection().server}
|
||||||
<HomeSessionGroupHeader
|
noResultsLabel={language.t("home.sessions.search.noResults", { query: search() })}
|
||||||
title={group.title}
|
bindFocus={(focus) => {
|
||||||
titleOpacity={sessionHeaderOpacity.titleOpacity(group.id)}
|
focusSessionSearch = focus
|
||||||
ref={(el) => sessionHeaderOpacity.setHeaderRef(group.id, el)}
|
}}
|
||||||
elevated={index() === 0}
|
onInput={(value) => setState("search", value)}
|
||||||
/>
|
onFocus={() => setState("searchFocused", true)}
|
||||||
<div
|
onClose={closeSearch}
|
||||||
class={`flex min-w-0 flex-col gap-px pt-4 ${index() === groups().length - 1 ? "" : "mb-6"}`}
|
onSelect={selectSearchSession}
|
||||||
>
|
/>
|
||||||
<For each={group.sessions}>
|
<Show when={groups().length > 0 && newSessionProject()}>
|
||||||
{(record) => (
|
<div class="pointer-events-none absolute right-0 top-[84px] z-20 flex lg:top-[108px]">
|
||||||
<HomeSessionRow
|
<ButtonV2
|
||||||
record={record}
|
data-action="home-new-session"
|
||||||
showProjectName={!selectedProject()}
|
variant="ghost-muted"
|
||||||
server={selection().server}
|
size="normal"
|
||||||
openSession={openSession}
|
icon="edit"
|
||||||
archiveSession={archiveSession}
|
class="pointer-events-auto h-7 px-2 [font-weight:530]"
|
||||||
/>
|
onClick={openNewSession}
|
||||||
)}
|
>
|
||||||
</For>
|
{language.t("command.session.new")}
|
||||||
</div>
|
</ButtonV2>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</div>
|
||||||
</ScrollView>
|
{/* Sticky chrome for the portaled session scrollbar — matches old sessions ScrollView bounds */}
|
||||||
</section>
|
<div class="pointer-events-none sticky top-[84px] z-40 h-0 -mr-3 lg:top-[108px]">
|
||||||
<HomeUtilityNav
|
<div
|
||||||
class="flex lg:hidden"
|
ref={setSessionThumbTrack}
|
||||||
openSettings={openSettings}
|
data-component="home-session-scroll-track"
|
||||||
openHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
|
class="relative ml-auto h-[calc(100cqh-84px)] w-3 lg:h-[calc(100cqh-108px)]"
|
||||||
language={language}
|
/>
|
||||||
/>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -683,6 +771,7 @@ function HomeProjectColumn(props: {
|
|||||||
openSettings: () => void
|
openSettings: () => void
|
||||||
openHelp: () => void
|
openHelp: () => void
|
||||||
language: ReturnType<typeof useLanguage>
|
language: ReturnType<typeof useLanguage>
|
||||||
|
onWheel: (event: WheelEvent) => void
|
||||||
}) {
|
}) {
|
||||||
const global = useGlobal()
|
const global = useGlobal()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
@@ -699,8 +788,12 @@ function HomeProjectColumn(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
class="mt-6 flex min-h-0 min-w-0 flex-col gap-4 overflow-hidden lg:mt-14 lg:pt-[52px]"
|
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]"
|
||||||
aria-label={props.language.t("home.projects")}
|
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="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>
|
<div class="text-v2-text-text-muted [font-weight:530]">{props.language.t("home.projects")}</div>
|
||||||
@@ -1418,7 +1511,7 @@ function HomeSessionGroupHeader(props: {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={props.ref}
|
ref={props.ref}
|
||||||
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"}`}
|
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"}`}
|
||||||
>
|
>
|
||||||
<div class={HOME_SECTION_LABEL} style={{ opacity: props.titleOpacity }}>
|
<div class={HOME_SECTION_LABEL} style={{ opacity: props.titleOpacity }}>
|
||||||
{props.title}
|
{props.title}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createEffect, Suspense, type ParentProps } from "solid-js"
|
import { createEffect, Suspense, type ParentProps } from "solid-js"
|
||||||
import { useNavigate } from "@solidjs/router"
|
import { useNavigate } from "@solidjs/router"
|
||||||
import { DebugBar } from "@/components/debug-bar"
|
import { DebugBar } from "@/components/debug-bar"
|
||||||
import { HelpButton, TabsInfoPopup } from "@/components/help-button"
|
import { TabsInfoPopup } from "@/components/help-button"
|
||||||
import { Titlebar, type TitlebarUpdate } from "@/components/titlebar"
|
import { Titlebar, type TitlebarUpdate } from "@/components/titlebar"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { setNavigate } from "@/utils/notification-click"
|
import { setNavigate } from "@/utils/notification-click"
|
||||||
@@ -38,7 +38,6 @@ export default function NewLayout(props: ParentProps) {
|
|||||||
</main>
|
</main>
|
||||||
{import.meta.env.DEV && <DebugBar inline />}
|
{import.meta.env.DEV && <DebugBar inline />}
|
||||||
<TabsInfoPopup />
|
<TabsInfoPopup />
|
||||||
<HelpButton />
|
|
||||||
<ToastRegion v2 />
|
<ToastRegion v2 />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context"
|
|||||||
import { useCommand, type CommandOption } from "@/context/command"
|
import { useCommand, type CommandOption } from "@/context/command"
|
||||||
import { ConstrainDragXAxis, getDraggableId } from "@/utils/solid-dnd"
|
import { ConstrainDragXAxis, getDraggableId } from "@/utils/solid-dnd"
|
||||||
import { DebugBar } from "@/components/debug-bar"
|
import { DebugBar } from "@/components/debug-bar"
|
||||||
import { HelpButton, TabsInfoPopup } from "@/components/help-button"
|
import { TabsInfoPopup } from "@/components/help-button"
|
||||||
import { Titlebar, type TitlebarUpdate } from "@/components/titlebar"
|
import { Titlebar, type TitlebarUpdate } from "@/components/titlebar"
|
||||||
import { useDirectoryPicker } from "@/components/directory-picker"
|
import { useDirectoryPicker } from "@/components/directory-picker"
|
||||||
import { ServerConnection, useServer } from "@/context/server"
|
import { ServerConnection, useServer } from "@/context/server"
|
||||||
@@ -2396,7 +2396,6 @@ export default function LegacyLayout(props: ParentProps) {
|
|||||||
{import.meta.env.DEV && import.meta.env.VITE_DISABLE_DEBUG_BAR !== "1" && <DebugBar />}
|
{import.meta.env.DEV && import.meta.env.VITE_DISABLE_DEBUG_BAR !== "1" && <DebugBar />}
|
||||||
</div>
|
</div>
|
||||||
<TabsInfoPopup />
|
<TabsInfoPopup />
|
||||||
<HelpButton />
|
|
||||||
<ToastRegion v2={false} />
|
<ToastRegion v2={false} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { createStore } from "solid-js/store"
|
|||||||
import { Portal } from "solid-js/web"
|
import { Portal } from "solid-js/web"
|
||||||
import { useSearchParams } from "@solidjs/router"
|
import { useSearchParams } from "@solidjs/router"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
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 { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||||
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||||
import { NewSessionDesignView } from "@/components/session"
|
import { NewSessionDesignView } from "@/components/session"
|
||||||
@@ -51,6 +52,7 @@ export default function NewSessionPage() {
|
|||||||
const comments = useComments()
|
const comments = useComments()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
|
const dialog = useDialog()
|
||||||
const command = useCommand()
|
const command = useCommand()
|
||||||
const providers = useProviders(() => sdk().directory)
|
const providers = useProviders(() => sdk().directory)
|
||||||
const openProviderSettings = useSettingsDialog("providers")
|
const openProviderSettings = useSettingsDialog("providers")
|
||||||
@@ -77,6 +79,15 @@ export default function NewSessionPage() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
command.register("new-session", () => [
|
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",
|
id: "input.focus",
|
||||||
title: language.t("command.input.focus"),
|
title: language.t("command.input.focus"),
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import { useLocation, useNavigate, useParams, useSearchParams } from "@solidjs/r
|
|||||||
import { NewSessionView, SessionHeader } from "@/components/session"
|
import { NewSessionView, SessionHeader } from "@/components/session"
|
||||||
import { ErrorPage } from "@/pages/error"
|
import { ErrorPage } from "@/pages/error"
|
||||||
import { CommentsProvider, useComments } from "@/context/comments"
|
import { CommentsProvider, useComments } from "@/context/comments"
|
||||||
|
import { useCommand } from "@/context/command"
|
||||||
import { DirectoryDataProvider } from "@/pages/directory-layout"
|
import { DirectoryDataProvider } from "@/pages/directory-layout"
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useServerSync } from "@/context/server-sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
@@ -362,6 +363,7 @@ export default function Page() {
|
|||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const prompt = usePrompt()
|
const prompt = usePrompt()
|
||||||
const comments = useComments()
|
const comments = useComments()
|
||||||
|
const command = useCommand()
|
||||||
const terminal = useTerminal()
|
const terminal = useTerminal()
|
||||||
const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>()
|
const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
@@ -1098,6 +1100,14 @@ export default function Page() {
|
|||||||
review: reviewTab,
|
review: reviewTab,
|
||||||
fileBrowser: () => newSessionDesign() && isDesktop() && !!params.id,
|
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({
|
const openReviewFile = createOpenReviewFile({
|
||||||
showAllFiles,
|
showAllFiles,
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export function createPromptInputController(input: {
|
|||||||
options: local.agent.list().map((agent) => agent.name),
|
options: local.agent.list().map((agent) => agent.name),
|
||||||
current: local.agent.current()?.name ?? "",
|
current: local.agent.current()?.name ?? "",
|
||||||
loading: agentsQuery.isLoading,
|
loading: agentsQuery.isLoading,
|
||||||
visible: settings.visibility.customAgents(),
|
visible: local.agent.visible(),
|
||||||
select: local.agent.set,
|
select: local.agent.set,
|
||||||
},
|
},
|
||||||
model: {
|
model: {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useCommand, type CommandOption } from "@/context/command"
|
import { useCommand, type CommandOption } from "@/context/command"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useLocal, type ModelSelection } from "@/context/local"
|
import { useLocal, type ModelSelection } from "@/context/local"
|
||||||
import { useSettings } from "@/context/settings"
|
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { getCursorPosition, setCursorPosition } from "@/components/prompt-input/editor-dom"
|
import { getCursorPosition, setCursorPosition } from "@/components/prompt-input/editor-dom"
|
||||||
import { useSessionLayout } from "./session-layout"
|
import { useSessionLayout } from "./session-layout"
|
||||||
@@ -19,7 +18,6 @@ export const useComposerCommands = (input: { model?: ModelSelection } = {}) => {
|
|||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
const settings = useSettings()
|
|
||||||
const { sessionKey } = useSessionLayout()
|
const { sessionKey } = useSessionLayout()
|
||||||
const sessionOwnership = createSessionOwnership(sessionKey)
|
const sessionOwnership = createSessionOwnership(sessionKey)
|
||||||
const model = input.model ?? local.model
|
const model = input.model ?? local.model
|
||||||
@@ -70,7 +68,7 @@ export const useComposerCommands = (input: { model?: ModelSelection } = {}) => {
|
|||||||
description: language.t("command.agent.cycle.description"),
|
description: language.t("command.agent.cycle.description"),
|
||||||
keybind: "mod+.",
|
keybind: "mod+.",
|
||||||
slash: "agent",
|
slash: "agent",
|
||||||
disabled: !settings.visibility.customAgents(),
|
disabled: !local.agent.visible(),
|
||||||
onSelect: () => local.agent.move(1),
|
onSelect: () => local.agent.move(1),
|
||||||
}),
|
}),
|
||||||
agentCommand({
|
agentCommand({
|
||||||
@@ -78,7 +76,7 @@ export const useComposerCommands = (input: { model?: ModelSelection } = {}) => {
|
|||||||
title: language.t("command.agent.cycle.reverse"),
|
title: language.t("command.agent.cycle.reverse"),
|
||||||
description: language.t("command.agent.cycle.reverse.description"),
|
description: language.t("command.agent.cycle.reverse.description"),
|
||||||
keybind: "shift+mod+.",
|
keybind: "shift+mod+.",
|
||||||
disabled: !settings.visibility.customAgents(),
|
disabled: !local.agent.visible(),
|
||||||
onSelect: () => local.agent.move(-1),
|
onSelect: () => local.agent.move(-1),
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -468,7 +468,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
id: "file.open",
|
id: "file.open",
|
||||||
title: language.t("command.file.open"),
|
title: language.t("command.file.open"),
|
||||||
description: language.t("palette.search.placeholder"),
|
description: language.t("palette.search.placeholder"),
|
||||||
keybind: "mod+k,mod+p",
|
keybind: "mod+p",
|
||||||
slash: "open",
|
slash: "open",
|
||||||
onSelect: openFile,
|
onSelect: openFile,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
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,11 +31,13 @@ function run(target) {
|
|||||||
|
|
||||||
const envPath = process.env.OPENCODE_BIN_PATH
|
const envPath = process.env.OPENCODE_BIN_PATH
|
||||||
const scriptDir = path.dirname(fs.realpathSync(__filename))
|
const scriptDir = path.dirname(fs.realpathSync(__filename))
|
||||||
const cached = path.join(scriptDir, ".opencode2")
|
const command = path.basename(__filename).replace(/\.cjs$/, "")
|
||||||
|
const nodeBuild = command === "opencode2-node"
|
||||||
|
const cached = path.join(scriptDir, `.${command}`)
|
||||||
const platform = { darwin: "darwin", linux: "linux", win32: "windows" }[os.platform()] || os.platform()
|
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 arch = { x64: "x64", arm64: "arm64", arm: "arm" }[os.arch()] || os.arch()
|
||||||
const base = "@opencode-ai/cli-" + platform + "-" + arch
|
const base = `@opencode-ai/cli${nodeBuild ? "-node" : ""}-` + platform + "-" + arch
|
||||||
const binary = platform === "windows" ? "opencode2.exe" : "opencode2"
|
const binary = platform === "windows" ? `${command}.exe` : command
|
||||||
|
|
||||||
function supportsAvx2() {
|
function supportsAvx2() {
|
||||||
if (arch !== "x64") return false
|
if (arch !== "x64") return false
|
||||||
@@ -77,6 +79,7 @@ function supportsAvx2() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const names = (() => {
|
const names = (() => {
|
||||||
|
if (nodeBuild) return [base]
|
||||||
const baseline = arch === "x64" && !supportsAvx2()
|
const baseline = arch === "x64" && !supportsAvx2()
|
||||||
if (platform === "linux") {
|
if (platform === "linux") {
|
||||||
const musl = (() => {
|
const musl = (() => {
|
||||||
@@ -121,7 +124,7 @@ function findBinary(startDir) {
|
|||||||
const resolved = envPath || (fs.existsSync(cached) ? cached : findBinary(scriptDir))
|
const resolved = envPath || (fs.existsSync(cached) ? cached : findBinary(scriptDir))
|
||||||
if (!resolved) {
|
if (!resolved) {
|
||||||
console.error(
|
console.error(
|
||||||
"It seems that your package manager failed to install the right opencode2 CLI package. Try manually installing " +
|
`It seems that your package manager failed to install the right ${command} CLI package. Try manually installing ` +
|
||||||
names.map((name) => `"${name}"`).join(" or ") +
|
names.map((name) => `"${name}"`).join(" or ") +
|
||||||
" package",
|
" package",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/cli",
|
"name": "@opencode-ai/cli",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "bun run script/build.ts",
|
"build": "bun run script/build.ts",
|
||||||
|
"build:node": "bun run script/build-node.ts",
|
||||||
"dev": "bun run src/index.ts",
|
"dev": "bun run src/index.ts",
|
||||||
"test": "bun test --timeout 30000 --only-failures",
|
"test": "bun test --timeout 30000 --only-failures",
|
||||||
"typecheck": "tsgo --noEmit"
|
"typecheck": "tsgo --noEmit"
|
||||||
@@ -51,7 +52,8 @@
|
|||||||
"semver": "catalog:",
|
"semver": "catalog:",
|
||||||
"solid-js": "catalog:",
|
"solid-js": "catalog:",
|
||||||
"strip-ansi": "7.1.2",
|
"strip-ansi": "7.1.2",
|
||||||
"uqr": "0.1.3"
|
"uqr": "0.1.3",
|
||||||
|
"ws": "8.21.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@opencode-ai/script": "workspace:*",
|
"@opencode-ai/script": "workspace:*",
|
||||||
@@ -59,6 +61,19 @@
|
|||||||
"@tsconfig/bun": "catalog:",
|
"@tsconfig/bun": "catalog:",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
"@types/semver": "catalog:",
|
"@types/semver": "catalog:",
|
||||||
"@typescript/native-preview": "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:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
#!/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,7 +1,6 @@
|
|||||||
#!/usr/bin/env bun
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
import { $ } from "bun"
|
import { $ } from "bun"
|
||||||
import fs from "fs"
|
|
||||||
import { rm } from "fs/promises"
|
import { rm } from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Script } from "@opencode-ai/script"
|
import { Script } from "@opencode-ai/script"
|
||||||
@@ -11,9 +10,14 @@ import { modelsData } from "./generate"
|
|||||||
|
|
||||||
const dir = path.resolve(import.meta.dirname, "..")
|
const dir = path.resolve(import.meta.dirname, "..")
|
||||||
const binary = "opencode2"
|
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)
|
process.chdir(dir)
|
||||||
|
|
||||||
await rm("dist", { recursive: true, force: true })
|
await rm(outdir, { recursive: true, force: true })
|
||||||
|
|
||||||
const singleFlag = process.argv.includes("--single")
|
const singleFlag = process.argv.includes("--single")
|
||||||
const baselineFlag = process.argv.includes("--baseline")
|
const baselineFlag = process.argv.includes("--baseline")
|
||||||
@@ -50,10 +54,6 @@ const targets = singleFlag
|
|||||||
|
|
||||||
if (!skipInstall) await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}`
|
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) {
|
for (const item of targets) {
|
||||||
const target = [
|
const target = [
|
||||||
binary,
|
binary,
|
||||||
@@ -67,7 +67,7 @@ for (const item of targets) {
|
|||||||
const name = target.replace(binary, "cli")
|
const name = target.replace(binary, "cli")
|
||||||
console.log(`building ${name}`)
|
console.log(`building ${name}`)
|
||||||
const result = await Bun.build({
|
const result = await Bun.build({
|
||||||
entrypoints: ["./src/index.ts", parserWorker],
|
entrypoints: ["./src/index.ts"],
|
||||||
tsconfig: "./tsconfig.json",
|
tsconfig: "./tsconfig.json",
|
||||||
plugins: [plugin],
|
plugins: [plugin],
|
||||||
external: ["node-gyp"],
|
external: ["node-gyp"],
|
||||||
@@ -81,7 +81,7 @@ for (const item of targets) {
|
|||||||
autoloadTsconfig: true,
|
autoloadTsconfig: true,
|
||||||
autoloadPackageJson: true,
|
autoloadPackageJson: true,
|
||||||
target: target.replace(binary, "bun") as Bun.Build.CompileTarget,
|
target: target.replace(binary, "bun") as Bun.Build.CompileTarget,
|
||||||
outfile: `./dist/${name}/bin/${binary}`,
|
outfile: path.join(outdir, name, "bin", binary),
|
||||||
execArgv: [`--user-agent=${binary}/${Script.version}`, "--use-system-ca", "--"],
|
execArgv: [`--user-agent=${binary}/${Script.version}`, "--use-system-ca", "--"],
|
||||||
windows: {},
|
windows: {},
|
||||||
},
|
},
|
||||||
@@ -93,10 +93,6 @@ for (const item of targets) {
|
|||||||
OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "undefined",
|
OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "undefined",
|
||||||
// FFF_LIBC selects the fff native lib variant: "musl" or "gnu".
|
// FFF_LIBC selects the fff native lib variant: "musl" or "gnu".
|
||||||
FFF_LIBC: item.os === "linux" ? `'${item.abi ?? "gnu"}'` : "undefined",
|
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") } : {}),
|
...(item.os === "linux" ? { "process.env.OPENTUI_LIBC": JSON.stringify(item.abi ?? "glibc") } : {}),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -107,7 +103,7 @@ for (const item of targets) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await Bun.write(
|
await Bun.write(
|
||||||
`./dist/${name}/package.json`,
|
path.join(outdir, name, "package.json"),
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{
|
{
|
||||||
name: `@opencode-ai/${name}`,
|
name: `@opencode-ai/${name}`,
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import { readFile } from "node:fs/promises"
|
||||||
|
|
||||||
const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.dev"
|
const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.dev"
|
||||||
|
|
||||||
export const modelsData = process.env.MODELS_DEV_API_JSON
|
export const modelsData = process.env.MODELS_DEV_API_JSON
|
||||||
? await Bun.file(process.env.MODELS_DEV_API_JSON).text()
|
? await readFile(process.env.MODELS_DEV_API_JSON, "utf8")
|
||||||
: await fetch(`${modelsUrl}/api.json`).then((response) => response.text())
|
: await fetch(`${modelsUrl}/api.json`).then((response) => response.text())
|
||||||
|
|
||||||
console.log("Loaded models.dev snapshot")
|
console.log("Loaded models.dev snapshot")
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
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)]))
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
#!/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,37 +18,66 @@ async function publish(dir: string, name: string, version: string) {
|
|||||||
await $`npm publish *.tgz --access public --tag ${Script.channel}`.cwd(dir)
|
await $`npm publish *.tgz --access public --tag ${Script.channel}`.cwd(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
const binaries: Record<string, string> = {}
|
async function publishDistribution(input: { root: string; name: string; binary: string; packagePrefix: string }) {
|
||||||
for (const filepath of new Bun.Glob("*/package.json").scanSync({ cwd: "./dist" })) {
|
const binaries: Record<string, string> = {}
|
||||||
const item = await Bun.file(`./dist/${filepath}`).json()
|
for (const filepath of new Bun.Glob("*/package.json").scanSync({ cwd: input.root })) {
|
||||||
binaries[item.name] = item.version
|
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)
|
||||||
}
|
}
|
||||||
console.log("binaries", binaries)
|
|
||||||
const version = Object.values(binaries)[0]
|
|
||||||
const name = pkg.name
|
|
||||||
|
|
||||||
await $`mkdir -p ./dist/${name}/bin`
|
await publishDistribution({
|
||||||
await $`cp ./bin/opencode2.cjs ./dist/${name}/bin/opencode2`
|
root: "./dist",
|
||||||
await Bun.file(`./dist/${name}/package.json`).write(
|
name: pkg.name,
|
||||||
JSON.stringify(
|
binary: "opencode2",
|
||||||
{
|
packagePrefix: "@opencode-ai/cli-",
|
||||||
name,
|
})
|
||||||
bin: { opencode2: "./bin/opencode2" },
|
await publishDistribution({
|
||||||
version,
|
root: "./dist/node",
|
||||||
license: pkg.license,
|
name: "opencode-node",
|
||||||
repository: { type: "git", url: "git+https://github.com/anomalyco/opencode.git" },
|
binary: "opencode2-node",
|
||||||
os: ["darwin", "linux", "win32"],
|
packagePrefix: "@opencode-ai/cli-node-",
|
||||||
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,9 +7,10 @@ import fs from "node:fs/promises"
|
|||||||
import os from "node:os"
|
import os from "node:os"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
|
|
||||||
const target = `cli-${process.platform === "win32" ? "windows" : process.platform}-${process.arch}`
|
const nodeBuild = process.argv.includes("--node")
|
||||||
const directory = path.join(import.meta.dir, "..", "dist", target, "bin")
|
const target = `cli${nodeBuild ? "-node" : ""}-${process.platform === "win32" ? "windows" : process.platform}-${process.arch}`
|
||||||
const binary = path.join(directory, `opencode2${process.platform === "win32" ? ".exe" : ""}`)
|
const directory = path.join(import.meta.dir, "..", "dist", ...(nodeBuild ? ["node"] : []), target, "bin")
|
||||||
|
const binary = path.join(directory, `opencode2${nodeBuild ? "-node" : ""}${process.platform === "win32" ? ".exe" : ""}`)
|
||||||
if (!(await Bun.file(binary).exists())) throw new Error(`Missing compiled CLI in ${directory}`)
|
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-"))
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-smoke-"))
|
||||||
|
|||||||
@@ -114,6 +114,10 @@ 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("migrate", { description: "Migrate v1 data to v2" }),
|
||||||
Spec.make("mini", {
|
Spec.make("mini", {
|
||||||
description: "Start the minimal interactive interface",
|
description: "Start the minimal interactive interface",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
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 { Global } from "@opencode-ai/core/global"
|
||||||
import { run } from "@opencode-ai/tui"
|
import { run } from "@opencode-ai/tui"
|
||||||
import { Commands } from "../commands"
|
import { Commands } from "../commands"
|
||||||
@@ -75,6 +75,6 @@ export default Runtime.handler(Commands, (input) =>
|
|||||||
: Effect.logInfo(message, tags)
|
: Effect.logInfo(message, tags)
|
||||||
runFork(effect)
|
runFork(effect)
|
||||||
},
|
},
|
||||||
}).pipe(Effect.provide(AppNodeBuilder.build(Global.node)))
|
}).pipe(Effect.provide(LayerNode.compile(Global.node)))
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { EOL } from "node:os"
|
import { EOL } from "node:os"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
|
import { readFile, stat, writeFile } from "node:fs/promises"
|
||||||
import { Effect, Option } from "effect"
|
import { Effect, Option } from "effect"
|
||||||
import { applyEdits, modify } from "jsonc-parser"
|
import { applyEdits, modify } from "jsonc-parser"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
@@ -35,7 +36,7 @@ export default Runtime.handler(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
async function resolveConfigPath(directory: string) {
|
export async function resolveConfigPath(directory: string) {
|
||||||
const candidates = [
|
const candidates = [
|
||||||
path.join(directory, "opencode.json"),
|
path.join(directory, "opencode.json"),
|
||||||
path.join(directory, "opencode.jsonc"),
|
path.join(directory, "opencode.jsonc"),
|
||||||
@@ -43,16 +44,24 @@ async function resolveConfigPath(directory: string) {
|
|||||||
path.join(directory, ".opencode", "opencode.jsonc"),
|
path.join(directory, ".opencode", "opencode.jsonc"),
|
||||||
]
|
]
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
if (await Bun.file(candidate).exists()) return candidate
|
if (
|
||||||
|
await stat(candidate).then(
|
||||||
|
(info) => info.isFile(),
|
||||||
|
() => false,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return candidate
|
||||||
}
|
}
|
||||||
return candidates[0]
|
return candidates[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
async function write(configPath: string, name: string, server: unknown) {
|
async function write(configPath: string, name: string, server: unknown) {
|
||||||
const file = Bun.file(configPath)
|
const text = await readFile(configPath, "utf8").catch((error) => {
|
||||||
const text = (await file.exists()) ? await file.text() : "{}"
|
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return "{}"
|
||||||
|
throw error
|
||||||
|
})
|
||||||
const edits = modify(text, ["mcp", "servers", name], server, {
|
const edits = modify(text, ["mcp", "servers", name], server, {
|
||||||
formattingOptions: { tabSize: 2, insertSpaces: true },
|
formattingOptions: { tabSize: 2, insertSpaces: true },
|
||||||
})
|
})
|
||||||
await Bun.write(configPath, applyEdits(text, edits))
|
await writeFile(configPath, applyEdits(text, edits))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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(
|
export default Runtime.handler(
|
||||||
Commands.commands.serve,
|
Commands.commands.serve,
|
||||||
Effect.fn("cli.serve")(function* (input) {
|
Effect.fnUntraced(function* (input) {
|
||||||
if (input.service && input.stdio) return yield* Effect.fail(new Error("--service and --stdio cannot be combined"))
|
if (input.service && input.stdio) return yield* Effect.fail(new Error("--service and --stdio cannot be combined"))
|
||||||
return yield* ServerProcess.run({
|
return yield* ServerProcess.run({
|
||||||
mode: input.service ? "service" : input.stdio ? "stdio" : "default",
|
mode: input.service ? "service" : input.stdio ? "stdio" : "default",
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { Runtime } from "./framework/runtime"
|
|||||||
import { Observability } from "@opencode-ai/core/observability"
|
import { Observability } from "@opencode-ai/core/observability"
|
||||||
import { Updater } from "./services/updater"
|
import { Updater } from "./services/updater"
|
||||||
import { InstallationChannel, InstallationVersion, InstallationLocal } from "@opencode-ai/core/installation/version"
|
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 { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { AppProcess } from "@opencode-ai/core/process"
|
import { AppProcess } from "@opencode-ai/core/process"
|
||||||
@@ -32,6 +31,9 @@ const Handlers = Runtime.handlers(Commands, {
|
|||||||
auth: () => import("./commands/handlers/mcp/auth"),
|
auth: () => import("./commands/handlers/mcp/auth"),
|
||||||
logout: () => import("./commands/handlers/mcp/logout"),
|
logout: () => import("./commands/handlers/mcp/logout"),
|
||||||
},
|
},
|
||||||
|
plugin: {
|
||||||
|
list: () => import("./commands/handlers/plugin/list"),
|
||||||
|
},
|
||||||
migrate: () => import("./commands/handlers/migrate"),
|
migrate: () => import("./commands/handlers/migrate"),
|
||||||
mini: () => import("./commands/handlers/mini"),
|
mini: () => import("./commands/handlers/mini"),
|
||||||
run: () => import("./commands/handlers/run"),
|
run: () => import("./commands/handlers/run"),
|
||||||
@@ -58,7 +60,7 @@ Effect.logInfo("cli starting", {
|
|||||||
Effect.annotateLogs({ role: "cli" }),
|
Effect.annotateLogs({ role: "cli" }),
|
||||||
Effect.provide(Config.layer),
|
Effect.provide(Config.layer),
|
||||||
Effect.provide(Updater.layer),
|
Effect.provide(Updater.layer),
|
||||||
Effect.provide(AppNodeBuilder.build(LayerNode.group([Global.node, AppProcess.node, Npm.node]))),
|
Effect.provide(LayerNode.compile(LayerNode.group([Global.node, AppProcess.node, Npm.node]))),
|
||||||
Effect.provide(Observability.layer),
|
Effect.provide(Observability.layer),
|
||||||
Effect.provide(NodeServices.layer),
|
Effect.provide(NodeServices.layer),
|
||||||
Effect.scoped,
|
Effect.scoped,
|
||||||
|
|||||||
@@ -633,6 +633,12 @@ export function RunSubagentSelectBody(props: {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.name.toLowerCase() === "up" && menu.selected() === 0) {
|
||||||
|
event.preventDefault()
|
||||||
|
props.onClose()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
handleKey({ event, menu, field: () => field, setQuery, select, close: props.onClose })
|
handleKey({ event, menu, field: () => field, setQuery, select, close: props.onClose })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useTerminalDimensions } from "@opentui/solid"
|
|||||||
import { createEffect, createMemo, createSignal, type Accessor } from "solid-js"
|
import { createEffect, createMemo, createSignal, type Accessor } from "solid-js"
|
||||||
import { transparent, type RunFooterTheme } from "./theme"
|
import { transparent, type RunFooterTheme } from "./theme"
|
||||||
import { Locale } from "@opencode-ai/tui/util/locale"
|
import { Locale } from "@opencode-ai/tui/util/locale"
|
||||||
|
import { stringWidth } from "@opencode-ai/tui/util/string-width"
|
||||||
|
|
||||||
export const FOOTER_MENU_ROWS = 8
|
export const FOOTER_MENU_ROWS = 8
|
||||||
|
|
||||||
@@ -196,7 +197,7 @@ export function RunFooterMenu(props: {
|
|||||||
...props
|
...props
|
||||||
.items()
|
.items()
|
||||||
.filter((item) => item.description)
|
.filter((item) => item.description)
|
||||||
.map((item) => Bun.stringWidth(item.display)),
|
.map((item) => stringWidth(item.display)),
|
||||||
)
|
)
|
||||||
return width === 0 ? 0 : width + 2
|
return width === 0 ? 0 : width + 2
|
||||||
})
|
})
|
||||||
@@ -205,14 +206,14 @@ export function RunFooterMenu(props: {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return " ".repeat(Math.max(1, descriptionColumn() - Bun.stringWidth(item.display)))
|
return " ".repeat(Math.max(1, descriptionColumn() - stringWidth(item.display)))
|
||||||
}
|
}
|
||||||
const descriptionText = (item: RunFooterMenuItem) => {
|
const descriptionText = (item: RunFooterMenuItem) => {
|
||||||
if (!item.description) {
|
if (!item.description) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const footerWidth = item.footer ? Bun.stringWidth(item.footer) + 1 : 0
|
const footerWidth = item.footer ? stringWidth(item.footer) + 1 : 0
|
||||||
const available =
|
const available =
|
||||||
term().width -
|
term().width -
|
||||||
(border() ? 1 : 0) -
|
(border() ? 1 : 0) -
|
||||||
|
|||||||
@@ -5,14 +5,15 @@
|
|||||||
// It produces a PromptState that RunPromptBody renders as a slim single-line
|
// It produces a PromptState that RunPromptBody renders as a slim single-line
|
||||||
// composer while the footer view renders any active menus below it.
|
// composer while the footer view renders any active menus below it.
|
||||||
/** @jsxImportSource @opentui/solid */
|
/** @jsxImportSource @opentui/solid */
|
||||||
import { pathToFileURL } from "bun"
|
|
||||||
import { StyledText, fg, type ColorInput, type KeyEvent, type TextareaRenderable } from "@opentui/core"
|
import { StyledText, fg, type ColorInput, type KeyEvent, type TextareaRenderable } from "@opentui/core"
|
||||||
import { useRenderer } from "@opentui/solid"
|
import { useRenderer } from "@opentui/solid"
|
||||||
import { normalizePromptContent } from "@opencode-ai/tui/prompt/content"
|
import { normalizePromptContent } from "@opencode-ai/tui/prompt/content"
|
||||||
import fuzzysort from "fuzzysort"
|
import fuzzysort from "fuzzysort"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
import { pathToFileURL } from "node:url"
|
||||||
import { createEffect, createMemo, createResource, createSignal, onCleanup, onMount, type Accessor } from "solid-js"
|
import { createEffect, createMemo, createResource, createSignal, onCleanup, onMount, type Accessor } from "solid-js"
|
||||||
import { Locale } from "@opencode-ai/tui/util/locale"
|
import { Locale } from "@opencode-ai/tui/util/locale"
|
||||||
|
import { stringWidth } from "@opencode-ai/tui/util/string-width"
|
||||||
import {
|
import {
|
||||||
createPromptHistory,
|
createPromptHistory,
|
||||||
displayCharAt,
|
displayCharAt,
|
||||||
@@ -23,7 +24,7 @@ import {
|
|||||||
movePromptHistory,
|
movePromptHistory,
|
||||||
pushPromptHistory,
|
pushPromptHistory,
|
||||||
} from "./prompt.shared"
|
} from "./prompt.shared"
|
||||||
import { OPENCODE_BASE_MODE, useBindings } from "@opencode-ai/tui/keymap"
|
import { Keymap } from "@opencode-ai/tui/context/keymap"
|
||||||
import { realignEditorPromptParts, resolveEditorSlashValue } from "./prompt.editor"
|
import { realignEditorPromptParts, resolveEditorSlashValue } from "./prompt.editor"
|
||||||
import { FOOTER_MENU_ROWS, createFooterMenuState, type RunFooterMenuItem } from "./footer.menu"
|
import { FOOTER_MENU_ROWS, createFooterMenuState, type RunFooterMenuItem } from "./footer.menu"
|
||||||
import type { RunFooterTheme } from "./theme"
|
import type { RunFooterTheme } from "./theme"
|
||||||
@@ -602,7 +603,7 @@ export function createPromptState(input: PromptInput): PromptState {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const restore = (value: RunPrompt, cursor = Bun.stringWidth(value.text)) => {
|
const restore = (value: RunPrompt, cursor = stringWidth(value.text)) => {
|
||||||
draft = clonePrompt(value)
|
draft = clonePrompt(value)
|
||||||
setShell(value.mode === "shell")
|
setShell(value.mode === "shell")
|
||||||
if (!area || area.isDestroyed) {
|
if (!area || area.isDestroyed) {
|
||||||
@@ -612,7 +613,7 @@ export function createPromptState(input: PromptInput): PromptState {
|
|||||||
hide()
|
hide()
|
||||||
area.setText(value.text)
|
area.setText(value.text)
|
||||||
restoreParts(value.parts)
|
restoreParts(value.parts)
|
||||||
area.cursorOffset = Math.min(cursor, Bun.stringWidth(area.plainText))
|
area.cursorOffset = Math.min(cursor, stringWidth(area.plainText))
|
||||||
scheduleRows()
|
scheduleRows()
|
||||||
area.focus()
|
area.focus()
|
||||||
}
|
}
|
||||||
@@ -643,7 +644,7 @@ export function createPromptState(input: PromptInput): PromptState {
|
|||||||
area.setText(text)
|
area.setText(text)
|
||||||
clearParts()
|
clearParts()
|
||||||
draft = shell() ? { text: area.plainText, parts: [], mode: "shell" } : { text: area.plainText, parts: [] }
|
draft = shell() ? { text: area.plainText, parts: [], mode: "shell" } : { text: area.plainText, parts: [] }
|
||||||
area.cursorOffset = Math.min(Bun.stringWidth(text), Bun.stringWidth(area.plainText))
|
area.cursorOffset = Math.min(stringWidth(text), stringWidth(area.plainText))
|
||||||
scheduleRows()
|
scheduleRows()
|
||||||
area.focus()
|
area.focus()
|
||||||
}
|
}
|
||||||
@@ -777,7 +778,7 @@ export function createPromptState(input: PromptInput): PromptState {
|
|||||||
if (move(dir, event)) return
|
if (move(dir, event)) return
|
||||||
if (!area || area.isDestroyed) return false
|
if (!area || area.isDestroyed) return false
|
||||||
|
|
||||||
const endOffset = Bun.stringWidth(area.plainText)
|
const endOffset = stringWidth(area.plainText)
|
||||||
if (dir === -1) {
|
if (dir === -1) {
|
||||||
if (area.cursorOffset === 0) return false
|
if (area.cursorOffset === 0) return false
|
||||||
if (area.visualCursor.visualRow === 0) {
|
if (area.visualCursor.visualRow === 0) {
|
||||||
@@ -886,16 +887,12 @@ export function createPromptState(input: PromptInput): PromptState {
|
|||||||
area.cursorOffset = 0
|
area.cursorOffset = 0
|
||||||
const start = area.logicalCursor
|
const start = area.logicalCursor
|
||||||
area.cursorOffset =
|
area.cursorOffset =
|
||||||
shell() || !head
|
shell() || !head ? cursor : local ? stringWidth(area.plainText) : stringWidth(area.plainText.slice(0, head.end))
|
||||||
? cursor
|
|
||||||
: local
|
|
||||||
? Bun.stringWidth(area.plainText)
|
|
||||||
: Bun.stringWidth(area.plainText.slice(0, head.end))
|
|
||||||
const end = area.logicalCursor
|
const end = area.logicalCursor
|
||||||
|
|
||||||
area.deleteRange(start.row, start.col, end.row, end.col)
|
area.deleteRange(start.row, start.col, end.row, end.col)
|
||||||
area.insertText(text)
|
area.insertText(text)
|
||||||
area.cursorOffset = Bun.stringWidth(text)
|
area.cursorOffset = stringWidth(text)
|
||||||
hide()
|
hide()
|
||||||
syncDraft()
|
syncDraft()
|
||||||
if (!shell()) {
|
if (!shell()) {
|
||||||
@@ -920,7 +917,7 @@ export function createPromptState(input: PromptInput): PromptState {
|
|||||||
|
|
||||||
const text = "@" + next.value
|
const text = "@" + next.value
|
||||||
const startOffset = at()
|
const startOffset = at()
|
||||||
const endOffset = startOffset + Bun.stringWidth(text)
|
const endOffset = startOffset + stringWidth(text)
|
||||||
const part = structuredClone(next.part)
|
const part = structuredClone(next.part)
|
||||||
if (part.type === "agent") {
|
if (part.type === "agent") {
|
||||||
part.source = {
|
part.source = {
|
||||||
@@ -993,93 +990,83 @@ export function createPromptState(input: PromptInput): PromptState {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
useBindings(() => ({
|
Keymap.createLayer(() => ({
|
||||||
mode: OPENCODE_BASE_MODE,
|
|
||||||
enabled: baseBindingsEnabled(),
|
enabled: baseBindingsEnabled(),
|
||||||
commands: [
|
commands: [
|
||||||
{
|
{
|
||||||
name: "prompt.clear",
|
id: "prompt.clear",
|
||||||
title: "Clear prompt or exit",
|
title: "Clear prompt or exit",
|
||||||
category: "Prompt",
|
group: "Prompt",
|
||||||
run() {
|
run() {
|
||||||
if (requestExit()) return
|
if (requestExit()) return
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
bindings: input.tuiConfig.keybinds.get("prompt.clear"),
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
useBindings(() => ({
|
Keymap.createLayer(() => ({
|
||||||
mode: OPENCODE_BASE_MODE,
|
|
||||||
enabled: input.prompt(),
|
enabled: input.prompt(),
|
||||||
commands: [
|
commands: [
|
||||||
{
|
{
|
||||||
name: "session.interrupt",
|
id: "session.interrupt",
|
||||||
title: "Interrupt session",
|
title: "Interrupt session",
|
||||||
category: "Session",
|
group: "Session",
|
||||||
run() {
|
run() {
|
||||||
if (input.onInterrupt()) return
|
if (input.onInterrupt()) return
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
bindings: input.tuiConfig.keybinds.get("session.interrupt"),
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
useBindings(() => ({
|
Keymap.createLayer(() => ({
|
||||||
mode: OPENCODE_BASE_MODE,
|
|
||||||
enabled: input.prompt() && !visible(),
|
enabled: input.prompt() && !visible(),
|
||||||
commands: [
|
commands: [
|
||||||
{
|
{
|
||||||
name: "prompt.editor",
|
id: "prompt.editor",
|
||||||
title: "Open editor",
|
title: "Open editor",
|
||||||
category: "Prompt",
|
group: "Prompt",
|
||||||
run() {
|
run() {
|
||||||
void openEditor()
|
void openEditor()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
bindings: input.tuiConfig.keybinds.get("prompt.editor"),
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
useBindings(() => ({
|
Keymap.createLayer(() => ({
|
||||||
priority: 1,
|
priority: 1,
|
||||||
mode: OPENCODE_BASE_MODE,
|
|
||||||
enabled: input.prompt() && !visible(),
|
enabled: input.prompt() && !visible(),
|
||||||
commands: [
|
commands: [
|
||||||
{
|
{
|
||||||
name: "prompt.history.previous",
|
id: "prompt.history.previous",
|
||||||
title: "Previous prompt history",
|
title: "Previous prompt history",
|
||||||
category: "Prompt",
|
group: "Prompt",
|
||||||
run(ctx: { event: KeyEvent }) {
|
run(_input: string | undefined, event?: KeyEvent) {
|
||||||
return historyCommand(-1, ctx.event)
|
if (!event) return false
|
||||||
|
return historyCommand(-1, event)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "prompt.history.next",
|
id: "prompt.history.next",
|
||||||
title: "Next prompt history",
|
title: "Next prompt history",
|
||||||
category: "Prompt",
|
group: "Prompt",
|
||||||
run(ctx: { event: KeyEvent }) {
|
run(_input: string | undefined, event?: KeyEvent) {
|
||||||
return historyCommand(1, ctx.event)
|
if (!event) return false
|
||||||
|
return historyCommand(1, event)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
bindings: [
|
|
||||||
...input.tuiConfig.keybinds.get("prompt.history.previous"),
|
|
||||||
...input.tuiConfig.keybinds.get("prompt.history.next"),
|
|
||||||
],
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
useBindings(() => ({
|
Keymap.createLayer(() => ({
|
||||||
mode: OPENCODE_BASE_MODE,
|
|
||||||
enabled: input.prompt() && !visible(),
|
enabled: input.prompt() && !visible(),
|
||||||
bindings: [
|
commands: [
|
||||||
{
|
{
|
||||||
key: "!",
|
bind: "!",
|
||||||
desc: "Shell mode",
|
title: "Shell mode",
|
||||||
group: "Prompt",
|
group: "Prompt",
|
||||||
cmd() {
|
run() {
|
||||||
if (shell()) return false
|
if (shell()) return false
|
||||||
if (!area || area.isDestroyed) return false
|
if (!area || area.isDestroyed) return false
|
||||||
if (area.cursorOffset !== 0) return false
|
if (area.cursorOffset !== 0) return false
|
||||||
@@ -1089,21 +1076,20 @@ export function createPromptState(input: PromptInput): PromptState {
|
|||||||
],
|
],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
useBindings(() => ({
|
Keymap.createLayer(() => ({
|
||||||
mode: OPENCODE_BASE_MODE,
|
|
||||||
enabled: input.prompt() && shell() && !visible(),
|
enabled: input.prompt() && shell() && !visible(),
|
||||||
bindings: [
|
commands: [
|
||||||
{
|
{
|
||||||
key: "escape",
|
bind: "escape",
|
||||||
desc: "Exit shell mode",
|
title: "Exit shell mode",
|
||||||
group: "Prompt",
|
group: "Prompt",
|
||||||
cmd: () => setShellMode(false),
|
run: () => setShellMode(false),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "backspace",
|
bind: "backspace",
|
||||||
desc: "Exit shell mode",
|
title: "Exit shell mode",
|
||||||
group: "Prompt",
|
group: "Prompt",
|
||||||
cmd() {
|
run() {
|
||||||
if (!area || area.isDestroyed) return false
|
if (!area || area.isDestroyed) return false
|
||||||
if (area.cursorOffset !== 0) return false
|
if (area.cursorOffset !== 0) return false
|
||||||
setShellMode(false)
|
setShellMode(false)
|
||||||
@@ -1112,32 +1098,31 @@ export function createPromptState(input: PromptInput): PromptState {
|
|||||||
],
|
],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
useBindings(() => ({
|
Keymap.createLayer(() => ({
|
||||||
mode: OPENCODE_BASE_MODE,
|
|
||||||
enabled: input.prompt() && visible(),
|
enabled: input.prompt() && visible(),
|
||||||
commands: [
|
commands: [
|
||||||
{
|
{
|
||||||
name: "prompt.autocomplete.prev",
|
id: "prompt.autocomplete.prev",
|
||||||
title: "Previous autocomplete item",
|
title: "Previous autocomplete item",
|
||||||
category: "Autocomplete",
|
group: "Autocomplete",
|
||||||
run: () => menu.move(-1),
|
run: () => menu.move(-1),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "prompt.autocomplete.next",
|
id: "prompt.autocomplete.next",
|
||||||
title: "Next autocomplete item",
|
title: "Next autocomplete item",
|
||||||
category: "Autocomplete",
|
group: "Autocomplete",
|
||||||
run: () => menu.move(1),
|
run: () => menu.move(1),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "prompt.autocomplete.hide",
|
id: "prompt.autocomplete.hide",
|
||||||
title: "Hide autocomplete",
|
title: "Hide autocomplete",
|
||||||
category: "Autocomplete",
|
group: "Autocomplete",
|
||||||
run: cancelAutocomplete,
|
run: cancelAutocomplete,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "prompt.autocomplete.select",
|
id: "prompt.autocomplete.select",
|
||||||
title: "Select autocomplete item",
|
title: "Select autocomplete item",
|
||||||
category: "Autocomplete",
|
group: "Autocomplete",
|
||||||
run() {
|
run() {
|
||||||
if (mode() === "slash" && options().length === 0) {
|
if (mode() === "slash" && options().length === 0) {
|
||||||
hide()
|
hide()
|
||||||
@@ -1147,9 +1132,9 @@ export function createPromptState(input: PromptInput): PromptState {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "prompt.autocomplete.complete",
|
id: "prompt.autocomplete.complete",
|
||||||
title: "Complete autocomplete item",
|
title: "Complete autocomplete item",
|
||||||
category: "Autocomplete",
|
group: "Autocomplete",
|
||||||
run() {
|
run() {
|
||||||
if (mode() === "slash" && options().length === 0) {
|
if (mode() === "slash" && options().length === 0) {
|
||||||
hide()
|
hide()
|
||||||
@@ -1164,13 +1149,6 @@ 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) => {
|
const onKeyDown = (event: KeyEvent) => {
|
||||||
|
|||||||
@@ -24,12 +24,11 @@
|
|||||||
// Ctrl-c clears a live prompt draft first; otherwise interrupt and exit use a
|
// 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
|
// two-press pattern where the first press shows a hint and the second press
|
||||||
// within 5 seconds actually fires the action.
|
// within 5 seconds actually fires the action.
|
||||||
import { CliRenderEvents, type CliRenderer, type KeyEvent, type Renderable, type TreeSitterClient } from "@opentui/core"
|
import { CliRenderEvents, type CliRenderer, type TreeSitterClient } from "@opentui/core"
|
||||||
import type { Keymap } from "@opentui/keymap"
|
|
||||||
import { render } from "@opentui/solid"
|
import { render } from "@opentui/solid"
|
||||||
import { createComponent, createSignal, type Accessor, type Setter } from "solid-js"
|
import { createComponent, createSignal, type Accessor, type Setter } from "solid-js"
|
||||||
import { createStore, reconcile } from "solid-js/store"
|
import { createStore, reconcile } from "solid-js/store"
|
||||||
import { OpencodeKeymapProvider } from "@opencode-ai/tui/keymap"
|
import { Keymap } from "@opencode-ai/tui/context/keymap"
|
||||||
import { RUN_COMMAND_PANEL_ROWS, RUN_SUBAGENT_PANEL_ROWS } from "./footer.command"
|
import { RUN_COMMAND_PANEL_ROWS, RUN_SUBAGENT_PANEL_ROWS } from "./footer.command"
|
||||||
import { SUBAGENT_INSPECTOR_ROWS } from "./footer.subagent"
|
import { SUBAGENT_INSPECTOR_ROWS } from "./footer.subagent"
|
||||||
import { PROMPT_MAX_ROWS, TEXTAREA_MIN_ROWS } from "./footer.prompt"
|
import { PROMPT_MAX_ROWS, TEXTAREA_MIN_ROWS } from "./footer.prompt"
|
||||||
@@ -82,7 +81,6 @@ type RunFooterOptions = {
|
|||||||
first: boolean
|
first: boolean
|
||||||
history?: RunPrompt[]
|
history?: RunPrompt[]
|
||||||
theme: RunTheme
|
theme: RunTheme
|
||||||
keymap: Keymap<Renderable, KeyEvent>
|
|
||||||
tuiConfig: RunTuiConfig
|
tuiConfig: RunTuiConfig
|
||||||
diffStyle: RunDiffStyle
|
diffStyle: RunDiffStyle
|
||||||
onPermissionReply: (input: PermissionReply) => void | Promise<void>
|
onPermissionReply: (input: PermissionReply) => void | Promise<void>
|
||||||
@@ -305,8 +303,8 @@ export class RunFooter implements FooterApi {
|
|||||||
const footer = this
|
const footer = this
|
||||||
void render(
|
void render(
|
||||||
() =>
|
() =>
|
||||||
createComponent(OpencodeKeymapProvider, {
|
createComponent(Keymap.Provider, {
|
||||||
keymap: options.keymap,
|
config: options.tuiConfig,
|
||||||
get children() {
|
get children() {
|
||||||
return createComponent(RunFooterView, {
|
return createComponent(RunFooterView, {
|
||||||
directory: options.directory,
|
directory: options.directory,
|
||||||
|
|||||||
@@ -27,14 +27,8 @@ import { RunPromptBody, createPromptState } from "./footer.prompt"
|
|||||||
import { RunPermissionBody } from "./footer.permission"
|
import { RunPermissionBody } from "./footer.permission"
|
||||||
import { RunQuestionBody } from "./footer.question"
|
import { RunQuestionBody } from "./footer.question"
|
||||||
import { footerWidthPolicy } from "./footer.width"
|
import { footerWidthPolicy } from "./footer.width"
|
||||||
import {
|
import { Keymap } from "@opencode-ai/tui/context/keymap"
|
||||||
OPENCODE_BASE_MODE,
|
|
||||||
formatKeyBindings,
|
|
||||||
formatKeySequence,
|
|
||||||
useBindings,
|
|
||||||
useKeymapSelector,
|
|
||||||
type OpenTuiKeymap,
|
|
||||||
} from "@opencode-ai/tui/keymap"
|
|
||||||
import type {
|
import type {
|
||||||
FooterPromptRoute,
|
FooterPromptRoute,
|
||||||
FooterQueuedPrompt,
|
FooterQueuedPrompt,
|
||||||
@@ -177,75 +171,15 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||||||
const current = route()
|
const current = route()
|
||||||
return current.type === "subagent" ? subagent().details[current.sessionID] : undefined
|
return current.type === "subagent" ? subagent().details[current.sessionID] : undefined
|
||||||
})
|
})
|
||||||
const command = useKeymapSelector(
|
const shortcuts = Keymap.useShortcuts()
|
||||||
(keymap: OpenTuiKeymap) =>
|
const command = () => shortcuts.get("command.palette.show") ?? ""
|
||||||
formatKeySequence(
|
const subagentShortcut = () => shortcuts.get("session.child.first") ?? ""
|
||||||
keymap
|
const queuedShortcut = () => shortcuts.get("session.queued_prompts") ?? ""
|
||||||
.getCommandBindings({ visibility: "registered", commands: ["command.palette.show"] })
|
const backgroundShortcut = () => shortcuts.get("session.background") ?? ""
|
||||||
.get("command.palette.show")?.[0]?.sequence,
|
const subagentInterruptShortcut = () => shortcuts.get("subagent.interrupt") ?? ""
|
||||||
props.tuiConfig,
|
const interrupt = () => shortcuts.get("session.interrupt") ?? ""
|
||||||
) ?? "",
|
const variantCycle = () => shortcuts.all("variant.cycle") ?? ""
|
||||||
)
|
const clearShortcut = () => shortcuts.get("prompt.clear") ?? ""
|
||||||
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 busy = createMemo(() => props.state().phase === "running")
|
||||||
const armed = createMemo(() => props.state().interrupt > 0)
|
const armed = createMemo(() => props.state().interrupt > 0)
|
||||||
const exiting = createMemo(() => props.state().exit > 0)
|
const exiting = createMemo(() => props.state().exit > 0)
|
||||||
@@ -504,74 +438,62 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||||||
props.onRequestExit?.(undefined)
|
props.onRequestExit?.(undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
useBindings(() => ({
|
Keymap.createLayer(() => ({
|
||||||
mode: OPENCODE_BASE_MODE,
|
|
||||||
enabled: active().type === "prompt" && route().type === "composer" && !composer.visible(),
|
enabled: active().type === "prompt" && route().type === "composer" && !composer.visible(),
|
||||||
commands: [
|
commands: [
|
||||||
{
|
{
|
||||||
name: "command.palette.show",
|
id: "command.palette.show",
|
||||||
title: "Open command palette",
|
title: "Open command palette",
|
||||||
category: "Prompt",
|
group: "Prompt",
|
||||||
run: openCommand,
|
run: openCommand,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "variant.cycle",
|
id: "variant.cycle",
|
||||||
title: "Cycle model variant",
|
title: "Cycle model variant",
|
||||||
category: "Model",
|
group: "Model",
|
||||||
run: props.onCycle,
|
run: props.onCycle,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
bindings: [
|
|
||||||
...props.tuiConfig.keybinds.get("command.palette.show"),
|
|
||||||
...props.tuiConfig.keybinds.get("variant.cycle"),
|
|
||||||
],
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
useBindings(() => ({
|
Keymap.createLayer(() => ({
|
||||||
mode: OPENCODE_BASE_MODE,
|
|
||||||
enabled: active().type === "prompt" && route().type === "composer" && foregroundSubagents() && !!props.onBackground,
|
enabled: active().type === "prompt" && route().type === "composer" && foregroundSubagents() && !!props.onBackground,
|
||||||
priority: 1,
|
priority: 1,
|
||||||
commands: [
|
commands: [
|
||||||
{
|
{
|
||||||
name: "session.background",
|
id: "session.background",
|
||||||
title: "Background subagents",
|
title: "Background subagents",
|
||||||
category: "Session",
|
group: "Session",
|
||||||
run: () => props.onBackground?.(),
|
run: () => props.onBackground?.(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
bindings: props.tuiConfig.keybinds.get("session.background"),
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
useBindings(() => ({
|
Keymap.createLayer(() => ({
|
||||||
mode: OPENCODE_BASE_MODE,
|
|
||||||
enabled: active().type === "prompt" && route().type === "composer" && tabs().length > 0,
|
enabled: active().type === "prompt" && route().type === "composer" && tabs().length > 0,
|
||||||
commands: [
|
commands: [
|
||||||
{
|
{
|
||||||
name: "session.child.first",
|
id: "session.child.first",
|
||||||
title: "View subagents",
|
title: "View subagents",
|
||||||
category: "Session",
|
group: "Session",
|
||||||
run: openSubagentMenu,
|
run: openSubagentMenu,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
bindings: props.tuiConfig.keybinds.get("session.child.first"),
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
useBindings(() => ({
|
Keymap.createLayer(() => ({
|
||||||
mode: OPENCODE_BASE_MODE,
|
|
||||||
enabled: active().type === "prompt" && route().type === "composer" && queuedPrompts().length > 0,
|
enabled: active().type === "prompt" && route().type === "composer" && queuedPrompts().length > 0,
|
||||||
commands: [
|
commands: [
|
||||||
{
|
{
|
||||||
name: "session.queued_prompts",
|
id: "session.queued_prompts",
|
||||||
title: "Manage queued prompts",
|
title: "Manage queued prompts",
|
||||||
category: "Session",
|
group: "Session",
|
||||||
run: openQueuedMenu,
|
run: openQueuedMenu,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
bindings: props.tuiConfig.keybinds.get("session.queued_prompts"),
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
useBindings(() => ({
|
Keymap.createLayer(() => ({
|
||||||
mode: OPENCODE_BASE_MODE,
|
|
||||||
enabled:
|
enabled:
|
||||||
active().type === "prompt" &&
|
active().type === "prompt" &&
|
||||||
route().type === "subagent" &&
|
route().type === "subagent" &&
|
||||||
@@ -580,9 +502,10 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||||||
priority: 1,
|
priority: 1,
|
||||||
commands: [
|
commands: [
|
||||||
{
|
{
|
||||||
name: "subagent.interrupt",
|
id: "subagent.interrupt",
|
||||||
title: "Interrupt subagent",
|
title: "Interrupt subagent",
|
||||||
category: "Session",
|
group: "Session",
|
||||||
|
bind: "ctrl+d",
|
||||||
run: () => {
|
run: () => {
|
||||||
const current = selectedTab()
|
const current = selectedTab()
|
||||||
if (current?.status !== "running") {
|
if (current?.status !== "running") {
|
||||||
@@ -593,7 +516,6 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
bindings: [{ key: "ctrl+d", desc: "Interrupt subagent", group: "Subagents", cmd: "subagent.interrupt" }],
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { ServerConnection } from "../services/server-connection"
|
|||||||
import { waitForCatalogReady } from "./catalog.shared"
|
import { waitForCatalogReady } from "./catalog.shared"
|
||||||
import { INTERACTIVE_INPUT_ERROR, resolveInteractiveStdin } from "./runtime.stdin"
|
import { INTERACTIVE_INPUT_ERROR, resolveInteractiveStdin } from "./runtime.stdin"
|
||||||
import type { RunInput, RunTuiConfig } from "./types"
|
import type { RunInput, RunTuiConfig } from "./types"
|
||||||
|
import { readStdin } from "../util/io"
|
||||||
|
import { setTimeout } from "node:timers/promises"
|
||||||
|
|
||||||
export type MiniCommandInput = {
|
export type MiniCommandInput = {
|
||||||
server: ServerConnection.Resolved
|
server: ServerConnection.Resolved
|
||||||
@@ -22,7 +24,7 @@ export type MiniCommandInput = {
|
|||||||
type Session = Awaited<ReturnType<OpenCodeClient["session"]["get"]>>
|
type Session = Awaited<ReturnType<OpenCodeClient["session"]["get"]>>
|
||||||
export async function runMini(input: MiniCommandInput) {
|
export async function runMini(input: MiniCommandInput) {
|
||||||
validate(input)
|
validate(input)
|
||||||
const initialInput = mergeInput(process.stdin.isTTY ? undefined : await Bun.stdin.text(), input.prompt)
|
const initialInput = mergeInput(process.stdin.isTTY ? undefined : await readStdin(), input.prompt)
|
||||||
const runtimeTask = import("./runtime")
|
const runtimeTask = import("./runtime")
|
||||||
const directory = localDirectory()
|
const directory = localDirectory()
|
||||||
|
|
||||||
@@ -123,7 +125,7 @@ async function validateAgent(sdk: OpenCodeClient, directory: string, name?: stri
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (agent) return name
|
if (agent) return name
|
||||||
await Bun.sleep(25)
|
await setTimeout(25)
|
||||||
}
|
}
|
||||||
if (!agents) {
|
if (!agents) {
|
||||||
warning("failed to list agents. Falling back to default agent")
|
warning("failed to list agents. Falling back to default agent")
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { EventSubscribeOutput, OpenCodeClient } from "@opencode-ai/client/promise"
|
import type { EventSubscribeOutput, OpenCodeClient } from "@opencode-ai/client/promise"
|
||||||
import { SessionMessage } from "@opencode-ai/schema/session-message"
|
import { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||||
import { EOL } from "node:os"
|
import { EOL } from "node:os"
|
||||||
|
import { readFile } from "node:fs/promises"
|
||||||
import { UI } from "./ui"
|
import { UI } from "./ui"
|
||||||
import type { MiniToolPart } from "./types"
|
import type { MiniToolPart } from "./types"
|
||||||
|
|
||||||
@@ -478,11 +479,11 @@ async function prepareFile(file: File) {
|
|||||||
if (file.mime !== "text/plain") {
|
if (file.mime !== "text/plain") {
|
||||||
const uri = file.url.startsWith("data:")
|
const uri = file.url.startsWith("data:")
|
||||||
? file.url
|
? file.url
|
||||||
: `data:${file.mime};base64,${Buffer.from(await Bun.file(new URL(file.url)).arrayBuffer()).toString("base64")}`
|
: `data:${file.mime};base64,${(await readFile(new URL(file.url))).toString("base64")}`
|
||||||
return { attachment: { uri, mime: file.mime, name: file.filename } }
|
return { attachment: { uri, mime: file.mime, name: file.filename } }
|
||||||
}
|
}
|
||||||
const content = file.url.startsWith("data:")
|
const content = file.url.startsWith("data:")
|
||||||
? Buffer.from(file.url.slice(file.url.indexOf(",") + 1), "base64").toString("utf8")
|
? Buffer.from(file.url.slice(file.url.indexOf(",") + 1), "base64").toString("utf8")
|
||||||
: await Bun.file(new URL(file.url)).text()
|
: await readFile(new URL(file.url), "utf8")
|
||||||
return { text: `<file name="${file.filename}">\n${content}\n</file>` }
|
return { text: `<file name="${file.filename}">\n${content}\n</file>` }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
// the current draft is saved and history begins. Arrowing past the end
|
// the current draft is saved and history begins. Arrowing past the end
|
||||||
// restores the draft.
|
// restores the draft.
|
||||||
export { displayCharAt, displaySlice, mentionTriggerIndex } from "@opencode-ai/tui/prompt/display"
|
export { displayCharAt, displaySlice, mentionTriggerIndex } from "@opencode-ai/tui/prompt/display"
|
||||||
|
import { stringWidth } from "@opencode-ai/tui/util/string-width"
|
||||||
import type { RunPrompt } from "./types"
|
import type { RunPrompt } from "./types"
|
||||||
|
|
||||||
const HISTORY_LIMIT = 200
|
const HISTORY_LIMIT = 200
|
||||||
@@ -102,7 +103,7 @@ export function movePromptHistory(state: PromptHistoryState, dir: -1 | 1, text:
|
|||||||
return { state, apply: false }
|
return { state, apply: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dir === 1 && cursor !== Bun.stringWidth(text)) {
|
if (dir === 1 && cursor !== stringWidth(text)) {
|
||||||
return { state, apply: false }
|
return { state, apply: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +137,7 @@ export function movePromptHistory(state: PromptHistoryState, dir: -1 | 1, text:
|
|||||||
index: null,
|
index: null,
|
||||||
},
|
},
|
||||||
text: state.draft,
|
text: state.draft,
|
||||||
cursor: Bun.stringWidth(state.draft),
|
cursor: stringWidth(state.draft),
|
||||||
apply: true,
|
apply: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,7 +148,7 @@ export function movePromptHistory(state: PromptHistoryState, dir: -1 | 1, text:
|
|||||||
index: idx,
|
index: idx,
|
||||||
},
|
},
|
||||||
text: state.items[idx].text,
|
text: state.items[idx].text,
|
||||||
cursor: dir === -1 ? 0 : Bun.stringWidth(state.items[idx].text),
|
cursor: dir === -1 ? 0 : stringWidth(state.items[idx].text),
|
||||||
apply: true,
|
apply: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { FSUtil } from "@opencode-ai/core/fs-util"
|
|||||||
import { Model } from "@opencode-ai/schema/model"
|
import { Model } from "@opencode-ai/schema/model"
|
||||||
import { open } from "node:fs/promises"
|
import { open } from "node:fs/promises"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
|
import { readStdin } from "../util/io"
|
||||||
import { ServerConnection } from "../services/server-connection"
|
import { ServerConnection } from "../services/server-connection"
|
||||||
import { loadRunAgents, waitForCatalogReady } from "./catalog.shared"
|
import { loadRunAgents, waitForCatalogReady } from "./catalog.shared"
|
||||||
import { runNonInteractivePrompt } from "./noninteractive"
|
import { runNonInteractivePrompt } from "./noninteractive"
|
||||||
@@ -48,7 +49,7 @@ async function run(input: RunCommandInput) {
|
|||||||
if (input.fork && !input.continue && !input.session) fail("--fork requires --continue or --session")
|
if (input.fork && !input.continue && !input.session) fail("--fork requires --continue or --session")
|
||||||
const root = process.env.PWD ?? process.cwd()
|
const root = process.env.PWD ?? process.cwd()
|
||||||
const directory = localDirectory(root)
|
const directory = localDirectory(root)
|
||||||
const message = mergeInput(formatMessage(input.message), process.stdin.isTTY ? undefined : await Bun.stdin.text())
|
const message = mergeInput(formatMessage(input.message), process.stdin.isTTY ? undefined : await readStdin())
|
||||||
if (!message?.trim()) fail("You must provide a message")
|
if (!message?.trim()) fail("You must provide a message")
|
||||||
const files = await Promise.all(input.file.map((file) => prepareFile(file, root)))
|
const files = await Promise.all(input.file.map((file) => prepareFile(file, root)))
|
||||||
const prepared = { directory, message, files }
|
const prepared = { directory, message, files }
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
// none block each other.
|
// none block each other.
|
||||||
import { Context, Effect, Layer } from "effect"
|
import { Context, Effect, Layer } from "effect"
|
||||||
import { resolve } from "@opencode-ai/tui/config/v1"
|
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 { 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 { makeRuntime } from "@opencode-ai/core/effect/runtime"
|
||||||
import { loadRunProviders } from "./catalog.shared"
|
import { loadRunProviders } from "./catalog.shared"
|
||||||
import { resolveCurrentSession, sessionHistory } from "./session.shared"
|
import { resolveCurrentSession, sessionHistory } from "./session.shared"
|
||||||
@@ -130,7 +130,7 @@ const layer = Layer.effect(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const node = makeGlobalNode({ service: Service, layer, deps: [] })
|
const node = makeGlobalNode({ service: Service, layer, deps: [] })
|
||||||
const runtime = makeRuntime(Service, AppNodeBuilder.build(node))
|
const runtime = makeRuntime(Service, LayerNode.compile(node))
|
||||||
|
|
||||||
// Fetches available variants and context limits for every provider/model pair.
|
// Fetches available variants and context limits for every provider/model pair.
|
||||||
export async function resolveModelInfo(
|
export async function resolveModelInfo(
|
||||||
|
|||||||
@@ -10,9 +10,7 @@
|
|||||||
// back to the usual two-press exit sequence through RunFooter.requestExit().
|
// back to the usual two-press exit sequence through RunFooter.requestExit().
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { CliRenderEvents, createCliRenderer, type CliRenderer, type ScrollbackWriter } from "@opentui/core"
|
import { CliRenderEvents, createCliRenderer, type CliRenderer, type ScrollbackWriter } from "@opentui/core"
|
||||||
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
|
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { registerOpencodeKeymap } from "@opencode-ai/tui/keymap"
|
|
||||||
import { isDefaultTitle } from "@opencode-ai/tui/util/session"
|
import { isDefaultTitle } from "@opencode-ai/tui/util/session"
|
||||||
import { Locale } from "@opencode-ai/tui/util/locale"
|
import { Locale } from "@opencode-ai/tui/util/locale"
|
||||||
import { resolveInteractiveStdin } from "./runtime.stdin"
|
import { resolveInteractiveStdin } from "./runtime.stdin"
|
||||||
@@ -167,8 +165,6 @@ function queueSplash(
|
|||||||
export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lifecycle> {
|
export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lifecycle> {
|
||||||
const source = resolveInteractiveStdin()
|
const source = resolveInteractiveStdin()
|
||||||
const footerTask = import("./footer")
|
const footerTask = import("./footer")
|
||||||
let unregisterKeymap: (() => void) | undefined
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const renderer = await createCliRenderer({
|
const renderer = await createCliRenderer({
|
||||||
stdin: source.stdin,
|
stdin: source.stdin,
|
||||||
@@ -187,8 +183,6 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
|
|||||||
})
|
})
|
||||||
const [theme, tuiConfig] = await Promise.all([resolveRunTheme(renderer), input.tuiConfig])
|
const [theme, tuiConfig] = await Promise.all([resolveRunTheme(renderer), input.tuiConfig])
|
||||||
renderer.setBackgroundColor(theme.background)
|
renderer.setBackgroundColor(theme.background)
|
||||||
const keymap = createDefaultOpenTuiKeymap(renderer)
|
|
||||||
unregisterKeymap = registerOpencodeKeymap(keymap, renderer, tuiConfig)
|
|
||||||
const state: SplashState = {
|
const state: SplashState = {
|
||||||
entry: false,
|
entry: false,
|
||||||
exit: false,
|
exit: false,
|
||||||
@@ -233,7 +227,6 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
|
|||||||
history: input.history,
|
history: input.history,
|
||||||
theme,
|
theme,
|
||||||
wrote,
|
wrote,
|
||||||
keymap,
|
|
||||||
tuiConfig,
|
tuiConfig,
|
||||||
diffStyle: tuiConfig.diff_style ?? "auto",
|
diffStyle: tuiConfig.diff_style ?? "auto",
|
||||||
onPermissionReply: input.onPermissionReply,
|
onPermissionReply: input.onPermissionReply,
|
||||||
@@ -333,7 +326,6 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
|
|||||||
footer.close()
|
footer.close()
|
||||||
await footer.idle().catch(() => {})
|
await footer.idle().catch(() => {})
|
||||||
footer.destroy()
|
footer.destroy()
|
||||||
unregisterKeymap?.()
|
|
||||||
shutdown(renderer)
|
shutdown(renderer)
|
||||||
if (!wroteExit) {
|
if (!wroteExit) {
|
||||||
process.stdout.write("\n")
|
process.stdout.write("\n")
|
||||||
@@ -391,7 +383,6 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
|
|||||||
close,
|
close,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
unregisterKeymap?.()
|
|
||||||
source.cleanup?.()
|
source.cleanup?.()
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { readFile } from "node:fs/promises"
|
||||||
import type {
|
import type {
|
||||||
EventSubscribeOutput,
|
EventSubscribeOutput,
|
||||||
OpenCodeClient,
|
OpenCodeClient,
|
||||||
@@ -161,7 +162,7 @@ async function prepareFile(file: RunFilePart) {
|
|||||||
if (file.mime !== "text/plain") return { attachment: { uri: file.url, name: file.filename } }
|
if (file.mime !== "text/plain") return { attachment: { uri: file.url, name: file.filename } }
|
||||||
const content = file.url.startsWith("data:")
|
const content = file.url.startsWith("data:")
|
||||||
? Buffer.from(file.url.slice(file.url.indexOf(",") + 1), "base64").toString("utf8")
|
? Buffer.from(file.url.slice(file.url.indexOf(",") + 1), "base64").toString("utf8")
|
||||||
: await Bun.file(new URL(file.url)).text()
|
: await readFile(new URL(file.url), "utf8")
|
||||||
return { text: `<file name="${file.filename}">\n${content}\n</file>` }
|
return { text: `<file name="${file.filename}">\n${content}\n</file>` }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
// variant and the persisted file.
|
// variant and the persisted file.
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
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 { Context, Effect, Layer } from "effect"
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { makeGlobalNode } from "@opencode-ai/core/effect/app-node"
|
import { makeGlobalNode } from "@opencode-ai/core/effect/app-node"
|
||||||
@@ -203,7 +202,7 @@ const node = makeGlobalNode({ service: Service, layer, deps: [FSUtil.node] })
|
|||||||
|
|
||||||
/** @internal Exported for testing. */
|
/** @internal Exported for testing. */
|
||||||
export function createVariantRuntime(replacements?: readonly LayerNode.Replacement[]): VariantRuntime {
|
export function createVariantRuntime(replacements?: readonly LayerNode.Replacement[]): VariantRuntime {
|
||||||
const runtime = makeRuntime(Service, AppNodeBuilder.build(node, replacements))
|
const runtime = makeRuntime(Service, LayerNode.compile(node, replacements))
|
||||||
return {
|
return {
|
||||||
resolveSavedVariant: (model) => runtime.runPromise((svc) => svc.resolveSavedVariant(model)).catch(() => undefined),
|
resolveSavedVariant: (model) => runtime.runPromise((svc) => svc.resolveSavedVariant(model)).catch(() => undefined),
|
||||||
saveVariant: (model, variant) => runtime.runPromise((svc) => svc.saveVariant(model, variant)).catch(() => {}),
|
saveVariant: (model, variant) => runtime.runPromise((svc) => svc.saveVariant(model, variant)).catch(() => {}),
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
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")
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
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,13 +1,11 @@
|
|||||||
export * as ServerProcess from "./server-process"
|
export * as ServerProcess from "./server-process"
|
||||||
|
|
||||||
import { NodeServices } from "@effect/platform-node"
|
import { NodeServices } from "@effect/platform-node"
|
||||||
import { Service } from "@opencode-ai/client/effect/service"
|
import { Service, type DiscoverOptions, type Info } 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 { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||||
import { AppProcess } from "@opencode-ai/core/process"
|
import { AppProcess } from "@opencode-ai/core/process"
|
||||||
import { ProcessLock } from "@opencode-ai/core/util/process-lock"
|
|
||||||
import { randomBytes, randomUUID } from "node:crypto"
|
import { randomBytes, randomUUID } from "node:crypto"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
import { Effect, FileSystem, Logger, Option, Redacted, Schedule, Schema } from "effect"
|
import { Effect, FileSystem, Logger, Option, Redacted, Schedule, Schema } from "effect"
|
||||||
@@ -24,27 +22,29 @@ export type Options = {
|
|||||||
readonly port?: number
|
readonly port?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const run = Effect.fn("cli.server-process.run")((options: Options) =>
|
// The process effect lives until server shutdown; tracing it would parent every request to one process-lifetime trace.
|
||||||
processEffect(options).pipe(
|
export const run = Effect.fnUntraced(function* (options: Options) {
|
||||||
|
return yield* processEffect(options).pipe(
|
||||||
Effect.provide(Updater.layer),
|
Effect.provide(Updater.layer),
|
||||||
Effect.provide(AppNodeBuilder.build(LayerNode.group([Global.node, AppProcess.node]))),
|
Effect.provide(LayerNode.compile(LayerNode.group([Global.node, AppProcess.node]))),
|
||||||
Effect.provide(NodeServices.layer),
|
Effect.provide(NodeServices.layer),
|
||||||
),
|
)
|
||||||
)
|
})
|
||||||
|
|
||||||
const processEffect = Effect.fnUntraced(function* (options: Options) {
|
const processEffect = Effect.fnUntraced(function* (options: Options) {
|
||||||
if (options.mode === "service") yield* Effect.sync(() => process.chdir(Global.Path.home))
|
if (options.mode === "service") yield* Effect.sync(() => process.chdir(Global.Path.home))
|
||||||
return yield* Effect.scoped(
|
return yield* Effect.scoped(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const serviceOptions = options.mode === "service" ? yield* ServiceConfig.options() : undefined
|
const serviceOptions = options.mode === "service" ? yield* ServiceConfig.options() : undefined
|
||||||
if (serviceOptions !== undefined) {
|
const config = options.mode === "service" ? yield* ServiceConfig.read() : {}
|
||||||
const acquired = yield* ProcessLock.acquire(serviceOptions.file + ".lock").pipe(
|
const hostname = options.hostname ?? config.hostname ?? "127.0.0.1"
|
||||||
Effect.as(true),
|
const port = options.port ?? config.port ?? (options.mode === "service" ? ServiceConfig.defaultPort() : undefined)
|
||||||
Effect.catchTag("ProcessLockHeldError", () => Effect.succeed(false)),
|
if (
|
||||||
)
|
serviceOptions !== undefined &&
|
||||||
if (!acquired) return yield* Effect.void
|
port !== undefined &&
|
||||||
if ((yield* Service.discover(serviceOptions)) !== undefined) return yield* Effect.void
|
(yield* Service.incumbent({ ...serviceOptions, url: serviceURL(hostname, port) })) !== undefined
|
||||||
}
|
)
|
||||||
|
return
|
||||||
const { start } = yield* Effect.promise(() => import("@opencode-ai/server/process"))
|
const { start } = yield* Effect.promise(() => import("@opencode-ai/server/process"))
|
||||||
const environmentPassword = yield* Env.password
|
const environmentPassword = yield* Env.password
|
||||||
// Keep the lease credential out of the environment inherited by tools.
|
// Keep the lease credential out of the environment inherited by tools.
|
||||||
@@ -52,7 +52,6 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
|
|||||||
delete process.env.OPENCODE_PASSWORD
|
delete process.env.OPENCODE_PASSWORD
|
||||||
delete process.env.OPENCODE_SERVER_PASSWORD
|
delete process.env.OPENCODE_SERVER_PASSWORD
|
||||||
}
|
}
|
||||||
const config = options.mode === "service" ? yield* ServiceConfig.read() : {}
|
|
||||||
const password =
|
const password =
|
||||||
options.mode === "service"
|
options.mode === "service"
|
||||||
? yield* ServiceConfig.password()
|
? yield* ServiceConfig.password()
|
||||||
@@ -62,15 +61,36 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
|
|||||||
if (!password) return yield* Effect.fail(new Error("Missing server password"))
|
if (!password) return yield* Effect.fail(new Error("Missing server password"))
|
||||||
const instanceID = randomUUID()
|
const instanceID = randomUUID()
|
||||||
const server = yield* start({
|
const server = yield* start({
|
||||||
hostname: options.hostname ?? config.hostname ?? "127.0.0.1",
|
hostname,
|
||||||
port: Option.fromNullishOr(options.port ?? config.port),
|
port: Option.fromNullishOr(port),
|
||||||
password,
|
password,
|
||||||
instanceID,
|
instanceID,
|
||||||
service:
|
service:
|
||||||
serviceOptions === undefined
|
serviceOptions === undefined
|
||||||
? undefined
|
? undefined
|
||||||
: { onListen: (address) => register(address, password, instanceID, serviceOptions.file) },
|
: {
|
||||||
}).pipe(Effect.provide(Logger.layer([], { mergeWithExisting: false })))
|
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
|
||||||
const url = HttpServer.formatAddress(server.address)
|
const url = HttpServer.formatAddress(server.address)
|
||||||
console.log(options.mode === "stdio" ? JSON.stringify({ url }) : `server listening on ${url}`)
|
console.log(options.mode === "stdio" ? JSON.stringify({ url }) : `server listening on ${url}`)
|
||||||
if (options.mode === "default" && !environmentPassword) console.log(`server password ${password}`)
|
if (options.mode === "default" && !environmentPassword) console.log(`server password ${password}`)
|
||||||
@@ -94,6 +114,7 @@ const register = Effect.fnUntraced(function* (
|
|||||||
password: string,
|
password: string,
|
||||||
id: string,
|
id: string,
|
||||||
file: string,
|
file: string,
|
||||||
|
shutdown: Effect.Effect<void>,
|
||||||
) {
|
) {
|
||||||
const fs = yield* FileSystem.FileSystem
|
const fs = yield* FileSystem.FileSystem
|
||||||
const temp = file + "." + id + ".tmp"
|
const temp = file + "." + id + ".tmp"
|
||||||
@@ -106,38 +127,49 @@ const register = Effect.fnUntraced(function* (
|
|||||||
password,
|
password,
|
||||||
}
|
}
|
||||||
const encoded = yield* encodeInfo(info)
|
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(
|
const current = fs.readFileString(file).pipe(
|
||||||
Effect.flatMap(decodeInfo),
|
Effect.flatMap(decodeInfo),
|
||||||
Effect.orElseSucceed(() => undefined),
|
Effect.orElseSucceed(() => undefined),
|
||||||
)
|
)
|
||||||
const assertRegistration = Effect.gen(function* () {
|
const owns = (found: Info | undefined) =>
|
||||||
const found = yield* current
|
found?.id === info.id &&
|
||||||
if (
|
found.version === info.version &&
|
||||||
found !== undefined &&
|
found.url === info.url &&
|
||||||
found.id === info.id &&
|
found.pid === info.pid &&
|
||||||
found.version === info.version &&
|
found.password === info.password
|
||||||
found.url === info.url &&
|
yield* fs.writeFileString(temp, encoded, { mode: 0o600 }).pipe(Effect.andThen(fs.rename(temp, file)))
|
||||||
found.pid === info.pid &&
|
yield* current.pipe(
|
||||||
found.password === info.password
|
Effect.filterOrFail(owns),
|
||||||
)
|
|
||||||
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.repeat(Schedule.spaced("5 seconds")),
|
||||||
|
Effect.ignore,
|
||||||
|
Effect.andThen(shutdown),
|
||||||
Effect.forkScoped,
|
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() {
|
function waitForStdinClose() {
|
||||||
return Effect.callback<void>((resume) => {
|
return Effect.callback<void>((resume) => {
|
||||||
const close = () => resume(Effect.void)
|
const close = () => resume(Effect.void)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Service } from "@opencode-ai/client/effect/service"
|
|||||||
import { Effect, FileSystem, Option, Schema } from "effect"
|
import { Effect, FileSystem, Option, Schema } from "effect"
|
||||||
import { randomBytes } from "crypto"
|
import { randomBytes } from "crypto"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
import { selfCommand } from "../util/process"
|
||||||
|
|
||||||
// The CLI's service configuration file, plus the Service.EnsureOptions binding that
|
// The CLI's service configuration file, plus the Service.EnsureOptions binding that
|
||||||
// points the client package's service operations at this CLI: which
|
// points the client package's service operations at this CLI: which
|
||||||
@@ -29,6 +30,12 @@ export function filename(channel = InstallationChannel) {
|
|||||||
return `service-${Hash.fast(channel)}.json`
|
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(
|
export function versionBelongsToChannel(
|
||||||
version: string | undefined,
|
version: string | undefined,
|
||||||
channel = InstallationChannel,
|
channel = InstallationChannel,
|
||||||
@@ -78,13 +85,10 @@ const paths = Effect.gen(function* () {
|
|||||||
export const options = Effect.fnUntraced(function* () {
|
export const options = Effect.fnUntraced(function* () {
|
||||||
const { file, legacyFile } = yield* paths
|
const { file, legacyFile } = yield* paths
|
||||||
yield* migrateRegistration(legacyFile, file)
|
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 {
|
return {
|
||||||
file,
|
file,
|
||||||
version: InstallationVersion,
|
version: InstallationVersion,
|
||||||
command: [process.execPath, ...(entrypoint ? [entrypoint] : []), "serve", "--service"],
|
command: [...selfCommand(), "serve", "--service"],
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Service, type Endpoint } from "@opencode-ai/client/effect/service"
|
import { Service, type Endpoint } from "@opencode-ai/client/effect/service"
|
||||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { Effect, Schema, Stream } from "effect"
|
import { Deferred, Effect, Schema, Stream } from "effect"
|
||||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||||
import { randomBytes } from "node:crypto"
|
import { randomBytes } from "node:crypto"
|
||||||
import path from "node:path"
|
import { selfCommand } from "../util/process"
|
||||||
|
|
||||||
const Ready = Schema.Struct({ url: Schema.String })
|
const Ready = Schema.Struct({ url: Schema.String })
|
||||||
const decodeReady = Schema.decodeUnknownPromise(Schema.fromJsonString(Ready))
|
const decodeReady = Schema.decodeUnknownPromise(Schema.fromJsonString(Ready))
|
||||||
@@ -14,10 +14,7 @@ type Options = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function command(password: string, options: Options) {
|
function command(password: string, options: Options) {
|
||||||
const compiled = path.basename(process.execPath).replace(/\.exe$/, "") !== "bun"
|
const [executable, ...args] = options.command ?? [...selfCommand(), "serve"]
|
||||||
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")
|
if (!executable) throw new Error("Failed to resolve standalone server command")
|
||||||
return ChildProcess.make(executable, [...args, "--stdio", "--port", "0"], {
|
return ChildProcess.make(executable, [...args, "--stdio", "--port", "0"], {
|
||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
@@ -39,8 +36,16 @@ const makeEndpoint = Effect.fn("cli.standalone.endpoint")(
|
|||||||
const password = randomBytes(32).toString("base64url")
|
const password = randomBytes(32).toString("base64url")
|
||||||
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
||||||
const proc = yield* spawner.spawn(command(password, options))
|
const proc = yield* spawner.spawn(command(password, options))
|
||||||
const output = yield* proc.stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.take(1), Stream.mkString)
|
const readyLine = yield* Deferred.make<string, Error>()
|
||||||
if (!output) return yield* Effect.fail(new Error("Standalone server exited before reporting readiness"))
|
// 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 ready = yield* Effect.tryPromise(() => decodeReady(output))
|
const ready = yield* Effect.tryPromise(() => decodeReady(output))
|
||||||
return {
|
return {
|
||||||
url: ready.url,
|
url: ready.url,
|
||||||
@@ -48,7 +53,7 @@ const makeEndpoint = Effect.fn("cli.standalone.endpoint")(
|
|||||||
pid: proc.pid,
|
pid: proc.pid,
|
||||||
} satisfies Endpoint & { readonly pid: number }
|
} satisfies Endpoint & { readonly pid: number }
|
||||||
},
|
},
|
||||||
Effect.provide(AppNodeBuilder.build(CrossSpawnSpawner.node)),
|
Effect.provide(LayerNode.compile(CrossSpawnSpawner.node)),
|
||||||
)
|
)
|
||||||
|
|
||||||
export function start(options: Options = {}) {
|
export function start(options: Options = {}) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
|||||||
import { registerOpencodeSpinner } from "@opencode-ai/tui/component/register-spinner"
|
import { registerOpencodeSpinner } from "@opencode-ai/tui/component/register-spinner"
|
||||||
import { SPINNER_FRAMES } from "@opencode-ai/tui/component/spinner"
|
import { SPINNER_FRAMES } from "@opencode-ai/tui/component/spinner"
|
||||||
import { go } from "@opencode-ai/tui/logo"
|
import { go } from "@opencode-ai/tui/logo"
|
||||||
|
import { setTimeout } from "node:timers/promises"
|
||||||
import {
|
import {
|
||||||
batch,
|
batch,
|
||||||
createEffect,
|
createEffect,
|
||||||
@@ -123,7 +124,7 @@ async function open(from?: string): Promise<Session> {
|
|||||||
let shownAt = performance.now()
|
let shownAt = performance.now()
|
||||||
const waitForStage = async () => {
|
const waitForStage = async () => {
|
||||||
const remaining = stageFloor - (performance.now() - shownAt)
|
const remaining = stageFloor - (performance.now() - shownAt)
|
||||||
if (remaining > 0) await Bun.sleep(remaining)
|
if (remaining > 0) await setTimeout(remaining)
|
||||||
}
|
}
|
||||||
const advance = async (stage: number) => {
|
const advance = async (stage: number) => {
|
||||||
await waitForStage()
|
await waitForStage()
|
||||||
@@ -140,11 +141,11 @@ async function open(from?: string): Promise<Session> {
|
|||||||
setOutcome(next)
|
setOutcome(next)
|
||||||
const completed = await Promise.race([
|
const completed = await Promise.race([
|
||||||
settled.promise.then(() => true),
|
settled.promise.then(() => true),
|
||||||
Bun.sleep(transitionDuration + 500).then(() => false),
|
setTimeout(transitionDuration + 500).then(() => false),
|
||||||
])
|
])
|
||||||
resolveOutcome = undefined
|
resolveOutcome = undefined
|
||||||
setAnimating(false)
|
setAnimating(false)
|
||||||
if (completed) await Bun.sleep(hold)
|
if (completed) await setTimeout(hold)
|
||||||
}
|
}
|
||||||
let closing: Promise<void> | undefined
|
let closing: Promise<void> | undefined
|
||||||
let transferred = false
|
let transferred = false
|
||||||
@@ -154,7 +155,7 @@ async function open(from?: string): Promise<Session> {
|
|||||||
setAnimating(false)
|
setAnimating(false)
|
||||||
if (renderer.isDestroyed) return
|
if (renderer.isDestroyed) return
|
||||||
renderer.pause()
|
renderer.pause()
|
||||||
await Promise.race([renderer.idle(), Bun.sleep(500)])
|
await Promise.race([renderer.idle(), setTimeout(500)])
|
||||||
renderer.destroy()
|
renderer.destroy()
|
||||||
})())
|
})())
|
||||||
let loading: Promise<void> | undefined
|
let loading: Promise<void> | undefined
|
||||||
@@ -178,7 +179,7 @@ async function open(from?: string): Promise<Session> {
|
|||||||
renderer.screenMode = "alternate-screen"
|
renderer.screenMode = "alternate-screen"
|
||||||
renderer.consoleMode = "console-overlay"
|
renderer.consoleMode = "console-overlay"
|
||||||
renderer.requestRender()
|
renderer.requestRender()
|
||||||
await Promise.race([renderer.idle(), Bun.sleep(500)])
|
await Promise.race([renderer.idle(), setTimeout(500)])
|
||||||
transferred = true
|
transferred = true
|
||||||
return {
|
return {
|
||||||
renderer,
|
renderer,
|
||||||
|
|||||||
@@ -12,11 +12,16 @@ import { parse, type ParseError } from "jsonc-parser"
|
|||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
import semver from "semver"
|
import semver from "semver"
|
||||||
|
|
||||||
|
declare const OPENCODE_CLI_NAME: string | undefined
|
||||||
|
|
||||||
export type Policy = boolean | "notify"
|
export type Policy = boolean | "notify"
|
||||||
export type Action = "none" | "upgrade"
|
export type Action = "none" | "upgrade"
|
||||||
type Method = "npm" | "pnpm" | "bun" | "yarn"
|
type Method = "npm" | "pnpm" | "bun" | "yarn"
|
||||||
|
|
||||||
const packageName = "@opencode-ai/cli"
|
const packageName =
|
||||||
|
typeof OPENCODE_CLI_NAME === "string" && OPENCODE_CLI_NAME === "opencode2-node"
|
||||||
|
? OPENCODE_CLI_NAME
|
||||||
|
: "@opencode-ai/cli"
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly check: () => Effect.Effect<void>
|
readonly check: () => Effect.Effect<void>
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { text } from "node:stream/consumers"
|
||||||
|
|
||||||
|
export function readStdin() {
|
||||||
|
return text(process.stdin)
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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,10 +1,9 @@
|
|||||||
/** @jsxImportSource @opentui/solid */
|
/** @jsxImportSource @opentui/solid */
|
||||||
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
|
import { testRender } from "@opentui/solid"
|
||||||
import { testRender, useRenderer } from "@opentui/solid"
|
import { Keymap } from "@opencode-ai/tui/context/keymap"
|
||||||
import { OpencodeKeymapProvider, registerOpencodeKeymap } from "@opencode-ai/tui/keymap"
|
|
||||||
import { resolve } from "@opencode-ai/tui/config/v1"
|
import { resolve } from "@opencode-ai/tui/config/v1"
|
||||||
import { expect, test } from "bun:test"
|
import { expect, test } from "bun:test"
|
||||||
import { createComponent, createSignal } from "solid-js"
|
import { createSignal } from "solid-js"
|
||||||
import { RunFooterView } from "../src/mini/footer.view"
|
import { RunFooterView } from "../src/mini/footer.view"
|
||||||
import { RUN_THEME_FALLBACK } from "../src/mini/theme"
|
import { RUN_THEME_FALLBACK } from "../src/mini/theme"
|
||||||
import type { FooterState, FooterSubagentState, FooterView } from "../src/mini/types"
|
import type { FooterState, FooterSubagentState, FooterView } from "../src/mini/types"
|
||||||
@@ -42,52 +41,43 @@ test("down opens subagents from an empty prompt", async () => {
|
|||||||
{ keybinds: { editor_open: "none", session_queued_prompts: "none" } },
|
{ keybinds: { editor_open: "none", session_queued_prompts: "none" } },
|
||||||
{ terminalSuspend: true },
|
{ terminalSuspend: true },
|
||||||
)
|
)
|
||||||
let offKeymap: (() => void) | undefined
|
|
||||||
|
|
||||||
function Harness() {
|
function Harness() {
|
||||||
const renderer = useRenderer()
|
return (
|
||||||
const keymap = createDefaultOpenTuiKeymap(renderer)
|
<Keymap.Provider config={config}>
|
||||||
offKeymap = registerOpencodeKeymap(keymap, renderer, config)
|
<RunFooterView
|
||||||
|
directory="/tmp"
|
||||||
return createComponent(OpencodeKeymapProvider, {
|
findFiles={async () => []}
|
||||||
keymap,
|
agents={() => []}
|
||||||
get children() {
|
references={() => []}
|
||||||
return (
|
commands={() => []}
|
||||||
<RunFooterView
|
providers={() => undefined}
|
||||||
directory="/tmp"
|
currentModel={() => undefined}
|
||||||
findFiles={async () => []}
|
variants={() => []}
|
||||||
agents={() => []}
|
currentVariant={() => undefined}
|
||||||
references={() => []}
|
state={state}
|
||||||
commands={() => []}
|
view={view}
|
||||||
providers={() => undefined}
|
subagent={subagents}
|
||||||
currentModel={() => undefined}
|
theme={() => RUN_THEME_FALLBACK}
|
||||||
variants={() => []}
|
tuiConfig={config}
|
||||||
currentVariant={() => undefined}
|
agent="opencode"
|
||||||
state={state}
|
onSubmit={() => true}
|
||||||
view={view}
|
onPermissionReply={() => {}}
|
||||||
subagent={subagents}
|
onQuestionReply={() => {}}
|
||||||
theme={() => RUN_THEME_FALLBACK}
|
onQuestionReject={() => {}}
|
||||||
tuiConfig={config}
|
onCycle={() => {}}
|
||||||
agent="opencode"
|
onInterrupt={() => false}
|
||||||
onSubmit={() => true}
|
onEditorOpen={async () => undefined}
|
||||||
onPermissionReply={() => {}}
|
onInputClear={() => {}}
|
||||||
onQuestionReply={() => {}}
|
onExit={() => {}}
|
||||||
onQuestionReject={() => {}}
|
onModelSelect={() => {}}
|
||||||
onCycle={() => {}}
|
onVariantSelect={() => {}}
|
||||||
onInterrupt={() => false}
|
onRows={() => {}}
|
||||||
onEditorOpen={async () => undefined}
|
onLayout={() => {}}
|
||||||
onInputClear={() => {}}
|
onStatus={() => {}}
|
||||||
onExit={() => {}}
|
onQueuedRemove={async () => true}
|
||||||
onModelSelect={() => {}}
|
/>
|
||||||
onVariantSelect={() => {}}
|
</Keymap.Provider>
|
||||||
onRows={() => {}}
|
)
|
||||||
onLayout={() => {}}
|
|
||||||
onStatus={() => {}}
|
|
||||||
onQueuedRemove={async () => true}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const app = await testRender(() => <Harness />, { width: 100, height: 8, kittyKeyboard: true })
|
const app = await testRender(() => <Harness />, { width: 100, height: 8, kittyKeyboard: true })
|
||||||
@@ -100,7 +90,6 @@ test("down opens subagents from an empty prompt", async () => {
|
|||||||
} finally {
|
} finally {
|
||||||
app.renderer.currentFocusedRenderable?.blur()
|
app.renderer.currentFocusedRenderable?.blur()
|
||||||
app.renderer.currentFocusedEditor?.blur()
|
app.renderer.currentFocusedEditor?.blur()
|
||||||
offKeymap?.()
|
|
||||||
app.renderer.destroy()
|
app.renderer.destroy()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NodeFileSystem } from "@effect/platform-node"
|
import { NodeFileSystem } from "@effect/platform-node"
|
||||||
import { Service } from "@opencode-ai/client/effect/service"
|
import { Service, type Info } from "@opencode-ai/client/effect/service"
|
||||||
import { Database } from "@opencode-ai/core/database/database"
|
import { Database } from "@opencode-ai/core/database/database"
|
||||||
import { EventV2 } from "@opencode-ai/core/event"
|
import { EventV2 } from "@opencode-ai/core/event"
|
||||||
import { EventTable } from "@opencode-ai/core/event/sql"
|
import { EventTable } from "@opencode-ai/core/event/sql"
|
||||||
@@ -17,6 +17,13 @@ import os from "node:os"
|
|||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
import { ServiceConfig } from "../src/services/service-config"
|
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 () => {
|
test("local channel stores service config with the local service filename", async () => {
|
||||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-"))
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-"))
|
||||||
try {
|
try {
|
||||||
@@ -90,6 +97,80 @@ 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 () => {
|
test("concurrent service processes elect one server", async () => {
|
||||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-election-"))
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-election-"))
|
||||||
const database = path.join(root, "opencode.db")
|
const database = path.join(root, "opencode.db")
|
||||||
@@ -130,6 +211,9 @@ test("concurrent service processes elect one server", async () => {
|
|||||||
)
|
)
|
||||||
const command = [process.execPath, path.join(import.meta.dir, "../src/index.ts"), "serve", "--service"]
|
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 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" }))
|
const processes = Array.from({ length: 10 }, () => Bun.spawn(command, { env, stderr: "pipe", stdout: "ignore" }))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -137,11 +221,13 @@ test("concurrent service processes elect one server", async () => {
|
|||||||
const winner = processes.find((process) => process.pid === info.pid)
|
const winner = processes.find((process) => process.pid === info.pid)
|
||||||
const losers = processes.filter((process) => process.pid !== info.pid)
|
const losers = processes.filter((process) => process.pid !== info.pid)
|
||||||
const exited = await Promise.all(
|
const exited = await Promise.all(
|
||||||
losers.map((process) => Promise.race([process.exited.then(() => true), Bun.sleep(10_000).then(() => false)])),
|
losers.map((process) => Promise.race([process.exited.then(() => true), Bun.sleep(60_000).then(() => false)])),
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(exited).toEqual(losers.map(() => true))
|
expect(exited).toEqual(losers.map(() => true))
|
||||||
expect(winner?.exitCode).toBe(null)
|
expect(winner?.exitCode).toBe(null)
|
||||||
|
expect(new URL(info.url).port).toBe(String(port))
|
||||||
|
expect(await Bun.file(registration + ".lock").exists()).toBe(false)
|
||||||
expect(
|
expect(
|
||||||
await fetch(new URL("/api/health", info.url), {
|
await fetch(new URL("/api/health", info.url), {
|
||||||
headers: { authorization: "Basic " + btoa(`opencode:${info.password}`) },
|
headers: { authorization: "Basic " + btoa(`opencode:${info.password}`) },
|
||||||
@@ -151,20 +237,6 @@ test("concurrent service processes elect one server", async () => {
|
|||||||
version: info.version,
|
version: info.version,
|
||||||
pid: info.pid,
|
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" })
|
const contender = Bun.spawn(command, { env, stderr: "pipe", stdout: "ignore" })
|
||||||
try {
|
try {
|
||||||
const contenderExited = await Promise.race([
|
const contenderExited = await Promise.race([
|
||||||
@@ -191,9 +263,7 @@ test("concurrent service processes elect one server", async () => {
|
|||||||
),
|
),
|
||||||
).toEqual({ timeSuspended: null })
|
).toEqual({ timeSuspended: null })
|
||||||
expect(await waitForExecutionStart(database, sessionID)).toBe(1)
|
expect(await waitForExecutionStart(database, sessionID)).toBe(1)
|
||||||
await Effect.runPromise(
|
await Effect.runPromise(Service.stop({ file: registration }).pipe(Effect.provide(NodeFileSystem.layer)))
|
||||||
Service.stop({ file: registration }).pipe(Effect.provide(NodeFileSystem.layer)),
|
|
||||||
)
|
|
||||||
await winner?.exited
|
await winner?.exited
|
||||||
} finally {
|
} finally {
|
||||||
processes.forEach((process) => process.kill("SIGTERM"))
|
processes.forEach((process) => process.kill("SIGTERM"))
|
||||||
@@ -204,9 +274,88 @@ test("concurrent service processes elect one server", async () => {
|
|||||||
await fs.rm(root, { recursive: true, force: true })
|
await fs.rm(root, { recursive: true, force: true })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 60_000)
|
}, 120_000)
|
||||||
|
|
||||||
test("a failed service stays registered and owns the lock until stopped", async () => {
|
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 () => {
|
||||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-failed-"))
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-failed-"))
|
||||||
const database = path.join(root, "database")
|
const database = path.join(root, "database")
|
||||||
await fs.mkdir(database)
|
await fs.mkdir(database)
|
||||||
@@ -271,19 +420,92 @@ function waitForExecutionStart(file: string, sessionID: SessionV2.ID) {
|
|||||||
),
|
),
|
||||||
Effect.filterOrFail((rows) => rows.length > 0),
|
Effect.filterOrFail((rows) => rows.length > 0),
|
||||||
Effect.map((rows) => rows.length),
|
Effect.map((rows) => rows.length),
|
||||||
Effect.retry(Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(200)))),
|
Effect.retry(Schedule.max([Schedule.spaced("50 millis"), Schedule.recurs(200)])),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitForInfo(file: string) {
|
async function waitForInfo(file: string, accept: (info: Info) => boolean = () => true) {
|
||||||
for (let attempt = 0; attempt < 400; attempt++) {
|
for (let attempt = 0; attempt < 400; attempt++) {
|
||||||
const value = await Bun.file(file)
|
const value = await Bun.file(file)
|
||||||
.json()
|
.json()
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
if (value !== undefined) return Schema.decodeUnknownPromise(Service.Info)(value)
|
if (value !== undefined) {
|
||||||
|
const info = await Schema.decodeUnknownPromise(Service.Info)(value)
|
||||||
|
if (accept(info)) return info
|
||||||
|
}
|
||||||
await Bun.sleep(50)
|
await Bun.sleep(50)
|
||||||
}
|
}
|
||||||
throw new Error("Timed out waiting for service registration")
|
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,5 +6,6 @@
|
|||||||
"jsxImportSource": "@opentui/solid",
|
"jsxImportSource": "@opentui/solid",
|
||||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||||
"noUncheckedIndexedAccess": false
|
"noUncheckedIndexedAccess": false
|
||||||
}
|
},
|
||||||
|
"exclude": ["dist", "dist-node"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,220 @@
|
|||||||
|
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:*"
|
"@opencode-ai/protocol": "workspace:*"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"effect": "4.0.0-beta.83"
|
"effect": "4.0.0-beta.98"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"effect": {
|
"effect": {
|
||||||
|
|||||||
@@ -269,32 +269,40 @@ export type SessionInstructionsEntryRemoveOperation<E = never> = (
|
|||||||
input: Endpoint5_24Input,
|
input: Endpoint5_24Input,
|
||||||
) => Effect.Effect<Endpoint5_24Output, E>
|
) => Effect.Effect<Endpoint5_24Output, E>
|
||||||
|
|
||||||
type Endpoint5_25Request = Parameters<RawClient["server.session"]["session.log"]>[0]
|
type Endpoint5_25Request = Parameters<RawClient["server.session"]["session.generate"]>[0]
|
||||||
export type Endpoint5_25Input = {
|
export type Endpoint5_25Input = {
|
||||||
readonly sessionID: Endpoint5_25Request["params"]["sessionID"]
|
readonly sessionID: Endpoint5_25Request["params"]["sessionID"]
|
||||||
readonly after?: Endpoint5_25Request["query"]["after"]
|
readonly prompt: Endpoint5_25Request["payload"]["prompt"]
|
||||||
readonly follow?: Endpoint5_25Request["query"]["follow"]
|
|
||||||
}
|
}
|
||||||
export type Endpoint5_25Output = StreamValue<EffectValue<ReturnType<RawClient["server.session"]["session.log"]>>>
|
export type Endpoint5_25Output = EffectValue<ReturnType<RawClient["server.session"]["session.generate"]>>["data"]
|
||||||
export type SessionLogOperation<E = never> = (input: Endpoint5_25Input) => Stream.Stream<Endpoint5_25Output, E>
|
export type SessionGenerateOperation<E = never> = (input: Endpoint5_25Input) => Effect.Effect<Endpoint5_25Output, E>
|
||||||
|
|
||||||
type Endpoint5_26Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
type Endpoint5_26Request = Parameters<RawClient["server.session"]["session.log"]>[0]
|
||||||
export type Endpoint5_26Input = { readonly sessionID: Endpoint5_26Request["params"]["sessionID"] }
|
export type Endpoint5_26Input = {
|
||||||
export type Endpoint5_26Output = EffectValue<ReturnType<RawClient["server.session"]["session.interrupt"]>>
|
readonly sessionID: Endpoint5_26Request["params"]["sessionID"]
|
||||||
export type SessionInterruptOperation<E = never> = (input: Endpoint5_26Input) => Effect.Effect<Endpoint5_26Output, E>
|
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_27Request = Parameters<RawClient["server.session"]["session.background"]>[0]
|
type Endpoint5_27Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
||||||
export type Endpoint5_27Input = { readonly sessionID: Endpoint5_27Request["params"]["sessionID"] }
|
export type Endpoint5_27Input = { readonly sessionID: Endpoint5_27Request["params"]["sessionID"] }
|
||||||
export type Endpoint5_27Output = EffectValue<ReturnType<RawClient["server.session"]["session.background"]>>
|
export type Endpoint5_27Output = EffectValue<ReturnType<RawClient["server.session"]["session.interrupt"]>>
|
||||||
export type SessionBackgroundOperation<E = never> = (input: Endpoint5_27Input) => Effect.Effect<Endpoint5_27Output, E>
|
export type SessionInterruptOperation<E = never> = (input: Endpoint5_27Input) => Effect.Effect<Endpoint5_27Output, E>
|
||||||
|
|
||||||
type Endpoint5_28Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
type Endpoint5_28Request = Parameters<RawClient["server.session"]["session.background"]>[0]
|
||||||
export type Endpoint5_28Input = {
|
export type Endpoint5_28Input = { readonly sessionID: Endpoint5_28Request["params"]["sessionID"] }
|
||||||
readonly sessionID: Endpoint5_28Request["params"]["sessionID"]
|
export type Endpoint5_28Output = EffectValue<ReturnType<RawClient["server.session"]["session.background"]>>
|
||||||
readonly messageID: Endpoint5_28Request["params"]["messageID"]
|
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"]
|
||||||
}
|
}
|
||||||
export type Endpoint5_28Output = EffectValue<ReturnType<RawClient["server.session"]["session.message"]>>["data"]
|
export type Endpoint5_29Output = EffectValue<ReturnType<RawClient["server.session"]["session.message"]>>["data"]
|
||||||
export type SessionMessageOperation<E = never> = (input: Endpoint5_28Input) => Effect.Effect<Endpoint5_28Output, E>
|
export type SessionMessageOperation<E = never> = (input: Endpoint5_29Input) => Effect.Effect<Endpoint5_29Output, E>
|
||||||
|
|
||||||
export interface SessionApi<E = never> {
|
export interface SessionApi<E = never> {
|
||||||
readonly list: SessionListOperation<E>
|
readonly list: SessionListOperation<E>
|
||||||
@@ -328,6 +336,7 @@ export interface SessionApi<E = never> {
|
|||||||
readonly remove: SessionInstructionsEntryRemoveOperation<E>
|
readonly remove: SessionInstructionsEntryRemoveOperation<E>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
readonly generate: SessionGenerateOperation<E>
|
||||||
readonly log: SessionLogOperation<E>
|
readonly log: SessionLogOperation<E>
|
||||||
readonly interrupt: SessionInterruptOperation<E>
|
readonly interrupt: SessionInterruptOperation<E>
|
||||||
readonly background: SessionBackgroundOperation<E>
|
readonly background: SessionBackgroundOperation<E>
|
||||||
|
|||||||
@@ -361,13 +361,24 @@ const Endpoint5_24 = (raw: RawClient["server.session"]) => (input: Endpoint5_24I
|
|||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint5_25Request = Parameters<RawClient["server.session"]["session.log"]>[0]
|
type Endpoint5_25Request = Parameters<RawClient["server.session"]["session.generate"]>[0]
|
||||||
type Endpoint5_25Input = {
|
type Endpoint5_25Input = {
|
||||||
readonly sessionID: Endpoint5_25Request["params"]["sessionID"]
|
readonly sessionID: Endpoint5_25Request["params"]["sessionID"]
|
||||||
readonly after?: Endpoint5_25Request["query"]["after"]
|
readonly prompt: Endpoint5_25Request["payload"]["prompt"]
|
||||||
readonly follow?: Endpoint5_25Request["query"]["follow"]
|
|
||||||
}
|
}
|
||||||
const Endpoint5_25 = (raw: RawClient["server.session"]) => (input: Endpoint5_25Input) =>
|
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(
|
Stream.unwrap(
|
||||||
raw["session.log"]({
|
raw["session.log"]({
|
||||||
params: { sessionID: input["sessionID"] },
|
params: { sessionID: input["sessionID"] },
|
||||||
@@ -378,22 +389,22 @@ const Endpoint5_25 = (raw: RawClient["server.session"]) => (input: Endpoint5_25I
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
type Endpoint5_26Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
type Endpoint5_27Request = 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_27Request = Parameters<RawClient["server.session"]["session.background"]>[0]
|
|
||||||
type Endpoint5_27Input = { readonly sessionID: Endpoint5_27Request["params"]["sessionID"] }
|
type Endpoint5_27Input = { readonly sessionID: Endpoint5_27Request["params"]["sessionID"] }
|
||||||
const Endpoint5_27 = (raw: RawClient["server.session"]) => (input: Endpoint5_27Input) =>
|
const Endpoint5_27 = (raw: RawClient["server.session"]) => (input: Endpoint5_27Input) =>
|
||||||
|
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) =>
|
||||||
raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
type Endpoint5_28Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
type Endpoint5_29Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
||||||
type Endpoint5_28Input = {
|
type Endpoint5_29Input = {
|
||||||
readonly sessionID: Endpoint5_28Request["params"]["sessionID"]
|
readonly sessionID: Endpoint5_29Request["params"]["sessionID"]
|
||||||
readonly messageID: Endpoint5_28Request["params"]["messageID"]
|
readonly messageID: Endpoint5_29Request["params"]["messageID"]
|
||||||
}
|
}
|
||||||
const Endpoint5_28 = (raw: RawClient["server.session"]) => (input: Endpoint5_28Input) =>
|
const Endpoint5_29 = (raw: RawClient["server.session"]) => (input: Endpoint5_29Input) =>
|
||||||
raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe(
|
raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe(
|
||||||
Effect.mapError(mapClientError),
|
Effect.mapError(mapClientError),
|
||||||
Effect.map((value) => value.data),
|
Effect.map((value) => value.data),
|
||||||
@@ -421,10 +432,11 @@ const adaptGroup5 = (raw: RawClient["server.session"]) => ({
|
|||||||
context: Endpoint5_20(raw),
|
context: Endpoint5_20(raw),
|
||||||
pending: { list: Endpoint5_21(raw) },
|
pending: { list: Endpoint5_21(raw) },
|
||||||
instructions: { entry: { list: Endpoint5_22(raw), put: Endpoint5_23(raw), remove: Endpoint5_24(raw) } },
|
instructions: { entry: { list: Endpoint5_22(raw), put: Endpoint5_23(raw), remove: Endpoint5_24(raw) } },
|
||||||
log: Endpoint5_25(raw),
|
generate: Endpoint5_25(raw),
|
||||||
interrupt: Endpoint5_26(raw),
|
log: Endpoint5_26(raw),
|
||||||
background: Endpoint5_27(raw),
|
interrupt: Endpoint5_27(raw),
|
||||||
message: Endpoint5_28(raw),
|
background: Endpoint5_28(raw),
|
||||||
|
message: Endpoint5_29(raw),
|
||||||
})
|
})
|
||||||
|
|
||||||
type Endpoint6_0Request = Parameters<RawClient["server.message"]["session.messages"]>[0]
|
type Endpoint6_0Request = Parameters<RawClient["server.message"]["session.messages"]>[0]
|
||||||
|
|||||||
@@ -3,12 +3,7 @@ import { Effect, FileSystem, Option, Schedule, Schema } from "effect"
|
|||||||
import { spawn, type ChildProcess } from "node:child_process"
|
import { spawn, type ChildProcess } from "node:child_process"
|
||||||
import { homedir } from "node:os"
|
import { homedir } from "node:os"
|
||||||
import { join } from "node:path"
|
import { join } from "node:path"
|
||||||
import type {
|
import type { DiscoverOptions, Endpoint, EnsureOptions, StopOptions } from "../service.js"
|
||||||
DiscoverOptions,
|
|
||||||
Endpoint,
|
|
||||||
EnsureOptions,
|
|
||||||
StopOptions,
|
|
||||||
} from "../service.js"
|
|
||||||
|
|
||||||
export * from "../service.js"
|
export * from "../service.js"
|
||||||
/** Contents of the local service registration file. */
|
/** Contents of the local service registration file. */
|
||||||
@@ -34,6 +29,17 @@ export const discover = Effect.fn("service.discover")(function* (options: Discov
|
|||||||
return (yield* discoverLocal(options))?.endpoint
|
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 discoverLocal = Effect.fnUntraced(function* (options: DiscoverOptions) {
|
||||||
const found = (yield* registered(options.file)).service
|
const found = (yield* registered(options.file)).service
|
||||||
if (found?.state !== "ready") return undefined
|
if (found?.state !== "ready") return undefined
|
||||||
@@ -82,7 +88,8 @@ export const ensure = Effect.fn("service.ensure")(function* (options: EnsureOpti
|
|||||||
spawnDelay = 5_000
|
spawnDelay = 5_000
|
||||||
const compatible = !service.legacy && (options.version === undefined || service.version === options.version)
|
const compatible = !service.legacy && (options.version === undefined || service.version === options.version)
|
||||||
if (compatible && service.state === "ready") return Option.some(service)
|
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>()
|
if (compatible) return Option.none<LocalService>()
|
||||||
yield* announce("version-mismatch", service.version)
|
yield* announce("version-mismatch", service.version)
|
||||||
yield* kill(service, options).pipe(Effect.ignore)
|
yield* kill(service, options).pipe(Effect.ignore)
|
||||||
@@ -105,8 +112,15 @@ export const ensure = Effect.fn("service.ensure")(function* (options: EnsureOpti
|
|||||||
lastSpawn = Date.now()
|
lastSpawn = Date.now()
|
||||||
}
|
}
|
||||||
return Option.none<LocalService>()
|
return Option.none<LocalService>()
|
||||||
}).pipe(Effect.repeat({ until: Option.isSome, schedule: Schedule.spaced("1 second") }))
|
}).pipe(
|
||||||
return Option.getOrThrow(found).endpoint
|
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
|
||||||
})
|
})
|
||||||
|
|
||||||
function contenderFailure(contender: Contender) {
|
function contenderFailure(contender: Contender) {
|
||||||
@@ -221,7 +235,7 @@ const find = Effect.fnUntraced(function* (options: { readonly file?: string }) {
|
|||||||
|
|
||||||
// 50ms cadence bounded at ~5s, shared by stop escalation and each ensure
|
// 50ms cadence bounded at ~5s, shared by stop escalation and each ensure
|
||||||
// discovery window.
|
// discovery window.
|
||||||
const poll = Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(100)))
|
const poll = Schedule.max([Schedule.spaced("50 millis"), Schedule.recurs(100)])
|
||||||
|
|
||||||
const signal = (pid: number, name: NodeJS.Signals) =>
|
const signal = (pid: number, name: NodeJS.Signals) =>
|
||||||
Effect.try({ try: () => process.kill(pid, name), catch: (cause) => cause }).pipe(Effect.ignore)
|
Effect.try({ try: () => process.kill(pid, name), catch: (cause) => cause }).pipe(Effect.ignore)
|
||||||
@@ -238,10 +252,7 @@ function same(left: Info, right: Info) {
|
|||||||
return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid
|
return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid
|
||||||
}
|
}
|
||||||
|
|
||||||
const kill = Effect.fnUntraced(function* (
|
const kill = Effect.fnUntraced(function* (service: LocalService, options: { readonly file?: string }) {
|
||||||
service: LocalService,
|
|
||||||
options: { readonly file?: string },
|
|
||||||
) {
|
|
||||||
const requested = yield* requestStop(service)
|
const requested = yield* requestStop(service)
|
||||||
if (requested === "rejected") return
|
if (requested === "rejected") return
|
||||||
if (requested === "unsupported") {
|
if (requested === "unsupported") {
|
||||||
@@ -280,4 +291,4 @@ const requestStop = Effect.fnUntraced(function* (service: LocalService) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
/** Effect-based local service lifecycle operations. */
|
/** Effect-based local service lifecycle operations. */
|
||||||
export const Service = { discover, ensure, stop, headers, Info }
|
export const Service = { discover, incumbent, ensure, stop, headers, Info }
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ import type {
|
|||||||
SessionInstructionsEntryPutOutput,
|
SessionInstructionsEntryPutOutput,
|
||||||
SessionInstructionsEntryRemoveInput,
|
SessionInstructionsEntryRemoveInput,
|
||||||
SessionInstructionsEntryRemoveOutput,
|
SessionInstructionsEntryRemoveOutput,
|
||||||
|
SessionGenerateInput,
|
||||||
|
SessionGenerateOutput,
|
||||||
SessionLogInput,
|
SessionLogInput,
|
||||||
SessionLogOutput,
|
SessionLogOutput,
|
||||||
SessionInterruptInput,
|
SessionInterruptInput,
|
||||||
@@ -743,6 +745,18 @@ 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> =>
|
log: (input: SessionLogInput, requestOptions?: RequestOptions): AsyncIterable<SessionLogOutput> =>
|
||||||
sse<SessionLogOutput>(
|
sse<SessionLogOutput>(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -134,6 +134,8 @@ export type SessionMessageCompactionCompleted = {
|
|||||||
|
|
||||||
export type InstructionEntryKey = string
|
export type InstructionEntryKey = string
|
||||||
|
|
||||||
|
export type SessionGenerateResponse = { data: { text: string } }
|
||||||
|
|
||||||
export type SessionPendingSyntheticData1 = { text: string; description?: string; metadata?: { [x: string]: any } }
|
export type SessionPendingSyntheticData1 = { text: string; description?: string; metadata?: { [x: string]: any } }
|
||||||
|
|
||||||
export type ShellInfo = {
|
export type ShellInfo = {
|
||||||
@@ -803,6 +805,16 @@ export type SessionRevertCommitted = {
|
|||||||
data: { sessionID: string; to: string }
|
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 = {
|
export type ModelsDevRefreshed = {
|
||||||
id: string
|
id: string
|
||||||
created: number
|
created: number
|
||||||
@@ -2216,6 +2228,7 @@ export type SessionEventDurable =
|
|||||||
| SessionRevertStaged
|
| SessionRevertStaged
|
||||||
| SessionRevertCleared
|
| SessionRevertCleared
|
||||||
| SessionRevertCommitted
|
| SessionRevertCommitted
|
||||||
|
| SessionUsageRecorded
|
||||||
|
|
||||||
export type SessionMessagesResponse = {
|
export type SessionMessagesResponse = {
|
||||||
data: Array<SessionMessageInfo>
|
data: Array<SessionMessageInfo>
|
||||||
@@ -3176,6 +3189,13 @@ export type SessionInstructionsEntryRemoveInput = {
|
|||||||
|
|
||||||
export type SessionInstructionsEntryRemoveOutput = void
|
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 = {
|
export type SessionLogInput = {
|
||||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||||
readonly after?: { readonly after?: number | undefined; readonly follow?: boolean | undefined }["after"]
|
readonly after?: { readonly after?: number | undefined; readonly follow?: boolean | undefined }["after"]
|
||||||
|
|||||||
@@ -2,13 +2,7 @@ import { readFile } from "node:fs/promises"
|
|||||||
import { spawn, type ChildProcess } from "node:child_process"
|
import { spawn, type ChildProcess } from "node:child_process"
|
||||||
import { homedir } from "node:os"
|
import { homedir } from "node:os"
|
||||||
import { join } from "node:path"
|
import { join } from "node:path"
|
||||||
import type {
|
import type { DiscoverOptions, Endpoint, Info, EnsureOptions, StopOptions } from "../service.js"
|
||||||
DiscoverOptions,
|
|
||||||
Endpoint,
|
|
||||||
Info,
|
|
||||||
EnsureOptions,
|
|
||||||
StopOptions,
|
|
||||||
} from "../service.js"
|
|
||||||
import type { ServiceHealth, ServiceStopResponse } from "./generated/types.js"
|
import type { ServiceHealth, ServiceStopResponse } from "./generated/types.js"
|
||||||
|
|
||||||
export * from "../service.js"
|
export * from "../service.js"
|
||||||
@@ -38,6 +32,7 @@ async function discoverLocal(options: DiscoverOptions) {
|
|||||||
|
|
||||||
/** Ensure a healthy, compatible local service is running. */
|
/** Ensure a healthy, compatible local service is running. */
|
||||||
export async function ensure(options: EnsureOptions = {}): Promise<Endpoint> {
|
export async function ensure(options: EnsureOptions = {}): Promise<Endpoint> {
|
||||||
|
const deadline = Date.now() + 120_000
|
||||||
const contenders = new Set<Contender>()
|
const contenders = new Set<Contender>()
|
||||||
let announced = false
|
let announced = false
|
||||||
let lastSpawn = 0
|
let lastSpawn = 0
|
||||||
@@ -66,6 +61,7 @@ export async function ensure(options: EnsureOptions = {}): Promise<Endpoint> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
while (true) {
|
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)
|
const registration = await registered(options.file, true)
|
||||||
|
|
||||||
if (registration.service !== undefined) {
|
if (registration.service !== undefined) {
|
||||||
@@ -128,7 +124,9 @@ function fallback() {
|
|||||||
/** Create HTTP authentication headers for a service endpoint. */
|
/** Create HTTP authentication headers for a service endpoint. */
|
||||||
export function headers(endpoint: Endpoint) {
|
export function headers(endpoint: Endpoint) {
|
||||||
if (endpoint.auth === undefined) return undefined
|
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) {
|
async function read(file?: string) {
|
||||||
@@ -227,7 +225,8 @@ async function kill(service: LocalService, options: { readonly file?: string })
|
|||||||
const latest = await find(options)
|
const latest = await find(options)
|
||||||
if (latest === undefined || !same(latest.info, service.info)) return
|
if (latest === undefined || !same(latest.info, service.info)) return
|
||||||
signal(service.info.pid, "SIGKILL")
|
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) {
|
async function requestStop(service: LocalService) {
|
||||||
|
|||||||
@@ -379,6 +379,7 @@ test("session methods use the public HTTP contract", async () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (url.includes("/prompt")) return Response.json(admission)
|
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.includes("/synthetic")) return Response.json(syntheticAdmission)
|
||||||
if (url.endsWith("/compact")) return Response.json(compactionAdmission)
|
if (url.endsWith("/compact")) return Response.json(compactionAdmission)
|
||||||
if (url.includes("/context")) return Response.json({ data: [] })
|
if (url.includes("/context")) return Response.json({ data: [] })
|
||||||
@@ -403,6 +404,7 @@ test("session methods use the public HTTP contract", async () => {
|
|||||||
text: "Hello",
|
text: "Hello",
|
||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
|
const generated = await client.session.generate({ sessionID: "ses_test", prompt: "Summarize this session" })
|
||||||
const synthetic = await client.session.synthetic({
|
const synthetic = await client.session.synthetic({
|
||||||
sessionID: "ses_test",
|
sessionID: "ses_test",
|
||||||
text: "Completed",
|
text: "Completed",
|
||||||
@@ -421,6 +423,7 @@ test("session methods use the public HTTP contract", async () => {
|
|||||||
expect(active).toEqual({ ses_test: { type: "running" } })
|
expect(active).toEqual({ ses_test: { type: "running" } })
|
||||||
expect(created.id).toBe("ses_test")
|
expect(created.id).toBe("ses_test")
|
||||||
expect(admitted.id).toBe("msg_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(synthetic).toMatchObject({ type: "synthetic", data: { text: "Completed" }, delivery: "queue" })
|
||||||
expect(context).toEqual([])
|
expect(context).toEqual([])
|
||||||
expect(log).toEqual([modelSwitchedEvent, synced])
|
expect(log).toEqual([modelSwitchedEvent, synced])
|
||||||
@@ -432,6 +435,7 @@ 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/agent"],
|
||||||
["POST", "http://localhost:3000/api/session/ses_test/model"],
|
["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/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/synthetic"],
|
||||||
["POST", "http://localhost:3000/api/session/ses_test/compact"],
|
["POST", "http://localhost:3000/api/session/ses_test/compact"],
|
||||||
["POST", "http://localhost:3000/api/session/ses_test/wait"],
|
["POST", "http://localhost:3000/api/session/ses_test/wait"],
|
||||||
|
|||||||
@@ -108,7 +108,9 @@ const runtime = CodeMode.make({ tools: { opencode: api.tools } })
|
|||||||
|
|
||||||
It is synchronous and returns `{ tools, skipped }`: operations with unsupported encodings, non-JSON bodies, binary
|
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
|
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. See the option docstrings in
|
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
|
||||||
`src/openapi/types.ts` for full semantics.
|
`src/openapi/types.ts` for full semantics.
|
||||||
|
|
||||||
## Outputs
|
## Outputs
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ ultimate source of truth.
|
|||||||
- [x] Direct function declarations are hoisted in program and block statement lists.
|
- [x] Direct function declarations are hoisted in program and block statement lists.
|
||||||
- [x] Parameter defaults observe a temporal dead zone for later parameters.
|
- [x] Parameter defaults observe a temporal dead zone for later parameters.
|
||||||
- [ ] JavaScript-correct function scoping, hoisting, and redeclaration for accepted `var` declarations.
|
- [ ] JavaScript-correct function scoping, hoisting, and redeclaration for accepted `var` declarations.
|
||||||
- [ ] Predeclare `let` and `const` bindings in every lexical scope, including program/block bodies, switch bodies, and
|
- [x] 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
|
loop headers, so reads before initialization and self- or cross-referential initializers observe the JavaScript
|
||||||
temporal dead zone.
|
temporal dead zone.
|
||||||
- [ ] Hoist function declarations accepted directly in switch cases.
|
- [ ] Hoist function declarations accepted directly in switch cases.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/codemode",
|
"name": "@opencode-ai/codemode",
|
||||||
"version": "1.18.2",
|
"version": "1.18.3",
|
||||||
"description": "Effect-native confined code execution over schema-described tools",
|
"description": "Effect-native confined code execution over schema-described tools",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -144,6 +144,20 @@ const collectPatternNames = (pattern: AstNode, out: Array<string> = []): Array<s
|
|||||||
return out
|
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> {
|
export class Interpreter<R> {
|
||||||
private scopes: ScopeStack
|
private scopes: ScopeStack
|
||||||
private readonly invokeTool: (path: ReadonlyArray<string>, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>
|
private readonly invokeTool: (path: ReadonlyArray<string>, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>
|
||||||
@@ -207,6 +221,7 @@ export class Interpreter<R> {
|
|||||||
// Keep top-level declarations separate so they can shadow builtins.
|
// Keep top-level declarations separate so they can shadow builtins.
|
||||||
this.scopes.push()
|
this.scopes.push()
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
|
self.predeclareLexical(program.body)
|
||||||
self.hoistFunctions(program.body)
|
self.hoistFunctions(program.body)
|
||||||
let value: unknown = undefined
|
let value: unknown = undefined
|
||||||
for (const [index, statement] of program.body.entries()) {
|
for (const [index, statement] of program.body.entries()) {
|
||||||
@@ -303,6 +318,7 @@ export class Interpreter<R> {
|
|||||||
const self = this
|
const self = this
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const body = getArray(node, "body")
|
const body = getArray(node, "body")
|
||||||
|
self.predeclareLexical(body)
|
||||||
self.hoistFunctions(body)
|
self.hoistFunctions(body)
|
||||||
|
|
||||||
for (const statementValue of body) {
|
for (const statementValue of body) {
|
||||||
@@ -343,6 +359,25 @@ 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> {
|
private evaluateIfStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
|
||||||
const testNode = getNode(node, "test")
|
const testNode = getNode(node, "test")
|
||||||
const consequentNode = getNode(node, "consequent")
|
const consequentNode = getNode(node, "consequent")
|
||||||
@@ -359,7 +394,6 @@ export class Interpreter<R> {
|
|||||||
|
|
||||||
private evaluateSwitchStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
|
private evaluateSwitchStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
|
||||||
const self = this
|
const self = this
|
||||||
this.scopes.push()
|
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const discriminant = yield* self.evaluateExpression(getNode(node, "discriminant"))
|
const discriminant = yield* self.evaluateExpression(getNode(node, "discriminant"))
|
||||||
if (containsOpaqueReference(discriminant)) {
|
if (containsOpaqueReference(discriminant)) {
|
||||||
@@ -369,39 +403,43 @@ export class Interpreter<R> {
|
|||||||
"InvalidDataValue",
|
"InvalidDataValue",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const cases = getArray(node, "cases").map((value, index) => asNode(value, `cases[${index}]`))
|
self.scopes.push()
|
||||||
let defaultIndex: number | undefined
|
return yield* Effect.gen(function* () {
|
||||||
let selected: number | undefined
|
const cases = getArray(node, "cases").map((value, index) => asNode(value, `cases[${index}]`))
|
||||||
for (const [index, branch] of cases.entries()) {
|
self.predeclareLexical(cases.flatMap((branch) => getArray(branch, "consequent")))
|
||||||
const test = getOptionalNode(branch, "test")
|
let defaultIndex: number | undefined
|
||||||
if (!test) {
|
let selected: number | undefined
|
||||||
defaultIndex = index
|
for (const [index, branch] of cases.entries()) {
|
||||||
continue
|
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 candidate = yield* self.evaluateExpression(test)
|
const start = selected ?? defaultIndex
|
||||||
if (containsOpaqueReference(candidate)) {
|
if (start === undefined) return { kind: "none" } satisfies StatementResult
|
||||||
throw new InterpreterRuntimeError(
|
for (let index = start; index < cases.length; index += 1) {
|
||||||
"Switch case values must be data values in CodeMode.",
|
for (const statementValue of getArray(cases[index]!, "consequent")) {
|
||||||
test,
|
const result = yield* self.evaluateStatement(asNode(statementValue, "consequent"))
|
||||||
"InvalidDataValue",
|
if (result.kind === "break") return { kind: "none" } satisfies StatementResult
|
||||||
)
|
if (result.kind === "return" || result.kind === "continue") return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (candidate === discriminant) {
|
return { kind: "none" } satisfies StatementResult
|
||||||
selected = index
|
}).pipe(Effect.ensuring(Effect.sync(() => self.scopes.pop())))
|
||||||
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> {
|
private evaluateWhileStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
|
||||||
@@ -465,6 +503,10 @@ export class Interpreter<R> {
|
|||||||
const updateNode = getOptionalNode(node, "update")
|
const updateNode = getOptionalNode(node, "update")
|
||||||
const bodyNode = getNode(node, "body")
|
const bodyNode = getNode(node, "body")
|
||||||
|
|
||||||
|
if (initNode?.type === "VariableDeclaration" && getString(initNode, "kind") !== "var") {
|
||||||
|
self.predeclareLexical([initNode])
|
||||||
|
}
|
||||||
|
|
||||||
if (initNode) {
|
if (initNode) {
|
||||||
if (initNode.type === "VariableDeclaration") {
|
if (initNode.type === "VariableDeclaration") {
|
||||||
yield* self.evaluateVariableDeclaration(initNode)
|
yield* self.evaluateVariableDeclaration(initNode)
|
||||||
@@ -478,21 +520,18 @@ export class Interpreter<R> {
|
|||||||
? Array.from(self.scopes.current().keys())
|
? Array.from(self.scopes.current().keys())
|
||||||
: []
|
: []
|
||||||
|
|
||||||
while (testNode ? yield* self.evaluateExpression(testNode) : true) {
|
const nextIteration = () => {
|
||||||
const iterationScope =
|
if (perIterationBindings.length === 0) return
|
||||||
perIterationBindings.length > 0
|
const current = self.scopes.current()
|
||||||
? new Map(
|
self.scopes.pop()
|
||||||
perIterationBindings.map((name): [string, Binding] => [name, { ...self.scopes.current().get(name)! }]),
|
self.scopes.push(
|
||||||
)
|
new Map(perIterationBindings.map((name): [string, Binding] => [name, { ...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()
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
nextIteration()
|
||||||
|
|
||||||
|
while (testNode ? yield* self.evaluateExpression(testNode) : true) {
|
||||||
|
const result = yield* self.evaluateStatement(bodyNode)
|
||||||
|
|
||||||
if (result.kind === "return") {
|
if (result.kind === "return") {
|
||||||
return result
|
return result
|
||||||
@@ -502,13 +541,7 @@ export class Interpreter<R> {
|
|||||||
return { kind: "none" } satisfies StatementResult
|
return { kind: "none" } satisfies StatementResult
|
||||||
}
|
}
|
||||||
|
|
||||||
if (iterationScope) {
|
nextIteration()
|
||||||
const loopScope = self.scopes.current()
|
|
||||||
for (const name of perIterationBindings) {
|
|
||||||
loopScope.set(name, { ...iterationScope.get(name)! })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updateNode) {
|
if (updateNode) {
|
||||||
yield* self.evaluateExpression(updateNode)
|
yield* self.evaluateExpression(updateNode)
|
||||||
}
|
}
|
||||||
@@ -527,9 +560,13 @@ export class Interpreter<R> {
|
|||||||
throw new InterpreterRuntimeError("for await...of is not supported.", node)
|
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
|
const self = this
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const left = getNode(node, "left")
|
if (declared?.lexical) self.predeclarePattern(declared.pattern, declared.mutable, left)
|
||||||
const right = yield* self.evaluateExpression(getNode(node, "right"))
|
const right = yield* self.evaluateExpression(getNode(node, "right"))
|
||||||
const body = getNode(node, "body")
|
const body = getNode(node, "body")
|
||||||
|
|
||||||
@@ -538,40 +575,34 @@ export class Interpreter<R> {
|
|||||||
throw new InterpreterRuntimeError("for...of requires an array, string, Map, or Set value in CodeMode.", node)
|
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
|
let assignment: AstNode | undefined
|
||||||
|
|
||||||
if (left.type === "VariableDeclaration") {
|
if (
|
||||||
const declarations = getArray(left, "declarations")
|
left.type !== "VariableDeclaration" &&
|
||||||
if (declarations.length !== 1) {
|
(left.type === "Identifier" ||
|
||||||
throw new InterpreterRuntimeError("for...of supports one declared binding.", left)
|
left.type === "MemberExpression" ||
|
||||||
}
|
left.type === "ArrayPattern" ||
|
||||||
|
left.type === "ObjectPattern")
|
||||||
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
|
assignment = left
|
||||||
} else {
|
} else if (left.type !== "VariableDeclaration") {
|
||||||
throw new InterpreterRuntimeError("Unsupported for...of binding.", left)
|
throw new InterpreterRuntimeError("Unsupported for...of binding.", left)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const value of iterable) {
|
for (const value of iterable) {
|
||||||
if (declaration) {
|
const result = yield* Effect.gen(function* () {
|
||||||
self.scopes.push()
|
if (declared) {
|
||||||
yield* self.declarePattern(declaration.pattern, value, declaration.mutable, left)
|
self.scopes.push()
|
||||||
} else if (assignment) {
|
if (declared.lexical) self.predeclarePattern(declared.pattern, declared.mutable, left)
|
||||||
yield* self.assignPattern(assignment, value, left)
|
yield* self.declarePattern(declared.pattern, value, declared.mutable, left, declared.lexical)
|
||||||
}
|
} else if (assignment) {
|
||||||
|
yield* self.assignPattern(assignment, value, left)
|
||||||
const result = yield* self.evaluateStatement(body).pipe(
|
}
|
||||||
|
return yield* self.evaluateStatement(body)
|
||||||
|
}).pipe(
|
||||||
Effect.ensuring(
|
Effect.ensuring(
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
if (declaration) self.scopes.pop()
|
if (declared) self.scopes.pop()
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -581,7 +612,7 @@ export class Interpreter<R> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (result.kind === "break") {
|
if (result.kind === "break") {
|
||||||
return { kind: "none" }
|
return { kind: "none" } satisfies StatementResult
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.kind === "continue") {
|
if (result.kind === "continue") {
|
||||||
@@ -589,8 +620,14 @@ export class Interpreter<R> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { kind: "none" }
|
return { kind: "none" } satisfies StatementResult
|
||||||
})
|
}).pipe(
|
||||||
|
Effect.ensuring(
|
||||||
|
Effect.sync(() => {
|
||||||
|
if (declared?.lexical) self.scopes.pop()
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private enumerableKeys(value: unknown): Array<string> | undefined {
|
private enumerableKeys(value: unknown): Array<string> | undefined {
|
||||||
@@ -607,9 +644,13 @@ export class Interpreter<R> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private evaluateForInStatement(node: AstNode): Effect.Effect<StatementResult, unknown, 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
|
const self = this
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const left = getNode(node, "left")
|
if (declared?.lexical) self.predeclarePattern(declared.pattern, declared.mutable, left)
|
||||||
const right = yield* self.evaluateExpression(getNode(node, "right"))
|
const right = yield* self.evaluateExpression(getNode(node, "right"))
|
||||||
const body = getNode(node, "body")
|
const body = getNode(node, "body")
|
||||||
|
|
||||||
@@ -621,35 +662,28 @@ export class Interpreter<R> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let declaration: { readonly pattern: AstNode; readonly mutable: boolean } | undefined
|
|
||||||
let assignmentName: string | undefined
|
let assignmentName: string | undefined
|
||||||
|
|
||||||
if (left.type === "VariableDeclaration") {
|
if (left.type === "Identifier") {
|
||||||
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")
|
assignmentName = getString(left, "name")
|
||||||
} else {
|
} else if (left.type !== "VariableDeclaration") {
|
||||||
throw new InterpreterRuntimeError("Unsupported for...in binding.", left)
|
throw new InterpreterRuntimeError("Unsupported for...in binding.", left)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
if (declaration) {
|
const result = yield* Effect.gen(function* () {
|
||||||
self.scopes.push()
|
if (declared) {
|
||||||
yield* self.declarePattern(declaration.pattern, key, declaration.mutable, left)
|
self.scopes.push()
|
||||||
} else if (assignmentName) {
|
if (declared.lexical) self.predeclarePattern(declared.pattern, declared.mutable, left)
|
||||||
self.scopes.set(assignmentName, key, left)
|
yield* self.declarePattern(declared.pattern, key, declared.mutable, left, declared.lexical)
|
||||||
}
|
} else if (assignmentName) {
|
||||||
|
self.scopes.set(assignmentName, key, left)
|
||||||
const result = yield* self.evaluateStatement(body).pipe(
|
}
|
||||||
|
return yield* self.evaluateStatement(body)
|
||||||
|
}).pipe(
|
||||||
Effect.ensuring(
|
Effect.ensuring(
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
if (declaration) self.scopes.pop()
|
if (declared) self.scopes.pop()
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -659,7 +693,7 @@ export class Interpreter<R> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (result.kind === "break") {
|
if (result.kind === "break") {
|
||||||
return { kind: "none" }
|
return { kind: "none" } satisfies StatementResult
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.kind === "continue") {
|
if (result.kind === "continue") {
|
||||||
@@ -667,8 +701,14 @@ export class Interpreter<R> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { kind: "none" }
|
return { kind: "none" } satisfies StatementResult
|
||||||
})
|
}).pipe(
|
||||||
|
Effect.ensuring(
|
||||||
|
Effect.sync(() => {
|
||||||
|
if (declared?.lexical) self.scopes.pop()
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private evaluateBreakStatement(node: AstNode): StatementResult {
|
private evaluateBreakStatement(node: AstNode): StatementResult {
|
||||||
@@ -752,7 +792,7 @@ export class Interpreter<R> {
|
|||||||
|
|
||||||
const init = getOptionalNode(declaration, "init")
|
const init = getOptionalNode(declaration, "init")
|
||||||
const value = init ? yield* self.evaluateExpression(init) : undefined
|
const value = init ? yield* self.evaluateExpression(init) : undefined
|
||||||
yield* self.declarePattern(getNode(declaration, "id"), value, kind !== "const", declaration)
|
yield* self.declarePattern(getNode(declaration, "id"), value, kind !== "const", declaration, kind !== "var")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -762,17 +802,20 @@ export class Interpreter<R> {
|
|||||||
value: unknown,
|
value: unknown,
|
||||||
mutable: boolean,
|
mutable: boolean,
|
||||||
node: AstNode,
|
node: AstNode,
|
||||||
|
initialize = false,
|
||||||
): Effect.Effect<void, unknown, R> {
|
): Effect.Effect<void, unknown, R> {
|
||||||
const self = this
|
const self = this
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
if (pattern.type === "Identifier") {
|
if (pattern.type === "Identifier") {
|
||||||
self.scopes.declare(getString(pattern, "name"), value, mutable, node)
|
const name = getString(pattern, "name")
|
||||||
|
if (initialize) self.scopes.initialize(name, value, node)
|
||||||
|
else self.scopes.declare(name, value, mutable, node)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pattern.type === "AssignmentPattern") {
|
if (pattern.type === "AssignmentPattern") {
|
||||||
const resolved = value === undefined ? yield* self.evaluateExpression(getNode(pattern, "right")) : value
|
const resolved = value === undefined ? yield* self.evaluateExpression(getNode(pattern, "right")) : value
|
||||||
yield* self.declarePattern(getNode(pattern, "left"), resolved, mutable, node)
|
yield* self.declarePattern(getNode(pattern, "left"), resolved, mutable, node, initialize)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -794,7 +837,7 @@ export class Interpreter<R> {
|
|||||||
for (const [key, item] of Object.entries(value as SafeObject)) {
|
for (const [key, item] of Object.entries(value as SafeObject)) {
|
||||||
if (!consumed.has(key) && !isBlockedMember(key)) rest[key] = item
|
if (!consumed.has(key) && !isBlockedMember(key)) rest[key] = item
|
||||||
}
|
}
|
||||||
yield* self.declarePattern(getNode(property, "argument"), rest, mutable, property)
|
yield* self.declarePattern(getNode(property, "argument"), rest, mutable, property, initialize)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -808,6 +851,7 @@ export class Interpreter<R> {
|
|||||||
self.destructuringPropertyValue(value as SafeObject | Array<unknown>, key),
|
self.destructuringPropertyValue(value as SafeObject | Array<unknown>, key),
|
||||||
mutable,
|
mutable,
|
||||||
property,
|
property,
|
||||||
|
initialize,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -823,10 +867,10 @@ export class Interpreter<R> {
|
|||||||
if (item === null) continue
|
if (item === null) continue
|
||||||
const element = asNode(item, `elements[${index}]`)
|
const element = asNode(item, `elements[${index}]`)
|
||||||
if (element.type === "RestElement") {
|
if (element.type === "RestElement") {
|
||||||
yield* self.declarePattern(getNode(element, "argument"), items.slice(index), mutable, element)
|
yield* self.declarePattern(getNode(element, "argument"), items.slice(index), mutable, element, initialize)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
yield* self.declarePattern(element, items[index], mutable, pattern)
|
yield* self.declarePattern(element, items[index], mutable, pattern, initialize)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1577,10 +1621,10 @@ export class Interpreter<R> {
|
|||||||
}
|
}
|
||||||
for (const [index, parameter] of fn.parameters.entries()) {
|
for (const [index, parameter] of fn.parameters.entries()) {
|
||||||
if (parameter.type === "RestElement") {
|
if (parameter.type === "RestElement") {
|
||||||
yield* invocation.declarePattern(getNode(parameter, "argument"), args.slice(index), true, parameter)
|
yield* invocation.declarePattern(getNode(parameter, "argument"), args.slice(index), true, parameter, true)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
yield* invocation.declarePattern(parameter, args[index], true, parameter)
|
yield* invocation.declarePattern(parameter, args[index], true, parameter, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fn.body.type === "BlockStatement") {
|
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