mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-15 17:08:21 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 73a4f5a654 |
@@ -25,7 +25,6 @@ kommander
|
|||||||
-opencodeengineer bot that spams issues
|
-opencodeengineer bot that spams issues
|
||||||
r44vc0rp
|
r44vc0rp
|
||||||
rekram1-node
|
rekram1-node
|
||||||
-ricardo-m-l
|
|
||||||
-robinmordasiewicz
|
-robinmordasiewicz
|
||||||
simonklee
|
simonklee
|
||||||
-spider-yamet clawdbot/llm psychosis, spam pinging the team
|
-spider-yamet clawdbot/llm psychosis, spam pinging the team
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ jobs:
|
|||||||
- build-cli
|
- build-cli
|
||||||
- version
|
- version
|
||||||
runs-on: blacksmith-4vcpu-windows-2025
|
runs-on: blacksmith-4vcpu-windows-2025
|
||||||
if: github.repository == 'anomalyco/opencode'
|
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'beta'
|
||||||
env:
|
env:
|
||||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
@@ -213,6 +213,7 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- build-cli
|
- build-cli
|
||||||
- version
|
- version
|
||||||
|
if: github.ref_name != 'beta'
|
||||||
continue-on-error: false
|
continue-on-error: false
|
||||||
env:
|
env:
|
||||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
@@ -389,7 +390,7 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- build-cli
|
- build-cli
|
||||||
- version
|
- version
|
||||||
if: github.repository == 'anomalyco/opencode'
|
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'beta'
|
||||||
continue-on-error: false
|
continue-on-error: false
|
||||||
env:
|
env:
|
||||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
@@ -590,12 +591,13 @@ jobs:
|
|||||||
path: packages/opencode/dist
|
path: packages/opencode/dist
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
|
if: github.ref_name != 'beta'
|
||||||
with:
|
with:
|
||||||
name: opencode-cli-signed-windows
|
name: opencode-cli-signed-windows
|
||||||
path: packages/opencode/dist
|
path: packages/opencode/dist
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
if: needs.version.outputs.release
|
if: needs.version.outputs.release && github.ref_name != 'beta'
|
||||||
with:
|
with:
|
||||||
pattern: latest-yml-*
|
pattern: latest-yml-*
|
||||||
path: /tmp/latest-yml
|
path: /tmp/latest-yml
|
||||||
|
|||||||
@@ -3,5 +3,4 @@ plans
|
|||||||
package.json
|
package.json
|
||||||
bun.lock
|
bun.lock
|
||||||
.gitignore
|
.gitignore
|
||||||
package-lock.json
|
package-lock.json
|
||||||
references/
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
---
|
|
||||||
name: effect
|
|
||||||
description: Answer questions about the Effect framework
|
|
||||||
---
|
|
||||||
|
|
||||||
# Effect
|
|
||||||
|
|
||||||
This codebase uses Effect, a framework for writing typescript.
|
|
||||||
|
|
||||||
## How to Answer Effect Questions
|
|
||||||
|
|
||||||
1. Clone the Effect repository: `https://github.com/Effect-TS/effect-smol` to
|
|
||||||
`.opencode/references/effect-smol` in this project NOT the skill folder.
|
|
||||||
2. Use the explore agent to search the codebase for answers about Effect patterns, APIs, and concepts
|
|
||||||
3. Provide responses based on the actual Effect source code and documentation
|
|
||||||
|
|
||||||
## Guidelines
|
|
||||||
|
|
||||||
- Always use the explore agent with the cloned repository when answering Effect-related questions
|
|
||||||
- Reference specific files and patterns found in the Effect codebase
|
|
||||||
- Do not answer from memory - always verify against the source
|
|
||||||
@@ -116,8 +116,8 @@
|
|||||||
"light": "nord5"
|
"light": "nord5"
|
||||||
},
|
},
|
||||||
"diffLineNumber": {
|
"diffLineNumber": {
|
||||||
"dark": "#abafb7",
|
"dark": "nord2",
|
||||||
"light": "textMuted"
|
"light": "nord4"
|
||||||
},
|
},
|
||||||
"diffAddedLineNumberBg": {
|
"diffAddedLineNumberBg": {
|
||||||
"dark": "#3B4252",
|
"dark": "#3B4252",
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.4.4",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.4.4",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.4.4",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-sts": "3.782.0",
|
"@aws-sdk/client-sts": "3.782.0",
|
||||||
"@jsx-email/render": "1.1.1",
|
"@jsx-email/render": "1.1.1",
|
||||||
@@ -142,7 +142,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.4.4",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "3.0.64",
|
"@ai-sdk/anthropic": "3.0.64",
|
||||||
"@ai-sdk/openai": "3.0.48",
|
"@ai-sdk/openai": "3.0.48",
|
||||||
@@ -166,7 +166,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.4.4",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
@@ -190,7 +190,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.4.4",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/app": "workspace:*",
|
"@opencode-ai/app": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
@@ -223,7 +223,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop-electron": {
|
"packages/desktop-electron": {
|
||||||
"name": "@opencode-ai/desktop-electron",
|
"name": "@opencode-ai/desktop-electron",
|
||||||
"version": "1.4.4",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"electron-context-menu": "4.1.2",
|
"electron-context-menu": "4.1.2",
|
||||||
@@ -266,7 +266,7 @@
|
|||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.4.4",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
@@ -295,7 +295,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.4.4",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
@@ -311,7 +311,7 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.4.4",
|
"version": "1.4.3",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -319,7 +319,6 @@
|
|||||||
"@actions/core": "1.11.1",
|
"@actions/core": "1.11.1",
|
||||||
"@actions/github": "6.0.1",
|
"@actions/github": "6.0.1",
|
||||||
"@agentclientprotocol/sdk": "0.16.1",
|
"@agentclientprotocol/sdk": "0.16.1",
|
||||||
"@ai-sdk/alibaba": "1.0.17",
|
|
||||||
"@ai-sdk/amazon-bedrock": "4.0.93",
|
"@ai-sdk/amazon-bedrock": "4.0.93",
|
||||||
"@ai-sdk/anthropic": "3.0.67",
|
"@ai-sdk/anthropic": "3.0.67",
|
||||||
"@ai-sdk/azure": "3.0.49",
|
"@ai-sdk/azure": "3.0.49",
|
||||||
@@ -358,8 +357,8 @@
|
|||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
"@openrouter/ai-sdk-provider": "2.5.1",
|
"@openrouter/ai-sdk-provider": "2.5.1",
|
||||||
"@opentui/core": "0.1.99",
|
"@opentui/core": "0.1.97",
|
||||||
"@opentui/solid": "0.1.99",
|
"@opentui/solid": "0.1.97",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
@@ -371,7 +370,6 @@
|
|||||||
"bonjour-service": "1.3.0",
|
"bonjour-service": "1.3.0",
|
||||||
"bun-pty": "0.4.8",
|
"bun-pty": "0.4.8",
|
||||||
"chokidar": "4.0.3",
|
"chokidar": "4.0.3",
|
||||||
"cli-sound": "1.1.3",
|
|
||||||
"clipboardy": "4.0.0",
|
"clipboardy": "4.0.0",
|
||||||
"cross-spawn": "catalog:",
|
"cross-spawn": "catalog:",
|
||||||
"decimal.js": "10.5.0",
|
"decimal.js": "10.5.0",
|
||||||
@@ -386,7 +384,6 @@
|
|||||||
"hono": "catalog:",
|
"hono": "catalog:",
|
||||||
"hono-openapi": "catalog:",
|
"hono-openapi": "catalog:",
|
||||||
"ignore": "7.0.5",
|
"ignore": "7.0.5",
|
||||||
"immer": "11.1.4",
|
|
||||||
"jsonc-parser": "3.3.1",
|
"jsonc-parser": "3.3.1",
|
||||||
"mime-types": "3.0.2",
|
"mime-types": "3.0.2",
|
||||||
"minimatch": "10.0.3",
|
"minimatch": "10.0.3",
|
||||||
@@ -397,7 +394,6 @@
|
|||||||
"opentui-spinner": "0.0.6",
|
"opentui-spinner": "0.0.6",
|
||||||
"partial-json": "0.1.7",
|
"partial-json": "0.1.7",
|
||||||
"remeda": "catalog:",
|
"remeda": "catalog:",
|
||||||
"ripgrep": "0.3.1",
|
|
||||||
"semver": "^7.6.3",
|
"semver": "^7.6.3",
|
||||||
"solid-js": "catalog:",
|
"solid-js": "catalog:",
|
||||||
"strip-ansi": "7.1.2",
|
"strip-ansi": "7.1.2",
|
||||||
@@ -450,23 +446,23 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.4.4",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@opentui/core": "0.1.99",
|
"@opentui/core": "0.1.97",
|
||||||
"@opentui/solid": "0.1.99",
|
"@opentui/solid": "0.1.97",
|
||||||
"@tsconfig/node22": "catalog:",
|
"@tsconfig/node22": "catalog:",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@opentui/core": ">=0.1.99",
|
"@opentui/core": ">=0.1.97",
|
||||||
"@opentui/solid": ">=0.1.99",
|
"@opentui/solid": ">=0.1.97",
|
||||||
},
|
},
|
||||||
"optionalPeers": [
|
"optionalPeers": [
|
||||||
"@opentui/core",
|
"@opentui/core",
|
||||||
@@ -485,7 +481,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.4.4",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-spawn": "catalog:",
|
"cross-spawn": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -498,16 +494,9 @@
|
|||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/server": {
|
|
||||||
"name": "@opencode-ai/server",
|
|
||||||
"version": "1.4.4",
|
|
||||||
"devDependencies": {
|
|
||||||
"typescript": "catalog:",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.4.4",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -542,7 +531,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.4.4",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -591,7 +580,7 @@
|
|||||||
},
|
},
|
||||||
"packages/util": {
|
"packages/util": {
|
||||||
"name": "@opencode-ai/util",
|
"name": "@opencode-ai/util",
|
||||||
"version": "1.4.4",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -602,7 +591,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.4.4",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
@@ -718,8 +707,6 @@
|
|||||||
|
|
||||||
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.16.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-1ad+Sc/0sCtZGHthxxvgEUo5Wsbw16I+aF+YwdiLnPwkZG8KAGUEAPK6LM6Pf69lCyJPt1Aomk1d+8oE3C4ZEw=="],
|
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.16.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-1ad+Sc/0sCtZGHthxxvgEUo5Wsbw16I+aF+YwdiLnPwkZG8KAGUEAPK6LM6Pf69lCyJPt1Aomk1d+8oE3C4ZEw=="],
|
||||||
|
|
||||||
"@ai-sdk/alibaba": ["@ai-sdk/alibaba@1.0.17", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZbE+U5bWz2JBc5DERLowx5+TKbjGBE93LqKZAWvuEn7HOSQMraxFMZuc0ST335QZJAyfBOzh7m1mPQ+y7EaaoA=="],
|
|
||||||
|
|
||||||
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.93", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.69", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hcXDU8QDwpAzLVTuY932TQVlIij9+iaVTxc5mPGY6yb//JMAAC5hMVhg93IrxlrxWLvMgjezNgoZGwquR+SGnw=="],
|
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.93", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.69", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hcXDU8QDwpAzLVTuY932TQVlIij9+iaVTxc5mPGY6yb//JMAAC5hMVhg93IrxlrxWLvMgjezNgoZGwquR+SGnw=="],
|
||||||
|
|
||||||
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rwLi/Rsuj2pYniQXIrvClHvXDzgM4UQHHnvHTWEF14efnlKclG/1ghpNC+adsRujAbCTr6gRsSbDE2vEqriV7g=="],
|
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rwLi/Rsuj2pYniQXIrvClHvXDzgM4UQHHnvHTWEF14efnlKclG/1ghpNC+adsRujAbCTr6gRsSbDE2vEqriV7g=="],
|
||||||
@@ -1540,8 +1527,6 @@
|
|||||||
|
|
||||||
"@opencode-ai/sdk": ["@opencode-ai/sdk@workspace:packages/sdk/js"],
|
"@opencode-ai/sdk": ["@opencode-ai/sdk@workspace:packages/sdk/js"],
|
||||||
|
|
||||||
"@opencode-ai/server": ["@opencode-ai/server@workspace:packages/server"],
|
|
||||||
|
|
||||||
"@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"],
|
"@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"],
|
||||||
|
|
||||||
"@opencode-ai/storybook": ["@opencode-ai/storybook@workspace:packages/storybook"],
|
"@opencode-ai/storybook": ["@opencode-ai/storybook@workspace:packages/storybook"],
|
||||||
@@ -1556,21 +1541,21 @@
|
|||||||
|
|
||||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||||
|
|
||||||
"@opentui/core": ["@opentui/core@0.1.99", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.99", "@opentui/core-darwin-x64": "0.1.99", "@opentui/core-linux-arm64": "0.1.99", "@opentui/core-linux-x64": "0.1.99", "@opentui/core-win32-arm64": "0.1.99", "@opentui/core-win32-x64": "0.1.99", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-I3+AEgGzqNWIpWX9g2WOscSPwtQDNOm4KlBjxBWCZjLxkF07u77heWXF7OiAdhKLtNUW6TFiyt6yznqAZPdG3A=="],
|
"@opentui/core": ["@opentui/core@0.1.97", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.97", "@opentui/core-darwin-x64": "0.1.97", "@opentui/core-linux-arm64": "0.1.97", "@opentui/core-linux-x64": "0.1.97", "@opentui/core-win32-arm64": "0.1.97", "@opentui/core-win32-x64": "0.1.97", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-2ENH0Dc4NUAeHeeQCQhF1lg68RuyntOUP68UvortvDqTz/hqLG0tIwF+DboCKtWi8Nmao4SAQEJ7lfmyQNEDOQ=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.99", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bzVrqeX2vb5iWrc/ftOUOqeUY8XO+qSgoTwj5TXHuwagavgwD3Hpeyjx8+icnTTeM4pao0som1WR9xfye6/X5Q=="],
|
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.97", "", { "os": "darwin", "cpu": "arm64" }, "sha512-t7oMGEfMPQsqLEx7/rPqv/UGJ+vqhe4RWHRRQRYcuHuLKssZ2S8P9mSS7MBPtDqGcxg4PosCrh5nHYeZ94EXUw=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.99", "", { "os": "darwin", "cpu": "x64" }, "sha512-VE4FrXBYpkxnvkqcCV1a8aN9jyyMJMihVW+V2NLCtp+4yQsj0AapG5TiUSN76XnmSZRptxDy5rBmEempeoIZbg=="],
|
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.97", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZuPWAawlVat6ZHb8vaH/CVUeGwI0pI4vd+6zz1ZocZn95ZWJztfyhzNZOJrq1WjHmUROieJ7cOuYUZfvYNuLrg=="],
|
||||||
|
|
||||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.99", "", { "os": "linux", "cpu": "arm64" }, "sha512-viXQsbpS7yHjYkl7+am32JdvG96QU9lvHh1UiZtpOxcNUUqiYmA2ZwZFPD2Bi54jNyj5l2hjH6YkD3DzE2FEWA=="],
|
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.97", "", { "os": "linux", "cpu": "arm64" }, "sha512-QXxhz654vXgEu2wrFFFFnrSWbyk6/r6nXNnDTcMRWofdMZQLx87NhbcsErNmz9KmFdzoPiQSmlpYubLflKKzqQ=="],
|
||||||
|
|
||||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.99", "", { "os": "linux", "cpu": "x64" }, "sha512-WLoEFINOSp0tZSR9y4LUuGc7n4Y7H1wcpjUPzQ9vChkYDXrfZltEanzoDWbDcQ4kZQW5tHVC7LrZHpAsRLwFZg=="],
|
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.97", "", { "os": "linux", "cpu": "x64" }, "sha512-v3z0QWpRS3p8blE/A7pTu15hcFMtSndeiYhRxhrjp6zAhQ+UlruQs9DAG1ifSuVO1RJJ0pUKklFivdbu0pMzuw=="],
|
||||||
|
|
||||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.99", "", { "os": "win32", "cpu": "arm64" }, "sha512-yWMOLWCEO8HdrctU1dMkgZC8qGkiO4Dwr4/e11tTvVpRmYhDsP/IR89ZjEEtOwnKwFOFuB/MxvflqaEWVQ2g5Q=="],
|
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.97", "", { "os": "win32", "cpu": "arm64" }, "sha512-o/m9mD1dvOCwkxOUUyoEILl+d6tzh/85foJc4uqjXYi71NNcwg8u+Eq3/gdHuSKnlT1pusCPKoS1IDuBvZE24A=="],
|
||||||
|
|
||||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.99", "", { "os": "win32", "cpu": "x64" }, "sha512-aYRlsL2w8YRL6vPd7/hrqlNVkXU3QowWb01TOvAcHS8UAsXaGFUr47kSDyjxDi1wg1MzmVduCfsC7T3NoThV1w=="],
|
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.97", "", { "os": "win32", "cpu": "x64" }, "sha512-Rwp7JOwrYm4wtzPHY2vv+2l91LXmKSI7CtbmWN1sSUGhBPtPGSvfwux3W5xaAZQa2KPEXicPjaKJZc+pob3YRg=="],
|
||||||
|
|
||||||
"@opentui/solid": ["@opentui/solid@0.1.99", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.99", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-DrqqO4h2V88FmeIP2cErYkMU0ZK5MrUsZw3w6IzZpoXyyiL4/9qpWzUq+CXx+r16VP2iGxDJwGKUmtFAzUch2Q=="],
|
"@opentui/solid": ["@opentui/solid@0.1.97", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.97", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-ma/uihG38F+6oLJVD8yR7z82FWmR8QhfesNV5SBXbN74riMCRyy6kyQ6SI4xs4ykt9BbZOjrKLq+Xt/0Pd0SJQ=="],
|
||||||
|
|
||||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||||
|
|
||||||
@@ -2680,8 +2665,6 @@
|
|||||||
|
|
||||||
"cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="],
|
"cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="],
|
||||||
|
|
||||||
"cli-sound": ["cli-sound@1.1.3", "", { "dependencies": { "find-exec": "^1.0.3" }, "bin": { "cli-sound": "dist/esm/cli.js" } }, "sha512-dpdF3KS3wjo1fobKG5iU9KyKqzQWAqueymHzZ9epus/dZ40487gAvS6aXFeBul+GiQAQYUTAtUWgQvw6Jftbyg=="],
|
|
||||||
|
|
||||||
"cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="],
|
"cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="],
|
||||||
|
|
||||||
"cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="],
|
"cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="],
|
||||||
@@ -3106,8 +3089,6 @@
|
|||||||
|
|
||||||
"find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="],
|
"find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="],
|
||||||
|
|
||||||
"find-exec": ["find-exec@1.0.3", "", { "dependencies": { "shell-quote": "^1.8.1" } }, "sha512-gnG38zW90mS8hm5smNcrBnakPEt+cGJoiMkJwCU0IYnEb0H2NQk0NIljhNW+48oniCriFek/PH6QXbwsJo/qug=="],
|
|
||||||
|
|
||||||
"find-my-way": ["find-my-way@9.5.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", "safe-regex2": "^5.0.0" } }, "sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ=="],
|
"find-my-way": ["find-my-way@9.5.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", "safe-regex2": "^5.0.0" } }, "sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ=="],
|
||||||
|
|
||||||
"find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="],
|
"find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="],
|
||||||
@@ -3346,8 +3327,6 @@
|
|||||||
|
|
||||||
"image-q": ["image-q@4.0.0", "", { "dependencies": { "@types/node": "16.9.1" } }, "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw=="],
|
"image-q": ["image-q@4.0.0", "", { "dependencies": { "@types/node": "16.9.1" } }, "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw=="],
|
||||||
|
|
||||||
"immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="],
|
|
||||||
|
|
||||||
"import-local": ["import-local@3.2.0", "", { "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" }, "bin": { "import-local-fixture": "fixtures/cli.js" } }, "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA=="],
|
"import-local": ["import-local@3.2.0", "", { "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" }, "bin": { "import-local-fixture": "fixtures/cli.js" } }, "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA=="],
|
||||||
|
|
||||||
"import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="],
|
"import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="],
|
||||||
@@ -4358,8 +4337,6 @@
|
|||||||
|
|
||||||
"rimraf": ["rimraf@2.6.3", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA=="],
|
"rimraf": ["rimraf@2.6.3", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA=="],
|
||||||
|
|
||||||
"ripgrep": ["ripgrep@0.3.1", "", { "bin": { "rg": "lib/rg.mjs", "ripgrep": "lib/rg.mjs" } }, "sha512-6bDtNIBh1qPviVIU685/4uv0Ap5t8eS4wiJhy/tR2LdIeIey9CVasENlGS+ul3HnTmGANIp7AjnfsztsRmALfQ=="],
|
|
||||||
|
|
||||||
"roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="],
|
"roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="],
|
||||||
|
|
||||||
"rollup": ["rollup@4.60.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.1", "@rollup/rollup-android-arm64": "4.60.1", "@rollup/rollup-darwin-arm64": "4.60.1", "@rollup/rollup-darwin-x64": "4.60.1", "@rollup/rollup-freebsd-arm64": "4.60.1", "@rollup/rollup-freebsd-x64": "4.60.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.1", "@rollup/rollup-linux-arm64-gnu": "4.60.1", "@rollup/rollup-linux-arm64-musl": "4.60.1", "@rollup/rollup-linux-loong64-gnu": "4.60.1", "@rollup/rollup-linux-loong64-musl": "4.60.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.1", "@rollup/rollup-linux-ppc64-musl": "4.60.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.1", "@rollup/rollup-linux-riscv64-musl": "4.60.1", "@rollup/rollup-linux-s390x-gnu": "4.60.1", "@rollup/rollup-linux-x64-gnu": "4.60.1", "@rollup/rollup-linux-x64-musl": "4.60.1", "@rollup/rollup-openbsd-x64": "4.60.1", "@rollup/rollup-openharmony-arm64": "4.60.1", "@rollup/rollup-win32-arm64-msvc": "4.60.1", "@rollup/rollup-win32-ia32-msvc": "4.60.1", "@rollup/rollup-win32-x64-gnu": "4.60.1", "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w=="],
|
"rollup": ["rollup@4.60.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.1", "@rollup/rollup-android-arm64": "4.60.1", "@rollup/rollup-darwin-arm64": "4.60.1", "@rollup/rollup-darwin-x64": "4.60.1", "@rollup/rollup-freebsd-arm64": "4.60.1", "@rollup/rollup-freebsd-x64": "4.60.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.1", "@rollup/rollup-linux-arm64-gnu": "4.60.1", "@rollup/rollup-linux-arm64-musl": "4.60.1", "@rollup/rollup-linux-loong64-gnu": "4.60.1", "@rollup/rollup-linux-loong64-musl": "4.60.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.1", "@rollup/rollup-linux-ppc64-musl": "4.60.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.1", "@rollup/rollup-linux-riscv64-musl": "4.60.1", "@rollup/rollup-linux-s390x-gnu": "4.60.1", "@rollup/rollup-linux-x64-gnu": "4.60.1", "@rollup/rollup-linux-x64-musl": "4.60.1", "@rollup/rollup-openbsd-x64": "4.60.1", "@rollup/rollup-openharmony-arm64": "4.60.1", "@rollup/rollup-win32-arm64-msvc": "4.60.1", "@rollup/rollup-win32-ia32-msvc": "4.60.1", "@rollup/rollup-win32-x64-gnu": "4.60.1", "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w=="],
|
||||||
@@ -4432,8 +4409,6 @@
|
|||||||
|
|
||||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||||
|
|
||||||
"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
|
|
||||||
|
|
||||||
"shiki": ["shiki@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/engine-javascript": "3.20.0", "@shikijs/engine-oniguruma": "3.20.0", "@shikijs/langs": "3.20.0", "@shikijs/themes": "3.20.0", "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kgCOlsnyWb+p0WU+01RjkCH+eBVsjL1jOwUYWv0YDWkM2/A46+LDKVs5yZCUXjJG6bj4ndFoAg5iLIIue6dulg=="],
|
"shiki": ["shiki@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/engine-javascript": "3.20.0", "@shikijs/engine-oniguruma": "3.20.0", "@shikijs/langs": "3.20.0", "@shikijs/themes": "3.20.0", "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kgCOlsnyWb+p0WU+01RjkCH+eBVsjL1jOwUYWv0YDWkM2/A46+LDKVs5yZCUXjJG6bj4ndFoAg5iLIIue6dulg=="],
|
||||||
|
|
||||||
"shikiji": ["shikiji@0.6.13", "", { "dependencies": { "hast-util-to-html": "^9.0.0" } }, "sha512-4T7X39csvhT0p7GDnq9vysWddf2b6BeioiN3Ymhnt3xcy9tXmDcnsEFVxX18Z4YcQgEE/w48dLJ4pPPUcG9KkA=="],
|
"shikiji": ["shikiji@0.6.13", "", { "dependencies": { "hast-util-to-html": "^9.0.0" } }, "sha512-4T7X39csvhT0p7GDnq9vysWddf2b6BeioiN3Ymhnt3xcy9tXmDcnsEFVxX18Z4YcQgEE/w48dLJ4pPPUcG9KkA=="],
|
||||||
@@ -5028,8 +5003,6 @@
|
|||||||
|
|
||||||
"@actions/http-client/undici": ["undici@6.24.1", "", {}, "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA=="],
|
"@actions/http-client/undici": ["undici@6.24.1", "", {}, "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA=="],
|
||||||
|
|
||||||
"@ai-sdk/alibaba/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
|
||||||
|
|
||||||
"@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.69", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LshR7X3pFugY0o41G2VKTmg1XoGpSl7uoYWfzk6zjVZLhCfeFiwgpOga+eTV4XY1VVpZwKVqRnkDbIL7K2eH5g=="],
|
"@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.69", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LshR7X3pFugY0o41G2VKTmg1XoGpSl7uoYWfzk6zjVZLhCfeFiwgpOga+eTV4XY1VVpZwKVqRnkDbIL7K2eH5g=="],
|
||||||
|
|
||||||
"@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.12", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA=="],
|
"@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.12", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA=="],
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-2p0WOk7qE2zC8S5mIDmpefjhJv8zhsgT33crGFWl6LI=",
|
"x86_64-linux": "sha256-fNRQYkucjXr1D61HJRScJpDa6+oBdyhgTBxCu+PE2kQ=",
|
||||||
"aarch64-linux": "sha256-sMW7pXoFtV6r4ySoYB8ISqKFHFeAMmiCUvHtiplwxak=",
|
"aarch64-linux": "sha256-V8J6kn2nSdXrplyqi6aIqNlHcVjSxvye+yC/YFO7PF4=",
|
||||||
"aarch64-darwin": "sha256-/4g2e39t9huLXOObdolDPmImGNhndOsxeAGJjw+bE8g=",
|
"aarch64-darwin": "sha256-6cLmUJVUycGALCmslXuloVGBSlFOSHRjsWjx7KOW8rg=",
|
||||||
"x86_64-darwin": "sha256-SJ9y58ZwQnXhMtus0ITQo3sfHzHfOSPkJRK24n5pnBw="
|
"x86_64-darwin": "sha256-kcOSO3NFIJh79ylLotG41ovWLQfH5kh1WYFghUu+4HE="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-17
@@ -7,6 +7,7 @@
|
|||||||
sysctl,
|
sysctl,
|
||||||
makeBinaryWrapper,
|
makeBinaryWrapper,
|
||||||
models-dev,
|
models-dev,
|
||||||
|
ripgrep,
|
||||||
installShellFiles,
|
installShellFiles,
|
||||||
versionCheckHook,
|
versionCheckHook,
|
||||||
writableTmpDirAsHomeHook,
|
writableTmpDirAsHomeHook,
|
||||||
@@ -51,25 +52,25 @@ stdenvNoCC.mkDerivation (finalAttrs: {
|
|||||||
runHook postBuild
|
runHook postBuild
|
||||||
'';
|
'';
|
||||||
|
|
||||||
installPhase =
|
installPhase = ''
|
||||||
''
|
runHook preInstall
|
||||||
runHook preInstall
|
|
||||||
|
|
||||||
install -Dm755 dist/opencode-*/bin/opencode $out/bin/opencode
|
install -Dm755 dist/opencode-*/bin/opencode $out/bin/opencode
|
||||||
install -Dm644 schema.json $out/share/opencode/schema.json
|
install -Dm644 schema.json $out/share/opencode/schema.json
|
||||||
''
|
|
||||||
# bun runs sysctl to detect if dunning on rosetta2
|
wrapProgram $out/bin/opencode \
|
||||||
+ lib.optionalString stdenvNoCC.hostPlatform.isDarwin ''
|
--prefix PATH : ${
|
||||||
wrapProgram $out/bin/opencode \
|
lib.makeBinPath (
|
||||||
--prefix PATH : ${
|
[
|
||||||
lib.makeBinPath [
|
ripgrep
|
||||||
sysctl
|
|
||||||
]
|
]
|
||||||
}
|
# bun runs sysctl to detect if dunning on rosetta2
|
||||||
''
|
++ lib.optional stdenvNoCC.hostPlatform.isDarwin sysctl
|
||||||
+ ''
|
)
|
||||||
runHook postInstall
|
}
|
||||||
'';
|
|
||||||
|
runHook postInstall
|
||||||
|
'';
|
||||||
|
|
||||||
postInstall = lib.optionalString (stdenvNoCC.buildPlatform.canExecute stdenvNoCC.hostPlatform) ''
|
postInstall = lib.optionalString (stdenvNoCC.buildPlatform.canExecute stdenvNoCC.hostPlatform) ''
|
||||||
# trick yargs into also generating zsh completions
|
# trick yargs into also generating zsh completions
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.4.4",
|
"version": "1.4.3",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -155,12 +155,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
|||||||
resetHeartbeat()
|
resetHeartbeat()
|
||||||
streamErrorLogged = false
|
streamErrorLogged = false
|
||||||
const directory = event.directory ?? "global"
|
const directory = event.directory ?? "global"
|
||||||
if (event.payload.type === "sync") {
|
const payload = event.payload
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = event.payload as Event
|
|
||||||
|
|
||||||
const k = key(directory, payload)
|
const k = key(directory, payload)
|
||||||
if (k) {
|
if (k) {
|
||||||
const i = coalesced.get(k)
|
const i = coalesced.get(k)
|
||||||
|
|||||||
@@ -122,21 +122,20 @@ export async function bootstrapGlobal(input: {
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
await runAll(fast)
|
|
||||||
// showErrors({
|
showErrors({
|
||||||
// errors: errors(await runAll(fast)),
|
errors: errors(await runAll(fast)),
|
||||||
// title: input.requestFailedTitle,
|
title: input.requestFailedTitle,
|
||||||
// translate: input.translate,
|
translate: input.translate,
|
||||||
// formatMoreCount: input.formatMoreCount,
|
formatMoreCount: input.formatMoreCount,
|
||||||
// })
|
})
|
||||||
await waitForPaint()
|
await waitForPaint()
|
||||||
await runAll(slow)
|
showErrors({
|
||||||
// showErrors({
|
errors: errors(await runAll(slow)),
|
||||||
// errors: errors(),
|
title: input.requestFailedTitle,
|
||||||
// title: input.requestFailedTitle,
|
translate: input.translate,
|
||||||
// translate: input.translate,
|
formatMoreCount: input.formatMoreCount,
|
||||||
// formatMoreCount: input.formatMoreCount,
|
})
|
||||||
// })
|
|
||||||
input.setGlobalStore("ready", true)
|
input.setGlobalStore("ready", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ const WorkspaceSessionList = (props: {
|
|||||||
<div class="relative w-full py-1">
|
<div class="relative w-full py-1">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="flex w-full text-left justify-start text-14-regular text-text-weak pl-2 pr-10"
|
class="flex w-full text-left justify-start text-14-regular text-text-weak pl-9 pr-10"
|
||||||
size="large"
|
size="large"
|
||||||
onClick={(e: MouseEvent) => {
|
onClick={(e: MouseEvent) => {
|
||||||
props.loadMore()
|
props.loadMore()
|
||||||
|
|||||||
@@ -642,10 +642,10 @@ export function MessageTimeline(props: {
|
|||||||
onClick={props.onResumeScroll}
|
onClick={props.onResumeScroll}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-center w-8 h-6 rounded-[6px] border border-border-weaker-base bg-[color-mix(in_srgb,var(--surface-raised-stronger-non-alpha)_80%,transparent)] backdrop-blur-[0.75px] transition-colors group-hover:border-[var(--border-weak-base)] group-hover:[--icon-base:var(--icon-hover)]"
|
class="flex items-center justify-center w-8 h-6 rounded-[6px] border border-[var(--gray-dark-7)] bg-[color-mix(in_srgb,var(--gray-dark-3)_80%,transparent)] backdrop-blur-[0.75px] transition-colors group-hover:border-[var(--gray-dark-8)] [--icon-base:var(--gray-dark-10)] group-hover:[--icon-base:var(--gray-dark-11)]"
|
||||||
style={{
|
style={{
|
||||||
"box-shadow":
|
"box-shadow":
|
||||||
"0 51px 60px 0 rgba(0,0,0,0.10), 0 15px 18px 0 rgba(0,0,0,0.12), 0 6.386px 7.513px 0 rgba(0,0,0,0.12), 0 2.31px 2.717px 0 rgba(0,0,0,0.20)",
|
"0 51px 60px 0 rgba(0,0,0,0.13), 0 15.375px 18.088px 0 rgba(0,0,0,0.19), 0 6.386px 7.513px 0 rgba(0,0,0,0.25), 0 2.31px 2.717px 0 rgba(0,0,0,0.38)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon name="arrow-down-to-line" size="small" />
|
<Icon name="arrow-down-to-line" size="small" />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.4.4",
|
"version": "1.4.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -11,6 +11,5 @@ class LimitError extends Error {
|
|||||||
this.retryAfter = retryAfter
|
this.retryAfter = retryAfter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class RateLimitError extends LimitError {}
|
|
||||||
export class FreeUsageLimitError extends LimitError {}
|
export class FreeUsageLimitError extends LimitError {}
|
||||||
export class SubscriptionUsageLimitError extends LimitError {}
|
export class SubscriptionUsageLimitError extends LimitError {}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
MonthlyLimitError,
|
MonthlyLimitError,
|
||||||
UserLimitError,
|
UserLimitError,
|
||||||
ModelError,
|
ModelError,
|
||||||
RateLimitError,
|
|
||||||
FreeUsageLimitError,
|
FreeUsageLimitError,
|
||||||
SubscriptionUsageLimitError,
|
SubscriptionUsageLimitError,
|
||||||
} from "./error"
|
} from "./error"
|
||||||
@@ -36,8 +35,7 @@ import { anthropicHelper } from "./provider/anthropic"
|
|||||||
import { googleHelper } from "./provider/google"
|
import { googleHelper } from "./provider/google"
|
||||||
import { openaiHelper } from "./provider/openai"
|
import { openaiHelper } from "./provider/openai"
|
||||||
import { oaCompatHelper } from "./provider/openai-compatible"
|
import { oaCompatHelper } from "./provider/openai-compatible"
|
||||||
import { createRateLimiter as createIpRateLimiter } from "./ipRateLimiter"
|
import { createRateLimiter } from "./rateLimiter"
|
||||||
import { createRateLimiter as createKeyRateLimiter } from "./keyRateLimiter"
|
|
||||||
import { createDataDumper } from "./dataDumper"
|
import { createDataDumper } from "./dataDumper"
|
||||||
import { createTrialLimiter } from "./trialLimiter"
|
import { createTrialLimiter } from "./trialLimiter"
|
||||||
import { createStickyTracker } from "./stickyProviderTracker"
|
import { createStickyTracker } from "./stickyProviderTracker"
|
||||||
@@ -94,8 +92,6 @@ export async function handler(
|
|||||||
const isStream = opts.parseIsStream(url, body)
|
const isStream = opts.parseIsStream(url, body)
|
||||||
const rawIp = input.request.headers.get("x-real-ip") ?? ""
|
const rawIp = input.request.headers.get("x-real-ip") ?? ""
|
||||||
const ip = rawIp.includes(":") ? rawIp.split(":").slice(0, 4).join(":") : rawIp
|
const ip = rawIp.includes(":") ? rawIp.split(":").slice(0, 4).join(":") : rawIp
|
||||||
const rawZenApiKey = opts.parseApiKey(input.request.headers)
|
|
||||||
const zenApiKey = rawZenApiKey === "public" ? undefined : rawZenApiKey
|
|
||||||
const sessionId = input.request.headers.get("x-opencode-session") ?? ""
|
const sessionId = input.request.headers.get("x-opencode-session") ?? ""
|
||||||
const requestId = input.request.headers.get("x-opencode-request") ?? ""
|
const requestId = input.request.headers.get("x-opencode-request") ?? ""
|
||||||
const projectId = input.request.headers.get("x-opencode-project") ?? ""
|
const projectId = input.request.headers.get("x-opencode-project") ?? ""
|
||||||
@@ -110,15 +106,19 @@ export async function handler(
|
|||||||
const zenData = ZenData.list(opts.modelList)
|
const zenData = ZenData.list(opts.modelList)
|
||||||
const modelInfo = validateModel(zenData, model)
|
const modelInfo = validateModel(zenData, model)
|
||||||
const dataDumper = createDataDumper(sessionId, requestId, projectId)
|
const dataDumper = createDataDumper(sessionId, requestId, projectId)
|
||||||
const trialLimiter = createTrialLimiter(modelInfo.trialProvider, ip)
|
const trialLimiter = createTrialLimiter(modelInfo.trialProviders, ip)
|
||||||
const trialProviders = await trialLimiter?.check()
|
const trialProviders = await trialLimiter?.check()
|
||||||
const rateLimiter = modelInfo.allowAnonymous
|
const rateLimiter = createRateLimiter(
|
||||||
? createIpRateLimiter(modelInfo.id, modelInfo.rateLimit, ip, input.request)
|
modelInfo.id,
|
||||||
: createKeyRateLimiter(modelInfo.id, zenApiKey, input.request)
|
modelInfo.allowAnonymous,
|
||||||
|
modelInfo.rateLimit,
|
||||||
|
ip,
|
||||||
|
input.request,
|
||||||
|
)
|
||||||
await rateLimiter?.check()
|
await rateLimiter?.check()
|
||||||
const stickyTracker = createStickyTracker(modelInfo.stickyProvider, sessionId)
|
const stickyTracker = createStickyTracker(modelInfo.stickyProvider, sessionId)
|
||||||
const stickyProvider = await stickyTracker?.get()
|
const stickyProvider = await stickyTracker?.get()
|
||||||
const authInfo = await authenticate(modelInfo, zenApiKey)
|
const authInfo = await authenticate(modelInfo)
|
||||||
const billingSource = validateBilling(authInfo, modelInfo)
|
const billingSource = validateBilling(authInfo, modelInfo)
|
||||||
logger.metric({ source: billingSource })
|
logger.metric({ source: billingSource })
|
||||||
|
|
||||||
@@ -363,11 +363,7 @@ export async function handler(
|
|||||||
{ status: 401 },
|
{ status: 401 },
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if (error instanceof FreeUsageLimitError || error instanceof SubscriptionUsageLimitError) {
|
||||||
error instanceof RateLimitError ||
|
|
||||||
error instanceof FreeUsageLimitError ||
|
|
||||||
error instanceof SubscriptionUsageLimitError
|
|
||||||
) {
|
|
||||||
const headers = new Headers()
|
const headers = new Headers()
|
||||||
if (error.retryAfter) {
|
if (error.retryAfter) {
|
||||||
headers.set("retry-after", String(error.retryAfter))
|
headers.set("retry-after", String(error.retryAfter))
|
||||||
@@ -396,7 +392,7 @@ export async function handler(
|
|||||||
function validateModel(zenData: ZenData, reqModel: string) {
|
function validateModel(zenData: ZenData, reqModel: string) {
|
||||||
if (!(reqModel in zenData.models)) throw new ModelError(t("zen.api.error.modelNotSupported", { model: reqModel }))
|
if (!(reqModel in zenData.models)) throw new ModelError(t("zen.api.error.modelNotSupported", { model: reqModel }))
|
||||||
|
|
||||||
const modelId = reqModel
|
const modelId = reqModel as keyof typeof zenData.models
|
||||||
const modelData = Array.isArray(zenData.models[modelId])
|
const modelData = Array.isArray(zenData.models[modelId])
|
||||||
? zenData.models[modelId].find((model) => opts.format === model.formatFilter)
|
? zenData.models[modelId].find((model) => opts.format === model.formatFilter)
|
||||||
: zenData.models[modelId]
|
: zenData.models[modelId]
|
||||||
@@ -496,8 +492,9 @@ export async function handler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function authenticate(modelInfo: ModelInfo, zenApiKey?: string) {
|
async function authenticate(modelInfo: ModelInfo) {
|
||||||
if (!zenApiKey) {
|
const apiKey = opts.parseApiKey(input.request.headers)
|
||||||
|
if (!apiKey || apiKey === "public") {
|
||||||
if (modelInfo.allowAnonymous) return
|
if (modelInfo.allowAnonymous) return
|
||||||
throw new AuthError(t("zen.api.error.missingApiKey"))
|
throw new AuthError(t("zen.api.error.missingApiKey"))
|
||||||
}
|
}
|
||||||
@@ -576,7 +573,7 @@ export async function handler(
|
|||||||
isNull(LiteTable.timeDeleted),
|
isNull(LiteTable.timeDeleted),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.where(and(eq(KeyTable.key, zenApiKey), isNull(KeyTable.timeDeleted)))
|
.where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted)))
|
||||||
.then((rows) => rows[0]),
|
.then((rows) => rows[0]),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
import { Database, eq, and, sql } from "@opencode-ai/console-core/drizzle/index.js"
|
|
||||||
import { KeyRateLimitTable } from "@opencode-ai/console-core/schema/ip.sql.js"
|
|
||||||
import { RateLimitError } from "./error"
|
|
||||||
import { i18n } from "~/i18n"
|
|
||||||
import { localeFromRequest } from "~/lib/language"
|
|
||||||
|
|
||||||
export function createRateLimiter(modelId: string, zenApiKey: string | undefined, request: Request) {
|
|
||||||
if (!zenApiKey) return
|
|
||||||
const dict = i18n(localeFromRequest(request))
|
|
||||||
|
|
||||||
const LIMIT = 100
|
|
||||||
const yyyyMMddHHmm = new Date(Date.now())
|
|
||||||
.toISOString()
|
|
||||||
.replace(/[^0-9]/g, "")
|
|
||||||
.substring(0, 12)
|
|
||||||
const interval = `${modelId.substring(0, 27)}-${yyyyMMddHHmm}`
|
|
||||||
|
|
||||||
return {
|
|
||||||
check: async () => {
|
|
||||||
const rows = await Database.use((tx) =>
|
|
||||||
tx
|
|
||||||
.select({ interval: KeyRateLimitTable.interval, count: KeyRateLimitTable.count })
|
|
||||||
.from(KeyRateLimitTable)
|
|
||||||
.where(and(eq(KeyRateLimitTable.key, zenApiKey), eq(KeyRateLimitTable.interval, interval))),
|
|
||||||
).then((rows) => rows[0])
|
|
||||||
const count = rows?.count ?? 0
|
|
||||||
|
|
||||||
if (count >= LIMIT) throw new RateLimitError(dict["zen.api.error.rateLimitExceeded"], 60)
|
|
||||||
},
|
|
||||||
track: async () => {
|
|
||||||
await Database.use((tx) =>
|
|
||||||
tx
|
|
||||||
.insert(KeyRateLimitTable)
|
|
||||||
.values({ key: zenApiKey, interval, count: 1 })
|
|
||||||
.onDuplicateKeyUpdate({ set: { count: sql`${KeyRateLimitTable.count} + 1` } }),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,14 +6,12 @@ type Usage = {
|
|||||||
total_tokens?: number
|
total_tokens?: number
|
||||||
// used by moonshot
|
// used by moonshot
|
||||||
cached_tokens?: number
|
cached_tokens?: number
|
||||||
// used by xai & alibaba
|
// used by xai
|
||||||
prompt_tokens_details?: {
|
prompt_tokens_details?: {
|
||||||
text_tokens?: number
|
text_tokens?: number
|
||||||
audio_tokens?: number
|
audio_tokens?: number
|
||||||
image_tokens?: number
|
image_tokens?: number
|
||||||
cached_tokens?: number
|
cached_tokens?: number
|
||||||
// used by alibaba
|
|
||||||
cache_creation_input_tokens?: number
|
|
||||||
}
|
}
|
||||||
completion_tokens_details?: {
|
completion_tokens_details?: {
|
||||||
reasoning_tokens?: number
|
reasoning_tokens?: number
|
||||||
@@ -64,7 +62,6 @@ export const oaCompatHelper: ProviderHelper = ({ adjustCacheUsage, safetyIdentif
|
|||||||
const outputTokens = usage.completion_tokens ?? 0
|
const outputTokens = usage.completion_tokens ?? 0
|
||||||
const reasoningTokens = usage.completion_tokens_details?.reasoning_tokens ?? undefined
|
const reasoningTokens = usage.completion_tokens_details?.reasoning_tokens ?? undefined
|
||||||
let cacheReadTokens = usage.cached_tokens ?? usage.prompt_tokens_details?.cached_tokens ?? undefined
|
let cacheReadTokens = usage.cached_tokens ?? usage.prompt_tokens_details?.cached_tokens ?? undefined
|
||||||
const cacheWriteTokens = usage.prompt_tokens_details?.cache_creation_input_tokens ?? undefined
|
|
||||||
|
|
||||||
if (adjustCacheUsage && !cacheReadTokens) {
|
if (adjustCacheUsage && !cacheReadTokens) {
|
||||||
cacheReadTokens = Math.floor(inputTokens * 0.9)
|
cacheReadTokens = Math.floor(inputTokens * 0.9)
|
||||||
@@ -75,7 +72,7 @@ export const oaCompatHelper: ProviderHelper = ({ adjustCacheUsage, safetyIdentif
|
|||||||
outputTokens,
|
outputTokens,
|
||||||
reasoningTokens,
|
reasoningTokens,
|
||||||
cacheReadTokens,
|
cacheReadTokens,
|
||||||
cacheWrite5mTokens: cacheWriteTokens,
|
cacheWrite5mTokens: undefined,
|
||||||
cacheWrite1hTokens: undefined,
|
cacheWrite1hTokens: undefined,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+8
-1
@@ -6,7 +6,14 @@ import { i18n } from "~/i18n"
|
|||||||
import { localeFromRequest } from "~/lib/language"
|
import { localeFromRequest } from "~/lib/language"
|
||||||
import { Subscription } from "@opencode-ai/console-core/subscription.js"
|
import { Subscription } from "@opencode-ai/console-core/subscription.js"
|
||||||
|
|
||||||
export function createRateLimiter(modelId: string, rateLimit: number | undefined, rawIp: string, request: Request) {
|
export function createRateLimiter(
|
||||||
|
modelId: string,
|
||||||
|
allowAnonymous: boolean | undefined,
|
||||||
|
rateLimit: number | undefined,
|
||||||
|
rawIp: string,
|
||||||
|
request: Request,
|
||||||
|
) {
|
||||||
|
if (!allowAnonymous) return
|
||||||
const dict = i18n(localeFromRequest(request))
|
const dict = i18n(localeFromRequest(request))
|
||||||
|
|
||||||
const limits = Subscription.getFreeLimits()
|
const limits = Subscription.getFreeLimits()
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { getRetryAfterDay } from "../src/routes/zen/util/ipRateLimiter"
|
import { getRetryAfterDay } from "../src/routes/zen/util/rateLimiter"
|
||||||
|
|
||||||
describe("getRetryAfterDay", () => {
|
describe("getRetryAfterDay", () => {
|
||||||
test("returns full day at midnight UTC", () => {
|
test("returns full day at midnight UTC", () => {
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
CREATE TABLE `key_rate_limit` (
|
|
||||||
`key` varchar(255) NOT NULL,
|
|
||||||
`interval` varchar(12) NOT NULL,
|
|
||||||
`count` int NOT NULL,
|
|
||||||
CONSTRAINT PRIMARY KEY(`key`,`interval`)
|
|
||||||
);
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE `key_rate_limit` MODIFY COLUMN `interval` varchar(20) NOT NULL;
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE `key_rate_limit` MODIFY COLUMN `interval` varchar(40) NOT NULL;
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.4.4",
|
"version": "1.4.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export namespace ZenData {
|
|||||||
allowAnonymous: z.boolean().optional(),
|
allowAnonymous: z.boolean().optional(),
|
||||||
byokProvider: z.enum(["openai", "anthropic", "google"]).optional(),
|
byokProvider: z.enum(["openai", "anthropic", "google"]).optional(),
|
||||||
stickyProvider: z.enum(["strict", "prefer"]).optional(),
|
stickyProvider: z.enum(["strict", "prefer"]).optional(),
|
||||||
trialProvider: z.string().optional(),
|
trialProviders: z.array(z.string()).optional(),
|
||||||
trialEnded: z.boolean().optional(),
|
trialEnded: z.boolean().optional(),
|
||||||
fallbackProvider: z.string().optional(),
|
fallbackProvider: z.string().optional(),
|
||||||
rateLimit: z.number().optional(),
|
rateLimit: z.number().optional(),
|
||||||
@@ -45,7 +45,7 @@ export namespace ZenData {
|
|||||||
|
|
||||||
const ProviderSchema = z.object({
|
const ProviderSchema = z.object({
|
||||||
api: z.string(),
|
api: z.string(),
|
||||||
apiKey: z.union([z.string(), z.record(z.string(), z.string())]),
|
apiKey: z.string(),
|
||||||
format: FormatSchema.optional(),
|
format: FormatSchema.optional(),
|
||||||
headerMappings: z.record(z.string(), z.string()).optional(),
|
headerMappings: z.record(z.string(), z.string()).optional(),
|
||||||
payloadModifier: z.record(z.string(), z.any()).optional(),
|
payloadModifier: z.record(z.string(), z.any()).optional(),
|
||||||
@@ -54,10 +54,7 @@ export namespace ZenData {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const ModelsSchema = z.object({
|
const ModelsSchema = z.object({
|
||||||
zenModels: z.record(
|
models: z.record(z.string(), z.union([ModelSchema, z.array(ModelSchema.extend({ formatFilter: FormatSchema }))])),
|
||||||
z.string(),
|
|
||||||
z.union([ModelSchema, z.array(ModelSchema.extend({ formatFilter: FormatSchema }))]),
|
|
||||||
),
|
|
||||||
liteModels: z.record(
|
liteModels: z.record(
|
||||||
z.string(),
|
z.string(),
|
||||||
z.union([ModelSchema, z.array(ModelSchema.extend({ formatFilter: FormatSchema }))]),
|
z.union([ModelSchema, z.array(ModelSchema.extend({ formatFilter: FormatSchema }))]),
|
||||||
@@ -102,66 +99,10 @@ export namespace ZenData {
|
|||||||
Resource.ZEN_MODELS29.value +
|
Resource.ZEN_MODELS29.value +
|
||||||
Resource.ZEN_MODELS30.value,
|
Resource.ZEN_MODELS30.value,
|
||||||
)
|
)
|
||||||
const { zenModels, liteModels, providers } = ModelsSchema.parse(json)
|
const { models, liteModels, providers } = ModelsSchema.parse(json)
|
||||||
const compositeProviders = Object.fromEntries(
|
|
||||||
Object.entries(providers).map(([id, provider]) => [
|
|
||||||
id,
|
|
||||||
typeof provider.apiKey === "string"
|
|
||||||
? [{ id: id, key: provider.apiKey }]
|
|
||||||
: Object.entries(provider.apiKey).map(([kid, key]) => ({
|
|
||||||
id: `${id}.${kid}`,
|
|
||||||
key,
|
|
||||||
})),
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
return {
|
return {
|
||||||
providers: Object.fromEntries(
|
models: modelList === "lite" ? liteModels : models,
|
||||||
Object.entries(providers).flatMap(([providerId, provider]) =>
|
providers,
|
||||||
compositeProviders[providerId].map((p) => [p.id, { ...provider, apiKey: p.key }]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
models: (() => {
|
|
||||||
const normalize = (model: z.infer<typeof ModelSchema>) => {
|
|
||||||
const composite = model.providers.find((p) => compositeProviders[p.id].length > 1)
|
|
||||||
if (!composite)
|
|
||||||
return {
|
|
||||||
trialProvider: model.trialProvider ? [model.trialProvider] : undefined,
|
|
||||||
}
|
|
||||||
|
|
||||||
const weightMulti = compositeProviders[composite.id].length
|
|
||||||
|
|
||||||
return {
|
|
||||||
trialProvider: (() => {
|
|
||||||
if (!model.trialProvider) return undefined
|
|
||||||
if (model.trialProvider === composite.id) return compositeProviders[composite.id].map((p) => p.id)
|
|
||||||
return [model.trialProvider]
|
|
||||||
})(),
|
|
||||||
providers: model.providers.flatMap((p) =>
|
|
||||||
p.id === composite.id
|
|
||||||
? compositeProviders[p.id].map((sub) => ({
|
|
||||||
...p,
|
|
||||||
id: sub.id,
|
|
||||||
weight: p.weight ?? 1,
|
|
||||||
}))
|
|
||||||
: [
|
|
||||||
{
|
|
||||||
...p,
|
|
||||||
weight: (p.weight ?? 1) * weightMulti,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.fromEntries(
|
|
||||||
Object.entries(modelList === "lite" ? liteModels : zenModels).map(([modelId, model]) => {
|
|
||||||
const n = Array.isArray(model)
|
|
||||||
? model.map((m) => ({ ...m, ...normalize(m) }))
|
|
||||||
: { ...model, ...normalize(model) }
|
|
||||||
return [modelId, n]
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})(),
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,13 +20,3 @@ export const IpRateLimitTable = mysqlTable(
|
|||||||
},
|
},
|
||||||
(table) => [primaryKey({ columns: [table.ip, table.interval] })],
|
(table) => [primaryKey({ columns: [table.ip, table.interval] })],
|
||||||
)
|
)
|
||||||
|
|
||||||
export const KeyRateLimitTable = mysqlTable(
|
|
||||||
"key_rate_limit",
|
|
||||||
{
|
|
||||||
key: varchar("key", { length: 255 }).notNull(),
|
|
||||||
interval: varchar("interval", { length: 40 }).notNull(),
|
|
||||||
count: int("count").notNull(),
|
|
||||||
},
|
|
||||||
(table) => [primaryKey({ columns: [table.key, table.interval] })],
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.4.4",
|
"version": "1.4.3",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.4.4",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/desktop-electron",
|
"name": "@opencode-ai/desktop-electron",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.4.4",
|
"version": "1.4.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://opencode.ai",
|
"homepage": "https://opencode.ai",
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export function createMainWindow(globals: Globals) {
|
|||||||
y: state.y,
|
y: state.y,
|
||||||
width: state.width,
|
width: state.width,
|
||||||
height: state.height,
|
height: state.height,
|
||||||
show: false,
|
show: true,
|
||||||
title: "OpenCode",
|
title: "OpenCode",
|
||||||
icon: iconPath(),
|
icon: iconPath(),
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
@@ -94,10 +94,6 @@ export function createMainWindow(globals: Globals) {
|
|||||||
wireZoom(win)
|
wireZoom(win)
|
||||||
injectGlobals(win, globals)
|
injectGlobals(win, globals)
|
||||||
|
|
||||||
win.once("ready-to-show", () => {
|
|
||||||
win.show()
|
|
||||||
})
|
|
||||||
|
|
||||||
return win
|
return win
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.4.4",
|
"version": "1.4.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.4.4",
|
"version": "1.4.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
id = "opencode"
|
id = "opencode"
|
||||||
name = "OpenCode"
|
name = "OpenCode"
|
||||||
description = "The open source coding agent."
|
description = "The open source coding agent."
|
||||||
version = "1.4.4"
|
version = "1.4.3"
|
||||||
schema_version = 1
|
schema_version = 1
|
||||||
authors = ["Anomaly"]
|
authors = ["Anomaly"]
|
||||||
repository = "https://github.com/anomalyco/opencode"
|
repository = "https://github.com/anomalyco/opencode"
|
||||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
|||||||
icon = "./icons/opencode.svg"
|
icon = "./icons/opencode.svg"
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-aarch64]
|
[agent_servers.opencode.targets.darwin-aarch64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.4/opencode-darwin-arm64.zip"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.3/opencode-darwin-arm64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-x86_64]
|
[agent_servers.opencode.targets.darwin-x86_64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.4/opencode-darwin-x64.zip"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.3/opencode-darwin-x64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-aarch64]
|
[agent_servers.opencode.targets.linux-aarch64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.4/opencode-linux-arm64.tar.gz"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.3/opencode-linux-arm64.tar.gz"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-x86_64]
|
[agent_servers.opencode.targets.linux-x86_64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.4/opencode-linux-x64.tar.gz"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.3/opencode-linux-x64.tar.gz"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.windows-x86_64]
|
[agent_servers.opencode.targets.windows-x86_64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.4/opencode-windows-x64.zip"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.3/opencode-windows-x64.zip"
|
||||||
cmd = "./opencode.exe"
|
cmd = "./opencode.exe"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.4.4",
|
"version": "1.4.3",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
Use these rules when writing or migrating Effect code.
|
Use these rules when writing or migrating Effect code.
|
||||||
|
|
||||||
See `specs/effect/migration.md` for the compact pattern reference and examples.
|
See `specs/effect-migration.md` for the compact pattern reference and examples.
|
||||||
|
|
||||||
## Core
|
## Core
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ See `specs/effect/migration.md` for the compact pattern reference and examples.
|
|||||||
|
|
||||||
## Effect.cached for deduplication
|
## Effect.cached for deduplication
|
||||||
|
|
||||||
Use `Effect.cached` when multiple concurrent callers should share a single in-flight computation rather than storing `Fiber | undefined` or `Promise | undefined` manually. See `specs/effect/migration.md` for the full pattern.
|
Use `Effect.cached` when multiple concurrent callers should share a single in-flight computation rather than storing `Fiber | undefined` or `Promise | undefined` manually. See `specs/effect-migration.md` for the full pattern.
|
||||||
|
|
||||||
## Instance.bind — ALS for native callbacks
|
## Instance.bind — ALS for native callbacks
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
|
||||||
CREATE TABLE `__new_workspace` (
|
|
||||||
`id` text PRIMARY KEY,
|
|
||||||
`type` text NOT NULL,
|
|
||||||
`name` text DEFAULT '' NOT NULL,
|
|
||||||
`branch` text,
|
|
||||||
`directory` text,
|
|
||||||
`extra` text,
|
|
||||||
`project_id` text NOT NULL,
|
|
||||||
CONSTRAINT `fk_workspace_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
INSERT INTO `__new_workspace`(`id`, `type`, `branch`, `name`, `directory`, `extra`, `project_id`) SELECT `id`, `type`, `branch`, `name`, `directory`, `extra`, `project_id` FROM `workspace`;--> statement-breakpoint
|
|
||||||
DROP TABLE `workspace`;--> statement-breakpoint
|
|
||||||
ALTER TABLE `__new_workspace` RENAME TO `workspace`;--> statement-breakpoint
|
|
||||||
PRAGMA foreign_keys=ON;
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +0,0 @@
|
|||||||
CREATE TABLE `session_entry` (
|
|
||||||
`id` text PRIMARY KEY,
|
|
||||||
`session_id` text NOT NULL,
|
|
||||||
`type` text NOT NULL,
|
|
||||||
`time_created` integer NOT NULL,
|
|
||||||
`time_updated` integer NOT NULL,
|
|
||||||
`data` text NOT NULL,
|
|
||||||
CONSTRAINT `fk_session_entry_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE INDEX `session_entry_session_idx` ON `session_entry` (`session_id`);--> statement-breakpoint
|
|
||||||
CREATE INDEX `session_entry_session_type_idx` ON `session_entry` (`session_id`,`type`);--> statement-breakpoint
|
|
||||||
CREATE INDEX `session_entry_time_created_idx` ON `session_entry` (`time_created`);
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.4.4",
|
"version": "1.4.3",
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -14,11 +14,18 @@
|
|||||||
"fix-node-pty": "bun run script/fix-node-pty.ts",
|
"fix-node-pty": "bun run script/fix-node-pty.ts",
|
||||||
"upgrade-opentui": "bun run script/upgrade-opentui.ts",
|
"upgrade-opentui": "bun run script/upgrade-opentui.ts",
|
||||||
"dev": "bun run --conditions=browser ./src/index.ts",
|
"dev": "bun run --conditions=browser ./src/index.ts",
|
||||||
|
"random": "echo 'Random script updated at $(date)' && echo 'Change queued successfully' && echo 'Another change made' && echo 'Yet another change' && echo 'One more change' && echo 'Final change' && echo 'Another final change' && echo 'Yet another final change'",
|
||||||
|
"clean": "echo 'Cleaning up...' && rm -rf node_modules dist",
|
||||||
|
"lint": "echo 'Running lint checks...' && bun test --coverage",
|
||||||
|
"format": "echo 'Formatting code...' && bun run --prettier --write src/**/*.ts",
|
||||||
|
"docs": "echo 'Generating documentation...' && find src -name '*.ts' -exec echo 'Processing: {}' \\;",
|
||||||
|
"deploy": "echo 'Deploying application...' && bun run build && echo 'Deployment completed successfully'",
|
||||||
"db": "bun drizzle-kit"
|
"db": "bun drizzle-kit"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode"
|
"opencode": "./bin/opencode"
|
||||||
},
|
},
|
||||||
|
"randomField": "this-is-a-random-value-12345",
|
||||||
"exports": {
|
"exports": {
|
||||||
"./*": "./src/*.ts"
|
"./*": "./src/*.ts"
|
||||||
},
|
},
|
||||||
@@ -76,7 +83,6 @@
|
|||||||
"@actions/core": "1.11.1",
|
"@actions/core": "1.11.1",
|
||||||
"@actions/github": "6.0.1",
|
"@actions/github": "6.0.1",
|
||||||
"@agentclientprotocol/sdk": "0.16.1",
|
"@agentclientprotocol/sdk": "0.16.1",
|
||||||
"@ai-sdk/alibaba": "1.0.17",
|
|
||||||
"@ai-sdk/amazon-bedrock": "4.0.93",
|
"@ai-sdk/amazon-bedrock": "4.0.93",
|
||||||
"@ai-sdk/anthropic": "3.0.67",
|
"@ai-sdk/anthropic": "3.0.67",
|
||||||
"@ai-sdk/azure": "3.0.49",
|
"@ai-sdk/azure": "3.0.49",
|
||||||
@@ -115,8 +121,8 @@
|
|||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
"@openrouter/ai-sdk-provider": "2.5.1",
|
"@openrouter/ai-sdk-provider": "2.5.1",
|
||||||
"@opentui/core": "0.1.99",
|
"@opentui/core": "0.1.97",
|
||||||
"@opentui/solid": "0.1.99",
|
"@opentui/solid": "0.1.97",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
@@ -128,7 +134,6 @@
|
|||||||
"bonjour-service": "1.3.0",
|
"bonjour-service": "1.3.0",
|
||||||
"bun-pty": "0.4.8",
|
"bun-pty": "0.4.8",
|
||||||
"chokidar": "4.0.3",
|
"chokidar": "4.0.3",
|
||||||
"cli-sound": "1.1.3",
|
|
||||||
"clipboardy": "4.0.0",
|
"clipboardy": "4.0.0",
|
||||||
"cross-spawn": "catalog:",
|
"cross-spawn": "catalog:",
|
||||||
"decimal.js": "10.5.0",
|
"decimal.js": "10.5.0",
|
||||||
@@ -143,7 +148,6 @@
|
|||||||
"hono": "catalog:",
|
"hono": "catalog:",
|
||||||
"hono-openapi": "catalog:",
|
"hono-openapi": "catalog:",
|
||||||
"ignore": "7.0.5",
|
"ignore": "7.0.5",
|
||||||
"immer": "11.1.4",
|
|
||||||
"jsonc-parser": "3.3.1",
|
"jsonc-parser": "3.3.1",
|
||||||
"mime-types": "3.0.2",
|
"mime-types": "3.0.2",
|
||||||
"minimatch": "10.0.3",
|
"minimatch": "10.0.3",
|
||||||
@@ -154,7 +158,6 @@
|
|||||||
"opentui-spinner": "0.0.6",
|
"opentui-spinner": "0.0.6",
|
||||||
"partial-json": "0.1.7",
|
"partial-json": "0.1.7",
|
||||||
"remeda": "catalog:",
|
"remeda": "catalog:",
|
||||||
"ripgrep": "0.3.1",
|
|
||||||
"semver": "^7.6.3",
|
"semver": "^7.6.3",
|
||||||
"solid-js": "catalog:",
|
"solid-js": "catalog:",
|
||||||
"strip-ansi": "7.1.2",
|
"strip-ansi": "7.1.2",
|
||||||
|
|||||||
@@ -187,7 +187,6 @@ for (const item of targets) {
|
|||||||
const rootPath = path.resolve(dir, "../../node_modules/@opentui/core/parser.worker.js")
|
const rootPath = path.resolve(dir, "../../node_modules/@opentui/core/parser.worker.js")
|
||||||
const parserWorker = fs.realpathSync(fs.existsSync(localPath) ? localPath : rootPath)
|
const parserWorker = fs.realpathSync(fs.existsSync(localPath) ? localPath : rootPath)
|
||||||
const workerPath = "./src/cli/cmd/tui/worker.ts"
|
const workerPath = "./src/cli/cmd/tui/worker.ts"
|
||||||
const rgPath = "./src/file/ripgrep.worker.ts"
|
|
||||||
|
|
||||||
// Use platform-specific bunfs root path based on target OS
|
// Use platform-specific bunfs root path based on target OS
|
||||||
const bunfsRoot = item.os === "win32" ? "B:/~BUN/root/" : "/$bunfs/root/"
|
const bunfsRoot = item.os === "win32" ? "B:/~BUN/root/" : "/$bunfs/root/"
|
||||||
@@ -198,9 +197,6 @@ for (const item of targets) {
|
|||||||
tsconfig: "./tsconfig.json",
|
tsconfig: "./tsconfig.json",
|
||||||
plugins: [plugin],
|
plugins: [plugin],
|
||||||
external: ["node-gyp"],
|
external: ["node-gyp"],
|
||||||
format: "esm",
|
|
||||||
minify: true,
|
|
||||||
splitting: true,
|
|
||||||
compile: {
|
compile: {
|
||||||
autoloadBunfig: false,
|
autoloadBunfig: false,
|
||||||
autoloadDotenv: false,
|
autoloadDotenv: false,
|
||||||
@@ -214,19 +210,12 @@ for (const item of targets) {
|
|||||||
files: {
|
files: {
|
||||||
...(embeddedFileMap ? { "opencode-web-ui.gen.ts": embeddedFileMap } : {}),
|
...(embeddedFileMap ? { "opencode-web-ui.gen.ts": embeddedFileMap } : {}),
|
||||||
},
|
},
|
||||||
entrypoints: [
|
entrypoints: ["./src/index.ts", parserWorker, workerPath, ...(embeddedFileMap ? ["opencode-web-ui.gen.ts"] : [])],
|
||||||
"./src/index.ts",
|
|
||||||
parserWorker,
|
|
||||||
workerPath,
|
|
||||||
rgPath,
|
|
||||||
...(embeddedFileMap ? ["opencode-web-ui.gen.ts"] : []),
|
|
||||||
],
|
|
||||||
define: {
|
define: {
|
||||||
OPENCODE_VERSION: `'${Script.version}'`,
|
OPENCODE_VERSION: `'${Script.version}'`,
|
||||||
OPENCODE_MIGRATIONS: JSON.stringify(migrations),
|
OPENCODE_MIGRATIONS: JSON.stringify(migrations),
|
||||||
OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath,
|
OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath,
|
||||||
OPENCODE_WORKER_PATH: workerPath,
|
OPENCODE_WORKER_PATH: workerPath,
|
||||||
OPENCODE_RIPGREP_WORKER_PATH: rgPath,
|
|
||||||
OPENCODE_CHANNEL: `'${Script.channel}'`,
|
OPENCODE_CHANNEL: `'${Script.channel}'`,
|
||||||
OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "",
|
OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
|
|
||||||
const dir = process.env.OPENCODE_E2E_PROJECT_DIR ?? process.cwd()
|
const dir = process.env.OPENCODE_E2E_PROJECT_DIR ?? process.cwd()
|
||||||
const title = process.env.OPENCODE_E2E_SESSION_TITLE ?? "E2E Session"
|
const title = process.env.OPENCODE_E2E_SESSION_TITLE ?? "E2E Session"
|
||||||
const text = process.env.OPENCODE_E2E_MESSAGE ?? "Seeded for UI e2e"
|
const text = process.env.OPENCODE_E2E_MESSAGE ?? "Seeded for UI e2e"
|
||||||
@@ -18,58 +16,44 @@ const seed = async () => {
|
|||||||
const { Project } = await import("../src/project/project")
|
const { Project } = await import("../src/project/project")
|
||||||
const { ModelID, ProviderID } = await import("../src/provider/schema")
|
const { ModelID, ProviderID } = await import("../src/provider/schema")
|
||||||
const { ToolRegistry } = await import("../src/tool/registry")
|
const { ToolRegistry } = await import("../src/tool/registry")
|
||||||
const { Effect } = await import("effect")
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: dir,
|
directory: dir,
|
||||||
init: () => AppRuntime.runPromise(InstanceBootstrap),
|
init: InstanceBootstrap,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.waitForDependencies()))
|
await Config.waitForDependencies()
|
||||||
await AppRuntime.runPromise(
|
await ToolRegistry.ids()
|
||||||
Effect.gen(function* () {
|
|
||||||
const registry = yield* ToolRegistry.Service
|
|
||||||
yield* registry.ids()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
await AppRuntime.runPromise(
|
const session = await Session.create({ title })
|
||||||
Effect.gen(function* () {
|
const messageID = MessageID.ascending()
|
||||||
const session = yield* Session.Service
|
const partID = PartID.ascending()
|
||||||
const result = yield* session.create({ title })
|
const message = {
|
||||||
const messageID = MessageID.ascending()
|
id: messageID,
|
||||||
const partID = PartID.ascending()
|
sessionID: session.id,
|
||||||
const message = {
|
role: "user" as const,
|
||||||
id: messageID,
|
time: { created: now },
|
||||||
sessionID: result.id,
|
agent: "build",
|
||||||
role: "user" as const,
|
model: {
|
||||||
time: { created: now },
|
providerID: ProviderID.make(providerID),
|
||||||
agent: "build",
|
modelID: ModelID.make(modelID),
|
||||||
model: {
|
},
|
||||||
providerID: ProviderID.make(providerID),
|
}
|
||||||
modelID: ModelID.make(modelID),
|
const part = {
|
||||||
},
|
id: partID,
|
||||||
}
|
sessionID: session.id,
|
||||||
const part = {
|
messageID,
|
||||||
id: partID,
|
type: "text" as const,
|
||||||
sessionID: result.id,
|
text,
|
||||||
messageID,
|
time: { start: now },
|
||||||
type: "text" as const,
|
}
|
||||||
text,
|
await Session.updateMessage(message)
|
||||||
time: { start: now },
|
await Session.updatePart(part)
|
||||||
}
|
await Project.update({ projectID: Instance.project.id, name: "E2E Project" })
|
||||||
yield* session.updateMessage(message)
|
|
||||||
yield* session.updatePart(part)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
await AppRuntime.runPromise(
|
|
||||||
Project.Service.use((svc) => svc.update({ projectID: Instance.project.id, name: "E2E Project" })),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
await Instance.disposeAll().catch(() => {})
|
await Instance.disposeAll().catch(() => {})
|
||||||
await AppRuntime.dispose().catch(() => {})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+100
-20
@@ -178,9 +178,7 @@ That is fine for leaf files like `schema.ts`. Keep the service surface in the ow
|
|||||||
|
|
||||||
## Migration checklist
|
## Migration checklist
|
||||||
|
|
||||||
Service-shape migrated (single namespace, traced methods, `InstanceState` where needed).
|
Fully migrated (single namespace, InstanceState where needed, flattened facade):
|
||||||
|
|
||||||
This checklist is only about the service shape migration. Many of these services still keep `makeRuntime(...)` plus async facade exports; that facade-removal phase is tracked separately in `facades.md`.
|
|
||||||
|
|
||||||
- [x] `Account` — `account/index.ts`
|
- [x] `Account` — `account/index.ts`
|
||||||
- [x] `Agent` — `agent/agent.ts`
|
- [x] `Agent` — `agent/agent.ts`
|
||||||
@@ -223,16 +221,59 @@ This checklist is only about the service shape migration. Many of these services
|
|||||||
- [x] `Provider` — `provider/provider.ts`
|
- [x] `Provider` — `provider/provider.ts`
|
||||||
- [x] `Storage` — `storage/storage.ts`
|
- [x] `Storage` — `storage/storage.ts`
|
||||||
- [x] `ShareNext` — `share/share-next.ts`
|
- [x] `ShareNext` — `share/share-next.ts`
|
||||||
|
|
||||||
|
Still open:
|
||||||
|
|
||||||
- [x] `SessionTodo` — `session/todo.ts`
|
- [x] `SessionTodo` — `session/todo.ts`
|
||||||
|
- [ ] `SyncEvent` — `sync/index.ts`
|
||||||
|
- [ ] `Workspace` — `control-plane/workspace.ts`
|
||||||
|
|
||||||
Still open at the service-shape level:
|
## Tool interface → Effect
|
||||||
|
|
||||||
- [ ] `SyncEvent` — `sync/index.ts` (deferred pending sync with James)
|
`Tool.Def.execute` and `Tool.Info.init` already return `Effect` on this branch. Tool definitions should now stay Effect-native all the way through initialization instead of using Promise-returning init callbacks. Tools can still use lazy init callbacks when they need instance-bound state at init time, but those callbacks should return `Effect`, not `Promise`. Remaining work is:
|
||||||
- [ ] `Workspace` — `control-plane/workspace.ts` (deferred pending sync with James)
|
|
||||||
|
|
||||||
## Tool migration
|
1. Migrate each tool body to return Effects
|
||||||
|
2. Keep `Tool.define()` inputs Effect-native
|
||||||
|
3. Update remaining callers to `yield*` tool initialization instead of `await`ing
|
||||||
|
|
||||||
Tool-specific migration guidance and checklist live in `tools.md`.
|
### Tool migration details
|
||||||
|
|
||||||
|
With `Tool.Info.init()` now effectful, use this transitional pattern for migrated tools that still need Promise-based boundaries internally:
|
||||||
|
|
||||||
|
- `Tool.defineEffect(...)` should `yield*` the services the tool depends on and close over them in the returned tool definition.
|
||||||
|
- Keep the bridge at the Promise boundary only inside the tool body when required by external APIs. Do not return Promise-based init callbacks from `Tool.define()`.
|
||||||
|
- If a tool starts requiring new services, wire them into `ToolRegistry.defaultLayer` so production callers resolve the same dependencies as tests.
|
||||||
|
|
||||||
|
Tool tests should use the existing Effect helpers in `packages/opencode/test/lib/effect.ts`:
|
||||||
|
|
||||||
|
- Use `testEffect(...)` / `it.live(...)` instead of creating fake local wrappers around effectful tools.
|
||||||
|
- Yield the real tool export, then initialize it: `const info = yield* ReadTool`, `const tool = yield* info.init()`.
|
||||||
|
- Run tests inside a real instance with `provideTmpdirInstance(...)` or `provideInstance(tmpdirScoped(...))` so instance-scoped services resolve exactly as they do in production.
|
||||||
|
|
||||||
|
This keeps migrated tool tests aligned with the production service graph today, and makes the eventual `Tool.Info` → `Effect` cleanup mostly mechanical later.
|
||||||
|
|
||||||
|
Individual tools, ordered by value:
|
||||||
|
|
||||||
|
- [ ] `apply_patch.ts` — HIGH: multi-step orchestration, error accumulation, Bus events
|
||||||
|
- [ ] `bash.ts` — HIGH: shell orchestration, quoting, timeout handling, output capture
|
||||||
|
- [x] `read.ts` — HIGH: streaming I/O, readline, binary detection → FileSystem + Stream
|
||||||
|
- [ ] `edit.ts` — HIGH: multi-step diff/format/publish pipeline, FileWatcher lock
|
||||||
|
- [ ] `grep.ts` — MEDIUM: spawns ripgrep → ChildProcessSpawner, timeout handling
|
||||||
|
- [ ] `write.ts` — MEDIUM: permission checks, diagnostics polling, Bus events
|
||||||
|
- [ ] `codesearch.ts` — MEDIUM: HTTP + SSE + manual timeout → HttpClient + Effect.timeout
|
||||||
|
- [ ] `webfetch.ts` — MEDIUM: fetch with UA retry, size limits → HttpClient
|
||||||
|
- [ ] `websearch.ts` — MEDIUM: MCP over HTTP → HttpClient
|
||||||
|
- [ ] `batch.ts` — MEDIUM: parallel execution, per-call error recovery → Effect.all
|
||||||
|
- [ ] `task.ts` — MEDIUM: task state management
|
||||||
|
- [ ] `ls.ts` — MEDIUM: bounded directory listing over ripgrep-backed traversal
|
||||||
|
- [ ] `multiedit.ts` — MEDIUM: sequential edit orchestration over `edit.ts`
|
||||||
|
- [ ] `glob.ts` — LOW: simple async generator
|
||||||
|
- [ ] `lsp.ts` — LOW: dispatch switch over LSP operations
|
||||||
|
- [ ] `question.ts` — LOW: prompt wrapper
|
||||||
|
- [ ] `skill.ts` — LOW: skill tool adapter
|
||||||
|
- [ ] `todo.ts` — LOW: todo persistence wrapper
|
||||||
|
- [ ] `invalid.ts` — LOW: invalid-tool fallback
|
||||||
|
- [ ] `plan.ts` — LOW: plan file operations
|
||||||
|
|
||||||
## Effect service adoption in already-migrated code
|
## Effect service adoption in already-migrated code
|
||||||
|
|
||||||
@@ -240,19 +281,27 @@ Some already-effectified areas still use raw `Filesystem.*` or `Process.spawn` i
|
|||||||
|
|
||||||
### `Filesystem.*` → `AppFileSystem.Service` (yield in layer)
|
### `Filesystem.*` → `AppFileSystem.Service` (yield in layer)
|
||||||
|
|
||||||
- [x] `config/config.ts` — `installDependencies()` now uses `AppFileSystem`
|
- [ ] `file/index.ts` — 1 remaining `Filesystem.readText()` call in untracked diff handling
|
||||||
- [x] `provider/provider.ts` — recent model state now reads via `AppFileSystem.Service`
|
- [ ] `config/config.ts` — 5 remaining `Filesystem.*` calls in `installDependencies()`
|
||||||
|
- [ ] `provider/provider.ts` — 1 remaining `Filesystem.readJson()` call for recent model state
|
||||||
|
|
||||||
### `Process.spawn` → `ChildProcessSpawner` (yield in layer)
|
### `Process.spawn` → `ChildProcessSpawner` (yield in layer)
|
||||||
|
|
||||||
- [x] `format/formatter.ts` — direct `Process.spawn()` checks removed (`air`, `uv`)
|
- [ ] `format/formatter.ts` — 2 remaining `Process.spawn()` checks (`air`, `uv`)
|
||||||
- [ ] `lsp/server.ts` — multiple `Process.spawn()` installs/download helpers
|
- [ ] `lsp/server.ts` — multiple `Process.spawn()` installs/download helpers
|
||||||
|
|
||||||
## Filesystem consolidation
|
## Filesystem consolidation
|
||||||
|
|
||||||
`util/filesystem.ts` is still used widely across `src/`, and raw `fs` / `fs/promises` imports still exist in multiple tooling and infrastructure files. As services and tools are effectified, they should switch from `Filesystem.*` to yielding `AppFileSystem.Service` where possible — this should happen naturally during each migration, not as a separate sweep.
|
`util/filesystem.ts` (raw fs wrapper) is currently imported by **34 files**. The effectified `AppFileSystem` service (`filesystem/index.ts`) is currently imported by **15 files**. As services and tools are effectified, they should switch from `Filesystem.*` to yielding `AppFileSystem.Service` — this happens naturally during each migration, not as a separate effort.
|
||||||
|
|
||||||
Tool-specific filesystem cleanup notes live in `tools.md`.
|
Similarly, **21 files** still import raw `fs` or `fs/promises` directly. These should migrate to `AppFileSystem` or `Filesystem.*` as they're touched.
|
||||||
|
|
||||||
|
Current raw fs users that will convert during tool migration:
|
||||||
|
|
||||||
|
- `tool/read.ts` — fs.createReadStream, readline
|
||||||
|
- `tool/apply_patch.ts` — fs/promises
|
||||||
|
- `file/ripgrep.ts` — fs/promises
|
||||||
|
- `patch/index.ts` — fs, fs/promises
|
||||||
|
|
||||||
## Primitives & utilities
|
## Primitives & utilities
|
||||||
|
|
||||||
@@ -263,9 +312,7 @@ Tool-specific filesystem cleanup notes live in `tools.md`.
|
|||||||
|
|
||||||
## Destroying the facades
|
## Destroying the facades
|
||||||
|
|
||||||
This phase is still broadly open. As of 2026-04-13 there are still 15 `makeRuntime(...)` call sites under `src/`, with 13 still in scope for facade removal. The live checklist now lives in `facades.md`.
|
Every service currently exports async facade functions at the bottom of its namespace — `export async function read(...) { return runPromise(...) }` — backed by a per-service `makeRuntime`. These exist because cyclic imports used to force each service to build its own independent runtime. Now that the layer DAG is acyclic and `AppRuntime` (`src/effect/app-runtime.ts`) composes everything into one `ManagedRuntime`, we're removing them.
|
||||||
|
|
||||||
These facades exist because cyclic imports used to force each service to build its own independent runtime. Now that the layer DAG is acyclic and `AppRuntime` (`src/effect/app-runtime.ts`) composes everything into one `ManagedRuntime`, we're removing them.
|
|
||||||
|
|
||||||
### Process
|
### Process
|
||||||
|
|
||||||
@@ -294,14 +341,47 @@ For each service, the migration is roughly:
|
|||||||
- `ShareNext` — migrated 2026-04-11. Swapped remaining async callers to `AppRuntime.runPromise(ShareNext.Service.use(...))`, removed the `makeRuntime(...)` facade, and kept instance bootstrap on the shared app runtime.
|
- `ShareNext` — migrated 2026-04-11. Swapped remaining async callers to `AppRuntime.runPromise(ShareNext.Service.use(...))`, removed the `makeRuntime(...)` facade, and kept instance bootstrap on the shared app runtime.
|
||||||
- `SessionTodo` — migrated 2026-04-10. Already matched the target service shape in `session/todo.ts`: single namespace, traced Effect methods, and no `makeRuntime(...)` facade remained; checklist updated to reflect the completed migration.
|
- `SessionTodo` — migrated 2026-04-10. Already matched the target service shape in `session/todo.ts`: single namespace, traced Effect methods, and no `makeRuntime(...)` facade remained; checklist updated to reflect the completed migration.
|
||||||
- `Storage` — migrated 2026-04-10. One production caller (`Session.diff`) and all storage.test.ts tests converted to effectful style. Facades and `makeRuntime` removed.
|
- `Storage` — migrated 2026-04-10. One production caller (`Session.diff`) and all storage.test.ts tests converted to effectful style. Facades and `makeRuntime` removed.
|
||||||
- `SessionRunState` — migrated 2026-04-11. Single caller in `server/instance/session.ts` converted; facade removed.
|
- `SessionRunState` — migrated 2026-04-11. Single caller in `server/routes/session.ts` converted; facade removed.
|
||||||
- `Account` — migrated 2026-04-11. Callers in `server/instance/experimental.ts` and `cli/cmd/account.ts` converted; facade removed.
|
- `Account` — migrated 2026-04-11. Callers in `server/routes/experimental.ts` and `cli/cmd/account.ts` converted; facade removed.
|
||||||
- `Instruction` — migrated 2026-04-11. Test-only callers converted; facade removed.
|
- `Instruction` — migrated 2026-04-11. Test-only callers converted; facade removed.
|
||||||
- `FileTime` — migrated 2026-04-11. Test-only callers converted; facade removed.
|
- `FileTime` — migrated 2026-04-11. Test-only callers converted; facade removed.
|
||||||
- `FileWatcher` — migrated 2026-04-11. Callers in `project/bootstrap.ts` and test converted; facade removed.
|
- `FileWatcher` — migrated 2026-04-11. Callers in `project/bootstrap.ts` and test converted; facade removed.
|
||||||
- `Question` — migrated 2026-04-11. Callers in `server/instance/question.ts` and test converted; facade removed.
|
- `Question` — migrated 2026-04-11. Callers in `server/routes/question.ts` and test converted; facade removed.
|
||||||
- `Truncate` — migrated 2026-04-11. Caller in `tool/tool.ts` and test converted; facade removed.
|
- `Truncate` — migrated 2026-04-11. Caller in `tool/tool.ts` and test converted; facade removed.
|
||||||
|
|
||||||
## Route handler effectification
|
## Route handler effectification
|
||||||
|
|
||||||
Route-handler migration guidance and checklist live in `routes.md`.
|
Route handlers should wrap their entire body in a single `AppRuntime.runPromise(Effect.gen(...))` call, yielding services from context rather than calling facades one-by-one. This eliminates multiple `runPromise` round-trips and lets handlers compose naturally.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Before — one facade call per service
|
||||||
|
;async (c) => {
|
||||||
|
await SessionRunState.assertNotBusy(id)
|
||||||
|
await Session.removeMessage({ sessionID: id, messageID })
|
||||||
|
return c.json(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// After — one Effect.gen, yield services from context
|
||||||
|
;async (c) => {
|
||||||
|
await AppRuntime.runPromise(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const state = yield* SessionRunState.Service
|
||||||
|
const session = yield* Session.Service
|
||||||
|
yield* state.assertNotBusy(id)
|
||||||
|
yield* session.removeMessage({ sessionID: id, messageID })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
return c.json(true)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When migrating, always use `{ concurrency: "unbounded" }` with `Effect.all` — route handlers should run independent service calls in parallel, not sequentially.
|
||||||
|
|
||||||
|
Route files to convert (each handler that calls facades should be wrapped):
|
||||||
|
|
||||||
|
- [ ] `server/routes/session.ts` — heaviest; uses Session, SessionPrompt, SessionRevert, SessionCompaction, SessionShare, SessionSummary, SessionRunState, Agent, Permission, Bus
|
||||||
|
- [ ] `server/routes/global.ts` — uses Config, Project, Provider, Vcs, Snapshot, Agent
|
||||||
|
- [ ] `server/routes/provider.ts` — uses Provider, Auth, Config
|
||||||
|
- [ ] `server/routes/question.ts` — uses Question
|
||||||
|
- [ ] `server/routes/pty.ts` — uses Pty
|
||||||
|
- [ ] `server/routes/experimental.ts` — uses Account, ToolRegistry, Agent, MCP, Config
|
||||||
@@ -1,238 +0,0 @@
|
|||||||
# Facade removal checklist
|
|
||||||
|
|
||||||
Concrete inventory of the remaining `makeRuntime(...)`-backed service facades in `packages/opencode`.
|
|
||||||
|
|
||||||
As of 2026-04-13, latest `origin/dev`:
|
|
||||||
|
|
||||||
- `src/` still has 15 `makeRuntime(...)` call sites.
|
|
||||||
- 13 of those are still in scope for facade removal.
|
|
||||||
- 2 are excluded from this checklist: `bus/index.ts` and `effect/cross-spawn-spawner.ts`.
|
|
||||||
|
|
||||||
Recent progress:
|
|
||||||
|
|
||||||
- Wave 1 is merged: `Pty`, `Skill`, `Vcs`, `ToolRegistry`, `Auth`.
|
|
||||||
- Wave 2 is merged: `Config`, `Provider`, `File`, `LSP`, `MCP`.
|
|
||||||
|
|
||||||
## Priority hotspots
|
|
||||||
|
|
||||||
- `server/instance/session.ts` still depends on `Session`, `SessionPrompt`, `SessionRevert`, `SessionCompaction`, `SessionSummary`, `ShareSession`, `Agent`, and `Permission` facades.
|
|
||||||
- `src/effect/app-runtime.ts` still references many facade namespaces directly, so it should stay in view during each deletion.
|
|
||||||
|
|
||||||
## Completed Batches
|
|
||||||
|
|
||||||
Low-risk batch, all merged:
|
|
||||||
|
|
||||||
1. `src/pty/index.ts`
|
|
||||||
2. `src/skill/index.ts`
|
|
||||||
3. `src/project/vcs.ts`
|
|
||||||
4. `src/tool/registry.ts`
|
|
||||||
5. `src/auth/index.ts`
|
|
||||||
|
|
||||||
Caller-heavy batch, all merged:
|
|
||||||
|
|
||||||
1. `src/config/config.ts`
|
|
||||||
2. `src/provider/provider.ts`
|
|
||||||
3. `src/file/index.ts`
|
|
||||||
4. `src/lsp/index.ts`
|
|
||||||
5. `src/mcp/index.ts`
|
|
||||||
|
|
||||||
Shared pattern:
|
|
||||||
|
|
||||||
- one service file still exports `makeRuntime(...)` + async facades
|
|
||||||
- one or two route or CLI entrypoints call those facades directly
|
|
||||||
- tests call the facade directly and need to switch to `yield* svc.method(...)`
|
|
||||||
- once callers are gone, delete `makeRuntime(...)`, remove async facade exports, and drop the `makeRuntime` import
|
|
||||||
|
|
||||||
## Done means
|
|
||||||
|
|
||||||
For each service in the low-risk batch, the work is complete only when all of these are true:
|
|
||||||
|
|
||||||
1. all production callers stop using `Namespace.method(...)` facade calls
|
|
||||||
2. all direct test callers stop using the facade and instead yield the service from context
|
|
||||||
3. the service file no longer has `makeRuntime(...)`
|
|
||||||
4. the service file no longer exports runtime-backed facade helpers
|
|
||||||
5. `grep` for the migrated facade methods only finds the service implementation itself or unrelated names
|
|
||||||
|
|
||||||
## Caller templates
|
|
||||||
|
|
||||||
### Route handlers
|
|
||||||
|
|
||||||
Use one `AppRuntime.runPromise(Effect.gen(...))` body and yield the service inside it.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
const value = await AppRuntime.runPromise(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const pty = yield* Pty.Service
|
|
||||||
return yield* pty.list()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
If two service calls are independent, keep them in the same effect body and use `Effect.all(...)`.
|
|
||||||
|
|
||||||
### Plain async CLI or script entrypoints
|
|
||||||
|
|
||||||
If the caller is not itself an Effect service yet, still prefer one contiguous `AppRuntime.runPromise(Effect.gen(...))` block for the whole unit of work.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
const skills = await AppRuntime.runPromise(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const auth = yield* Auth.Service
|
|
||||||
const skill = yield* Skill.Service
|
|
||||||
yield* auth.set(key, info)
|
|
||||||
return yield* skill.all()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Only fall back to `AppRuntime.runPromise(Service.use(...))` for truly isolated one-off calls or awkward callback boundaries. Do not stack multiple tiny `runPromise(...)` calls in the same contiguous workflow.
|
|
||||||
|
|
||||||
This is the right intermediate state. Do not block facade removal on effectifying the whole CLI file.
|
|
||||||
|
|
||||||
### Bootstrap or fire-and-forget startup code
|
|
||||||
|
|
||||||
If the old facade call existed only to kick off initialization, call the service through the existing runtime for that file.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
void BootstrapRuntime.runPromise(Vcs.Service.use((svc) => svc.init()))
|
|
||||||
```
|
|
||||||
|
|
||||||
Do not reintroduce a dedicated runtime in the service just for bootstrap.
|
|
||||||
|
|
||||||
### Tests
|
|
||||||
|
|
||||||
Convert facade tests to full effect style.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
it.effect("does the thing", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const svc = yield* Pty.Service
|
|
||||||
const info = yield* svc.create({ command: "cat", title: "a" })
|
|
||||||
yield* svc.remove(info.id)
|
|
||||||
}).pipe(Effect.provide(Pty.defaultLayer)),
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
If the repo test already uses `testEffect(...)`, prefer `testEffect(Service.defaultLayer)` and `yield* Service.Service` inside the test body.
|
|
||||||
|
|
||||||
Do not route tests through `AppRuntime` unless the test is explicitly exercising the app runtime. For facade removal, tests should usually provide the specific service layer they need.
|
|
||||||
|
|
||||||
If the test uses `provideTmpdirInstance(...)`, remember that fixture needs a live `ChildProcessSpawner` layer. For services whose `defaultLayer` does not already provide that infra, prefer the repo-standard cross-spawn layer:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
const infra = CrossSpawnSpawner.defaultLayer
|
|
||||||
|
|
||||||
const it = testEffect(Layer.mergeAll(MyService.defaultLayer, infra))
|
|
||||||
```
|
|
||||||
|
|
||||||
Without that extra layer, tests fail at runtime with `Service not found: effect/process/ChildProcessSpawner`.
|
|
||||||
|
|
||||||
## Questions already answered
|
|
||||||
|
|
||||||
### Do we need to effectify the whole caller first?
|
|
||||||
|
|
||||||
No.
|
|
||||||
|
|
||||||
- route files: compose the handler with `AppRuntime.runPromise(Effect.gen(...))`
|
|
||||||
- CLI and scripts: use `AppRuntime.runPromise(Service.use(...))`
|
|
||||||
- bootstrap: use the existing bootstrap runtime
|
|
||||||
|
|
||||||
Facade removal does not require a bigger refactor than that.
|
|
||||||
|
|
||||||
### Should tests keep calling the namespace from async test bodies?
|
|
||||||
|
|
||||||
No. Convert them now.
|
|
||||||
|
|
||||||
The end state is `yield* svc.method(...)`, not `await Namespace.method(...)` inside `async` tests.
|
|
||||||
|
|
||||||
### Should we keep `runPromise` exported for convenience?
|
|
||||||
|
|
||||||
No. For this batch the goal is to delete the service-local runtime entirely.
|
|
||||||
|
|
||||||
### What if a route has websocket callbacks or nested async handlers?
|
|
||||||
|
|
||||||
Keep the route shape, but replace each facade call with `AppRuntime.runPromise(Service.use(...))` or wrap the surrounding async section in one `Effect.gen(...)` when practical. Do not keep the service facade just because the route has callback-shaped code.
|
|
||||||
|
|
||||||
### Should we use one `runPromise` per service call?
|
|
||||||
|
|
||||||
No.
|
|
||||||
|
|
||||||
Default to one contiguous `AppRuntime.runPromise(Effect.gen(...))` block per handler, command, or workflow. Yield every service you need inside that block.
|
|
||||||
|
|
||||||
Multiple tiny `runPromise(...)` calls are only acceptable when the caller structure forces it, such as websocket lifecycle callbacks, external callback APIs, or genuinely unrelated one-off operations.
|
|
||||||
|
|
||||||
### Should we wrap a single service expression in `Effect.gen(...)`?
|
|
||||||
|
|
||||||
Usually no.
|
|
||||||
|
|
||||||
Prefer the direct form when there is only one expression:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
await AppRuntime.runPromise(File.Service.use((svc) => svc.read(path)))
|
|
||||||
```
|
|
||||||
|
|
||||||
Use `Effect.gen(...)` when the workflow actually needs multiple yielded values or branching.
|
|
||||||
|
|
||||||
## Learnings
|
|
||||||
|
|
||||||
These were the recurring mistakes and useful corrections from the first two batches:
|
|
||||||
|
|
||||||
1. Tests should usually provide the specific service layer, not `AppRuntime`.
|
|
||||||
2. If a test uses `provideTmpdirInstance(...)` and needs child processes, prefer `CrossSpawnSpawner.defaultLayer`.
|
|
||||||
3. Instance-scoped services may need both the service layer and the right instance fixture. `File` tests, for example, needed `provideInstance(...)` plus `File.defaultLayer`.
|
|
||||||
4. Do not wrap a single `Service.use(...)` call in `Effect.gen(...)` just to return it. Use the direct form.
|
|
||||||
5. For CLI readability, extract file-local preload helpers when the handler starts doing config load + service load + batched effect fanout inline.
|
|
||||||
6. When rebasing a facade branch after nearby merges, prefer the already-cleaned service/test version over older inline facade-era code.
|
|
||||||
|
|
||||||
## Next batch
|
|
||||||
|
|
||||||
Recommended next five, in order:
|
|
||||||
|
|
||||||
1. `src/permission/index.ts`
|
|
||||||
2. `src/agent/agent.ts`
|
|
||||||
3. `src/session/summary.ts`
|
|
||||||
4. `src/session/revert.ts`
|
|
||||||
5. `src/mcp/auth.ts`
|
|
||||||
|
|
||||||
Why this batch:
|
|
||||||
|
|
||||||
- It keeps pushing the session-adjacent cleanup without jumping straight into `session/index.ts` or `session/prompt.ts`.
|
|
||||||
- `Permission`, `Agent`, `SessionSummary`, and `SessionRevert` all reduce fanout in `server/instance/session.ts`.
|
|
||||||
- `McpAuth` is small and closely related to the just-landed `MCP` cleanup.
|
|
||||||
|
|
||||||
After that batch, the expected follow-up is the main session cluster:
|
|
||||||
|
|
||||||
1. `src/session/index.ts`
|
|
||||||
2. `src/session/prompt.ts`
|
|
||||||
3. `src/session/compaction.ts`
|
|
||||||
|
|
||||||
## Checklist
|
|
||||||
|
|
||||||
- [ ] `src/session/index.ts` (`Session`) - facades: `create`, `fork`, `get`, `setTitle`, `setArchived`, `setPermission`, `setRevert`, `messages`, `children`, `remove`, `updateMessage`, `removeMessage`, `removePart`, `updatePart`; main callers: `server/instance/session.ts`, `cli/cmd/session.ts`, `cli/cmd/export.ts`, `cli/cmd/github.ts`; tests: `test/server/session-actions.test.ts`, `test/server/session-list.test.ts`, `test/server/global-session-list.test.ts`
|
|
||||||
- [ ] `src/session/prompt.ts` (`SessionPrompt`) - facades: `prompt`, `resolvePromptParts`, `cancel`, `loop`, `shell`, `command`; main callers: `server/instance/session.ts`, `cli/cmd/github.ts`; tests: `test/session/prompt.test.ts`, `test/session/prompt-effect.test.ts`, `test/session/structured-output-integration.test.ts`
|
|
||||||
- [ ] `src/session/revert.ts` (`SessionRevert`) - facades: `revert`, `unrevert`, `cleanup`; main callers: `server/instance/session.ts`; tests: `test/session/revert-compact.test.ts`
|
|
||||||
- [ ] `src/session/compaction.ts` (`SessionCompaction`) - facades: `isOverflow`, `prune`, `create`; main callers: `server/instance/session.ts`; tests: `test/session/compaction.test.ts`
|
|
||||||
- [ ] `src/session/summary.ts` (`SessionSummary`) - facades: `summarize`, `diff`; main callers: `session/prompt.ts`, `session/processor.ts`, `server/instance/session.ts`; tests: `test/session/snapshot-tool-race.test.ts`
|
|
||||||
- [ ] `src/share/session.ts` (`ShareSession`) - facades: `create`, `share`, `unshare`; main callers: `server/instance/session.ts`, `cli/cmd/github.ts`
|
|
||||||
- [ ] `src/agent/agent.ts` (`Agent`) - facades: `get`, `list`, `defaultAgent`, `generate`; main callers: `cli/cmd/agent.ts`, `server/instance/session.ts`, `server/instance/experimental.ts`; tests: `test/agent/agent.test.ts`
|
|
||||||
- [ ] `src/permission/index.ts` (`Permission`) - facades: `ask`, `reply`, `list`; main callers: `server/instance/permission.ts`, `server/instance/session.ts`, `session/llm.ts`; tests: `test/permission/next.test.ts`
|
|
||||||
- [x] `src/file/index.ts` (`File`) - facades removed and merged.
|
|
||||||
- [x] `src/lsp/index.ts` (`LSP`) - facades removed and merged.
|
|
||||||
- [x] `src/mcp/index.ts` (`MCP`) - facades removed and merged.
|
|
||||||
- [x] `src/config/config.ts` (`Config`) - facades removed and merged.
|
|
||||||
- [x] `src/provider/provider.ts` (`Provider`) - facades removed and merged.
|
|
||||||
- [x] `src/pty/index.ts` (`Pty`) - facades removed and merged.
|
|
||||||
- [x] `src/skill/index.ts` (`Skill`) - facades removed and merged.
|
|
||||||
- [x] `src/project/vcs.ts` (`Vcs`) - facades removed and merged.
|
|
||||||
- [x] `src/tool/registry.ts` (`ToolRegistry`) - facades removed and merged.
|
|
||||||
- [ ] `src/worktree/index.ts` (`Worktree`) - facades: `makeWorktreeInfo`, `createFromInfo`, `create`, `remove`, `reset`; main callers: `control-plane/adaptors/worktree.ts`, `server/instance/experimental.ts`; tests: `test/project/worktree.test.ts`, `test/project/worktree-remove.test.ts`
|
|
||||||
- [x] `src/auth/index.ts` (`Auth`) - facades removed and merged.
|
|
||||||
- [ ] `src/mcp/auth.ts` (`McpAuth`) - facades: `get`, `getForUrl`, `all`, `set`, `remove`, `updateTokens`, `updateClientInfo`, `updateCodeVerifier`, `updateOAuthState`; main callers: `mcp/oauth-provider.ts`, `cli/cmd/mcp.ts`; tests: `test/mcp/oauth-auto-connect.test.ts`
|
|
||||||
- [ ] `src/plugin/index.ts` (`Plugin`) - facades: `trigger`, `list`, `init`; main callers: `agent/agent.ts`, `session/llm.ts`, `project/bootstrap.ts`; tests: `test/plugin/trigger.test.ts`, `test/provider/provider.test.ts`
|
|
||||||
- [ ] `src/project/project.ts` (`Project`) - facades: `fromDirectory`, `discover`, `initGit`, `update`, `sandboxes`, `addSandbox`, `removeSandbox`; main callers: `project/instance.ts`, `server/instance/project.ts`, `server/instance/experimental.ts`; tests: `test/project/project.test.ts`, `test/project/migrate-global.test.ts`
|
|
||||||
- [ ] `src/snapshot/index.ts` (`Snapshot`) - facades: `init`, `track`, `patch`, `restore`, `revert`, `diff`, `diffFull`; main callers: `project/bootstrap.ts`, `cli/cmd/debug/snapshot.ts`; tests: `test/snapshot/snapshot.test.ts`, `test/session/revert-compact.test.ts`
|
|
||||||
|
|
||||||
## Excluded `makeRuntime(...)` sites
|
|
||||||
|
|
||||||
- `src/bus/index.ts` - core bus plumbing, not a normal facade-removal target.
|
|
||||||
- `src/effect/cross-spawn-spawner.ts` - runtime helper for `ChildProcessSpawner`, not a service namespace facade.
|
|
||||||
@@ -1,394 +0,0 @@
|
|||||||
# HttpApi migration
|
|
||||||
|
|
||||||
Practical notes for an eventual migration of `packages/opencode` server routes from the current Hono handlers to Effect `HttpApi`, either as a full replacement or as a parallel surface.
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Use Effect `HttpApi` where it gives us a better typed contract for:
|
|
||||||
|
|
||||||
- route definition
|
|
||||||
- request decoding and validation
|
|
||||||
- typed success and error responses
|
|
||||||
- OpenAPI generation
|
|
||||||
- handler composition inside Effect
|
|
||||||
|
|
||||||
This should be treated as a later-stage HTTP boundary migration, not a prerequisite for ongoing service, route-handler, or schema work.
|
|
||||||
|
|
||||||
## Core model
|
|
||||||
|
|
||||||
`HttpApi` is definition-first.
|
|
||||||
|
|
||||||
- `HttpApi` is the root API
|
|
||||||
- `HttpApiGroup` groups related endpoints
|
|
||||||
- `HttpApiEndpoint` defines a single route and its request / response schemas
|
|
||||||
- handlers are implemented separately from the contract
|
|
||||||
|
|
||||||
This is a better fit once route inputs and outputs are already moving toward Effect Schema-first models.
|
|
||||||
|
|
||||||
## Why it is relevant here
|
|
||||||
|
|
||||||
The current route-effectification work is already pushing handlers toward:
|
|
||||||
|
|
||||||
- one `AppRuntime.runPromise(Effect.gen(...))` body
|
|
||||||
- yielding services from context
|
|
||||||
- using typed Effect errors instead of Promise wrappers
|
|
||||||
|
|
||||||
That work is a good prerequisite for `HttpApi`. Once the handler body is already a composed Effect, the remaining migration is mostly about replacing the Hono route declaration and validator layer.
|
|
||||||
|
|
||||||
## What HttpApi gives us
|
|
||||||
|
|
||||||
### Contracts
|
|
||||||
|
|
||||||
Request params, query, payload, success payloads, and typed error payloads are declared in one place using Effect Schema.
|
|
||||||
|
|
||||||
### Validation and decoding
|
|
||||||
|
|
||||||
Incoming data is decoded through Effect Schema instead of hand-maintained Zod validators per route.
|
|
||||||
|
|
||||||
### OpenAPI
|
|
||||||
|
|
||||||
`HttpApi` can derive OpenAPI from the API definition, which overlaps with the current `describeRoute(...)` and `resolver(...)` pattern.
|
|
||||||
|
|
||||||
### Typed errors
|
|
||||||
|
|
||||||
`Schema.TaggedErrorClass` maps naturally to endpoint error contracts.
|
|
||||||
|
|
||||||
## Likely fit for opencode
|
|
||||||
|
|
||||||
Best fit first:
|
|
||||||
|
|
||||||
- JSON request / response endpoints
|
|
||||||
- route groups that already mostly delegate into services
|
|
||||||
- endpoints whose request and response models can be defined with Effect Schema
|
|
||||||
|
|
||||||
Harder / later fit:
|
|
||||||
|
|
||||||
- SSE endpoints
|
|
||||||
- websocket endpoints
|
|
||||||
- streaming handlers
|
|
||||||
- routes with heavy Hono-specific middleware assumptions
|
|
||||||
|
|
||||||
## Current blockers and gaps
|
|
||||||
|
|
||||||
### Schema split
|
|
||||||
|
|
||||||
Many route boundaries still use Zod-first validators. That does not block all experimentation, but full `HttpApi` adoption is easier after the domain and boundary types are more consistently Schema-first with `.zod` compatibility only where needed.
|
|
||||||
|
|
||||||
### Mixed handler styles
|
|
||||||
|
|
||||||
Many current `server/instance/*.ts` handlers still call async facades directly. Migrating those to composed `Effect.gen(...)` handlers is the low-risk step to do first.
|
|
||||||
|
|
||||||
### Non-JSON routes
|
|
||||||
|
|
||||||
The server currently includes SSE, websocket, and streaming-style endpoints. Those should not be the first `HttpApi` targets.
|
|
||||||
|
|
||||||
### Existing Hono integration
|
|
||||||
|
|
||||||
The current server composition, middleware, and docs flow are Hono-centered today. That suggests a parallel or incremental adoption plan is safer than a flag day rewrite.
|
|
||||||
|
|
||||||
## Recommended strategy
|
|
||||||
|
|
||||||
### 1. Finish the prerequisites first
|
|
||||||
|
|
||||||
- continue route-handler effectification in `server/instance/*.ts`
|
|
||||||
- continue schema migration toward Effect Schema-first DTOs and errors
|
|
||||||
- keep removing service facades
|
|
||||||
|
|
||||||
### 2. Start with one parallel group
|
|
||||||
|
|
||||||
Introduce one small `HttpApi` group for plain JSON endpoints only. Good initial candidates are the least stateful endpoints in:
|
|
||||||
|
|
||||||
- `server/instance/question.ts`
|
|
||||||
- `server/instance/provider.ts`
|
|
||||||
- `server/instance/permission.ts`
|
|
||||||
|
|
||||||
Avoid `session.ts`, SSE, websocket, and TUI-facing routes first.
|
|
||||||
|
|
||||||
Recommended first slice:
|
|
||||||
|
|
||||||
- start with `question`
|
|
||||||
- start with `GET /question`
|
|
||||||
- start with `POST /question/:requestID/reply`
|
|
||||||
|
|
||||||
Why `question` first:
|
|
||||||
|
|
||||||
- already JSON-only
|
|
||||||
- already delegates into an Effect service
|
|
||||||
- proves list + mutation + params + payload + OpenAPI in one small slice
|
|
||||||
- avoids the harder streaming and middleware cases
|
|
||||||
|
|
||||||
### 3. Reuse existing services
|
|
||||||
|
|
||||||
Do not re-architect business logic during the HTTP migration. `HttpApi` handlers should call the same Effect services already used by the Hono handlers.
|
|
||||||
|
|
||||||
### 4. Run in parallel before replacing
|
|
||||||
|
|
||||||
Prefer mounting an experimental `HttpApi` surface alongside the existing Hono routes first. That lowers migration risk and lets us compare:
|
|
||||||
|
|
||||||
- handler ergonomics
|
|
||||||
- OpenAPI output
|
|
||||||
- auth and middleware integration
|
|
||||||
- test ergonomics
|
|
||||||
|
|
||||||
### 5. Migrate JSON route groups gradually
|
|
||||||
|
|
||||||
If the parallel slice works well, migrate additional JSON route groups one at a time. Leave streaming-style endpoints on Hono until there is a clear reason to move them.
|
|
||||||
|
|
||||||
## Schema rule for HttpApi work
|
|
||||||
|
|
||||||
Every `HttpApi` slice should follow `specs/effect/schema.md` and the Schema -> Zod interop rule in `specs/effect/migration.md`.
|
|
||||||
|
|
||||||
Default rule:
|
|
||||||
|
|
||||||
- Effect Schema owns the type
|
|
||||||
- `.zod` exists only as a compatibility surface
|
|
||||||
- do not introduce a new hand-written Zod schema for a type that is already migrating to Effect Schema
|
|
||||||
|
|
||||||
Practical implication for `HttpApi` migration:
|
|
||||||
|
|
||||||
- if a route boundary already depends on a shared DTO, ID, input, output, or tagged error, migrate that model to Effect Schema first or in the same change
|
|
||||||
- if an existing Hono route or tool still needs Zod, derive it with `@/util/effect-zod`
|
|
||||||
- avoid maintaining parallel Zod and Effect definitions for the same request or response type
|
|
||||||
|
|
||||||
Ordering for a route-group migration:
|
|
||||||
|
|
||||||
1. move implicated shared `schema.ts` leaf types to Effect Schema first
|
|
||||||
2. move exported `Info` / `Input` / `Output` route DTOs to Effect Schema
|
|
||||||
3. move tagged route-facing errors to `Schema.TaggedErrorClass` where needed
|
|
||||||
4. switch existing Zod boundary validators to derived `.zod`
|
|
||||||
5. define the `HttpApi` contract from the canonical Effect schemas
|
|
||||||
|
|
||||||
Temporary exception:
|
|
||||||
|
|
||||||
- it is acceptable to keep a route-local Zod schema for the first spike only when the type is boundary-local and migrating it would create unrelated churn
|
|
||||||
- if that happens, leave a short note so the type does not become a permanent second source of truth
|
|
||||||
|
|
||||||
## First vertical slice
|
|
||||||
|
|
||||||
The first `HttpApi` spike should be intentionally small and repeatable.
|
|
||||||
|
|
||||||
Chosen slice:
|
|
||||||
|
|
||||||
- group: `question`
|
|
||||||
- endpoints: `GET /question` and `POST /question/:requestID/reply`
|
|
||||||
|
|
||||||
Non-goals:
|
|
||||||
|
|
||||||
- no `session` routes
|
|
||||||
- no SSE or websocket routes
|
|
||||||
- no auth redesign
|
|
||||||
- no broad service refactor
|
|
||||||
|
|
||||||
Behavior rule:
|
|
||||||
|
|
||||||
- preserve current runtime behavior first
|
|
||||||
- treat semantic changes such as introducing new `404` behavior as a separate follow-up unless they are required to make the contract honest
|
|
||||||
|
|
||||||
Add `POST /question/:requestID/reject` only after the first two endpoints work cleanly.
|
|
||||||
|
|
||||||
## Repeatable slice template
|
|
||||||
|
|
||||||
Use the same sequence for each route group.
|
|
||||||
|
|
||||||
1. Pick one JSON-only route group that already mostly delegates into services.
|
|
||||||
2. Identify the shared DTOs, IDs, and errors implicated by that slice.
|
|
||||||
3. Apply the schema migration ordering above so those types are Effect Schema-first.
|
|
||||||
4. Define the `HttpApi` contract separately from the handlers.
|
|
||||||
5. Implement handlers by yielding the existing service from context.
|
|
||||||
6. Mount the new surface in parallel under an experimental prefix.
|
|
||||||
7. Add one end-to-end test and one OpenAPI-focused test.
|
|
||||||
8. Compare ergonomics before migrating the next endpoint.
|
|
||||||
|
|
||||||
Rule of thumb:
|
|
||||||
|
|
||||||
- migrate one route group at a time
|
|
||||||
- migrate one or two endpoints first, not the whole file
|
|
||||||
- keep business logic in the existing service
|
|
||||||
- keep the first spike easy to delete if the experiment is not worth continuing
|
|
||||||
|
|
||||||
## Example structure
|
|
||||||
|
|
||||||
Placement rule:
|
|
||||||
|
|
||||||
- keep `HttpApi` code under `src/server`, not `src/effect`
|
|
||||||
- `src/effect` should stay focused on runtimes, layers, instance state, and shared Effect plumbing
|
|
||||||
- place each `HttpApi` slice next to the HTTP boundary it serves
|
|
||||||
- for instance-scoped routes, prefer `src/server/instance/httpapi/*`
|
|
||||||
- if control-plane routes ever migrate, prefer `src/server/control/httpapi/*`
|
|
||||||
|
|
||||||
Suggested file layout for a repeatable spike:
|
|
||||||
|
|
||||||
- `src/server/instance/httpapi/question.ts`
|
|
||||||
- `src/server/instance/httpapi/index.ts`
|
|
||||||
- `test/server/question-httpapi.test.ts`
|
|
||||||
- `test/server/question-httpapi-openapi.test.ts`
|
|
||||||
|
|
||||||
Suggested responsibilities:
|
|
||||||
|
|
||||||
- `question.ts` defines the `HttpApi` contract and `HttpApiBuilder.group(...)` handlers for the experimental slice
|
|
||||||
- `index.ts` combines experimental `HttpApi` groups and exposes the mounted handler or layer
|
|
||||||
- `question-httpapi.test.ts` proves the route works end-to-end against the real service
|
|
||||||
- `question-httpapi-openapi.test.ts` proves the generated OpenAPI is acceptable for the migrated endpoints
|
|
||||||
|
|
||||||
## Example migration shape
|
|
||||||
|
|
||||||
Each route-group spike should follow the same shape.
|
|
||||||
|
|
||||||
### 1. Contract
|
|
||||||
|
|
||||||
- define an experimental `HttpApi`
|
|
||||||
- define one `HttpApiGroup`
|
|
||||||
- define endpoint params, payload, success, and error schemas from canonical Effect schemas
|
|
||||||
- annotate summary, description, and operation ids explicitly so generated docs are stable
|
|
||||||
|
|
||||||
### 2. Handler layer
|
|
||||||
|
|
||||||
- implement with `HttpApiBuilder.group(api, groupName, ...)`
|
|
||||||
- yield the existing Effect service from context
|
|
||||||
- keep handler bodies thin
|
|
||||||
- keep transport mapping at the HTTP boundary only
|
|
||||||
|
|
||||||
### 3. Mounting
|
|
||||||
|
|
||||||
- mount under an experimental prefix such as `/experimental/httpapi`
|
|
||||||
- keep existing Hono routes unchanged
|
|
||||||
- expose separate OpenAPI output for the experimental slice first
|
|
||||||
|
|
||||||
### 4. Verification
|
|
||||||
|
|
||||||
- seed real state through the existing service
|
|
||||||
- call the experimental endpoints
|
|
||||||
- assert that the service behavior is unchanged
|
|
||||||
- assert that the generated OpenAPI contains the migrated paths and schemas
|
|
||||||
|
|
||||||
## Boundary composition
|
|
||||||
|
|
||||||
The first slices should keep the existing outer server composition and only replace the route contract and handler layer.
|
|
||||||
|
|
||||||
### Auth
|
|
||||||
|
|
||||||
- keep `AuthMiddleware` at the outer Hono app level
|
|
||||||
- do not duplicate auth checks inside each `HttpApi` group for the first parallel slices
|
|
||||||
- treat auth as an already-satisfied transport concern before the request reaches the `HttpApi` handler
|
|
||||||
|
|
||||||
Practical rule:
|
|
||||||
|
|
||||||
- if a route is currently protected by the shared server middleware stack, the experimental `HttpApi` route should stay mounted behind that same stack
|
|
||||||
|
|
||||||
### Instance and workspace lookup
|
|
||||||
|
|
||||||
- keep `WorkspaceRouterMiddleware` as the source of truth for resolving `directory`, `workspace`, and session-derived workspace context
|
|
||||||
- let that middleware provide `Instance.current` and `WorkspaceContext` before the request reaches the `HttpApi` handler
|
|
||||||
- keep the `HttpApi` handlers unaware of path-to-instance lookup details when the existing Hono middleware already handles them
|
|
||||||
|
|
||||||
Practical rule:
|
|
||||||
|
|
||||||
- `HttpApi` handlers should yield services from context and assume the correct instance has already been provided
|
|
||||||
- only move instance lookup into the `HttpApi` layer if we later decide to migrate the outer middleware boundary itself
|
|
||||||
|
|
||||||
### Error mapping
|
|
||||||
|
|
||||||
- keep domain and service errors typed in the service layer
|
|
||||||
- declare typed transport errors on the endpoint only when the route can actually return them intentionally
|
|
||||||
- prefer explicit endpoint-level error schemas over relying on the outer Hono `ErrorMiddleware` for expected route behavior
|
|
||||||
|
|
||||||
Practical rule:
|
|
||||||
|
|
||||||
- request decoding failures should remain transport-level `400`s
|
|
||||||
- storage or lookup failures that are part of the route contract should be declared as typed endpoint errors
|
|
||||||
- unexpected defects can still fall through to the outer error middleware while the slice is experimental
|
|
||||||
|
|
||||||
For the current parallel slices, this means:
|
|
||||||
|
|
||||||
- auth still composes outside `HttpApi`
|
|
||||||
- instance selection still composes outside `HttpApi`
|
|
||||||
- success payloads should be schema-defined from canonical Effect schemas
|
|
||||||
- known route errors should be modeled at the endpoint boundary incrementally instead of all at once
|
|
||||||
|
|
||||||
## Exit criteria for the spike
|
|
||||||
|
|
||||||
The first slice is successful if:
|
|
||||||
|
|
||||||
- the endpoints run in parallel with the current Hono routes
|
|
||||||
- the handlers reuse the existing Effect service
|
|
||||||
- request decoding and response shapes are schema-defined from canonical Effect schemas
|
|
||||||
- any remaining Zod boundary usage is derived from `.zod` or clearly temporary
|
|
||||||
- OpenAPI is generated from the `HttpApi` contract
|
|
||||||
- the tests are straightforward enough that the next slice feels mechanical
|
|
||||||
|
|
||||||
## Learnings from the question slice
|
|
||||||
|
|
||||||
The first parallel `question` spike gave us a concrete pattern to reuse.
|
|
||||||
|
|
||||||
- `Schema.Class` works well for route DTOs such as `Question.Request`, `Question.Info`, and `Question.Reply`.
|
|
||||||
- scalar or collection schemas such as `Question.Answer` should stay as schemas and use helpers like `withStatics(...)` instead of being forced into classes.
|
|
||||||
- if an `HttpApi` success schema uses `Schema.Class`, the handler or underlying service needs to return real schema instances rather than plain objects.
|
|
||||||
- internal event payloads can stay anonymous when we want to avoid adding extra named OpenAPI component churn for non-route shapes.
|
|
||||||
- the experimental slice should stay mounted in parallel and keep calling the existing service layer unchanged.
|
|
||||||
- compare generated OpenAPI semantically at the route and schema level; in the current setup the exported OpenAPI paths do not include the outer Hono mount prefix.
|
|
||||||
|
|
||||||
## Route inventory
|
|
||||||
|
|
||||||
Status legend:
|
|
||||||
|
|
||||||
- `done` - parallel `HttpApi` slice exists
|
|
||||||
- `next` - good near-term candidate
|
|
||||||
- `later` - possible, but not first wave
|
|
||||||
- `defer` - not a good early `HttpApi` target
|
|
||||||
|
|
||||||
Current instance route inventory:
|
|
||||||
|
|
||||||
- `question` - `done`
|
|
||||||
endpoints in slice: `GET /question`, `POST /question/:requestID/reply`
|
|
||||||
- `permission` - `done`
|
|
||||||
endpoints in slice: `GET /permission`, `POST /permission/:requestID/reply`
|
|
||||||
- `provider` - `next`
|
|
||||||
best next endpoint: `GET /provider/auth`
|
|
||||||
later endpoint: `GET /provider`
|
|
||||||
defer first-wave OAuth mutations
|
|
||||||
- `config` - `next`
|
|
||||||
best next endpoint: `GET /config/providers`
|
|
||||||
later endpoint: `GET /config`
|
|
||||||
defer `PATCH /config` for now
|
|
||||||
- `project` - `later`
|
|
||||||
best small reads: `GET /project`, `GET /project/current`
|
|
||||||
defer git-init mutation first
|
|
||||||
- `workspace` - `later`
|
|
||||||
best small reads: `GET /experimental/workspace/adaptor`, `GET /experimental/workspace`, `GET /experimental/workspace/status`
|
|
||||||
defer create/remove mutations first
|
|
||||||
- `file` - `later`
|
|
||||||
good JSON-only candidate set, but larger than the current first-wave slices
|
|
||||||
- `mcp` - `later`
|
|
||||||
has JSON-only endpoints, but interactive OAuth/auth flows make it a worse early fit
|
|
||||||
- `session` - `defer`
|
|
||||||
large, stateful, mixes CRUD with prompt/shell/command/share/revert flows and a streaming route
|
|
||||||
- `event` - `defer`
|
|
||||||
SSE only
|
|
||||||
- `global` - `defer`
|
|
||||||
mixed bag with SSE and process-level side effects
|
|
||||||
- `pty` - `defer`
|
|
||||||
websocket-heavy route surface
|
|
||||||
- `tui` - `defer`
|
|
||||||
queue-style UI bridge, weak early `HttpApi` fit
|
|
||||||
|
|
||||||
Recommended near-term sequence after the first spike:
|
|
||||||
|
|
||||||
1. `provider` auth read endpoint
|
|
||||||
2. `config` providers read endpoint
|
|
||||||
3. `project` read endpoints
|
|
||||||
4. `workspace` read endpoints
|
|
||||||
|
|
||||||
## Checklist
|
|
||||||
|
|
||||||
- [x] add one small spike that defines an `HttpApi` group for a simple JSON route set
|
|
||||||
- [x] use Effect Schema request / response types for that slice
|
|
||||||
- [x] keep the underlying service calls identical to the current handlers
|
|
||||||
- [x] compare generated OpenAPI against the current Hono/OpenAPI setup
|
|
||||||
- [x] document how auth, instance lookup, and error mapping would compose in the new stack
|
|
||||||
- [ ] decide after the spike whether `HttpApi` should stay parallel, replace only some groups, or become the long-term default
|
|
||||||
|
|
||||||
## Rule of thumb
|
|
||||||
|
|
||||||
Do not start with the hardest route file.
|
|
||||||
|
|
||||||
If `HttpApi` is adopted here, it should arrive after the handler body is already Effect-native and after the relevant request / response models have moved to Effect Schema.
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# Effect loose ends
|
|
||||||
|
|
||||||
Small follow-ups that do not fit neatly into the main facade, route, tool, or schema migration checklists.
|
|
||||||
|
|
||||||
## Config / TUI
|
|
||||||
|
|
||||||
- [ ] `config/tui.ts` - finish the internal Effect migration after the `Instance.state(...)` removal.
|
|
||||||
Keep the current precedence and migration semantics intact while converting the remaining internal async helpers (`loadState`, `mergeFile`, `loadFile`, `load`) to `Effect.gen(...)` / `Effect.fn(...)`.
|
|
||||||
- [ ] `config/tui.ts` callers - once the internal service is stable, migrate plain async callers to use `TuiConfig.Service` directly where that actually simplifies the code.
|
|
||||||
Likely first callers: `cli/cmd/tui/attach.ts`, `cli/cmd/tui/thread.ts`, `cli/cmd/tui/plugin/runtime.ts`.
|
|
||||||
- [ ] `env/index.ts` - move the last production `Instance.state(...)` usage onto `InstanceState` (or its replacement) so `Instance.state` can be deleted.
|
|
||||||
|
|
||||||
## ConfigPaths
|
|
||||||
|
|
||||||
- [ ] `config/paths.ts` - split pure helpers from effectful helpers.
|
|
||||||
Keep `fileInDirectory(...)` as a plain function.
|
|
||||||
- [ ] `config/paths.ts` - add a `ConfigPaths.Service` for the effectful operations so callers do not inherit `AppFileSystem.Service` directly.
|
|
||||||
Initial service surface should cover:
|
|
||||||
- `projectFiles(...)`
|
|
||||||
- `directories(...)`
|
|
||||||
- `readFile(...)`
|
|
||||||
- `parseText(...)`
|
|
||||||
- [ ] `config/config.ts` - switch internal config loading from `Effect.promise(() => ConfigPaths.*(...))` to `yield* paths.*(...)` once the service exists.
|
|
||||||
- [ ] `config/tui.ts` - switch TUI config loading from async `ConfigPaths.*` wrappers to the `ConfigPaths.Service` once that service exists.
|
|
||||||
- [ ] `config/tui-migrate.ts` - decide whether to leave this as a plain async module using wrapper functions or effectify it fully after `ConfigPaths.Service` lands.
|
|
||||||
|
|
||||||
## Instance cleanup
|
|
||||||
|
|
||||||
- [ ] `project/instance.ts` - remove `Instance.state(...)` once `env/index.ts` is migrated.
|
|
||||||
- [ ] `project/state.ts` - delete the bespoke per-instance state helper after the last production caller is gone.
|
|
||||||
- [ ] `test/project/state.test.ts` - replace or delete the old `Instance.state(...)` tests after the removal.
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- Prefer small, semantics-preserving config migrations. Config precedence, legacy key migration, and plugin origin tracking are easy to break accidentally.
|
|
||||||
- When changing config loading internals, rerun the config and TUI suites first before broad package sweeps.
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
# Route handler effectification
|
|
||||||
|
|
||||||
Practical reference for converting server route handlers in `packages/opencode` to a single `AppRuntime.runPromise(Effect.gen(...))` body.
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Route handlers should wrap their entire body in a single `AppRuntime.runPromise(Effect.gen(...))` call, yielding services from context rather than calling facades one-by-one.
|
|
||||||
|
|
||||||
This eliminates multiple `runPromise` round-trips and lets handlers compose naturally.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// Before - one facade call per service
|
|
||||||
;async (c) => {
|
|
||||||
await SessionRunState.assertNotBusy(id)
|
|
||||||
await Session.removeMessage({ sessionID: id, messageID })
|
|
||||||
return c.json(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// After - one Effect.gen, yield services from context
|
|
||||||
;async (c) => {
|
|
||||||
await AppRuntime.runPromise(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const state = yield* SessionRunState.Service
|
|
||||||
const session = yield* Session.Service
|
|
||||||
yield* state.assertNotBusy(id)
|
|
||||||
yield* session.removeMessage({ sessionID: id, messageID })
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
return c.json(true)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Rules
|
|
||||||
|
|
||||||
- Wrap the whole handler body in one `AppRuntime.runPromise(Effect.gen(...))` call when the handler is service-heavy.
|
|
||||||
- Yield services from context instead of calling async facades repeatedly.
|
|
||||||
- When independent service calls can run in parallel, use `Effect.all(..., { concurrency: "unbounded" })`.
|
|
||||||
- Prefer one composed Effect body over multiple separate `runPromise(...)` calls in the same handler.
|
|
||||||
|
|
||||||
## Current route files
|
|
||||||
|
|
||||||
Current instance route files live under `src/server/instance`, not `server/routes`.
|
|
||||||
|
|
||||||
The main migration targets are:
|
|
||||||
|
|
||||||
- [ ] `server/instance/session.ts` — heaviest; still has many direct facade calls for Session, SessionPrompt, SessionRevert, SessionCompaction, SessionShare, SessionSummary, Agent, Bus
|
|
||||||
- [ ] `server/instance/global.ts` — still has direct facade calls for Config and instance lifecycle actions
|
|
||||||
- [ ] `server/instance/provider.ts` — still has direct facade calls for Config and Provider
|
|
||||||
- [ ] `server/instance/question.ts` — partially converted; still worth tracking here until it consistently uses the composed style
|
|
||||||
- [ ] `server/instance/pty.ts` — still calls Pty facades directly
|
|
||||||
- [ ] `server/instance/experimental.ts` — mixed state; some handlers are already composed, others still use facades
|
|
||||||
|
|
||||||
Additional route files that still participate in the migration:
|
|
||||||
|
|
||||||
- [ ] `server/instance/index.ts` — Vcs, Agent, Skill, LSP, Format
|
|
||||||
- [ ] `server/instance/file.ts` — Ripgrep, File, LSP
|
|
||||||
- [ ] `server/instance/mcp.ts` — MCP facade-heavy
|
|
||||||
- [ ] `server/instance/permission.ts` — Permission
|
|
||||||
- [ ] `server/instance/workspace.ts` — Workspace
|
|
||||||
- [ ] `server/instance/tui.ts` — Bus and Session
|
|
||||||
- [ ] `server/instance/middleware.ts` — Session and Workspace lookups
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- Some handlers already use `AppRuntime.runPromise(Effect.gen(...))` in isolated places. Keep pushing those files toward one consistent style.
|
|
||||||
- Route conversion is closely tied to facade removal. As services lose `makeRuntime`-backed async exports, route handlers should switch to yielding the service directly.
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
# Schema migration
|
|
||||||
|
|
||||||
Practical reference for migrating data types in `packages/opencode` from Zod-first definitions to Effect Schema with Zod compatibility shims.
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Use Effect Schema as the source of truth for domain models, IDs, inputs, outputs, and typed errors.
|
|
||||||
|
|
||||||
Keep Zod available at existing HTTP, tool, and compatibility boundaries by exposing a `.zod` field derived from the Effect schema.
|
|
||||||
|
|
||||||
## Preferred shapes
|
|
||||||
|
|
||||||
### Data objects
|
|
||||||
|
|
||||||
Use `Schema.Class` for structured data.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
export class Info extends Schema.Class<Info>("Foo.Info")({
|
|
||||||
id: FooID,
|
|
||||||
name: Schema.String,
|
|
||||||
enabled: Schema.Boolean,
|
|
||||||
}) {
|
|
||||||
static readonly zod = zod(Info)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
If the class cannot reference itself cleanly during initialization, use the existing two-step pattern:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
const _Info = Schema.Struct({
|
|
||||||
id: FooID,
|
|
||||||
name: Schema.String,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const Info = Object.assign(_Info, {
|
|
||||||
zod: zod(_Info),
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Errors
|
|
||||||
|
|
||||||
Use `Schema.TaggedErrorClass` for domain errors.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("FooNotFoundError", {
|
|
||||||
id: FooID,
|
|
||||||
}) {}
|
|
||||||
```
|
|
||||||
|
|
||||||
### IDs and branded leaf types
|
|
||||||
|
|
||||||
Keep branded/schema-backed IDs as Effect schemas and expose `static readonly zod` for compatibility when callers still expect Zod.
|
|
||||||
|
|
||||||
## Compatibility rule
|
|
||||||
|
|
||||||
During migration, route validators, tool parameters, and any existing Zod-based boundary should consume the derived `.zod` schema instead of maintaining a second hand-written Zod schema.
|
|
||||||
|
|
||||||
The default should be:
|
|
||||||
|
|
||||||
- Effect Schema owns the type
|
|
||||||
- `.zod` exists only as a compatibility surface
|
|
||||||
- new domain models should not start Zod-first unless there is a concrete boundary-specific need
|
|
||||||
|
|
||||||
## When Zod can stay
|
|
||||||
|
|
||||||
It is fine to keep a Zod-native schema temporarily when:
|
|
||||||
|
|
||||||
- the type is only used at an HTTP or tool boundary
|
|
||||||
- the validator depends on Zod-only transforms or behavior not yet covered by `zod()`
|
|
||||||
- the migration would force unrelated churn across a large call graph
|
|
||||||
|
|
||||||
When this happens, prefer leaving a short note or TODO rather than silently creating a parallel schema source of truth.
|
|
||||||
|
|
||||||
## Ordering
|
|
||||||
|
|
||||||
Migrate in this order:
|
|
||||||
|
|
||||||
1. Shared leaf models and `schema.ts` files
|
|
||||||
2. Exported `Info`, `Input`, `Output`, and DTO types
|
|
||||||
3. Tagged domain errors
|
|
||||||
4. Service-local internal models
|
|
||||||
5. Route and tool boundary validators that can switch to `.zod`
|
|
||||||
|
|
||||||
This keeps shared types canonical first and makes boundary updates mostly mechanical.
|
|
||||||
|
|
||||||
## Checklist
|
|
||||||
|
|
||||||
- [ ] Shared `schema.ts` leaf models are Effect Schema-first
|
|
||||||
- [ ] Exported `Info` / `Input` / `Output` types use `Schema.Class` where appropriate
|
|
||||||
- [ ] Domain errors use `Schema.TaggedErrorClass`
|
|
||||||
- [ ] Migrated types expose `.zod` for back compatibility
|
|
||||||
- [ ] Route and tool validators consume derived `.zod` instead of duplicate Zod definitions
|
|
||||||
- [ ] New domain models default to Effect Schema first
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- Use `@/util/effect-zod` for all Schema -> Zod conversion.
|
|
||||||
- Prefer one canonical schema definition. Avoid maintaining parallel Zod and Effect definitions for the same domain type.
|
|
||||||
- Keep the migration incremental. Converting the domain model first is more valuable than converting every boundary in the same change.
|
|
||||||
@@ -1,666 +0,0 @@
|
|||||||
# Server package extraction
|
|
||||||
|
|
||||||
Practical reference for extracting a future `packages/server` from the current `packages/opencode` monolith while `packages/core` is still being migrated to Effect.
|
|
||||||
|
|
||||||
This document is intentionally execution-oriented.
|
|
||||||
|
|
||||||
It should give an agent enough context to land one incremental PR at a time without needing to rediscover the package strategy, route migration rules, or current constraints.
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Create `packages/server` as the home for:
|
|
||||||
|
|
||||||
- HTTP contract definitions
|
|
||||||
- HTTP handler implementations
|
|
||||||
- OpenAPI generation
|
|
||||||
- eventual embeddable server APIs for Node apps
|
|
||||||
|
|
||||||
Do this without blocking on the full `packages/core` extraction.
|
|
||||||
|
|
||||||
## Future state
|
|
||||||
|
|
||||||
Target package layout:
|
|
||||||
|
|
||||||
- `packages/core` - all opencode services, Effect-first source of truth
|
|
||||||
- `packages/server` - opencode server, with separate contract and implementation, still producing `openapi.json`
|
|
||||||
- `packages/cli` - TUI + CLI entrypoints
|
|
||||||
- `packages/sdk` - generated from the server OpenAPI spec, may add higher-level wrappers
|
|
||||||
- `packages/plugin` - generated or semi-hand-rolled non-Effect package built from core plugin definitions
|
|
||||||
|
|
||||||
Desired user stories:
|
|
||||||
|
|
||||||
- import from `core` and build a custom agent or app-specific runtime
|
|
||||||
- import from `server` and embed the full opencode server into an existing Node app
|
|
||||||
- spawn the CLI and talk to the server through that boundary
|
|
||||||
|
|
||||||
## Current state
|
|
||||||
|
|
||||||
Everything still lives in `packages/opencode`.
|
|
||||||
|
|
||||||
Important current facts:
|
|
||||||
|
|
||||||
- there is no `packages/core` or `packages/cli` workspace yet
|
|
||||||
- `packages/server` now exists as a minimal scaffold package, but it does not own any real route contracts, handlers, or runtime composition yet
|
|
||||||
- the main host server is still Hono-based in `src/server/server.ts`
|
|
||||||
- current OpenAPI generation is Hono-based through `Server.openapi()` and `cli/cmd/generate.ts`
|
|
||||||
- the Effect runtime and app layer are centralized in `src/effect/app-runtime.ts` and `src/effect/run-service.ts`
|
|
||||||
- there is already one experimental Effect `HttpApi` slice at `src/server/instance/httpapi/question.ts`
|
|
||||||
- that experimental slice is mounted under `/experimental/httpapi/question`
|
|
||||||
- that experimental slice already has an end-to-end test at `test/server/question-httpapi.test.ts`
|
|
||||||
|
|
||||||
This means the package split should start from an extraction path, not from greenfield package ownership.
|
|
||||||
|
|
||||||
## Structural reference
|
|
||||||
|
|
||||||
Use `anomalyco/opentunnel` as the structural reference for `packages/server`.
|
|
||||||
|
|
||||||
The important pattern there is:
|
|
||||||
|
|
||||||
- `packages/core` owns services and domain schemas
|
|
||||||
- `packages/server/src/definition/*` owns pure `HttpApi` contracts
|
|
||||||
- `packages/server/src/api/*` owns `HttpApiBuilder.group(...)` implementations and server-side middleware wiring
|
|
||||||
- `packages/server/src/index.ts` becomes the composition root only after the server package really owns runtime hosting
|
|
||||||
|
|
||||||
Relevant `opentunnel` files:
|
|
||||||
|
|
||||||
- `packages/server/src/definition/index.ts`
|
|
||||||
- `packages/server/src/definition/tunnel.ts`
|
|
||||||
- `packages/server/src/api/index.ts`
|
|
||||||
- `packages/server/src/api/tunnel.ts`
|
|
||||||
- `packages/server/src/api/client.ts`
|
|
||||||
- `packages/server/src/index.ts`
|
|
||||||
|
|
||||||
The intended direction here is the same, but the current `opencode` package split is earlier in the migration.
|
|
||||||
|
|
||||||
That means:
|
|
||||||
|
|
||||||
- we should follow the same `definition` and `api` naming
|
|
||||||
- we should keep contract and implementation as separate modules from the start
|
|
||||||
- we should postpone the runtime composition root until `packages/core` exists enough to support it cleanly
|
|
||||||
|
|
||||||
## Key decision
|
|
||||||
|
|
||||||
Start `packages/server` as a contract and implementation package only.
|
|
||||||
|
|
||||||
Do not make it the runtime host yet.
|
|
||||||
|
|
||||||
Why:
|
|
||||||
|
|
||||||
- `packages/core` does not exist yet
|
|
||||||
- the current server host still lives in `packages/opencode`
|
|
||||||
- moving host ownership immediately would force a large package and runtime shuffle while Effect service extraction is still in flight
|
|
||||||
- if `packages/server` imports services from `packages/opencode` while `packages/opencode` imports `packages/server` to host routes, we create a package cycle immediately
|
|
||||||
|
|
||||||
Short version:
|
|
||||||
|
|
||||||
1. create `packages/server`
|
|
||||||
2. move pure `HttpApi` contracts there
|
|
||||||
3. move handler factories there
|
|
||||||
4. keep `packages/opencode` as the temporary Hono host
|
|
||||||
5. merge `packages/server` OpenAPI with the legacy Hono OpenAPI during the transition
|
|
||||||
6. move server hosting later, after `packages/core` exists enough
|
|
||||||
|
|
||||||
## Dependency rule
|
|
||||||
|
|
||||||
Phase 1 rule:
|
|
||||||
|
|
||||||
- `packages/server` must not import from `packages/opencode`
|
|
||||||
|
|
||||||
Allowed in phase 1:
|
|
||||||
|
|
||||||
- `packages/opencode` imports `packages/server`
|
|
||||||
- `packages/server` accepts host-provided services, layers, or callbacks as inputs
|
|
||||||
- `packages/server` may temporarily own transport-local placeholder schemas when a canonical shared schema does not exist yet
|
|
||||||
|
|
||||||
Future rule after `packages/core` exists:
|
|
||||||
|
|
||||||
- `packages/server` imports from `packages/core`
|
|
||||||
- `packages/cli` imports from `packages/server` and `packages/core`
|
|
||||||
- `packages/opencode` shrinks or disappears as package responsibilities are fully split
|
|
||||||
|
|
||||||
## HttpApi model
|
|
||||||
|
|
||||||
Use Effect v4 `HttpApi` as the source of truth for migrated HTTP routes.
|
|
||||||
|
|
||||||
Important properties from the current `effect` / `effect-smol` model:
|
|
||||||
|
|
||||||
- `HttpApi`, `HttpApiGroup`, and `HttpApiEndpoint` are pure contract definitions
|
|
||||||
- handlers are implemented separately with `HttpApiBuilder.group(...)`
|
|
||||||
- OpenAPI can be generated from the contract alone
|
|
||||||
- auth and middleware can later be modeled with `HttpApiMiddleware.Service`
|
|
||||||
- SSE and websocket routes are not good first-wave `HttpApi` targets
|
|
||||||
|
|
||||||
This package split should preserve that separation explicitly.
|
|
||||||
|
|
||||||
Default shape for migrated routes:
|
|
||||||
|
|
||||||
- contract lives in `packages/server/src/definition/*`
|
|
||||||
- implementation lives in `packages/server/src/api/*`
|
|
||||||
- host mounting stays outside for now
|
|
||||||
|
|
||||||
## OpenAPI rule
|
|
||||||
|
|
||||||
During the transition there is still one spec artifact.
|
|
||||||
|
|
||||||
Default rule:
|
|
||||||
|
|
||||||
- `packages/server` generates OpenAPI from `HttpApi` contract
|
|
||||||
- `packages/opencode` keeps generating legacy OpenAPI from Hono routes
|
|
||||||
- the temporary exported server spec is a merged document
|
|
||||||
- `packages/sdk` continues consuming one `openapi.json`
|
|
||||||
|
|
||||||
Merge safety rules:
|
|
||||||
|
|
||||||
- fail on duplicate `path + method`
|
|
||||||
- fail on duplicate `operationId`
|
|
||||||
- prefer explicit summary, description, and operation ids on all new `HttpApi` endpoints
|
|
||||||
|
|
||||||
Practical implication:
|
|
||||||
|
|
||||||
- do not make the SDK consume two specs
|
|
||||||
- do not switch SDK generation to `packages/server` only until enough of the route surface has moved
|
|
||||||
|
|
||||||
## Package shape
|
|
||||||
|
|
||||||
Minimum viable `packages/server`:
|
|
||||||
|
|
||||||
- `src/index.ts`
|
|
||||||
- `src/definition/index.ts`
|
|
||||||
- `src/definition/api.ts`
|
|
||||||
- `src/definition/question.ts`
|
|
||||||
- `src/api/index.ts`
|
|
||||||
- `src/api/question.ts`
|
|
||||||
- `src/openapi.ts`
|
|
||||||
- `src/bridge/hono.ts`
|
|
||||||
- `src/types.ts`
|
|
||||||
|
|
||||||
Later additions, once there is enough real contract surface:
|
|
||||||
|
|
||||||
- `src/api/client.ts`
|
|
||||||
- runtime composition in `src/index.ts`
|
|
||||||
|
|
||||||
Suggested initial exports:
|
|
||||||
|
|
||||||
- `api`
|
|
||||||
- `openapi`
|
|
||||||
- `questionApi`
|
|
||||||
- `makeQuestionHandler`
|
|
||||||
|
|
||||||
Phase 1 responsibilities:
|
|
||||||
|
|
||||||
- own pure API contracts
|
|
||||||
- own handler factories for migrated slices
|
|
||||||
- own contract-generated OpenAPI
|
|
||||||
- expose host adapters needed by `packages/opencode`
|
|
||||||
|
|
||||||
Phase 1 non-goals:
|
|
||||||
|
|
||||||
- do not own `listen()`
|
|
||||||
- do not own adapter selection
|
|
||||||
- do not own global server middleware
|
|
||||||
- do not own websocket or SSE transport
|
|
||||||
- do not own process bootstrapping for CLI entrypoints
|
|
||||||
|
|
||||||
## Current source inventory
|
|
||||||
|
|
||||||
These files matter for the first phase.
|
|
||||||
|
|
||||||
Current host and route composition:
|
|
||||||
|
|
||||||
- `src/server/server.ts`
|
|
||||||
- `src/server/control/index.ts`
|
|
||||||
- `src/server/instance/index.ts`
|
|
||||||
- `src/server/middleware.ts`
|
|
||||||
- `src/server/adapter.bun.ts`
|
|
||||||
- `src/server/adapter.node.ts`
|
|
||||||
|
|
||||||
Current experimental `HttpApi` slice:
|
|
||||||
|
|
||||||
- `src/server/instance/httpapi/question.ts`
|
|
||||||
- `src/server/instance/httpapi/index.ts`
|
|
||||||
- `src/server/instance/experimental.ts`
|
|
||||||
- `test/server/question-httpapi.test.ts`
|
|
||||||
|
|
||||||
Current OpenAPI flow:
|
|
||||||
|
|
||||||
- `src/server/server.ts` via `Server.openapi()`
|
|
||||||
- `src/cli/cmd/generate.ts`
|
|
||||||
- `packages/sdk/js/script/build.ts`
|
|
||||||
|
|
||||||
Current runtime and service layer:
|
|
||||||
|
|
||||||
- `src/effect/app-runtime.ts`
|
|
||||||
- `src/effect/run-service.ts`
|
|
||||||
|
|
||||||
## Ownership rules
|
|
||||||
|
|
||||||
Move first into `packages/server`:
|
|
||||||
|
|
||||||
- the experimental `question` `HttpApi` slice
|
|
||||||
- future `provider` and `config` JSON read slices
|
|
||||||
- any new `HttpApi` route groups
|
|
||||||
- transport-local OpenAPI generation for migrated routes
|
|
||||||
|
|
||||||
Keep in `packages/opencode` for now:
|
|
||||||
|
|
||||||
- `src/server/server.ts`
|
|
||||||
- `src/server/control/index.ts`
|
|
||||||
- `src/server/instance/*.ts`
|
|
||||||
- `src/server/middleware.ts`
|
|
||||||
- `src/server/adapter.*.ts`
|
|
||||||
- `src/effect/app-runtime.ts`
|
|
||||||
- `src/effect/run-service.ts`
|
|
||||||
- all Effect services until they move to `packages/core`
|
|
||||||
|
|
||||||
## Placeholder schema rule
|
|
||||||
|
|
||||||
`packages/core` is allowed to lag behind.
|
|
||||||
|
|
||||||
Until shared canonical schemas move to `packages/core`:
|
|
||||||
|
|
||||||
- prefer importing existing Effect Schema DTOs from current locations when practical
|
|
||||||
- if a route only needs a transport-local type and moving the canonical schema would create unrelated churn, allow a temporary server-local placeholder schema
|
|
||||||
- if a placeholder is introduced, leave a short note so it does not become permanent
|
|
||||||
|
|
||||||
The default rule from `schema.md` still applies:
|
|
||||||
|
|
||||||
- Effect Schema owns the type
|
|
||||||
- `.zod` is compatibility only
|
|
||||||
- avoid parallel hand-written Zod and Effect definitions for the same migrated route shape
|
|
||||||
|
|
||||||
## Host boundary rule
|
|
||||||
|
|
||||||
Until host ownership moves:
|
|
||||||
|
|
||||||
- auth stays at the outer Hono app level
|
|
||||||
- compression stays at the outer Hono app level
|
|
||||||
- CORS stays at the outer Hono app level
|
|
||||||
- instance and workspace lookup stay at the current middleware layer
|
|
||||||
- `packages/server` handlers should assume the host already provided the right request context
|
|
||||||
- do not redesign host middleware just to land the package split
|
|
||||||
|
|
||||||
This matches the current guidance in `http-api.md`:
|
|
||||||
|
|
||||||
- keep auth outside the first parallel `HttpApi` slices
|
|
||||||
- keep instance lookup outside the first parallel `HttpApi` slices
|
|
||||||
- keep the first migrations transport-focused and semantics-preserving
|
|
||||||
|
|
||||||
## Route selection rules
|
|
||||||
|
|
||||||
Good early migration targets:
|
|
||||||
|
|
||||||
- `question`
|
|
||||||
- `provider` auth read endpoint
|
|
||||||
- `config` providers read endpoint
|
|
||||||
- small read-only instance routes
|
|
||||||
|
|
||||||
Bad early migration targets:
|
|
||||||
|
|
||||||
- `session`
|
|
||||||
- `event`
|
|
||||||
- `pty`
|
|
||||||
- most `global` streaming or process-heavy routes
|
|
||||||
- anything requiring websocket upgrade handling
|
|
||||||
- anything that mixes many mutations and streaming in one file
|
|
||||||
|
|
||||||
## First vertical slice
|
|
||||||
|
|
||||||
The first slice for the package split is the existing experimental `question` group.
|
|
||||||
|
|
||||||
Why `question` first:
|
|
||||||
|
|
||||||
- it already exists as an experimental `HttpApi` slice
|
|
||||||
- it already follows the desired contract and implementation split in one file
|
|
||||||
- it is already mounted through the current Hono host
|
|
||||||
- it already has an end-to-end test
|
|
||||||
- it is JSON-only
|
|
||||||
- it has low blast radius
|
|
||||||
|
|
||||||
Use the first slice to prove:
|
|
||||||
|
|
||||||
- package boundary
|
|
||||||
- contract and implementation split
|
|
||||||
- host mounting from `packages/opencode`
|
|
||||||
- merged OpenAPI output
|
|
||||||
- test ergonomics for future slices
|
|
||||||
|
|
||||||
Do not broaden scope in the first slice.
|
|
||||||
|
|
||||||
## Incremental migration order
|
|
||||||
|
|
||||||
Use small PRs.
|
|
||||||
|
|
||||||
Each PR should be easy to review, easy to revert, and should not mix extraction work with unrelated service refactors.
|
|
||||||
|
|
||||||
### PR 1. Create `packages/server`
|
|
||||||
|
|
||||||
Scope:
|
|
||||||
|
|
||||||
- add the new workspace package
|
|
||||||
- add package manifest and tsconfig
|
|
||||||
- add empty `src/index.ts`, `src/definition/api.ts`, `src/definition/index.ts`, `src/api/index.ts`, `src/openapi.ts`, and supporting scaffolding
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
|
|
||||||
- no production behavior changes
|
|
||||||
- no host server changes yet
|
|
||||||
- no imports from `packages/opencode` inside `packages/server`
|
|
||||||
- prefer `opentunnel`-style naming from the start: `definition` for contracts, `api` for implementations
|
|
||||||
|
|
||||||
Done means:
|
|
||||||
|
|
||||||
- `packages/server` typechecks
|
|
||||||
- the workspace can import it
|
|
||||||
- the package boundary is in place for follow-up PRs
|
|
||||||
|
|
||||||
### PR 2. Move the experimental question contract
|
|
||||||
|
|
||||||
Scope:
|
|
||||||
|
|
||||||
- extract the pure `HttpApi` contract from `src/server/instance/httpapi/question.ts`
|
|
||||||
- place it in `packages/server/src/definition/question.ts`
|
|
||||||
- aggregate it in `packages/server/src/definition/api.ts`
|
|
||||||
- generate OpenAPI in `packages/server/src/openapi.ts`
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
|
|
||||||
- contract only in this PR
|
|
||||||
- no handler movement yet if that keeps the diff simpler
|
|
||||||
- keep operation ids and docs metadata stable
|
|
||||||
|
|
||||||
Done means:
|
|
||||||
|
|
||||||
- question contract lives in `packages/server`
|
|
||||||
- OpenAPI can be generated from contract alone
|
|
||||||
- no runtime behavior changes yet
|
|
||||||
|
|
||||||
### PR 3. Move the experimental question handler factory
|
|
||||||
|
|
||||||
Scope:
|
|
||||||
|
|
||||||
- extract the question `HttpApiBuilder.group(...)` implementation into `packages/server/src/api/question.ts`
|
|
||||||
- expose it as a factory that accepts host-provided dependencies or wiring
|
|
||||||
- add a small Hono bridge in `packages/server/src/bridge/hono.ts` if needed
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
|
|
||||||
- `packages/server` must still not import from `packages/opencode`
|
|
||||||
- handler code should stay thin and service-delegating
|
|
||||||
- do not redesign the question service itself in this PR
|
|
||||||
|
|
||||||
Done means:
|
|
||||||
|
|
||||||
- `packages/server` can produce the experimental question handler
|
|
||||||
- the package still stays cycle-free
|
|
||||||
|
|
||||||
### PR 4. Mount `packages/server` question from `packages/opencode`
|
|
||||||
|
|
||||||
Scope:
|
|
||||||
|
|
||||||
- replace local experimental question route wiring in `packages/opencode`
|
|
||||||
- keep the same mount path:
|
|
||||||
- `/experimental/httpapi/question`
|
|
||||||
- `/experimental/httpapi/question/doc`
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
|
|
||||||
- no behavior change
|
|
||||||
- preserve existing docs path
|
|
||||||
- preserve current request and response shapes
|
|
||||||
|
|
||||||
Done means:
|
|
||||||
|
|
||||||
- existing question `HttpApi` test still passes
|
|
||||||
- runtime behavior is unchanged
|
|
||||||
- the current host server is now consuming `packages/server`
|
|
||||||
|
|
||||||
### PR 5. Merge legacy and contract OpenAPI
|
|
||||||
|
|
||||||
Scope:
|
|
||||||
|
|
||||||
- keep `Server.openapi()` as the temporary spec entrypoint
|
|
||||||
- generate legacy Hono spec
|
|
||||||
- generate `packages/server` contract spec
|
|
||||||
- merge them into one document
|
|
||||||
- keep `cli/cmd/generate.ts` and `packages/sdk/js/script/build.ts` consuming one spec
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
|
|
||||||
- fail loudly on duplicate `path + method`
|
|
||||||
- fail loudly on duplicate `operationId`
|
|
||||||
- do not silently overwrite one source with the other
|
|
||||||
|
|
||||||
Done means:
|
|
||||||
|
|
||||||
- one merged spec is produced
|
|
||||||
- migrated question paths can come from `packages/server`
|
|
||||||
- existing SDK generation path still works
|
|
||||||
|
|
||||||
### PR 6. Add merged OpenAPI coverage
|
|
||||||
|
|
||||||
Scope:
|
|
||||||
|
|
||||||
- add one test for merged OpenAPI
|
|
||||||
- assert both a legacy Hono route and a migrated `HttpApi` route exist
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
|
|
||||||
- test the merged document, not just the `packages/server` contract spec in isolation
|
|
||||||
- pick one stable legacy route and one stable migrated route
|
|
||||||
|
|
||||||
Done means:
|
|
||||||
|
|
||||||
- the merged-spec path is covered
|
|
||||||
- future route migrations have a guardrail
|
|
||||||
|
|
||||||
### PR 7. Migrate `GET /provider/auth`
|
|
||||||
|
|
||||||
Scope:
|
|
||||||
|
|
||||||
- add `GET /provider/auth` as the next `HttpApi` slice in `packages/server`
|
|
||||||
- mount it in parallel from `packages/opencode`
|
|
||||||
|
|
||||||
Why this route:
|
|
||||||
|
|
||||||
- JSON-only
|
|
||||||
- simple service delegation
|
|
||||||
- small response shape
|
|
||||||
- already listed as the best next `provider` candidate in `http-api.md`
|
|
||||||
|
|
||||||
Done means:
|
|
||||||
|
|
||||||
- route works through the current host
|
|
||||||
- route appears in merged OpenAPI
|
|
||||||
- no semantic change to provider auth behavior
|
|
||||||
|
|
||||||
### PR 8. Migrate `GET /config/providers`
|
|
||||||
|
|
||||||
Scope:
|
|
||||||
|
|
||||||
- add `GET /config/providers` as a `HttpApi` slice in `packages/server`
|
|
||||||
- mount it in parallel from `packages/opencode`
|
|
||||||
|
|
||||||
Why this route:
|
|
||||||
|
|
||||||
- JSON-only
|
|
||||||
- read-only
|
|
||||||
- low transport complexity
|
|
||||||
- already listed as the best next `config` candidate in `http-api.md`
|
|
||||||
|
|
||||||
Done means:
|
|
||||||
|
|
||||||
- route works unchanged
|
|
||||||
- route appears in merged OpenAPI
|
|
||||||
|
|
||||||
### PR 9+. Migrate small read-only instance routes
|
|
||||||
|
|
||||||
Candidate order:
|
|
||||||
|
|
||||||
1. `GET /path`
|
|
||||||
2. `GET /vcs`
|
|
||||||
3. `GET /vcs/diff`
|
|
||||||
4. `GET /command`
|
|
||||||
5. `GET /agent`
|
|
||||||
6. `GET /skill`
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
|
|
||||||
- one or two endpoints per PR
|
|
||||||
- prefer read-only routes first
|
|
||||||
- keep outer middleware unchanged
|
|
||||||
- keep business logic in the existing service layer
|
|
||||||
|
|
||||||
Done means for each PR:
|
|
||||||
|
|
||||||
- contract lives in `packages/server`
|
|
||||||
- handler lives in `packages/server`
|
|
||||||
- route is mounted from the current host
|
|
||||||
- route appears in merged OpenAPI
|
|
||||||
- behavior remains unchanged
|
|
||||||
|
|
||||||
### Later PR. Move host ownership into `packages/server`
|
|
||||||
|
|
||||||
Only start this after there is enough `packages/core` surface to depend on directly.
|
|
||||||
|
|
||||||
Scope:
|
|
||||||
|
|
||||||
- move server composition into `packages/server`
|
|
||||||
- add embeddable APIs such as `createServer(...)`, `listen(...)`, or `createApp(...)`
|
|
||||||
- move adapter selection and server startup out of `packages/opencode`
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
|
|
||||||
- do not start this while `packages/server` still depends on `packages/opencode`
|
|
||||||
- do not mix this with route migration PRs
|
|
||||||
|
|
||||||
Done means:
|
|
||||||
|
|
||||||
- `packages/server` can be embedded in another Node app
|
|
||||||
- `packages/cli` can depend on `packages/server`
|
|
||||||
- host logic no longer lives in `packages/opencode`
|
|
||||||
|
|
||||||
## PR sizing rule
|
|
||||||
|
|
||||||
Every migration PR should satisfy all of these:
|
|
||||||
|
|
||||||
- one route group or one to two endpoints
|
|
||||||
- no unrelated service refactor
|
|
||||||
- no auth redesign
|
|
||||||
- no middleware redesign
|
|
||||||
- OpenAPI updated
|
|
||||||
- at least one route test or spec test added or updated
|
|
||||||
|
|
||||||
## Done means for a migrated route group
|
|
||||||
|
|
||||||
A route group migration is complete only when:
|
|
||||||
|
|
||||||
1. the `HttpApi` contract lives in `packages/server`
|
|
||||||
2. handler implementation lives in `packages/server`
|
|
||||||
3. the route is mounted from the current host in `packages/opencode`
|
|
||||||
4. the route appears in merged OpenAPI
|
|
||||||
5. request and response schemas are Effect Schema-first or clearly temporary placeholders
|
|
||||||
6. existing behavior remains unchanged
|
|
||||||
7. the route has straightforward test coverage
|
|
||||||
|
|
||||||
## Validation expectations
|
|
||||||
|
|
||||||
For package-split PRs, validate the smallest useful thing.
|
|
||||||
|
|
||||||
Typical validation for the first waves:
|
|
||||||
|
|
||||||
- `bun typecheck` in the touched package directory or directories
|
|
||||||
- the relevant route test, especially `test/server/question-httpapi.test.ts`
|
|
||||||
- merged OpenAPI coverage if the PR touches spec generation
|
|
||||||
|
|
||||||
Do not run tests from repo root.
|
|
||||||
|
|
||||||
## Main risks
|
|
||||||
|
|
||||||
### Package cycle
|
|
||||||
|
|
||||||
This is the biggest risk.
|
|
||||||
|
|
||||||
Bad state:
|
|
||||||
|
|
||||||
- `packages/server` imports services or runtime from `packages/opencode`
|
|
||||||
- `packages/opencode` imports route definitions or handlers from `packages/server`
|
|
||||||
|
|
||||||
Avoid by:
|
|
||||||
|
|
||||||
- keeping phase-1 `packages/server` free of `packages/opencode` imports
|
|
||||||
- using factories and host-provided wiring instead of direct service imports
|
|
||||||
|
|
||||||
### Spec drift
|
|
||||||
|
|
||||||
During the transition there are two route-definition sources.
|
|
||||||
|
|
||||||
Avoid by:
|
|
||||||
|
|
||||||
- one merged spec
|
|
||||||
- collision checks
|
|
||||||
- explicit `operationId`s
|
|
||||||
- merged OpenAPI tests
|
|
||||||
|
|
||||||
### Middleware mismatch
|
|
||||||
|
|
||||||
Current auth, compression, CORS, and instance selection are Hono-centered.
|
|
||||||
|
|
||||||
Avoid by:
|
|
||||||
|
|
||||||
- leaving them where they are during the first wave
|
|
||||||
- not trying to solve `HttpApiMiddleware.Service` globally in the package-split PRs
|
|
||||||
|
|
||||||
### Core lag
|
|
||||||
|
|
||||||
`packages/core` will not be ready everywhere.
|
|
||||||
|
|
||||||
Avoid by:
|
|
||||||
|
|
||||||
- allowing small transport-local placeholder schemas where necessary
|
|
||||||
- keeping those placeholders clearly temporary
|
|
||||||
- not blocking the server extraction on full schema movement
|
|
||||||
|
|
||||||
### Scope creep
|
|
||||||
|
|
||||||
The first vertical slice is easy to overload.
|
|
||||||
|
|
||||||
Avoid by:
|
|
||||||
|
|
||||||
- proving the package boundary first
|
|
||||||
- not mixing package creation, route migration, host redesign, and core extraction in the same change
|
|
||||||
|
|
||||||
## Non-goals for the first wave
|
|
||||||
|
|
||||||
- do not replace all Hono routes at once
|
|
||||||
- do not migrate SSE or websocket routes first
|
|
||||||
- do not redesign auth
|
|
||||||
- do not redesign instance lookup
|
|
||||||
- do not wait for full `packages/core` before starting `packages/server`
|
|
||||||
- do not change SDK generation to consume multiple specs
|
|
||||||
|
|
||||||
## Checklist
|
|
||||||
|
|
||||||
- [x] create `packages/server`
|
|
||||||
- [x] add package-level exports for contract and OpenAPI
|
|
||||||
- [ ] extract `question` contract into `packages/server`
|
|
||||||
- [ ] extract `question` handler factory into `packages/server`
|
|
||||||
- [ ] mount `question` from `packages/opencode`
|
|
||||||
- [ ] merge legacy and contract OpenAPI into one document
|
|
||||||
- [ ] add merged-spec coverage
|
|
||||||
- [ ] migrate `GET /provider/auth`
|
|
||||||
- [ ] migrate `GET /config/providers`
|
|
||||||
- [ ] migrate small read-only instance routes one or two at a time
|
|
||||||
- [ ] move host ownership into `packages/server` only after `packages/core` is ready enough
|
|
||||||
- [ ] split `packages/cli` after server and core boundaries are stable
|
|
||||||
|
|
||||||
## Rule of thumb
|
|
||||||
|
|
||||||
The fastest correct path is:
|
|
||||||
|
|
||||||
1. establish `packages/server` as the contract-first boundary
|
|
||||||
2. keep `packages/opencode` as the temporary host
|
|
||||||
3. migrate a few safe JSON routes
|
|
||||||
4. keep one merged OpenAPI document
|
|
||||||
5. move actual host ownership only after `packages/core` can support it cleanly
|
|
||||||
|
|
||||||
If a proposed PR would make `packages/server` import from `packages/opencode`, stop and restructure the boundary first.
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
# Tool migration
|
|
||||||
|
|
||||||
Practical reference for the current tool-migration state in `packages/opencode`.
|
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
`Tool.Def.execute` and `Tool.Info.init` already return `Effect` on this branch, and the built-in tool surface is now largely on the target shape.
|
|
||||||
|
|
||||||
The current exported tools in `src/tool` all use `Tool.define(...)` with Effect-based initialization, and nearly all of them already build their tool body with `Effect.gen(...)` and `Effect.fn(...)`.
|
|
||||||
|
|
||||||
So the remaining work is no longer "convert tools to Effect at all". The remaining work is mostly:
|
|
||||||
|
|
||||||
1. remove Promise and raw platform bridges inside individual tool bodies
|
|
||||||
2. swap tool internals to Effect-native services like `AppFileSystem`, `HttpClient`, and `ChildProcessSpawner`
|
|
||||||
3. keep tests and callers aligned with `yield* info.init()` and real service graphs
|
|
||||||
|
|
||||||
## Current shape
|
|
||||||
|
|
||||||
`Tool.define(...)` is already the Effect-native helper here.
|
|
||||||
|
|
||||||
- `init` is an `Effect`
|
|
||||||
- `info.init()` returns an `Effect`
|
|
||||||
- `execute(...)` returns an `Effect`
|
|
||||||
|
|
||||||
That means a tool does not need a separate `Tool.defineEffect(...)` helper to count as migrated. A tool is effectively migrated when its init and execute path stay Effect-native, even if some internals still bridge to Promise-based or raw APIs.
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
Tool tests should use the existing Effect helpers in `packages/opencode/test/lib/effect.ts`:
|
|
||||||
|
|
||||||
- Use `testEffect(...)` / `it.live(...)` instead of creating fake local wrappers around effectful tools.
|
|
||||||
- Yield the real tool export, then initialize it: `const info = yield* ReadTool`, `const tool = yield* info.init()`.
|
|
||||||
- Run tests inside a real instance with `provideTmpdirInstance(...)` or `provideInstance(tmpdirScoped(...))` so instance-scoped services resolve exactly as they do in production.
|
|
||||||
|
|
||||||
This keeps tool tests aligned with the production service graph and makes follow-up cleanup mostly mechanical.
|
|
||||||
|
|
||||||
## Exported tools
|
|
||||||
|
|
||||||
These exported tool definitions already exist in `src/tool` and are on the current Effect-native `Tool.define(...)` path:
|
|
||||||
|
|
||||||
- [x] `apply_patch.ts`
|
|
||||||
- [x] `bash.ts`
|
|
||||||
- [x] `codesearch.ts`
|
|
||||||
- [x] `edit.ts`
|
|
||||||
- [x] `glob.ts`
|
|
||||||
- [x] `grep.ts`
|
|
||||||
- [x] `invalid.ts`
|
|
||||||
- [x] `ls.ts`
|
|
||||||
- [x] `lsp.ts`
|
|
||||||
- [x] `multiedit.ts`
|
|
||||||
- [x] `plan.ts`
|
|
||||||
- [x] `question.ts`
|
|
||||||
- [x] `read.ts`
|
|
||||||
- [x] `skill.ts`
|
|
||||||
- [x] `task.ts`
|
|
||||||
- [x] `todo.ts`
|
|
||||||
- [x] `webfetch.ts`
|
|
||||||
- [x] `websearch.ts`
|
|
||||||
- [x] `write.ts`
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
|
|
||||||
- `batch.ts` is no longer a current tool file and should not be tracked here.
|
|
||||||
- `truncate.ts` is an Effect service used by tools, not a tool definition itself.
|
|
||||||
- `mcp-exa.ts`, `external-directory.ts`, and `schema.ts` are support modules, not standalone tool definitions.
|
|
||||||
|
|
||||||
## Follow-up cleanup
|
|
||||||
|
|
||||||
Most exported tools are already on the intended Effect-native shape. The remaining cleanup is narrower than the old checklist implied.
|
|
||||||
|
|
||||||
Current spot cleanups worth tracking:
|
|
||||||
|
|
||||||
- [ ] `read.ts` — still bridges to Node stream / `readline` helpers and Promise-based binary detection
|
|
||||||
- [ ] `bash.ts` — already uses Effect child-process primitives; only keep tracking shell-specific platform bridges and parser/loading details as they come up
|
|
||||||
- [ ] `webfetch.ts` — already uses `HttpClient`; remaining work is limited to smaller boundary helpers like HTML text extraction
|
|
||||||
- [ ] `file/ripgrep.ts` — adjacent to tool migration; still has raw fs/process usage that affects `grep.ts` and `ls.ts`
|
|
||||||
- [ ] `patch/index.ts` — adjacent to tool migration; still has raw fs usage behind patch application
|
|
||||||
|
|
||||||
Notable items that are already effectively on the target path and do not need separate migration bullets right now:
|
|
||||||
|
|
||||||
- `apply_patch.ts`
|
|
||||||
- `grep.ts`
|
|
||||||
- `write.ts`
|
|
||||||
- `codesearch.ts`
|
|
||||||
- `websearch.ts`
|
|
||||||
- `ls.ts`
|
|
||||||
- `multiedit.ts`
|
|
||||||
- `edit.ts`
|
|
||||||
|
|
||||||
## Filesystem notes
|
|
||||||
|
|
||||||
Current raw fs users that still appear relevant here:
|
|
||||||
|
|
||||||
- `tool/read.ts` — `fs.createReadStream`, `readline`
|
|
||||||
- `file/ripgrep.ts` — `fs/promises`
|
|
||||||
- `patch/index.ts` — `fs`, `fs/promises`
|
|
||||||
@@ -40,7 +40,6 @@ import type { ACPConfig } from "./types"
|
|||||||
import { Provider } from "../provider/provider"
|
import { Provider } from "../provider/provider"
|
||||||
import { ModelID, ProviderID } from "../provider/schema"
|
import { ModelID, ProviderID } from "../provider/schema"
|
||||||
import { Agent as AgentModule } from "../agent/agent"
|
import { Agent as AgentModule } from "../agent/agent"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
import { Installation } from "@/installation"
|
import { Installation } from "@/installation"
|
||||||
import { MessageV2 } from "@/session/message-v2"
|
import { MessageV2 } from "@/session/message-v2"
|
||||||
import { Config } from "@/config/config"
|
import { Config } from "@/config/config"
|
||||||
@@ -453,12 +452,19 @@ export namespace ACP {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ACP clients already know the prompt they just submitted, so replaying
|
|
||||||
// live user parts duplicates the message. We still replay user history in
|
|
||||||
// loadSession() and forkSession() via processMessage().
|
|
||||||
if (part.type !== "text" && part.type !== "file") return
|
if (part.type !== "text" && part.type !== "file") return
|
||||||
|
const msg = await this.sdk.session
|
||||||
|
.message(
|
||||||
|
{ sessionID: part.sessionID, messageID: part.messageID, directory: session.cwd },
|
||||||
|
{ throwOnError: true },
|
||||||
|
)
|
||||||
|
.then((x) => x.data)
|
||||||
|
.catch((err) => {
|
||||||
|
log.error("failed to fetch message for user chunk", { error: err })
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
if (!msg || msg.info.role !== "user") return
|
||||||
|
await this.processMessage({ info: msg.info, parts: [part] })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1160,7 +1166,7 @@ export namespace ACP {
|
|||||||
this.sessionManager.get(sessionId).modeId ||
|
this.sessionManager.get(sessionId).modeId ||
|
||||||
(await (async () => {
|
(await (async () => {
|
||||||
if (!availableModes.length) return undefined
|
if (!availableModes.length) return undefined
|
||||||
const defaultAgentName = await AppRuntime.runPromise(AgentModule.Service.use((svc) => svc.defaultAgent()))
|
const defaultAgentName = await AgentModule.defaultAgent()
|
||||||
const resolvedModeId =
|
const resolvedModeId =
|
||||||
availableModes.find((mode) => mode.name === defaultAgentName)?.id ?? availableModes[0].id
|
availableModes.find((mode) => mode.name === defaultAgentName)?.id ?? availableModes[0].id
|
||||||
this.sessionManager.setMode(sessionId, resolvedModeId)
|
this.sessionManager.setMode(sessionId, resolvedModeId)
|
||||||
@@ -1361,8 +1367,7 @@ export namespace ACP {
|
|||||||
if (!current) {
|
if (!current) {
|
||||||
this.sessionManager.setModel(session.id, model)
|
this.sessionManager.setModel(session.id, model)
|
||||||
}
|
}
|
||||||
const agent =
|
const agent = session.modeId ?? (await AgentModule.defaultAgent())
|
||||||
session.modeId ?? (await AppRuntime.runPromise(AgentModule.Service.use((svc) => svc.defaultAgent())))
|
|
||||||
|
|
||||||
const parts: Array<
|
const parts: Array<
|
||||||
| { type: "text"; text: string; synthetic?: boolean; ignored?: boolean }
|
| { type: "text"; text: string; synthetic?: boolean; ignored?: boolean }
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { Plugin } from "@/plugin"
|
|||||||
import { Skill } from "../skill"
|
import { Skill } from "../skill"
|
||||||
import { Effect, Context, Layer } from "effect"
|
import { Effect, Context, Layer } from "effect"
|
||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
|
import { makeRuntime } from "@/effect/run-service"
|
||||||
|
|
||||||
export namespace Agent {
|
export namespace Agent {
|
||||||
export const Info = z
|
export const Info = z
|
||||||
@@ -73,7 +74,6 @@ export namespace Agent {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
const auth = yield* Auth.Service
|
const auth = yield* Auth.Service
|
||||||
const plugin = yield* Plugin.Service
|
|
||||||
const skill = yield* Skill.Service
|
const skill = yield* Skill.Service
|
||||||
const provider = yield* Provider.Service
|
const provider = yield* Provider.Service
|
||||||
|
|
||||||
@@ -336,7 +336,9 @@ export namespace Agent {
|
|||||||
const language = yield* provider.getLanguage(resolved)
|
const language = yield* provider.getLanguage(resolved)
|
||||||
|
|
||||||
const system = [PROMPT_GENERATE]
|
const system = [PROMPT_GENERATE]
|
||||||
yield* plugin.trigger("experimental.chat.system.transform", { model: resolved }, { system })
|
yield* Effect.promise(() =>
|
||||||
|
Plugin.trigger("experimental.chat.system.transform", { model: resolved }, { system }),
|
||||||
|
)
|
||||||
const existing = yield* InstanceState.useEffect(state, (s) => s.list())
|
const existing = yield* InstanceState.useEffect(state, (s) => s.list())
|
||||||
|
|
||||||
// TODO: clean this up so provider specific logic doesnt bleed over
|
// TODO: clean this up so provider specific logic doesnt bleed over
|
||||||
@@ -397,10 +399,27 @@ export namespace Agent {
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(
|
export const defaultLayer = layer.pipe(
|
||||||
Layer.provide(Plugin.defaultLayer),
|
|
||||||
Layer.provide(Provider.defaultLayer),
|
Layer.provide(Provider.defaultLayer),
|
||||||
Layer.provide(Auth.defaultLayer),
|
Layer.provide(Auth.defaultLayer),
|
||||||
Layer.provide(Config.defaultLayer),
|
Layer.provide(Config.defaultLayer),
|
||||||
Layer.provide(Skill.defaultLayer),
|
Layer.provide(Skill.defaultLayer),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
||||||
|
export async function get(agent: string) {
|
||||||
|
return runPromise((svc) => svc.get(agent))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function list() {
|
||||||
|
return runPromise((svc) => svc.list())
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function defaultAgent() {
|
||||||
|
return runPromise((svc) => svc.defaultAgent())
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generate(input: { description: string; model?: { providerID: ProviderID; modelID: ModelID } }) {
|
||||||
|
return runPromise((svc) => svc.generate(input))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
-4
@@ -1,4 +0,0 @@
|
|||||||
declare module "*.wav" {
|
|
||||||
const file: string
|
|
||||||
export default file
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect, Layer, Record, Result, Schema, Context } from "effect"
|
import { Effect, Layer, Record, Result, Schema, Context } from "effect"
|
||||||
|
import { makeRuntime } from "@/effect/run-service"
|
||||||
import { zod } from "@/util/effect-zod"
|
import { zod } from "@/util/effect-zod"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
import { AppFileSystem } from "../filesystem"
|
import { AppFileSystem } from "../filesystem"
|
||||||
@@ -88,4 +89,22 @@ export namespace Auth {
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
|
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
|
||||||
|
|
||||||
|
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||||
|
|
||||||
|
export async function get(providerID: string) {
|
||||||
|
return runPromise((service) => service.get(providerID))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function all(): Promise<Record<string, Info>> {
|
||||||
|
return runPromise((service) => service.all())
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function set(key: string, info: Info) {
|
||||||
|
return runPromise((service) => service.set(key, info))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function remove(key: string) {
|
||||||
|
return runPromise((service) => service.remove(key))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,18 +16,25 @@ export namespace BusEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function payloads() {
|
export function payloads() {
|
||||||
return registry
|
return z
|
||||||
.entries()
|
.discriminatedUnion(
|
||||||
.map(([type, def]) => {
|
"type",
|
||||||
return z
|
registry
|
||||||
.object({
|
.entries()
|
||||||
type: z.literal(type),
|
.map(([type, def]) => {
|
||||||
properties: def.properties,
|
return z
|
||||||
})
|
.object({
|
||||||
.meta({
|
type: z.literal(type),
|
||||||
ref: "Event" + "." + def.type,
|
properties: def.properties,
|
||||||
|
})
|
||||||
|
.meta({
|
||||||
|
ref: "Event" + "." + def.type,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
.toArray() as any,
|
||||||
|
)
|
||||||
|
.meta({
|
||||||
|
ref: "Event",
|
||||||
})
|
})
|
||||||
.toArray()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
import { InstanceBootstrap } from "../project/bootstrap"
|
import { InstanceBootstrap } from "../project/bootstrap"
|
||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
|
|
||||||
export async function bootstrap<T>(directory: string, cb: () => Promise<T>) {
|
export async function bootstrap<T>(directory: string, cb: () => Promise<T>) {
|
||||||
return Instance.provide({
|
return Instance.provide({
|
||||||
directory,
|
directory,
|
||||||
init: () => AppRuntime.runPromise(InstanceBootstrap),
|
init: InstanceBootstrap,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
try {
|
try {
|
||||||
const result = await cb()
|
const result = await cb()
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
import * as prompts from "@clack/prompts"
|
import * as prompts from "@clack/prompts"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import { Global } from "../../global"
|
import { Global } from "../../global"
|
||||||
import { Agent } from "../../agent/agent"
|
import { Agent } from "../../agent/agent"
|
||||||
@@ -111,9 +110,7 @@ const AgentCreateCommand = cmd({
|
|||||||
const spinner = prompts.spinner()
|
const spinner = prompts.spinner()
|
||||||
spinner.start("Generating agent configuration...")
|
spinner.start("Generating agent configuration...")
|
||||||
const model = args.model ? Provider.parseModel(args.model) : undefined
|
const model = args.model ? Provider.parseModel(args.model) : undefined
|
||||||
const generated = await AppRuntime.runPromise(
|
const generated = await Agent.generate({ description, model }).catch((error) => {
|
||||||
Agent.Service.use((svc) => svc.generate({ description, model })),
|
|
||||||
).catch((error) => {
|
|
||||||
spinner.stop(`LLM failed to generate agent: ${error.message}`, 1)
|
spinner.stop(`LLM failed to generate agent: ${error.message}`, 1)
|
||||||
if (isFullyNonInteractive) process.exit(1)
|
if (isFullyNonInteractive) process.exit(1)
|
||||||
throw new UI.CancelledError()
|
throw new UI.CancelledError()
|
||||||
@@ -223,7 +220,7 @@ const AgentListCommand = cmd({
|
|||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: process.cwd(),
|
directory: process.cwd(),
|
||||||
async fn() {
|
async fn() {
|
||||||
const agents = await AppRuntime.runPromise(Agent.Service.use((svc) => svc.list()))
|
const agents = await Agent.list()
|
||||||
const sortedAgents = agents.sort((a, b) => {
|
const sortedAgents = agents.sort((a, b) => {
|
||||||
if (a.native !== b.native) {
|
if (a.native !== b.native) {
|
||||||
return a.native ? -1 : 1
|
return a.native ? -1 : 1
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { Permission } from "../../../permission"
|
|||||||
import { iife } from "../../../util/iife"
|
import { iife } from "../../../util/iife"
|
||||||
import { bootstrap } from "../../bootstrap"
|
import { bootstrap } from "../../bootstrap"
|
||||||
import { cmd } from "../cmd"
|
import { cmd } from "../cmd"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
|
|
||||||
export const AgentCommand = cmd({
|
export const AgentCommand = cmd({
|
||||||
command: "agent <name>",
|
command: "agent <name>",
|
||||||
@@ -35,7 +34,7 @@ export const AgentCommand = cmd({
|
|||||||
async handler(args) {
|
async handler(args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
const agentName = args.name as string
|
const agentName = args.name as string
|
||||||
const agent = await AppRuntime.runPromise(Agent.Service.use((svc) => svc.get(agentName)))
|
const agent = await Agent.get(agentName)
|
||||||
if (!agent) {
|
if (!agent) {
|
||||||
process.stderr.write(
|
process.stderr.write(
|
||||||
`Agent ${agentName} not found, run '${basename(process.execPath)} agent list' to get an agent list` + EOL,
|
`Agent ${agentName} not found, run '${basename(process.execPath)} agent list' to get an agent list` + EOL,
|
||||||
@@ -72,17 +71,11 @@ export const AgentCommand = cmd({
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function getAvailableTools(agent: Agent.Info) {
|
async function getAvailableTools(agent: Agent.Info) {
|
||||||
return AppRuntime.runPromise(
|
const model = agent.model ?? (await Provider.defaultModel())
|
||||||
Effect.gen(function* () {
|
return ToolRegistry.tools({
|
||||||
const provider = yield* Provider.Service
|
...model,
|
||||||
const registry = yield* ToolRegistry.Service
|
agent,
|
||||||
const model = agent.model ?? (yield* provider.defaultModel())
|
})
|
||||||
return yield* registry.tools({
|
|
||||||
...model,
|
|
||||||
agent,
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveTools(agent: Agent.Info, availableTools: Awaited<ReturnType<typeof getAvailableTools>>) {
|
async function resolveTools(agent: Agent.Info, availableTools: Awaited<ReturnType<typeof getAvailableTools>>) {
|
||||||
@@ -123,49 +116,38 @@ function parseToolParams(input?: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createToolContext(agent: Agent.Info) {
|
async function createToolContext(agent: Agent.Info) {
|
||||||
const { session, messageID } = await AppRuntime.runPromise(
|
const session = await Session.create({ title: `Debug tool run (${agent.name})` })
|
||||||
Effect.gen(function* () {
|
const messageID = MessageID.ascending()
|
||||||
const session = yield* Session.Service
|
const model = agent.model ?? (await Provider.defaultModel())
|
||||||
const result = yield* session.create({ title: `Debug tool run (${agent.name})` })
|
const now = Date.now()
|
||||||
const messageID = MessageID.ascending()
|
const message: MessageV2.Assistant = {
|
||||||
const model = agent.model
|
id: messageID,
|
||||||
? agent.model
|
sessionID: session.id,
|
||||||
: yield* Effect.gen(function* () {
|
role: "assistant",
|
||||||
const provider = yield* Provider.Service
|
time: {
|
||||||
return yield* provider.defaultModel()
|
created: now,
|
||||||
})
|
},
|
||||||
const now = Date.now()
|
parentID: messageID,
|
||||||
const message: MessageV2.Assistant = {
|
modelID: model.modelID,
|
||||||
id: messageID,
|
providerID: model.providerID,
|
||||||
sessionID: result.id,
|
mode: "debug",
|
||||||
role: "assistant",
|
agent: agent.name,
|
||||||
time: {
|
path: {
|
||||||
created: now,
|
cwd: Instance.directory,
|
||||||
},
|
root: Instance.worktree,
|
||||||
parentID: messageID,
|
},
|
||||||
modelID: model.modelID,
|
cost: 0,
|
||||||
providerID: model.providerID,
|
tokens: {
|
||||||
mode: "debug",
|
input: 0,
|
||||||
agent: agent.name,
|
output: 0,
|
||||||
path: {
|
reasoning: 0,
|
||||||
cwd: Instance.directory,
|
cache: {
|
||||||
root: Instance.worktree,
|
read: 0,
|
||||||
},
|
write: 0,
|
||||||
cost: 0,
|
},
|
||||||
tokens: {
|
},
|
||||||
input: 0,
|
}
|
||||||
output: 0,
|
await Session.updateMessage(message)
|
||||||
reasoning: 0,
|
|
||||||
cache: {
|
|
||||||
read: 0,
|
|
||||||
write: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
yield* session.updateMessage(message)
|
|
||||||
return { session: result, messageID }
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const ruleset = Permission.merge(agent.permission, session.permission ?? [])
|
const ruleset = Permission.merge(agent.permission, session.permission ?? [])
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { Config } from "../../../config/config"
|
import { Config } from "../../../config/config"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
import { bootstrap } from "../../bootstrap"
|
import { bootstrap } from "../../bootstrap"
|
||||||
import { cmd } from "../cmd"
|
import { cmd } from "../cmd"
|
||||||
|
|
||||||
@@ -10,7 +9,7 @@ export const ConfigCommand = cmd({
|
|||||||
builder: (yargs) => yargs,
|
builder: (yargs) => yargs,
|
||||||
async handler() {
|
async handler() {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get()))
|
const config = await Config.get()
|
||||||
process.stdout.write(JSON.stringify(config, null, 2) + EOL)
|
process.stdout.write(JSON.stringify(config, null, 2) + EOL)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { Effect } from "effect"
|
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
import { File } from "../../../file"
|
import { File } from "../../../file"
|
||||||
import { bootstrap } from "../../bootstrap"
|
import { bootstrap } from "../../bootstrap"
|
||||||
import { cmd } from "../cmd"
|
import { cmd } from "../cmd"
|
||||||
@@ -17,11 +15,7 @@ const FileSearchCommand = cmd({
|
|||||||
}),
|
}),
|
||||||
async handler(args) {
|
async handler(args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
const results = await AppRuntime.runPromise(
|
const results = await File.search({ query: args.query })
|
||||||
Effect.gen(function* () {
|
|
||||||
return yield* File.Service.use((svc) => svc.search({ query: args.query }))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
process.stdout.write(results.join(EOL) + EOL)
|
process.stdout.write(results.join(EOL) + EOL)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -38,11 +32,7 @@ const FileReadCommand = cmd({
|
|||||||
}),
|
}),
|
||||||
async handler(args) {
|
async handler(args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
const content = await AppRuntime.runPromise(
|
const content = await File.read(args.path)
|
||||||
Effect.gen(function* () {
|
|
||||||
return yield* File.Service.use((svc) => svc.read(args.path))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
process.stdout.write(JSON.stringify(content, null, 2) + EOL)
|
process.stdout.write(JSON.stringify(content, null, 2) + EOL)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -54,11 +44,7 @@ const FileStatusCommand = cmd({
|
|||||||
builder: (yargs) => yargs,
|
builder: (yargs) => yargs,
|
||||||
async handler() {
|
async handler() {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
const status = await AppRuntime.runPromise(
|
const status = await File.status()
|
||||||
Effect.gen(function* () {
|
|
||||||
return yield* File.Service.use((svc) => svc.status())
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
process.stdout.write(JSON.stringify(status, null, 2) + EOL)
|
process.stdout.write(JSON.stringify(status, null, 2) + EOL)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -75,11 +61,7 @@ const FileListCommand = cmd({
|
|||||||
}),
|
}),
|
||||||
async handler(args) {
|
async handler(args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
const files = await AppRuntime.runPromise(
|
const files = await File.list(args.path)
|
||||||
Effect.gen(function* () {
|
|
||||||
return yield* File.Service.use((svc) => svc.list(args.path))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
process.stdout.write(JSON.stringify(files, null, 2) + EOL)
|
process.stdout.write(JSON.stringify(files, null, 2) + EOL)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { LSP } from "../../../lsp"
|
import { LSP } from "../../../lsp"
|
||||||
import { AppRuntime } from "../../../effect/app-runtime"
|
|
||||||
import { Effect } from "effect"
|
|
||||||
import { bootstrap } from "../../bootstrap"
|
import { bootstrap } from "../../bootstrap"
|
||||||
import { cmd } from "../cmd"
|
import { cmd } from "../cmd"
|
||||||
import { Log } from "../../../util/log"
|
import { Log } from "../../../util/log"
|
||||||
@@ -21,16 +19,9 @@ const DiagnosticsCommand = cmd({
|
|||||||
builder: (yargs) => yargs.positional("file", { type: "string", demandOption: true }),
|
builder: (yargs) => yargs.positional("file", { type: "string", demandOption: true }),
|
||||||
async handler(args) {
|
async handler(args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
const out = await AppRuntime.runPromise(
|
await LSP.touchFile(args.file, true)
|
||||||
LSP.Service.use((lsp) =>
|
await sleep(1000)
|
||||||
Effect.gen(function* () {
|
process.stdout.write(JSON.stringify(await LSP.diagnostics(), null, 2) + EOL)
|
||||||
yield* lsp.touchFile(args.file, true)
|
|
||||||
yield* Effect.sleep(1000)
|
|
||||||
return yield* lsp.diagnostics()
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
process.stdout.write(JSON.stringify(out, null, 2) + EOL)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -42,7 +33,7 @@ export const SymbolsCommand = cmd({
|
|||||||
async handler(args) {
|
async handler(args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
using _ = Log.Default.time("symbols")
|
using _ = Log.Default.time("symbols")
|
||||||
const results = await AppRuntime.runPromise(LSP.Service.use((lsp) => lsp.workspaceSymbol(args.query)))
|
const results = await LSP.workspaceSymbol(args.query)
|
||||||
process.stdout.write(JSON.stringify(results, null, 2) + EOL)
|
process.stdout.write(JSON.stringify(results, null, 2) + EOL)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -55,7 +46,7 @@ export const DocumentSymbolsCommand = cmd({
|
|||||||
async handler(args) {
|
async handler(args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
using _ = Log.Default.time("document-symbols")
|
using _ = Log.Default.time("document-symbols")
|
||||||
const results = await AppRuntime.runPromise(LSP.Service.use((lsp) => lsp.documentSymbol(args.uri)))
|
const results = await LSP.documentSymbol(args.uri)
|
||||||
process.stdout.write(JSON.stringify(results, null, 2) + EOL)
|
process.stdout.write(JSON.stringify(results, null, 2) + EOL)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { AppRuntime } from "../../../effect/app-runtime"
|
|
||||||
import { Ripgrep } from "../../../file/ripgrep"
|
import { Ripgrep } from "../../../file/ripgrep"
|
||||||
import { Instance } from "../../../project/instance"
|
import { Instance } from "../../../project/instance"
|
||||||
import { bootstrap } from "../../bootstrap"
|
import { bootstrap } from "../../bootstrap"
|
||||||
@@ -46,7 +45,7 @@ const FilesCommand = cmd({
|
|||||||
async handler(args) {
|
async handler(args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
const files: string[] = []
|
const files: string[] = []
|
||||||
for await (const file of await Ripgrep.files({
|
for await (const file of Ripgrep.files({
|
||||||
cwd: Instance.directory,
|
cwd: Instance.directory,
|
||||||
glob: args.glob ? [args.glob] : undefined,
|
glob: args.glob ? [args.glob] : undefined,
|
||||||
})) {
|
})) {
|
||||||
@@ -77,18 +76,12 @@ const SearchCommand = cmd({
|
|||||||
description: "Limit number of results",
|
description: "Limit number of results",
|
||||||
}),
|
}),
|
||||||
async handler(args) {
|
async handler(args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
const results = await Ripgrep.search({
|
||||||
const results = await AppRuntime.runPromise(
|
cwd: process.cwd(),
|
||||||
Ripgrep.Service.use((svc) =>
|
pattern: args.pattern,
|
||||||
svc.search({
|
glob: args.glob as string[] | undefined,
|
||||||
cwd: Instance.directory,
|
limit: args.limit,
|
||||||
pattern: args.pattern,
|
|
||||||
glob: args.glob as string[] | undefined,
|
|
||||||
limit: args.limit,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
process.stdout.write(JSON.stringify(results.items, null, 2) + EOL)
|
|
||||||
})
|
})
|
||||||
|
process.stdout.write(JSON.stringify(results, null, 2) + EOL)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { Effect } from "effect"
|
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
import { Skill } from "../../../skill"
|
import { Skill } from "../../../skill"
|
||||||
import { bootstrap } from "../../bootstrap"
|
import { bootstrap } from "../../bootstrap"
|
||||||
import { cmd } from "../cmd"
|
import { cmd } from "../cmd"
|
||||||
@@ -11,12 +9,7 @@ export const SkillCommand = cmd({
|
|||||||
builder: (yargs) => yargs,
|
builder: (yargs) => yargs,
|
||||||
async handler() {
|
async handler() {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
const skills = await AppRuntime.runPromise(
|
const skills = await Skill.all()
|
||||||
Effect.gen(function* () {
|
|
||||||
const skill = yield* Skill.Service
|
|
||||||
return yield* skill.all()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
process.stdout.write(JSON.stringify(skills, null, 2) + EOL)
|
process.stdout.write(JSON.stringify(skills, null, 2) + EOL)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
import { Snapshot } from "../../../snapshot"
|
import { Snapshot } from "../../../snapshot"
|
||||||
import { bootstrap } from "../../bootstrap"
|
import { bootstrap } from "../../bootstrap"
|
||||||
import { cmd } from "../cmd"
|
import { cmd } from "../cmd"
|
||||||
@@ -15,7 +14,7 @@ const TrackCommand = cmd({
|
|||||||
describe: "track current snapshot state",
|
describe: "track current snapshot state",
|
||||||
async handler() {
|
async handler() {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
console.log(await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track())))
|
console.log(await Snapshot.track())
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -31,7 +30,7 @@ const PatchCommand = cmd({
|
|||||||
}),
|
}),
|
||||||
async handler(args) {
|
async handler(args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
console.log(await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(args.hash))))
|
console.log(await Snapshot.patch(args.hash))
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -47,7 +46,7 @@ const DiffCommand = cmd({
|
|||||||
}),
|
}),
|
||||||
async handler(args) {
|
async handler(args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
console.log(await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.diff(args.hash))))
|
console.log(await Snapshot.diff(args.hash))
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,238 +1,20 @@
|
|||||||
import type { Argv } from "yargs"
|
import type { Argv } from "yargs"
|
||||||
import { Session } from "../../session"
|
import { Session } from "../../session"
|
||||||
import { MessageV2 } from "../../session/message-v2"
|
|
||||||
import { SessionID } from "../../session/schema"
|
import { SessionID } from "../../session/schema"
|
||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
import { bootstrap } from "../bootstrap"
|
import { bootstrap } from "../bootstrap"
|
||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import * as prompts from "@clack/prompts"
|
import * as prompts from "@clack/prompts"
|
||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
|
|
||||||
function redact(kind: string, id: string, value: string) {
|
|
||||||
return value.trim() ? `[redacted:${kind}:${id}]` : value
|
|
||||||
}
|
|
||||||
|
|
||||||
function data(kind: string, id: string, value: Record<string, unknown> | undefined) {
|
|
||||||
if (!value) return value
|
|
||||||
return Object.keys(value).length ? { redacted: `${kind}:${id}` } : value
|
|
||||||
}
|
|
||||||
|
|
||||||
function span(id: string, value: { value: string; start: number; end: number }) {
|
|
||||||
return {
|
|
||||||
...value,
|
|
||||||
value: redact("file-text", id, value.value),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function diff(kind: string, diffs: { file: string; patch: string }[] | undefined) {
|
|
||||||
return diffs?.map((item, i) => ({
|
|
||||||
...item,
|
|
||||||
file: redact(`${kind}-file`, String(i), item.file),
|
|
||||||
patch: redact(`${kind}-patch`, String(i), item.patch),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
function source(part: MessageV2.FilePart) {
|
|
||||||
if (!part.source) return part.source
|
|
||||||
if (part.source.type === "symbol") {
|
|
||||||
return {
|
|
||||||
...part.source,
|
|
||||||
path: redact("file-path", part.id, part.source.path),
|
|
||||||
name: redact("file-symbol", part.id, part.source.name),
|
|
||||||
text: span(part.id, part.source.text),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (part.source.type === "resource") {
|
|
||||||
return {
|
|
||||||
...part.source,
|
|
||||||
clientName: redact("file-client", part.id, part.source.clientName),
|
|
||||||
uri: redact("file-uri", part.id, part.source.uri),
|
|
||||||
text: span(part.id, part.source.text),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...part.source,
|
|
||||||
path: redact("file-path", part.id, part.source.path),
|
|
||||||
text: span(part.id, part.source.text),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function filepart(part: MessageV2.FilePart): MessageV2.FilePart {
|
|
||||||
return {
|
|
||||||
...part,
|
|
||||||
url: redact("file-url", part.id, part.url),
|
|
||||||
filename: part.filename === undefined ? undefined : redact("file-name", part.id, part.filename),
|
|
||||||
source: source(part),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function part(part: MessageV2.Part): MessageV2.Part {
|
|
||||||
switch (part.type) {
|
|
||||||
case "text":
|
|
||||||
return {
|
|
||||||
...part,
|
|
||||||
text: redact("text", part.id, part.text),
|
|
||||||
metadata: data("text-metadata", part.id, part.metadata),
|
|
||||||
}
|
|
||||||
case "reasoning":
|
|
||||||
return {
|
|
||||||
...part,
|
|
||||||
text: redact("reasoning", part.id, part.text),
|
|
||||||
metadata: data("reasoning-metadata", part.id, part.metadata),
|
|
||||||
}
|
|
||||||
case "file":
|
|
||||||
return filepart(part)
|
|
||||||
case "subtask":
|
|
||||||
return {
|
|
||||||
...part,
|
|
||||||
prompt: redact("subtask-prompt", part.id, part.prompt),
|
|
||||||
description: redact("subtask-description", part.id, part.description),
|
|
||||||
command: part.command === undefined ? undefined : redact("subtask-command", part.id, part.command),
|
|
||||||
}
|
|
||||||
case "tool":
|
|
||||||
return {
|
|
||||||
...part,
|
|
||||||
metadata: data("tool-metadata", part.id, part.metadata),
|
|
||||||
state:
|
|
||||||
part.state.status === "pending"
|
|
||||||
? {
|
|
||||||
...part.state,
|
|
||||||
input: data("tool-input", part.id, part.state.input) ?? part.state.input,
|
|
||||||
raw: redact("tool-raw", part.id, part.state.raw),
|
|
||||||
}
|
|
||||||
: part.state.status === "running"
|
|
||||||
? {
|
|
||||||
...part.state,
|
|
||||||
input: data("tool-input", part.id, part.state.input) ?? part.state.input,
|
|
||||||
title: part.state.title === undefined ? undefined : redact("tool-title", part.id, part.state.title),
|
|
||||||
metadata: data("tool-state-metadata", part.id, part.state.metadata),
|
|
||||||
}
|
|
||||||
: part.state.status === "completed"
|
|
||||||
? {
|
|
||||||
...part.state,
|
|
||||||
input: data("tool-input", part.id, part.state.input) ?? part.state.input,
|
|
||||||
output: redact("tool-output", part.id, part.state.output),
|
|
||||||
title: redact("tool-title", part.id, part.state.title),
|
|
||||||
metadata: data("tool-state-metadata", part.id, part.state.metadata) ?? part.state.metadata,
|
|
||||||
attachments: part.state.attachments?.map(filepart),
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
...part.state,
|
|
||||||
input: data("tool-input", part.id, part.state.input) ?? part.state.input,
|
|
||||||
metadata: data("tool-state-metadata", part.id, part.state.metadata),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
case "patch":
|
|
||||||
return {
|
|
||||||
...part,
|
|
||||||
hash: redact("patch", part.id, part.hash),
|
|
||||||
files: part.files.map((item: string, i: number) => redact("patch-file", `${part.id}-${i}`, item)),
|
|
||||||
}
|
|
||||||
case "snapshot":
|
|
||||||
return {
|
|
||||||
...part,
|
|
||||||
snapshot: redact("snapshot", part.id, part.snapshot),
|
|
||||||
}
|
|
||||||
case "step-start":
|
|
||||||
return {
|
|
||||||
...part,
|
|
||||||
snapshot: part.snapshot === undefined ? undefined : redact("snapshot", part.id, part.snapshot),
|
|
||||||
}
|
|
||||||
case "step-finish":
|
|
||||||
return {
|
|
||||||
...part,
|
|
||||||
snapshot: part.snapshot === undefined ? undefined : redact("snapshot", part.id, part.snapshot),
|
|
||||||
}
|
|
||||||
case "agent":
|
|
||||||
return {
|
|
||||||
...part,
|
|
||||||
source: !part.source
|
|
||||||
? part.source
|
|
||||||
: {
|
|
||||||
...part.source,
|
|
||||||
value: redact("agent-source", part.id, part.source.value),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return part
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const partFn = part
|
|
||||||
|
|
||||||
function sanitize(data: { info: Session.Info; messages: MessageV2.WithParts[] }) {
|
|
||||||
return {
|
|
||||||
info: {
|
|
||||||
...data.info,
|
|
||||||
title: redact("session-title", data.info.id, data.info.title),
|
|
||||||
directory: redact("session-directory", data.info.id, data.info.directory),
|
|
||||||
summary: !data.info.summary
|
|
||||||
? data.info.summary
|
|
||||||
: {
|
|
||||||
...data.info.summary,
|
|
||||||
diffs: diff("session-diff", data.info.summary.diffs),
|
|
||||||
},
|
|
||||||
revert: !data.info.revert
|
|
||||||
? data.info.revert
|
|
||||||
: {
|
|
||||||
...data.info.revert,
|
|
||||||
snapshot:
|
|
||||||
data.info.revert.snapshot === undefined
|
|
||||||
? undefined
|
|
||||||
: redact("revert-snapshot", data.info.id, data.info.revert.snapshot),
|
|
||||||
diff:
|
|
||||||
data.info.revert.diff === undefined
|
|
||||||
? undefined
|
|
||||||
: redact("revert-diff", data.info.id, data.info.revert.diff),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
messages: data.messages.map((msg) => ({
|
|
||||||
info:
|
|
||||||
msg.info.role === "user"
|
|
||||||
? {
|
|
||||||
...msg.info,
|
|
||||||
system: msg.info.system === undefined ? undefined : redact("system", msg.info.id, msg.info.system),
|
|
||||||
summary: !msg.info.summary
|
|
||||||
? msg.info.summary
|
|
||||||
: {
|
|
||||||
...msg.info.summary,
|
|
||||||
title:
|
|
||||||
msg.info.summary.title === undefined
|
|
||||||
? undefined
|
|
||||||
: redact("summary-title", msg.info.id, msg.info.summary.title),
|
|
||||||
body:
|
|
||||||
msg.info.summary.body === undefined
|
|
||||||
? undefined
|
|
||||||
: redact("summary-body", msg.info.id, msg.info.summary.body),
|
|
||||||
diffs: diff("message-diff", msg.info.summary.diffs),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
...msg.info,
|
|
||||||
path: {
|
|
||||||
cwd: redact("cwd", msg.info.id, msg.info.path.cwd),
|
|
||||||
root: redact("root", msg.info.id, msg.info.path.root),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
parts: msg.parts.map(partFn),
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ExportCommand = cmd({
|
export const ExportCommand = cmd({
|
||||||
command: "export [sessionID]",
|
command: "export [sessionID]",
|
||||||
describe: "export session data as JSON",
|
describe: "export session data as JSON",
|
||||||
builder: (yargs: Argv) => {
|
builder: (yargs: Argv) => {
|
||||||
return yargs
|
return yargs.positional("sessionID", {
|
||||||
.positional("sessionID", {
|
describe: "session id to export",
|
||||||
describe: "session id to export",
|
type: "string",
|
||||||
type: "string",
|
})
|
||||||
})
|
|
||||||
.option("sanitize", {
|
|
||||||
describe: "redact sensitive transcript and file data",
|
|
||||||
type: "boolean",
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
handler: async (args) => {
|
handler: async (args) => {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
@@ -285,17 +67,18 @@ export const ExportCommand = cmd({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sessionInfo = await AppRuntime.runPromise(Session.Service.use((svc) => svc.get(sessionID!)))
|
const sessionInfo = await Session.get(sessionID!)
|
||||||
const messages = await AppRuntime.runPromise(
|
const messages = await Session.messages({ sessionID: sessionInfo.id })
|
||||||
Session.Service.use((svc) => svc.messages({ sessionID: sessionInfo.id })),
|
|
||||||
)
|
|
||||||
|
|
||||||
const exportData = {
|
const exportData = {
|
||||||
info: sessionInfo,
|
info: sessionInfo,
|
||||||
messages,
|
messages: messages.map((msg) => ({
|
||||||
|
info: msg.info,
|
||||||
|
parts: msg.parts,
|
||||||
|
})),
|
||||||
}
|
}
|
||||||
|
|
||||||
process.stdout.write(JSON.stringify(args.sanitize ? sanitize(exportData) : exportData, null, 2))
|
process.stdout.write(JSON.stringify(exportData, null, 2))
|
||||||
process.stdout.write(EOL)
|
process.stdout.write(EOL)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
UI.error(`Session not found: ${sessionID!}`)
|
UI.error(`Session not found: ${sessionID!}`)
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ import { AppRuntime } from "@/effect/app-runtime"
|
|||||||
import { Git } from "@/git"
|
import { Git } from "@/git"
|
||||||
import { setTimeout as sleep } from "node:timers/promises"
|
import { setTimeout as sleep } from "node:timers/promises"
|
||||||
import { Process } from "@/util/process"
|
import { Process } from "@/util/process"
|
||||||
import { Effect } from "effect"
|
|
||||||
|
|
||||||
type GitHubAuthor = {
|
type GitHubAuthor = {
|
||||||
login: string
|
login: string
|
||||||
@@ -552,24 +551,20 @@ export const GithubRunCommand = cmd({
|
|||||||
|
|
||||||
// Setup opencode session
|
// Setup opencode session
|
||||||
const repoData = await fetchRepo()
|
const repoData = await fetchRepo()
|
||||||
session = await AppRuntime.runPromise(
|
session = await Session.create({
|
||||||
Session.Service.use((svc) =>
|
permission: [
|
||||||
svc.create({
|
{
|
||||||
permission: [
|
permission: "question",
|
||||||
{
|
action: "deny",
|
||||||
permission: "question",
|
pattern: "*",
|
||||||
action: "deny",
|
},
|
||||||
pattern: "*",
|
],
|
||||||
},
|
})
|
||||||
],
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
subscribeSessionEvents()
|
subscribeSessionEvents()
|
||||||
shareId = await (async () => {
|
shareId = await (async () => {
|
||||||
if (share === false) return
|
if (share === false) return
|
||||||
if (!share && repoData.data.private) return
|
if (!share && repoData.data.private) return
|
||||||
await AppRuntime.runPromise(SessionShare.Service.use((svc) => svc.share(session.id)))
|
await SessionShare.share(session.id)
|
||||||
return session.id.slice(-8)
|
return session.id.slice(-8)
|
||||||
})()
|
})()
|
||||||
console.log("opencode session", session.id)
|
console.log("opencode session", session.id)
|
||||||
@@ -942,86 +937,96 @@ export const GithubRunCommand = cmd({
|
|||||||
async function chat(message: string, files: PromptFiles = []) {
|
async function chat(message: string, files: PromptFiles = []) {
|
||||||
console.log("Sending message to opencode...")
|
console.log("Sending message to opencode...")
|
||||||
|
|
||||||
return AppRuntime.runPromise(
|
const result = await SessionPrompt.prompt({
|
||||||
Effect.gen(function* () {
|
sessionID: session.id,
|
||||||
const prompt = yield* SessionPrompt.Service
|
messageID: MessageID.ascending(),
|
||||||
const result = yield* prompt.prompt({
|
variant,
|
||||||
sessionID: session.id,
|
model: {
|
||||||
messageID: MessageID.ascending(),
|
providerID,
|
||||||
variant,
|
modelID,
|
||||||
model: {
|
},
|
||||||
providerID,
|
// agent is omitted - server will use default_agent from config or fall back to "build"
|
||||||
modelID,
|
parts: [
|
||||||
},
|
{
|
||||||
// agent is omitted - server will use default_agent from config or fall back to "build"
|
id: PartID.ascending(),
|
||||||
parts: [
|
type: "text",
|
||||||
{
|
text: message,
|
||||||
id: PartID.ascending(),
|
},
|
||||||
type: "text",
|
...files.flatMap((f) => [
|
||||||
text: message,
|
{
|
||||||
},
|
id: PartID.ascending(),
|
||||||
...files.flatMap((f) => [
|
type: "file" as const,
|
||||||
{
|
mime: f.mime,
|
||||||
id: PartID.ascending(),
|
url: `data:${f.mime};base64,${f.content}`,
|
||||||
type: "file" as const,
|
filename: f.filename,
|
||||||
mime: f.mime,
|
source: {
|
||||||
url: `data:${f.mime};base64,${f.content}`,
|
type: "file" as const,
|
||||||
filename: f.filename,
|
text: {
|
||||||
source: {
|
value: f.replacement,
|
||||||
type: "file" as const,
|
start: f.start,
|
||||||
text: {
|
end: f.end,
|
||||||
value: f.replacement,
|
|
||||||
start: f.start,
|
|
||||||
end: f.end,
|
|
||||||
},
|
|
||||||
path: f.filename,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
]),
|
path: f.filename,
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
if (result.info.role === "assistant" && result.info.error) {
|
|
||||||
const err = result.info.error
|
|
||||||
console.error("Agent error:", err)
|
|
||||||
if (err.name === "ContextOverflowError") throw new Error(formatPromptTooLargeError(files))
|
|
||||||
throw new Error(`${err.name}: ${err.data?.message || ""}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = extractResponseText(result.parts)
|
|
||||||
if (text) return text
|
|
||||||
|
|
||||||
console.log("Requesting summary from agent...")
|
|
||||||
const summary = yield* prompt.prompt({
|
|
||||||
sessionID: session.id,
|
|
||||||
messageID: MessageID.ascending(),
|
|
||||||
variant,
|
|
||||||
model: {
|
|
||||||
providerID,
|
|
||||||
modelID,
|
|
||||||
},
|
|
||||||
tools: { "*": false },
|
|
||||||
parts: [
|
|
||||||
{
|
|
||||||
id: PartID.ascending(),
|
|
||||||
type: "text",
|
|
||||||
text: "Summarize the actions (tool calls & reasoning) you did for the user in 1-2 sentences.",
|
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
})
|
]),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
if (summary.info.role === "assistant" && summary.info.error) {
|
// result should always be assistant just satisfying type checker
|
||||||
const err = summary.info.error
|
if (result.info.role === "assistant" && result.info.error) {
|
||||||
console.error("Summary agent error:", err)
|
const err = result.info.error
|
||||||
if (err.name === "ContextOverflowError") throw new Error(formatPromptTooLargeError(files))
|
console.error("Agent error:", err)
|
||||||
throw new Error(`${err.name}: ${err.data?.message || ""}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const summaryText = extractResponseText(summary.parts)
|
if (err.name === "ContextOverflowError") {
|
||||||
if (!summaryText) throw new Error("Failed to get summary from agent")
|
throw new Error(formatPromptTooLargeError(files))
|
||||||
return summaryText
|
}
|
||||||
}),
|
|
||||||
)
|
const errorMsg = err.data?.message || ""
|
||||||
|
throw new Error(`${err.name}: ${errorMsg}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = extractResponseText(result.parts)
|
||||||
|
if (text) return text
|
||||||
|
|
||||||
|
// No text part (tool-only or reasoning-only) - ask agent to summarize
|
||||||
|
console.log("Requesting summary from agent...")
|
||||||
|
const summary = await SessionPrompt.prompt({
|
||||||
|
sessionID: session.id,
|
||||||
|
messageID: MessageID.ascending(),
|
||||||
|
variant,
|
||||||
|
model: {
|
||||||
|
providerID,
|
||||||
|
modelID,
|
||||||
|
},
|
||||||
|
tools: { "*": false }, // Disable all tools to force text response
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
id: PartID.ascending(),
|
||||||
|
type: "text",
|
||||||
|
text: "Summarize the actions (tool calls & reasoning) you did for the user in 1-2 sentences.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (summary.info.role === "assistant" && summary.info.error) {
|
||||||
|
const err = summary.info.error
|
||||||
|
console.error("Summary agent error:", err)
|
||||||
|
|
||||||
|
if (err.name === "ContextOverflowError") {
|
||||||
|
throw new Error(formatPromptTooLargeError(files))
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorMsg = err.data?.message || ""
|
||||||
|
throw new Error(`${err.name}: ${errorMsg}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const summaryText = extractResponseText(summary.parts)
|
||||||
|
if (!summaryText) {
|
||||||
|
throw new Error("Failed to get summary from agent")
|
||||||
|
}
|
||||||
|
|
||||||
|
return summaryText
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getOidcToken() {
|
async function getOidcToken() {
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ import { Global } from "../../global"
|
|||||||
import { modify, applyEdits } from "jsonc-parser"
|
import { modify, applyEdits } from "jsonc-parser"
|
||||||
import { Filesystem } from "../../util/filesystem"
|
import { Filesystem } from "../../util/filesystem"
|
||||||
import { Bus } from "../../bus"
|
import { Bus } from "../../bus"
|
||||||
import { AppRuntime } from "../../effect/app-runtime"
|
|
||||||
import { Effect } from "effect"
|
|
||||||
|
|
||||||
function getAuthStatusIcon(status: MCP.AuthStatus): string {
|
function getAuthStatusIcon(status: MCP.AuthStatus): string {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@@ -52,47 +50,6 @@ function isMcpRemote(config: McpEntry): config is McpRemote {
|
|||||||
return isMcpConfigured(config) && config.type === "remote"
|
return isMcpConfigured(config) && config.type === "remote"
|
||||||
}
|
}
|
||||||
|
|
||||||
function configuredServers(config: Config.Info) {
|
|
||||||
return Object.entries(config.mcp ?? {}).filter((entry): entry is [string, McpConfigured] => isMcpConfigured(entry[1]))
|
|
||||||
}
|
|
||||||
|
|
||||||
function oauthServers(config: Config.Info) {
|
|
||||||
return configuredServers(config).filter(
|
|
||||||
(entry): entry is [string, McpRemote] => isMcpRemote(entry[1]) && entry[1].oauth !== false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function listState() {
|
|
||||||
return AppRuntime.runPromise(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const cfg = yield* Config.Service
|
|
||||||
const mcp = yield* MCP.Service
|
|
||||||
const config = yield* cfg.get()
|
|
||||||
const statuses = yield* mcp.status()
|
|
||||||
const stored = yield* Effect.all(
|
|
||||||
Object.fromEntries(configuredServers(config).map(([name]) => [name, mcp.hasStoredTokens(name)])),
|
|
||||||
{ concurrency: "unbounded" },
|
|
||||||
)
|
|
||||||
return { config, statuses, stored }
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function authState() {
|
|
||||||
return AppRuntime.runPromise(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const cfg = yield* Config.Service
|
|
||||||
const mcp = yield* MCP.Service
|
|
||||||
const config = yield* cfg.get()
|
|
||||||
const auth = yield* Effect.all(
|
|
||||||
Object.fromEntries(oauthServers(config).map(([name]) => [name, mcp.getAuthStatus(name)])),
|
|
||||||
{ concurrency: "unbounded" },
|
|
||||||
)
|
|
||||||
return { config, auth }
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const McpCommand = cmd({
|
export const McpCommand = cmd({
|
||||||
command: "mcp",
|
command: "mcp",
|
||||||
describe: "manage MCP (Model Context Protocol) servers",
|
describe: "manage MCP (Model Context Protocol) servers",
|
||||||
@@ -118,8 +75,13 @@ export const McpListCommand = cmd({
|
|||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("MCP Servers")
|
prompts.intro("MCP Servers")
|
||||||
|
|
||||||
const { config, statuses, stored } = await listState()
|
const config = await Config.get()
|
||||||
const servers = configuredServers(config)
|
const mcpServers = config.mcp ?? {}
|
||||||
|
const statuses = await MCP.status()
|
||||||
|
|
||||||
|
const servers = Object.entries(mcpServers).filter((entry): entry is [string, McpConfigured] =>
|
||||||
|
isMcpConfigured(entry[1]),
|
||||||
|
)
|
||||||
|
|
||||||
if (servers.length === 0) {
|
if (servers.length === 0) {
|
||||||
prompts.log.warn("No MCP servers configured")
|
prompts.log.warn("No MCP servers configured")
|
||||||
@@ -130,7 +92,7 @@ export const McpListCommand = cmd({
|
|||||||
for (const [name, serverConfig] of servers) {
|
for (const [name, serverConfig] of servers) {
|
||||||
const status = statuses[name]
|
const status = statuses[name]
|
||||||
const hasOAuth = isMcpRemote(serverConfig) && !!serverConfig.oauth
|
const hasOAuth = isMcpRemote(serverConfig) && !!serverConfig.oauth
|
||||||
const hasStoredTokens = stored[name]
|
const hasStoredTokens = await MCP.hasStoredTokens(name)
|
||||||
|
|
||||||
let statusIcon: string
|
let statusIcon: string
|
||||||
let statusText: string
|
let statusText: string
|
||||||
@@ -190,11 +152,15 @@ export const McpAuthCommand = cmd({
|
|||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("MCP OAuth Authentication")
|
prompts.intro("MCP OAuth Authentication")
|
||||||
|
|
||||||
const { config, auth } = await authState()
|
const config = await Config.get()
|
||||||
const mcpServers = config.mcp ?? {}
|
const mcpServers = config.mcp ?? {}
|
||||||
const servers = oauthServers(config)
|
|
||||||
|
|
||||||
if (servers.length === 0) {
|
// Get OAuth-capable servers (remote servers with oauth not explicitly disabled)
|
||||||
|
const oauthServers = Object.entries(mcpServers).filter(
|
||||||
|
(entry): entry is [string, McpRemote] => isMcpRemote(entry[1]) && entry[1].oauth !== false,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (oauthServers.length === 0) {
|
||||||
prompts.log.warn("No OAuth-capable MCP servers configured")
|
prompts.log.warn("No OAuth-capable MCP servers configured")
|
||||||
prompts.log.info("Remote MCP servers support OAuth by default. Add a remote server in opencode.json:")
|
prompts.log.info("Remote MCP servers support OAuth by default. Add a remote server in opencode.json:")
|
||||||
prompts.log.info(`
|
prompts.log.info(`
|
||||||
@@ -211,17 +177,19 @@ export const McpAuthCommand = cmd({
|
|||||||
let serverName = args.name
|
let serverName = args.name
|
||||||
if (!serverName) {
|
if (!serverName) {
|
||||||
// Build options with auth status
|
// Build options with auth status
|
||||||
const options = servers.map(([name, cfg]) => {
|
const options = await Promise.all(
|
||||||
const authStatus = auth[name]
|
oauthServers.map(async ([name, cfg]) => {
|
||||||
const icon = getAuthStatusIcon(authStatus)
|
const authStatus = await MCP.getAuthStatus(name)
|
||||||
const statusText = getAuthStatusText(authStatus)
|
const icon = getAuthStatusIcon(authStatus)
|
||||||
const url = cfg.url
|
const statusText = getAuthStatusText(authStatus)
|
||||||
return {
|
const url = cfg.url
|
||||||
label: `${icon} ${name} (${statusText})`,
|
return {
|
||||||
value: name,
|
label: `${icon} ${name} (${statusText})`,
|
||||||
hint: url,
|
value: name,
|
||||||
}
|
hint: url,
|
||||||
})
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
const selected = await prompts.select({
|
const selected = await prompts.select({
|
||||||
message: "Select MCP server to authenticate",
|
message: "Select MCP server to authenticate",
|
||||||
@@ -245,8 +213,7 @@ export const McpAuthCommand = cmd({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if already authenticated
|
// Check if already authenticated
|
||||||
const authStatus =
|
const authStatus = await MCP.getAuthStatus(serverName)
|
||||||
auth[serverName] ?? (await AppRuntime.runPromise(MCP.Service.use((mcp) => mcp.getAuthStatus(serverName))))
|
|
||||||
if (authStatus === "authenticated") {
|
if (authStatus === "authenticated") {
|
||||||
const confirm = await prompts.confirm({
|
const confirm = await prompts.confirm({
|
||||||
message: `${serverName} already has valid credentials. Re-authenticate?`,
|
message: `${serverName} already has valid credentials. Re-authenticate?`,
|
||||||
@@ -273,7 +240,7 @@ export const McpAuthCommand = cmd({
|
|||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const status = await AppRuntime.runPromise(MCP.Service.use((mcp) => mcp.authenticate(serverName)))
|
const status = await MCP.authenticate(serverName)
|
||||||
|
|
||||||
if (status.status === "connected") {
|
if (status.status === "connected") {
|
||||||
spinner.stop("Authentication successful!")
|
spinner.stop("Authentication successful!")
|
||||||
@@ -322,17 +289,22 @@ export const McpAuthListCommand = cmd({
|
|||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("MCP OAuth Status")
|
prompts.intro("MCP OAuth Status")
|
||||||
|
|
||||||
const { config, auth } = await authState()
|
const config = await Config.get()
|
||||||
const servers = oauthServers(config)
|
const mcpServers = config.mcp ?? {}
|
||||||
|
|
||||||
if (servers.length === 0) {
|
// Get OAuth-capable servers
|
||||||
|
const oauthServers = Object.entries(mcpServers).filter(
|
||||||
|
(entry): entry is [string, McpRemote] => isMcpRemote(entry[1]) && entry[1].oauth !== false,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (oauthServers.length === 0) {
|
||||||
prompts.log.warn("No OAuth-capable MCP servers configured")
|
prompts.log.warn("No OAuth-capable MCP servers configured")
|
||||||
prompts.outro("Done")
|
prompts.outro("Done")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [name, serverConfig] of servers) {
|
for (const [name, serverConfig] of oauthServers) {
|
||||||
const authStatus = auth[name]
|
const authStatus = await MCP.getAuthStatus(name)
|
||||||
const icon = getAuthStatusIcon(authStatus)
|
const icon = getAuthStatusIcon(authStatus)
|
||||||
const statusText = getAuthStatusText(authStatus)
|
const statusText = getAuthStatusText(authStatus)
|
||||||
const url = serverConfig.url
|
const url = serverConfig.url
|
||||||
@@ -340,7 +312,7 @@ export const McpAuthListCommand = cmd({
|
|||||||
prompts.log.info(`${icon} ${name} ${UI.Style.TEXT_DIM}${statusText}\n ${UI.Style.TEXT_DIM}${url}`)
|
prompts.log.info(`${icon} ${name} ${UI.Style.TEXT_DIM}${statusText}\n ${UI.Style.TEXT_DIM}${url}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
prompts.outro(`${servers.length} OAuth-capable server(s)`)
|
prompts.outro(`${oauthServers.length} OAuth-capable server(s)`)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -361,7 +333,8 @@ export const McpLogoutCommand = cmd({
|
|||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("MCP OAuth Logout")
|
prompts.intro("MCP OAuth Logout")
|
||||||
|
|
||||||
const credentials = await AppRuntime.runPromise(McpAuth.Service.use((auth) => auth.all()))
|
const authPath = path.join(Global.Path.data, "mcp-auth.json")
|
||||||
|
const credentials = await McpAuth.all()
|
||||||
const serverNames = Object.keys(credentials)
|
const serverNames = Object.keys(credentials)
|
||||||
|
|
||||||
if (serverNames.length === 0) {
|
if (serverNames.length === 0) {
|
||||||
@@ -399,7 +372,7 @@ export const McpLogoutCommand = cmd({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await AppRuntime.runPromise(MCP.Service.use((mcp) => mcp.removeAuth(serverName)))
|
await MCP.removeAuth(serverName)
|
||||||
prompts.log.success(`Removed OAuth credentials for ${serverName}`)
|
prompts.log.success(`Removed OAuth credentials for ${serverName}`)
|
||||||
prompts.outro("Done")
|
prompts.outro("Done")
|
||||||
},
|
},
|
||||||
@@ -622,7 +595,7 @@ export const McpDebugCommand = cmd({
|
|||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("MCP OAuth Debug")
|
prompts.intro("MCP OAuth Debug")
|
||||||
|
|
||||||
const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get()))
|
const config = await Config.get()
|
||||||
const mcpServers = config.mcp ?? {}
|
const mcpServers = config.mcp ?? {}
|
||||||
const serverName = args.name
|
const serverName = args.name
|
||||||
|
|
||||||
@@ -649,18 +622,10 @@ export const McpDebugCommand = cmd({
|
|||||||
prompts.log.info(`URL: ${serverConfig.url}`)
|
prompts.log.info(`URL: ${serverConfig.url}`)
|
||||||
|
|
||||||
// Check stored auth status
|
// Check stored auth status
|
||||||
const { authStatus, entry } = await AppRuntime.runPromise(
|
const authStatus = await MCP.getAuthStatus(serverName)
|
||||||
Effect.gen(function* () {
|
|
||||||
const mcp = yield* MCP.Service
|
|
||||||
const auth = yield* McpAuth.Service
|
|
||||||
return {
|
|
||||||
authStatus: yield* mcp.getAuthStatus(serverName),
|
|
||||||
entry: yield* auth.get(serverName),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
prompts.log.info(`Auth status: ${getAuthStatusIcon(authStatus)} ${getAuthStatusText(authStatus)}`)
|
prompts.log.info(`Auth status: ${getAuthStatusIcon(authStatus)} ${getAuthStatusText(authStatus)}`)
|
||||||
|
|
||||||
|
const entry = await McpAuth.get(serverName)
|
||||||
if (entry?.tokens) {
|
if (entry?.tokens) {
|
||||||
prompts.log.info(` Access token: ${entry.tokens.accessToken.substring(0, 20)}...`)
|
prompts.log.info(` Access token: ${entry.tokens.accessToken.substring(0, 20)}...`)
|
||||||
if (entry.tokens.expiresAt) {
|
if (entry.tokens.expiresAt) {
|
||||||
@@ -716,11 +681,6 @@ export const McpDebugCommand = cmd({
|
|||||||
|
|
||||||
// Try to discover OAuth metadata
|
// Try to discover OAuth metadata
|
||||||
const oauthConfig = typeof serverConfig.oauth === "object" ? serverConfig.oauth : undefined
|
const oauthConfig = typeof serverConfig.oauth === "object" ? serverConfig.oauth : undefined
|
||||||
const auth = await AppRuntime.runPromise(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
return yield* McpAuth.Service
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
const authProvider = new McpOAuthProvider(
|
const authProvider = new McpOAuthProvider(
|
||||||
serverName,
|
serverName,
|
||||||
serverConfig.url,
|
serverConfig.url,
|
||||||
@@ -733,7 +693,6 @@ export const McpDebugCommand = cmd({
|
|||||||
{
|
{
|
||||||
onRedirect: async () => {},
|
onRedirect: async () => {},
|
||||||
},
|
},
|
||||||
auth,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
prompts.log.info("Testing OAuth flow (without completing authorization)...")
|
prompts.log.info("Testing OAuth flow (without completing authorization)...")
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import { ModelsDev } from "../../provider/models"
|
|||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
import { Effect } from "effect"
|
|
||||||
|
|
||||||
export const ModelsCommand = cmd({
|
export const ModelsCommand = cmd({
|
||||||
command: "models [provider]",
|
command: "models [provider]",
|
||||||
@@ -37,51 +35,43 @@ export const ModelsCommand = cmd({
|
|||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: process.cwd(),
|
directory: process.cwd(),
|
||||||
async fn() {
|
async fn() {
|
||||||
await AppRuntime.runPromise(
|
const providers = await Provider.list()
|
||||||
Effect.gen(function* () {
|
|
||||||
const svc = yield* Provider.Service
|
|
||||||
const providers = yield* svc.list()
|
|
||||||
|
|
||||||
const print = (providerID: ProviderID, verbose?: boolean) => {
|
function printModels(providerID: ProviderID, verbose?: boolean) {
|
||||||
const provider = providers[providerID]
|
const provider = providers[providerID]
|
||||||
const sorted = Object.entries(provider.models).sort(([a], [b]) => a.localeCompare(b))
|
const sortedModels = Object.entries(provider.models).sort(([a], [b]) => a.localeCompare(b))
|
||||||
for (const [modelID, model] of sorted) {
|
for (const [modelID, model] of sortedModels) {
|
||||||
process.stdout.write(`${providerID}/${modelID}`)
|
process.stdout.write(`${providerID}/${modelID}`)
|
||||||
process.stdout.write(EOL)
|
process.stdout.write(EOL)
|
||||||
if (verbose) {
|
if (verbose) {
|
||||||
process.stdout.write(JSON.stringify(model, null, 2))
|
process.stdout.write(JSON.stringify(model, null, 2))
|
||||||
process.stdout.write(EOL)
|
process.stdout.write(EOL)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (args.provider) {
|
if (args.provider) {
|
||||||
const providerID = ProviderID.make(args.provider)
|
const provider = providers[ProviderID.make(args.provider)]
|
||||||
const provider = providers[providerID]
|
if (!provider) {
|
||||||
if (!provider) {
|
UI.error(`Provider not found: ${args.provider}`)
|
||||||
yield* Effect.sync(() => UI.error(`Provider not found: ${args.provider}`))
|
return
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
|
||||||
yield* Effect.sync(() => print(providerID, args.verbose))
|
printModels(ProviderID.make(args.provider), args.verbose)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const ids = Object.keys(providers).sort((a, b) => {
|
const providerIDs = Object.keys(providers).sort((a, b) => {
|
||||||
const aIsOpencode = a.startsWith("opencode")
|
const aIsOpencode = a.startsWith("opencode")
|
||||||
const bIsOpencode = b.startsWith("opencode")
|
const bIsOpencode = b.startsWith("opencode")
|
||||||
if (aIsOpencode && !bIsOpencode) return -1
|
if (aIsOpencode && !bIsOpencode) return -1
|
||||||
if (!aIsOpencode && bIsOpencode) return 1
|
if (!aIsOpencode && bIsOpencode) return 1
|
||||||
return a.localeCompare(b)
|
return a.localeCompare(b)
|
||||||
})
|
})
|
||||||
|
|
||||||
yield* Effect.sync(() => {
|
for (const providerID of providerIDs) {
|
||||||
for (const providerID of ids) {
|
printModels(ProviderID.make(providerID), args.verbose)
|
||||||
print(ProviderID.make(providerID), args.verbose)
|
}
|
||||||
}
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Auth } from "../../auth"
|
import { Auth } from "../../auth"
|
||||||
import { AppRuntime } from "../../effect/app-runtime"
|
|
||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
import * as prompts from "@clack/prompts"
|
import * as prompts from "@clack/prompts"
|
||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
@@ -14,18 +13,9 @@ import { Instance } from "../../project/instance"
|
|||||||
import type { Hooks } from "@opencode-ai/plugin"
|
import type { Hooks } from "@opencode-ai/plugin"
|
||||||
import { Process } from "../../util/process"
|
import { Process } from "../../util/process"
|
||||||
import { text } from "node:stream/consumers"
|
import { text } from "node:stream/consumers"
|
||||||
import { Effect } from "effect"
|
|
||||||
|
|
||||||
type PluginAuth = NonNullable<Hooks["auth"]>
|
type PluginAuth = NonNullable<Hooks["auth"]>
|
||||||
|
|
||||||
const put = (key: string, info: Auth.Info) =>
|
|
||||||
AppRuntime.runPromise(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const auth = yield* Auth.Service
|
|
||||||
yield* auth.set(key, info)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string, methodName?: string): Promise<boolean> {
|
async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string, methodName?: string): Promise<boolean> {
|
||||||
let index = 0
|
let index = 0
|
||||||
if (methodName) {
|
if (methodName) {
|
||||||
@@ -103,7 +93,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
|
|||||||
const saveProvider = result.provider ?? provider
|
const saveProvider = result.provider ?? provider
|
||||||
if ("refresh" in result) {
|
if ("refresh" in result) {
|
||||||
const { type: _, provider: __, refresh, access, expires, ...extraFields } = result
|
const { type: _, provider: __, refresh, access, expires, ...extraFields } = result
|
||||||
await put(saveProvider, {
|
await Auth.set(saveProvider, {
|
||||||
type: "oauth",
|
type: "oauth",
|
||||||
refresh,
|
refresh,
|
||||||
access,
|
access,
|
||||||
@@ -112,7 +102,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if ("key" in result) {
|
if ("key" in result) {
|
||||||
await put(saveProvider, {
|
await Auth.set(saveProvider, {
|
||||||
type: "api",
|
type: "api",
|
||||||
key: result.key,
|
key: result.key,
|
||||||
})
|
})
|
||||||
@@ -135,7 +125,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
|
|||||||
const saveProvider = result.provider ?? provider
|
const saveProvider = result.provider ?? provider
|
||||||
if ("refresh" in result) {
|
if ("refresh" in result) {
|
||||||
const { type: _, provider: __, refresh, access, expires, ...extraFields } = result
|
const { type: _, provider: __, refresh, access, expires, ...extraFields } = result
|
||||||
await put(saveProvider, {
|
await Auth.set(saveProvider, {
|
||||||
type: "oauth",
|
type: "oauth",
|
||||||
refresh,
|
refresh,
|
||||||
access,
|
access,
|
||||||
@@ -144,7 +134,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if ("key" in result) {
|
if ("key" in result) {
|
||||||
await put(saveProvider, {
|
await Auth.set(saveProvider, {
|
||||||
type: "api",
|
type: "api",
|
||||||
key: result.key,
|
key: result.key,
|
||||||
})
|
})
|
||||||
@@ -159,21 +149,15 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
|
|||||||
|
|
||||||
if (method.type === "api") {
|
if (method.type === "api") {
|
||||||
if (method.authorize) {
|
if (method.authorize) {
|
||||||
const key = await prompts.password({
|
|
||||||
message: "Enter your API key",
|
|
||||||
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
|
||||||
})
|
|
||||||
if (prompts.isCancel(key)) throw new UI.CancelledError()
|
|
||||||
|
|
||||||
const result = await method.authorize(inputs)
|
const result = await method.authorize(inputs)
|
||||||
if (result.type === "failed") {
|
if (result.type === "failed") {
|
||||||
prompts.log.error("Failed to authorize")
|
prompts.log.error("Failed to authorize")
|
||||||
}
|
}
|
||||||
if (result.type === "success") {
|
if (result.type === "success") {
|
||||||
const saveProvider = result.provider ?? provider
|
const saveProvider = result.provider ?? provider
|
||||||
await put(saveProvider, {
|
await Auth.set(saveProvider, {
|
||||||
type: "api",
|
type: "api",
|
||||||
key: result.key ?? key,
|
key: result.key,
|
||||||
})
|
})
|
||||||
prompts.log.success("Login successful")
|
prompts.log.success("Login successful")
|
||||||
}
|
}
|
||||||
@@ -231,12 +215,7 @@ export const ProvidersListCommand = cmd({
|
|||||||
const homedir = os.homedir()
|
const homedir = os.homedir()
|
||||||
const displayPath = authPath.startsWith(homedir) ? authPath.replace(homedir, "~") : authPath
|
const displayPath = authPath.startsWith(homedir) ? authPath.replace(homedir, "~") : authPath
|
||||||
prompts.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`)
|
prompts.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`)
|
||||||
const results = await AppRuntime.runPromise(
|
const results = Object.entries(await Auth.all())
|
||||||
Effect.gen(function* () {
|
|
||||||
const auth = yield* Auth.Service
|
|
||||||
return Object.entries(yield* auth.all())
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
const database = await ModelsDev.get()
|
const database = await ModelsDev.get()
|
||||||
|
|
||||||
for (const [providerID, result] of results) {
|
for (const [providerID, result] of results) {
|
||||||
@@ -315,7 +294,7 @@ export const ProvidersLoginCommand = cmd({
|
|||||||
prompts.outro("Done")
|
prompts.outro("Done")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await put(url, {
|
await Auth.set(url, {
|
||||||
type: "wellknown",
|
type: "wellknown",
|
||||||
key: wellknown.auth.env,
|
key: wellknown.auth.env,
|
||||||
token: token.trim(),
|
token: token.trim(),
|
||||||
@@ -326,7 +305,7 @@ export const ProvidersLoginCommand = cmd({
|
|||||||
}
|
}
|
||||||
await ModelsDev.refresh(true).catch(() => {})
|
await ModelsDev.refresh(true).catch(() => {})
|
||||||
|
|
||||||
const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get()))
|
const config = await Config.get()
|
||||||
|
|
||||||
const disabled = new Set(config.disabled_providers ?? [])
|
const disabled = new Set(config.disabled_providers ?? [])
|
||||||
const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
|
const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
|
||||||
@@ -340,12 +319,6 @@ export const ProvidersLoginCommand = cmd({
|
|||||||
}
|
}
|
||||||
return filtered
|
return filtered
|
||||||
})
|
})
|
||||||
const hooks = await AppRuntime.runPromise(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const plugin = yield* Plugin.Service
|
|
||||||
return yield* plugin.list()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const priority: Record<string, number> = {
|
const priority: Record<string, number> = {
|
||||||
opencode: 0,
|
opencode: 0,
|
||||||
@@ -357,7 +330,7 @@ export const ProvidersLoginCommand = cmd({
|
|||||||
vercel: 6,
|
vercel: 6,
|
||||||
}
|
}
|
||||||
const pluginProviders = resolvePluginProviders({
|
const pluginProviders = resolvePluginProviders({
|
||||||
hooks,
|
hooks: await Plugin.list(),
|
||||||
existingProviders: providers,
|
existingProviders: providers,
|
||||||
disabled,
|
disabled,
|
||||||
enabled,
|
enabled,
|
||||||
@@ -414,7 +387,7 @@ export const ProvidersLoginCommand = cmd({
|
|||||||
provider = selected as string
|
provider = selected as string
|
||||||
}
|
}
|
||||||
|
|
||||||
const plugin = hooks.findLast((x) => x.auth?.provider === provider)
|
const plugin = await Plugin.list().then((x) => x.findLast((x) => x.auth?.provider === provider))
|
||||||
if (plugin && plugin.auth) {
|
if (plugin && plugin.auth) {
|
||||||
const handled = await handlePluginAuth({ auth: plugin.auth }, provider, args.method)
|
const handled = await handlePluginAuth({ auth: plugin.auth }, provider, args.method)
|
||||||
if (handled) return
|
if (handled) return
|
||||||
@@ -428,7 +401,7 @@ export const ProvidersLoginCommand = cmd({
|
|||||||
if (prompts.isCancel(custom)) throw new UI.CancelledError()
|
if (prompts.isCancel(custom)) throw new UI.CancelledError()
|
||||||
provider = custom.replace(/^@ai-sdk\//, "")
|
provider = custom.replace(/^@ai-sdk\//, "")
|
||||||
|
|
||||||
const customPlugin = hooks.findLast((x) => x.auth?.provider === provider)
|
const customPlugin = await Plugin.list().then((x) => x.findLast((x) => x.auth?.provider === provider))
|
||||||
if (customPlugin && customPlugin.auth) {
|
if (customPlugin && customPlugin.auth) {
|
||||||
const handled = await handlePluginAuth({ auth: customPlugin.auth }, provider, args.method)
|
const handled = await handlePluginAuth({ auth: customPlugin.auth }, provider, args.method)
|
||||||
if (handled) return
|
if (handled) return
|
||||||
@@ -468,7 +441,7 @@ export const ProvidersLoginCommand = cmd({
|
|||||||
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
||||||
})
|
})
|
||||||
if (prompts.isCancel(key)) throw new UI.CancelledError()
|
if (prompts.isCancel(key)) throw new UI.CancelledError()
|
||||||
await put(provider, {
|
await Auth.set(provider, {
|
||||||
type: "api",
|
type: "api",
|
||||||
key,
|
key,
|
||||||
})
|
})
|
||||||
@@ -484,33 +457,22 @@ export const ProvidersLogoutCommand = cmd({
|
|||||||
describe: "log out from a configured provider",
|
describe: "log out from a configured provider",
|
||||||
async handler(_args) {
|
async handler(_args) {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
const credentials: Array<[string, Auth.Info]> = await AppRuntime.runPromise(
|
const credentials = await Auth.all().then((x) => Object.entries(x))
|
||||||
Effect.gen(function* () {
|
|
||||||
const auth = yield* Auth.Service
|
|
||||||
return Object.entries(yield* auth.all())
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
prompts.intro("Remove credential")
|
prompts.intro("Remove credential")
|
||||||
if (credentials.length === 0) {
|
if (credentials.length === 0) {
|
||||||
prompts.log.error("No credentials found")
|
prompts.log.error("No credentials found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const database = await ModelsDev.get()
|
const database = await ModelsDev.get()
|
||||||
const selected = await prompts.select({
|
const providerID = await prompts.select({
|
||||||
message: "Select provider",
|
message: "Select provider",
|
||||||
options: credentials.map(([key, value]) => ({
|
options: credentials.map(([key, value]) => ({
|
||||||
label: (database[key]?.name || key) + UI.Style.TEXT_DIM + " (" + value.type + ")",
|
label: (database[key]?.name || key) + UI.Style.TEXT_DIM + " (" + value.type + ")",
|
||||||
value: key,
|
value: key,
|
||||||
})),
|
})),
|
||||||
})
|
})
|
||||||
if (prompts.isCancel(selected)) throw new UI.CancelledError()
|
if (prompts.isCancel(providerID)) throw new UI.CancelledError()
|
||||||
const providerID = selected as string
|
await Auth.remove(providerID)
|
||||||
await AppRuntime.runPromise(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const auth = yield* Auth.Service
|
|
||||||
yield* auth.remove(providerID)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
prompts.outro("Logout successful")
|
prompts.outro("Logout successful")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import { SkillTool } from "../../tool/skill"
|
|||||||
import { BashTool } from "../../tool/bash"
|
import { BashTool } from "../../tool/bash"
|
||||||
import { TodoWriteTool } from "../../tool/todo"
|
import { TodoWriteTool } from "../../tool/todo"
|
||||||
import { Locale } from "../../util/locale"
|
import { Locale } from "../../util/locale"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
|
|
||||||
type ToolProps<T> = {
|
type ToolProps<T> = {
|
||||||
input: Tool.InferParameters<T>
|
input: Tool.InferParameters<T>
|
||||||
@@ -574,7 +573,6 @@ export const RunCommand = cmd({
|
|||||||
// Validate agent if specified
|
// Validate agent if specified
|
||||||
const agent = await (async () => {
|
const agent = await (async () => {
|
||||||
if (!args.agent) return undefined
|
if (!args.agent) return undefined
|
||||||
const name = args.agent
|
|
||||||
|
|
||||||
// When attaching, validate against the running server instead of local Instance state.
|
// When attaching, validate against the running server instead of local Instance state.
|
||||||
if (args.attach) {
|
if (args.attach) {
|
||||||
@@ -592,12 +590,12 @@ export const RunCommand = cmd({
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const agent = modes.find((a) => a.name === name)
|
const agent = modes.find((a) => a.name === args.agent)
|
||||||
if (!agent) {
|
if (!agent) {
|
||||||
UI.println(
|
UI.println(
|
||||||
UI.Style.TEXT_WARNING_BOLD + "!",
|
UI.Style.TEXT_WARNING_BOLD + "!",
|
||||||
UI.Style.TEXT_NORMAL,
|
UI.Style.TEXT_NORMAL,
|
||||||
`agent "${name}" not found. Falling back to default agent`,
|
`agent "${args.agent}" not found. Falling back to default agent`,
|
||||||
)
|
)
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
@@ -606,20 +604,20 @@ export const RunCommand = cmd({
|
|||||||
UI.println(
|
UI.println(
|
||||||
UI.Style.TEXT_WARNING_BOLD + "!",
|
UI.Style.TEXT_WARNING_BOLD + "!",
|
||||||
UI.Style.TEXT_NORMAL,
|
UI.Style.TEXT_NORMAL,
|
||||||
`agent "${name}" is a subagent, not a primary agent. Falling back to default agent`,
|
`agent "${args.agent}" is a subagent, not a primary agent. Falling back to default agent`,
|
||||||
)
|
)
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
return name
|
return args.agent
|
||||||
}
|
}
|
||||||
|
|
||||||
const entry = await AppRuntime.runPromise(Agent.Service.use((svc) => svc.get(name)))
|
const entry = await Agent.get(args.agent)
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
UI.println(
|
UI.println(
|
||||||
UI.Style.TEXT_WARNING_BOLD + "!",
|
UI.Style.TEXT_WARNING_BOLD + "!",
|
||||||
UI.Style.TEXT_NORMAL,
|
UI.Style.TEXT_NORMAL,
|
||||||
`agent "${name}" not found. Falling back to default agent`,
|
`agent "${args.agent}" not found. Falling back to default agent`,
|
||||||
)
|
)
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
@@ -627,11 +625,11 @@ export const RunCommand = cmd({
|
|||||||
UI.println(
|
UI.println(
|
||||||
UI.Style.TEXT_WARNING_BOLD + "!",
|
UI.Style.TEXT_WARNING_BOLD + "!",
|
||||||
UI.Style.TEXT_NORMAL,
|
UI.Style.TEXT_NORMAL,
|
||||||
`agent "${name}" is a subagent, not a primary agent. Falling back to default agent`,
|
`agent "${args.agent}" is a subagent, not a primary agent. Falling back to default agent`,
|
||||||
)
|
)
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
return name
|
return args.agent
|
||||||
})()
|
})()
|
||||||
|
|
||||||
const sessionID = await session(sdk)
|
const sessionID = await session(sdk)
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { Process } from "../../util/process"
|
|||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { which } from "../../util/which"
|
import { which } from "../../util/which"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
|
|
||||||
function pagerCmd(): string[] {
|
function pagerCmd(): string[] {
|
||||||
const lessOptions = ["-R", "-S"]
|
const lessOptions = ["-R", "-S"]
|
||||||
@@ -61,12 +60,12 @@ export const SessionDeleteCommand = cmd({
|
|||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
const sessionID = SessionID.make(args.sessionID)
|
const sessionID = SessionID.make(args.sessionID)
|
||||||
try {
|
try {
|
||||||
await AppRuntime.runPromise(Session.Service.use((svc) => svc.get(sessionID)))
|
await Session.get(sessionID)
|
||||||
} catch {
|
} catch {
|
||||||
UI.error(`Session not found: ${args.sessionID}`)
|
UI.error(`Session not found: ${args.sessionID}`)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
await AppRuntime.runPromise(Session.Service.use((svc) => svc.remove(sessionID)))
|
await Session.remove(sessionID)
|
||||||
UI.println(UI.Style.TEXT_SUCCESS_BOLD + `Session ${args.sessionID} deleted` + UI.Style.TEXT_NORMAL)
|
UI.println(UI.Style.TEXT_SUCCESS_BOLD + `Session ${args.sessionID} deleted` + UI.Style.TEXT_NORMAL)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { Database } from "../../storage/db"
|
|||||||
import { SessionTable } from "../../session/session.sql"
|
import { SessionTable } from "../../session/session.sql"
|
||||||
import { Project } from "../../project/project"
|
import { Project } from "../../project/project"
|
||||||
import { Instance } from "../../project/instance"
|
import { Instance } from "../../project/instance"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
|
||||||
|
|
||||||
interface SessionStats {
|
interface SessionStats {
|
||||||
totalSessions: number
|
totalSessions: number
|
||||||
@@ -168,9 +167,7 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
|
|||||||
const batch = filteredSessions.slice(i, i + BATCH_SIZE)
|
const batch = filteredSessions.slice(i, i + BATCH_SIZE)
|
||||||
|
|
||||||
const batchPromises = batch.map(async (session) => {
|
const batchPromises = batch.map(async (session) => {
|
||||||
const messages = await AppRuntime.runPromise(
|
const messages = await Session.messages({ sessionID: session.id })
|
||||||
Session.Service.use((svc) => svc.messages({ sessionID: session.id })),
|
|
||||||
)
|
|
||||||
|
|
||||||
let sessionCost = 0
|
let sessionCost = 0
|
||||||
let sessionTokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }
|
let sessionTokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { render, TimeToFirstDraw, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
|
import { render, TimeToFirstDraw, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
|
||||||
import { Clipboard } from "@tui/util/clipboard"
|
import { Clipboard } from "@tui/util/clipboard"
|
||||||
import { Selection } from "@tui/util/selection"
|
import { Selection } from "@tui/util/selection"
|
||||||
import { Terminal } from "@tui/util/terminal"
|
|
||||||
import { createCliRenderer, MouseButton, type CliRendererConfig } from "@opentui/core"
|
import { createCliRenderer, MouseButton, type CliRendererConfig } from "@opentui/core"
|
||||||
import { RouteProvider, useRoute } from "@tui/context/route"
|
import { RouteProvider, useRoute } from "@tui/context/route"
|
||||||
import {
|
import {
|
||||||
@@ -60,6 +59,67 @@ import { TuiConfigProvider, useTuiConfig } from "./context/tui-config"
|
|||||||
import { TuiConfig } from "@/config/tui"
|
import { TuiConfig } from "@/config/tui"
|
||||||
import { createTuiApi, TuiPluginRuntime, type RouteMap } from "./plugin"
|
import { createTuiApi, TuiPluginRuntime, type RouteMap } from "./plugin"
|
||||||
import { FormatError, FormatUnknownError } from "@/cli/error"
|
import { FormatError, FormatUnknownError } from "@/cli/error"
|
||||||
|
import { Keybind } from "@/util/keybind"
|
||||||
|
|
||||||
|
async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
|
||||||
|
// can't set raw mode if not a TTY
|
||||||
|
if (!process.stdin.isTTY) return "dark"
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let timeout: NodeJS.Timeout
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
process.stdin.setRawMode(false)
|
||||||
|
process.stdin.removeListener("data", handler)
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = (data: Buffer) => {
|
||||||
|
const str = data.toString()
|
||||||
|
const match = str.match(/\x1b]11;([^\x07\x1b]+)/)
|
||||||
|
if (match) {
|
||||||
|
cleanup()
|
||||||
|
const color = match[1]
|
||||||
|
// Parse RGB values from color string
|
||||||
|
// Formats: rgb:RR/GG/BB or #RRGGBB or rgb(R,G,B)
|
||||||
|
let r = 0,
|
||||||
|
g = 0,
|
||||||
|
b = 0
|
||||||
|
|
||||||
|
if (color.startsWith("rgb:")) {
|
||||||
|
const parts = color.substring(4).split("/")
|
||||||
|
r = parseInt(parts[0], 16) >> 8 // Convert 16-bit to 8-bit
|
||||||
|
g = parseInt(parts[1], 16) >> 8 // Convert 16-bit to 8-bit
|
||||||
|
b = parseInt(parts[2], 16) >> 8 // Convert 16-bit to 8-bit
|
||||||
|
} else if (color.startsWith("#")) {
|
||||||
|
r = parseInt(color.substring(1, 3), 16)
|
||||||
|
g = parseInt(color.substring(3, 5), 16)
|
||||||
|
b = parseInt(color.substring(5, 7), 16)
|
||||||
|
} else if (color.startsWith("rgb(")) {
|
||||||
|
const parts = color.substring(4, color.length - 1).split(",")
|
||||||
|
r = parseInt(parts[0])
|
||||||
|
g = parseInt(parts[1])
|
||||||
|
b = parseInt(parts[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate luminance using relative luminance formula
|
||||||
|
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
|
||||||
|
|
||||||
|
// Determine if dark or light based on luminance threshold
|
||||||
|
resolve(luminance > 0.5 ? "light" : "dark")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process.stdin.setRawMode(true)
|
||||||
|
process.stdin.on("data", handler)
|
||||||
|
process.stdout.write("\x1b]11;?\x07")
|
||||||
|
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
cleanup()
|
||||||
|
resolve("dark")
|
||||||
|
}, 1000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
import type { EventSource } from "./context/sdk"
|
import type { EventSource } from "./context/sdk"
|
||||||
import { DialogVariant } from "./component/dialog-variant"
|
import { DialogVariant } from "./component/dialog-variant"
|
||||||
@@ -119,7 +179,7 @@ export function tui(input: {
|
|||||||
const unguard = win32InstallCtrlCGuard()
|
const unguard = win32InstallCtrlCGuard()
|
||||||
win32DisableProcessedInput()
|
win32DisableProcessedInput()
|
||||||
|
|
||||||
const mode = await Terminal.getTerminalBackgroundColor()
|
const mode = await getTerminalBackgroundColor()
|
||||||
|
|
||||||
// Re-clear after getTerminalBackgroundColor() — setRawMode(false) restores
|
// Re-clear after getTerminalBackgroundColor() — setRawMode(false) restores
|
||||||
// the original console mode which re-enables ENABLE_PROCESSED_INPUT.
|
// the original console mode which re-enables ENABLE_PROCESSED_INPUT.
|
||||||
@@ -251,7 +311,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
|||||||
// - Ctrl+C copies and dismisses selection
|
// - Ctrl+C copies and dismisses selection
|
||||||
// - Esc dismisses selection
|
// - Esc dismisses selection
|
||||||
// - Most other key input dismisses selection and is passed through
|
// - Most other key input dismisses selection and is passed through
|
||||||
if (evt.ctrl && evt.name === "c") {
|
if (Keybind.matchParsedKey("ctrl+c", evt)) {
|
||||||
if (!Selection.copy(renderer, toast)) {
|
if (!Selection.copy(renderer, toast)) {
|
||||||
renderer.clearSelection()
|
renderer.clearSelection()
|
||||||
return
|
return
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -47,7 +47,7 @@ export function DialogMcp() {
|
|||||||
|
|
||||||
const keybinds = createMemo(() => [
|
const keybinds = createMemo(() => [
|
||||||
{
|
{
|
||||||
keybind: Keybind.parse("space")[0],
|
keybind: Keybind.parseOne("space"),
|
||||||
title: "toggle",
|
title: "toggle",
|
||||||
onTrigger: async (option: DialogSelectOption<string>) => {
|
onTrigger: async (option: DialogSelectOption<string>) => {
|
||||||
// Prevent toggling while an operation is already in progress
|
// Prevent toggling while an operation is already in progress
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ export function DialogSessionList() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
keybind: Keybind.parse("ctrl+w")[0],
|
keybind: Keybind.parseOne("ctrl+w"),
|
||||||
title: "new workspace",
|
title: "new workspace",
|
||||||
side: "right",
|
side: "right",
|
||||||
disabled: !Flag.OPENCODE_EXPERIMENTAL_WORKSPACES,
|
disabled: !Flag.OPENCODE_EXPERIMENTAL_WORKSPACES,
|
||||||
|
|||||||
@@ -9,12 +9,6 @@ import { setTimeout as sleep } from "node:timers/promises"
|
|||||||
import { useSDK } from "../context/sdk"
|
import { useSDK } from "../context/sdk"
|
||||||
import { useToast } from "../ui/toast"
|
import { useToast } from "../ui/toast"
|
||||||
|
|
||||||
type Adaptor = {
|
|
||||||
type: string
|
|
||||||
name: string
|
|
||||||
description: string
|
|
||||||
}
|
|
||||||
|
|
||||||
function scoped(sdk: ReturnType<typeof useSDK>, sync: ReturnType<typeof useSync>, workspaceID: string) {
|
function scoped(sdk: ReturnType<typeof useSDK>, sync: ReturnType<typeof useSync>, workspaceID: string) {
|
||||||
return createOpencodeClient({
|
return createOpencodeClient({
|
||||||
baseUrl: sdk.url,
|
baseUrl: sdk.url,
|
||||||
@@ -69,27 +63,9 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) =
|
|||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const [creating, setCreating] = createSignal<string>()
|
const [creating, setCreating] = createSignal<string>()
|
||||||
const [adaptors, setAdaptors] = createSignal<Adaptor[]>()
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
dialog.setSize("medium")
|
dialog.setSize("medium")
|
||||||
void (async () => {
|
|
||||||
const dir = sync.path.directory || sdk.directory
|
|
||||||
const url = new URL("/experimental/workspace/adaptor", sdk.url)
|
|
||||||
if (dir) url.searchParams.set("directory", dir)
|
|
||||||
const res = await sdk
|
|
||||||
.fetch(url)
|
|
||||||
.then((x) => x.json() as Promise<Adaptor[]>)
|
|
||||||
.catch(() => undefined)
|
|
||||||
if (!res) {
|
|
||||||
toast.show({
|
|
||||||
message: "Failed to load workspace adaptors",
|
|
||||||
variant: "error",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setAdaptors(res)
|
|
||||||
})()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const options = createMemo(() => {
|
const options = createMemo(() => {
|
||||||
@@ -103,21 +79,13 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) =
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
const list = adaptors()
|
return [
|
||||||
if (!list) {
|
{
|
||||||
return [
|
title: "Worktree",
|
||||||
{
|
value: "worktree" as const,
|
||||||
title: "Loading workspaces...",
|
description: "Create a local git worktree",
|
||||||
value: "loading" as const,
|
},
|
||||||
description: "Fetching available workspace adaptors",
|
]
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
return list.map((item) => ({
|
|
||||||
title: item.name,
|
|
||||||
value: item.type,
|
|
||||||
description: item.description,
|
|
||||||
}))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const create = async (type: string) => {
|
const create = async (type: string) => {
|
||||||
@@ -145,7 +113,7 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) =
|
|||||||
skipFilter={true}
|
skipFilter={true}
|
||||||
options={options()}
|
options={options()}
|
||||||
onSelect={(option) => {
|
onSelect={(option) => {
|
||||||
if (option.value === "creating" || option.value === "loading") return
|
if (option.value === "creating") return
|
||||||
void create(option.value)
|
void create(option.value)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { createSignal } from "solid-js"
|
|||||||
import { Installation } from "@/installation"
|
import { Installation } from "@/installation"
|
||||||
import { win32FlushInputBuffer } from "../win32"
|
import { win32FlushInputBuffer } from "../win32"
|
||||||
import { getScrollAcceleration } from "../util/scroll"
|
import { getScrollAcceleration } from "../util/scroll"
|
||||||
|
import { Keybind } from "@/util/keybind"
|
||||||
|
|
||||||
export function ErrorComponent(props: {
|
export function ErrorComponent(props: {
|
||||||
error: Error
|
error: Error
|
||||||
@@ -25,7 +26,7 @@ export function ErrorComponent(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useKeyboard((evt) => {
|
useKeyboard((evt) => {
|
||||||
if (evt.ctrl && evt.name === "c") {
|
if (Keybind.matchParsedKey("ctrl+c", evt)) {
|
||||||
handleExit()
|
handleExit()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,630 +1,82 @@
|
|||||||
import { BoxRenderable, MouseButton, MouseEvent, RGBA, TextAttributes } from "@opentui/core"
|
import { TextAttributes, RGBA } from "@opentui/core"
|
||||||
import { For, createMemo, createSignal, onCleanup, type JSX } from "solid-js"
|
import { For, type JSX } from "solid-js"
|
||||||
import { useTheme, tint } from "@tui/context/theme"
|
import { useTheme, tint } from "@tui/context/theme"
|
||||||
import { Sound } from "@tui/util/sound"
|
import { logo, marks } from "@/cli/logo"
|
||||||
import { logo } from "@/cli/logo"
|
|
||||||
|
|
||||||
// Shadow markers (rendered chars in parens):
|
// Shadow markers (rendered chars in parens):
|
||||||
// _ = full shadow cell (space with bg=shadow)
|
// _ = full shadow cell (space with bg=shadow)
|
||||||
// ^ = letter top, shadow bottom (▀ with fg=letter, bg=shadow)
|
// ^ = letter top, shadow bottom (▀ with fg=letter, bg=shadow)
|
||||||
// ~ = shadow top only (▀ with fg=shadow)
|
// ~ = shadow top only (▀ with fg=shadow)
|
||||||
const GAP = 1
|
const SHADOW_MARKER = new RegExp(`[${marks}]`)
|
||||||
const WIDTH = 0.76
|
|
||||||
const GAIN = 2.3
|
|
||||||
const FLASH = 2.15
|
|
||||||
const TRAIL = 0.28
|
|
||||||
const SWELL = 0.24
|
|
||||||
const WIDE = 1.85
|
|
||||||
const DRIFT = 1.45
|
|
||||||
const EXPAND = 1.62
|
|
||||||
const LIFE = 1020
|
|
||||||
const CHARGE = 3000
|
|
||||||
const HOLD = 90
|
|
||||||
const SINK = 40
|
|
||||||
const ARC = 2.2
|
|
||||||
const FORK = 1.2
|
|
||||||
const DIM = 1.04
|
|
||||||
const KICK = 0.86
|
|
||||||
const LAG = 60
|
|
||||||
const SUCK = 0.34
|
|
||||||
const SHIMMER_IN = 60
|
|
||||||
const SHIMMER_OUT = 2.8
|
|
||||||
const TRACE = 0.033
|
|
||||||
const TAIL = 1.8
|
|
||||||
const TRACE_IN = 200
|
|
||||||
const GLOW_OUT = 1600
|
|
||||||
const PEAK = RGBA.fromInts(255, 255, 255)
|
|
||||||
|
|
||||||
type Ring = {
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
at: number
|
|
||||||
force: number
|
|
||||||
kick: number
|
|
||||||
}
|
|
||||||
|
|
||||||
type Hold = {
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
at: number
|
|
||||||
glyph: number | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
type Release = {
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
at: number
|
|
||||||
glyph: number | undefined
|
|
||||||
level: number
|
|
||||||
rise: number
|
|
||||||
}
|
|
||||||
|
|
||||||
type Glow = {
|
|
||||||
glyph: number
|
|
||||||
at: number
|
|
||||||
force: number
|
|
||||||
}
|
|
||||||
|
|
||||||
type Frame = {
|
|
||||||
t: number
|
|
||||||
list: Ring[]
|
|
||||||
hold: Hold | undefined
|
|
||||||
release: Release | undefined
|
|
||||||
glow: Glow | undefined
|
|
||||||
spark: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const LEFT = logo.left[0]?.length ?? 0
|
|
||||||
const FULL = logo.left.map((line, i) => line + " ".repeat(GAP) + logo.right[i])
|
|
||||||
const SPAN = Math.hypot(FULL[0]?.length ?? 0, FULL.length * 2) * 0.94
|
|
||||||
const NEAR = [
|
|
||||||
[1, 0],
|
|
||||||
[1, 1],
|
|
||||||
[0, 1],
|
|
||||||
[-1, 1],
|
|
||||||
[-1, 0],
|
|
||||||
[-1, -1],
|
|
||||||
[0, -1],
|
|
||||||
[1, -1],
|
|
||||||
] as const
|
|
||||||
|
|
||||||
type Trace = {
|
|
||||||
glyph: number
|
|
||||||
i: number
|
|
||||||
l: number
|
|
||||||
}
|
|
||||||
|
|
||||||
function clamp(n: number) {
|
|
||||||
return Math.max(0, Math.min(1, n))
|
|
||||||
}
|
|
||||||
|
|
||||||
function lerp(a: number, b: number, t: number) {
|
|
||||||
return a + (b - a) * clamp(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ease(t: number) {
|
|
||||||
const p = clamp(t)
|
|
||||||
return p * p * (3 - 2 * p)
|
|
||||||
}
|
|
||||||
|
|
||||||
function push(t: number) {
|
|
||||||
const p = clamp(t)
|
|
||||||
return ease(p * p)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ramp(t: number, start: number, end: number) {
|
|
||||||
if (end <= start) return ease(t >= end ? 1 : 0)
|
|
||||||
return ease((t - start) / (end - start))
|
|
||||||
}
|
|
||||||
|
|
||||||
function glow(base: RGBA, theme: ReturnType<typeof useTheme>["theme"], n: number) {
|
|
||||||
const mid = tint(base, theme.primary, 0.84)
|
|
||||||
const top = tint(theme.primary, PEAK, 0.96)
|
|
||||||
if (n <= 1) return tint(base, mid, Math.min(1, Math.sqrt(Math.max(0, n)) * 1.14))
|
|
||||||
return tint(mid, top, Math.min(1, 1 - Math.exp(-2.4 * (n - 1))))
|
|
||||||
}
|
|
||||||
|
|
||||||
function shade(base: RGBA, theme: ReturnType<typeof useTheme>["theme"], n: number) {
|
|
||||||
if (n >= 0) return glow(base, theme, n)
|
|
||||||
return tint(base, theme.background, Math.min(0.82, -n * 0.64))
|
|
||||||
}
|
|
||||||
|
|
||||||
function ghost(n: number, scale: number) {
|
|
||||||
if (n < 0) return n
|
|
||||||
return n * scale
|
|
||||||
}
|
|
||||||
|
|
||||||
function noise(x: number, y: number, t: number) {
|
|
||||||
const n = Math.sin(x * 12.9898 + y * 78.233 + t * 0.043) * 43758.5453
|
|
||||||
return n - Math.floor(n)
|
|
||||||
}
|
|
||||||
|
|
||||||
function lit(char: string) {
|
|
||||||
return char !== " " && char !== "_" && char !== "~"
|
|
||||||
}
|
|
||||||
|
|
||||||
function key(x: number, y: number) {
|
|
||||||
return `${x},${y}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function route(list: Array<{ x: number; y: number }>) {
|
|
||||||
const left = new Map(list.map((item) => [key(item.x, item.y), item]))
|
|
||||||
const path: Array<{ x: number; y: number }> = []
|
|
||||||
let cur = [...left.values()].sort((a, b) => a.y - b.y || a.x - b.x)[0]
|
|
||||||
let dir = { x: 1, y: 0 }
|
|
||||||
|
|
||||||
while (cur) {
|
|
||||||
path.push(cur)
|
|
||||||
left.delete(key(cur.x, cur.y))
|
|
||||||
if (!left.size) return path
|
|
||||||
|
|
||||||
const next = NEAR.map(([dx, dy]) => left.get(key(cur.x + dx, cur.y + dy)))
|
|
||||||
.filter((item): item is { x: number; y: number } => !!item)
|
|
||||||
.sort((a, b) => {
|
|
||||||
const ax = a.x - cur.x
|
|
||||||
const ay = a.y - cur.y
|
|
||||||
const bx = b.x - cur.x
|
|
||||||
const by = b.y - cur.y
|
|
||||||
const adot = ax * dir.x + ay * dir.y
|
|
||||||
const bdot = bx * dir.x + by * dir.y
|
|
||||||
if (adot !== bdot) return bdot - adot
|
|
||||||
return Math.abs(ax) + Math.abs(ay) - (Math.abs(bx) + Math.abs(by))
|
|
||||||
})[0]
|
|
||||||
|
|
||||||
if (!next) {
|
|
||||||
cur = [...left.values()].sort((a, b) => {
|
|
||||||
const da = (a.x - cur.x) ** 2 + (a.y - cur.y) ** 2
|
|
||||||
const db = (b.x - cur.x) ** 2 + (b.y - cur.y) ** 2
|
|
||||||
return da - db
|
|
||||||
})[0]
|
|
||||||
dir = { x: 1, y: 0 }
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
dir = { x: next.x - cur.x, y: next.y - cur.y }
|
|
||||||
cur = next
|
|
||||||
}
|
|
||||||
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapGlyphs() {
|
|
||||||
const cells = [] as Array<{ x: number; y: number }>
|
|
||||||
|
|
||||||
for (let y = 0; y < FULL.length; y++) {
|
|
||||||
for (let x = 0; x < (FULL[y]?.length ?? 0); x++) {
|
|
||||||
if (lit(FULL[y]?.[x] ?? " ")) cells.push({ x, y })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const all = new Map(cells.map((item) => [key(item.x, item.y), item]))
|
|
||||||
const seen = new Set<string>()
|
|
||||||
const glyph = new Map<string, number>()
|
|
||||||
const trace = new Map<string, Trace>()
|
|
||||||
const center = new Map<number, { x: number; y: number }>()
|
|
||||||
let id = 0
|
|
||||||
|
|
||||||
for (const item of cells) {
|
|
||||||
const start = key(item.x, item.y)
|
|
||||||
if (seen.has(start)) continue
|
|
||||||
const stack = [item]
|
|
||||||
const part = [] as Array<{ x: number; y: number }>
|
|
||||||
seen.add(start)
|
|
||||||
|
|
||||||
while (stack.length) {
|
|
||||||
const cur = stack.pop()!
|
|
||||||
part.push(cur)
|
|
||||||
glyph.set(key(cur.x, cur.y), id)
|
|
||||||
for (const [dx, dy] of NEAR) {
|
|
||||||
const next = all.get(key(cur.x + dx, cur.y + dy))
|
|
||||||
if (!next) continue
|
|
||||||
const mark = key(next.x, next.y)
|
|
||||||
if (seen.has(mark)) continue
|
|
||||||
seen.add(mark)
|
|
||||||
stack.push(next)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const path = route(part)
|
|
||||||
path.forEach((cell, i) => trace.set(key(cell.x, cell.y), { glyph: id, i, l: path.length }))
|
|
||||||
center.set(id, {
|
|
||||||
x: part.reduce((sum, item) => sum + item.x, 0) / part.length + 0.5,
|
|
||||||
y: (part.reduce((sum, item) => sum + item.y, 0) / part.length) * 2 + 1,
|
|
||||||
})
|
|
||||||
id++
|
|
||||||
}
|
|
||||||
|
|
||||||
return { glyph, trace, center }
|
|
||||||
}
|
|
||||||
|
|
||||||
const MAP = mapGlyphs()
|
|
||||||
|
|
||||||
function shimmer(x: number, y: number, frame: Frame) {
|
|
||||||
return frame.list.reduce((best, item) => {
|
|
||||||
const age = frame.t - item.at
|
|
||||||
if (age < SHIMMER_IN || age > LIFE) return best
|
|
||||||
const dx = x + 0.5 - item.x
|
|
||||||
const dy = y * 2 + 1 - item.y
|
|
||||||
const dist = Math.hypot(dx, dy)
|
|
||||||
const p = age / LIFE
|
|
||||||
const r = SPAN * (1 - (1 - p) ** EXPAND)
|
|
||||||
const lag = r - dist
|
|
||||||
if (lag < 0.18 || lag > SHIMMER_OUT) return best
|
|
||||||
const band = Math.exp(-(((lag - 1.05) / 0.68) ** 2))
|
|
||||||
const wobble = 0.5 + 0.5 * Math.sin(frame.t * 0.035 + x * 0.9 + y * 1.7)
|
|
||||||
const n = band * wobble * (1 - p) ** 1.45
|
|
||||||
if (n > best) return n
|
|
||||||
return best
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
function remain(x: number, y: number, item: Release, t: number) {
|
|
||||||
const age = t - item.at
|
|
||||||
if (age < 0 || age > LIFE) return 0
|
|
||||||
const p = age / LIFE
|
|
||||||
const dx = x + 0.5 - item.x - 0.5
|
|
||||||
const dy = y * 2 + 1 - item.y * 2 - 1
|
|
||||||
const dist = Math.hypot(dx, dy)
|
|
||||||
const r = SPAN * (1 - (1 - p) ** EXPAND)
|
|
||||||
if (dist > r) return 1
|
|
||||||
return clamp((r - dist) / 1.35 < 1 ? 1 - (r - dist) / 1.35 : 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
function wave(x: number, y: number, frame: Frame, live: boolean) {
|
|
||||||
return frame.list.reduce((sum, item) => {
|
|
||||||
const age = frame.t - item.at
|
|
||||||
if (age < 0 || age > LIFE) return sum
|
|
||||||
const p = age / LIFE
|
|
||||||
const dx = x + 0.5 - item.x
|
|
||||||
const dy = y * 2 + 1 - item.y
|
|
||||||
const dist = Math.hypot(dx, dy)
|
|
||||||
const r = SPAN * (1 - (1 - p) ** EXPAND)
|
|
||||||
const fade = (1 - p) ** 1.32
|
|
||||||
const j = 1.02 + noise(x + item.x * 0.7, y + item.y * 0.7, item.at * 0.002 + age * 0.06) * 0.52
|
|
||||||
const edge = Math.exp(-(((dist - r) / WIDTH) ** 2)) * GAIN * fade * item.force * j
|
|
||||||
const swell = Math.exp(-(((dist - Math.max(0, r - DRIFT)) / WIDE) ** 2)) * SWELL * fade * item.force
|
|
||||||
const trail = dist < r ? Math.exp(-(r - dist) / 2.4) * TRAIL * fade * item.force * lerp(0.92, 1.22, j) : 0
|
|
||||||
const flash = Math.exp(-(dist * dist) / 3.2) * FLASH * item.force * Math.max(0, 1 - age / 140) * lerp(0.95, 1.18, j)
|
|
||||||
const kick = Math.exp(-(dist * dist) / 2) * item.kick * Math.max(0, 1 - age / 100)
|
|
||||||
const suck = Math.exp(-(((dist - 1.25) / 0.75) ** 2)) * item.kick * SUCK * Math.max(0, 1 - age / 110)
|
|
||||||
const wake = live && dist < r ? Math.exp(-(r - dist) / 1.25) * 0.32 * fade : 0
|
|
||||||
return sum + edge + swell + trail + flash + wake - kick - suck
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
function field(x: number, y: number, frame: Frame) {
|
|
||||||
const held = frame.hold
|
|
||||||
const rest = frame.release
|
|
||||||
const item = held ?? rest
|
|
||||||
if (!item) return 0
|
|
||||||
const rise = held ? ramp(frame.t - held.at, HOLD, CHARGE) : rest!.rise
|
|
||||||
const level = held ? push(rise) : rest!.level
|
|
||||||
const body = rise
|
|
||||||
const storm = level * level
|
|
||||||
const sink = held ? ramp(frame.t - held.at, SINK, CHARGE) : rest!.rise
|
|
||||||
const dx = x + 0.5 - item.x - 0.5
|
|
||||||
const dy = y * 2 + 1 - item.y * 2 - 1
|
|
||||||
const dist = Math.hypot(dx, dy)
|
|
||||||
const angle = Math.atan2(dy, dx)
|
|
||||||
const spin = frame.t * lerp(0.008, 0.018, storm)
|
|
||||||
const dim = lerp(0, DIM, sink) * lerp(0.99, 1.01, 0.5 + 0.5 * Math.sin(frame.t * 0.014))
|
|
||||||
const core = Math.exp(-(dist * dist) / Math.max(0.22, lerp(0.22, 3.2, body))) * lerp(0.42, 2.45, body)
|
|
||||||
const shell =
|
|
||||||
Math.exp(-(((dist - lerp(0.16, 2.05, body)) / Math.max(0.18, lerp(0.18, 0.82, body))) ** 2)) * lerp(0.1, 0.95, body)
|
|
||||||
const ember =
|
|
||||||
Math.exp(-(((dist - lerp(0.45, 2.65, body)) / Math.max(0.14, lerp(0.14, 0.62, body))) ** 2)) *
|
|
||||||
lerp(0.02, 0.78, body)
|
|
||||||
const arc = Math.max(0, Math.cos(angle * 3 - spin + frame.spark * 2.2)) ** 8
|
|
||||||
const seam = Math.max(0, Math.cos(angle * 5 + spin * 1.55)) ** 12
|
|
||||||
const ring = Math.exp(-(((dist - lerp(1.05, 3, level)) / 0.48) ** 2)) * arc * lerp(0.03, 0.5 + ARC, storm)
|
|
||||||
const fork = Math.exp(-(((dist - (1.55 + storm * 2.1)) / 0.36) ** 2)) * seam * storm * FORK
|
|
||||||
const spark = Math.max(0, noise(x, y, frame.t) - lerp(0.94, 0.66, storm)) * lerp(0, 5.4, storm)
|
|
||||||
const glitch = spark * Math.exp(-dist / Math.max(1.2, 3.1 - storm))
|
|
||||||
const crack = Math.max(0, Math.cos((dx - dy) * 1.6 + spin * 2.1)) ** 18
|
|
||||||
const lash = crack * Math.exp(-(((dist - (1.95 + storm * 2)) / 0.28) ** 2)) * storm * 1.1
|
|
||||||
const flicker =
|
|
||||||
Math.max(0, noise(item.x * 3.1, item.y * 2.7, frame.t * 1.7) - 0.72) *
|
|
||||||
Math.exp(-(dist * dist) / 0.15) *
|
|
||||||
lerp(0.08, 0.42, body)
|
|
||||||
const fade = frame.release && !frame.hold ? remain(x, y, frame.release, frame.t) : 1
|
|
||||||
return (core + shell + ember + ring + fork + glitch + lash + flicker - dim) * fade
|
|
||||||
}
|
|
||||||
|
|
||||||
function pick(x: number, y: number, frame: Frame) {
|
|
||||||
const held = frame.hold
|
|
||||||
const rest = frame.release
|
|
||||||
const item = held ?? rest
|
|
||||||
if (!item) return 0
|
|
||||||
const rise = held ? ramp(frame.t - held.at, HOLD, CHARGE) : rest!.rise
|
|
||||||
const dx = x + 0.5 - item.x - 0.5
|
|
||||||
const dy = y * 2 + 1 - item.y * 2 - 1
|
|
||||||
const dist = Math.hypot(dx, dy)
|
|
||||||
const fade = frame.release && !frame.hold ? remain(x, y, frame.release, frame.t) : 1
|
|
||||||
return Math.exp(-(dist * dist) / 1.7) * lerp(0.2, 0.96, rise) * fade
|
|
||||||
}
|
|
||||||
|
|
||||||
function select(x: number, y: number) {
|
|
||||||
const direct = MAP.glyph.get(key(x, y))
|
|
||||||
if (direct !== undefined) return direct
|
|
||||||
|
|
||||||
const near = NEAR.map(([dx, dy]) => MAP.glyph.get(key(x + dx, y + dy))).find(
|
|
||||||
(item): item is number => item !== undefined,
|
|
||||||
)
|
|
||||||
return near
|
|
||||||
}
|
|
||||||
|
|
||||||
function trace(x: number, y: number, frame: Frame) {
|
|
||||||
const held = frame.hold
|
|
||||||
const rest = frame.release
|
|
||||||
const item = held ?? rest
|
|
||||||
if (!item || item.glyph === undefined) return 0
|
|
||||||
const step = MAP.trace.get(key(x, y))
|
|
||||||
if (!step || step.glyph !== item.glyph || step.l < 2) return 0
|
|
||||||
const age = frame.t - item.at
|
|
||||||
const rise = held ? ramp(age, HOLD, CHARGE) : rest!.rise
|
|
||||||
const appear = held ? ramp(age, 0, TRACE_IN) : 1
|
|
||||||
const speed = lerp(TRACE * 0.48, TRACE * 0.88, rise)
|
|
||||||
const head = (age * speed) % step.l
|
|
||||||
const dist = Math.min(Math.abs(step.i - head), step.l - Math.abs(step.i - head))
|
|
||||||
const tail = (head - TAIL + step.l) % step.l
|
|
||||||
const lag = Math.min(Math.abs(step.i - tail), step.l - Math.abs(step.i - tail))
|
|
||||||
const fade = frame.release && !frame.hold ? remain(x, y, frame.release, frame.t) : 1
|
|
||||||
const core = Math.exp(-((dist / 1.05) ** 2)) * lerp(0.8, 2.35, rise)
|
|
||||||
const glow = Math.exp(-((dist / 1.85) ** 2)) * lerp(0.08, 0.34, rise)
|
|
||||||
const trail = Math.exp(-((lag / 1.45) ** 2)) * lerp(0.04, 0.42, rise)
|
|
||||||
return (core + glow + trail) * appear * fade
|
|
||||||
}
|
|
||||||
|
|
||||||
function bloom(x: number, y: number, frame: Frame) {
|
|
||||||
const item = frame.glow
|
|
||||||
if (!item) return 0
|
|
||||||
const glyph = MAP.glyph.get(key(x, y))
|
|
||||||
if (glyph !== item.glyph) return 0
|
|
||||||
const age = frame.t - item.at
|
|
||||||
if (age < 0 || age > GLOW_OUT) return 0
|
|
||||||
const p = age / GLOW_OUT
|
|
||||||
const flash = (1 - p) ** 2
|
|
||||||
const dx = x + 0.5 - MAP.center.get(item.glyph)!.x
|
|
||||||
const dy = y * 2 + 1 - MAP.center.get(item.glyph)!.y
|
|
||||||
const bias = Math.exp(-((Math.hypot(dx, dy) / 2.8) ** 2))
|
|
||||||
return lerp(item.force, item.force * 0.18, p) * lerp(0.72, 1.1, bias) * flash
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Logo() {
|
export function Logo() {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const [rings, setRings] = createSignal<Ring[]>([])
|
|
||||||
const [hold, setHold] = createSignal<Hold>()
|
|
||||||
const [release, setRelease] = createSignal<Release>()
|
|
||||||
const [glow, setGlow] = createSignal<Glow>()
|
|
||||||
const [now, setNow] = createSignal(0)
|
|
||||||
let box: BoxRenderable | undefined
|
|
||||||
let timer: ReturnType<typeof setInterval> | undefined
|
|
||||||
let hum = false
|
|
||||||
|
|
||||||
const stop = () => {
|
const renderLine = (line: string, fg: RGBA, bold: boolean): JSX.Element[] => {
|
||||||
if (!timer) return
|
const shadow = tint(theme.background, fg, 0.25)
|
||||||
clearInterval(timer)
|
|
||||||
timer = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const tick = () => {
|
|
||||||
const t = performance.now()
|
|
||||||
setNow(t)
|
|
||||||
const item = hold()
|
|
||||||
if (item && !hum && t - item.at >= HOLD) {
|
|
||||||
hum = true
|
|
||||||
Sound.start()
|
|
||||||
}
|
|
||||||
if (item && t - item.at >= CHARGE) {
|
|
||||||
burst(item.x, item.y)
|
|
||||||
}
|
|
||||||
let live = false
|
|
||||||
setRings((list) => {
|
|
||||||
const next = list.filter((item) => t - item.at < LIFE)
|
|
||||||
live = next.length > 0
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
const flash = glow()
|
|
||||||
if (flash && t - flash.at >= GLOW_OUT) {
|
|
||||||
setGlow(undefined)
|
|
||||||
}
|
|
||||||
if (!live) setRelease(undefined)
|
|
||||||
if (live || hold() || release() || glow()) return
|
|
||||||
stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
const start = () => {
|
|
||||||
if (timer) return
|
|
||||||
timer = setInterval(tick, 16)
|
|
||||||
}
|
|
||||||
|
|
||||||
const hit = (x: number, y: number) => {
|
|
||||||
const char = FULL[y]?.[x]
|
|
||||||
return char !== undefined && char !== " "
|
|
||||||
}
|
|
||||||
|
|
||||||
const press = (x: number, y: number, t: number) => {
|
|
||||||
const last = hold()
|
|
||||||
if (last) burst(last.x, last.y)
|
|
||||||
setNow(t)
|
|
||||||
if (!last) setRelease(undefined)
|
|
||||||
setHold({ x, y, at: t, glyph: select(x, y) })
|
|
||||||
hum = false
|
|
||||||
start()
|
|
||||||
}
|
|
||||||
|
|
||||||
const burst = (x: number, y: number) => {
|
|
||||||
const item = hold()
|
|
||||||
if (!item) return
|
|
||||||
hum = false
|
|
||||||
const t = performance.now()
|
|
||||||
const age = t - item.at
|
|
||||||
const rise = ramp(age, HOLD, CHARGE)
|
|
||||||
const level = push(rise)
|
|
||||||
setHold(undefined)
|
|
||||||
setRelease({ x, y, at: t, glyph: item.glyph, level, rise })
|
|
||||||
if (item.glyph !== undefined) {
|
|
||||||
setGlow({ glyph: item.glyph, at: t, force: lerp(0.18, 1.5, rise * level) })
|
|
||||||
}
|
|
||||||
setRings((list) => [
|
|
||||||
...list,
|
|
||||||
{
|
|
||||||
x: x + 0.5,
|
|
||||||
y: y * 2 + 1,
|
|
||||||
at: t,
|
|
||||||
force: lerp(0.82, 2.55, level),
|
|
||||||
kick: lerp(0.32, 0.32 + KICK, level),
|
|
||||||
},
|
|
||||||
])
|
|
||||||
setNow(t)
|
|
||||||
start()
|
|
||||||
Sound.pulse(lerp(0.8, 1, level))
|
|
||||||
}
|
|
||||||
|
|
||||||
const frame = createMemo(() => {
|
|
||||||
const t = now()
|
|
||||||
const item = hold()
|
|
||||||
return {
|
|
||||||
t,
|
|
||||||
list: rings(),
|
|
||||||
hold: item,
|
|
||||||
release: release(),
|
|
||||||
glow: glow(),
|
|
||||||
spark: item ? noise(item.x, item.y, t) : 0,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const dusk = createMemo(() => {
|
|
||||||
const base = frame()
|
|
||||||
const t = base.t - LAG
|
|
||||||
const item = base.hold
|
|
||||||
return {
|
|
||||||
t,
|
|
||||||
list: base.list,
|
|
||||||
hold: item,
|
|
||||||
release: base.release,
|
|
||||||
glow: base.glow,
|
|
||||||
spark: item ? noise(item.x, item.y, t) : 0,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const renderLine = (
|
|
||||||
line: string,
|
|
||||||
y: number,
|
|
||||||
ink: RGBA,
|
|
||||||
bold: boolean,
|
|
||||||
off: number,
|
|
||||||
frame: Frame,
|
|
||||||
dusk: Frame,
|
|
||||||
): JSX.Element[] => {
|
|
||||||
const shadow = tint(theme.background, ink, 0.25)
|
|
||||||
const attrs = bold ? TextAttributes.BOLD : undefined
|
const attrs = bold ? TextAttributes.BOLD : undefined
|
||||||
|
const elements: JSX.Element[] = []
|
||||||
|
let i = 0
|
||||||
|
|
||||||
return [...line].map((char, i) => {
|
while (i < line.length) {
|
||||||
const h = field(off + i, y, frame)
|
const rest = line.slice(i)
|
||||||
const n = wave(off + i, y, frame, lit(char)) + h
|
const markerIndex = rest.search(SHADOW_MARKER)
|
||||||
const s = wave(off + i, y, dusk, false) + h
|
|
||||||
const p = lit(char) ? pick(off + i, y, frame) : 0
|
|
||||||
const e = lit(char) ? trace(off + i, y, frame) : 0
|
|
||||||
const b = lit(char) ? bloom(off + i, y, frame) : 0
|
|
||||||
const q = shimmer(off + i, y, frame)
|
|
||||||
|
|
||||||
if (char === "_") {
|
if (markerIndex === -1) {
|
||||||
return (
|
elements.push(
|
||||||
<text
|
<text fg={fg} attributes={attrs} selectable={false}>
|
||||||
fg={shade(ink, theme, s * 0.08)}
|
{rest}
|
||||||
bg={shade(shadow, theme, ghost(s, 0.24) + ghost(q, 0.06))}
|
</text>,
|
||||||
attributes={attrs}
|
)
|
||||||
selectable={false}
|
break
|
||||||
>
|
}
|
||||||
{" "}
|
|
||||||
</text>
|
if (markerIndex > 0) {
|
||||||
|
elements.push(
|
||||||
|
<text fg={fg} attributes={attrs} selectable={false}>
|
||||||
|
{rest.slice(0, markerIndex)}
|
||||||
|
</text>,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (char === "^") {
|
const marker = rest[markerIndex]
|
||||||
return (
|
switch (marker) {
|
||||||
<text
|
case "_":
|
||||||
fg={shade(ink, theme, n + p + e + b)}
|
elements.push(
|
||||||
bg={shade(shadow, theme, ghost(s, 0.18) + ghost(q, 0.05) + ghost(b, 0.08))}
|
<text fg={fg} bg={shadow} attributes={attrs} selectable={false}>
|
||||||
attributes={attrs}
|
{" "}
|
||||||
selectable={false}
|
</text>,
|
||||||
>
|
)
|
||||||
▀
|
break
|
||||||
</text>
|
case "^":
|
||||||
)
|
elements.push(
|
||||||
|
<text fg={fg} bg={shadow} attributes={attrs} selectable={false}>
|
||||||
|
▀
|
||||||
|
</text>,
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case "~":
|
||||||
|
elements.push(
|
||||||
|
<text fg={shadow} attributes={attrs} selectable={false}>
|
||||||
|
▀
|
||||||
|
</text>,
|
||||||
|
)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if (char === "~") {
|
i += markerIndex + 1
|
||||||
return (
|
|
||||||
<text fg={shade(shadow, theme, ghost(s, 0.22) + ghost(q, 0.05))} attributes={attrs} selectable={false}>
|
|
||||||
▀
|
|
||||||
</text>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (char === " ") {
|
|
||||||
return (
|
|
||||||
<text fg={ink} attributes={attrs} selectable={false}>
|
|
||||||
{char}
|
|
||||||
</text>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<text fg={shade(ink, theme, n + p + e + b)} attributes={attrs} selectable={false}>
|
|
||||||
{char}
|
|
||||||
</text>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
stop()
|
|
||||||
hum = false
|
|
||||||
Sound.dispose()
|
|
||||||
})
|
|
||||||
|
|
||||||
const mouse = (evt: MouseEvent) => {
|
|
||||||
if (!box) return
|
|
||||||
if ((evt.type === "down" || evt.type === "drag") && evt.button === MouseButton.LEFT) {
|
|
||||||
const x = evt.x - box.x
|
|
||||||
const y = evt.y - box.y
|
|
||||||
if (!hit(x, y)) return
|
|
||||||
if (evt.type === "drag" && hold()) return
|
|
||||||
evt.preventDefault()
|
|
||||||
evt.stopPropagation()
|
|
||||||
const t = performance.now()
|
|
||||||
press(x, y, t)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hold()) return
|
return elements
|
||||||
if (evt.type === "up") {
|
|
||||||
const item = hold()
|
|
||||||
if (!item) return
|
|
||||||
burst(item.x, item.y)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<box ref={(item: BoxRenderable) => (box = item)}>
|
<box>
|
||||||
<box
|
|
||||||
position="absolute"
|
|
||||||
top={0}
|
|
||||||
left={0}
|
|
||||||
width={FULL[0]?.length ?? 0}
|
|
||||||
height={FULL.length}
|
|
||||||
zIndex={1}
|
|
||||||
onMouse={mouse}
|
|
||||||
/>
|
|
||||||
<For each={logo.left}>
|
<For each={logo.left}>
|
||||||
{(line, index) => (
|
{(line, index) => (
|
||||||
<box flexDirection="row" gap={1}>
|
<box flexDirection="row" gap={1}>
|
||||||
<box flexDirection="row">{renderLine(line, index(), theme.textMuted, false, 0, frame(), dusk())}</box>
|
<box flexDirection="row">{renderLine(line, theme.textMuted, false)}</box>
|
||||||
<box flexDirection="row">
|
<box flexDirection="row">{renderLine(logo.right[index()], theme.text, true)}</box>
|
||||||
{renderLine(logo.right[index()], index(), theme.text, true, LEFT + GAP, frame(), dusk())}
|
|
||||||
</box>
|
|
||||||
</box>
|
</box>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|||||||
@@ -8,10 +8,6 @@ export function useEvent() {
|
|||||||
|
|
||||||
function subscribe(handler: (event: Event) => void) {
|
function subscribe(handler: (event: Event) => void) {
|
||||||
return sdk.event.on("event", (event) => {
|
return sdk.event.on("event", (event) => {
|
||||||
if (event.payload.type === "sync") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special hack for truly global events
|
// Special hack for truly global events
|
||||||
if (event.directory === "global") {
|
if (event.directory === "global") {
|
||||||
handler(event.payload)
|
handler(event.payload)
|
||||||
|
|||||||
@@ -542,10 +542,8 @@ function generateSystem(colors: TerminalColors, mode: "dark" | "light"): ThemeJs
|
|||||||
const diffAlpha = isDark ? 0.22 : 0.14
|
const diffAlpha = isDark ? 0.22 : 0.14
|
||||||
const diffAddedBg = tint(bg, ansiColors.green, diffAlpha)
|
const diffAddedBg = tint(bg, ansiColors.green, diffAlpha)
|
||||||
const diffRemovedBg = tint(bg, ansiColors.red, diffAlpha)
|
const diffRemovedBg = tint(bg, ansiColors.red, diffAlpha)
|
||||||
const diffContextBg = grays[2]
|
const diffAddedLineNumberBg = tint(grays[3], ansiColors.green, diffAlpha)
|
||||||
const diffAddedLineNumberBg = tint(diffContextBg, ansiColors.green, diffAlpha)
|
const diffRemovedLineNumberBg = tint(grays[3], ansiColors.red, diffAlpha)
|
||||||
const diffRemovedLineNumberBg = tint(diffContextBg, ansiColors.red, diffAlpha)
|
|
||||||
const diffLineNumber = textMuted
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
theme: {
|
theme: {
|
||||||
@@ -585,8 +583,8 @@ function generateSystem(colors: TerminalColors, mode: "dark" | "light"): ThemeJs
|
|||||||
diffHighlightRemoved: ansiColors.redBright,
|
diffHighlightRemoved: ansiColors.redBright,
|
||||||
diffAddedBg,
|
diffAddedBg,
|
||||||
diffRemovedBg,
|
diffRemovedBg,
|
||||||
diffContextBg,
|
diffContextBg: grays[1],
|
||||||
diffLineNumber,
|
diffLineNumber: grays[6],
|
||||||
diffAddedLineNumberBg,
|
diffAddedLineNumberBg,
|
||||||
diffRemovedLineNumberBg,
|
diffRemovedLineNumberBg,
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
"diffAddedBg": "#354933",
|
"diffAddedBg": "#354933",
|
||||||
"diffRemovedBg": "#3f191a",
|
"diffRemovedBg": "#3f191a",
|
||||||
"diffContextBg": "darkBgPanel",
|
"diffContextBg": "darkBgPanel",
|
||||||
"diffLineNumber": "#898989",
|
"diffLineNumber": "darkBorder",
|
||||||
"diffAddedLineNumberBg": "#162620",
|
"diffAddedLineNumberBg": "#162620",
|
||||||
"diffRemovedLineNumberBg": "#26161a",
|
"diffRemovedLineNumberBg": "#26161a",
|
||||||
"markdownText": "darkFg",
|
"markdownText": "darkFg",
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
"diffAddedBg": "#20303b",
|
"diffAddedBg": "#20303b",
|
||||||
"diffRemovedBg": "#37222c",
|
"diffRemovedBg": "#37222c",
|
||||||
"diffContextBg": "darkPanel",
|
"diffContextBg": "darkPanel",
|
||||||
"diffLineNumber": "diffContext",
|
"diffLineNumber": "darkGutter",
|
||||||
"diffAddedLineNumberBg": "#1b2b34",
|
"diffAddedLineNumberBg": "#1b2b34",
|
||||||
"diffRemovedLineNumberBg": "#2d1f26",
|
"diffRemovedLineNumberBg": "#2d1f26",
|
||||||
"markdownText": "darkFg",
|
"markdownText": "darkFg",
|
||||||
|
|||||||
@@ -141,8 +141,8 @@
|
|||||||
"light": "lbg1"
|
"light": "lbg1"
|
||||||
},
|
},
|
||||||
"diffLineNumber": {
|
"diffLineNumber": {
|
||||||
"dark": "#808792",
|
"dark": "fg3",
|
||||||
"light": "textMuted"
|
"light": "lfg3"
|
||||||
},
|
},
|
||||||
"diffAddedLineNumberBg": {
|
"diffAddedLineNumberBg": {
|
||||||
"dark": "diffGreenBg",
|
"dark": "diffGreenBg",
|
||||||
|
|||||||
@@ -125,7 +125,10 @@
|
|||||||
"dark": "frappeMantle",
|
"dark": "frappeMantle",
|
||||||
"light": "frappeMantle"
|
"light": "frappeMantle"
|
||||||
},
|
},
|
||||||
"diffLineNumber": "textMuted",
|
"diffLineNumber": {
|
||||||
|
"dark": "frappeSurface1",
|
||||||
|
"light": "frappeSurface1"
|
||||||
|
},
|
||||||
"diffAddedLineNumberBg": {
|
"diffAddedLineNumberBg": {
|
||||||
"dark": "#223025",
|
"dark": "#223025",
|
||||||
"light": "#223025"
|
"light": "#223025"
|
||||||
|
|||||||
@@ -125,7 +125,10 @@
|
|||||||
"dark": "macMantle",
|
"dark": "macMantle",
|
||||||
"light": "macMantle"
|
"light": "macMantle"
|
||||||
},
|
},
|
||||||
"diffLineNumber": "textMuted",
|
"diffLineNumber": {
|
||||||
|
"dark": "macSurface1",
|
||||||
|
"light": "macSurface1"
|
||||||
|
},
|
||||||
"diffAddedLineNumberBg": {
|
"diffAddedLineNumberBg": {
|
||||||
"dark": "#223025",
|
"dark": "#223025",
|
||||||
"light": "#223025"
|
"light": "#223025"
|
||||||
|
|||||||
@@ -79,7 +79,7 @@
|
|||||||
"diffAddedBg": { "dark": "#24312b", "light": "#d6f0d9" },
|
"diffAddedBg": { "dark": "#24312b", "light": "#d6f0d9" },
|
||||||
"diffRemovedBg": { "dark": "#3c2a32", "light": "#f6dfe2" },
|
"diffRemovedBg": { "dark": "#3c2a32", "light": "#f6dfe2" },
|
||||||
"diffContextBg": { "dark": "darkMantle", "light": "lightMantle" },
|
"diffContextBg": { "dark": "darkMantle", "light": "lightMantle" },
|
||||||
"diffLineNumber": { "dark": "textMuted", "light": "#5b5d63" },
|
"diffLineNumber": { "dark": "darkSurface1", "light": "lightSurface1" },
|
||||||
"diffAddedLineNumberBg": { "dark": "#1e2a25", "light": "#c9e3cb" },
|
"diffAddedLineNumberBg": { "dark": "#1e2a25", "light": "#c9e3cb" },
|
||||||
"diffRemovedLineNumberBg": { "dark": "#32232a", "light": "#e9d3d6" },
|
"diffRemovedLineNumberBg": { "dark": "#32232a", "light": "#e9d3d6" },
|
||||||
"markdownText": { "dark": "darkText", "light": "lightText" },
|
"markdownText": { "dark": "darkText", "light": "lightText" },
|
||||||
|
|||||||
@@ -120,7 +120,10 @@
|
|||||||
"dark": "#122738",
|
"dark": "#122738",
|
||||||
"light": "#f5f7fa"
|
"light": "#f5f7fa"
|
||||||
},
|
},
|
||||||
"diffLineNumber": "textMuted",
|
"diffLineNumber": {
|
||||||
|
"dark": "#2d5a7b",
|
||||||
|
"light": "#b0bec5"
|
||||||
|
},
|
||||||
"diffAddedLineNumberBg": {
|
"diffAddedLineNumberBg": {
|
||||||
"dark": "#1a3a2a",
|
"dark": "#1a3a2a",
|
||||||
"light": "#e8f5e9"
|
"light": "#e8f5e9"
|
||||||
|
|||||||
@@ -142,8 +142,8 @@
|
|||||||
"light": "lightPanel"
|
"light": "lightPanel"
|
||||||
},
|
},
|
||||||
"diffLineNumber": {
|
"diffLineNumber": {
|
||||||
"dark": "#eeeeee87",
|
"dark": "#e4e4e442",
|
||||||
"light": "textMuted"
|
"light": "#1414147a"
|
||||||
},
|
},
|
||||||
"diffAddedLineNumberBg": {
|
"diffAddedLineNumberBg": {
|
||||||
"dark": "#3fa26633",
|
"dark": "#3fa26633",
|
||||||
|
|||||||
@@ -112,8 +112,8 @@
|
|||||||
"light": "#e8e8e2"
|
"light": "#e8e8e2"
|
||||||
},
|
},
|
||||||
"diffLineNumber": {
|
"diffLineNumber": {
|
||||||
"dark": "#989aa4",
|
"dark": "currentLine",
|
||||||
"light": "#686865"
|
"light": "#c8c8c2"
|
||||||
},
|
},
|
||||||
"diffAddedLineNumberBg": {
|
"diffAddedLineNumberBg": {
|
||||||
"dark": "#1a3a1a",
|
"dark": "#1a3a1a",
|
||||||
|
|||||||
@@ -134,8 +134,8 @@
|
|||||||
"light": "lightStep2"
|
"light": "lightStep2"
|
||||||
},
|
},
|
||||||
"diffLineNumber": {
|
"diffLineNumber": {
|
||||||
"dark": "#a0a5a7",
|
"dark": "darkStep3",
|
||||||
"light": "#5b5951"
|
"light": "lightStep3"
|
||||||
},
|
},
|
||||||
"diffAddedLineNumberBg": {
|
"diffAddedLineNumberBg": {
|
||||||
"dark": "#1b2b34",
|
"dark": "#1b2b34",
|
||||||
|
|||||||
@@ -130,8 +130,8 @@
|
|||||||
"light": "base50"
|
"light": "base50"
|
||||||
},
|
},
|
||||||
"diffLineNumber": {
|
"diffLineNumber": {
|
||||||
"dark": "#888883",
|
"dark": "base600",
|
||||||
"light": "#5a5955"
|
"light": "base600"
|
||||||
},
|
},
|
||||||
"diffAddedLineNumberBg": {
|
"diffAddedLineNumberBg": {
|
||||||
"dark": "#152515",
|
"dark": "#152515",
|
||||||
|
|||||||
@@ -126,8 +126,8 @@
|
|||||||
"light": "lightBgAlt"
|
"light": "lightBgAlt"
|
||||||
},
|
},
|
||||||
"diffLineNumber": {
|
"diffLineNumber": {
|
||||||
"dark": "#95999e",
|
"dark": "#484f58",
|
||||||
"light": "textMuted"
|
"light": "#afb8c1"
|
||||||
},
|
},
|
||||||
"diffAddedLineNumberBg": {
|
"diffAddedLineNumberBg": {
|
||||||
"dark": "#033a16",
|
"dark": "#033a16",
|
||||||
|
|||||||
@@ -135,8 +135,8 @@
|
|||||||
"light": "lightBg1"
|
"light": "lightBg1"
|
||||||
},
|
},
|
||||||
"diffLineNumber": {
|
"diffLineNumber": {
|
||||||
"dark": "#a8a29e",
|
"dark": "darkBg3",
|
||||||
"light": "#564f43"
|
"light": "lightBg3"
|
||||||
},
|
},
|
||||||
"diffAddedLineNumberBg": {
|
"diffAddedLineNumberBg": {
|
||||||
"dark": "#2a2827",
|
"dark": "#2a2827",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user