Compare commits
179
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eeca56a943 | ||
|
|
60b8041ebb | ||
|
|
3b2a2c461d | ||
|
|
6706358a6e | ||
|
|
f6409759e5 | ||
|
|
f9d99f044d | ||
|
|
bbd5faf5cd | ||
|
|
af5d7b67b0 | ||
|
|
aeb7d99d20 | ||
|
|
3695057bee | ||
|
|
4ed3afea84 | ||
|
|
3cf7c7536b | ||
|
|
85674f4bfd | ||
|
|
f2525a63c9 | ||
|
|
8c42d391f5 | ||
|
|
7f9bf91073 | ||
|
|
6ce5c01b1a | ||
|
|
a53fae1511 | ||
|
|
4626458175 | ||
|
|
9a5178e4ac | ||
|
|
68384613be | ||
|
|
4f967d5bc0 | ||
|
|
ff60859e36 | ||
|
|
020c47a055 | ||
|
|
64171db173 | ||
|
|
ad265797ab | ||
|
|
b1312a3181 | ||
|
|
a8f9f6b705 | ||
|
|
d312c677c5 | ||
|
|
5b60e51c9f | ||
|
|
7cbe1627ec | ||
|
|
d6840868d4 | ||
|
|
9b2648dd57 | ||
|
|
f954854232 | ||
|
|
6a99079012 | ||
|
|
0a8b6298cd | ||
|
|
f40209bdfb | ||
|
|
a2cb4909da | ||
|
|
7a05ba47d1 | ||
|
|
36745caa2a | ||
|
|
c2403d0f15 | ||
|
|
34e2429c49 | ||
|
|
10ba68c772 | ||
|
|
e8471256f2 | ||
|
|
43b37346b6 | ||
|
|
d199648aeb | ||
|
|
a06f40297b | ||
|
|
59c0fc28ee | ||
|
|
b22add292c | ||
|
|
67aaecacac | ||
|
|
29c202e6ab | ||
|
|
dcbf11f41a | ||
|
|
14ccff4037 | ||
|
|
5b8b874732 | ||
|
|
1d81c0266c | ||
|
|
913120759a | ||
|
|
7a6ce05d09 | ||
|
|
1dc69359d5 | ||
|
|
329fcb040b | ||
|
|
bf50d1c028 | ||
|
|
b8801dbd22 | ||
|
|
f7c6943817 | ||
|
|
91fe4db27c | ||
|
|
21d7a85e76 | ||
|
|
663e798e76 | ||
|
|
5bc2d2498d | ||
|
|
c22e34853d | ||
|
|
6825b0bbc7 | ||
|
|
3644581b55 | ||
|
|
79cc15335e | ||
|
|
ca6200121b | ||
|
|
7239b38b7f | ||
|
|
9ae8dc2d01 | ||
|
|
7164662be2 | ||
|
|
94f71f59a3 | ||
|
|
3eb6508a64 | ||
|
|
6fdb8ab90d | ||
|
|
321bf1f8e1 | ||
|
|
62bd023086 | ||
|
|
cb1a50055c | ||
|
|
65e3348232 | ||
|
|
a6b9f0dac1 | ||
|
|
34f5bdbc99 | ||
|
|
0b4fe14b0a | ||
|
|
7230cd2683 | ||
|
|
a915fe74be | ||
|
|
26d35583c5 | ||
|
|
ae17b416b8 | ||
|
|
8ffadde85c | ||
|
|
3c0ad70653 | ||
|
|
264418c0cd | ||
|
|
fa2c69f09c | ||
|
|
113304a058 | ||
|
|
8c4d49c2bc | ||
|
|
2aa6110c6e | ||
|
|
8b9b9ad31e | ||
|
|
3729fd5706 | ||
|
|
74b14a2d4e | ||
|
|
cdb951ec2f | ||
|
|
fc01cad2b8 | ||
|
|
c1ddc0ea2d | ||
|
|
319b7655b7 | ||
|
|
824c12c01a | ||
|
|
17b2900884 | ||
|
|
003010bdb6 | ||
|
|
82a4292934 | ||
|
|
eea4253d67 | ||
|
|
1eacc3c339 | ||
|
|
1a509d62a0 | ||
|
|
4c4eef46f1 | ||
|
|
d62ec7776e | ||
|
|
cb1e5d9e41 | ||
|
|
ca5f086759 | ||
|
|
57c40eb7c2 | ||
|
|
63035f977f | ||
|
|
514d2a36bc | ||
|
|
0b6fd5f612 | ||
|
|
029e7135b7 | ||
|
|
c43591f8a2 | ||
|
|
a2c22714cb | ||
|
|
312f10f797 | ||
|
|
d1f05b0f3a | ||
|
|
ccb0b320e1 | ||
|
|
5ee7edaf9e | ||
|
|
27190635ea | ||
|
|
2e340d976f | ||
|
|
fe4dfb9f6f | ||
|
|
5e3dc80999 | ||
|
|
d84cc33742 | ||
|
|
c92c462148 | ||
|
|
9ca06e0336 | ||
|
|
3b523b32f5 | ||
|
|
ba3600a515 | ||
|
|
03ce2e5288 | ||
|
|
87e23abb10 | ||
|
|
2868000c20 | ||
|
|
f38f415bf0 | ||
|
|
4341ab838e | ||
|
|
cd004cf0b2 | ||
|
|
19ae8c88b0 | ||
|
|
3dd09147c2 | ||
|
|
9581bf0670 | ||
|
|
af8aff3788 | ||
|
|
2a8a59ded9 | ||
|
|
5917ac2162 | ||
|
|
b6af4d0dc6 | ||
|
|
577139c626 | ||
|
|
c5fb6281f0 | ||
|
|
f99812443c | ||
|
|
b898c6d0ea | ||
|
|
9e7045eaec | ||
|
|
a17ac02061 | ||
|
|
57f9397677 | ||
|
|
a4c686025c | ||
|
|
face879100 | ||
|
|
605559b165 | ||
|
|
5cd4c6eb22 | ||
|
|
40358d60a0 | ||
|
|
96c1c0363d | ||
|
|
33819932ec | ||
|
|
5d6fe01465 | ||
|
|
cf27a73397 | ||
|
|
f2c492a8e6 | ||
|
|
0556774097 | ||
|
|
d9d5a0615e | ||
|
|
d72ddd71fa | ||
|
|
fb26308bc7 | ||
|
|
b41fa8e318 | ||
|
|
57b2e64345 | ||
|
|
346b3e1b8d | ||
|
|
b139bc2ef3 | ||
|
|
378b8ca241 | ||
|
|
f63bdc8e08 | ||
|
|
ce26120205 | ||
|
|
d2d5d84d1e | ||
|
|
847f1d99c9 | ||
|
|
59d08683ea | ||
|
|
f7514d9eca | ||
|
|
180ded6a27 |
@@ -25,7 +25,9 @@ kommander
|
|||||||
-opencodeengineer bot that spams issues
|
-opencodeengineer bot that spams issues
|
||||||
r44vc0rp
|
r44vc0rp
|
||||||
rekram1-node
|
rekram1-node
|
||||||
|
-ricardo-m-l
|
||||||
-robinmordasiewicz
|
-robinmordasiewicz
|
||||||
|
simonklee
|
||||||
-spider-yamet clawdbot/llm psychosis, spam pinging the team
|
-spider-yamet clawdbot/llm psychosis, spam pinging the team
|
||||||
thdxr
|
thdxr
|
||||||
-toastythebot
|
-toastythebot
|
||||||
|
|||||||
@@ -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' && github.ref_name != 'beta'
|
if: github.repository == 'anomalyco/opencode'
|
||||||
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,7 +213,6 @@ 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 }}
|
||||||
@@ -390,7 +389,7 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- build-cli
|
- build-cli
|
||||||
- version
|
- version
|
||||||
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'beta'
|
if: github.repository == 'anomalyco/opencode'
|
||||||
continue-on-error: false
|
continue-on-error: false
|
||||||
env:
|
env:
|
||||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
@@ -591,13 +590,12 @@ 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 && github.ref_name != 'beta'
|
if: needs.version.outputs.release
|
||||||
with:
|
with:
|
||||||
pattern: latest-yml-*
|
pattern: latest-yml-*
|
||||||
path: /tmp/latest-yml
|
path: /tmp/latest-yml
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ package.json
|
|||||||
bun.lock
|
bun.lock
|
||||||
.gitignore
|
.gitignore
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
references/
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
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": "nord2",
|
"dark": "#abafb7",
|
||||||
"light": "nord4"
|
"light": "textMuted"
|
||||||
},
|
},
|
||||||
"diffAddedLineNumberBg": {
|
"diffAddedLineNumberBg": {
|
||||||
"dark": "#3B4252",
|
"dark": "#3B4252",
|
||||||
|
|||||||
@@ -319,7 +319,8 @@
|
|||||||
"@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/amazon-bedrock": "4.0.83",
|
"@ai-sdk/alibaba": "1.0.17",
|
||||||
|
"@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",
|
||||||
"@ai-sdk/cerebras": "2.0.41",
|
"@ai-sdk/cerebras": "2.0.41",
|
||||||
@@ -331,7 +332,7 @@
|
|||||||
"@ai-sdk/groq": "3.0.31",
|
"@ai-sdk/groq": "3.0.31",
|
||||||
"@ai-sdk/mistral": "3.0.27",
|
"@ai-sdk/mistral": "3.0.27",
|
||||||
"@ai-sdk/openai": "3.0.48",
|
"@ai-sdk/openai": "3.0.48",
|
||||||
"@ai-sdk/openai-compatible": "2.0.37",
|
"@ai-sdk/openai-compatible": "2.0.41",
|
||||||
"@ai-sdk/perplexity": "3.0.26",
|
"@ai-sdk/perplexity": "3.0.26",
|
||||||
"@ai-sdk/provider": "3.0.8",
|
"@ai-sdk/provider": "3.0.8",
|
||||||
"@ai-sdk/provider-utils": "4.0.23",
|
"@ai-sdk/provider-utils": "4.0.23",
|
||||||
@@ -341,7 +342,6 @@
|
|||||||
"@aws-sdk/credential-providers": "3.993.0",
|
"@aws-sdk/credential-providers": "3.993.0",
|
||||||
"@clack/prompts": "1.0.0-alpha.1",
|
"@clack/prompts": "1.0.0-alpha.1",
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
"@gitlab/gitlab-ai-provider": "3.6.0",
|
|
||||||
"@gitlab/opencode-gitlab-auth": "1.3.3",
|
"@gitlab/opencode-gitlab-auth": "1.3.3",
|
||||||
"@hono/node-server": "1.19.11",
|
"@hono/node-server": "1.19.11",
|
||||||
"@hono/node-ws": "1.3.0",
|
"@hono/node-ws": "1.3.0",
|
||||||
@@ -357,9 +357,9 @@
|
|||||||
"@opencode-ai/script": "workspace:*",
|
"@opencode-ai/script": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
"@openrouter/ai-sdk-provider": "2.4.2",
|
"@openrouter/ai-sdk-provider": "2.5.1",
|
||||||
"@opentui/core": "0.1.97",
|
"@opentui/core": "0.1.99",
|
||||||
"@opentui/solid": "0.1.97",
|
"@opentui/solid": "0.1.99",
|
||||||
"@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,6 +371,7 @@
|
|||||||
"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",
|
||||||
@@ -385,6 +386,7 @@
|
|||||||
"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",
|
||||||
@@ -395,6 +397,7 @@
|
|||||||
"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",
|
||||||
@@ -413,7 +416,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.28.4",
|
"@babel/core": "7.28.4",
|
||||||
"@effect/language-service": "0.79.0",
|
"@effect/language-service": "0.84.2",
|
||||||
"@octokit/webhooks-types": "7.6.1",
|
"@octokit/webhooks-types": "7.6.1",
|
||||||
"@opencode-ai/script": "workspace:*",
|
"@opencode-ai/script": "workspace:*",
|
||||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||||
@@ -450,19 +453,20 @@
|
|||||||
"version": "1.4.3",
|
"version": "1.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
|
"effect": "catalog:",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@opentui/core": "0.1.97",
|
"@opentui/core": "0.1.99",
|
||||||
"@opentui/solid": "0.1.97",
|
"@opentui/solid": "0.1.99",
|
||||||
"@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.97",
|
"@opentui/core": ">=0.1.99",
|
||||||
"@opentui/solid": ">=0.1.97",
|
"@opentui/solid": ">=0.1.99",
|
||||||
},
|
},
|
||||||
"optionalPeers": [
|
"optionalPeers": [
|
||||||
"@opentui/core",
|
"@opentui/core",
|
||||||
@@ -494,6 +498,13 @@
|
|||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"packages/server": {
|
||||||
|
"name": "@opencode-ai/server",
|
||||||
|
"version": "1.4.3",
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "catalog:",
|
||||||
|
},
|
||||||
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.4.3",
|
"version": "1.4.3",
|
||||||
@@ -641,7 +652,7 @@
|
|||||||
},
|
},
|
||||||
"catalog": {
|
"catalog": {
|
||||||
"@cloudflare/workers-types": "4.20251008.0",
|
"@cloudflare/workers-types": "4.20251008.0",
|
||||||
"@effect/platform-node": "4.0.0-beta.43",
|
"@effect/platform-node": "4.0.0-beta.46",
|
||||||
"@hono/zod-validator": "0.4.2",
|
"@hono/zod-validator": "0.4.2",
|
||||||
"@kobalte/core": "0.13.11",
|
"@kobalte/core": "0.13.11",
|
||||||
"@lydell/node-pty": "1.2.0-beta.10",
|
"@lydell/node-pty": "1.2.0-beta.10",
|
||||||
@@ -662,13 +673,13 @@
|
|||||||
"@types/node": "22.13.9",
|
"@types/node": "22.13.9",
|
||||||
"@types/semver": "7.7.1",
|
"@types/semver": "7.7.1",
|
||||||
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
||||||
"ai": "6.0.149",
|
"ai": "6.0.158",
|
||||||
"cross-spawn": "7.0.6",
|
"cross-spawn": "7.0.6",
|
||||||
"diff": "8.0.2",
|
"diff": "8.0.2",
|
||||||
"dompurify": "3.3.1",
|
"dompurify": "3.3.1",
|
||||||
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
||||||
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
||||||
"effect": "4.0.0-beta.43",
|
"effect": "4.0.0-beta.46",
|
||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
"hono": "4.10.7",
|
"hono": "4.10.7",
|
||||||
"hono-openapi": "1.1.2",
|
"hono-openapi": "1.1.2",
|
||||||
@@ -707,7 +718,9 @@
|
|||||||
|
|
||||||
"@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/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.83", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.64", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21", "@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-DoRpvIWGU/r83UeJAM9L93Lca8Kf/yP5fIhfEOltMPGP/PXrGe0BZaz0maLSRn8djJ6+HzWIsgu5ZI6bZqXEXg=="],
|
"@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/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=="],
|
||||||
|
|
||||||
@@ -1025,11 +1038,11 @@
|
|||||||
|
|
||||||
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="],
|
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="],
|
||||||
|
|
||||||
"@effect/language-service": ["@effect/language-service@0.79.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-DEmIOsg1GjjP6s9HXH1oJrW+gDmzkhVv9WOZl6to5eNyyCrjz1S2PDqQ7aYrW/HuifhfwI5Bik1pK4pj7Z+lrg=="],
|
"@effect/language-service": ["@effect/language-service@0.84.2", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-l04qNxpiA8rY5yXWckRPJ7Mk5MNerXuNymSFf+IdflfI5i8jgL1bpBNLuP6ijg7wgjdHc/KmTnCj2kT0SCntuA=="],
|
||||||
|
|
||||||
"@effect/platform-node": ["@effect/platform-node@4.0.0-beta.43", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.43", "mime": "^4.1.0", "undici": "^7.24.0" }, "peerDependencies": { "effect": "^4.0.0-beta.43", "ioredis": "^5.7.0" } }, "sha512-Uq6E1rjaIpjHauzjwoB2HzAg3battYt2Boy8XO50GoHiWCXKE6WapYZ0/AnaBx5v5qg2sOfqpuiLsUf9ZgxOkA=="],
|
"@effect/platform-node": ["@effect/platform-node@4.0.0-beta.46", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.46", "mime": "^4.1.0", "undici": "^7.24.0" }, "peerDependencies": { "effect": "^4.0.0-beta.46", "ioredis": "^5.7.0" } }, "sha512-6AFRKjJO95dFl5lK/YnJi04uePjQDFi3+K1aXwcz/EfVlRwJ4+lg5O4vbievfKL/hnfcShVp3/eXnNS9tvlMZQ=="],
|
||||||
|
|
||||||
"@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.43", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.19.0" }, "peerDependencies": { "effect": "^4.0.0-beta.43" } }, "sha512-A9q0GEb61pYcQ06Dr6gXj1nKlDI3KHsar1sk3qb1ZY+kVSR64tBAylI8zGon23KY+NPtTUj/sEIToB7jc3Qt5w=="],
|
"@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.46", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.19.0" }, "peerDependencies": { "effect": "^4.0.0-beta.46" } }, "sha512-Yzci82XbZ1W3tuiownsJawrJZTGeTrTZKLD0uxdBWCBzlVyqDwoSwRwO5qh33DurJj9B7iS8MDf14fpGRBPNGQ=="],
|
||||||
|
|
||||||
"@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
|
"@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
|
||||||
|
|
||||||
@@ -1161,8 +1174,6 @@
|
|||||||
|
|
||||||
"@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="],
|
"@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="],
|
||||||
|
|
||||||
"@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.6.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-8LmcIQ86xkMtC7L4P1/QYVEC+yKMTRerfPeniaaQGalnzXKtX6iMHLjLPOL9Rxp55lOXi6ed0WrFuJzZx+fNRg=="],
|
|
||||||
|
|
||||||
"@gitlab/opencode-gitlab-auth": ["@gitlab/opencode-gitlab-auth@1.3.3", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-FT+KsCmAJjtqWr1YAq0MywGgL9kaLQ4apmsoowAXrPqHtoYf2i/nY10/A+L06kNj22EATeEDRpbB1NWXMto/SA=="],
|
"@gitlab/opencode-gitlab-auth": ["@gitlab/opencode-gitlab-auth@1.3.3", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-FT+KsCmAJjtqWr1YAq0MywGgL9kaLQ4apmsoowAXrPqHtoYf2i/nY10/A+L06kNj22EATeEDRpbB1NWXMto/SA=="],
|
||||||
|
|
||||||
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
|
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
|
||||||
@@ -1529,6 +1540,8 @@
|
|||||||
|
|
||||||
"@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"],
|
||||||
@@ -1539,25 +1552,25 @@
|
|||||||
|
|
||||||
"@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"],
|
"@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"],
|
||||||
|
|
||||||
"@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.4.2", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-uRQZ4da77gru1I7/lNGJhKbqEIY7o/sPsLlbCM97VY9muGDjM/TaJzuwqIviqKTtXLzF0WDj5qBAi6FhxjvlSg=="],
|
"@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.5.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-r1fJL1Cb3gQDa2MpWH/sfx1BsEW0uzlRriJM6eihaKqbtKDmZoBisF32VcVaQYassighX7NGCkF68EsrZA43uQ=="],
|
||||||
|
|
||||||
"@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.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": ["@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-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.97", "", { "os": "darwin", "cpu": "arm64" }, "sha512-t7oMGEfMPQsqLEx7/rPqv/UGJ+vqhe4RWHRRQRYcuHuLKssZ2S8P9mSS7MBPtDqGcxg4PosCrh5nHYeZ94EXUw=="],
|
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.99", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bzVrqeX2vb5iWrc/ftOUOqeUY8XO+qSgoTwj5TXHuwagavgwD3Hpeyjx8+icnTTeM4pao0som1WR9xfye6/X5Q=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.97", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZuPWAawlVat6ZHb8vaH/CVUeGwI0pI4vd+6zz1ZocZn95ZWJztfyhzNZOJrq1WjHmUROieJ7cOuYUZfvYNuLrg=="],
|
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.99", "", { "os": "darwin", "cpu": "x64" }, "sha512-VE4FrXBYpkxnvkqcCV1a8aN9jyyMJMihVW+V2NLCtp+4yQsj0AapG5TiUSN76XnmSZRptxDy5rBmEempeoIZbg=="],
|
||||||
|
|
||||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.97", "", { "os": "linux", "cpu": "arm64" }, "sha512-QXxhz654vXgEu2wrFFFFnrSWbyk6/r6nXNnDTcMRWofdMZQLx87NhbcsErNmz9KmFdzoPiQSmlpYubLflKKzqQ=="],
|
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.99", "", { "os": "linux", "cpu": "arm64" }, "sha512-viXQsbpS7yHjYkl7+am32JdvG96QU9lvHh1UiZtpOxcNUUqiYmA2ZwZFPD2Bi54jNyj5l2hjH6YkD3DzE2FEWA=="],
|
||||||
|
|
||||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.97", "", { "os": "linux", "cpu": "x64" }, "sha512-v3z0QWpRS3p8blE/A7pTu15hcFMtSndeiYhRxhrjp6zAhQ+UlruQs9DAG1ifSuVO1RJJ0pUKklFivdbu0pMzuw=="],
|
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.99", "", { "os": "linux", "cpu": "x64" }, "sha512-WLoEFINOSp0tZSR9y4LUuGc7n4Y7H1wcpjUPzQ9vChkYDXrfZltEanzoDWbDcQ4kZQW5tHVC7LrZHpAsRLwFZg=="],
|
||||||
|
|
||||||
"@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-arm64": ["@opentui/core-win32-arm64@0.1.99", "", { "os": "win32", "cpu": "arm64" }, "sha512-yWMOLWCEO8HdrctU1dMkgZC8qGkiO4Dwr4/e11tTvVpRmYhDsP/IR89ZjEEtOwnKwFOFuB/MxvflqaEWVQ2g5Q=="],
|
||||||
|
|
||||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.97", "", { "os": "win32", "cpu": "x64" }, "sha512-Rwp7JOwrYm4wtzPHY2vv+2l91LXmKSI7CtbmWN1sSUGhBPtPGSvfwux3W5xaAZQa2KPEXicPjaKJZc+pob3YRg=="],
|
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.99", "", { "os": "win32", "cpu": "x64" }, "sha512-aYRlsL2w8YRL6vPd7/hrqlNVkXU3QowWb01TOvAcHS8UAsXaGFUr47kSDyjxDi1wg1MzmVduCfsC7T3NoThV1w=="],
|
||||||
|
|
||||||
"@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=="],
|
"@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=="],
|
||||||
|
|
||||||
"@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=="],
|
||||||
|
|
||||||
@@ -2385,7 +2398,7 @@
|
|||||||
|
|
||||||
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
|
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
|
||||||
|
|
||||||
"ai": ["ai@6.0.149", "", { "dependencies": { "@ai-sdk/gateway": "3.0.91", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3asRb/m3ZGH7H4+VTuTgj8eQYJZ9IJUmV0ljLslY92mQp6Zj+NVn4SmFj0TBr2Y/wFBWC3xgn++47tSGOXxdbw=="],
|
"ai": ["ai@6.0.158", "", { "dependencies": { "@ai-sdk/gateway": "3.0.95", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-gLTp1UXFtMqKUi3XHs33K7UFglbvojkxF/aq337TxnLGOhHIW9+GyP2jwW4hYX87f1es+wId3VQoPRRu9zEStQ=="],
|
||||||
|
|
||||||
"ai-gateway-provider": ["ai-gateway-provider@3.1.2", "", { "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^4.0.62", "@ai-sdk/anthropic": "^3.0.46", "@ai-sdk/azure": "^3.0.31", "@ai-sdk/cerebras": "^2.0.34", "@ai-sdk/cohere": "^3.0.21", "@ai-sdk/deepgram": "^2.0.20", "@ai-sdk/deepseek": "^2.0.20", "@ai-sdk/elevenlabs": "^2.0.20", "@ai-sdk/fireworks": "^2.0.34", "@ai-sdk/google": "^3.0.30", "@ai-sdk/google-vertex": "^4.0.61", "@ai-sdk/groq": "^3.0.24", "@ai-sdk/mistral": "^3.0.20", "@ai-sdk/openai": "^3.0.30", "@ai-sdk/perplexity": "^3.0.19", "@ai-sdk/xai": "^3.0.57", "@openrouter/ai-sdk-provider": "^2.2.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^2.0.0", "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.0", "ai": "^6.0.0" } }, "sha512-krGNnJSoO/gJ7Hbe5nQDlsBpDUGIBGtMQTRUaW7s1MylsfvLduba0TLWzQaGtOmNRkP0pGhtGlwsnS6FNQMlyw=="],
|
"ai-gateway-provider": ["ai-gateway-provider@3.1.2", "", { "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^4.0.62", "@ai-sdk/anthropic": "^3.0.46", "@ai-sdk/azure": "^3.0.31", "@ai-sdk/cerebras": "^2.0.34", "@ai-sdk/cohere": "^3.0.21", "@ai-sdk/deepgram": "^2.0.20", "@ai-sdk/deepseek": "^2.0.20", "@ai-sdk/elevenlabs": "^2.0.20", "@ai-sdk/fireworks": "^2.0.34", "@ai-sdk/google": "^3.0.30", "@ai-sdk/google-vertex": "^4.0.61", "@ai-sdk/groq": "^3.0.24", "@ai-sdk/mistral": "^3.0.20", "@ai-sdk/openai": "^3.0.30", "@ai-sdk/perplexity": "^3.0.19", "@ai-sdk/xai": "^3.0.57", "@openrouter/ai-sdk-provider": "^2.2.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^2.0.0", "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.0", "ai": "^6.0.0" } }, "sha512-krGNnJSoO/gJ7Hbe5nQDlsBpDUGIBGtMQTRUaW7s1MylsfvLduba0TLWzQaGtOmNRkP0pGhtGlwsnS6FNQMlyw=="],
|
||||||
|
|
||||||
@@ -2667,6 +2680,8 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
@@ -2889,7 +2904,7 @@
|
|||||||
|
|
||||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||||
|
|
||||||
"effect": ["effect@4.0.0-beta.43", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-AJYyDimIwJOn87uUz/JzmgDc5GfjxJbXvEbTvNzMa+M3Uer344bLo/O5mMRkqc1vBleA+Ygs4+dbE3QsqOkKTQ=="],
|
"effect": ["effect@4.0.0-beta.46", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-3f6gXvvUMtEueCRY0tU76Vq2Pej1SAwwE+s0Owd5nD53yS5n4RZhUA1rlCGFuSbQFA225pGy8vO72+lpvu7u5A=="],
|
||||||
|
|
||||||
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
||||||
|
|
||||||
@@ -3091,6 +3106,8 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
@@ -3329,6 +3346,8 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
@@ -4339,6 +4358,8 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
@@ -4411,6 +4432,8 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
@@ -5005,7 +5028,13 @@
|
|||||||
|
|
||||||
"@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/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
"@ai-sdk/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/@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/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
||||||
|
|
||||||
"@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
"@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||||
|
|
||||||
@@ -5281,10 +5310,6 @@
|
|||||||
|
|
||||||
"@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
|
"@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
|
||||||
|
|
||||||
"@gitlab/gitlab-ai-provider/openai": ["openai@6.33.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-xAYN1W3YsDXJWA5F277135YfkEk6H7D3D6vWwRhJ3OEkzRgcyK8z/P5P9Gyi/wB4N8kK9kM5ZjprfvyHagKmpw=="],
|
|
||||||
|
|
||||||
"@gitlab/gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
|
||||||
|
|
||||||
"@gitlab/opencode-gitlab-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
|
"@gitlab/opencode-gitlab-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
|
||||||
|
|
||||||
"@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
|
"@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
|
||||||
@@ -5509,6 +5534,10 @@
|
|||||||
|
|
||||||
"@solidjs/start/vite-plugin-solid": ["vite-plugin-solid@2.11.11", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-YMZCXsLw9kyuvQFEdwLP27fuTQJLmjNoHy90AOJnbRuJ6DwShUxKFo38gdFrWn9v11hnGicKCZEaeI/TFs6JKw=="],
|
"@solidjs/start/vite-plugin-solid": ["vite-plugin-solid@2.11.11", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-YMZCXsLw9kyuvQFEdwLP27fuTQJLmjNoHy90AOJnbRuJ6DwShUxKFo38gdFrWn9v11hnGicKCZEaeI/TFs6JKw=="],
|
||||||
|
|
||||||
|
"@standard-community/standard-json/effect": ["effect@4.0.0-beta.43", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-AJYyDimIwJOn87uUz/JzmgDc5GfjxJbXvEbTvNzMa+M3Uer344bLo/O5mMRkqc1vBleA+Ygs4+dbE3QsqOkKTQ=="],
|
||||||
|
|
||||||
|
"@standard-community/standard-openapi/effect": ["effect@4.0.0-beta.43", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-AJYyDimIwJOn87uUz/JzmgDc5GfjxJbXvEbTvNzMa+M3Uer344bLo/O5mMRkqc1vBleA+Ygs4+dbE3QsqOkKTQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
"@tailwindcss/oxide/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="],
|
||||||
@@ -5549,7 +5578,9 @@
|
|||||||
|
|
||||||
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
|
|
||||||
"ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.91", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-J39Dh6Gyg6HjG3A7OFKnJMp3QyZ3Eex+XDiX8aFBdRwwZm3jGWaMhkCxQPH7yiQ9kRiErZwHXX/Oexx4SyGGGA=="],
|
"ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.95", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZmUNNbZl3V42xwQzPaNUi+s8eqR2lnrxf0bvB6YbLXpLjHYv0k2Y78t12cNOfY0bxGeuVVTLyk856uLuQIuXEQ=="],
|
||||||
|
|
||||||
|
"ai-gateway-provider/@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.83", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.64", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21", "@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-DoRpvIWGU/r83UeJAM9L93Lca8Kf/yP5fIhfEOltMPGP/PXrGe0BZaz0maLSRn8djJ6+HzWIsgu5ZI6bZqXEXg=="],
|
||||||
|
|
||||||
"ai-gateway-provider/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.3.3", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-4fVteGkVedc7fGoA9+qJs4tpYwALezMq14m2Sjub3KmyRlksCbK+WJf67NPdGem8+NZrV2tAN42A1NU3+SiV3w=="],
|
"ai-gateway-provider/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.3.3", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-4fVteGkVedc7fGoA9+qJs4tpYwALezMq14m2Sjub3KmyRlksCbK+WJf67NPdGem8+NZrV2tAN42A1NU3+SiV3w=="],
|
||||||
|
|
||||||
@@ -5765,6 +5796,8 @@
|
|||||||
|
|
||||||
"opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.67", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-FFX4P5Fd6lcQJc2OLngZQkbbJHa0IDDZi087Edb8qRZx6h90krtM61ArbMUL8us/7ZUwojCXnyJ/wQ2Eflx2jQ=="],
|
"opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.67", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-FFX4P5Fd6lcQJc2OLngZQkbbJHa0IDDZi087Edb8qRZx6h90krtM61ArbMUL8us/7ZUwojCXnyJ/wQ2Eflx2jQ=="],
|
||||||
|
|
||||||
|
"opencode/@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=="],
|
||||||
|
|
||||||
"opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="],
|
"opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="],
|
||||||
|
|
||||||
"opencontrol/@tsconfig/bun": ["@tsconfig/bun@1.0.7", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="],
|
"opencontrol/@tsconfig/bun": ["@tsconfig/bun@1.0.7", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="],
|
||||||
@@ -5947,8 +5980,6 @@
|
|||||||
|
|
||||||
"@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
|
"@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
|
||||||
|
|
||||||
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
|
||||||
|
|
||||||
"@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
"@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
"@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
"@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
@@ -6435,12 +6466,18 @@
|
|||||||
|
|
||||||
"@solidjs/start/shiki/@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="],
|
"@solidjs/start/shiki/@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="],
|
||||||
|
|
||||||
|
"@standard-community/standard-json/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
|
"@standard-community/standard-openapi/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||||
|
|
||||||
"@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
|
"@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
|
||||||
|
|
||||||
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
|
|
||||||
|
"ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||||
|
|
||||||
"ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
"ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||||
|
|
||||||
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
@@ -6813,6 +6850,8 @@
|
|||||||
|
|
||||||
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
|
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
|
||||||
|
|
||||||
|
"ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
"app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
|
"app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-285KZ7rZLRoc6XqCZRHc25NE+mmpGh/BVeMpv8aPQtQ=",
|
"x86_64-linux": "sha256-1qeXIfqPCo5Px0ebdTvL09kav5Ib79E35Ed1FgiLnPY=",
|
||||||
"aarch64-linux": "sha256-qIwmY4TP4CI7R7G6A5OMYRrorVNXjkg25tTtVpIHm2o=",
|
"aarch64-linux": "sha256-987XZxDqDf6gm0kjXg606BSEYvryrUD8vZopsADQIN8=",
|
||||||
"aarch64-darwin": "sha256-RwvnZQhdYZ0u7h7evyfxuPLHHX9eO/jXTAxIFc8B+IE=",
|
"aarch64-darwin": "sha256-RI3D3bPxotudTWmdQNIPZ/oBrmDl5PAdJGc93M4bKHs=",
|
||||||
"x86_64-darwin": "sha256-vVj40al+TEeMpbe5XG2GmJEpN+eQAvtr9W0T98l5PBE="
|
"x86_64-darwin": "sha256-LE6hrDPXWKjcePc1nv+O6tIN0ZXUrzWI1XBN6Fm/NKw="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-18
@@ -7,7 +7,6 @@
|
|||||||
sysctl,
|
sysctl,
|
||||||
makeBinaryWrapper,
|
makeBinaryWrapper,
|
||||||
models-dev,
|
models-dev,
|
||||||
ripgrep,
|
|
||||||
installShellFiles,
|
installShellFiles,
|
||||||
versionCheckHook,
|
versionCheckHook,
|
||||||
writableTmpDirAsHomeHook,
|
writableTmpDirAsHomeHook,
|
||||||
@@ -52,25 +51,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
|
||||||
|
''
|
||||||
wrapProgram $out/bin/opencode \
|
# bun runs sysctl to detect if dunning on rosetta2
|
||||||
--prefix PATH : ${
|
+ lib.optionalString stdenvNoCC.hostPlatform.isDarwin ''
|
||||||
lib.makeBinPath (
|
wrapProgram $out/bin/opencode \
|
||||||
[
|
--prefix PATH : ${
|
||||||
ripgrep
|
lib.makeBinPath [
|
||||||
|
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
|
||||||
|
|||||||
+3
-3
@@ -26,7 +26,7 @@
|
|||||||
"packages/slack"
|
"packages/slack"
|
||||||
],
|
],
|
||||||
"catalog": {
|
"catalog": {
|
||||||
"@effect/platform-node": "4.0.0-beta.43",
|
"@effect/platform-node": "4.0.0-beta.46",
|
||||||
"@types/bun": "1.3.11",
|
"@types/bun": "1.3.11",
|
||||||
"@types/cross-spawn": "6.0.6",
|
"@types/cross-spawn": "6.0.6",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
@@ -47,8 +47,8 @@
|
|||||||
"dompurify": "3.3.1",
|
"dompurify": "3.3.1",
|
||||||
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
||||||
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
||||||
"effect": "4.0.0-beta.43",
|
"effect": "4.0.0-beta.46",
|
||||||
"ai": "6.0.149",
|
"ai": "6.0.158",
|
||||||
"cross-spawn": "7.0.6",
|
"cross-spawn": "7.0.6",
|
||||||
"hono": "4.10.7",
|
"hono": "4.10.7",
|
||||||
"hono-openapi": "1.1.2",
|
"hono-openapi": "1.1.2",
|
||||||
|
|||||||
@@ -155,7 +155,12 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
|||||||
resetHeartbeat()
|
resetHeartbeat()
|
||||||
streamErrorLogged = false
|
streamErrorLogged = false
|
||||||
const directory = event.directory ?? "global"
|
const directory = event.directory ?? "global"
|
||||||
const payload = event.payload
|
if (event.payload.type === "sync") {
|
||||||
|
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)
|
||||||
|
|||||||
@@ -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-9 pr-10"
|
class="flex w-full text-left justify-start text-14-regular text-text-weak pl-2 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-[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)]"
|
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)]"
|
||||||
style={{
|
style={{
|
||||||
"box-shadow":
|
"box-shadow":
|
||||||
"0 51px 60px 0 rgba(0,0,0,0.13), 0 15.375px 18.088px 0 rgba(0,0,0,0.19), 0 6.386px 7.513px 0 rgba(0,0,0,0.25), 0 2.31px 2.717px 0 rgba(0,0,0,0.38)",
|
"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)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon name="arrow-down-to-line" size="small" />
|
<Icon name="arrow-down-to-line" size="small" />
|
||||||
|
|||||||
@@ -316,7 +316,8 @@
|
|||||||
|
|
||||||
/* Download Hero Section */
|
/* Download Hero Section */
|
||||||
[data-component="download-hero"] {
|
[data-component="download-hero"] {
|
||||||
display: grid;
|
/* display: grid; */
|
||||||
|
display: none;
|
||||||
grid-template-columns: 260px 1fr;
|
grid-template-columns: 260px 1fr;
|
||||||
gap: 4rem;
|
gap: 4rem;
|
||||||
padding-bottom: 2rem;
|
padding-bottom: 2rem;
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ 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.trialProviders, ip)
|
const trialLimiter = createTrialLimiter(modelInfo.trialProvider, ip)
|
||||||
const trialProviders = await trialLimiter?.check()
|
const trialProviders = await trialLimiter?.check()
|
||||||
const rateLimiter = createRateLimiter(
|
const rateLimiter = createRateLimiter(
|
||||||
modelInfo.id,
|
modelInfo.id,
|
||||||
@@ -392,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 as keyof typeof zenData.models
|
const modelId = reqModel
|
||||||
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]
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ type Usage = {
|
|||||||
total_tokens?: number
|
total_tokens?: number
|
||||||
// used by moonshot
|
// used by moonshot
|
||||||
cached_tokens?: number
|
cached_tokens?: number
|
||||||
// used by xai
|
// used by xai & alibaba
|
||||||
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
|
||||||
@@ -62,6 +64,7 @@ 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)
|
||||||
@@ -72,7 +75,7 @@ export const oaCompatHelper: ProviderHelper = ({ adjustCacheUsage, safetyIdentif
|
|||||||
outputTokens,
|
outputTokens,
|
||||||
reasoningTokens,
|
reasoningTokens,
|
||||||
cacheReadTokens,
|
cacheReadTokens,
|
||||||
cacheWrite5mTokens: undefined,
|
cacheWrite5mTokens: cacheWriteTokens,
|
||||||
cacheWrite1hTokens: undefined,
|
cacheWrite1hTokens: undefined,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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(),
|
||||||
trialProviders: z.array(z.string()).optional(),
|
trialProvider: 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.string(),
|
apiKey: z.union([z.string(), z.record(z.string(), 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,7 +54,10 @@ export namespace ZenData {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const ModelsSchema = z.object({
|
const ModelsSchema = z.object({
|
||||||
models: z.record(z.string(), z.union([ModelSchema, z.array(ModelSchema.extend({ formatFilter: FormatSchema }))])),
|
zenModels: z.record(
|
||||||
|
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 }))]),
|
||||||
@@ -99,10 +102,66 @@ export namespace ZenData {
|
|||||||
Resource.ZEN_MODELS29.value +
|
Resource.ZEN_MODELS29.value +
|
||||||
Resource.ZEN_MODELS30.value,
|
Resource.ZEN_MODELS30.value,
|
||||||
)
|
)
|
||||||
const { models, liteModels, providers } = ModelsSchema.parse(json)
|
const { zenModels, 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 {
|
||||||
models: modelList === "lite" ? liteModels : models,
|
providers: Object.fromEntries(
|
||||||
providers,
|
Object.entries(providers).flatMap(([providerId, provider]) =>
|
||||||
|
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]
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: true,
|
show: false,
|
||||||
title: "OpenCode",
|
title: "OpenCode",
|
||||||
icon: iconPath(),
|
icon: iconPath(),
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
@@ -94,6 +94,10 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
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
@@ -0,0 +1,13 @@
|
|||||||
|
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
@@ -14,18 +14,11 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -39,11 +32,16 @@
|
|||||||
"bun": "./src/pty/pty.bun.ts",
|
"bun": "./src/pty/pty.bun.ts",
|
||||||
"node": "./src/pty/pty.node.ts",
|
"node": "./src/pty/pty.node.ts",
|
||||||
"default": "./src/pty/pty.bun.ts"
|
"default": "./src/pty/pty.bun.ts"
|
||||||
|
},
|
||||||
|
"#hono": {
|
||||||
|
"bun": "./src/server/adapter.bun.ts",
|
||||||
|
"node": "./src/server/adapter.node.ts",
|
||||||
|
"default": "./src/server/adapter.bun.ts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.28.4",
|
"@babel/core": "7.28.4",
|
||||||
"@effect/language-service": "0.79.0",
|
"@effect/language-service": "0.84.2",
|
||||||
"@octokit/webhooks-types": "7.6.1",
|
"@octokit/webhooks-types": "7.6.1",
|
||||||
"@opencode-ai/script": "workspace:*",
|
"@opencode-ai/script": "workspace:*",
|
||||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||||
@@ -78,7 +76,8 @@
|
|||||||
"@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/amazon-bedrock": "4.0.83",
|
"@ai-sdk/alibaba": "1.0.17",
|
||||||
|
"@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",
|
||||||
"@ai-sdk/cerebras": "2.0.41",
|
"@ai-sdk/cerebras": "2.0.41",
|
||||||
@@ -90,7 +89,7 @@
|
|||||||
"@ai-sdk/groq": "3.0.31",
|
"@ai-sdk/groq": "3.0.31",
|
||||||
"@ai-sdk/mistral": "3.0.27",
|
"@ai-sdk/mistral": "3.0.27",
|
||||||
"@ai-sdk/openai": "3.0.48",
|
"@ai-sdk/openai": "3.0.48",
|
||||||
"@ai-sdk/openai-compatible": "2.0.37",
|
"@ai-sdk/openai-compatible": "2.0.41",
|
||||||
"@ai-sdk/perplexity": "3.0.26",
|
"@ai-sdk/perplexity": "3.0.26",
|
||||||
"@ai-sdk/provider": "3.0.8",
|
"@ai-sdk/provider": "3.0.8",
|
||||||
"@ai-sdk/provider-utils": "4.0.23",
|
"@ai-sdk/provider-utils": "4.0.23",
|
||||||
@@ -100,7 +99,6 @@
|
|||||||
"@aws-sdk/credential-providers": "3.993.0",
|
"@aws-sdk/credential-providers": "3.993.0",
|
||||||
"@clack/prompts": "1.0.0-alpha.1",
|
"@clack/prompts": "1.0.0-alpha.1",
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
"@gitlab/gitlab-ai-provider": "3.6.0",
|
|
||||||
"@gitlab/opencode-gitlab-auth": "1.3.3",
|
"@gitlab/opencode-gitlab-auth": "1.3.3",
|
||||||
"@hono/node-server": "1.19.11",
|
"@hono/node-server": "1.19.11",
|
||||||
"@hono/node-ws": "1.3.0",
|
"@hono/node-ws": "1.3.0",
|
||||||
@@ -116,9 +114,9 @@
|
|||||||
"@opencode-ai/script": "workspace:*",
|
"@opencode-ai/script": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
"@openrouter/ai-sdk-provider": "2.4.2",
|
"@openrouter/ai-sdk-provider": "2.5.1",
|
||||||
"@opentui/core": "0.1.97",
|
"@opentui/core": "0.1.99",
|
||||||
"@opentui/solid": "0.1.97",
|
"@opentui/solid": "0.1.99",
|
||||||
"@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",
|
||||||
@@ -130,6 +128,7 @@
|
|||||||
"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",
|
||||||
@@ -144,6 +143,7 @@
|
|||||||
"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,6 +154,7 @@
|
|||||||
"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,6 +187,7 @@ 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/"
|
||||||
@@ -197,6 +198,9 @@ 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,
|
||||||
@@ -210,12 +214,19 @@ for (const item of targets) {
|
|||||||
files: {
|
files: {
|
||||||
...(embeddedFileMap ? { "opencode-web-ui.gen.ts": embeddedFileMap } : {}),
|
...(embeddedFileMap ? { "opencode-web-ui.gen.ts": embeddedFileMap } : {}),
|
||||||
},
|
},
|
||||||
entrypoints: ["./src/index.ts", parserWorker, workerPath, ...(embeddedFileMap ? ["opencode-web-ui.gen.ts"] : [])],
|
entrypoints: [
|
||||||
|
"./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,3 +1,5 @@
|
|||||||
|
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"
|
||||||
@@ -16,44 +18,58 @@ 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: InstanceBootstrap,
|
init: () => AppRuntime.runPromise(InstanceBootstrap),
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
await Config.waitForDependencies()
|
await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.waitForDependencies()))
|
||||||
await ToolRegistry.ids()
|
await AppRuntime.runPromise(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const registry = yield* ToolRegistry.Service
|
||||||
|
yield* registry.ids()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
const session = await Session.create({ title })
|
await AppRuntime.runPromise(
|
||||||
const messageID = MessageID.ascending()
|
Effect.gen(function* () {
|
||||||
const partID = PartID.ascending()
|
const session = yield* Session.Service
|
||||||
const message = {
|
const result = yield* session.create({ title })
|
||||||
id: messageID,
|
const messageID = MessageID.ascending()
|
||||||
sessionID: session.id,
|
const partID = PartID.ascending()
|
||||||
role: "user" as const,
|
const message = {
|
||||||
time: { created: now },
|
id: messageID,
|
||||||
agent: "build",
|
sessionID: result.id,
|
||||||
model: {
|
role: "user" as const,
|
||||||
providerID: ProviderID.make(providerID),
|
time: { created: now },
|
||||||
modelID: ModelID.make(modelID),
|
agent: "build",
|
||||||
},
|
model: {
|
||||||
}
|
providerID: ProviderID.make(providerID),
|
||||||
const part = {
|
modelID: ModelID.make(modelID),
|
||||||
id: partID,
|
},
|
||||||
sessionID: session.id,
|
}
|
||||||
messageID,
|
const part = {
|
||||||
type: "text" as const,
|
id: partID,
|
||||||
text,
|
sessionID: result.id,
|
||||||
time: { start: now },
|
messageID,
|
||||||
}
|
type: "text" as const,
|
||||||
await Session.updateMessage(message)
|
text,
|
||||||
await Session.updatePart(part)
|
time: { start: now },
|
||||||
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(() => {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,238 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,394 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# 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.
|
||||||
+62
-65
@@ -23,7 +23,7 @@ export namespace Foo {
|
|||||||
readonly get: (id: FooID) => Effect.Effect<FooInfo, FooError>
|
readonly get: (id: FooID) => Effect.Effect<FooInfo, FooError>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Foo") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/Foo") {}
|
||||||
|
|
||||||
export const layer = Layer.effect(
|
export const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
@@ -178,7 +178,9 @@ That is fine for leaf files like `schema.ts`. Keep the service surface in the ow
|
|||||||
|
|
||||||
## Migration checklist
|
## Migration checklist
|
||||||
|
|
||||||
Fully migrated (single namespace, InstanceState where needed, flattened facade):
|
Service-shape migrated (single namespace, traced methods, `InstanceState` where needed).
|
||||||
|
|
||||||
|
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`
|
||||||
@@ -217,62 +219,20 @@ Fully migrated (single namespace, InstanceState where needed, flattened facade):
|
|||||||
- [x] `SessionSummary` — `session/summary.ts`
|
- [x] `SessionSummary` — `session/summary.ts`
|
||||||
- [x] `SessionRevert` — `session/revert.ts`
|
- [x] `SessionRevert` — `session/revert.ts`
|
||||||
- [x] `Instruction` — `session/instruction.ts`
|
- [x] `Instruction` — `session/instruction.ts`
|
||||||
|
- [x] `SystemPrompt` — `session/system.ts`
|
||||||
- [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] `SessionTodo` — `session/todo.ts`
|
||||||
|
|
||||||
Still open:
|
Still open at the service-shape level:
|
||||||
|
|
||||||
- [ ] `SessionTodo` — `session/todo.ts`
|
- [ ] `SyncEvent` — `sync/index.ts` (deferred pending sync with James)
|
||||||
- [ ] `ShareNext` — `share/share-next.ts`
|
- [ ] `Workspace` — `control-plane/workspace.ts` (deferred pending sync with James)
|
||||||
- [ ] `SyncEvent` — `sync/index.ts`
|
|
||||||
- [ ] `Workspace` — `control-plane/workspace.ts`
|
|
||||||
|
|
||||||
## Tool interface → Effect
|
## Tool migration
|
||||||
|
|
||||||
Once individual tools are effectified, change `Tool.Info` (`tool/tool.ts`) so `init` and `execute` return `Effect` instead of `Promise`. This lets tool implementations compose natively with the Effect pipeline rather than being wrapped in `Effect.promise()` at the call site. Requires:
|
Tool-specific migration guidance and checklist live in `tools.md`.
|
||||||
|
|
||||||
1. Migrate each tool to return Effects
|
|
||||||
2. Update `Tool.define()` factory to work with Effects
|
|
||||||
3. Update `SessionPrompt` to `yield*` tool results instead of `await`ing
|
|
||||||
|
|
||||||
### Tool migration details
|
|
||||||
|
|
||||||
Until the tool interface itself returns `Effect`, use this transitional pattern for migrated tools:
|
|
||||||
|
|
||||||
- `Tool.defineEffect(...)` should `yield*` the services the tool depends on and close over them in the returned tool definition.
|
|
||||||
- Keep the bridge at the Promise boundary only. Prefer a single `Effect.runPromise(...)` in the temporary `async execute(...)` implementation, and move the inner logic into `Effect.fn(...)` helpers instead of scattering `runPromise` islands through the tool body.
|
|
||||||
- If a tool starts requiring new services, wire them into `ToolRegistry.defaultLayer` so production callers resolve the same dependencies as tests.
|
|
||||||
|
|
||||||
Tool tests should use the existing Effect helpers in `packages/opencode/test/lib/effect.ts`:
|
|
||||||
|
|
||||||
- Use `testEffect(...)` / `it.live(...)` instead of creating fake local wrappers around effectful tools.
|
|
||||||
- Yield the real tool export, then initialize it: `const info = yield* ReadTool`, `const tool = yield* Effect.promise(() => info.init())`.
|
|
||||||
- Run tests inside a real instance with `provideTmpdirInstance(...)` or `provideInstance(tmpdirScoped(...))` so instance-scoped services resolve exactly as they do in production.
|
|
||||||
|
|
||||||
This keeps migrated tool tests aligned with the production service graph today, and makes the eventual `Tool.Info` → `Effect` cleanup mostly mechanical later.
|
|
||||||
|
|
||||||
Individual tools, ordered by value:
|
|
||||||
|
|
||||||
- [ ] `apply_patch.ts` — HIGH: multi-step orchestration, error accumulation, Bus events
|
|
||||||
- [ ] `bash.ts` — HIGH: shell orchestration, quoting, timeout handling, output capture
|
|
||||||
- [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
|
||||||
|
|
||||||
@@ -280,27 +240,19 @@ Some already-effectified areas still use raw `Filesystem.*` or `Process.spawn` i
|
|||||||
|
|
||||||
### `Filesystem.*` → `AppFileSystem.Service` (yield in layer)
|
### `Filesystem.*` → `AppFileSystem.Service` (yield in layer)
|
||||||
|
|
||||||
- [ ] `file/index.ts` — 1 remaining `Filesystem.readText()` call in untracked diff handling
|
- [x] `config/config.ts` — `installDependencies()` now uses `AppFileSystem`
|
||||||
- [ ] `config/config.ts` — 5 remaining `Filesystem.*` calls in `installDependencies()`
|
- [x] `provider/provider.ts` — recent model state now reads via `AppFileSystem.Service`
|
||||||
- [ ] `provider/provider.ts` — 1 remaining `Filesystem.readJson()` call for recent model state
|
|
||||||
|
|
||||||
### `Process.spawn` → `ChildProcessSpawner` (yield in layer)
|
### `Process.spawn` → `ChildProcessSpawner` (yield in layer)
|
||||||
|
|
||||||
- [ ] `format/formatter.ts` — 2 remaining `Process.spawn()` checks (`air`, `uv`)
|
- [x] `format/formatter.ts` — direct `Process.spawn()` checks removed (`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` (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.
|
`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.
|
||||||
|
|
||||||
Similarly, **21 files** still import raw `fs` or `fs/promises` directly. These should migrate to `AppFileSystem` or `Filesystem.*` as they're touched.
|
Tool-specific filesystem cleanup notes live in `tools.md`.
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -308,3 +260,48 @@ Current raw fs users that will convert during tool migration:
|
|||||||
- [ ] `util/flock.ts` — file-based distributed lock with heartbeat → Effect.repeat + addFinalizer
|
- [ ] `util/flock.ts` — file-based distributed lock with heartbeat → Effect.repeat + addFinalizer
|
||||||
- [ ] `util/process.ts` — child process spawn wrapper → return Effect instead of Promise
|
- [ ] `util/process.ts` — child process spawn wrapper → return Effect instead of Promise
|
||||||
- [ ] `util/lazy.ts` — replace uses in Effect code with Effect.cached; keep for sync-only code
|
- [ ] `util/lazy.ts` — replace uses in Effect code with Effect.cached; keep for sync-only code
|
||||||
|
|
||||||
|
## 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`.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
For each service, the migration is roughly:
|
||||||
|
|
||||||
|
1. **Find callers.** `grep -n "Namespace\.(methodA|methodB|...)"` across `src/` and `test/`. Skip the service file itself.
|
||||||
|
2. **Migrate production callers.** For each effectful caller that does `Effect.tryPromise(() => Namespace.method(...))`:
|
||||||
|
- Add the service to the caller's layer R type (`Layer.Layer<Self, never, ... | Namespace.Service>`)
|
||||||
|
- Yield it at the top of the layer: `const ns = yield* Namespace.Service`
|
||||||
|
- Replace `Effect.tryPromise(() => Namespace.method(...))` with `yield* ns.method(...)` (or `ns.method(...).pipe(Effect.orElseSucceed(...))` for the common fallback case)
|
||||||
|
- Add `Layer.provide(Namespace.defaultLayer)` to the caller's own `defaultLayer` chain
|
||||||
|
3. **Fix tests that used the caller's raw `.layer`.** Any test that composes `Caller.layer` (not `defaultLayer`) needs to also provide the newly-required service tag. The fastest fix is usually switching to `Caller.defaultLayer` since it now pulls in the new dependency.
|
||||||
|
4. **Migrate test callers of the facade.** Tests calling `Namespace.method(...)` directly get converted to full effectful style using `testEffect(Namespace.defaultLayer)` + `it.live` / `it.effect` + `yield* svc.method(...)`. Don't wrap the test body in `Effect.promise(async () => {...})` — do the whole thing in `Effect.gen` and use `AppFileSystem.Service` / `tmpdirScoped` / `Effect.addFinalizer` for what used to be raw `fs` / `Bun.write` / `try/finally`.
|
||||||
|
5. **Delete the facades.** Once `grep` shows zero callers, remove the `export async function` block AND the `makeRuntime(...)` line from the service namespace. Also remove the now-unused `import { makeRuntime }`.
|
||||||
|
|
||||||
|
### Pitfalls
|
||||||
|
|
||||||
|
- **Layer caching inside tests.** `testEffect(layer)` constructs the Storage (or whatever) service once and memoizes it. If a test then tries `inner.pipe(Effect.provide(customStorage))` to swap in a differently-configured Storage, the outer cached one wins and the inner provision is a no-op. Fix: wrap the overriding layer in `Layer.fresh(...)`, which forces a new instance to be built instead of hitting the memoMap cache. This lets a single `testEffect(...)` serve both simple and per-test-customized cases.
|
||||||
|
- **`Effect.tryPromise` → `yield*` drops the Promise layer.** The old code was `Effect.tryPromise(() => Storage.read(...))` — a `tryPromise` wrapper because the facade returned a Promise. The new code is `yield* storage.read(...)` directly — the service method already returns an Effect, so no wrapper is needed. Don't reach for `Effect.promise` or `Effect.tryPromise` during migration; if you're using them on a service method call, you're doing it wrong.
|
||||||
|
- **Raw `.layer` test callers break silently in the type checker.** When you add a new R requirement to a service's `.layer`, any test that composes it raw (not `defaultLayer`) becomes under-specified. `tsgo` will flag this — the error looks like `Type 'Storage.Service' is not assignable to type '... | Service | TestConsole'`. Usually the fix is to switch that composition to `defaultLayer`, or add `Layer.provide(NewDep.defaultLayer)` to the custom composition.
|
||||||
|
- **Tests that do async setup with `fs`, `Bun.write`, `tmpdir`.** Convert these to `AppFileSystem.Service` calls inside `Effect.gen`, and use `tmpdirScoped()` instead of `tmpdir()` so cleanup happens via the scope finalizer. For file operations on the actual filesystem (not via a service), a small helper like `const writeJson = Effect.fnUntraced(function* (file, value) { const fs = yield* AppFileSystem.Service; yield* fs.makeDirectory(path.dirname(file), { recursive: true }); yield* fs.writeFileString(file, JSON.stringify(value, null, 2)) })` keeps the migration tests clean.
|
||||||
|
|
||||||
|
### Migration log
|
||||||
|
|
||||||
|
- `SessionStatus` — migrated 2026-04-11. Replaced the last route and retry-policy callers with `AppRuntime.runPromise(SessionStatus.Service.use(...))` and removed the `makeRuntime(...)` facade.
|
||||||
|
- `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.
|
||||||
|
- `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.
|
||||||
|
- `Account` — migrated 2026-04-11. Callers in `server/instance/experimental.ts` and `cli/cmd/account.ts` 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.
|
||||||
|
- `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.
|
||||||
|
- `Truncate` — migrated 2026-04-11. Caller in `tool/tool.ts` and test converted; facade removed.
|
||||||
|
|
||||||
|
## Route handler effectification
|
||||||
|
|
||||||
|
Route-handler migration guidance and checklist live in `routes.md`.
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,666 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# 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`
|
||||||
@@ -202,7 +202,7 @@ Top-level API groups exposed to `tui(api, options, meta)`:
|
|||||||
- `api.kv.get`, `set`, `ready`
|
- `api.kv.get`, `set`, `ready`
|
||||||
- `api.state`
|
- `api.state`
|
||||||
- `api.theme.current`, `selected`, `has`, `set`, `install`, `mode`, `ready`
|
- `api.theme.current`, `selected`, `has`, `set`, `install`, `mode`, `ready`
|
||||||
- `api.client`, `api.scopedClient(workspaceID?)`, `api.workspace.current()`, `api.workspace.set(workspaceID?)`
|
- `api.client`
|
||||||
- `api.event.on(type, handler)`
|
- `api.event.on(type, handler)`
|
||||||
- `api.renderer`
|
- `api.renderer`
|
||||||
- `api.slots.register(plugin)`
|
- `api.slots.register(plugin)`
|
||||||
@@ -270,7 +270,6 @@ Command behavior:
|
|||||||
- `provider`
|
- `provider`
|
||||||
- `path.{state,config,worktree,directory}`
|
- `path.{state,config,worktree,directory}`
|
||||||
- `vcs?.branch`
|
- `vcs?.branch`
|
||||||
- `workspace.list()` / `workspace.get(workspaceID)`
|
|
||||||
- `session.count()`
|
- `session.count()`
|
||||||
- `session.diff(sessionID)`
|
- `session.diff(sessionID)`
|
||||||
- `session.todo(sessionID)`
|
- `session.todo(sessionID)`
|
||||||
@@ -282,8 +281,6 @@ Command behavior:
|
|||||||
- `lsp()`
|
- `lsp()`
|
||||||
- `mcp()`
|
- `mcp()`
|
||||||
- `api.client` always reflects the current runtime client.
|
- `api.client` always reflects the current runtime client.
|
||||||
- `api.scopedClient(workspaceID?)` creates or reuses a client bound to a workspace.
|
|
||||||
- `api.workspace.set(...)` rebinds the active workspace; `api.client` follows that rebind.
|
|
||||||
- `api.event.on(type, handler)` subscribes to the TUI event stream and returns an unsubscribe function.
|
- `api.event.on(type, handler)` subscribes to the TUI event stream and returns an unsubscribe function.
|
||||||
- `api.renderer` exposes the raw `CliRenderer`.
|
- `api.renderer` exposes the raw `CliRenderer`.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Cache, Clock, Duration, Effect, Layer, Option, Schema, SchemaGetter, ServiceMap } from "effect"
|
import { Cache, Clock, Duration, Effect, Layer, Option, Schema, SchemaGetter, Context } from "effect"
|
||||||
import {
|
import {
|
||||||
FetchHttpClient,
|
FetchHttpClient,
|
||||||
HttpClient,
|
HttpClient,
|
||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
HttpClientResponse,
|
HttpClientResponse,
|
||||||
} from "effect/unstable/http"
|
} from "effect/unstable/http"
|
||||||
|
|
||||||
import { makeRuntime } from "@/effect/run-service"
|
|
||||||
import { withTransientReadRetry } from "@/util/effect-http-client"
|
import { withTransientReadRetry } from "@/util/effect-http-client"
|
||||||
import { AccountRepo, type AccountRow } from "./repo"
|
import { AccountRepo, type AccountRow } from "./repo"
|
||||||
import { normalizeServerUrl } from "./url"
|
import { normalizeServerUrl } from "./url"
|
||||||
@@ -181,7 +180,7 @@ export namespace Account {
|
|||||||
readonly poll: (input: Login) => Effect.Effect<PollResult, AccountError>
|
readonly poll: (input: Login) => Effect.Effect<PollResult, AccountError>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Account") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/Account") {}
|
||||||
|
|
||||||
export const layer: Layer.Layer<Service, never, AccountRepo | HttpClient.HttpClient> = Layer.effect(
|
export const layer: Layer.Layer<Service, never, AccountRepo | HttpClient.HttpClient> = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
@@ -454,18 +453,4 @@ export namespace Account {
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(AccountRepo.layer), Layer.provide(FetchHttpClient.layer))
|
export const defaultLayer = layer.pipe(Layer.provide(AccountRepo.layer), Layer.provide(FetchHttpClient.layer))
|
||||||
|
|
||||||
export const { runPromise } = makeRuntime(Service, defaultLayer)
|
|
||||||
|
|
||||||
export async function active(): Promise<Info | undefined> {
|
|
||||||
return Option.getOrUndefined(await runPromise((service) => service.active()))
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function orgsByAccount(): Promise<readonly AccountOrgs[]> {
|
|
||||||
return runPromise((service) => service.orgsByAccount())
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function switchOrg(accountID: AccountID, orgID: OrgID) {
|
|
||||||
return runPromise((service) => service.use(accountID, Option.some(orgID)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { eq } from "drizzle-orm"
|
import { eq } from "drizzle-orm"
|
||||||
import { Effect, Layer, Option, Schema, ServiceMap } from "effect"
|
import { Effect, Layer, Option, Schema, Context } from "effect"
|
||||||
|
|
||||||
import { Database } from "@/storage/db"
|
import { Database } from "@/storage/db"
|
||||||
import { AccountStateTable, AccountTable } from "./account.sql"
|
import { AccountStateTable, AccountTable } from "./account.sql"
|
||||||
@@ -38,7 +38,7 @@ export namespace AccountRepo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AccountRepo extends ServiceMap.Service<AccountRepo, AccountRepo.Service>()("@opencode/AccountRepo") {
|
export class AccountRepo extends Context.Service<AccountRepo, AccountRepo.Service>()("@opencode/AccountRepo") {
|
||||||
static readonly layer: Layer.Layer<AccountRepo> = Layer.effect(
|
static readonly layer: Layer.Layer<AccountRepo> = Layer.effect(
|
||||||
AccountRepo,
|
AccountRepo,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|||||||
@@ -1,42 +1,22 @@
|
|||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import type * as HttpClientError from "effect/unstable/http/HttpClientError"
|
import type * as HttpClientError from "effect/unstable/http/HttpClientError"
|
||||||
|
|
||||||
import { withStatics } from "@/util/schema"
|
export const AccountID = Schema.String.pipe(Schema.brand("AccountID"))
|
||||||
|
|
||||||
export const AccountID = Schema.String.pipe(
|
|
||||||
Schema.brand("AccountID"),
|
|
||||||
withStatics((s) => ({ make: (id: string) => s.makeUnsafe(id) })),
|
|
||||||
)
|
|
||||||
export type AccountID = Schema.Schema.Type<typeof AccountID>
|
export type AccountID = Schema.Schema.Type<typeof AccountID>
|
||||||
|
|
||||||
export const OrgID = Schema.String.pipe(
|
export const OrgID = Schema.String.pipe(Schema.brand("OrgID"))
|
||||||
Schema.brand("OrgID"),
|
|
||||||
withStatics((s) => ({ make: (id: string) => s.makeUnsafe(id) })),
|
|
||||||
)
|
|
||||||
export type OrgID = Schema.Schema.Type<typeof OrgID>
|
export type OrgID = Schema.Schema.Type<typeof OrgID>
|
||||||
|
|
||||||
export const AccessToken = Schema.String.pipe(
|
export const AccessToken = Schema.String.pipe(Schema.brand("AccessToken"))
|
||||||
Schema.brand("AccessToken"),
|
|
||||||
withStatics((s) => ({ make: (token: string) => s.makeUnsafe(token) })),
|
|
||||||
)
|
|
||||||
export type AccessToken = Schema.Schema.Type<typeof AccessToken>
|
export type AccessToken = Schema.Schema.Type<typeof AccessToken>
|
||||||
|
|
||||||
export const RefreshToken = Schema.String.pipe(
|
export const RefreshToken = Schema.String.pipe(Schema.brand("RefreshToken"))
|
||||||
Schema.brand("RefreshToken"),
|
|
||||||
withStatics((s) => ({ make: (token: string) => s.makeUnsafe(token) })),
|
|
||||||
)
|
|
||||||
export type RefreshToken = Schema.Schema.Type<typeof RefreshToken>
|
export type RefreshToken = Schema.Schema.Type<typeof RefreshToken>
|
||||||
|
|
||||||
export const DeviceCode = Schema.String.pipe(
|
export const DeviceCode = Schema.String.pipe(Schema.brand("DeviceCode"))
|
||||||
Schema.brand("DeviceCode"),
|
|
||||||
withStatics((s) => ({ make: (code: string) => s.makeUnsafe(code) })),
|
|
||||||
)
|
|
||||||
export type DeviceCode = Schema.Schema.Type<typeof DeviceCode>
|
export type DeviceCode = Schema.Schema.Type<typeof DeviceCode>
|
||||||
|
|
||||||
export const UserCode = Schema.String.pipe(
|
export const UserCode = Schema.String.pipe(Schema.brand("UserCode"))
|
||||||
Schema.brand("UserCode"),
|
|
||||||
withStatics((s) => ({ make: (code: string) => s.makeUnsafe(code) })),
|
|
||||||
)
|
|
||||||
export type UserCode = Schema.Schema.Type<typeof UserCode>
|
export type UserCode = Schema.Schema.Type<typeof UserCode>
|
||||||
|
|
||||||
export class Info extends Schema.Class<Info>("Account")({
|
export class Info extends Schema.Class<Info>("Account")({
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ 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"
|
||||||
@@ -452,19 +453,12 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1166,7 +1160,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 AgentModule.defaultAgent()
|
const defaultAgentName = await AppRuntime.runPromise(AgentModule.Service.use((svc) => svc.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)
|
||||||
@@ -1367,7 +1361,8 @@ export namespace ACP {
|
|||||||
if (!current) {
|
if (!current) {
|
||||||
this.sessionManager.setModel(session.id, model)
|
this.sessionManager.setModel(session.id, model)
|
||||||
}
|
}
|
||||||
const agent = session.modeId ?? (await AgentModule.defaultAgent())
|
const agent =
|
||||||
|
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 }
|
||||||
|
|||||||
@@ -19,9 +19,8 @@ import { Global } from "@/global"
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { Plugin } from "@/plugin"
|
import { Plugin } from "@/plugin"
|
||||||
import { Skill } from "../skill"
|
import { Skill } from "../skill"
|
||||||
import { Effect, ServiceMap, 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
|
||||||
@@ -67,13 +66,14 @@ export namespace Agent {
|
|||||||
|
|
||||||
type State = Omit<Interface, "generate">
|
type State = Omit<Interface, "generate">
|
||||||
|
|
||||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Agent") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/Agent") {}
|
||||||
|
|
||||||
export const layer = Layer.effect(
|
export const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
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,9 +336,7 @@ export namespace Agent {
|
|||||||
const language = yield* provider.getLanguage(resolved)
|
const language = yield* provider.getLanguage(resolved)
|
||||||
|
|
||||||
const system = [PROMPT_GENERATE]
|
const system = [PROMPT_GENERATE]
|
||||||
yield* Effect.promise(() =>
|
yield* plugin.trigger("experimental.chat.system.transform", { model: resolved }, { system })
|
||||||
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
|
||||||
@@ -398,30 +396,11 @@ export namespace Agent {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = Layer.suspend(() =>
|
export const defaultLayer = layer.pipe(
|
||||||
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
@@ -0,0 +1,4 @@
|
|||||||
|
declare module "*.wav" {
|
||||||
|
const file: string
|
||||||
|
export default file
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect, Layer, Record, Result, Schema, ServiceMap } 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"
|
||||||
@@ -49,7 +48,7 @@ export namespace Auth {
|
|||||||
readonly remove: (key: string) => Effect.Effect<void, AuthError>
|
readonly remove: (key: string) => Effect.Effect<void, AuthError>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Auth") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/Auth") {}
|
||||||
|
|
||||||
export const layer = Layer.effect(
|
export const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
@@ -89,22 +88,4 @@ 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,25 +16,18 @@ export namespace BusEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function payloads() {
|
export function payloads() {
|
||||||
return z
|
return registry
|
||||||
.discriminatedUnion(
|
.entries()
|
||||||
"type",
|
.map(([type, def]) => {
|
||||||
registry
|
return z
|
||||||
.entries()
|
.object({
|
||||||
.map(([type, def]) => {
|
type: z.literal(type),
|
||||||
return z
|
properties: def.properties,
|
||||||
.object({
|
})
|
||||||
type: z.literal(type),
|
.meta({
|
||||||
properties: def.properties,
|
ref: "Event" + "." + def.type,
|
||||||
})
|
|
||||||
.meta({
|
|
||||||
ref: "Event" + "." + def.type,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.toArray() as any,
|
|
||||||
)
|
|
||||||
.meta({
|
|
||||||
ref: "Event",
|
|
||||||
})
|
})
|
||||||
|
.toArray()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Effect, Exit, Layer, PubSub, Scope, ServiceMap, Stream } from "effect"
|
import { Effect, Exit, Layer, PubSub, Scope, Context, Stream } from "effect"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { BusEvent } from "./bus-event"
|
import { BusEvent } from "./bus-event"
|
||||||
import { GlobalBus } from "./global"
|
import { GlobalBus } from "./global"
|
||||||
@@ -41,7 +41,7 @@ export namespace Bus {
|
|||||||
readonly subscribeAllCallback: (callback: (event: any) => unknown) => Effect.Effect<() => void>
|
readonly subscribeAllCallback: (callback: (event: any) => unknown) => Effect.Effect<() => void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Bus") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/Bus") {}
|
||||||
|
|
||||||
export const layer = Layer.effect(
|
export const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
@@ -127,6 +127,7 @@ export namespace Bus {
|
|||||||
function on<T>(pubsub: PubSub.PubSub<T>, type: string, callback: (event: T) => unknown) {
|
function on<T>(pubsub: PubSub.PubSub<T>, type: string, callback: (event: T) => unknown) {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
log.info("subscribing", { type })
|
log.info("subscribing", { type })
|
||||||
|
const ctx = yield* Effect.context()
|
||||||
const scope = yield* Scope.make()
|
const scope = yield* Scope.make()
|
||||||
const subscription = yield* Scope.provide(scope)(PubSub.subscribe(pubsub))
|
const subscription = yield* Scope.provide(scope)(PubSub.subscribe(pubsub))
|
||||||
|
|
||||||
@@ -146,7 +147,7 @@ export namespace Bus {
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
log.info("unsubscribing", { type })
|
log.info("unsubscribing", { type })
|
||||||
Effect.runFork(Scope.close(scope, Exit.void))
|
Effect.runForkWith(ctx)(Scope.close(scope, Exit.void))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -169,6 +170,8 @@ export namespace Bus {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const defaultLayer = layer
|
||||||
|
|
||||||
const { runPromise, runSync } = makeRuntime(Service, layer)
|
const { runPromise, runSync } = makeRuntime(Service, layer)
|
||||||
|
|
||||||
// runSync is safe here because the subscribe chain (InstanceState.get, PubSub.subscribe,
|
// runSync is safe here because the subscribe chain (InstanceState.get, PubSub.subscribe,
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
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: InstanceBootstrap,
|
init: () => AppRuntime.runPromise(InstanceBootstrap),
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
try {
|
try {
|
||||||
const result = await cb()
|
const result = await cb()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Duration, Effect, Match, Option } from "effect"
|
|||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import { AccountID, Account, OrgID, PollExpired, type PollResult } from "@/account"
|
import { AccountID, Account, OrgID, PollExpired, type PollResult } from "@/account"
|
||||||
import { type AccountError } from "@/account/schema"
|
import { type AccountError } from "@/account/schema"
|
||||||
|
import { AppRuntime } from "@/effect/app-runtime"
|
||||||
import * as Prompt from "../effect/prompt"
|
import * as Prompt from "../effect/prompt"
|
||||||
import open from "open"
|
import open from "open"
|
||||||
|
|
||||||
@@ -182,7 +183,7 @@ export const LoginCommand = cmd({
|
|||||||
}),
|
}),
|
||||||
async handler(args) {
|
async handler(args) {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
await Account.runPromise((_svc) => loginEffect(args.url))
|
await AppRuntime.runPromise(loginEffect(args.url))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -196,7 +197,7 @@ export const LogoutCommand = cmd({
|
|||||||
}),
|
}),
|
||||||
async handler(args) {
|
async handler(args) {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
await Account.runPromise((_svc) => logoutEffect(args.email))
|
await AppRuntime.runPromise(logoutEffect(args.email))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -205,7 +206,7 @@ export const SwitchCommand = cmd({
|
|||||||
describe: false,
|
describe: false,
|
||||||
async handler() {
|
async handler() {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
await Account.runPromise((_svc) => switchEffect())
|
await AppRuntime.runPromise(switchEffect())
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -214,7 +215,7 @@ export const OrgsCommand = cmd({
|
|||||||
describe: false,
|
describe: false,
|
||||||
async handler() {
|
async handler() {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
await Account.runPromise((_svc) => orgsEffect())
|
await AppRuntime.runPromise(orgsEffect())
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -223,7 +224,7 @@ export const OpenCommand = cmd({
|
|||||||
describe: false,
|
describe: false,
|
||||||
async handler() {
|
async handler() {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
await Account.runPromise((_svc) => openEffect())
|
await AppRuntime.runPromise(openEffect())
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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"
|
||||||
@@ -110,7 +111,9 @@ 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 Agent.generate({ description, model }).catch((error) => {
|
const generated = await AppRuntime.runPromise(
|
||||||
|
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()
|
||||||
@@ -220,7 +223,7 @@ const AgentListCommand = cmd({
|
|||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: process.cwd(),
|
directory: process.cwd(),
|
||||||
async fn() {
|
async fn() {
|
||||||
const agents = await Agent.list()
|
const agents = await AppRuntime.runPromise(Agent.Service.use((svc) => svc.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
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { basename } from "path"
|
import { basename } from "path"
|
||||||
|
import { Effect } from "effect"
|
||||||
import { Agent } from "../../../agent/agent"
|
import { Agent } from "../../../agent/agent"
|
||||||
import { Provider } from "../../../provider/provider"
|
import { Provider } from "../../../provider/provider"
|
||||||
import { Session } from "../../../session"
|
import { Session } from "../../../session"
|
||||||
@@ -11,6 +12,7 @@ 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>",
|
||||||
@@ -33,7 +35,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 Agent.get(agentName)
|
const agent = await AppRuntime.runPromise(Agent.Service.use((svc) => svc.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,
|
||||||
@@ -70,11 +72,17 @@ export const AgentCommand = cmd({
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function getAvailableTools(agent: Agent.Info) {
|
async function getAvailableTools(agent: Agent.Info) {
|
||||||
const model = agent.model ?? (await Provider.defaultModel())
|
return AppRuntime.runPromise(
|
||||||
return ToolRegistry.tools({
|
Effect.gen(function* () {
|
||||||
...model,
|
const provider = yield* Provider.Service
|
||||||
agent,
|
const registry = yield* ToolRegistry.Service
|
||||||
})
|
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>>) {
|
||||||
@@ -115,38 +123,49 @@ function parseToolParams(input?: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createToolContext(agent: Agent.Info) {
|
async function createToolContext(agent: Agent.Info) {
|
||||||
const session = await Session.create({ title: `Debug tool run (${agent.name})` })
|
const { session, messageID } = await AppRuntime.runPromise(
|
||||||
const messageID = MessageID.ascending()
|
Effect.gen(function* () {
|
||||||
const model = agent.model ?? (await Provider.defaultModel())
|
const session = yield* Session.Service
|
||||||
const now = Date.now()
|
const result = yield* session.create({ title: `Debug tool run (${agent.name})` })
|
||||||
const message: MessageV2.Assistant = {
|
const messageID = MessageID.ascending()
|
||||||
id: messageID,
|
const model = agent.model
|
||||||
sessionID: session.id,
|
? agent.model
|
||||||
role: "assistant",
|
: yield* Effect.gen(function* () {
|
||||||
time: {
|
const provider = yield* Provider.Service
|
||||||
created: now,
|
return yield* provider.defaultModel()
|
||||||
},
|
})
|
||||||
parentID: messageID,
|
const now = Date.now()
|
||||||
modelID: model.modelID,
|
const message: MessageV2.Assistant = {
|
||||||
providerID: model.providerID,
|
id: messageID,
|
||||||
mode: "debug",
|
sessionID: result.id,
|
||||||
agent: agent.name,
|
role: "assistant",
|
||||||
path: {
|
time: {
|
||||||
cwd: Instance.directory,
|
created: now,
|
||||||
root: Instance.worktree,
|
},
|
||||||
},
|
parentID: messageID,
|
||||||
cost: 0,
|
modelID: model.modelID,
|
||||||
tokens: {
|
providerID: model.providerID,
|
||||||
input: 0,
|
mode: "debug",
|
||||||
output: 0,
|
agent: agent.name,
|
||||||
reasoning: 0,
|
path: {
|
||||||
cache: {
|
cwd: Instance.directory,
|
||||||
read: 0,
|
root: Instance.worktree,
|
||||||
write: 0,
|
},
|
||||||
},
|
cost: 0,
|
||||||
},
|
tokens: {
|
||||||
}
|
input: 0,
|
||||||
await Session.updateMessage(message)
|
output: 0,
|
||||||
|
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 ?? [])
|
||||||
|
|
||||||
@@ -157,14 +176,16 @@ async function createToolContext(agent: Agent.Info) {
|
|||||||
agent: agent.name,
|
agent: agent.name,
|
||||||
abort: new AbortController().signal,
|
abort: new AbortController().signal,
|
||||||
messages: [],
|
messages: [],
|
||||||
metadata: () => {},
|
metadata: () => Effect.void,
|
||||||
async ask(req: Omit<Permission.Request, "id" | "sessionID" | "tool">) {
|
ask(req: Omit<Permission.Request, "id" | "sessionID" | "tool">) {
|
||||||
for (const pattern of req.patterns) {
|
return Effect.sync(() => {
|
||||||
const rule = Permission.evaluate(req.permission, pattern, ruleset)
|
for (const pattern of req.patterns) {
|
||||||
if (rule.action === "deny") {
|
const rule = Permission.evaluate(req.permission, pattern, ruleset)
|
||||||
throw new Permission.DeniedError({ ruleset })
|
if (rule.action === "deny") {
|
||||||
|
throw new Permission.DeniedError({ ruleset })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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"
|
||||||
|
|
||||||
@@ -9,7 +10,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 Config.get()
|
const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get()))
|
||||||
process.stdout.write(JSON.stringify(config, null, 2) + EOL)
|
process.stdout.write(JSON.stringify(config, null, 2) + EOL)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
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"
|
||||||
@@ -15,7 +17,11 @@ const FileSearchCommand = cmd({
|
|||||||
}),
|
}),
|
||||||
async handler(args) {
|
async handler(args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
const results = await File.search({ query: args.query })
|
const results = await AppRuntime.runPromise(
|
||||||
|
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)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -32,7 +38,11 @@ const FileReadCommand = cmd({
|
|||||||
}),
|
}),
|
||||||
async handler(args) {
|
async handler(args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
const content = await File.read(args.path)
|
const content = await AppRuntime.runPromise(
|
||||||
|
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)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -44,7 +54,11 @@ 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 File.status()
|
const status = await AppRuntime.runPromise(
|
||||||
|
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)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -61,7 +75,11 @@ const FileListCommand = cmd({
|
|||||||
}),
|
}),
|
||||||
async handler(args) {
|
async handler(args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
const files = await File.list(args.path)
|
const files = await AppRuntime.runPromise(
|
||||||
|
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,4 +1,6 @@
|
|||||||
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"
|
||||||
@@ -19,9 +21,16 @@ 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 () => {
|
||||||
await LSP.touchFile(args.file, true)
|
const out = await AppRuntime.runPromise(
|
||||||
await sleep(1000)
|
LSP.Service.use((lsp) =>
|
||||||
process.stdout.write(JSON.stringify(await LSP.diagnostics(), null, 2) + EOL)
|
Effect.gen(function* () {
|
||||||
|
yield* lsp.touchFile(args.file, true)
|
||||||
|
yield* Effect.sleep(1000)
|
||||||
|
return yield* lsp.diagnostics()
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
process.stdout.write(JSON.stringify(out, null, 2) + EOL)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -33,7 +42,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 LSP.workspaceSymbol(args.query)
|
const results = await AppRuntime.runPromise(LSP.Service.use((lsp) => lsp.workspaceSymbol(args.query)))
|
||||||
process.stdout.write(JSON.stringify(results, null, 2) + EOL)
|
process.stdout.write(JSON.stringify(results, null, 2) + EOL)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -46,7 +55,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 LSP.documentSymbol(args.uri)
|
const results = await AppRuntime.runPromise(LSP.Service.use((lsp) => lsp.documentSymbol(args.uri)))
|
||||||
process.stdout.write(JSON.stringify(results, null, 2) + EOL)
|
process.stdout.write(JSON.stringify(results, null, 2) + EOL)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
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"
|
||||||
@@ -45,7 +46,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 Ripgrep.files({
|
for await (const file of await Ripgrep.files({
|
||||||
cwd: Instance.directory,
|
cwd: Instance.directory,
|
||||||
glob: args.glob ? [args.glob] : undefined,
|
glob: args.glob ? [args.glob] : undefined,
|
||||||
})) {
|
})) {
|
||||||
@@ -76,12 +77,18 @@ const SearchCommand = cmd({
|
|||||||
description: "Limit number of results",
|
description: "Limit number of results",
|
||||||
}),
|
}),
|
||||||
async handler(args) {
|
async handler(args) {
|
||||||
const results = await Ripgrep.search({
|
await bootstrap(process.cwd(), async () => {
|
||||||
cwd: process.cwd(),
|
const results = await AppRuntime.runPromise(
|
||||||
pattern: args.pattern,
|
Ripgrep.Service.use((svc) =>
|
||||||
glob: args.glob as string[] | undefined,
|
svc.search({
|
||||||
limit: args.limit,
|
cwd: Instance.directory,
|
||||||
|
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,4 +1,6 @@
|
|||||||
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"
|
||||||
@@ -9,7 +11,12 @@ 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 Skill.all()
|
const skills = await AppRuntime.runPromise(
|
||||||
|
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,3 +1,4 @@
|
|||||||
|
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"
|
||||||
@@ -14,7 +15,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 Snapshot.track())
|
console.log(await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.track())))
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -30,7 +31,7 @@ const PatchCommand = cmd({
|
|||||||
}),
|
}),
|
||||||
async handler(args) {
|
async handler(args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
console.log(await Snapshot.patch(args.hash))
|
console.log(await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.patch(args.hash))))
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -46,7 +47,7 @@ const DiffCommand = cmd({
|
|||||||
}),
|
}),
|
||||||
async handler(args) {
|
async handler(args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
console.log(await Snapshot.diff(args.hash))
|
console.log(await AppRuntime.runPromise(Snapshot.Service.use((svc) => svc.diff(args.hash))))
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,20 +1,238 @@
|
|||||||
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.positional("sessionID", {
|
return yargs
|
||||||
describe: "session id to export",
|
.positional("sessionID", {
|
||||||
type: "string",
|
describe: "session id to export",
|
||||||
})
|
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 () => {
|
||||||
@@ -67,18 +285,17 @@ export const ExportCommand = cmd({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sessionInfo = await Session.get(sessionID!)
|
const sessionInfo = await AppRuntime.runPromise(Session.Service.use((svc) => svc.get(sessionID!)))
|
||||||
const messages = await Session.messages({ sessionID: sessionInfo.id })
|
const messages = await AppRuntime.runPromise(
|
||||||
|
Session.Service.use((svc) => svc.messages({ sessionID: sessionInfo.id })),
|
||||||
|
)
|
||||||
|
|
||||||
const exportData = {
|
const exportData = {
|
||||||
info: sessionInfo,
|
info: sessionInfo,
|
||||||
messages: messages.map((msg) => ({
|
messages,
|
||||||
info: msg.info,
|
|
||||||
parts: msg.parts,
|
|
||||||
})),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
process.stdout.write(JSON.stringify(exportData, null, 2))
|
process.stdout.write(JSON.stringify(args.sanitize ? sanitize(exportData) : 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!}`)
|
||||||
|
|||||||
@@ -29,9 +29,11 @@ import { Provider } from "../../provider/provider"
|
|||||||
import { Bus } from "../../bus"
|
import { Bus } from "../../bus"
|
||||||
import { MessageV2 } from "../../session/message-v2"
|
import { MessageV2 } from "../../session/message-v2"
|
||||||
import { SessionPrompt } from "@/session/prompt"
|
import { SessionPrompt } from "@/session/prompt"
|
||||||
|
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
|
||||||
@@ -258,7 +260,9 @@ export const GithubInstallCommand = cmd({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get repo info
|
// Get repo info
|
||||||
const info = (await Git.run(["remote", "get-url", "origin"], { cwd: Instance.worktree })).text().trim()
|
const info = await AppRuntime.runPromise(
|
||||||
|
Git.Service.use((git) => git.run(["remote", "get-url", "origin"], { cwd: Instance.worktree })),
|
||||||
|
).then((x) => x.text().trim())
|
||||||
const parsed = parseGitHubRemote(info)
|
const parsed = parseGitHubRemote(info)
|
||||||
if (!parsed) {
|
if (!parsed) {
|
||||||
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
|
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
|
||||||
@@ -497,20 +501,21 @@ export const GithubRunCommand = cmd({
|
|||||||
: "issue"
|
: "issue"
|
||||||
: undefined
|
: undefined
|
||||||
const gitText = async (args: string[]) => {
|
const gitText = async (args: string[]) => {
|
||||||
const result = await Git.run(args, { cwd: Instance.worktree })
|
const result = await AppRuntime.runPromise(Git.Service.use((git) => git.run(args, { cwd: Instance.worktree })))
|
||||||
if (result.exitCode !== 0) {
|
if (result.exitCode !== 0) {
|
||||||
throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr)
|
throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr)
|
||||||
}
|
}
|
||||||
return result.text().trim()
|
return result.text().trim()
|
||||||
}
|
}
|
||||||
const gitRun = async (args: string[]) => {
|
const gitRun = async (args: string[]) => {
|
||||||
const result = await Git.run(args, { cwd: Instance.worktree })
|
const result = await AppRuntime.runPromise(Git.Service.use((git) => git.run(args, { cwd: Instance.worktree })))
|
||||||
if (result.exitCode !== 0) {
|
if (result.exitCode !== 0) {
|
||||||
throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr)
|
throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
const gitStatus = (args: string[]) => Git.run(args, { cwd: Instance.worktree })
|
const gitStatus = (args: string[]) =>
|
||||||
|
AppRuntime.runPromise(Git.Service.use((git) => git.run(args, { cwd: Instance.worktree })))
|
||||||
const commitChanges = async (summary: string, actor?: string) => {
|
const commitChanges = async (summary: string, actor?: string) => {
|
||||||
const args = ["commit", "-m", summary]
|
const args = ["commit", "-m", summary]
|
||||||
if (actor) args.push("-m", `Co-authored-by: ${actor} <${actor}@users.noreply.github.com>`)
|
if (actor) args.push("-m", `Co-authored-by: ${actor} <${actor}@users.noreply.github.com>`)
|
||||||
@@ -547,20 +552,24 @@ export const GithubRunCommand = cmd({
|
|||||||
|
|
||||||
// Setup opencode session
|
// Setup opencode session
|
||||||
const repoData = await fetchRepo()
|
const repoData = await fetchRepo()
|
||||||
session = await Session.create({
|
session = await AppRuntime.runPromise(
|
||||||
permission: [
|
Session.Service.use((svc) =>
|
||||||
{
|
svc.create({
|
||||||
permission: "question",
|
permission: [
|
||||||
action: "deny",
|
{
|
||||||
pattern: "*",
|
permission: "question",
|
||||||
},
|
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 SessionShare.share(session.id)
|
await AppRuntime.runPromise(SessionShare.Service.use((svc) => svc.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)
|
||||||
@@ -933,96 +942,86 @@ 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...")
|
||||||
|
|
||||||
const result = await SessionPrompt.prompt({
|
return AppRuntime.runPromise(
|
||||||
sessionID: session.id,
|
Effect.gen(function* () {
|
||||||
messageID: MessageID.ascending(),
|
const prompt = yield* SessionPrompt.Service
|
||||||
variant,
|
const result = yield* prompt.prompt({
|
||||||
model: {
|
sessionID: session.id,
|
||||||
providerID,
|
messageID: MessageID.ascending(),
|
||||||
modelID,
|
variant,
|
||||||
},
|
model: {
|
||||||
// agent is omitted - server will use default_agent from config or fall back to "build"
|
providerID,
|
||||||
parts: [
|
modelID,
|
||||||
{
|
|
||||||
id: PartID.ascending(),
|
|
||||||
type: "text",
|
|
||||||
text: message,
|
|
||||||
},
|
|
||||||
...files.flatMap((f) => [
|
|
||||||
{
|
|
||||||
id: PartID.ascending(),
|
|
||||||
type: "file" as const,
|
|
||||||
mime: f.mime,
|
|
||||||
url: `data:${f.mime};base64,${f.content}`,
|
|
||||||
filename: f.filename,
|
|
||||||
source: {
|
|
||||||
type: "file" as const,
|
|
||||||
text: {
|
|
||||||
value: f.replacement,
|
|
||||||
start: f.start,
|
|
||||||
end: f.end,
|
|
||||||
},
|
|
||||||
path: f.filename,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
]),
|
// agent is omitted - server will use default_agent from config or fall back to "build"
|
||||||
],
|
parts: [
|
||||||
})
|
{
|
||||||
|
id: PartID.ascending(),
|
||||||
|
type: "text",
|
||||||
|
text: message,
|
||||||
|
},
|
||||||
|
...files.flatMap((f) => [
|
||||||
|
{
|
||||||
|
id: PartID.ascending(),
|
||||||
|
type: "file" as const,
|
||||||
|
mime: f.mime,
|
||||||
|
url: `data:${f.mime};base64,${f.content}`,
|
||||||
|
filename: f.filename,
|
||||||
|
source: {
|
||||||
|
type: "file" as const,
|
||||||
|
text: {
|
||||||
|
value: f.replacement,
|
||||||
|
start: f.start,
|
||||||
|
end: f.end,
|
||||||
|
},
|
||||||
|
path: f.filename,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
// result should always be assistant just satisfying type checker
|
if (result.info.role === "assistant" && result.info.error) {
|
||||||
if (result.info.role === "assistant" && result.info.error) {
|
const err = result.info.error
|
||||||
const err = result.info.error
|
console.error("Agent error:", err)
|
||||||
console.error("Agent error:", err)
|
if (err.name === "ContextOverflowError") throw new Error(formatPromptTooLargeError(files))
|
||||||
|
throw new Error(`${err.name}: ${err.data?.message || ""}`)
|
||||||
|
}
|
||||||
|
|
||||||
if (err.name === "ContextOverflowError") {
|
const text = extractResponseText(result.parts)
|
||||||
throw new Error(formatPromptTooLargeError(files))
|
if (text) return text
|
||||||
}
|
|
||||||
|
|
||||||
const errorMsg = err.data?.message || ""
|
console.log("Requesting summary from agent...")
|
||||||
throw new Error(`${err.name}: ${errorMsg}`)
|
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.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
const text = extractResponseText(result.parts)
|
if (summary.info.role === "assistant" && summary.info.error) {
|
||||||
if (text) return text
|
const err = summary.info.error
|
||||||
|
console.error("Summary agent error:", err)
|
||||||
|
if (err.name === "ContextOverflowError") throw new Error(formatPromptTooLargeError(files))
|
||||||
|
throw new Error(`${err.name}: ${err.data?.message || ""}`)
|
||||||
|
}
|
||||||
|
|
||||||
// No text part (tool-only or reasoning-only) - ask agent to summarize
|
const summaryText = extractResponseText(summary.parts)
|
||||||
console.log("Requesting summary from agent...")
|
if (!summaryText) throw new Error("Failed to get summary from agent")
|
||||||
const summary = await SessionPrompt.prompt({
|
return summaryText
|
||||||
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() {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { Instance } from "../../project/instance"
|
|||||||
import { ShareNext } from "../../share/share-next"
|
import { ShareNext } from "../../share/share-next"
|
||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { Filesystem } from "../../util/filesystem"
|
import { Filesystem } from "../../util/filesystem"
|
||||||
|
import { AppRuntime } from "@/effect/app-runtime"
|
||||||
|
|
||||||
/** Discriminated union returned by the ShareNext API (GET /api/shares/:id/data) */
|
/** Discriminated union returned by the ShareNext API (GET /api/shares/:id/data) */
|
||||||
export type ShareData =
|
export type ShareData =
|
||||||
@@ -100,7 +101,7 @@ export const ImportCommand = cmd({
|
|||||||
if (isUrl) {
|
if (isUrl) {
|
||||||
const slug = parseShareUrl(args.file)
|
const slug = parseShareUrl(args.file)
|
||||||
if (!slug) {
|
if (!slug) {
|
||||||
const baseUrl = await ShareNext.url()
|
const baseUrl = await AppRuntime.runPromise(ShareNext.Service.use((svc) => svc.url()))
|
||||||
process.stdout.write(`Invalid URL format. Expected: ${baseUrl}/share/<slug>`)
|
process.stdout.write(`Invalid URL format. Expected: ${baseUrl}/share/<slug>`)
|
||||||
process.stdout.write(EOL)
|
process.stdout.write(EOL)
|
||||||
return
|
return
|
||||||
@@ -108,7 +109,7 @@ export const ImportCommand = cmd({
|
|||||||
|
|
||||||
const parsed = new URL(args.file)
|
const parsed = new URL(args.file)
|
||||||
const baseUrl = parsed.origin
|
const baseUrl = parsed.origin
|
||||||
const req = await ShareNext.request()
|
const req = await AppRuntime.runPromise(ShareNext.Service.use((svc) => svc.request()))
|
||||||
const headers = shouldAttachShareAuthHeaders(args.file, req.baseUrl) ? req.headers : {}
|
const headers = shouldAttachShareAuthHeaders(args.file, req.baseUrl) ? req.headers : {}
|
||||||
|
|
||||||
const dataPath = req.api.data(slug)
|
const dataPath = req.api.data(slug)
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ 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) {
|
||||||
@@ -50,6 +52,47 @@ 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",
|
||||||
@@ -75,13 +118,8 @@ export const McpListCommand = cmd({
|
|||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("MCP Servers")
|
prompts.intro("MCP Servers")
|
||||||
|
|
||||||
const config = await Config.get()
|
const { config, statuses, stored } = await listState()
|
||||||
const mcpServers = config.mcp ?? {}
|
const servers = configuredServers(config)
|
||||||
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")
|
||||||
@@ -92,7 +130,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 = await MCP.hasStoredTokens(name)
|
const hasStoredTokens = stored[name]
|
||||||
|
|
||||||
let statusIcon: string
|
let statusIcon: string
|
||||||
let statusText: string
|
let statusText: string
|
||||||
@@ -152,15 +190,11 @@ export const McpAuthCommand = cmd({
|
|||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("MCP OAuth Authentication")
|
prompts.intro("MCP OAuth Authentication")
|
||||||
|
|
||||||
const config = await Config.get()
|
const { config, auth } = await authState()
|
||||||
const mcpServers = config.mcp ?? {}
|
const mcpServers = config.mcp ?? {}
|
||||||
|
const servers = oauthServers(config)
|
||||||
|
|
||||||
// Get OAuth-capable servers (remote servers with oauth not explicitly disabled)
|
if (servers.length === 0) {
|
||||||
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(`
|
||||||
@@ -177,19 +211,17 @@ 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 = await Promise.all(
|
const options = servers.map(([name, cfg]) => {
|
||||||
oauthServers.map(async ([name, cfg]) => {
|
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 = cfg.url
|
||||||
const url = cfg.url
|
return {
|
||||||
return {
|
label: `${icon} ${name} (${statusText})`,
|
||||||
label: `${icon} ${name} (${statusText})`,
|
value: name,
|
||||||
value: name,
|
hint: url,
|
||||||
hint: url,
|
}
|
||||||
}
|
})
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const selected = await prompts.select({
|
const selected = await prompts.select({
|
||||||
message: "Select MCP server to authenticate",
|
message: "Select MCP server to authenticate",
|
||||||
@@ -213,7 +245,8 @@ export const McpAuthCommand = cmd({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if already authenticated
|
// Check if already authenticated
|
||||||
const authStatus = await MCP.getAuthStatus(serverName)
|
const authStatus =
|
||||||
|
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?`,
|
||||||
@@ -240,7 +273,7 @@ export const McpAuthCommand = cmd({
|
|||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const status = await MCP.authenticate(serverName)
|
const status = await AppRuntime.runPromise(MCP.Service.use((mcp) => mcp.authenticate(serverName)))
|
||||||
|
|
||||||
if (status.status === "connected") {
|
if (status.status === "connected") {
|
||||||
spinner.stop("Authentication successful!")
|
spinner.stop("Authentication successful!")
|
||||||
@@ -289,22 +322,17 @@ export const McpAuthListCommand = cmd({
|
|||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("MCP OAuth Status")
|
prompts.intro("MCP OAuth Status")
|
||||||
|
|
||||||
const config = await Config.get()
|
const { config, auth } = await authState()
|
||||||
const mcpServers = config.mcp ?? {}
|
const servers = oauthServers(config)
|
||||||
|
|
||||||
// Get OAuth-capable servers
|
if (servers.length === 0) {
|
||||||
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 oauthServers) {
|
for (const [name, serverConfig] of servers) {
|
||||||
const authStatus = await MCP.getAuthStatus(name)
|
const authStatus = auth[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
|
||||||
@@ -312,7 +340,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(`${oauthServers.length} OAuth-capable server(s)`)
|
prompts.outro(`${servers.length} OAuth-capable server(s)`)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -333,8 +361,7 @@ export const McpLogoutCommand = cmd({
|
|||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("MCP OAuth Logout")
|
prompts.intro("MCP OAuth Logout")
|
||||||
|
|
||||||
const authPath = path.join(Global.Path.data, "mcp-auth.json")
|
const credentials = await AppRuntime.runPromise(McpAuth.Service.use((auth) => auth.all()))
|
||||||
const credentials = await McpAuth.all()
|
|
||||||
const serverNames = Object.keys(credentials)
|
const serverNames = Object.keys(credentials)
|
||||||
|
|
||||||
if (serverNames.length === 0) {
|
if (serverNames.length === 0) {
|
||||||
@@ -372,7 +399,7 @@ export const McpLogoutCommand = cmd({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await MCP.removeAuth(serverName)
|
await AppRuntime.runPromise(MCP.Service.use((mcp) => 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")
|
||||||
},
|
},
|
||||||
@@ -595,7 +622,7 @@ export const McpDebugCommand = cmd({
|
|||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("MCP OAuth Debug")
|
prompts.intro("MCP OAuth Debug")
|
||||||
|
|
||||||
const config = await Config.get()
|
const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get()))
|
||||||
const mcpServers = config.mcp ?? {}
|
const mcpServers = config.mcp ?? {}
|
||||||
const serverName = args.name
|
const serverName = args.name
|
||||||
|
|
||||||
@@ -622,10 +649,18 @@ 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 = await MCP.getAuthStatus(serverName)
|
const { authStatus, entry } = await AppRuntime.runPromise(
|
||||||
|
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) {
|
||||||
@@ -681,6 +716,11 @@ 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,
|
||||||
@@ -693,6 +733,7 @@ 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,6 +6,8 @@ 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]",
|
||||||
@@ -35,43 +37,51 @@ export const ModelsCommand = cmd({
|
|||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: process.cwd(),
|
directory: process.cwd(),
|
||||||
async fn() {
|
async fn() {
|
||||||
const providers = await Provider.list()
|
await AppRuntime.runPromise(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const svc = yield* Provider.Service
|
||||||
|
const providers = yield* svc.list()
|
||||||
|
|
||||||
function printModels(providerID: ProviderID, verbose?: boolean) {
|
const print = (providerID: ProviderID, verbose?: boolean) => {
|
||||||
const provider = providers[providerID]
|
const provider = providers[providerID]
|
||||||
const sortedModels = Object.entries(provider.models).sort(([a], [b]) => a.localeCompare(b))
|
const sorted = Object.entries(provider.models).sort(([a], [b]) => a.localeCompare(b))
|
||||||
for (const [modelID, model] of sortedModels) {
|
for (const [modelID, model] of sorted) {
|
||||||
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 provider = providers[ProviderID.make(args.provider)]
|
const providerID = ProviderID.make(args.provider)
|
||||||
if (!provider) {
|
const provider = providers[providerID]
|
||||||
UI.error(`Provider not found: ${args.provider}`)
|
if (!provider) {
|
||||||
return
|
yield* Effect.sync(() => UI.error(`Provider not found: ${args.provider}`))
|
||||||
}
|
return
|
||||||
|
}
|
||||||
|
|
||||||
printModels(ProviderID.make(args.provider), args.verbose)
|
yield* Effect.sync(() => print(providerID, args.verbose))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const providerIDs = Object.keys(providers).sort((a, b) => {
|
const ids = 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)
|
||||||
})
|
})
|
||||||
|
|
||||||
for (const providerID of providerIDs) {
|
yield* Effect.sync(() => {
|
||||||
printModels(ProviderID.make(providerID), args.verbose)
|
for (const providerID of ids) {
|
||||||
}
|
print(ProviderID.make(providerID), args.verbose)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
|
import { AppRuntime } from "@/effect/app-runtime"
|
||||||
import { Git } from "@/git"
|
import { Git } from "@/git"
|
||||||
import { Instance } from "@/project/instance"
|
import { Instance } from "@/project/instance"
|
||||||
import { Process } from "@/util/process"
|
import { Process } from "@/util/process"
|
||||||
@@ -67,19 +68,29 @@ export const PrCommand = cmd({
|
|||||||
const remoteName = forkOwner
|
const remoteName = forkOwner
|
||||||
|
|
||||||
// Check if remote already exists
|
// Check if remote already exists
|
||||||
const remotes = (await Git.run(["remote"], { cwd: Instance.worktree })).text().trim()
|
const remotes = await AppRuntime.runPromise(
|
||||||
|
Git.Service.use((git) => git.run(["remote"], { cwd: Instance.worktree })),
|
||||||
|
).then((x) => x.text().trim())
|
||||||
if (!remotes.split("\n").includes(remoteName)) {
|
if (!remotes.split("\n").includes(remoteName)) {
|
||||||
await Git.run(["remote", "add", remoteName, `https://github.com/${forkOwner}/${forkName}.git`], {
|
await AppRuntime.runPromise(
|
||||||
cwd: Instance.worktree,
|
Git.Service.use((git) =>
|
||||||
})
|
git.run(["remote", "add", remoteName, `https://github.com/${forkOwner}/${forkName}.git`], {
|
||||||
|
cwd: Instance.worktree,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
UI.println(`Added fork remote: ${remoteName}`)
|
UI.println(`Added fork remote: ${remoteName}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set upstream to the fork so pushes go there
|
// Set upstream to the fork so pushes go there
|
||||||
const headRefName = prInfo.headRefName
|
const headRefName = prInfo.headRefName
|
||||||
await Git.run(["branch", `--set-upstream-to=${remoteName}/${headRefName}`, localBranchName], {
|
await AppRuntime.runPromise(
|
||||||
cwd: Instance.worktree,
|
Git.Service.use((git) =>
|
||||||
})
|
git.run(["branch", `--set-upstream-to=${remoteName}/${headRefName}`, localBranchName], {
|
||||||
|
cwd: Instance.worktree,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for opencode session link in PR body
|
// Check for opencode session link in PR body
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
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"
|
||||||
@@ -13,9 +14,18 @@ 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) {
|
||||||
@@ -93,7 +103,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 Auth.set(saveProvider, {
|
await put(saveProvider, {
|
||||||
type: "oauth",
|
type: "oauth",
|
||||||
refresh,
|
refresh,
|
||||||
access,
|
access,
|
||||||
@@ -102,7 +112,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if ("key" in result) {
|
if ("key" in result) {
|
||||||
await Auth.set(saveProvider, {
|
await put(saveProvider, {
|
||||||
type: "api",
|
type: "api",
|
||||||
key: result.key,
|
key: result.key,
|
||||||
})
|
})
|
||||||
@@ -125,7 +135,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 Auth.set(saveProvider, {
|
await put(saveProvider, {
|
||||||
type: "oauth",
|
type: "oauth",
|
||||||
refresh,
|
refresh,
|
||||||
access,
|
access,
|
||||||
@@ -134,7 +144,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if ("key" in result) {
|
if ("key" in result) {
|
||||||
await Auth.set(saveProvider, {
|
await put(saveProvider, {
|
||||||
type: "api",
|
type: "api",
|
||||||
key: result.key,
|
key: result.key,
|
||||||
})
|
})
|
||||||
@@ -149,15 +159,21 @@ 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 Auth.set(saveProvider, {
|
await put(saveProvider, {
|
||||||
type: "api",
|
type: "api",
|
||||||
key: result.key,
|
key: result.key ?? key,
|
||||||
})
|
})
|
||||||
prompts.log.success("Login successful")
|
prompts.log.success("Login successful")
|
||||||
}
|
}
|
||||||
@@ -215,7 +231,12 @@ 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 = Object.entries(await Auth.all())
|
const results = await AppRuntime.runPromise(
|
||||||
|
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) {
|
||||||
@@ -294,7 +315,7 @@ export const ProvidersLoginCommand = cmd({
|
|||||||
prompts.outro("Done")
|
prompts.outro("Done")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await Auth.set(url, {
|
await put(url, {
|
||||||
type: "wellknown",
|
type: "wellknown",
|
||||||
key: wellknown.auth.env,
|
key: wellknown.auth.env,
|
||||||
token: token.trim(),
|
token: token.trim(),
|
||||||
@@ -305,7 +326,7 @@ export const ProvidersLoginCommand = cmd({
|
|||||||
}
|
}
|
||||||
await ModelsDev.refresh(true).catch(() => {})
|
await ModelsDev.refresh(true).catch(() => {})
|
||||||
|
|
||||||
const config = await Config.get()
|
const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.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
|
||||||
@@ -319,6 +340,12 @@ 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,
|
||||||
@@ -330,7 +357,7 @@ export const ProvidersLoginCommand = cmd({
|
|||||||
vercel: 6,
|
vercel: 6,
|
||||||
}
|
}
|
||||||
const pluginProviders = resolvePluginProviders({
|
const pluginProviders = resolvePluginProviders({
|
||||||
hooks: await Plugin.list(),
|
hooks,
|
||||||
existingProviders: providers,
|
existingProviders: providers,
|
||||||
disabled,
|
disabled,
|
||||||
enabled,
|
enabled,
|
||||||
@@ -387,7 +414,7 @@ export const ProvidersLoginCommand = cmd({
|
|||||||
provider = selected as string
|
provider = selected as string
|
||||||
}
|
}
|
||||||
|
|
||||||
const plugin = await Plugin.list().then((x) => x.findLast((x) => x.auth?.provider === provider))
|
const plugin = hooks.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
|
||||||
@@ -401,7 +428,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 = await Plugin.list().then((x) => x.findLast((x) => x.auth?.provider === provider))
|
const customPlugin = hooks.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
|
||||||
@@ -441,7 +468,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 Auth.set(provider, {
|
await put(provider, {
|
||||||
type: "api",
|
type: "api",
|
||||||
key,
|
key,
|
||||||
})
|
})
|
||||||
@@ -457,22 +484,33 @@ 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 = await Auth.all().then((x) => Object.entries(x))
|
const credentials: Array<[string, Auth.Info]> = await AppRuntime.runPromise(
|
||||||
|
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 providerID = await prompts.select({
|
const selected = 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(providerID)) throw new UI.CancelledError()
|
if (prompts.isCancel(selected)) throw new UI.CancelledError()
|
||||||
await Auth.remove(providerID)
|
const providerID = selected as string
|
||||||
|
await AppRuntime.runPromise(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const auth = yield* Auth.Service
|
||||||
|
yield* auth.remove(providerID)
|
||||||
|
}),
|
||||||
|
)
|
||||||
prompts.outro("Logout successful")
|
prompts.outro("Logout successful")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ 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>
|
||||||
@@ -573,6 +574,7 @@ 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) {
|
||||||
@@ -590,12 +592,12 @@ export const RunCommand = cmd({
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const agent = modes.find((a) => a.name === args.agent)
|
const agent = modes.find((a) => a.name === name)
|
||||||
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 "${args.agent}" not found. Falling back to default agent`,
|
`agent "${name}" not found. Falling back to default agent`,
|
||||||
)
|
)
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
@@ -604,20 +606,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 "${args.agent}" is a subagent, not a primary agent. Falling back to default agent`,
|
`agent "${name}" is a subagent, not a primary agent. Falling back to default agent`,
|
||||||
)
|
)
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
return args.agent
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
const entry = await Agent.get(args.agent)
|
const entry = await AppRuntime.runPromise(Agent.Service.use((svc) => svc.get(name)))
|
||||||
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 "${args.agent}" not found. Falling back to default agent`,
|
`agent "${name}" not found. Falling back to default agent`,
|
||||||
)
|
)
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
@@ -625,11 +627,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 "${args.agent}" is a subagent, not a primary agent. Falling back to default agent`,
|
`agent "${name}" is a subagent, not a primary agent. Falling back to default agent`,
|
||||||
)
|
)
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
return args.agent
|
return name
|
||||||
})()
|
})()
|
||||||
|
|
||||||
const sessionID = await session(sdk)
|
const sessionID = await session(sdk)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ 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"]
|
||||||
@@ -60,12 +61,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 Session.get(sessionID)
|
await AppRuntime.runPromise(Session.Service.use((svc) => svc.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 Session.remove(sessionID)
|
await AppRuntime.runPromise(Session.Service.use((svc) => svc.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,6 +6,7 @@ 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
|
||||||
@@ -167,7 +168,9 @@ 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 Session.messages({ sessionID: session.id })
|
const messages = await AppRuntime.runPromise(
|
||||||
|
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,6 +1,7 @@
|
|||||||
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 {
|
||||||
@@ -22,7 +23,7 @@ import { DialogProvider, useDialog } from "@tui/ui/dialog"
|
|||||||
import { DialogProvider as DialogProviderList } from "@tui/component/dialog-provider"
|
import { DialogProvider as DialogProviderList } from "@tui/component/dialog-provider"
|
||||||
import { ErrorComponent } from "@tui/component/error-component"
|
import { ErrorComponent } from "@tui/component/error-component"
|
||||||
import { PluginRouteMissing } from "@tui/component/plugin-route-missing"
|
import { PluginRouteMissing } from "@tui/component/plugin-route-missing"
|
||||||
import { ProjectProvider } from "@tui/context/project"
|
import { ProjectProvider, useProject } from "@tui/context/project"
|
||||||
import { useEvent } from "@tui/context/event"
|
import { useEvent } from "@tui/context/event"
|
||||||
import { SDKProvider, useSDK } from "@tui/context/sdk"
|
import { SDKProvider, useSDK } from "@tui/context/sdk"
|
||||||
import { StartupLoading } from "@tui/component/startup-loading"
|
import { StartupLoading } from "@tui/component/startup-loading"
|
||||||
@@ -36,7 +37,6 @@ import { DialogHelp } from "./ui/dialog-help"
|
|||||||
import { CommandProvider, useCommandDialog } from "@tui/component/dialog-command"
|
import { CommandProvider, useCommandDialog } from "@tui/component/dialog-command"
|
||||||
import { DialogAgent } from "@tui/component/dialog-agent"
|
import { DialogAgent } from "@tui/component/dialog-agent"
|
||||||
import { DialogSessionList } from "@tui/component/dialog-session-list"
|
import { DialogSessionList } from "@tui/component/dialog-session-list"
|
||||||
import { DialogWorkspaceList } from "@tui/component/dialog-workspace-list"
|
|
||||||
import { DialogConsoleOrg } from "@tui/component/dialog-console-org"
|
import { DialogConsoleOrg } from "@tui/component/dialog-console-org"
|
||||||
import { KeybindProvider, useKeybind } from "@tui/context/keybind"
|
import { KeybindProvider, useKeybind } from "@tui/context/keybind"
|
||||||
import { ThemeProvider, useTheme } from "@tui/context/theme"
|
import { ThemeProvider, useTheme } from "@tui/context/theme"
|
||||||
@@ -61,66 +61,6 @@ 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"
|
||||||
|
|
||||||
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"
|
||||||
|
|
||||||
@@ -179,7 +119,7 @@ export function tui(input: {
|
|||||||
const unguard = win32InstallCtrlCGuard()
|
const unguard = win32InstallCtrlCGuard()
|
||||||
win32DisableProcessedInput()
|
win32DisableProcessedInput()
|
||||||
|
|
||||||
const mode = await getTerminalBackgroundColor()
|
const mode = await Terminal.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.
|
||||||
@@ -465,22 +405,6 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
|||||||
dialog.replace(() => <DialogSessionList />)
|
dialog.replace(() => <DialogSessionList />)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
...(Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
title: "Manage workspaces",
|
|
||||||
value: "workspace.list",
|
|
||||||
category: "Workspace",
|
|
||||||
suggested: true,
|
|
||||||
slash: {
|
|
||||||
name: "workspaces",
|
|
||||||
},
|
|
||||||
onSelect: () => {
|
|
||||||
dialog.replace(() => <DialogWorkspaceList />)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
{
|
||||||
title: "New session",
|
title: "New session",
|
||||||
suggested: route.data.type === "session",
|
suggested: route.data.type === "session",
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -2,25 +2,31 @@ import { useDialog } from "@tui/ui/dialog"
|
|||||||
import { DialogSelect } from "@tui/ui/dialog-select"
|
import { DialogSelect } from "@tui/ui/dialog-select"
|
||||||
import { useRoute } from "@tui/context/route"
|
import { useRoute } from "@tui/context/route"
|
||||||
import { useSync } from "@tui/context/sync"
|
import { useSync } from "@tui/context/sync"
|
||||||
import { createMemo, createSignal, createResource, onMount, Show } from "solid-js"
|
import { createMemo, createResource, createSignal, onMount } from "solid-js"
|
||||||
import { Locale } from "@/util/locale"
|
import { Locale } from "@/util/locale"
|
||||||
|
import { useProject } from "@tui/context/project"
|
||||||
import { useKeybind } from "../context/keybind"
|
import { useKeybind } from "../context/keybind"
|
||||||
import { useTheme } from "../context/theme"
|
import { useTheme } from "../context/theme"
|
||||||
import { useSDK } from "../context/sdk"
|
import { useSDK } from "../context/sdk"
|
||||||
|
import { Flag } from "@/flag/flag"
|
||||||
import { DialogSessionRename } from "./dialog-session-rename"
|
import { DialogSessionRename } from "./dialog-session-rename"
|
||||||
import { useKV } from "../context/kv"
|
import { Keybind } from "@/util/keybind"
|
||||||
import { createDebouncedSignal } from "../util/signal"
|
import { createDebouncedSignal } from "../util/signal"
|
||||||
|
import { useToast } from "../ui/toast"
|
||||||
|
import { DialogWorkspaceCreate, openWorkspaceSession } from "./dialog-workspace-create"
|
||||||
import { Spinner } from "./spinner"
|
import { Spinner } from "./spinner"
|
||||||
|
|
||||||
|
type WorkspaceStatus = "connected" | "connecting" | "disconnected" | "error"
|
||||||
|
|
||||||
export function DialogSessionList() {
|
export function DialogSessionList() {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
|
const project = useProject()
|
||||||
const keybind = useKeybind()
|
const keybind = useKeybind()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const kv = useKV()
|
const toast = useToast()
|
||||||
|
|
||||||
const [toDelete, setToDelete] = createSignal<string>()
|
const [toDelete, setToDelete] = createSignal<string>()
|
||||||
const [search, setSearch] = createDebouncedSignal("", 150)
|
const [search, setSearch] = createDebouncedSignal("", 150)
|
||||||
|
|
||||||
@@ -31,15 +37,68 @@ export function DialogSessionList() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))
|
const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))
|
||||||
|
|
||||||
const sessions = createMemo(() => searchResults() ?? sync.data.session)
|
const sessions = createMemo(() => searchResults() ?? sync.data.session)
|
||||||
|
|
||||||
|
function createWorkspace() {
|
||||||
|
dialog.replace(() => (
|
||||||
|
<DialogWorkspaceCreate
|
||||||
|
onSelect={(workspaceID) =>
|
||||||
|
openWorkspaceSession({
|
||||||
|
dialog,
|
||||||
|
route,
|
||||||
|
sdk,
|
||||||
|
sync,
|
||||||
|
toast,
|
||||||
|
workspaceID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
const options = createMemo(() => {
|
const options = createMemo(() => {
|
||||||
const today = new Date().toDateString()
|
const today = new Date().toDateString()
|
||||||
return sessions()
|
return sessions()
|
||||||
.filter((x) => x.parentID === undefined)
|
.filter((x) => x.parentID === undefined)
|
||||||
.toSorted((a, b) => b.time.updated - a.time.updated)
|
.toSorted((a, b) => b.time.updated - a.time.updated)
|
||||||
.map((x) => {
|
.map((x) => {
|
||||||
|
const workspace = x.workspaceID ? project.workspace.get(x.workspaceID) : undefined
|
||||||
|
|
||||||
|
let workspaceStatus: WorkspaceStatus | null = null
|
||||||
|
if (x.workspaceID) {
|
||||||
|
workspaceStatus = project.workspace.status(x.workspaceID) || "error"
|
||||||
|
}
|
||||||
|
|
||||||
|
let footer = ""
|
||||||
|
if (Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) {
|
||||||
|
if (x.workspaceID) {
|
||||||
|
let desc = "unknown"
|
||||||
|
if (workspace) {
|
||||||
|
desc = `${workspace.type}: ${workspace.name}`
|
||||||
|
}
|
||||||
|
|
||||||
|
footer = (
|
||||||
|
<>
|
||||||
|
{desc}{" "}
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fg:
|
||||||
|
workspaceStatus === "error"
|
||||||
|
? theme.error
|
||||||
|
: workspaceStatus === "disconnected"
|
||||||
|
? theme.textMuted
|
||||||
|
: theme.success,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
■
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
footer = Locale.time(x.time.updated)
|
||||||
|
}
|
||||||
|
|
||||||
const date = new Date(x.time.updated)
|
const date = new Date(x.time.updated)
|
||||||
let category = date.toDateString()
|
let category = date.toDateString()
|
||||||
if (category === today) {
|
if (category === today) {
|
||||||
@@ -53,7 +112,7 @@ export function DialogSessionList() {
|
|||||||
bg: isDeleting ? theme.error : undefined,
|
bg: isDeleting ? theme.error : undefined,
|
||||||
value: x.id,
|
value: x.id,
|
||||||
category,
|
category,
|
||||||
footer: Locale.time(x.time.updated),
|
footer,
|
||||||
gutter: isWorking ? <Spinner /> : undefined,
|
gutter: isWorking ? <Spinner /> : undefined,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -102,6 +161,15 @@ export function DialogSessionList() {
|
|||||||
dialog.replace(() => <DialogSessionRename session={option.value} />)
|
dialog.replace(() => <DialogSessionRename session={option.value} />)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
keybind: Keybind.parse("ctrl+w")[0],
|
||||||
|
title: "new workspace",
|
||||||
|
side: "right",
|
||||||
|
disabled: !Flag.OPENCODE_EXPERIMENTAL_WORKSPACES,
|
||||||
|
onTrigger: () => {
|
||||||
|
createWorkspace()
|
||||||
|
},
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
||||||
|
import { useDialog } from "@tui/ui/dialog"
|
||||||
|
import { DialogSelect } from "@tui/ui/dialog-select"
|
||||||
|
import { useRoute } from "@tui/context/route"
|
||||||
|
import { useSync } from "@tui/context/sync"
|
||||||
|
import { useProject } from "@tui/context/project"
|
||||||
|
import { createMemo, createSignal, onMount } from "solid-js"
|
||||||
|
import { setTimeout as sleep } from "node:timers/promises"
|
||||||
|
import { useSDK } from "../context/sdk"
|
||||||
|
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) {
|
||||||
|
return createOpencodeClient({
|
||||||
|
baseUrl: sdk.url,
|
||||||
|
fetch: sdk.fetch,
|
||||||
|
directory: sync.path.directory || sdk.directory,
|
||||||
|
experimental_workspaceID: workspaceID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function openWorkspaceSession(input: {
|
||||||
|
dialog: ReturnType<typeof useDialog>
|
||||||
|
route: ReturnType<typeof useRoute>
|
||||||
|
sdk: ReturnType<typeof useSDK>
|
||||||
|
sync: ReturnType<typeof useSync>
|
||||||
|
toast: ReturnType<typeof useToast>
|
||||||
|
workspaceID: string
|
||||||
|
}) {
|
||||||
|
const client = scoped(input.sdk, input.sync, input.workspaceID)
|
||||||
|
while (true) {
|
||||||
|
const result = await client.session.create({ workspaceID: input.workspaceID }).catch(() => undefined)
|
||||||
|
if (!result) {
|
||||||
|
input.toast.show({
|
||||||
|
message: "Failed to create workspace session",
|
||||||
|
variant: "error",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (result.response.status >= 500 && result.response.status < 600) {
|
||||||
|
await sleep(1000)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (!result.data) {
|
||||||
|
input.toast.show({
|
||||||
|
message: "Failed to create workspace session",
|
||||||
|
variant: "error",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
input.route.navigate({
|
||||||
|
type: "session",
|
||||||
|
sessionID: result.data.id,
|
||||||
|
})
|
||||||
|
input.dialog.clear()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) => Promise<void> | void }) {
|
||||||
|
const dialog = useDialog()
|
||||||
|
const sync = useSync()
|
||||||
|
const project = useProject()
|
||||||
|
const sdk = useSDK()
|
||||||
|
const toast = useToast()
|
||||||
|
const [creating, setCreating] = createSignal<string>()
|
||||||
|
const [adaptors, setAdaptors] = createSignal<Adaptor[]>()
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
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 type = creating()
|
||||||
|
if (type) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: `Creating ${type} workspace...`,
|
||||||
|
value: "creating" as const,
|
||||||
|
description: "This can take a while for remote environments",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
const list = adaptors()
|
||||||
|
if (!list) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: "Loading workspaces...",
|
||||||
|
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) => {
|
||||||
|
if (creating()) return
|
||||||
|
setCreating(type)
|
||||||
|
|
||||||
|
const result = await sdk.client.experimental.workspace.create({ type, branch: null }).catch(() => undefined)
|
||||||
|
const workspace = result?.data
|
||||||
|
if (!workspace) {
|
||||||
|
setCreating(undefined)
|
||||||
|
toast.show({
|
||||||
|
message: "Failed to create workspace",
|
||||||
|
variant: "error",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await project.workspace.sync()
|
||||||
|
await props.onSelect(workspace.id)
|
||||||
|
setCreating(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogSelect
|
||||||
|
title={creating() ? "Creating Workspace" : "New Workspace"}
|
||||||
|
skipFilter={true}
|
||||||
|
options={options()}
|
||||||
|
onSelect={(option) => {
|
||||||
|
if (option.value === "creating" || option.value === "loading") return
|
||||||
|
void create(option.value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,319 +0,0 @@
|
|||||||
import { useDialog } from "@tui/ui/dialog"
|
|
||||||
import { DialogSelect } from "@tui/ui/dialog-select"
|
|
||||||
import { useProject } from "@tui/context/project"
|
|
||||||
import { useRoute } from "@tui/context/route"
|
|
||||||
import { useSync } from "@tui/context/sync"
|
|
||||||
import { createEffect, createMemo, createSignal, onMount } from "solid-js"
|
|
||||||
import { createOpencodeClient, type Session } from "@opencode-ai/sdk/v2"
|
|
||||||
import { useSDK } from "../context/sdk"
|
|
||||||
import { useToast } from "../ui/toast"
|
|
||||||
import { useKeybind } from "../context/keybind"
|
|
||||||
import { DialogSessionList } from "./workspace/dialog-session-list"
|
|
||||||
import { setTimeout as sleep } from "node:timers/promises"
|
|
||||||
|
|
||||||
function scoped(sdk: ReturnType<typeof useSDK>, sync: ReturnType<typeof useSync>, workspaceID?: string) {
|
|
||||||
return createOpencodeClient({
|
|
||||||
baseUrl: sdk.url,
|
|
||||||
fetch: sdk.fetch,
|
|
||||||
directory: sync.path.directory || sdk.directory,
|
|
||||||
experimental_workspaceID: workspaceID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openWorkspace(input: {
|
|
||||||
dialog: ReturnType<typeof useDialog>
|
|
||||||
route: ReturnType<typeof useRoute>
|
|
||||||
sdk: ReturnType<typeof useSDK>
|
|
||||||
sync: ReturnType<typeof useSync>
|
|
||||||
toast: ReturnType<typeof useToast>
|
|
||||||
workspaceID: string
|
|
||||||
forceCreate?: boolean
|
|
||||||
}) {
|
|
||||||
const cacheSession = (session: Session) => {
|
|
||||||
input.sync.set(
|
|
||||||
"session",
|
|
||||||
[...input.sync.data.session.filter((item) => item.id !== session.id), session].toSorted((a, b) =>
|
|
||||||
a.id.localeCompare(b.id),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = scoped(input.sdk, input.sync, input.workspaceID)
|
|
||||||
const listed = input.forceCreate ? undefined : await client.session.list({ roots: true, limit: 1 })
|
|
||||||
const session = listed?.data?.[0]
|
|
||||||
if (session?.id) {
|
|
||||||
cacheSession(session)
|
|
||||||
input.route.navigate({
|
|
||||||
type: "session",
|
|
||||||
sessionID: session.id,
|
|
||||||
})
|
|
||||||
input.dialog.clear()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let created: Session | undefined
|
|
||||||
while (!created) {
|
|
||||||
const result = await client.session.create({ workspaceID: input.workspaceID }).catch(() => undefined)
|
|
||||||
if (!result) {
|
|
||||||
input.toast.show({
|
|
||||||
message: "Failed to open workspace",
|
|
||||||
variant: "error",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (result.response.status >= 500 && result.response.status < 600) {
|
|
||||||
await sleep(1000)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (!result.data) {
|
|
||||||
input.toast.show({
|
|
||||||
message: "Failed to open workspace",
|
|
||||||
variant: "error",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
created = result.data
|
|
||||||
}
|
|
||||||
cacheSession(created)
|
|
||||||
input.route.navigate({
|
|
||||||
type: "session",
|
|
||||||
sessionID: created.id,
|
|
||||||
})
|
|
||||||
input.dialog.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) => Promise<void> }) {
|
|
||||||
const dialog = useDialog()
|
|
||||||
const sync = useSync()
|
|
||||||
const sdk = useSDK()
|
|
||||||
const toast = useToast()
|
|
||||||
const [creating, setCreating] = createSignal<string>()
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
dialog.setSize("medium")
|
|
||||||
})
|
|
||||||
|
|
||||||
const options = createMemo(() => {
|
|
||||||
const type = creating()
|
|
||||||
if (type) {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
title: `Creating ${type} workspace...`,
|
|
||||||
value: "creating" as const,
|
|
||||||
description: "This can take a while for remote environments",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
title: "Worktree",
|
|
||||||
value: "worktree" as const,
|
|
||||||
description: "Create a local git worktree",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
const createWorkspace = async (type: string) => {
|
|
||||||
if (creating()) return
|
|
||||||
setCreating(type)
|
|
||||||
|
|
||||||
const result = await sdk.client.experimental.workspace.create({ type, branch: null }).catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
return undefined
|
|
||||||
})
|
|
||||||
console.log(JSON.stringify(result, null, 2))
|
|
||||||
const workspace = result?.data
|
|
||||||
if (!workspace) {
|
|
||||||
setCreating(undefined)
|
|
||||||
toast.show({
|
|
||||||
message: "Failed to create workspace",
|
|
||||||
variant: "error",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await sync.workspace.sync()
|
|
||||||
await props.onSelect(workspace.id)
|
|
||||||
setCreating(undefined)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DialogSelect
|
|
||||||
title={creating() ? "Creating Workspace" : "New Workspace"}
|
|
||||||
skipFilter={true}
|
|
||||||
options={options()}
|
|
||||||
onSelect={(option) => {
|
|
||||||
if (option.value === "creating") return
|
|
||||||
void createWorkspace(option.value)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DialogWorkspaceList() {
|
|
||||||
const dialog = useDialog()
|
|
||||||
const project = useProject()
|
|
||||||
const route = useRoute()
|
|
||||||
const sync = useSync()
|
|
||||||
const sdk = useSDK()
|
|
||||||
const toast = useToast()
|
|
||||||
const keybind = useKeybind()
|
|
||||||
const [toDelete, setToDelete] = createSignal<string>()
|
|
||||||
const [counts, setCounts] = createSignal<Record<string, number | null | undefined>>({})
|
|
||||||
|
|
||||||
const open = (workspaceID: string, forceCreate?: boolean) =>
|
|
||||||
openWorkspace({
|
|
||||||
dialog,
|
|
||||||
route,
|
|
||||||
sdk,
|
|
||||||
sync,
|
|
||||||
toast,
|
|
||||||
workspaceID,
|
|
||||||
forceCreate,
|
|
||||||
})
|
|
||||||
|
|
||||||
async function selectWorkspace(workspaceID: string | null) {
|
|
||||||
if (workspaceID == null) {
|
|
||||||
project.workspace.set(undefined)
|
|
||||||
if (localCount() > 0) {
|
|
||||||
dialog.replace(() => <DialogSessionList localOnly={true} />)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
route.navigate({
|
|
||||||
type: "home",
|
|
||||||
})
|
|
||||||
dialog.clear()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const count = counts()[workspaceID]
|
|
||||||
if (count && count > 0) {
|
|
||||||
dialog.replace(() => <DialogSessionList workspaceID={workspaceID} />)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count === 0) {
|
|
||||||
await open(workspaceID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const client = scoped(sdk, sync, workspaceID)
|
|
||||||
const listed = await client.session.list({ roots: true, limit: 1 }).catch(() => undefined)
|
|
||||||
if (listed?.data?.length) {
|
|
||||||
dialog.replace(() => <DialogSessionList workspaceID={workspaceID} />)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await open(workspaceID)
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentWorkspaceID = createMemo(() => project.workspace.current())
|
|
||||||
|
|
||||||
const localCount = createMemo(
|
|
||||||
() => sync.data.session.filter((session) => !session.workspaceID && !session.parentID).length,
|
|
||||||
)
|
|
||||||
|
|
||||||
let run = 0
|
|
||||||
createEffect(() => {
|
|
||||||
const workspaces = sync.data.workspaceList
|
|
||||||
const next = ++run
|
|
||||||
if (!workspaces.length) {
|
|
||||||
setCounts({})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setCounts(Object.fromEntries(workspaces.map((workspace) => [workspace.id, undefined])))
|
|
||||||
void Promise.all(
|
|
||||||
workspaces.map(async (workspace) => {
|
|
||||||
const client = scoped(sdk, sync, workspace.id)
|
|
||||||
const result = await client.session.list({ roots: true }).catch(() => undefined)
|
|
||||||
return [workspace.id, result ? (result.data?.length ?? 0) : null] as const
|
|
||||||
}),
|
|
||||||
).then((entries) => {
|
|
||||||
if (run !== next) return
|
|
||||||
setCounts(Object.fromEntries(entries))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const options = createMemo(() => [
|
|
||||||
{
|
|
||||||
title: "Local",
|
|
||||||
value: null,
|
|
||||||
category: "Workspace",
|
|
||||||
description: "Use the local machine",
|
|
||||||
footer: `${localCount()} session${localCount() === 1 ? "" : "s"}`,
|
|
||||||
},
|
|
||||||
...sync.data.workspaceList.map((workspace) => {
|
|
||||||
const count = counts()[workspace.id]
|
|
||||||
return {
|
|
||||||
title:
|
|
||||||
toDelete() === workspace.id
|
|
||||||
? `Delete ${workspace.id}? Press ${keybind.print("session_delete")} again`
|
|
||||||
: workspace.id,
|
|
||||||
value: workspace.id,
|
|
||||||
category: workspace.type,
|
|
||||||
description: workspace.branch ? `Branch ${workspace.branch}` : undefined,
|
|
||||||
footer:
|
|
||||||
count === undefined
|
|
||||||
? "Loading sessions..."
|
|
||||||
: count === null
|
|
||||||
? "Sessions unavailable"
|
|
||||||
: `${count} session${count === 1 ? "" : "s"}`,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
title: "+ New workspace",
|
|
||||||
value: "__create__",
|
|
||||||
category: "Actions",
|
|
||||||
description: "Create a new workspace",
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
dialog.setSize("large")
|
|
||||||
void sync.workspace.sync()
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DialogSelect
|
|
||||||
title="Workspaces"
|
|
||||||
skipFilter={true}
|
|
||||||
options={options()}
|
|
||||||
current={currentWorkspaceID()}
|
|
||||||
onMove={() => {
|
|
||||||
setToDelete(undefined)
|
|
||||||
}}
|
|
||||||
onSelect={(option) => {
|
|
||||||
setToDelete(undefined)
|
|
||||||
if (option.value === "__create__") {
|
|
||||||
dialog.replace(() => <DialogWorkspaceCreate onSelect={(workspaceID) => open(workspaceID, true)} />)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
void selectWorkspace(option.value)
|
|
||||||
}}
|
|
||||||
keybind={[
|
|
||||||
{
|
|
||||||
keybind: keybind.all.session_delete?.[0],
|
|
||||||
title: "delete",
|
|
||||||
onTrigger: async (option) => {
|
|
||||||
if (option.value === "__create__" || option.value === null) return
|
|
||||||
if (toDelete() !== option.value) {
|
|
||||||
setToDelete(option.value)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const result = await sdk.client.experimental.workspace.remove({ id: option.value }).catch(() => undefined)
|
|
||||||
setToDelete(undefined)
|
|
||||||
if (result?.error) {
|
|
||||||
toast.show({
|
|
||||||
message: "Failed to delete workspace",
|
|
||||||
variant: "error",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (currentWorkspaceID() === option.value) {
|
|
||||||
project.workspace.set(undefined)
|
|
||||||
route.navigate({
|
|
||||||
type: "home",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
await sync.workspace.sync()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,82 +1,630 @@
|
|||||||
import { TextAttributes, RGBA } from "@opentui/core"
|
import { BoxRenderable, MouseButton, MouseEvent, RGBA, TextAttributes } from "@opentui/core"
|
||||||
import { For, type JSX } from "solid-js"
|
import { For, createMemo, createSignal, onCleanup, type JSX } from "solid-js"
|
||||||
import { useTheme, tint } from "@tui/context/theme"
|
import { useTheme, tint } from "@tui/context/theme"
|
||||||
import { logo, marks } from "@/cli/logo"
|
import { Sound } from "@tui/util/sound"
|
||||||
|
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 SHADOW_MARKER = new RegExp(`[${marks}]`)
|
const GAP = 1
|
||||||
|
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 renderLine = (line: string, fg: RGBA, bold: boolean): JSX.Element[] => {
|
const stop = () => {
|
||||||
const shadow = tint(theme.background, fg, 0.25)
|
if (!timer) return
|
||||||
|
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
|
|
||||||
|
|
||||||
while (i < line.length) {
|
return [...line].map((char, i) => {
|
||||||
const rest = line.slice(i)
|
const h = field(off + i, y, frame)
|
||||||
const markerIndex = rest.search(SHADOW_MARKER)
|
const n = wave(off + i, y, frame, lit(char)) + h
|
||||||
|
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 (markerIndex === -1) {
|
if (char === "_") {
|
||||||
elements.push(
|
return (
|
||||||
<text fg={fg} attributes={attrs} selectable={false}>
|
<text
|
||||||
{rest}
|
fg={shade(ink, theme, s * 0.08)}
|
||||||
</text>,
|
bg={shade(shadow, theme, ghost(s, 0.24) + ghost(q, 0.06))}
|
||||||
)
|
attributes={attrs}
|
||||||
break
|
selectable={false}
|
||||||
}
|
>
|
||||||
|
{" "}
|
||||||
if (markerIndex > 0) {
|
</text>
|
||||||
elements.push(
|
|
||||||
<text fg={fg} attributes={attrs} selectable={false}>
|
|
||||||
{rest.slice(0, markerIndex)}
|
|
||||||
</text>,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const marker = rest[markerIndex]
|
if (char === "^") {
|
||||||
switch (marker) {
|
return (
|
||||||
case "_":
|
<text
|
||||||
elements.push(
|
fg={shade(ink, theme, n + p + e + b)}
|
||||||
<text fg={fg} bg={shadow} attributes={attrs} selectable={false}>
|
bg={shade(shadow, theme, ghost(s, 0.18) + ghost(q, 0.05) + ghost(b, 0.08))}
|
||||||
{" "}
|
attributes={attrs}
|
||||||
</text>,
|
selectable={false}
|
||||||
)
|
>
|
||||||
break
|
▀
|
||||||
case "^":
|
</text>
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
i += markerIndex + 1
|
if (char === "~") {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
return elements
|
if (!hold()) return
|
||||||
|
if (evt.type === "up") {
|
||||||
|
const item = hold()
|
||||||
|
if (!item) return
|
||||||
|
burst(item.x, item.y)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<box>
|
<box ref={(item: BoxRenderable) => (box = item)}>
|
||||||
|
<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, theme.textMuted, false)}</box>
|
<box flexDirection="row">{renderLine(line, index(), theme.textMuted, false, 0, frame(), dusk())}</box>
|
||||||
<box flexDirection="row">{renderLine(logo.right[index()], theme.text, true)}</box>
|
<box flexDirection="row">
|
||||||
|
{renderLine(logo.right[index()], index(), theme.text, true, LEFT + GAP, frame(), dusk())}
|
||||||
|
</box>
|
||||||
</box>
|
</box>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|||||||
@@ -589,6 +589,13 @@ export function Prompt(props: PromptProps) {
|
|||||||
])
|
])
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
|
// IME: double-defer may fire before onContentChange flushes the last
|
||||||
|
// composed character (e.g. Korean hangul) to the store, so read
|
||||||
|
// plainText directly and sync before any downstream reads.
|
||||||
|
if (input && !input.isDestroyed && input.plainText !== store.prompt.input) {
|
||||||
|
setStore("prompt", "input", input.plainText)
|
||||||
|
syncExtmarksWithPromptParts()
|
||||||
|
}
|
||||||
if (props.disabled) return
|
if (props.disabled) return
|
||||||
if (autocomplete?.visible) return
|
if (autocomplete?.visible) return
|
||||||
if (!store.prompt.input) return
|
if (!store.prompt.input) return
|
||||||
@@ -994,7 +1001,11 @@ export function Prompt(props: PromptProps) {
|
|||||||
input.cursorOffset = input.plainText.length
|
input.cursorOffset = input.plainText.length
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onSubmit={submit}
|
onSubmit={() => {
|
||||||
|
// IME: double-defer so the last composed character (e.g. Korean
|
||||||
|
// hangul) is flushed to plainText before we read it for submission.
|
||||||
|
setTimeout(() => setTimeout(() => submit(), 0), 0)
|
||||||
|
}}
|
||||||
onPaste={async (event: PasteEvent) => {
|
onPaste={async (event: PasteEvent) => {
|
||||||
if (props.disabled) {
|
if (props.disabled) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|||||||
@@ -1,151 +0,0 @@
|
|||||||
import { useDialog } from "@tui/ui/dialog"
|
|
||||||
import { DialogSelect } from "@tui/ui/dialog-select"
|
|
||||||
import { useRoute } from "@tui/context/route"
|
|
||||||
import { useSync } from "@tui/context/sync"
|
|
||||||
import { createMemo, createSignal, createResource, onMount, Show } from "solid-js"
|
|
||||||
import { Locale } from "@/util/locale"
|
|
||||||
import { useKeybind } from "../../context/keybind"
|
|
||||||
import { useTheme } from "../../context/theme"
|
|
||||||
import { useSDK } from "../../context/sdk"
|
|
||||||
import { DialogSessionRename } from "../dialog-session-rename"
|
|
||||||
import { useKV } from "../../context/kv"
|
|
||||||
import { createDebouncedSignal } from "../../util/signal"
|
|
||||||
import { Spinner } from "../spinner"
|
|
||||||
import { useToast } from "../../ui/toast"
|
|
||||||
|
|
||||||
export function DialogSessionList(props: { workspaceID?: string; localOnly?: boolean } = {}) {
|
|
||||||
const dialog = useDialog()
|
|
||||||
const route = useRoute()
|
|
||||||
const sync = useSync()
|
|
||||||
const keybind = useKeybind()
|
|
||||||
const { theme } = useTheme()
|
|
||||||
const sdk = useSDK()
|
|
||||||
const kv = useKV()
|
|
||||||
const toast = useToast()
|
|
||||||
const [toDelete, setToDelete] = createSignal<string>()
|
|
||||||
const [search, setSearch] = createDebouncedSignal("", 150)
|
|
||||||
|
|
||||||
const [listed, listedActions] = createResource(
|
|
||||||
() => props.workspaceID,
|
|
||||||
async (workspaceID) => {
|
|
||||||
if (!workspaceID) return undefined
|
|
||||||
const result = await sdk.client.session.list({ roots: true })
|
|
||||||
return result.data ?? []
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const [searchResults] = createResource(search, async (query) => {
|
|
||||||
if (!query || props.localOnly) return undefined
|
|
||||||
const result = await sdk.client.session.list({
|
|
||||||
search: query,
|
|
||||||
limit: 30,
|
|
||||||
...(props.workspaceID ? { roots: true } : {}),
|
|
||||||
})
|
|
||||||
return result.data ?? []
|
|
||||||
})
|
|
||||||
|
|
||||||
const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))
|
|
||||||
|
|
||||||
const sessions = createMemo(() => {
|
|
||||||
if (searchResults()) return searchResults()!
|
|
||||||
if (props.workspaceID) return listed() ?? []
|
|
||||||
if (props.localOnly) return sync.data.session.filter((session) => !session.workspaceID)
|
|
||||||
return sync.data.session
|
|
||||||
})
|
|
||||||
|
|
||||||
const options = createMemo(() => {
|
|
||||||
const today = new Date().toDateString()
|
|
||||||
return sessions()
|
|
||||||
.filter((x) => {
|
|
||||||
if (x.parentID !== undefined) return false
|
|
||||||
if (props.workspaceID && listed()) return true
|
|
||||||
if (props.workspaceID) return x.workspaceID === props.workspaceID
|
|
||||||
if (props.localOnly) return !x.workspaceID
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
.toSorted((a, b) => b.time.updated - a.time.updated)
|
|
||||||
.map((x) => {
|
|
||||||
const date = new Date(x.time.updated)
|
|
||||||
let category = date.toDateString()
|
|
||||||
if (category === today) {
|
|
||||||
category = "Today"
|
|
||||||
}
|
|
||||||
const isDeleting = toDelete() === x.id
|
|
||||||
const status = sync.data.session_status?.[x.id]
|
|
||||||
const isWorking = status?.type === "busy"
|
|
||||||
return {
|
|
||||||
title: isDeleting ? `Press ${keybind.print("session_delete")} again to confirm` : x.title,
|
|
||||||
bg: isDeleting ? theme.error : undefined,
|
|
||||||
value: x.id,
|
|
||||||
category,
|
|
||||||
footer: Locale.time(x.time.updated),
|
|
||||||
gutter: isWorking ? <Spinner /> : undefined,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
dialog.setSize("large")
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DialogSelect
|
|
||||||
title={props.workspaceID ? `Workspace Sessions` : props.localOnly ? "Local Sessions" : "Sessions"}
|
|
||||||
options={options()}
|
|
||||||
skipFilter={!props.localOnly}
|
|
||||||
current={currentSessionID()}
|
|
||||||
onFilter={setSearch}
|
|
||||||
onMove={() => {
|
|
||||||
setToDelete(undefined)
|
|
||||||
}}
|
|
||||||
onSelect={(option) => {
|
|
||||||
route.navigate({
|
|
||||||
type: "session",
|
|
||||||
sessionID: option.value,
|
|
||||||
})
|
|
||||||
dialog.clear()
|
|
||||||
}}
|
|
||||||
keybind={[
|
|
||||||
{
|
|
||||||
keybind: keybind.all.session_delete?.[0],
|
|
||||||
title: "delete",
|
|
||||||
onTrigger: async (option) => {
|
|
||||||
if (toDelete() === option.value) {
|
|
||||||
const deleted = await sdk.client.session
|
|
||||||
.delete({
|
|
||||||
sessionID: option.value,
|
|
||||||
})
|
|
||||||
.then(() => true)
|
|
||||||
.catch(() => false)
|
|
||||||
setToDelete(undefined)
|
|
||||||
if (!deleted) {
|
|
||||||
toast.show({
|
|
||||||
message: "Failed to delete session",
|
|
||||||
variant: "error",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (props.workspaceID) {
|
|
||||||
listedActions.mutate((sessions) => sessions?.filter((session) => session.id !== option.value))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sync.set(
|
|
||||||
"session",
|
|
||||||
sync.data.session.filter((session) => session.id !== option.value),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setToDelete(option.value)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keybind: keybind.all.session_rename?.[0],
|
|
||||||
title: "rename",
|
|
||||||
onTrigger: async (option) => {
|
|
||||||
dialog.replace(() => <DialogSessionRename session={option.value} />)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -8,6 +8,10 @@ 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)
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { batch } from "solid-js"
|
import { batch } from "solid-js"
|
||||||
import type { Path } from "@opencode-ai/sdk"
|
import type { Path, Workspace } from "@opencode-ai/sdk/v2"
|
||||||
import { createStore, reconcile } from "solid-js/store"
|
import { createStore, reconcile } from "solid-js/store"
|
||||||
import { createSimpleContext } from "./helper"
|
import { createSimpleContext } from "./helper"
|
||||||
import { useSDK } from "./sdk"
|
import { useSDK } from "./sdk"
|
||||||
|
|
||||||
|
type WorkspaceStatus = "connected" | "connecting" | "disconnected" | "error"
|
||||||
|
|
||||||
export const { use: useProject, provider: ProjectProvider } = createSimpleContext({
|
export const { use: useProject, provider: ProjectProvider } = createSimpleContext({
|
||||||
name: "Project",
|
name: "Project",
|
||||||
init: () => {
|
init: () => {
|
||||||
@@ -14,17 +16,22 @@ export const { use: useProject, provider: ProjectProvider } = createSimpleContex
|
|||||||
},
|
},
|
||||||
instance: {
|
instance: {
|
||||||
path: {
|
path: {
|
||||||
|
home: "",
|
||||||
state: "",
|
state: "",
|
||||||
config: "",
|
config: "",
|
||||||
worktree: "",
|
worktree: "",
|
||||||
directory: sdk.directory ?? "",
|
directory: sdk.directory ?? "",
|
||||||
} satisfies Path,
|
} satisfies Path,
|
||||||
},
|
},
|
||||||
workspace: undefined as string | undefined,
|
workspace: {
|
||||||
|
current: undefined as string | undefined,
|
||||||
|
list: [] as Workspace[],
|
||||||
|
status: {} as Record<string, WorkspaceStatus>,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
async function sync() {
|
async function sync() {
|
||||||
const workspace = store.workspace
|
const workspace = store.workspace.current
|
||||||
const [path, project] = await Promise.all([
|
const [path, project] = await Promise.all([
|
||||||
sdk.client.path.get({ workspace }),
|
sdk.client.path.get({ workspace }),
|
||||||
sdk.client.project.current({ workspace }),
|
sdk.client.project.current({ workspace }),
|
||||||
@@ -36,6 +43,27 @@ export const { use: useProject, provider: ProjectProvider } = createSimpleContex
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function syncWorkspace() {
|
||||||
|
const listed = await sdk.client.experimental.workspace.list().catch(() => undefined)
|
||||||
|
if (!listed?.data) return
|
||||||
|
const status = await sdk.client.experimental.workspace.status().catch(() => undefined)
|
||||||
|
const next = Object.fromEntries((status?.data ?? []).map((item) => [item.workspaceID, item.status]))
|
||||||
|
|
||||||
|
batch(() => {
|
||||||
|
setStore("workspace", "list", reconcile(listed.data))
|
||||||
|
setStore("workspace", "status", reconcile(next))
|
||||||
|
if (!listed.data.some((item) => item.id === store.workspace.current)) {
|
||||||
|
setStore("workspace", "current", undefined)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
sdk.event.on("event", (event) => {
|
||||||
|
if (event.payload.type === "workspace.status") {
|
||||||
|
setStore("workspace", "status", event.payload.properties.workspaceID, event.payload.properties.status)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: store,
|
data: store,
|
||||||
project() {
|
project() {
|
||||||
@@ -51,13 +79,26 @@ export const { use: useProject, provider: ProjectProvider } = createSimpleContex
|
|||||||
},
|
},
|
||||||
workspace: {
|
workspace: {
|
||||||
current() {
|
current() {
|
||||||
return store.workspace
|
return store.workspace.current
|
||||||
},
|
},
|
||||||
set(next?: string | null) {
|
set(next?: string | null) {
|
||||||
const workspace = next ?? undefined
|
const workspace = next ?? undefined
|
||||||
if (store.workspace === workspace) return
|
if (store.workspace.current === workspace) return
|
||||||
setStore("workspace", workspace)
|
setStore("workspace", "current", workspace)
|
||||||
},
|
},
|
||||||
|
list() {
|
||||||
|
return store.workspace.list
|
||||||
|
},
|
||||||
|
get(workspaceID: string) {
|
||||||
|
return store.workspace.list.find((item) => item.id === workspaceID)
|
||||||
|
},
|
||||||
|
status(workspaceID: string) {
|
||||||
|
return store.workspace.status[workspaceID]
|
||||||
|
},
|
||||||
|
statuses() {
|
||||||
|
return store.workspace.status
|
||||||
|
},
|
||||||
|
sync: syncWorkspace,
|
||||||
},
|
},
|
||||||
sync,
|
sync,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import type {
|
|||||||
ProviderListResponse,
|
ProviderListResponse,
|
||||||
ProviderAuthMethod,
|
ProviderAuthMethod,
|
||||||
VcsInfo,
|
VcsInfo,
|
||||||
Workspace,
|
|
||||||
} from "@opencode-ai/sdk/v2"
|
} from "@opencode-ai/sdk/v2"
|
||||||
import { createStore, produce, reconcile } from "solid-js/store"
|
import { createStore, produce, reconcile } from "solid-js/store"
|
||||||
import { useProject } from "@tui/context/project"
|
import { useProject } from "@tui/context/project"
|
||||||
@@ -75,7 +74,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
[key: string]: McpResource
|
[key: string]: McpResource
|
||||||
}
|
}
|
||||||
formatter: FormatterStatus[]
|
formatter: FormatterStatus[]
|
||||||
workspaceList: Workspace[]
|
|
||||||
vcs: VcsInfo | undefined
|
vcs: VcsInfo | undefined
|
||||||
}>({
|
}>({
|
||||||
provider_next: {
|
provider_next: {
|
||||||
@@ -103,7 +101,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
mcp: {},
|
mcp: {},
|
||||||
mcp_resource: {},
|
mcp_resource: {},
|
||||||
formatter: [],
|
formatter: [],
|
||||||
workspaceList: [],
|
|
||||||
vcs: undefined,
|
vcs: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -111,16 +108,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
const project = useProject()
|
const project = useProject()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
|
|
||||||
async function syncWorkspaces() {
|
|
||||||
const workspace = project.workspace.current()
|
|
||||||
const result = await sdk.client.experimental.workspace.list().catch(() => undefined)
|
|
||||||
if (!result?.data) return
|
|
||||||
setStore("workspaceList", reconcile(result.data))
|
|
||||||
if (!result.data.some((item) => item.id === workspace)) {
|
|
||||||
project.workspace.set(undefined)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
event.subscribe((event) => {
|
event.subscribe((event) => {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case "server.instance.disposed":
|
case "server.instance.disposed":
|
||||||
@@ -368,7 +355,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
const workspace = project.workspace.current()
|
const workspace = project.workspace.current()
|
||||||
const start = Date.now() - 30 * 24 * 60 * 60 * 1000
|
const start = Date.now() - 30 * 24 * 60 * 60 * 1000
|
||||||
const sessionListPromise = sdk.client.session
|
const sessionListPromise = sdk.client.session
|
||||||
.list({ start: start, workspace })
|
.list({ start: start })
|
||||||
.then((x) => (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)))
|
.then((x) => (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)))
|
||||||
|
|
||||||
// blocking - include session.list when continuing a session
|
// blocking - include session.list when continuing a session
|
||||||
@@ -443,7 +430,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
}),
|
}),
|
||||||
sdk.client.provider.auth({ workspace }).then((x) => setStore("provider_auth", reconcile(x.data ?? {}))),
|
sdk.client.provider.auth({ workspace }).then((x) => setStore("provider_auth", reconcile(x.data ?? {}))),
|
||||||
sdk.client.vcs.get({ workspace }).then((x) => setStore("vcs", reconcile(x.data))),
|
sdk.client.vcs.get({ workspace }).then((x) => setStore("vcs", reconcile(x.data))),
|
||||||
syncWorkspaces(),
|
project.workspace.sync(),
|
||||||
]).then(() => {
|
]).then(() => {
|
||||||
setStore("status", "complete")
|
setStore("status", "complete")
|
||||||
})
|
})
|
||||||
@@ -522,15 +509,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
fullSyncedSessions.add(sessionID)
|
fullSyncedSessions.add(sessionID)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
workspace: {
|
|
||||||
list() {
|
|
||||||
return store.workspaceList
|
|
||||||
},
|
|
||||||
get(workspaceID: string) {
|
|
||||||
return store.workspaceList.find((item) => item.id === workspaceID)
|
|
||||||
},
|
|
||||||
sync: syncWorkspaces,
|
|
||||||
},
|
|
||||||
bootstrap,
|
bootstrap,
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -542,8 +542,10 @@ 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 diffAddedLineNumberBg = tint(grays[3], ansiColors.green, diffAlpha)
|
const diffContextBg = grays[2]
|
||||||
const diffRemovedLineNumberBg = tint(grays[3], ansiColors.red, diffAlpha)
|
const diffAddedLineNumberBg = tint(diffContextBg, ansiColors.green, diffAlpha)
|
||||||
|
const diffRemovedLineNumberBg = tint(diffContextBg, ansiColors.red, diffAlpha)
|
||||||
|
const diffLineNumber = textMuted
|
||||||
|
|
||||||
return {
|
return {
|
||||||
theme: {
|
theme: {
|
||||||
@@ -583,8 +585,8 @@ function generateSystem(colors: TerminalColors, mode: "dark" | "light"): ThemeJs
|
|||||||
diffHighlightRemoved: ansiColors.redBright,
|
diffHighlightRemoved: ansiColors.redBright,
|
||||||
diffAddedBg,
|
diffAddedBg,
|
||||||
diffRemovedBg,
|
diffRemovedBg,
|
||||||
diffContextBg: grays[1],
|
diffContextBg,
|
||||||
diffLineNumber: grays[6],
|
diffLineNumber,
|
||||||
diffAddedLineNumberBg,
|
diffAddedLineNumberBg,
|
||||||
diffRemovedLineNumberBg,
|
diffRemovedLineNumberBg,
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
"diffAddedBg": "#354933",
|
"diffAddedBg": "#354933",
|
||||||
"diffRemovedBg": "#3f191a",
|
"diffRemovedBg": "#3f191a",
|
||||||
"diffContextBg": "darkBgPanel",
|
"diffContextBg": "darkBgPanel",
|
||||||
"diffLineNumber": "darkBorder",
|
"diffLineNumber": "#898989",
|
||||||
"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": "darkGutter",
|
"diffLineNumber": "diffContext",
|
||||||
"diffAddedLineNumberBg": "#1b2b34",
|
"diffAddedLineNumberBg": "#1b2b34",
|
||||||
"diffRemovedLineNumberBg": "#2d1f26",
|
"diffRemovedLineNumberBg": "#2d1f26",
|
||||||
"markdownText": "darkFg",
|
"markdownText": "darkFg",
|
||||||
|
|||||||
@@ -141,8 +141,8 @@
|
|||||||
"light": "lbg1"
|
"light": "lbg1"
|
||||||
},
|
},
|
||||||
"diffLineNumber": {
|
"diffLineNumber": {
|
||||||
"dark": "fg3",
|
"dark": "#808792",
|
||||||
"light": "lfg3"
|
"light": "textMuted"
|
||||||
},
|
},
|
||||||
"diffAddedLineNumberBg": {
|
"diffAddedLineNumberBg": {
|
||||||
"dark": "diffGreenBg",
|
"dark": "diffGreenBg",
|
||||||
|
|||||||
@@ -125,10 +125,7 @@
|
|||||||
"dark": "frappeMantle",
|
"dark": "frappeMantle",
|
||||||
"light": "frappeMantle"
|
"light": "frappeMantle"
|
||||||
},
|
},
|
||||||
"diffLineNumber": {
|
"diffLineNumber": "textMuted",
|
||||||
"dark": "frappeSurface1",
|
|
||||||
"light": "frappeSurface1"
|
|
||||||
},
|
|
||||||
"diffAddedLineNumberBg": {
|
"diffAddedLineNumberBg": {
|
||||||
"dark": "#223025",
|
"dark": "#223025",
|
||||||
"light": "#223025"
|
"light": "#223025"
|
||||||
|
|||||||
@@ -125,10 +125,7 @@
|
|||||||
"dark": "macMantle",
|
"dark": "macMantle",
|
||||||
"light": "macMantle"
|
"light": "macMantle"
|
||||||
},
|
},
|
||||||
"diffLineNumber": {
|
"diffLineNumber": "textMuted",
|
||||||
"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": "darkSurface1", "light": "lightSurface1" },
|
"diffLineNumber": { "dark": "textMuted", "light": "#5b5d63" },
|
||||||
"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,10 +120,7 @@
|
|||||||
"dark": "#122738",
|
"dark": "#122738",
|
||||||
"light": "#f5f7fa"
|
"light": "#f5f7fa"
|
||||||
},
|
},
|
||||||
"diffLineNumber": {
|
"diffLineNumber": "textMuted",
|
||||||
"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": "#e4e4e442",
|
"dark": "#eeeeee87",
|
||||||
"light": "#1414147a"
|
"light": "textMuted"
|
||||||
},
|
},
|
||||||
"diffAddedLineNumberBg": {
|
"diffAddedLineNumberBg": {
|
||||||
"dark": "#3fa26633",
|
"dark": "#3fa26633",
|
||||||
|
|||||||
@@ -112,8 +112,8 @@
|
|||||||
"light": "#e8e8e2"
|
"light": "#e8e8e2"
|
||||||
},
|
},
|
||||||
"diffLineNumber": {
|
"diffLineNumber": {
|
||||||
"dark": "currentLine",
|
"dark": "#989aa4",
|
||||||
"light": "#c8c8c2"
|
"light": "#686865"
|
||||||
},
|
},
|
||||||
"diffAddedLineNumberBg": {
|
"diffAddedLineNumberBg": {
|
||||||
"dark": "#1a3a1a",
|
"dark": "#1a3a1a",
|
||||||
|
|||||||
@@ -134,8 +134,8 @@
|
|||||||
"light": "lightStep2"
|
"light": "lightStep2"
|
||||||
},
|
},
|
||||||
"diffLineNumber": {
|
"diffLineNumber": {
|
||||||
"dark": "darkStep3",
|
"dark": "#a0a5a7",
|
||||||
"light": "lightStep3"
|
"light": "#5b5951"
|
||||||
},
|
},
|
||||||
"diffAddedLineNumberBg": {
|
"diffAddedLineNumberBg": {
|
||||||
"dark": "#1b2b34",
|
"dark": "#1b2b34",
|
||||||
|
|||||||
@@ -130,8 +130,8 @@
|
|||||||
"light": "base50"
|
"light": "base50"
|
||||||
},
|
},
|
||||||
"diffLineNumber": {
|
"diffLineNumber": {
|
||||||
"dark": "base600",
|
"dark": "#888883",
|
||||||
"light": "base600"
|
"light": "#5a5955"
|
||||||
},
|
},
|
||||||
"diffAddedLineNumberBg": {
|
"diffAddedLineNumberBg": {
|
||||||
"dark": "#152515",
|
"dark": "#152515",
|
||||||
|
|||||||
@@ -126,8 +126,8 @@
|
|||||||
"light": "lightBgAlt"
|
"light": "lightBgAlt"
|
||||||
},
|
},
|
||||||
"diffLineNumber": {
|
"diffLineNumber": {
|
||||||
"dark": "#484f58",
|
"dark": "#95999e",
|
||||||
"light": "#afb8c1"
|
"light": "textMuted"
|
||||||
},
|
},
|
||||||
"diffAddedLineNumberBg": {
|
"diffAddedLineNumberBg": {
|
||||||
"dark": "#033a16",
|
"dark": "#033a16",
|
||||||
|
|||||||
@@ -135,8 +135,8 @@
|
|||||||
"light": "lightBg1"
|
"light": "lightBg1"
|
||||||
},
|
},
|
||||||
"diffLineNumber": {
|
"diffLineNumber": {
|
||||||
"dark": "darkBg3",
|
"dark": "#a8a29e",
|
||||||
"light": "lightBg3"
|
"light": "#564f43"
|
||||||
},
|
},
|
||||||
"diffAddedLineNumberBg": {
|
"diffAddedLineNumberBg": {
|
||||||
"dark": "#2a2827",
|
"dark": "#2a2827",
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
"diffAddedBg": { "dark": "#252E25", "light": "#EAF3E4" },
|
"diffAddedBg": { "dark": "#252E25", "light": "#EAF3E4" },
|
||||||
"diffRemovedBg": { "dark": "#362020", "light": "#FBE6E6" },
|
"diffRemovedBg": { "dark": "#362020", "light": "#FBE6E6" },
|
||||||
"diffContextBg": { "dark": "sumiInk1", "light": "lightPaper" },
|
"diffContextBg": { "dark": "sumiInk1", "light": "lightPaper" },
|
||||||
"diffLineNumber": { "dark": "sumiInk3", "light": "#C7BEB4" },
|
"diffLineNumber": { "dark": "#9090a0", "light": "#65615c" },
|
||||||
"diffAddedLineNumberBg": { "dark": "#202820", "light": "#DDE8D6" },
|
"diffAddedLineNumberBg": { "dark": "#202820", "light": "#DDE8D6" },
|
||||||
"diffRemovedLineNumberBg": { "dark": "#2D1C1C", "light": "#F2DADA" },
|
"diffRemovedLineNumberBg": { "dark": "#2D1C1C", "light": "#F2DADA" },
|
||||||
"markdownText": { "dark": "fujiWhite", "light": "lightText" },
|
"markdownText": { "dark": "fujiWhite", "light": "lightText" },
|
||||||
|
|||||||
@@ -129,10 +129,7 @@
|
|||||||
"dark": "transparent",
|
"dark": "transparent",
|
||||||
"light": "transparent"
|
"light": "transparent"
|
||||||
},
|
},
|
||||||
"diffLineNumber": {
|
"diffLineNumber": "textMuted",
|
||||||
"dark": "#666666",
|
|
||||||
"light": "#999999"
|
|
||||||
},
|
|
||||||
"diffAddedLineNumberBg": {
|
"diffAddedLineNumberBg": {
|
||||||
"dark": "transparent",
|
"dark": "transparent",
|
||||||
"light": "transparent"
|
"light": "transparent"
|
||||||
|
|||||||
@@ -128,8 +128,8 @@
|
|||||||
"light": "lightBgAlt"
|
"light": "lightBgAlt"
|
||||||
},
|
},
|
||||||
"diffLineNumber": {
|
"diffLineNumber": {
|
||||||
"dark": "#37474f",
|
"dark": "#9aa2a6",
|
||||||
"light": "#cfd8dc"
|
"light": "#6a6e70"
|
||||||
},
|
},
|
||||||
"diffAddedLineNumberBg": {
|
"diffAddedLineNumberBg": {
|
||||||
"dark": "#2e3c2b",
|
"dark": "#2e3c2b",
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
"diffAddedBg": { "dark": "#132616", "light": "#e0efde" },
|
"diffAddedBg": { "dark": "#132616", "light": "#e0efde" },
|
||||||
"diffRemovedBg": { "dark": "#261212", "light": "#f9e5e5" },
|
"diffRemovedBg": { "dark": "#261212", "light": "#f9e5e5" },
|
||||||
"diffContextBg": { "dark": "matrixInk1", "light": "lightPaper" },
|
"diffContextBg": { "dark": "matrixInk1", "light": "lightPaper" },
|
||||||
"diffLineNumber": { "dark": "matrixInk3", "light": "lightGray" },
|
"diffLineNumber": { "dark": "textMuted", "light": "#556156" },
|
||||||
"diffAddedLineNumberBg": { "dark": "#0f1b11", "light": "#d6e7d2" },
|
"diffAddedLineNumberBg": { "dark": "#0f1b11", "light": "#d6e7d2" },
|
||||||
"diffRemovedLineNumberBg": { "dark": "#1b1414", "light": "#f2d2d2" },
|
"diffRemovedLineNumberBg": { "dark": "#1b1414", "light": "#f2d2d2" },
|
||||||
"markdownText": { "dark": "rainGreenHi", "light": "lightText" },
|
"markdownText": { "dark": "rainGreenHi", "light": "lightText" },
|
||||||
|
|||||||
@@ -114,8 +114,8 @@
|
|||||||
"light": "#f0f0f0"
|
"light": "#f0f0f0"
|
||||||
},
|
},
|
||||||
"diffLineNumber": {
|
"diffLineNumber": {
|
||||||
"dark": "#3e3d32",
|
"dark": "#9b9b95",
|
||||||
"light": "#d0d0d0"
|
"light": "#686868"
|
||||||
},
|
},
|
||||||
"diffAddedLineNumberBg": {
|
"diffAddedLineNumberBg": {
|
||||||
"dark": "#1a3a1a",
|
"dark": "#1a3a1a",
|
||||||
|
|||||||
@@ -114,8 +114,8 @@
|
|||||||
"light": "nightOwlPanel"
|
"light": "nightOwlPanel"
|
||||||
},
|
},
|
||||||
"diffLineNumber": {
|
"diffLineNumber": {
|
||||||
"dark": "nightOwlMuted",
|
"dark": "#7791a6",
|
||||||
"light": "nightOwlMuted"
|
"light": "#7791a6"
|
||||||
},
|
},
|
||||||
"diffAddedLineNumberBg": {
|
"diffAddedLineNumberBg": {
|
||||||
"dark": "#0a2e1a",
|
"dark": "#0a2e1a",
|
||||||
|
|||||||
@@ -116,8 +116,8 @@
|
|||||||
"light": "nord5"
|
"light": "nord5"
|
||||||
},
|
},
|
||||||
"diffLineNumber": {
|
"diffLineNumber": {
|
||||||
"dark": "nord2",
|
"dark": "#a9aeb6",
|
||||||
"light": "nord4"
|
"light": "textMuted"
|
||||||
},
|
},
|
||||||
"diffAddedLineNumberBg": {
|
"diffAddedLineNumberBg": {
|
||||||
"dark": "#3B4252",
|
"dark": "#3B4252",
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
"diffAddedBg": { "dark": "#2c382b", "light": "#eafbe9" },
|
"diffAddedBg": { "dark": "#2c382b", "light": "#eafbe9" },
|
||||||
"diffRemovedBg": { "dark": "#3a2d2f", "light": "#fce9e8" },
|
"diffRemovedBg": { "dark": "#3a2d2f", "light": "#fce9e8" },
|
||||||
"diffContextBg": { "dark": "darkBgAlt", "light": "lightBgAlt" },
|
"diffContextBg": { "dark": "darkBgAlt", "light": "lightBgAlt" },
|
||||||
"diffLineNumber": { "dark": "#495162", "light": "#c9c9ca" },
|
"diffLineNumber": { "dark": "#9398a2", "light": "#666666" },
|
||||||
"diffAddedLineNumberBg": { "dark": "#283427", "light": "#e1f3df" },
|
"diffAddedLineNumberBg": { "dark": "#283427", "light": "#e1f3df" },
|
||||||
"diffRemovedLineNumberBg": { "dark": "#36292b", "light": "#f5e2e1" },
|
"diffRemovedLineNumberBg": { "dark": "#36292b", "light": "#f5e2e1" },
|
||||||
"markdownText": { "dark": "darkFg", "light": "lightFg" },
|
"markdownText": { "dark": "darkFg", "light": "lightFg" },
|
||||||
|
|||||||
@@ -138,8 +138,8 @@
|
|||||||
"light": "lightStep2"
|
"light": "lightStep2"
|
||||||
},
|
},
|
||||||
"diffLineNumber": {
|
"diffLineNumber": {
|
||||||
"dark": "darkStep3",
|
"dark": "#8f8f8f",
|
||||||
"light": "lightStep3"
|
"light": "#595959"
|
||||||
},
|
},
|
||||||
"diffAddedLineNumberBg": {
|
"diffAddedLineNumberBg": {
|
||||||
"dark": "#1b2b34",
|
"dark": "#1b2b34",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user