Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09ff3b9bb9 | ||
|
|
2256362ba2 | ||
|
|
077ca4454f | ||
|
|
05cbb11709 | ||
|
|
fcc561ebb7 | ||
|
|
ee6ca104e5 | ||
|
|
4347a77d89 | ||
|
|
76b10d85ee | ||
|
|
45a770cdb1 | ||
|
|
a57c8669b6 | ||
|
|
f9fcdead55 | ||
|
|
ff669d4414 | ||
|
|
9b2d595cfc | ||
|
|
3839d70a94 | ||
|
|
6fe265e7d8 | ||
|
|
2aed4d263b | ||
|
|
e2ac588c84 | ||
|
|
8917dfdf5e | ||
|
|
86900d71f5 | ||
|
|
adcc661798 | ||
|
|
f4a28b2659 | ||
|
|
a160a35d0c | ||
|
|
90eaf9b3fc | ||
|
|
16d516dbdb | ||
|
|
0026bc5815 | ||
|
|
bcdaf7e779 | ||
|
|
874e22a045 | ||
|
|
905226c01e | ||
|
|
73adf7e86f | ||
|
|
4c37e17ac2 | ||
|
|
cd6e07355b | ||
|
|
29703aee9a | ||
|
|
3997d3f2d7 | ||
|
|
1fccb3bda4 | ||
|
|
16b2bfa8ef | ||
|
|
4eb6b57503 | ||
|
|
7599396162 | ||
|
|
d99d1315ee | ||
|
|
d831432f93 | ||
|
|
0ddf8e6c6e | ||
|
|
a520c4ff98 | ||
|
|
a184714f67 | ||
|
|
9b76337236 | ||
|
|
4dc72669e5 | ||
|
|
dfa59dd21d | ||
|
|
f642a6c5b9 | ||
|
|
e37104cb10 | ||
|
|
dc654c93d2 | ||
|
|
c67b0a9ba4 | ||
|
|
5b699a0d9b | ||
|
|
bc557e828d | ||
|
|
fcaa041ef9 | ||
|
|
3c9d80d75f | ||
|
|
a761f66a16 | ||
|
|
15e80fca69 | ||
|
|
43680534df | ||
|
|
aa522aad62 | ||
|
|
82319bbd83 | ||
|
|
45fa4eda15 | ||
|
|
f242541ef3 | ||
|
|
562f067131 | ||
|
|
1ff46c75fa | ||
|
|
73d5cacc06 | ||
|
|
b8828f2609 | ||
|
|
2f7b2cf603 | ||
|
|
eaf18d9915 | ||
|
|
7aa7dd3690 | ||
|
|
bee4b6801e | ||
|
|
3565d8e44d | ||
|
|
0187b6bb72 | ||
|
|
0eb898abcf | ||
|
|
5a309c2dbf | ||
|
|
452f11ff77 | ||
|
|
774c24e76e | ||
|
|
ec4a44087b | ||
|
|
501347cda5 | ||
|
|
3f3816c0f2 | ||
|
|
0a3c72d678 | ||
|
|
66b7a4991e | ||
|
|
1550ae47c0 | ||
|
|
33ba064c40 | ||
|
|
96ae5925c3 | ||
|
|
3a750b0809 | ||
|
|
1258f7aeea | ||
|
|
797a56873d | ||
|
|
05867f9318 | ||
|
|
5947fe72e4 | ||
|
|
f3d4dd5099 | ||
|
|
b68a4a8838 | ||
|
|
b7a1d8f2f5 | ||
|
|
80e1173ef7 | ||
|
|
8ae10f1c94 | ||
|
|
f24251f89e | ||
|
|
3600bd27f4 | ||
|
|
92089bb295 | ||
|
|
a70932f742 | ||
|
|
217cf24c3c | ||
|
|
c87939ad12 | ||
|
|
a092f567b7 | ||
|
|
883a6577d5 | ||
|
|
20b52cad2a | ||
|
|
528291532b | ||
|
|
2b77a84c4f | ||
|
|
7d0b52dc29 | ||
|
|
c86c2acf4c | ||
|
|
29bf731d47 | ||
|
|
3c5a256f0f | ||
|
|
067338bc25 | ||
|
|
736cd10847 | ||
|
|
a03daa4252 | ||
|
|
b01eec38d1 | ||
|
|
35cb06e0e4 | ||
|
|
f3b7d2f786 | ||
|
|
1facf7d8e4 | ||
|
|
ddd9c71cca | ||
|
|
520a814fc2 | ||
|
|
2072c8681a | ||
|
|
21990621e2 | ||
|
|
68a0947292 | ||
|
|
f1f44644e2 | ||
|
|
afb8a0d28a | ||
|
|
f05f175842 | ||
|
|
c0b214232d | ||
|
|
f4f8f2d151 | ||
|
|
b4ad5c138e | ||
|
|
789e111a0f | ||
|
|
eaa76dad0c | ||
|
|
66f9bdab32 | ||
|
|
efaa9166fb | ||
|
|
eb2044989e | ||
|
|
5d37e58d34 | ||
|
|
20088a87b0 | ||
|
|
05019dae76 | ||
|
|
bf37a88f7f | ||
|
|
d839f70834 | ||
|
|
498a4ab408 | ||
|
|
835e48cd28 | ||
|
|
1a5a63843e | ||
|
|
d954e1e3b6 | ||
|
|
8f22a6b69d | ||
|
|
fd37d5b54e | ||
|
|
e146083b73 | ||
|
|
d7a1c268d9 | ||
|
|
5c4345da4f | ||
|
|
0be37cc2c6 | ||
|
|
64f0205f97 | ||
|
|
b3a1360ad9 | ||
|
|
08d4d6d4af | ||
|
|
9c69c1de9f | ||
|
|
735f3d17bc |
@@ -1,4 +1,4 @@
|
|||||||
- To test opencode in the `packages/opencode` directory you can run `bun dev`
|
- To test opencode in `packages/opencode`, run `bun dev`.
|
||||||
- To regenerate the javascript SDK, run ./packages/sdk/js/script/build.ts
|
- To regenerate the JavaScript SDK, run `./packages/sdk/js/script/build.ts`.
|
||||||
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE.
|
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE.
|
||||||
- the default branch in this repo is `dev`
|
- The default branch in this repo is `dev`.
|
||||||
|
|||||||
@@ -199,3 +199,5 @@
|
|||||||
| 2026-01-10 | 2,632,023 (+188,458) | 1,503,670 (+34,219) | 4,135,693 (+222,677) |
|
| 2026-01-10 | 2,632,023 (+188,458) | 1,503,670 (+34,219) | 4,135,693 (+222,677) |
|
||||||
| 2026-01-11 | 2,836,394 (+204,371) | 1,530,479 (+26,809) | 4,366,873 (+231,180) |
|
| 2026-01-11 | 2,836,394 (+204,371) | 1,530,479 (+26,809) | 4,366,873 (+231,180) |
|
||||||
| 2026-01-12 | 3,053,594 (+217,200) | 1,553,671 (+23,192) | 4,607,265 (+240,392) |
|
| 2026-01-12 | 3,053,594 (+217,200) | 1,553,671 (+23,192) | 4,607,265 (+240,392) |
|
||||||
|
| 2026-01-13 | 3,297,078 (+243,484) | 1,595,062 (+41,391) | 4,892,140 (+284,875) |
|
||||||
|
| 2026-01-14 | 3,568,928 (+271,850) | 1,645,362 (+50,300) | 5,214,290 (+322,150) |
|
||||||
|
|||||||
+9
-12
@@ -1,19 +1,16 @@
|
|||||||
## Style Guide
|
## Style Guide
|
||||||
|
|
||||||
- Try to keep things in one function unless composable or reusable
|
- Keep things in one function unless composable or reusable
|
||||||
- AVOID unnecessary destructuring of variables. instead of doing `const { a, b }
|
- Avoid unnecessary destructuring. Instead of `const { a, b } = obj`, use `obj.a` and `obj.b` to preserve context
|
||||||
= obj` just reference it as obj.a and obj.b. this preserves context
|
- Avoid `try`/`catch` where possible
|
||||||
- AVOID `try`/`catch` where possible
|
- Avoid using the `any` type
|
||||||
- AVOID using `any` type
|
- Prefer single word variable names where possible
|
||||||
- PREFER single word variable names where possible
|
- Use Bun APIs when possible, like `Bun.file()`
|
||||||
- Use as many bun apis as possible like Bun.file()
|
|
||||||
|
|
||||||
# Avoid let statements
|
# Avoid let statements
|
||||||
|
|
||||||
we don't like let statements, especially combined with if/else statements.
|
We don't like `let` statements, especially combined with if/else statements.
|
||||||
prefer const
|
Prefer `const`.
|
||||||
|
|
||||||
This is bad:
|
|
||||||
|
|
||||||
Good:
|
Good:
|
||||||
|
|
||||||
@@ -32,7 +29,7 @@ else foo = 2
|
|||||||
|
|
||||||
# Avoid else statements
|
# Avoid else statements
|
||||||
|
|
||||||
Prefer early returns or even using `iife` to avoid else statements
|
Prefer early returns or using an `iife` to avoid else statements.
|
||||||
|
|
||||||
Good:
|
Good:
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.1.15",
|
"version": "1.1.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.1.15",
|
"version": "1.1.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
@@ -84,10 +84,12 @@
|
|||||||
"@solidjs/meta": "catalog:",
|
"@solidjs/meta": "catalog:",
|
||||||
"@solidjs/router": "catalog:",
|
"@solidjs/router": "catalog:",
|
||||||
"@solidjs/start": "catalog:",
|
"@solidjs/start": "catalog:",
|
||||||
|
"@stripe/stripe-js": "8.6.1",
|
||||||
"chart.js": "4.5.1",
|
"chart.js": "4.5.1",
|
||||||
"nitro": "3.0.1-alpha.1",
|
"nitro": "3.0.1-alpha.1",
|
||||||
"solid-js": "catalog:",
|
"solid-js": "catalog:",
|
||||||
"solid-list": "0.3.0",
|
"solid-list": "0.3.0",
|
||||||
|
"solid-stripe": "0.8.1",
|
||||||
"vite": "catalog:",
|
"vite": "catalog:",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -99,7 +101,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.1.15",
|
"version": "1.1.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-sts": "3.782.0",
|
"@aws-sdk/client-sts": "3.782.0",
|
||||||
"@jsx-email/render": "1.1.1",
|
"@jsx-email/render": "1.1.1",
|
||||||
@@ -126,7 +128,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.1.15",
|
"version": "1.1.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "2.0.0",
|
"@ai-sdk/anthropic": "2.0.0",
|
||||||
"@ai-sdk/openai": "2.0.2",
|
"@ai-sdk/openai": "2.0.2",
|
||||||
@@ -150,7 +152,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.1.15",
|
"version": "1.1.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
@@ -174,7 +176,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.1.15",
|
"version": "1.1.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/app": "workspace:*",
|
"@opencode-ai/app": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
@@ -203,7 +205,7 @@
|
|||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.1.15",
|
"version": "1.1.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
@@ -232,7 +234,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.1.15",
|
"version": "1.1.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
@@ -248,7 +250,7 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.1.15",
|
"version": "1.1.20",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -276,6 +278,7 @@
|
|||||||
"@ai-sdk/vercel": "1.0.31",
|
"@ai-sdk/vercel": "1.0.31",
|
||||||
"@ai-sdk/xai": "2.0.51",
|
"@ai-sdk/xai": "2.0.51",
|
||||||
"@clack/prompts": "1.0.0-alpha.1",
|
"@clack/prompts": "1.0.0-alpha.1",
|
||||||
|
"@gitlab/gitlab-ai-provider": "3.1.0",
|
||||||
"@hono/standard-validator": "0.1.5",
|
"@hono/standard-validator": "0.1.5",
|
||||||
"@hono/zod-validator": "catalog:",
|
"@hono/zod-validator": "catalog:",
|
||||||
"@modelcontextprotocol/sdk": "1.25.2",
|
"@modelcontextprotocol/sdk": "1.25.2",
|
||||||
@@ -351,7 +354,7 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.1.15",
|
"version": "1.1.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
@@ -371,7 +374,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.1.15",
|
"version": "1.1.20",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hey-api/openapi-ts": "0.88.1",
|
"@hey-api/openapi-ts": "0.88.1",
|
||||||
"@tsconfig/node22": "catalog:",
|
"@tsconfig/node22": "catalog:",
|
||||||
@@ -382,7 +385,7 @@
|
|||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.1.15",
|
"version": "1.1.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -395,7 +398,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.1.15",
|
"version": "1.1.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -407,6 +410,7 @@
|
|||||||
"@solid-primitives/resize-observer": "2.1.3",
|
"@solid-primitives/resize-observer": "2.1.3",
|
||||||
"@solidjs/meta": "catalog:",
|
"@solidjs/meta": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
|
"dompurify": "catalog:",
|
||||||
"fuzzysort": "catalog:",
|
"fuzzysort": "catalog:",
|
||||||
"katex": "0.16.27",
|
"katex": "0.16.27",
|
||||||
"luxon": "catalog:",
|
"luxon": "catalog:",
|
||||||
@@ -434,7 +438,7 @@
|
|||||||
},
|
},
|
||||||
"packages/util": {
|
"packages/util": {
|
||||||
"name": "@opencode-ai/util",
|
"name": "@opencode-ai/util",
|
||||||
"version": "1.1.15",
|
"version": "1.1.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -445,7 +449,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.1.15",
|
"version": "1.1.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
@@ -507,6 +511,7 @@
|
|||||||
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
||||||
"ai": "5.0.119",
|
"ai": "5.0.119",
|
||||||
"diff": "8.0.2",
|
"diff": "8.0.2",
|
||||||
|
"dompurify": "3.3.1",
|
||||||
"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",
|
||||||
@@ -584,6 +589,10 @@
|
|||||||
|
|
||||||
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
||||||
|
|
||||||
|
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.71.2", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ=="],
|
||||||
|
|
||||||
|
"@anycable/core": ["@anycable/core@0.9.2", "", { "dependencies": { "nanoevents": "^7.0.1" } }, "sha512-x5ZXDcW/N4cxWl93CnbHs/u7qq4793jS2kNPWm+duPrXlrva+ml2ZGT7X9tuOBKzyIHf60zWCdIK7TUgMPAwXA=="],
|
||||||
|
|
||||||
"@astrojs/cloudflare": ["@astrojs/cloudflare@12.6.3", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.1", "@astrojs/underscore-redirects": "1.0.0", "@cloudflare/workers-types": "^4.20250507.0", "tinyglobby": "^0.2.13", "vite": "^6.3.5", "wrangler": "^4.14.1" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-xhJptF5tU2k5eo70nIMyL1Udma0CqmUEnGSlGyFflLqSY82CRQI6nWZ/xZt0ZvmXuErUjIx0YYQNfZsz5CNjLQ=="],
|
"@astrojs/cloudflare": ["@astrojs/cloudflare@12.6.3", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.1", "@astrojs/underscore-redirects": "1.0.0", "@cloudflare/workers-types": "^4.20250507.0", "tinyglobby": "^0.2.13", "vite": "^6.3.5", "wrangler": "^4.14.1" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-xhJptF5tU2k5eo70nIMyL1Udma0CqmUEnGSlGyFflLqSY82CRQI6nWZ/xZt0ZvmXuErUjIx0YYQNfZsz5CNjLQ=="],
|
||||||
|
|
||||||
"@astrojs/compiler": ["@astrojs/compiler@2.13.0", "", {}, "sha512-mqVORhUJViA28fwHYaWmsXSzLO9osbdZ5ImUfxBarqsYdMlPbqAqGJCxsNzvppp1BEzc1mJNjOVvQqeDN8Vspw=="],
|
"@astrojs/compiler": ["@astrojs/compiler@2.13.0", "", {}, "sha512-mqVORhUJViA28fwHYaWmsXSzLO9osbdZ5ImUfxBarqsYdMlPbqAqGJCxsNzvppp1BEzc1mJNjOVvQqeDN8Vspw=="],
|
||||||
@@ -904,6 +913,10 @@
|
|||||||
|
|
||||||
"@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="],
|
"@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="],
|
||||||
|
|
||||||
|
"@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.1.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.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-S0MVXsogrwbOboA/8L0CY5sBXg2HrrO8gdeUeHd9yLZDPsggFD0FzcSuzO5vBO6geUOpruRa8Hqrbb6WWu7Frw=="],
|
||||||
|
|
||||||
|
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
|
||||||
|
|
||||||
"@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="],
|
"@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="],
|
||||||
|
|
||||||
"@hey-api/codegen-core": ["@hey-api/codegen-core@0.3.3", "", { "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-vArVDtrvdzFewu1hnjUm4jX1NBITlSCeO81EdWq676MxQbyxsGcDPAgohaSA+Wvr4HjPSvsg2/1s2zYxUtXebg=="],
|
"@hey-api/codegen-core": ["@hey-api/codegen-core@0.3.3", "", { "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-vArVDtrvdzFewu1hnjUm4jX1NBITlSCeO81EdWq676MxQbyxsGcDPAgohaSA+Wvr4HjPSvsg2/1s2zYxUtXebg=="],
|
||||||
@@ -1598,6 +1611,8 @@
|
|||||||
|
|
||||||
"@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="],
|
"@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="],
|
||||||
|
|
||||||
|
"@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="],
|
||||||
|
|
||||||
"@solid-primitives/active-element": ["@solid-primitives/active-element@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-9t5K4aR2naVDj950XU8OjnLgOg94a8k5wr6JNOPK+N5ESLsJDq42c1ZP8UKpewi1R+wplMMxiM6OPKRzbxJY7A=="],
|
"@solid-primitives/active-element": ["@solid-primitives/active-element@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-9t5K4aR2naVDj950XU8OjnLgOg94a8k5wr6JNOPK+N5ESLsJDq42c1ZP8UKpewi1R+wplMMxiM6OPKRzbxJY7A=="],
|
||||||
|
|
||||||
"@solid-primitives/audio": ["@solid-primitives/audio@1.4.2", "", { "dependencies": { "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-UMD3ORQfI5Ky8yuKPxidDiEazsjv/dsoiKK5yZxLnsgaeNR1Aym3/77h/qT1jBYeXUgj4DX6t7NMpFUSVr14OQ=="],
|
"@solid-primitives/audio": ["@solid-primitives/audio@1.4.2", "", { "dependencies": { "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-UMD3ORQfI5Ky8yuKPxidDiEazsjv/dsoiKK5yZxLnsgaeNR1Aym3/77h/qT1jBYeXUgj4DX6t7NMpFUSVr14OQ=="],
|
||||||
@@ -1650,6 +1665,8 @@
|
|||||||
|
|
||||||
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
|
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
|
||||||
|
|
||||||
|
"@stripe/stripe-js": ["@stripe/stripe-js@8.6.1", "", {}, "sha512-UJ05U2062XDgydbUcETH1AoRQLNhigQ2KmDn1BG8sC3xfzu6JKg95Qt6YozdzFpxl1Npii/02m2LEWFt1RYjVA=="],
|
||||||
|
|
||||||
"@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="],
|
"@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="],
|
||||||
|
|
||||||
"@tailwindcss/node": ["@tailwindcss/node@4.1.11", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.11" } }, "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q=="],
|
"@tailwindcss/node": ["@tailwindcss/node@4.1.11", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.11" } }, "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q=="],
|
||||||
@@ -1828,6 +1845,8 @@
|
|||||||
|
|
||||||
"@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="],
|
"@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="],
|
||||||
|
|
||||||
|
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||||
|
|
||||||
"@types/tsscmp": ["@types/tsscmp@1.0.2", "", {}, "sha512-cy7BRSU8GYYgxjcx0Py+8lo5MthuDhlyu076KUcYzVNXL23luYgRHkMG2fIFEc6neckeh/ntP82mw+U4QjZq+g=="],
|
"@types/tsscmp": ["@types/tsscmp@1.0.2", "", {}, "sha512-cy7BRSU8GYYgxjcx0Py+8lo5MthuDhlyu076KUcYzVNXL23luYgRHkMG2fIFEc6neckeh/ntP82mw+U4QjZq+g=="],
|
||||||
|
|
||||||
"@types/tunnel": ["@types/tunnel@0.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA=="],
|
"@types/tunnel": ["@types/tunnel@0.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA=="],
|
||||||
@@ -2280,6 +2299,8 @@
|
|||||||
|
|
||||||
"domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
|
"domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
|
||||||
|
|
||||||
|
"dompurify": ["dompurify@3.3.1", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q=="],
|
||||||
|
|
||||||
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
|
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
|
||||||
|
|
||||||
"dot-case": ["dot-case@3.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w=="],
|
"dot-case": ["dot-case@3.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w=="],
|
||||||
@@ -2312,6 +2333,10 @@
|
|||||||
|
|
||||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||||
|
|
||||||
|
"engine.io-client": ["engine.io-client@6.6.4", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.18.3", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw=="],
|
||||||
|
|
||||||
|
"engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="],
|
||||||
|
|
||||||
"enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
|
"enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
|
||||||
|
|
||||||
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||||
@@ -2534,6 +2559,10 @@
|
|||||||
|
|
||||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||||
|
|
||||||
|
"graphql": ["graphql@16.12.0", "", {}, "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ=="],
|
||||||
|
|
||||||
|
"graphql-request": ["graphql-request@6.1.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.2.0", "cross-fetch": "^3.1.5" }, "peerDependencies": { "graphql": "14 - 16" } }, "sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw=="],
|
||||||
|
|
||||||
"gray-matter": ["gray-matter@4.0.3", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="],
|
"gray-matter": ["gray-matter@4.0.3", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="],
|
||||||
|
|
||||||
"gtoken": ["gtoken@8.0.0", "", { "dependencies": { "gaxios": "^7.0.0", "jws": "^4.0.0" } }, "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw=="],
|
"gtoken": ["gtoken@8.0.0", "", { "dependencies": { "gaxios": "^7.0.0", "jws": "^4.0.0" } }, "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw=="],
|
||||||
@@ -2762,6 +2791,8 @@
|
|||||||
|
|
||||||
"isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="],
|
"isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="],
|
||||||
|
|
||||||
|
"isomorphic-ws": ["isomorphic-ws@5.0.0", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="],
|
||||||
|
|
||||||
"iterate-iterator": ["iterate-iterator@1.0.2", "", {}, "sha512-t91HubM4ZDQ70M9wqp+pcNpu8OyJ9UAtXntT/Bcsvp5tZMnz9vRa+IunKXeI8AnfZMTv0jNuVEmGeLSMjVvfPw=="],
|
"iterate-iterator": ["iterate-iterator@1.0.2", "", {}, "sha512-t91HubM4ZDQ70M9wqp+pcNpu8OyJ9UAtXntT/Bcsvp5tZMnz9vRa+IunKXeI8AnfZMTv0jNuVEmGeLSMjVvfPw=="],
|
||||||
|
|
||||||
"iterate-value": ["iterate-value@1.0.2", "", { "dependencies": { "es-get-iterator": "^1.0.2", "iterate-iterator": "^1.0.1" } }, "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ=="],
|
"iterate-value": ["iterate-value@1.0.2", "", { "dependencies": { "es-get-iterator": "^1.0.2", "iterate-iterator": "^1.0.1" } }, "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ=="],
|
||||||
@@ -2794,6 +2825,8 @@
|
|||||||
|
|
||||||
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
|
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
|
||||||
|
|
||||||
|
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
|
||||||
|
|
||||||
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||||
|
|
||||||
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
|
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
|
||||||
@@ -3070,6 +3103,8 @@
|
|||||||
|
|
||||||
"named-placeholders": ["named-placeholders@1.1.3", "", { "dependencies": { "lru-cache": "^7.14.1" } }, "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w=="],
|
"named-placeholders": ["named-placeholders@1.1.3", "", { "dependencies": { "lru-cache": "^7.14.1" } }, "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w=="],
|
||||||
|
|
||||||
|
"nanoevents": ["nanoevents@7.0.1", "", {}, "sha512-o6lpKiCxLeijK4hgsqfR6CNToPyRU3keKyyI6uwuHRvpRTbZ0wXw51WRgyldVugZqoJfkGFrjrIenYH3bfEO3Q=="],
|
||||||
|
|
||||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
"negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
"negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
||||||
@@ -3512,6 +3547,10 @@
|
|||||||
|
|
||||||
"smol-toml": ["smol-toml@1.5.2", "", {}, "sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ=="],
|
"smol-toml": ["smol-toml@1.5.2", "", {}, "sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ=="],
|
||||||
|
|
||||||
|
"socket.io-client": ["socket.io-client@4.8.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g=="],
|
||||||
|
|
||||||
|
"socket.io-parser": ["socket.io-parser@4.2.5", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ=="],
|
||||||
|
|
||||||
"solid-js": ["solid-js@1.9.10", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", "seroval-plugins": "~1.3.0" } }, "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew=="],
|
"solid-js": ["solid-js@1.9.10", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", "seroval-plugins": "~1.3.0" } }, "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew=="],
|
||||||
|
|
||||||
"solid-list": ["solid-list@0.3.0", "", { "dependencies": { "@corvu/utils": "~0.4.0" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-t4hx/F/l8Vmq+ib9HtZYl7Z9F1eKxq3eKJTXlvcm7P7yI4Z8O7QSOOEVHb/K6DD7M0RxzVRobK/BS5aSfLRwKg=="],
|
"solid-list": ["solid-list@0.3.0", "", { "dependencies": { "@corvu/utils": "~0.4.0" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-t4hx/F/l8Vmq+ib9HtZYl7Z9F1eKxq3eKJTXlvcm7P7yI4Z8O7QSOOEVHb/K6DD7M0RxzVRobK/BS5aSfLRwKg=="],
|
||||||
@@ -3522,6 +3561,8 @@
|
|||||||
|
|
||||||
"solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="],
|
"solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="],
|
||||||
|
|
||||||
|
"solid-stripe": ["solid-stripe@0.8.1", "", { "peerDependencies": { "@stripe/stripe-js": ">=1.44.1 <8.0.0", "solid-js": "^1.6.0" } }, "sha512-l2SkWoe51rsvk9u1ILBRWyCHODZebChSGMR6zHYJTivTRC0XWrRnNNKs5x1PYXsaIU71KYI6ov5CZB5cOtGLWw=="],
|
||||||
|
|
||||||
"solid-use": ["solid-use@0.9.1", "", { "peerDependencies": { "solid-js": "^1.7" } }, "sha512-UwvXDVPlrrbj/9ewG9ys5uL2IO4jSiwys2KPzK4zsnAcmEl7iDafZWW1Mo4BSEWOmQCGK6IvpmGHo1aou8iOFw=="],
|
"solid-use": ["solid-use@0.9.1", "", { "peerDependencies": { "solid-js": "^1.7" } }, "sha512-UwvXDVPlrrbj/9ewG9ys5uL2IO4jSiwys2KPzK4zsnAcmEl7iDafZWW1Mo4BSEWOmQCGK6IvpmGHo1aou8iOFw=="],
|
||||||
|
|
||||||
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
||||||
@@ -3676,6 +3717,8 @@
|
|||||||
|
|
||||||
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
||||||
|
|
||||||
|
"ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="],
|
||||||
|
|
||||||
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
|
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
|
||||||
|
|
||||||
"tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="],
|
"tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="],
|
||||||
@@ -3868,6 +3911,8 @@
|
|||||||
|
|
||||||
"xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
|
"xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
|
||||||
|
|
||||||
|
"xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="],
|
||||||
|
|
||||||
"xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="],
|
"xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="],
|
||||||
|
|
||||||
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||||
@@ -4018,6 +4063,8 @@
|
|||||||
|
|
||||||
"@expressive-code/plugin-shiki/shiki": ["shiki@3.15.0", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/engine-javascript": "3.15.0", "@shikijs/engine-oniguruma": "3.15.0", "@shikijs/langs": "3.15.0", "@shikijs/themes": "3.15.0", "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw=="],
|
"@expressive-code/plugin-shiki/shiki": ["shiki@3.15.0", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/engine-javascript": "3.15.0", "@shikijs/engine-oniguruma": "3.15.0", "@shikijs/langs": "3.15.0", "@shikijs/themes": "3.15.0", "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw=="],
|
||||||
|
|
||||||
|
"@gitlab/gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
||||||
"@hey-api/json-schema-ref-parser/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
"@hey-api/json-schema-ref-parser/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
||||||
|
|
||||||
"@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=="],
|
||||||
@@ -4260,6 +4307,8 @@
|
|||||||
|
|
||||||
"editorconfig/minimatch": ["minimatch@9.0.1", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w=="],
|
"editorconfig/minimatch": ["minimatch@9.0.1", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w=="],
|
||||||
|
|
||||||
|
"engine.io-client/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
||||||
|
|
||||||
"es-get-iterator/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
"es-get-iterator/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||||
|
|
||||||
"esbuild-plugin-copy/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
"esbuild-plugin-copy/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||||
|
|||||||
Generated
+3
-3
@@ -2,11 +2,11 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1768178648,
|
"lastModified": 1768302833,
|
||||||
"narHash": "sha256-kz/F6mhESPvU1diB7tOM3nLcBfQe7GU7GQCymRlTi/s=",
|
"narHash": "sha256-h5bRFy9bco+8QcK7rGoOiqMxMbmn21moTACofNLRMP4=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "3fbab70c6e69c87ea2b6e48aa6629da2aa6a23b0",
|
"rev": "61db79b0c6b838d9894923920b612048e1201926",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
+6
-5
@@ -81,12 +81,13 @@ This will walk you through installing the GitHub app, creating the workflow, and
|
|||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Run opencode
|
- name: Run opencode
|
||||||
uses: anomalyco/opencode/github@latest
|
uses: anomalyco/opencode/github@latest
|
||||||
env:
|
env:
|
||||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ const ZEN_MODELS = [
|
|||||||
]
|
]
|
||||||
const ZEN_BLACK = new sst.Secret("ZEN_BLACK")
|
const ZEN_BLACK = new sst.Secret("ZEN_BLACK")
|
||||||
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
|
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
|
||||||
|
const STRIPE_PUBLISHABLE_KEY = new sst.Secret("STRIPE_PUBLISHABLE_KEY")
|
||||||
const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
|
const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
|
||||||
properties: { value: auth.url.apply((url) => url!) },
|
properties: { value: auth.url.apply((url) => url!) },
|
||||||
})
|
})
|
||||||
@@ -163,6 +164,7 @@ new sst.cloudflare.x.SolidStart("Console", {
|
|||||||
AWS_SES_ACCESS_KEY_ID,
|
AWS_SES_ACCESS_KEY_ID,
|
||||||
AWS_SES_SECRET_ACCESS_KEY,
|
AWS_SES_SECRET_ACCESS_KEY,
|
||||||
ZEN_BLACK,
|
ZEN_BLACK,
|
||||||
|
new sst.Secret("ZEN_SESSION_SECRET"),
|
||||||
...ZEN_MODELS,
|
...ZEN_MODELS,
|
||||||
...($dev
|
...($dev
|
||||||
? [
|
? [
|
||||||
@@ -176,6 +178,7 @@ new sst.cloudflare.x.SolidStart("Console", {
|
|||||||
//VITE_DOCS_URL: web.url.apply((url) => url!),
|
//VITE_DOCS_URL: web.url.apply((url) => url!),
|
||||||
//VITE_API_URL: gateway.url.apply((url) => url!),
|
//VITE_API_URL: gateway.url.apply((url) => url!),
|
||||||
VITE_AUTH_URL: auth.url.apply((url) => url!),
|
VITE_AUTH_URL: auth.url.apply((url) => url!),
|
||||||
|
VITE_STRIPE_PUBLISHABLE_KEY: STRIPE_PUBLISHABLE_KEY.value,
|
||||||
},
|
},
|
||||||
transform: {
|
transform: {
|
||||||
server: {
|
server: {
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-8nur5CuUCSV/SzD16hNXVoIlKsiPBXDzCnoITK0IhC4=",
|
"x86_64-linux": "sha256-wENwhwRVfgoVyA9YNGcG+fAfu46JxK4xvNgiPbRt//s=",
|
||||||
"aarch64-darwin": "sha256-vD1g9dviI2nMBTTPwI87sK01hSZ+cdnmb1V72AdJYq4="
|
"aarch64-darwin": "sha256-vm1DYl1erlbaqz5NHHlnZEMuFmidr/UkS84nIqLJ96Q="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ HASH_FILE=${HASH_FILE:-$DEFAULT_HASH_FILE}
|
|||||||
if [ ! -f "$HASH_FILE" ]; then
|
if [ ! -f "$HASH_FILE" ]; then
|
||||||
cat >"$HASH_FILE" <<EOF
|
cat >"$HASH_FILE" <<EOF
|
||||||
{
|
{
|
||||||
"nodeModules": "$DUMMY"
|
"nodeModules": {}
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
fi
|
fi
|
||||||
@@ -111,7 +111,7 @@ fi
|
|||||||
|
|
||||||
write_node_modules_hash "$CORRECT_HASH"
|
write_node_modules_hash "$CORRECT_HASH"
|
||||||
|
|
||||||
jq -e --arg hash "$CORRECT_HASH" '.nodeModules == $hash' "$HASH_FILE" >/dev/null
|
jq -e --arg system "$SYSTEM" --arg hash "$CORRECT_HASH" '.nodeModules[$system] == $hash' "$HASH_FILE" >/dev/null
|
||||||
|
|
||||||
echo "node_modules hash updated for ${SYSTEM}: $CORRECT_HASH"
|
echo "node_modules hash updated for ${SYSTEM}: $CORRECT_HASH"
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"@solid-primitives/storage": "4.3.3",
|
"@solid-primitives/storage": "4.3.3",
|
||||||
"@tailwindcss/vite": "4.1.11",
|
"@tailwindcss/vite": "4.1.11",
|
||||||
"diff": "8.0.2",
|
"diff": "8.0.2",
|
||||||
|
"dompurify": "3.3.1",
|
||||||
"ai": "5.0.119",
|
"ai": "5.0.119",
|
||||||
"hono": "4.10.7",
|
"hono": "4.10.7",
|
||||||
"hono-openapi": "1.1.2",
|
"hono-openapi": "1.1.2",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.1.15",
|
"version": "1.1.20",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const Loading = () => <div class="size-full flex items-center justify-center tex
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
__OPENCODE__?: { updaterEnabled?: boolean }
|
__OPENCODE__?: { updaterEnabled?: boolean; serverPassword?: string }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Dialog } from "@opencode-ai/ui/dialog"
|
|||||||
import { List } from "@opencode-ai/ui/list"
|
import { List } from "@opencode-ai/ui/list"
|
||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { normalizeServerUrl, serverDisplayName, useServer } from "@/context/server"
|
import { normalizeServerUrl, serverDisplayName, useServer } from "@/context/server"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||||
@@ -116,6 +117,10 @@ export function DialogSelectServer() {
|
|||||||
select(value, true)
|
select(value, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleRemove(url: string) {
|
||||||
|
server.remove(url)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog title="Servers" description="Switch which OpenCode server this app connects to.">
|
<Dialog title="Servers" description="Switch which OpenCode server this app connects to.">
|
||||||
<div class="flex flex-col gap-4 pb-4">
|
<div class="flex flex-col gap-4 pb-4">
|
||||||
@@ -130,20 +135,33 @@ export function DialogSelectServer() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(i) => (
|
{(i) => (
|
||||||
<div
|
<div class="flex items-center gap-2 min-w-0 flex-1 group/item">
|
||||||
class="flex items-center gap-2 min-w-0 flex-1"
|
|
||||||
classList={{ "opacity-50": store.status[i]?.healthy === false }}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
classList={{
|
class="flex items-center gap-2 min-w-0 flex-1"
|
||||||
"size-1.5 rounded-full shrink-0": true,
|
classList={{ "opacity-50": store.status[i]?.healthy === false }}
|
||||||
"bg-icon-success-base": store.status[i]?.healthy === true,
|
>
|
||||||
"bg-icon-critical-base": store.status[i]?.healthy === false,
|
<div
|
||||||
"bg-border-weak-base": store.status[i] === undefined,
|
classList={{
|
||||||
}}
|
"size-1.5 rounded-full shrink-0": true,
|
||||||
/>
|
"bg-icon-success-base": store.status[i]?.healthy === true,
|
||||||
<span class="truncate">{serverDisplayName(i)}</span>
|
"bg-icon-critical-base": store.status[i]?.healthy === false,
|
||||||
<span class="text-text-weak">{store.status[i]?.version}</span>
|
"bg-border-weak-base": store.status[i] === undefined,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span class="truncate">{serverDisplayName(i)}</span>
|
||||||
|
<span class="text-text-weak">{store.status[i]?.version}</span>
|
||||||
|
</div>
|
||||||
|
<Show when={current() !== i && server.list.includes(i)}>
|
||||||
|
<IconButton
|
||||||
|
icon="circle-x"
|
||||||
|
variant="ghost"
|
||||||
|
class="bg-transparent transition-opacity shrink-0 hover:scale-110"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleRemove(i)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</List>
|
</List>
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ import { useSync } from "@/context/sync"
|
|||||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
|
import type { IconName } from "@opencode-ai/ui/icons/provider"
|
||||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { Select } from "@opencode-ai/ui/select"
|
import { Select } from "@opencode-ai/ui/select"
|
||||||
@@ -362,6 +364,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
if (!isFocused()) setStore("popover", null)
|
if (!isFocused()) setStore("popover", null)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Safety: reset composing state on focus change to prevent stuck state
|
||||||
|
// This handles edge cases where compositionend event may not fire
|
||||||
|
createEffect(() => {
|
||||||
|
if (!isFocused()) setComposing(false)
|
||||||
|
})
|
||||||
|
|
||||||
type AtOption = { type: "agent"; name: string; display: string } | { type: "file"; path: string; display: string }
|
type AtOption = { type: "agent"; name: string; display: string } | { type: "file"; path: string; display: string }
|
||||||
|
|
||||||
const agentList = createMemo(() =>
|
const agentList = createMemo(() =>
|
||||||
@@ -879,6 +887,14 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle Shift+Enter BEFORE IME check - Shift+Enter is never used for IME input
|
||||||
|
// and should always insert a newline regardless of composition state
|
||||||
|
if (event.key === "Enter" && event.shiftKey) {
|
||||||
|
addPart({ type: "text", content: "\n", start: 0, end: 0 })
|
||||||
|
event.preventDefault()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (event.key === "Enter" && isImeComposing(event)) {
|
if (event.key === "Enter" && isImeComposing(event)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -942,11 +958,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === "Enter" && event.shiftKey) {
|
// Note: Shift+Enter is handled earlier, before IME check
|
||||||
addPart({ type: "text", content: "\n", start: 0, end: 0 })
|
|
||||||
event.preventDefault()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (event.key === "Enter" && !event.shiftKey) {
|
if (event.key === "Enter" && !event.shiftKey) {
|
||||||
handleSubmit(event)
|
handleSubmit(event)
|
||||||
}
|
}
|
||||||
@@ -1560,6 +1572,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
fallback={
|
fallback={
|
||||||
<TooltipKeybind placement="top" title="Choose model" keybind={command.keybind("model.choose")}>
|
<TooltipKeybind placement="top" title="Choose model" keybind={command.keybind("model.choose")}>
|
||||||
<Button as="div" variant="ghost" onClick={() => dialog.show(() => <DialogSelectModelUnpaid />)}>
|
<Button as="div" variant="ghost" onClick={() => dialog.show(() => <DialogSelectModelUnpaid />)}>
|
||||||
|
<Show when={local.model.current()?.provider?.id}>
|
||||||
|
<ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
|
||||||
|
</Show>
|
||||||
{local.model.current()?.name ?? "Select model"}
|
{local.model.current()?.name ?? "Select model"}
|
||||||
<Icon name="chevron-down" size="small" />
|
<Icon name="chevron-down" size="small" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1569,6 +1584,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
<ModelSelectorPopover>
|
<ModelSelectorPopover>
|
||||||
<TooltipKeybind placement="top" title="Choose model" keybind={command.keybind("model.choose")}>
|
<TooltipKeybind placement="top" title="Choose model" keybind={command.keybind("model.choose")}>
|
||||||
<Button as="div" variant="ghost">
|
<Button as="div" variant="ghost">
|
||||||
|
<Show when={local.model.current()?.provider?.id}>
|
||||||
|
<ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
|
||||||
|
</Show>
|
||||||
{local.model.current()?.name ?? "Select model"}
|
{local.model.current()?.name ?? "Select model"}
|
||||||
<Icon name="chevron-down" size="small" />
|
<Icon name="chevron-down" size="small" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1583,10 +1601,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="text-text-base _hidden group-hover/prompt-input:inline-block"
|
class="text-text-base _hidden group-hover/prompt-input:inline-block capitalize text-12-regular"
|
||||||
onClick={() => local.model.variant.cycle()}
|
onClick={() => local.model.variant.cycle()}
|
||||||
>
|
>
|
||||||
<span class="capitalize text-12-regular">{local.model.variant.current() ?? "Default"}</span>
|
{local.model.variant.current() ?? "Default"}
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -100,9 +100,12 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
const mod = await import("ghostty-web")
|
const mod = await import("ghostty-web")
|
||||||
ghostty = await mod.Ghostty.load()
|
ghostty = await mod.Ghostty.load()
|
||||||
|
|
||||||
const socket = new WebSocket(
|
const url = new URL(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`)
|
||||||
sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`,
|
if (window.__OPENCODE__?.serverPassword) {
|
||||||
)
|
url.username = "opencode"
|
||||||
|
url.password = window.__OPENCODE__?.serverPassword
|
||||||
|
}
|
||||||
|
const socket = new WebSocket(url)
|
||||||
ws = socket
|
ws = socket
|
||||||
|
|
||||||
const t = new mod.Terminal({
|
const t = new mod.Terminal({
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
|||||||
name: "GlobalSDK",
|
name: "GlobalSDK",
|
||||||
init: () => {
|
init: () => {
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
|
const platform = usePlatform()
|
||||||
const abort = new AbortController()
|
const abort = new AbortController()
|
||||||
|
|
||||||
const eventSdk = createOpencodeClient({
|
const eventSdk = createOpencodeClient({
|
||||||
baseUrl: server.url,
|
baseUrl: server.url,
|
||||||
signal: abort.signal,
|
signal: abort.signal,
|
||||||
|
fetch: platform.fetch,
|
||||||
})
|
})
|
||||||
const emitter = createGlobalEmitter<{
|
const emitter = createGlobalEmitter<{
|
||||||
[key: string]: Event
|
[key: string]: Event
|
||||||
@@ -93,7 +95,6 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
|||||||
stop()
|
stop()
|
||||||
})
|
})
|
||||||
|
|
||||||
const platform = usePlatform()
|
|
||||||
const sdk = createOpencodeClient({
|
const sdk = createOpencodeClient({
|
||||||
baseUrl: server.url,
|
baseUrl: server.url,
|
||||||
fetch: platform.fetch,
|
fetch: platform.fetch,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
type LspStatus,
|
type LspStatus,
|
||||||
type VcsInfo,
|
type VcsInfo,
|
||||||
type PermissionRequest,
|
type PermissionRequest,
|
||||||
|
type QuestionRequest,
|
||||||
createOpencodeClient,
|
createOpencodeClient,
|
||||||
} from "@opencode-ai/sdk/v2/client"
|
} from "@opencode-ai/sdk/v2/client"
|
||||||
import { createStore, produce, reconcile } from "solid-js/store"
|
import { createStore, produce, reconcile } from "solid-js/store"
|
||||||
@@ -26,6 +27,7 @@ import { ErrorPage, type InitError } from "../pages/error"
|
|||||||
import { batch, createContext, useContext, onCleanup, onMount, type ParentProps, Switch, Match } from "solid-js"
|
import { batch, createContext, useContext, onCleanup, onMount, type ParentProps, Switch, Match } from "solid-js"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { getFilename } from "@opencode-ai/util/path"
|
import { getFilename } from "@opencode-ai/util/path"
|
||||||
|
import { usePlatform } from "./platform"
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
status: "loading" | "partial" | "complete"
|
status: "loading" | "partial" | "complete"
|
||||||
@@ -36,6 +38,7 @@ type State = {
|
|||||||
config: Config
|
config: Config
|
||||||
path: Path
|
path: Path
|
||||||
session: Session[]
|
session: Session[]
|
||||||
|
sessionTotal: number
|
||||||
session_status: {
|
session_status: {
|
||||||
[sessionID: string]: SessionStatus
|
[sessionID: string]: SessionStatus
|
||||||
}
|
}
|
||||||
@@ -48,6 +51,9 @@ type State = {
|
|||||||
permission: {
|
permission: {
|
||||||
[sessionID: string]: PermissionRequest[]
|
[sessionID: string]: PermissionRequest[]
|
||||||
}
|
}
|
||||||
|
question: {
|
||||||
|
[sessionID: string]: QuestionRequest[]
|
||||||
|
}
|
||||||
mcp: {
|
mcp: {
|
||||||
[name: string]: McpStatus
|
[name: string]: McpStatus
|
||||||
}
|
}
|
||||||
@@ -64,6 +70,7 @@ type State = {
|
|||||||
|
|
||||||
function createGlobalSync() {
|
function createGlobalSync() {
|
||||||
const globalSDK = useGlobalSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
|
const platform = usePlatform()
|
||||||
const [globalStore, setGlobalStore] = createStore<{
|
const [globalStore, setGlobalStore] = createStore<{
|
||||||
ready: boolean
|
ready: boolean
|
||||||
error?: InitError
|
error?: InitError
|
||||||
@@ -92,10 +99,12 @@ function createGlobalSync() {
|
|||||||
agent: [],
|
agent: [],
|
||||||
command: [],
|
command: [],
|
||||||
session: [],
|
session: [],
|
||||||
|
sessionTotal: 0,
|
||||||
session_status: {},
|
session_status: {},
|
||||||
session_diff: {},
|
session_diff: {},
|
||||||
todo: {},
|
todo: {},
|
||||||
permission: {},
|
permission: {},
|
||||||
|
question: {},
|
||||||
mcp: {},
|
mcp: {},
|
||||||
lsp: [],
|
lsp: [],
|
||||||
vcs: undefined,
|
vcs: undefined,
|
||||||
@@ -110,8 +119,10 @@ function createGlobalSync() {
|
|||||||
|
|
||||||
async function loadSessions(directory: string) {
|
async function loadSessions(directory: string) {
|
||||||
const [store, setStore] = child(directory)
|
const [store, setStore] = child(directory)
|
||||||
globalSDK.client.session
|
const limit = store.limit
|
||||||
.list({ directory })
|
|
||||||
|
return globalSDK.client.session
|
||||||
|
.list({ directory, roots: true })
|
||||||
.then((x) => {
|
.then((x) => {
|
||||||
const fourHoursAgo = Date.now() - 4 * 60 * 60 * 1000
|
const fourHoursAgo = Date.now() - 4 * 60 * 60 * 1000
|
||||||
const nonArchived = (x.data ?? [])
|
const nonArchived = (x.data ?? [])
|
||||||
@@ -121,10 +132,12 @@ function createGlobalSync() {
|
|||||||
.sort((a, b) => a.id.localeCompare(b.id))
|
.sort((a, b) => a.id.localeCompare(b.id))
|
||||||
// Include up to the limit, plus any updated in the last 4 hours
|
// Include up to the limit, plus any updated in the last 4 hours
|
||||||
const sessions = nonArchived.filter((s, i) => {
|
const sessions = nonArchived.filter((s, i) => {
|
||||||
if (i < store.limit) return true
|
if (i < limit) return true
|
||||||
const updated = new Date(s.time?.updated ?? s.time?.created).getTime()
|
const updated = new Date(s.time?.updated ?? s.time?.created).getTime()
|
||||||
return updated > fourHoursAgo
|
return updated > fourHoursAgo
|
||||||
})
|
})
|
||||||
|
// Store total session count (used for "load more" pagination)
|
||||||
|
setStore("sessionTotal", nonArchived.length)
|
||||||
setStore("session", reconcile(sessions, { key: "id" }))
|
setStore("session", reconcile(sessions, { key: "id" }))
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@@ -139,6 +152,7 @@ function createGlobalSync() {
|
|||||||
const [store, setStore] = child(directory)
|
const [store, setStore] = child(directory)
|
||||||
const sdk = createOpencodeClient({
|
const sdk = createOpencodeClient({
|
||||||
baseUrl: globalSDK.url,
|
baseUrl: globalSDK.url,
|
||||||
|
fetch: platform.fetch,
|
||||||
directory,
|
directory,
|
||||||
throwOnError: true,
|
throwOnError: true,
|
||||||
})
|
})
|
||||||
@@ -205,6 +219,38 @@ function createGlobalSync() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
|
sdk.question.list().then((x) => {
|
||||||
|
const grouped: Record<string, QuestionRequest[]> = {}
|
||||||
|
for (const question of x.data ?? []) {
|
||||||
|
if (!question?.id || !question.sessionID) continue
|
||||||
|
const existing = grouped[question.sessionID]
|
||||||
|
if (existing) {
|
||||||
|
existing.push(question)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
grouped[question.sessionID] = [question]
|
||||||
|
}
|
||||||
|
|
||||||
|
batch(() => {
|
||||||
|
for (const sessionID of Object.keys(store.question)) {
|
||||||
|
if (grouped[sessionID]) continue
|
||||||
|
setStore("question", sessionID, [])
|
||||||
|
}
|
||||||
|
for (const [sessionID, questions] of Object.entries(grouped)) {
|
||||||
|
setStore(
|
||||||
|
"question",
|
||||||
|
sessionID,
|
||||||
|
reconcile(
|
||||||
|
questions
|
||||||
|
.filter((q) => !!q?.id)
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => a.id.localeCompare(b.id)),
|
||||||
|
{ key: "id" },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}),
|
||||||
]).then(() => {
|
]).then(() => {
|
||||||
setStore("status", "complete")
|
setStore("status", "complete")
|
||||||
})
|
})
|
||||||
@@ -393,9 +439,48 @@ function createGlobalSync() {
|
|||||||
)
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case "question.asked": {
|
||||||
|
const sessionID = event.properties.sessionID
|
||||||
|
const questions = store.question[sessionID]
|
||||||
|
if (!questions) {
|
||||||
|
setStore("question", sessionID, [event.properties])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = Binary.search(questions, event.properties.id, (q) => q.id)
|
||||||
|
if (result.found) {
|
||||||
|
setStore("question", sessionID, result.index, reconcile(event.properties))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
setStore(
|
||||||
|
"question",
|
||||||
|
sessionID,
|
||||||
|
produce((draft) => {
|
||||||
|
draft.splice(result.index, 0, event.properties)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "question.replied":
|
||||||
|
case "question.rejected": {
|
||||||
|
const questions = store.question[event.properties.sessionID]
|
||||||
|
if (!questions) break
|
||||||
|
const result = Binary.search(questions, event.properties.requestID, (q) => q.id)
|
||||||
|
if (!result.found) break
|
||||||
|
setStore(
|
||||||
|
"question",
|
||||||
|
event.properties.sessionID,
|
||||||
|
produce((draft) => {
|
||||||
|
draft.splice(result.index, 1)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
case "lsp.updated": {
|
case "lsp.updated": {
|
||||||
const sdk = createOpencodeClient({
|
const sdk = createOpencodeClient({
|
||||||
baseUrl: globalSDK.url,
|
baseUrl: globalSDK.url,
|
||||||
|
fetch: platform.fetch,
|
||||||
directory,
|
directory,
|
||||||
throwOnError: true,
|
throwOnError: true,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Persist, persisted } from "@/utils/persist"
|
|||||||
export type LocalPTY = {
|
export type LocalPTY = {
|
||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
|
titleNumber: number
|
||||||
rows?: number
|
rows?: number
|
||||||
cols?: number
|
cols?: number
|
||||||
buffer?: string
|
buffer?: string
|
||||||
@@ -42,8 +43,20 @@ function createTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, id:
|
|||||||
all: createMemo(() => Object.values(store.all)),
|
all: createMemo(() => Object.values(store.all)),
|
||||||
active: createMemo(() => store.active),
|
active: createMemo(() => store.active),
|
||||||
new() {
|
new() {
|
||||||
|
const existingTitleNumbers = new Set(
|
||||||
|
store.all.map((pty) => {
|
||||||
|
const match = pty.titleNumber
|
||||||
|
return match
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
let nextNumber = 1
|
||||||
|
while (existingTitleNumbers.has(nextNumber)) {
|
||||||
|
nextNumber++
|
||||||
|
}
|
||||||
|
|
||||||
sdk.client.pty
|
sdk.client.pty
|
||||||
.create({ title: `Terminal ${store.all.length + 1}` })
|
.create({ title: `Terminal ${nextNumber}` })
|
||||||
.then((pty) => {
|
.then((pty) => {
|
||||||
const id = pty.data?.id
|
const id = pty.data?.id
|
||||||
if (!id) return
|
if (!id) return
|
||||||
@@ -52,6 +65,7 @@ function createTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, id:
|
|||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
title: pty.data?.title ?? "Terminal",
|
title: pty.data?.title ?? "Terminal",
|
||||||
|
titleNumber: nextNumber,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
setStore("active", id)
|
setStore("active", id)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { LocalProvider } from "@/context/local"
|
|||||||
import { base64Decode } from "@opencode-ai/util/encode"
|
import { base64Decode } from "@opencode-ai/util/encode"
|
||||||
import { DataProvider } from "@opencode-ai/ui/context"
|
import { DataProvider } from "@opencode-ai/ui/context"
|
||||||
import { iife } from "@opencode-ai/util/iife"
|
import { iife } from "@opencode-ai/util/iife"
|
||||||
|
import type { QuestionAnswer } from "@opencode-ai/sdk/v2"
|
||||||
|
|
||||||
export default function Layout(props: ParentProps) {
|
export default function Layout(props: ParentProps) {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
@@ -27,6 +28,11 @@ export default function Layout(props: ParentProps) {
|
|||||||
response: "once" | "always" | "reject"
|
response: "once" | "always" | "reject"
|
||||||
}) => sdk.client.permission.respond(input)
|
}) => sdk.client.permission.respond(input)
|
||||||
|
|
||||||
|
const replyToQuestion = (input: { requestID: string; answers: QuestionAnswer[] }) =>
|
||||||
|
sdk.client.question.reply(input)
|
||||||
|
|
||||||
|
const rejectQuestion = (input: { requestID: string }) => sdk.client.question.reject(input)
|
||||||
|
|
||||||
const navigateToSession = (sessionID: string) => {
|
const navigateToSession = (sessionID: string) => {
|
||||||
navigate(`/${params.dir}/session/${sessionID}`)
|
navigate(`/${params.dir}/session/${sessionID}`)
|
||||||
}
|
}
|
||||||
@@ -36,6 +42,8 @@ export default function Layout(props: ParentProps) {
|
|||||||
data={sync.data}
|
data={sync.data}
|
||||||
directory={directory()}
|
directory={directory()}
|
||||||
onPermissionRespond={respond}
|
onPermissionRespond={respond}
|
||||||
|
onQuestionReply={replyToQuestion}
|
||||||
|
onQuestionReject={rejectQuestion}
|
||||||
onNavigateToSession={navigateToSession}
|
onNavigateToSession={navigateToSession}
|
||||||
>
|
>
|
||||||
<LocalProvider>{props.children}</LocalProvider>
|
<LocalProvider>{props.children}</LocalProvider>
|
||||||
|
|||||||
@@ -944,7 +944,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
.toSorted(sortSessions),
|
.toSorted(sortSessions),
|
||||||
)
|
)
|
||||||
const rootSessions = createMemo(() => sessions().filter((s) => !s.parentID))
|
const rootSessions = createMemo(() => sessions().filter((s) => !s.parentID))
|
||||||
const hasMoreSessions = createMemo(() => store.session.length >= store.limit)
|
const hasMoreSessions = createMemo(() => store.sessionTotal > store.session.length)
|
||||||
const loadMoreSessions = async () => {
|
const loadMoreSessions = async () => {
|
||||||
setProjectStore("limit", (limit) => limit + 5)
|
setProjectStore("limit", (limit) => limit + 5)
|
||||||
await globalSync.project.loadSessions(props.project.worktree)
|
await globalSync.project.loadSessions(props.project.worktree)
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.1.15",
|
"version": "1.1.20",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsgo --noEmit",
|
"typecheck": "tsgo --noEmit",
|
||||||
"dev": "vite dev --host 0.0.0.0",
|
"dev": "vite dev --host 0.0.0.0",
|
||||||
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
|
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai VITE_STRIPE_PUBLISHABLE_KEY=pk_test_51RtuLNE7fOCwHSD4mewwzFejyytjdGoSDK7CAvhbffwaZnPbNb2rwJICw6LTOXCmWO320fSNXvb5NzI08RZVkAxd00syfqrW7t bun sst shell --stage=dev bun dev",
|
||||||
"build": "./script/generate-sitemap.ts && vite build && ../../opencode/script/schema.ts ./.output/public/config.json",
|
"build": "./script/generate-sitemap.ts && vite build && ../../opencode/script/schema.ts ./.output/public/config.json",
|
||||||
"start": "vite start"
|
"start": "vite start"
|
||||||
},
|
},
|
||||||
@@ -23,10 +23,12 @@
|
|||||||
"@solidjs/meta": "catalog:",
|
"@solidjs/meta": "catalog:",
|
||||||
"@solidjs/router": "catalog:",
|
"@solidjs/router": "catalog:",
|
||||||
"@solidjs/start": "catalog:",
|
"@solidjs/start": "catalog:",
|
||||||
|
"@stripe/stripe-js": "8.6.1",
|
||||||
"chart.js": "4.5.1",
|
"chart.js": "4.5.1",
|
||||||
"nitro": "3.0.1-alpha.1",
|
"nitro": "3.0.1-alpha.1",
|
||||||
"solid-js": "catalog:",
|
"solid-js": "catalog:",
|
||||||
"solid-list": "0.3.0",
|
"solid-list": "0.3.0",
|
||||||
|
"solid-stripe": "0.8.1",
|
||||||
"vite": "catalog:",
|
"vite": "catalog:",
|
||||||
"zod": "catalog:"
|
"zod": "catalog:"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import { useSession } from "@solidjs/start/http"
|
|
||||||
|
|
||||||
export interface AuthSession {
|
|
||||||
account?: Record<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
id: string
|
|
||||||
email: string
|
|
||||||
}
|
|
||||||
>
|
|
||||||
current?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useAuthSession() {
|
|
||||||
return useSession<AuthSession>({
|
|
||||||
password: "0".repeat(32),
|
|
||||||
name: "auth",
|
|
||||||
maxAge: 60 * 60 * 24 * 365,
|
|
||||||
cookie: {
|
|
||||||
secure: false,
|
|
||||||
httpOnly: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,13 +5,38 @@ import { redirect } from "@solidjs/router"
|
|||||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||||
|
|
||||||
import { createClient } from "@openauthjs/openauth/client"
|
import { createClient } from "@openauthjs/openauth/client"
|
||||||
import { useAuthSession } from "./auth.session"
|
|
||||||
|
|
||||||
export const AuthClient = createClient({
|
export const AuthClient = createClient({
|
||||||
clientID: "app",
|
clientID: "app",
|
||||||
issuer: import.meta.env.VITE_AUTH_URL,
|
issuer: import.meta.env.VITE_AUTH_URL,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
import { useSession } from "@solidjs/start/http"
|
||||||
|
import { Resource } from "@opencode-ai/console-resource"
|
||||||
|
|
||||||
|
export interface AuthSession {
|
||||||
|
account?: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
id: string
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
>
|
||||||
|
current?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAuthSession() {
|
||||||
|
return useSession<AuthSession>({
|
||||||
|
password: Resource.ZEN_SESSION_SECRET.value,
|
||||||
|
name: "auth",
|
||||||
|
maxAge: 60 * 60 * 24 * 365,
|
||||||
|
cookie: {
|
||||||
|
secure: false,
|
||||||
|
httpOnly: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const getActor = async (workspace?: string): Promise<Actor.Info> => {
|
export const getActor = async (workspace?: string): Promise<Actor.Info> => {
|
||||||
"use server"
|
"use server"
|
||||||
const evt = getRequestEvent()
|
const evt = getRequestEvent()
|
||||||
|
|||||||
+3
-2
@@ -1,10 +1,11 @@
|
|||||||
import { redirect } from "@solidjs/router"
|
import { redirect } from "@solidjs/router"
|
||||||
import type { APIEvent } from "@solidjs/start/server"
|
import type { APIEvent } from "@solidjs/start/server"
|
||||||
import { AuthClient } from "~/context/auth"
|
import { AuthClient } from "~/context/auth"
|
||||||
import { useAuthSession } from "~/context/auth.session"
|
import { useAuthSession } from "~/context/auth"
|
||||||
|
|
||||||
export async function GET(input: APIEvent) {
|
export async function GET(input: APIEvent) {
|
||||||
const url = new URL(input.request.url)
|
const url = new URL(input.request.url)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const code = url.searchParams.get("code")
|
const code = url.searchParams.get("code")
|
||||||
if (!code) throw new Error("No code found")
|
if (!code) throw new Error("No code found")
|
||||||
@@ -27,7 +28,7 @@ export async function GET(input: APIEvent) {
|
|||||||
current: id,
|
current: id,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return redirect("/auth")
|
return redirect(url.pathname === "/auth/callback" ? "/auth" : url.pathname.replace("/auth/callback", ""))
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
@@ -2,6 +2,9 @@ import type { APIEvent } from "@solidjs/start/server"
|
|||||||
import { AuthClient } from "~/context/auth"
|
import { AuthClient } from "~/context/auth"
|
||||||
|
|
||||||
export async function GET(input: APIEvent) {
|
export async function GET(input: APIEvent) {
|
||||||
const result = await AuthClient.authorize(new URL("./callback", input.request.url).toString(), "code")
|
const url = new URL(input.request.url)
|
||||||
|
const cont = url.searchParams.get("continue") ?? ""
|
||||||
|
const callbackUrl = new URL(`./callback${cont}`, input.request.url)
|
||||||
|
const result = await AuthClient.authorize(callbackUrl.toString(), "code")
|
||||||
return Response.redirect(result.url, 302)
|
return Response.redirect(result.url, 302)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { redirect } from "@solidjs/router"
|
import { redirect } from "@solidjs/router"
|
||||||
import { APIEvent } from "@solidjs/start"
|
import { APIEvent } from "@solidjs/start"
|
||||||
import { useAuthSession } from "~/context/auth.session"
|
import { useAuthSession } from "~/context/auth"
|
||||||
|
|
||||||
export async function GET(event: APIEvent) {
|
export async function GET(event: APIEvent) {
|
||||||
const auth = await useAuthSession()
|
const auth = await useAuthSession()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { APIEvent } from "@solidjs/start"
|
import { APIEvent } from "@solidjs/start"
|
||||||
import { useAuthSession } from "~/context/auth.session"
|
import { useAuthSession } from "~/context/auth"
|
||||||
|
|
||||||
export async function GET(input: APIEvent) {
|
export async function GET(input: APIEvent) {
|
||||||
const session = await useAuthSession()
|
const session = await useAuthSession()
|
||||||
|
|||||||
@@ -0,0 +1,742 @@
|
|||||||
|
[data-page="black"] {
|
||||||
|
background: #000;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: stretch;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
[data-component="header-gradient"] {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 288px;
|
||||||
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.16) 0%, rgba(0, 0, 0, 0) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="header"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 40px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
/* [data-component="header-logo"] { */
|
||||||
|
/* } */
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="content"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
[data-slot="hero"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 40px;
|
||||||
|
padding: 0 20px;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
margin-top: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: rgba(255, 255, 255, 0.92);
|
||||||
|
font-size: 18px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 160%;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: rgba(255, 255, 255, 0.59);
|
||||||
|
font-size: 15px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 160%;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="hero-black"] {
|
||||||
|
margin-top: 40px;
|
||||||
|
padding: 0 20px;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
margin-top: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 540px;
|
||||||
|
height: auto;
|
||||||
|
filter: drop-shadow(0 0 20px rgba(255, 255, 255, 0.1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="cta"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: -40px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
margin-top: -20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="heading"] {
|
||||||
|
color: rgba(255, 255, 255, 0.92);
|
||||||
|
text-align: center;
|
||||||
|
font-size: 18px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 160%; /* 28.8px */
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[data-slot="subheading"] {
|
||||||
|
color: rgba(255, 255, 255, 0.59);
|
||||||
|
font-size: 15px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 160%;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 160%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[data-slot="button"] {
|
||||||
|
display: inline-flex;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 12px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
text-decoration: none;
|
||||||
|
color: #000;
|
||||||
|
font-family: "JetBrains Mono Nerd Font";
|
||||||
|
font-size: 16px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: normal;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[data-slot="back-soon"] {
|
||||||
|
color: rgba(255, 255, 255, 0.59);
|
||||||
|
text-align: center;
|
||||||
|
font-size: 13px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 160%; /* 20.8px */
|
||||||
|
}
|
||||||
|
[data-slot="follow-us"] {
|
||||||
|
display: inline-flex;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 12px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.17);
|
||||||
|
color: rgba(255, 255, 255, 0.59);
|
||||||
|
font-family: "JetBrains Mono Nerd Font";
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: normal;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="pricing"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 540px;
|
||||||
|
padding: 0 20px;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="pricing-card"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.17);
|
||||||
|
border-radius: 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: border-color 0.15s ease;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: rgba(255, 255, 255, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="icon"] {
|
||||||
|
color: rgba(255, 255, 255, 0.59);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="price"] {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="amount"] {
|
||||||
|
color: rgba(255, 255, 255, 0.92);
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="period"] {
|
||||||
|
color: rgba(255, 255, 255, 0.59);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="multiplier"] {
|
||||||
|
color: rgba(255, 255, 255, 0.39);
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "·";
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="selected-plan"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 32px;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: calc(100% - 40px);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="selected-card"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.17);
|
||||||
|
border-radius: 4px;
|
||||||
|
width: fit-content;
|
||||||
|
|
||||||
|
[data-slot="icon"] {
|
||||||
|
color: rgba(255, 255, 255, 0.59);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="price"] {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="amount"] {
|
||||||
|
color: rgba(255, 255, 255, 0.92);
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="period"] {
|
||||||
|
color: rgba(255, 255, 255, 0.59);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="multiplier"] {
|
||||||
|
color: rgba(255, 255, 255, 0.39);
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "·";
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="terms"] {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
li {
|
||||||
|
color: rgba(255, 255, 255, 0.59);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
padding-left: 16px;
|
||||||
|
position: relative;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "▪";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
color: rgba(255, 255, 255, 0.39);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="actions"] {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
|
button,
|
||||||
|
a {
|
||||||
|
flex: 1;
|
||||||
|
display: inline-flex;
|
||||||
|
height: 48px;
|
||||||
|
padding: 0 16px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 400;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="cancel"] {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.17);
|
||||||
|
color: rgba(255, 255, 255, 0.92);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: rgba(255, 255, 255, 0.35);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="continue"] {
|
||||||
|
background: rgba(255, 255, 255, 0.17);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.17);
|
||||||
|
color: rgba(255, 255, 255, 0.59);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="fine-print"] {
|
||||||
|
color: rgba(255, 255, 255, 0.39);
|
||||||
|
text-align: center;
|
||||||
|
font-size: 13px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 160%; /* 20.8px */
|
||||||
|
font-style: italic;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: rgba(255, 255, 255, 0.39);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subscribe page styles */
|
||||||
|
[data-slot="subscribe-form"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 32px;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: -18px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 540px;
|
||||||
|
padding: 0 20px;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
margin-top: 40px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="form-card"] {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.17);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="plan-header"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="title"] {
|
||||||
|
color: rgba(255, 255, 255, 0.92);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 400;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="icon"] {
|
||||||
|
color: rgba(255, 255, 255, 0.59);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="price"] {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="amount"] {
|
||||||
|
color: rgba(255, 255, 255, 0.92);
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="period"] {
|
||||||
|
color: rgba(255, 255, 255, 0.59);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="multiplier"] {
|
||||||
|
color: rgba(255, 255, 255, 0.39);
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "·";
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="divider"] {
|
||||||
|
height: 1px;
|
||||||
|
background: rgba(255, 255, 255, 0.17);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="section-title"] {
|
||||||
|
color: rgba(255, 255, 255, 0.92);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="tax-id-section"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
[data-slot="label"] {
|
||||||
|
color: rgba(255, 255, 255, 0.59);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="input"] {
|
||||||
|
width: 100%;
|
||||||
|
height: 44px;
|
||||||
|
padding: 0 12px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.17);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.15s ease;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.39);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: rgba(255, 255, 255, 0.35);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="checkout-form"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="error"] {
|
||||||
|
color: #ff6b6b;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="submit-button"] {
|
||||||
|
width: 100%;
|
||||||
|
height: 48px;
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #000;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="charge-notice"] {
|
||||||
|
color: #d4a500;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="success"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
|
||||||
|
[data-slot="title"] {
|
||||||
|
color: rgba(255, 255, 255, 0.92);
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 400;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="details"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt {
|
||||||
|
color: rgba(255, 255, 255, 0.59);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
dd {
|
||||||
|
color: rgba(255, 255, 255, 0.92);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
margin: 0;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="charge-notice"] {
|
||||||
|
color: #d4a500;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="loading"] {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40px 0;
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: rgba(255, 255, 255, 0.59);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="fine-print"] {
|
||||||
|
color: rgba(255, 255, 255, 0.39);
|
||||||
|
text-align: center;
|
||||||
|
font-size: 13px;
|
||||||
|
font-style: italic;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: rgba(255, 255, 255, 0.39);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="workspace-picker"] {
|
||||||
|
[data-slot="workspace-list"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
align-self: stretch;
|
||||||
|
outline: none;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 240px;
|
||||||
|
scrollbar-width: none;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="workspace-item"] {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
padding: 8px 12px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
align-self: stretch;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
[data-slot="selected-icon"] {
|
||||||
|
visibility: hidden;
|
||||||
|
color: rgba(255, 255, 255, 0.39);
|
||||||
|
font-family: "IBM Plex Mono", monospace;
|
||||||
|
font-size: 16px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 160%;
|
||||||
|
}
|
||||||
|
|
||||||
|
span:last-child {
|
||||||
|
color: rgba(255, 255, 255, 0.92);
|
||||||
|
font-size: 16px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 160%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&[data-active="true"] {
|
||||||
|
background: #161616;
|
||||||
|
|
||||||
|
[data-slot="selected-icon"] {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="footer"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="footer-content"] {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
gap: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span,
|
||||||
|
a {
|
||||||
|
color: rgba(255, 255, 255, 0.39);
|
||||||
|
font-family: "JetBrains Mono Nerd Font";
|
||||||
|
font-size: 16px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: normal;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="github-stars"] {
|
||||||
|
color: rgba(255, 255, 255, 0.25);
|
||||||
|
font-family: "JetBrains Mono Nerd Font";
|
||||||
|
font-size: 16px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="anomaly"] {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[data-slot="anomaly-alt"] {
|
||||||
|
color: rgba(255, 255, 255, 0.39);
|
||||||
|
font-family: "JetBrains Mono Nerd Font";
|
||||||
|
font-size: 16px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: normal;
|
||||||
|
text-decoration: none;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: rgba(255, 255, 255, 0.39);
|
||||||
|
font-family: "JetBrains Mono Nerd Font";
|
||||||
|
font-size: 16px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: normal;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,43 @@
|
|||||||
|
import { Match, Switch } from "solid-js"
|
||||||
|
|
||||||
|
export const plans = [
|
||||||
|
{ id: "20", multiplier: null },
|
||||||
|
{ id: "100", multiplier: "6x more usage than Black 20" },
|
||||||
|
{ id: "200", multiplier: "21x more usage than Black 20" },
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export type PlanID = (typeof plans)[number]["id"]
|
||||||
|
export type Plan = (typeof plans)[number]
|
||||||
|
|
||||||
|
export function PlanIcon(props: { plan: string }) {
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
<Match when={props.plan === "20"}>
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="4" y="4" width="16" height="16" rx="2" stroke="currentColor" stroke-width="1.5" />
|
||||||
|
</svg>
|
||||||
|
</Match>
|
||||||
|
<Match when={props.plan === "100"}>
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="3" y="3" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.5" />
|
||||||
|
<rect x="14" y="3" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.5" />
|
||||||
|
<rect x="3" y="14" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.5" />
|
||||||
|
<rect x="14" y="14" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.5" />
|
||||||
|
</svg>
|
||||||
|
</Match>
|
||||||
|
<Match when={props.plan === "200"}>
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="2" y="2" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
|
||||||
|
<rect x="10" y="2" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
|
||||||
|
<rect x="18" y="2" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
|
||||||
|
<rect x="2" y="10" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
|
||||||
|
<rect x="10" y="10" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
|
||||||
|
<rect x="18" y="10" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
|
||||||
|
<rect x="2" y="18" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
|
||||||
|
<rect x="10" y="18" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
|
||||||
|
<rect x="18" y="18" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
|
||||||
|
</svg>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
[data-page="black"] {
|
|
||||||
background: #000;
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: stretch;
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
color: #fff;
|
|
||||||
|
|
||||||
[data-component="header-gradient"] {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 288px;
|
|
||||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.16) 0%, rgba(0, 0, 0, 0) 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-component="header"] {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding-top: 40px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
/* [data-component="header-logo"] { */
|
|
||||||
/* } */
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-component="content"] {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
flex-grow: 1;
|
|
||||||
|
|
||||||
[data-slot="hero-black"] {
|
|
||||||
margin-top: 110px;
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
margin-top: 150px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="cta"] {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 32px;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
margin-top: -18px;
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
margin-top: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="heading"] {
|
|
||||||
color: rgba(255, 255, 255, 0.92);
|
|
||||||
text-align: center;
|
|
||||||
font-size: 18px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 160%; /* 28.8px */
|
|
||||||
|
|
||||||
span {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
[data-slot="subheading"] {
|
|
||||||
color: rgba(255, 255, 255, 0.59);
|
|
||||||
font-size: 15px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 160%;
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
font-size: 18px;
|
|
||||||
line-height: 160%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
[data-slot="button"] {
|
|
||||||
display: inline-flex;
|
|
||||||
height: 40px;
|
|
||||||
padding: 0 12px;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: rgba(255, 255, 255, 0.92);
|
|
||||||
text-decoration: none;
|
|
||||||
color: #000;
|
|
||||||
font-family: "JetBrains Mono Nerd Font";
|
|
||||||
font-size: 16px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: normal;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #e0e0e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
[data-slot="back-soon"] {
|
|
||||||
color: rgba(255, 255, 255, 0.59);
|
|
||||||
text-align: center;
|
|
||||||
font-size: 13px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 160%; /* 20.8px */
|
|
||||||
}
|
|
||||||
[data-slot="follow-us"] {
|
|
||||||
display: inline-flex;
|
|
||||||
height: 40px;
|
|
||||||
padding: 0 12px;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.17);
|
|
||||||
color: rgba(255, 255, 255, 0.59);
|
|
||||||
font-family: "JetBrains Mono Nerd Font";
|
|
||||||
font-size: 14px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: normal;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="fine-print"] {
|
|
||||||
color: rgba(255, 255, 255, 0.39);
|
|
||||||
text-align: center;
|
|
||||||
font-size: 13px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 160%; /* 20.8px */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-component="footer"] {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: 24px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
height: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="footer-content"] {
|
|
||||||
display: flex;
|
|
||||||
gap: 24px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
gap: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
span,
|
|
||||||
a {
|
|
||||||
color: rgba(255, 255, 255, 0.39);
|
|
||||||
font-family: "JetBrains Mono Nerd Font";
|
|
||||||
font-size: 16px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: normal;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="github-stars"] {
|
|
||||||
color: rgba(255, 255, 255, 0.25);
|
|
||||||
font-family: "JetBrains Mono Nerd Font";
|
|
||||||
font-size: 16px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="anomaly"] {
|
|
||||||
display: none;
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
[data-slot="anomaly-alt"] {
|
|
||||||
color: rgba(255, 255, 255, 0.39);
|
|
||||||
font-family: "JetBrains Mono Nerd Font";
|
|
||||||
font-size: 16px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: normal;
|
|
||||||
text-decoration: none;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: rgba(255, 255, 255, 0.39);
|
|
||||||
font-family: "JetBrains Mono Nerd Font";
|
|
||||||
font-size: 16px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: normal;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,450 @@
|
|||||||
|
import { A, createAsync, query, redirect, useParams } from "@solidjs/router"
|
||||||
|
import { Title } from "@solidjs/meta"
|
||||||
|
import { createEffect, createSignal, For, Match, Show, Switch } from "solid-js"
|
||||||
|
import { type Stripe, type PaymentMethod, loadStripe } from "@stripe/stripe-js"
|
||||||
|
import { Elements, PaymentElement, useStripe, useElements, AddressElement } from "solid-stripe"
|
||||||
|
import { PlanID, plans } from "../common"
|
||||||
|
import { getActor, useAuthSession } from "~/context/auth"
|
||||||
|
import { withActor } from "~/context/auth.withActor"
|
||||||
|
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||||
|
import { and, Database, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js"
|
||||||
|
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
|
||||||
|
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
|
||||||
|
import { createList } from "solid-list"
|
||||||
|
import { Modal } from "~/component/modal"
|
||||||
|
import { BillingTable } from "@opencode-ai/console-core/schema/billing.sql.js"
|
||||||
|
import { Billing } from "@opencode-ai/console-core/billing.js"
|
||||||
|
|
||||||
|
const plansMap = Object.fromEntries(plans.map((p) => [p.id, p])) as Record<PlanID, (typeof plans)[number]>
|
||||||
|
const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY!)
|
||||||
|
|
||||||
|
const getWorkspaces = query(async () => {
|
||||||
|
"use server"
|
||||||
|
const actor = await getActor()
|
||||||
|
if (actor.type === "public") throw redirect("/auth/authorize?continue=/black/subscribe")
|
||||||
|
return withActor(async () => {
|
||||||
|
return Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.select({
|
||||||
|
id: WorkspaceTable.id,
|
||||||
|
name: WorkspaceTable.name,
|
||||||
|
slug: WorkspaceTable.slug,
|
||||||
|
billing: {
|
||||||
|
customerID: BillingTable.customerID,
|
||||||
|
paymentMethodID: BillingTable.paymentMethodID,
|
||||||
|
paymentMethodType: BillingTable.paymentMethodType,
|
||||||
|
paymentMethodLast4: BillingTable.paymentMethodLast4,
|
||||||
|
subscriptionID: BillingTable.subscriptionID,
|
||||||
|
timeSubscriptionBooked: BillingTable.timeSubscriptionBooked,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.from(UserTable)
|
||||||
|
.innerJoin(WorkspaceTable, eq(UserTable.workspaceID, WorkspaceTable.id))
|
||||||
|
.innerJoin(BillingTable, eq(WorkspaceTable.id, BillingTable.workspaceID))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(UserTable.accountID, Actor.account()),
|
||||||
|
isNull(WorkspaceTable.timeDeleted),
|
||||||
|
isNull(UserTable.timeDeleted),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}, "black.subscribe.workspaces")
|
||||||
|
|
||||||
|
const createSetupIntent = async (input: { plan: string; workspaceID: string }) => {
|
||||||
|
"use server"
|
||||||
|
const { plan, workspaceID } = input
|
||||||
|
|
||||||
|
if (!plan || !["20", "100", "200"].includes(plan)) return { error: "Invalid plan" }
|
||||||
|
if (!workspaceID) return { error: "Workspace ID is required" }
|
||||||
|
|
||||||
|
return withActor(async () => {
|
||||||
|
const session = await useAuthSession()
|
||||||
|
const account = session.data.account?.[session.data.current ?? ""]
|
||||||
|
const email = account?.email
|
||||||
|
|
||||||
|
const customer = await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.select({
|
||||||
|
customerID: BillingTable.customerID,
|
||||||
|
subscriptionID: BillingTable.subscriptionID,
|
||||||
|
})
|
||||||
|
.from(BillingTable)
|
||||||
|
.where(eq(BillingTable.workspaceID, workspaceID))
|
||||||
|
.then((rows) => rows[0]),
|
||||||
|
)
|
||||||
|
if (customer?.subscriptionID) {
|
||||||
|
return { error: "This workspace already has a subscription" }
|
||||||
|
}
|
||||||
|
|
||||||
|
let customerID = customer?.customerID
|
||||||
|
if (!customerID) {
|
||||||
|
const customer = await Billing.stripe().customers.create({
|
||||||
|
email,
|
||||||
|
metadata: {
|
||||||
|
workspaceID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
customerID = customer.id
|
||||||
|
await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.update(BillingTable)
|
||||||
|
.set({
|
||||||
|
customerID,
|
||||||
|
})
|
||||||
|
.where(eq(BillingTable.workspaceID, workspaceID)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const intent = await Billing.stripe().setupIntents.create({
|
||||||
|
customer: customerID,
|
||||||
|
payment_method_types: ["card"],
|
||||||
|
metadata: {
|
||||||
|
workspaceID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return { clientSecret: intent.client_secret ?? undefined }
|
||||||
|
}, workspaceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
const bookSubscription = async (input: {
|
||||||
|
workspaceID: string
|
||||||
|
plan: PlanID
|
||||||
|
paymentMethodID: string
|
||||||
|
paymentMethodType: string
|
||||||
|
paymentMethodLast4?: string
|
||||||
|
}) => {
|
||||||
|
"use server"
|
||||||
|
return withActor(
|
||||||
|
() =>
|
||||||
|
Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.update(BillingTable)
|
||||||
|
.set({
|
||||||
|
paymentMethodID: input.paymentMethodID,
|
||||||
|
paymentMethodType: input.paymentMethodType,
|
||||||
|
paymentMethodLast4: input.paymentMethodLast4,
|
||||||
|
subscriptionPlan: input.plan,
|
||||||
|
timeSubscriptionBooked: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(BillingTable.workspaceID, input.workspaceID)),
|
||||||
|
),
|
||||||
|
input.workspaceID,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SuccessData {
|
||||||
|
plan: string
|
||||||
|
paymentMethodType: string
|
||||||
|
paymentMethodLast4?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function Failure(props: { message: string }) {
|
||||||
|
return (
|
||||||
|
<div data-slot="failure">
|
||||||
|
<p data-slot="message">Uh oh! {props.message}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Success(props: SuccessData) {
|
||||||
|
return (
|
||||||
|
<div data-slot="success">
|
||||||
|
<p data-slot="title">You're on the OpenCode Black waitlist</p>
|
||||||
|
<dl data-slot="details">
|
||||||
|
<div>
|
||||||
|
<dt>Subscription plan</dt>
|
||||||
|
<dd>OpenCode Black {props.plan}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Amount</dt>
|
||||||
|
<dd>${props.plan} per month</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Payment method</dt>
|
||||||
|
<dd>
|
||||||
|
<Show when={props.paymentMethodLast4} fallback={<span>{props.paymentMethodType}</span>}>
|
||||||
|
<span>
|
||||||
|
{props.paymentMethodType} - {props.paymentMethodLast4}
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Date joined</dt>
|
||||||
|
<dd>{new Date().toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
<p data-slot="charge-notice">Your card will be charged when your subscription is activated</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function IntentForm(props: { plan: PlanID; workspaceID: string; onSuccess: (data: SuccessData) => void }) {
|
||||||
|
const stripe = useStripe()
|
||||||
|
const elements = useElements()
|
||||||
|
const [error, setError] = createSignal<string | undefined>(undefined)
|
||||||
|
const [loading, setLoading] = createSignal(false)
|
||||||
|
|
||||||
|
const handleSubmit = async (e: Event) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!stripe() || !elements()) return
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
setError(undefined)
|
||||||
|
|
||||||
|
const result = await elements()!.submit()
|
||||||
|
if (result.error) {
|
||||||
|
setError(result.error.message ?? "An error occurred")
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error: confirmError, setupIntent } = await stripe()!.confirmSetup({
|
||||||
|
elements: elements()!,
|
||||||
|
confirmParams: {
|
||||||
|
expand: ["payment_method"],
|
||||||
|
payment_method_data: {
|
||||||
|
allow_redisplay: "always",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
redirect: "if_required",
|
||||||
|
})
|
||||||
|
|
||||||
|
if (confirmError) {
|
||||||
|
setError(confirmError.message ?? "An error occurred")
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
console.log(setupIntent)
|
||||||
|
if (setupIntent?.status === "succeeded") {
|
||||||
|
const pm = setupIntent.payment_method as PaymentMethod
|
||||||
|
|
||||||
|
await bookSubscription({
|
||||||
|
workspaceID: props.workspaceID,
|
||||||
|
plan: props.plan,
|
||||||
|
paymentMethodID: pm.id,
|
||||||
|
paymentMethodType: pm.type,
|
||||||
|
paymentMethodLast4: pm.card?.last4,
|
||||||
|
})
|
||||||
|
|
||||||
|
props.onSuccess({
|
||||||
|
plan: props.plan,
|
||||||
|
paymentMethodType: pm.type,
|
||||||
|
paymentMethodLast4: pm.card?.last4,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} data-slot="checkout-form">
|
||||||
|
<PaymentElement />
|
||||||
|
<AddressElement options={{ mode: "billing" }} />
|
||||||
|
<Show when={error()}>
|
||||||
|
<p data-slot="error">{error()}</p>
|
||||||
|
</Show>
|
||||||
|
<button type="submit" disabled={loading() || !stripe() || !elements()} data-slot="submit-button">
|
||||||
|
{loading() ? "Processing..." : `Subscribe $${props.plan}`}
|
||||||
|
</button>
|
||||||
|
<p data-slot="charge-notice">You will only be charged when your subscription is activated</p>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BlackSubscribe() {
|
||||||
|
const workspaces = createAsync(() => getWorkspaces())
|
||||||
|
const [selectedWorkspace, setSelectedWorkspace] = createSignal<string | undefined>(undefined)
|
||||||
|
const [success, setSuccess] = createSignal<SuccessData | undefined>(undefined)
|
||||||
|
const [failure, setFailure] = createSignal<string | undefined>(undefined)
|
||||||
|
const [clientSecret, setClientSecret] = createSignal<string | undefined>(undefined)
|
||||||
|
const [stripe, setStripe] = createSignal<Stripe | undefined>(undefined)
|
||||||
|
const params = useParams()
|
||||||
|
const planData = plansMap[(params.plan as PlanID) ?? "20"] ?? plansMap["20"]
|
||||||
|
const plan = planData.id
|
||||||
|
|
||||||
|
// Resolve stripe promise once
|
||||||
|
createEffect(() => {
|
||||||
|
stripePromise.then((s) => {
|
||||||
|
if (s) setStripe(s)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Auto-select if only one workspace
|
||||||
|
createEffect(() => {
|
||||||
|
const ws = workspaces()
|
||||||
|
if (ws?.length === 1 && !selectedWorkspace()) {
|
||||||
|
setSelectedWorkspace(ws[0].id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fetch setup intent when workspace is selected (unless workspace already has payment method)
|
||||||
|
createEffect(async () => {
|
||||||
|
const id = selectedWorkspace()
|
||||||
|
if (!id) return
|
||||||
|
|
||||||
|
const ws = workspaces()?.find((w) => w.id === id)
|
||||||
|
if (ws?.billing?.subscriptionID) {
|
||||||
|
setFailure("This workspace already has a subscription")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (ws?.billing?.paymentMethodID) {
|
||||||
|
if (!ws?.billing?.timeSubscriptionBooked) {
|
||||||
|
await bookSubscription({
|
||||||
|
workspaceID: id,
|
||||||
|
plan: planData.id,
|
||||||
|
paymentMethodID: ws.billing.paymentMethodID!,
|
||||||
|
paymentMethodType: ws.billing.paymentMethodType!,
|
||||||
|
paymentMethodLast4: ws.billing.paymentMethodLast4 ?? undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setSuccess({
|
||||||
|
plan: planData.id,
|
||||||
|
paymentMethodType: ws.billing.paymentMethodType!,
|
||||||
|
paymentMethodLast4: ws.billing.paymentMethodLast4 ?? undefined,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await createSetupIntent({ plan, workspaceID: id })
|
||||||
|
if (result.error) {
|
||||||
|
setFailure(result.error)
|
||||||
|
} else if ("clientSecret" in result) {
|
||||||
|
setClientSecret(result.clientSecret)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Keyboard navigation for workspace picker
|
||||||
|
const { active, setActive, onKeyDown } = createList({
|
||||||
|
items: () => workspaces()?.map((w) => w.id) ?? [],
|
||||||
|
initialActive: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSelectWorkspace = (id: string) => {
|
||||||
|
setSelectedWorkspace(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
let listRef: HTMLUListElement | undefined
|
||||||
|
|
||||||
|
// Show workspace picker if multiple workspaces and none selected
|
||||||
|
const showWorkspacePicker = () => {
|
||||||
|
const ws = workspaces()
|
||||||
|
return ws && ws.length > 1 && !selectedWorkspace()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Title>Subscribe to OpenCode Black</Title>
|
||||||
|
<section data-slot="subscribe-form">
|
||||||
|
<div data-slot="form-card">
|
||||||
|
<Switch>
|
||||||
|
<Match when={success()}>{(data) => <Success {...data()} />}</Match>
|
||||||
|
<Match when={failure()}>{(data) => <Failure message={data()} />}</Match>
|
||||||
|
<Match when={true}>
|
||||||
|
<>
|
||||||
|
<div data-slot="plan-header">
|
||||||
|
<p data-slot="title">Subscribe to OpenCode Black</p>
|
||||||
|
<p data-slot="price">
|
||||||
|
<span data-slot="amount">${planData.id}</span> <span data-slot="period">per month</span>
|
||||||
|
<Show when={planData.multiplier}>
|
||||||
|
<span data-slot="multiplier">{planData.multiplier}</span>
|
||||||
|
</Show>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div data-slot="divider" />
|
||||||
|
<p data-slot="section-title">Payment method</p>
|
||||||
|
|
||||||
|
<Show
|
||||||
|
when={clientSecret() && selectedWorkspace() && stripe()}
|
||||||
|
fallback={
|
||||||
|
<div data-slot="loading">
|
||||||
|
<p>{selectedWorkspace() ? "Loading payment form..." : "Select a workspace to continue"}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Elements
|
||||||
|
stripe={stripe()!}
|
||||||
|
options={{
|
||||||
|
clientSecret: clientSecret()!,
|
||||||
|
appearance: {
|
||||||
|
theme: "night",
|
||||||
|
variables: {
|
||||||
|
colorPrimary: "#ffffff",
|
||||||
|
colorBackground: "#1a1a1a",
|
||||||
|
colorText: "#ffffff",
|
||||||
|
colorTextSecondary: "#999999",
|
||||||
|
colorDanger: "#ff6b6b",
|
||||||
|
fontFamily: "JetBrains Mono, monospace",
|
||||||
|
borderRadius: "4px",
|
||||||
|
spacingUnit: "4px",
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
".Input": {
|
||||||
|
backgroundColor: "#1a1a1a",
|
||||||
|
border: "1px solid rgba(255, 255, 255, 0.17)",
|
||||||
|
color: "#ffffff",
|
||||||
|
},
|
||||||
|
".Input:focus": {
|
||||||
|
borderColor: "rgba(255, 255, 255, 0.35)",
|
||||||
|
boxShadow: "none",
|
||||||
|
},
|
||||||
|
".Label": {
|
||||||
|
color: "rgba(255, 255, 255, 0.59)",
|
||||||
|
fontSize: "14px",
|
||||||
|
marginBottom: "8px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IntentForm plan={plan} workspaceID={selectedWorkspace()!} onSuccess={setSuccess} />
|
||||||
|
</Elements>
|
||||||
|
</Show>
|
||||||
|
</>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Workspace picker modal */}
|
||||||
|
<Modal open={showWorkspacePicker() ?? false} onClose={() => {}} title="Select a workspace for this plan">
|
||||||
|
<div data-slot="workspace-picker">
|
||||||
|
<ul
|
||||||
|
ref={listRef}
|
||||||
|
data-slot="workspace-list"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" && active()) {
|
||||||
|
handleSelectWorkspace(active()!)
|
||||||
|
} else {
|
||||||
|
onKeyDown(e)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<For each={workspaces()}>
|
||||||
|
{(workspace) => (
|
||||||
|
<li
|
||||||
|
data-slot="workspace-item"
|
||||||
|
data-active={active() === workspace.id}
|
||||||
|
onMouseEnter={() => setActive(workspace.id)}
|
||||||
|
onClick={() => handleSelectWorkspace(workspace.id)}
|
||||||
|
>
|
||||||
|
<span data-slot="selected-icon">[*]</span>
|
||||||
|
<span>{workspace.name || workspace.slug}</span>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
<p data-slot="fine-print">
|
||||||
|
Prices shown don't include applicable tax · <A href="/legal/terms-of-service">Terms of Service</A>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ export async function GET({ params: { platform } }: APIEvent) {
|
|||||||
const resp = await fetch(`https://github.com/anomalyco/opencode/releases/latest/download/${assetName}`, {
|
const resp = await fetch(`https://github.com/anomalyco/opencode/releases/latest/download/${assetName}`, {
|
||||||
cf: {
|
cf: {
|
||||||
// in case gh releases has rate limits
|
// in case gh releases has rate limits
|
||||||
cacheTtl: 60 * 60 * 24,
|
cacheTtl: 60 * 5,
|
||||||
cacheEverything: true,
|
cacheEverything: true,
|
||||||
},
|
},
|
||||||
} as any)
|
} as any)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { action } from "@solidjs/router"
|
import { action } from "@solidjs/router"
|
||||||
import { getRequestEvent } from "solid-js/web"
|
import { getRequestEvent } from "solid-js/web"
|
||||||
import { useAuthSession } from "~/context/auth.session"
|
import { useAuthSession } from "~/context/auth"
|
||||||
import { Dropdown } from "~/component/dropdown"
|
import { Dropdown } from "~/component/dropdown"
|
||||||
import "./user-menu.css"
|
import "./user-menu.css"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `billing` ADD `time_subscription_booked` timestamp(3);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `billing` ADD `subscription_plan` enum('20','100','200');
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -358,6 +358,20 @@
|
|||||||
"when": 1767931290031,
|
"when": 1767931290031,
|
||||||
"tag": "0050_bumpy_mephistopheles",
|
"tag": "0050_bumpy_mephistopheles",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 51,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1768341152722,
|
||||||
|
"tag": "0051_jazzy_green_goblin",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 52,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1768343920467,
|
||||||
|
"tag": "0052_aromatic_agent_zero",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.1.15",
|
"version": "1.1.20",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { bigint, boolean, index, int, json, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
|
import { bigint, boolean, index, int, json, mysqlEnum, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
|
||||||
import { timestamps, ulid, utc, workspaceColumns } from "../drizzle/types"
|
import { timestamps, ulid, utc, workspaceColumns } from "../drizzle/types"
|
||||||
import { workspaceIndexes } from "./workspace.sql"
|
import { workspaceIndexes } from "./workspace.sql"
|
||||||
|
|
||||||
@@ -23,6 +23,8 @@ export const BillingTable = mysqlTable(
|
|||||||
timeReloadLockedTill: utc("time_reload_locked_till"),
|
timeReloadLockedTill: utc("time_reload_locked_till"),
|
||||||
subscriptionID: varchar("subscription_id", { length: 28 }),
|
subscriptionID: varchar("subscription_id", { length: 28 }),
|
||||||
subscriptionCouponID: varchar("subscription_coupon_id", { length: 28 }),
|
subscriptionCouponID: varchar("subscription_coupon_id", { length: 28 }),
|
||||||
|
subscriptionPlan: mysqlEnum("subscription_plan", ["20", "100", "200"] as const),
|
||||||
|
timeSubscriptionBooked: utc("time_subscription_booked"),
|
||||||
},
|
},
|
||||||
(table) => [
|
(table) => [
|
||||||
...workspaceIndexes(table),
|
...workspaceIndexes(table),
|
||||||
|
|||||||
Vendored
+8
@@ -78,6 +78,10 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"STRIPE_PUBLISHABLE_KEY": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
"STRIPE_SECRET_KEY": {
|
"STRIPE_SECRET_KEY": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
@@ -130,6 +134,10 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"ZEN_SESSION_SECRET": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// cloudflare
|
// cloudflare
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.1.15",
|
"version": "1.1.20",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
+8
@@ -78,6 +78,10 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"STRIPE_PUBLISHABLE_KEY": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
"STRIPE_SECRET_KEY": {
|
"STRIPE_SECRET_KEY": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
@@ -130,6 +134,10 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"ZEN_SESSION_SECRET": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// cloudflare
|
// cloudflare
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.1.15",
|
"version": "1.1.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
|
|||||||
+8
@@ -78,6 +78,10 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"STRIPE_PUBLISHABLE_KEY": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
"STRIPE_SECRET_KEY": {
|
"STRIPE_SECRET_KEY": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
@@ -130,6 +134,10 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"ZEN_SESSION_SECRET": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// cloudflare
|
// cloudflare
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.1.15",
|
"version": "1.1.20",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { $ } from "bun"
|
import { $ } from "bun"
|
||||||
|
|
||||||
import { copyBinaryToSidecarFolder, getCurrentSidecar } from "./utils"
|
import { copyBinaryToSidecarFolder, getCurrentSidecar, windowsify } from "./utils"
|
||||||
|
|
||||||
const RUST_TARGET = Bun.env.TAURI_ENV_TARGET_TRIPLE
|
const RUST_TARGET = Bun.env.TAURI_ENV_TARGET_TRIPLE
|
||||||
|
|
||||||
const sidecarConfig = getCurrentSidecar(RUST_TARGET)
|
const sidecarConfig = getCurrentSidecar(RUST_TARGET)
|
||||||
|
|
||||||
const binaryPath = `../opencode/dist/${sidecarConfig.ocBinary}/bin/opencode${process.platform === "win32" ? ".exe" : ""}`
|
const binaryPath = windowsify(`../opencode/dist/${sidecarConfig.ocBinary}/bin/opencode`)
|
||||||
|
|
||||||
await $`cd ../opencode && bun run build --single`
|
await $`cd ../opencode && bun run build --single`
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env bun
|
#!/usr/bin/env bun
|
||||||
import { $ } from "bun"
|
import { $ } from "bun"
|
||||||
|
|
||||||
import { copyBinaryToSidecarFolder, getCurrentSidecar } from "./utils"
|
import { copyBinaryToSidecarFolder, getCurrentSidecar, windowsify } from "./utils"
|
||||||
|
|
||||||
const sidecarConfig = getCurrentSidecar()
|
const sidecarConfig = getCurrentSidecar()
|
||||||
|
|
||||||
@@ -10,6 +10,4 @@ const dir = "src-tauri/target/opencode-binaries"
|
|||||||
await $`mkdir -p ${dir}`
|
await $`mkdir -p ${dir}`
|
||||||
await $`gh run download ${Bun.env.GITHUB_RUN_ID} -n opencode-cli`.cwd(dir)
|
await $`gh run download ${Bun.env.GITHUB_RUN_ID} -n opencode-cli`.cwd(dir)
|
||||||
|
|
||||||
await copyBinaryToSidecarFolder(
|
await copyBinaryToSidecarFolder(windowsify(`${dir}/${sidecarConfig.ocBinary}/bin/opencode`))
|
||||||
`${dir}/${sidecarConfig.ocBinary}/bin/opencode${process.platform === "win32" ? ".exe" : ""}`,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -41,8 +41,13 @@ export function getCurrentSidecar(target = RUST_TARGET) {
|
|||||||
|
|
||||||
export async function copyBinaryToSidecarFolder(source: string, target = RUST_TARGET) {
|
export async function copyBinaryToSidecarFolder(source: string, target = RUST_TARGET) {
|
||||||
await $`mkdir -p src-tauri/sidecars`
|
await $`mkdir -p src-tauri/sidecars`
|
||||||
const dest = `src-tauri/sidecars/opencode-cli-${target}${process.platform === "win32" ? ".exe" : ""}`
|
const dest = windowsify(`src-tauri/sidecars/opencode-cli-${target}`)
|
||||||
await $`cp ${source} ${dest}`
|
await $`cp ${source} ${dest}`
|
||||||
|
|
||||||
console.log(`Copied ${source} to ${dest}`)
|
console.log(`Copied ${source} to ${dest}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function windowsify(path: string) {
|
||||||
|
if (path.endsWith(".exe")) return path
|
||||||
|
return `${path}${process.platform === "win32" ? ".exe" : ""}`
|
||||||
|
}
|
||||||
|
|||||||
Generated
+5
-3
@@ -2814,7 +2814,9 @@ dependencies = [
|
|||||||
"tauri-plugin-updater",
|
"tauri-plugin-updater",
|
||||||
"tauri-plugin-window-state",
|
"tauri-plugin-window-state",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"uuid",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
|
"windows",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5364,13 +5366,13 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.18.1"
|
version = "1.19.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
|
checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"serde",
|
"serde_core",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,16 @@ tauri-plugin-os = "2"
|
|||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
semver = "1.0.27"
|
semver = "1.0.27"
|
||||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
|
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
|
||||||
|
uuid = { version = "1.19.0", features = ["v4"] }
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
gtk = "0.18.2"
|
gtk = "0.18.2"
|
||||||
webkit2gtk = "=2.0.1"
|
webkit2gtk = "=2.0.1"
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
windows = { version = "0.61", features = [
|
||||||
|
"Win32_Foundation",
|
||||||
|
"Win32_System_JobObjects",
|
||||||
|
"Win32_System_Threading",
|
||||||
|
"Win32_Security"
|
||||||
|
] }
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ pub fn create_command(app: &tauri::AppHandle, args: &str) -> Command {
|
|||||||
.shell()
|
.shell()
|
||||||
.sidecar("opencode-cli")
|
.sidecar("opencode-cli")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
.args(args.split_whitespace())
|
||||||
.env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true")
|
.env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true")
|
||||||
.env("OPENCODE_CLIENT", "desktop")
|
.env("OPENCODE_CLIENT", "desktop")
|
||||||
.env("XDG_STATE_HOME", &state_dir);
|
.env("XDG_STATE_HOME", &state_dir);
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
//! Windows Job Object for reliable child process cleanup.
|
||||||
|
//!
|
||||||
|
//! This module provides a wrapper around Windows Job Objects with the
|
||||||
|
//! `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` flag set. When the job object handle
|
||||||
|
//! is closed (including when the parent process exits or crashes), Windows
|
||||||
|
//! automatically terminates all processes assigned to the job.
|
||||||
|
//!
|
||||||
|
//! This is more reliable than manual cleanup because it works even if:
|
||||||
|
//! - The parent process crashes
|
||||||
|
//! - The parent is killed via Task Manager
|
||||||
|
//! - The RunEvent::Exit handler fails to run
|
||||||
|
|
||||||
|
use std::io::{Error, Result};
|
||||||
|
#[cfg(windows)]
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use windows::Win32::Foundation::{CloseHandle, HANDLE};
|
||||||
|
use windows::Win32::System::JobObjects::{
|
||||||
|
AssignProcessToJobObject, CreateJobObjectW, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
|
||||||
|
JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JobObjectExtendedLimitInformation,
|
||||||
|
SetInformationJobObject,
|
||||||
|
};
|
||||||
|
use windows::Win32::System::Threading::{OpenProcess, PROCESS_SET_QUOTA, PROCESS_TERMINATE};
|
||||||
|
|
||||||
|
/// A Windows Job Object configured to kill all assigned processes when closed.
|
||||||
|
///
|
||||||
|
/// When this struct is dropped or when the owning process exits (even abnormally),
|
||||||
|
/// Windows will automatically terminate all processes that have been assigned to it.
|
||||||
|
pub struct JobObject(HANDLE);
|
||||||
|
|
||||||
|
// SAFETY: HANDLE is just a pointer-sized value, and Windows job objects
|
||||||
|
// can be safely accessed from multiple threads.
|
||||||
|
unsafe impl Send for JobObject {}
|
||||||
|
unsafe impl Sync for JobObject {}
|
||||||
|
|
||||||
|
impl JobObject {
|
||||||
|
/// Creates a new anonymous job object with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` set.
|
||||||
|
///
|
||||||
|
/// When the last handle to this job is closed (including on process exit),
|
||||||
|
/// Windows will terminate all processes assigned to the job.
|
||||||
|
pub fn new() -> Result<Self> {
|
||||||
|
unsafe {
|
||||||
|
// Create an anonymous job object
|
||||||
|
let job = CreateJobObjectW(None, None).map_err(|e| Error::other(e.message()))?;
|
||||||
|
|
||||||
|
// Configure the job to kill all processes when the handle is closed
|
||||||
|
let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
|
||||||
|
info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
||||||
|
|
||||||
|
SetInformationJobObject(
|
||||||
|
job,
|
||||||
|
JobObjectExtendedLimitInformation,
|
||||||
|
&info as *const _ as *const std::ffi::c_void,
|
||||||
|
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
|
||||||
|
)
|
||||||
|
.map_err(|e| Error::other(e.message()))?;
|
||||||
|
|
||||||
|
Ok(Self(job))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assigns a process to this job object by its process ID.
|
||||||
|
///
|
||||||
|
/// Once assigned, the process will be terminated when this job object is dropped
|
||||||
|
/// or when the owning process exits.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `pid` - The process ID of the process to assign
|
||||||
|
pub fn assign_pid(&self, pid: u32) -> Result<()> {
|
||||||
|
unsafe {
|
||||||
|
// Open a handle to the process with the minimum required permissions
|
||||||
|
// PROCESS_SET_QUOTA and PROCESS_TERMINATE are required by AssignProcessToJobObject
|
||||||
|
let process = OpenProcess(PROCESS_SET_QUOTA | PROCESS_TERMINATE, false, pid)
|
||||||
|
.map_err(|e| Error::other(e.message()))?;
|
||||||
|
|
||||||
|
// Assign the process to the job
|
||||||
|
let result = AssignProcessToJobObject(self.0, process);
|
||||||
|
|
||||||
|
// Close our handle to the process - the job object maintains its own reference
|
||||||
|
let _ = CloseHandle(process);
|
||||||
|
|
||||||
|
result.map_err(|e| Error::other(e.message()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for JobObject {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
unsafe {
|
||||||
|
// When this handle is closed and it's the last handle to the job,
|
||||||
|
// Windows will terminate all processes in the job due to KILL_ON_JOB_CLOSE
|
||||||
|
let _ = CloseHandle(self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Holds the Windows Job Object that ensures child processes are killed when the app exits.
|
||||||
|
/// On Windows, when the job object handle is closed (including on crash), all assigned
|
||||||
|
/// processes are automatically terminated by the OS.
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub struct JobObjectState {
|
||||||
|
job: Mutex<Option<JobObject>>,
|
||||||
|
error: Mutex<Option<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
impl JobObjectState {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
match JobObject::new() {
|
||||||
|
Ok(job) => Self {
|
||||||
|
job: Mutex::new(Some(job)),
|
||||||
|
error: Mutex::new(None),
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to create job object: {e}");
|
||||||
|
Self {
|
||||||
|
job: Mutex::new(None),
|
||||||
|
error: Mutex::new(Some(format!("Failed to create job object: {e}"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assign_pid(&self, pid: u32) {
|
||||||
|
if let Some(job) = self.job.lock().unwrap().as_ref() {
|
||||||
|
if let Err(e) = job.assign_pid(pid) {
|
||||||
|
eprintln!("Failed to assign process {pid} to job object: {e}");
|
||||||
|
*self.error.lock().unwrap() =
|
||||||
|
Some(format!("Failed to assign process to job object: {e}"));
|
||||||
|
} else {
|
||||||
|
println!("Assigned process {pid} to job object for automatic cleanup");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_job_object_creation() {
|
||||||
|
let job = JobObject::new();
|
||||||
|
assert!(job.is_ok(), "Failed to create job object: {:?}", job.err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
mod cli;
|
mod cli;
|
||||||
|
#[cfg(windows)]
|
||||||
|
mod job_object;
|
||||||
mod window_customizer;
|
mod window_customizer;
|
||||||
|
|
||||||
use cli::{install_cli, sync_cli};
|
use cli::{install_cli, sync_cli};
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
|
use futures::future;
|
||||||
|
#[cfg(windows)]
|
||||||
|
use job_object::*;
|
||||||
use std::{
|
use std::{
|
||||||
collections::VecDeque,
|
collections::VecDeque,
|
||||||
net::TcpListener,
|
net::TcpListener,
|
||||||
@@ -13,22 +18,29 @@ use tauri::{AppHandle, LogicalSize, Manager, RunEvent, State, WebviewUrl, Webvie
|
|||||||
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult};
|
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult};
|
||||||
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
|
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
|
||||||
use tauri_plugin_store::StoreExt;
|
use tauri_plugin_store::StoreExt;
|
||||||
|
use tokio::sync::oneshot;
|
||||||
|
|
||||||
use crate::window_customizer::PinchZoomDisablePlugin;
|
use crate::window_customizer::PinchZoomDisablePlugin;
|
||||||
|
|
||||||
const SETTINGS_STORE: &str = "opencode.settings.dat";
|
const SETTINGS_STORE: &str = "opencode.settings.dat";
|
||||||
const DEFAULT_SERVER_URL_KEY: &str = "defaultServerUrl";
|
const DEFAULT_SERVER_URL_KEY: &str = "defaultServerUrl";
|
||||||
|
|
||||||
|
#[derive(Clone, serde::Serialize)]
|
||||||
|
struct ServerReadyData {
|
||||||
|
url: String,
|
||||||
|
password: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct ServerState {
|
struct ServerState {
|
||||||
child: Arc<Mutex<Option<CommandChild>>>,
|
child: Arc<Mutex<Option<CommandChild>>>,
|
||||||
status: futures::future::Shared<tokio::sync::oneshot::Receiver<Result<String, String>>>,
|
status: future::Shared<oneshot::Receiver<Result<ServerReadyData, String>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServerState {
|
impl ServerState {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
child: Option<CommandChild>,
|
child: Option<CommandChild>,
|
||||||
status: tokio::sync::oneshot::Receiver<Result<String, String>>,
|
status: oneshot::Receiver<Result<ServerReadyData, String>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
child: Arc::new(Mutex::new(child)),
|
child: Arc::new(Mutex::new(child)),
|
||||||
@@ -80,7 +92,7 @@ async fn get_logs(app: AppHandle) -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn ensure_server_ready(state: State<'_, ServerState>) -> Result<String, String> {
|
async fn ensure_server_ready(state: State<'_, ServerState>) -> Result<ServerReadyData, String> {
|
||||||
state
|
state
|
||||||
.status
|
.status
|
||||||
.clone()
|
.clone()
|
||||||
@@ -137,13 +149,14 @@ fn get_sidecar_port() -> u32 {
|
|||||||
}) as u32
|
}) as u32
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_sidecar(app: &AppHandle, port: u32) -> CommandChild {
|
fn spawn_sidecar(app: &AppHandle, port: u32, password: &str) -> CommandChild {
|
||||||
let log_state = app.state::<LogState>();
|
let log_state = app.state::<LogState>();
|
||||||
let log_state_clone = log_state.inner().clone();
|
let log_state_clone = log_state.inner().clone();
|
||||||
|
|
||||||
println!("spawning sidecar on port {port}");
|
println!("spawning sidecar on port {port}");
|
||||||
|
|
||||||
let (mut rx, child) = cli::create_command(app, format!("serve --port {port}").as_str())
|
let (mut rx, child) = cli::create_command(app, format!("serve --port {port}").as_str())
|
||||||
|
.env("OPENCODE_SERVER_PASSWORD", password)
|
||||||
.spawn()
|
.spawn()
|
||||||
.expect("Failed to spawn opencode");
|
.expect("Failed to spawn opencode");
|
||||||
|
|
||||||
@@ -184,8 +197,8 @@ fn spawn_sidecar(app: &AppHandle, port: u32) -> CommandChild {
|
|||||||
child
|
child
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn check_server_health(url: &str) -> bool {
|
async fn check_server_health(url: &str, password: Option<&str>) -> bool {
|
||||||
let health_url = format!("{}/health", url.trim_end_matches('/'));
|
let health_url = format!("{}/global/health", url.trim_end_matches('/'));
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.timeout(Duration::from_secs(3))
|
.timeout(Duration::from_secs(3))
|
||||||
.build();
|
.build();
|
||||||
@@ -194,9 +207,13 @@ async fn check_server_health(url: &str) -> bool {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
client
|
let mut req = client.get(&health_url);
|
||||||
.get(&health_url)
|
|
||||||
.send()
|
if let Some(password) = password {
|
||||||
|
req = req.basic_auth("opencode", Some(password));
|
||||||
|
}
|
||||||
|
|
||||||
|
req.send()
|
||||||
.await
|
.await
|
||||||
.map(|r| r.status().is_success())
|
.map(|r| r.status().is_success())
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
@@ -206,6 +223,11 @@ async fn check_server_health(url: &str) -> bool {
|
|||||||
pub fn run() {
|
pub fn run() {
|
||||||
let updater_enabled = option_env!("TAURI_SIGNING_PRIVATE_KEY").is_some();
|
let updater_enabled = option_env!("TAURI_SIGNING_PRIVATE_KEY").is_some();
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let _ = std::process::Command::new("killall")
|
||||||
|
.arg("opencode-cli")
|
||||||
|
.output();
|
||||||
|
|
||||||
let mut builder = tauri::Builder::default()
|
let mut builder = tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
||||||
// Focus existing window when another instance is launched
|
// Focus existing window when another instance is launched
|
||||||
@@ -238,6 +260,9 @@ pub fn run() {
|
|||||||
// Initialize log state
|
// Initialize log state
|
||||||
app.manage(LogState(Arc::new(Mutex::new(VecDeque::new()))));
|
app.manage(LogState(Arc::new(Mutex::new(VecDeque::new()))));
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
app.manage(JobObjectState::new());
|
||||||
|
|
||||||
let primary_monitor = app.primary_monitor().ok().flatten();
|
let primary_monitor = app.primary_monitor().ok().flatten();
|
||||||
let size = primary_monitor
|
let size = primary_monitor
|
||||||
.map(|m| m.size().to_logical(m.scale_factor()))
|
.map(|m| m.size().to_logical(m.scale_factor()))
|
||||||
@@ -267,7 +292,7 @@ pub fn run() {
|
|||||||
|
|
||||||
window_builder.build().expect("Failed to create window");
|
window_builder.build().expect("Failed to create window");
|
||||||
|
|
||||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
let (tx, rx) = oneshot::channel();
|
||||||
app.manage(ServerState::new(None, rx));
|
app.manage(ServerState::new(None, rx));
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -290,7 +315,14 @@ pub fn run() {
|
|||||||
|
|
||||||
let res = match setup_server_connection(&app, custom_url).await {
|
let res = match setup_server_connection(&app, custom_url).await {
|
||||||
Ok((child, url)) => {
|
Ok((child, url)) => {
|
||||||
|
#[cfg(windows)]
|
||||||
|
if let Some(child) = &child {
|
||||||
|
let job_state = app.state::<JobObjectState>();
|
||||||
|
job_state.assign_pid(child.pid());
|
||||||
|
}
|
||||||
|
|
||||||
app.state::<ServerState>().set_child(child);
|
app.state::<ServerState>().set_child(child);
|
||||||
|
|
||||||
Ok(url)
|
Ok(url)
|
||||||
}
|
}
|
||||||
Err(e) => Err(e),
|
Err(e) => Err(e),
|
||||||
@@ -344,12 +376,18 @@ fn get_server_url_from_config(config: &cli::Config) -> Option<String> {
|
|||||||
async fn setup_server_connection(
|
async fn setup_server_connection(
|
||||||
app: &AppHandle,
|
app: &AppHandle,
|
||||||
custom_url: Option<String>,
|
custom_url: Option<String>,
|
||||||
) -> Result<(Option<CommandChild>, String), String> {
|
) -> Result<(Option<CommandChild>, ServerReadyData), String> {
|
||||||
if let Some(url) = custom_url {
|
if let Some(url) = custom_url {
|
||||||
loop {
|
loop {
|
||||||
if check_server_health(&url).await {
|
if check_server_health(&url, None).await {
|
||||||
println!("Connected to custom server: {}", url);
|
println!("Connected to custom server: {}", url);
|
||||||
return Ok((None, url.clone()));
|
return Ok((
|
||||||
|
None,
|
||||||
|
ServerReadyData {
|
||||||
|
url: url.clone(),
|
||||||
|
password: None,
|
||||||
|
},
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
const RETRY: &str = "Retry";
|
const RETRY: &str = "Retry";
|
||||||
@@ -374,24 +412,41 @@ async fn setup_server_connection(
|
|||||||
let local_port = get_sidecar_port();
|
let local_port = get_sidecar_port();
|
||||||
let local_url = format!("http://127.0.0.1:{local_port}");
|
let local_url = format!("http://127.0.0.1:{local_port}");
|
||||||
|
|
||||||
if !check_server_health(&local_url).await {
|
if !check_server_health(&local_url, None).await {
|
||||||
match spawn_local_server(app, local_port).await {
|
let password = uuid::Uuid::new_v4().to_string();
|
||||||
Ok(child) => Ok(Some(child)),
|
|
||||||
|
match spawn_local_server(app, local_port, &password).await {
|
||||||
|
Ok(child) => Ok((
|
||||||
|
Some(child),
|
||||||
|
ServerReadyData {
|
||||||
|
url: local_url,
|
||||||
|
password: Some(password),
|
||||||
|
},
|
||||||
|
)),
|
||||||
Err(err) => Err(err),
|
Err(err) => Err(err),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok((
|
||||||
|
None,
|
||||||
|
ServerReadyData {
|
||||||
|
url: local_url,
|
||||||
|
password: None,
|
||||||
|
},
|
||||||
|
))
|
||||||
}
|
}
|
||||||
.map(|child| (child, local_url))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn spawn_local_server(app: &AppHandle, port: u32) -> Result<CommandChild, String> {
|
async fn spawn_local_server(
|
||||||
let child = spawn_sidecar(app, port);
|
app: &AppHandle,
|
||||||
|
port: u32,
|
||||||
|
password: &str,
|
||||||
|
) -> Result<CommandChild, String> {
|
||||||
|
let child = spawn_sidecar(app, port, password);
|
||||||
let url = format!("http://127.0.0.1:{port}");
|
let url = format!("http://127.0.0.1:{port}");
|
||||||
|
|
||||||
let timestamp = Instant::now();
|
let timestamp = Instant::now();
|
||||||
loop {
|
loop {
|
||||||
if timestamp.elapsed() > Duration::from_secs(7) {
|
if timestamp.elapsed() > Duration::from_secs(30) {
|
||||||
break Err(format!(
|
break Err(format!(
|
||||||
"Failed to spawn OpenCode Server. Logs:\n{}",
|
"Failed to spawn OpenCode Server. Logs:\n{}",
|
||||||
get_logs(app.clone()).await.unwrap()
|
get_logs(app.clone()).await.unwrap()
|
||||||
@@ -400,7 +455,7 @@ async fn spawn_local_server(app: &AppHandle, port: u32) -> Result<CommandChild,
|
|||||||
|
|
||||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||||
|
|
||||||
if check_server_health(&url).await {
|
if check_server_health(&url, Some(password)).await {
|
||||||
println!("Server ready after {:?}", timestamp.elapsed());
|
println!("Server ready after {:?}", timestamp.elapsed());
|
||||||
break Ok(child);
|
break Ok(child);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,11 @@ import { AsyncStorage } from "@solid-primitives/storage"
|
|||||||
import { fetch as tauriFetch } from "@tauri-apps/plugin-http"
|
import { fetch as tauriFetch } from "@tauri-apps/plugin-http"
|
||||||
import { Store } from "@tauri-apps/plugin-store"
|
import { Store } from "@tauri-apps/plugin-store"
|
||||||
import { Logo } from "@opencode-ai/ui/logo"
|
import { Logo } from "@opencode-ai/ui/logo"
|
||||||
import { Accessor, JSX, createResource } from "solid-js"
|
import { createSignal, Show, Accessor, JSX, createResource } from "solid-js"
|
||||||
|
|
||||||
import { UPDATER_ENABLED } from "./updater"
|
import { UPDATER_ENABLED } from "./updater"
|
||||||
import { createMenu } from "./menu"
|
import { createMenu } from "./menu"
|
||||||
import pkg from "../package.json"
|
import pkg from "../package.json"
|
||||||
import { Show } from "solid-js"
|
|
||||||
|
|
||||||
const root = document.getElementById("root")
|
const root = document.getElementById("root")
|
||||||
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
||||||
@@ -29,7 +28,7 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
|||||||
|
|
||||||
let update: Update | null = null
|
let update: Update | null = null
|
||||||
|
|
||||||
const platform: Platform = {
|
const createPlatform = (password: Accessor<string | null>): Platform => ({
|
||||||
platform: "desktop",
|
platform: "desktop",
|
||||||
version: pkg.version,
|
version: pkg.version,
|
||||||
|
|
||||||
@@ -256,7 +255,25 @@ const platform: Platform = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
fetch: tauriFetch,
|
fetch: (input, init) => {
|
||||||
|
const pw = password()
|
||||||
|
|
||||||
|
const addHeader = (headers: Headers, password: string) => {
|
||||||
|
headers.append("Authorization", `Basic ${btoa(`opencode:${password}`)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input instanceof Request) {
|
||||||
|
if (pw) addHeader(input.headers, pw)
|
||||||
|
return tauriFetch(input)
|
||||||
|
} else {
|
||||||
|
const headers = new Headers(init?.headers)
|
||||||
|
if (pw) addHeader(headers, pw)
|
||||||
|
return tauriFetch(input, {
|
||||||
|
...(init as any),
|
||||||
|
headers: headers,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
getDefaultServerUrl: async () => {
|
getDefaultServerUrl: async () => {
|
||||||
const result = await invoke<string | null>("get_default_server_url").catch(() => null)
|
const result = await invoke<string | null>("get_default_server_url").catch(() => null)
|
||||||
@@ -266,7 +283,7 @@ const platform: Platform = {
|
|||||||
setDefaultServerUrl: async (url: string | null) => {
|
setDefaultServerUrl: async (url: string | null) => {
|
||||||
await invoke("set_default_server_url", { url })
|
await invoke("set_default_server_url", { url })
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
|
|
||||||
createMenu()
|
createMenu()
|
||||||
|
|
||||||
@@ -276,26 +293,39 @@ root?.addEventListener("mousewheel", (e) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
render(() => {
|
render(() => {
|
||||||
|
const [serverPassword, setServerPassword] = createSignal<string | null>(null)
|
||||||
|
const platform = createPlatform(() => serverPassword())
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PlatformProvider value={platform}>
|
<PlatformProvider value={platform}>
|
||||||
{ostype() === "macos" && (
|
|
||||||
<div class="mx-px bg-background-base border-b border-border-weak-base h-8" data-tauri-drag-region />
|
|
||||||
)}
|
|
||||||
<AppBaseProviders>
|
<AppBaseProviders>
|
||||||
<ServerGate>{(serverUrl) => <AppInterface defaultUrl={serverUrl()} />}</ServerGate>
|
{ostype() === "macos" && (
|
||||||
|
<div class="mx-px bg-background-base border-b border-border-weak-base h-8" data-tauri-drag-region />
|
||||||
|
)}
|
||||||
|
<ServerGate>
|
||||||
|
{(data) => {
|
||||||
|
setServerPassword(data().password)
|
||||||
|
window.__OPENCODE__ ??= {}
|
||||||
|
window.__OPENCODE__.serverPassword = data().password ?? undefined
|
||||||
|
|
||||||
|
return <AppInterface defaultUrl={data().url} />
|
||||||
|
}}
|
||||||
|
</ServerGate>
|
||||||
</AppBaseProviders>
|
</AppBaseProviders>
|
||||||
</PlatformProvider>
|
</PlatformProvider>
|
||||||
)
|
)
|
||||||
}, root!)
|
}, root!)
|
||||||
|
|
||||||
|
type ServerReadyData = { url: string; password: string | null }
|
||||||
|
|
||||||
// Gate component that waits for the server to be ready
|
// Gate component that waits for the server to be ready
|
||||||
function ServerGate(props: { children: (url: Accessor<string>) => JSX.Element }) {
|
function ServerGate(props: { children: (data: Accessor<ServerReadyData>) => JSX.Element }) {
|
||||||
const [serverUrl] = createResource<string>(() => invoke("ensure_server_ready"))
|
const [serverData] = createResource<ServerReadyData>(() => invoke("ensure_server_ready"))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// Not using suspense as not all components are compatible with it (undefined refs)
|
// Not using suspense as not all components are compatible with it (undefined refs)
|
||||||
<Show
|
<Show
|
||||||
when={serverUrl.state !== "pending" && serverUrl()}
|
when={serverData.state !== "pending" && serverData()}
|
||||||
fallback={
|
fallback={
|
||||||
<div class="h-screen w-screen flex flex-col items-center justify-center bg-background-base">
|
<div class="h-screen w-screen flex flex-col items-center justify-center bg-background-base">
|
||||||
<Logo class="w-xl opacity-12 animate-pulse" />
|
<Logo class="w-xl opacity-12 animate-pulse" />
|
||||||
@@ -303,7 +333,7 @@ function ServerGate(props: { children: (url: Accessor<string>) => JSX.Element })
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(serverUrl) => props.children(serverUrl)}
|
{(data) => props.children(data)}
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.1.15",
|
"version": "1.1.20",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
Vendored
+8
@@ -78,6 +78,10 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"STRIPE_PUBLISHABLE_KEY": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
"STRIPE_SECRET_KEY": {
|
"STRIPE_SECRET_KEY": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
@@ -130,6 +134,10 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"ZEN_SESSION_SECRET": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// cloudflare
|
// cloudflare
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
id = "opencode"
|
id = "opencode"
|
||||||
name = "OpenCode"
|
name = "OpenCode"
|
||||||
description = "The open source coding agent."
|
description = "The open source coding agent."
|
||||||
version = "1.1.15"
|
version = "1.1.20"
|
||||||
schema_version = 1
|
schema_version = 1
|
||||||
authors = ["Anomaly"]
|
authors = ["Anomaly"]
|
||||||
repository = "https://github.com/anomalyco/opencode"
|
repository = "https://github.com/anomalyco/opencode"
|
||||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
|||||||
icon = "./icons/opencode.svg"
|
icon = "./icons/opencode.svg"
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-aarch64]
|
[agent_servers.opencode.targets.darwin-aarch64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.15/opencode-darwin-arm64.zip"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.20/opencode-darwin-arm64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-x86_64]
|
[agent_servers.opencode.targets.darwin-x86_64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.15/opencode-darwin-x64.zip"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.20/opencode-darwin-x64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-aarch64]
|
[agent_servers.opencode.targets.linux-aarch64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.15/opencode-linux-arm64.tar.gz"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.20/opencode-linux-arm64.tar.gz"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-x86_64]
|
[agent_servers.opencode.targets.linux-x86_64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.15/opencode-linux-x64.tar.gz"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.20/opencode-linux-x64.tar.gz"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.windows-x86_64]
|
[agent_servers.opencode.targets.windows-x86_64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.15/opencode-windows-x64.zip"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.20/opencode-windows-x64.zip"
|
||||||
cmd = "./opencode.exe"
|
cmd = "./opencode.exe"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.1.15",
|
"version": "1.1.20",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
Vendored
+8
@@ -78,6 +78,10 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"STRIPE_PUBLISHABLE_KEY": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
"STRIPE_SECRET_KEY": {
|
"STRIPE_SECRET_KEY": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
@@ -130,6 +134,10 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"ZEN_SESSION_SECRET": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// cloudflare
|
// cloudflare
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.1.15",
|
"version": "1.1.20",
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -70,6 +70,7 @@
|
|||||||
"@ai-sdk/vercel": "1.0.31",
|
"@ai-sdk/vercel": "1.0.31",
|
||||||
"@ai-sdk/xai": "2.0.51",
|
"@ai-sdk/xai": "2.0.51",
|
||||||
"@clack/prompts": "1.0.0-alpha.1",
|
"@clack/prompts": "1.0.0-alpha.1",
|
||||||
|
"@gitlab/gitlab-ai-provider": "3.1.0",
|
||||||
"@hono/standard-validator": "0.1.5",
|
"@hono/standard-validator": "0.1.5",
|
||||||
"@hono/zod-validator": "catalog:",
|
"@hono/zod-validator": "catalog:",
|
||||||
"@modelcontextprotocol/sdk": "1.25.2",
|
"@modelcontextprotocol/sdk": "1.25.2",
|
||||||
|
|||||||
@@ -106,8 +106,11 @@ async function main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { binaryPath, binaryName } = findBinary()
|
// On non-Windows platforms, just verify the binary package exists
|
||||||
symlinkBinary(binaryPath, binaryName)
|
// Don't replace the wrapper script - it handles binary execution
|
||||||
|
const { binaryPath } = findBinary()
|
||||||
|
console.log(`Platform binary verified at: ${binaryPath}`)
|
||||||
|
console.log("Wrapper script will handle binary execution")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to setup opencode binary:", error.message)
|
console.error("Failed to setup opencode binary:", error.message)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import PROMPT_SUMMARY from "./prompt/summary.txt"
|
|||||||
import PROMPT_TITLE from "./prompt/title.txt"
|
import PROMPT_TITLE from "./prompt/title.txt"
|
||||||
import { PermissionNext } from "@/permission/next"
|
import { PermissionNext } from "@/permission/next"
|
||||||
import { mergeDeep, pipe, sortBy, values } from "remeda"
|
import { mergeDeep, pipe, sortBy, values } from "remeda"
|
||||||
|
import { Global } from "@/global"
|
||||||
|
import path from "path"
|
||||||
|
|
||||||
export namespace Agent {
|
export namespace Agent {
|
||||||
export const Info = z
|
export const Info = z
|
||||||
@@ -50,13 +52,16 @@ export namespace Agent {
|
|||||||
external_directory: {
|
external_directory: {
|
||||||
"*": "ask",
|
"*": "ask",
|
||||||
[Truncate.DIR]: "allow",
|
[Truncate.DIR]: "allow",
|
||||||
|
[Truncate.GLOB]: "allow",
|
||||||
},
|
},
|
||||||
question: "deny",
|
question: "deny",
|
||||||
|
plan_enter: "deny",
|
||||||
|
plan_exit: "deny",
|
||||||
// mirrors github.com/github/gitignore Node.gitignore pattern for .env files
|
// mirrors github.com/github/gitignore Node.gitignore pattern for .env files
|
||||||
read: {
|
read: {
|
||||||
"*": "allow",
|
"*": "allow",
|
||||||
"*.env": "deny",
|
"*.env": "ask",
|
||||||
"*.env.*": "deny",
|
"*.env.*": "ask",
|
||||||
"*.env.example": "allow",
|
"*.env.example": "allow",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -70,6 +75,7 @@ export namespace Agent {
|
|||||||
defaults,
|
defaults,
|
||||||
PermissionNext.fromConfig({
|
PermissionNext.fromConfig({
|
||||||
question: "allow",
|
question: "allow",
|
||||||
|
plan_enter: "allow",
|
||||||
}),
|
}),
|
||||||
user,
|
user,
|
||||||
),
|
),
|
||||||
@@ -83,9 +89,14 @@ export namespace Agent {
|
|||||||
defaults,
|
defaults,
|
||||||
PermissionNext.fromConfig({
|
PermissionNext.fromConfig({
|
||||||
question: "allow",
|
question: "allow",
|
||||||
|
plan_exit: "allow",
|
||||||
|
external_directory: {
|
||||||
|
[path.join(Global.Path.data, "plans", "*")]: "allow",
|
||||||
|
},
|
||||||
edit: {
|
edit: {
|
||||||
"*": "deny",
|
"*": "deny",
|
||||||
".opencode/plan/*.md": "allow",
|
".opencode/plans/*.md": "allow",
|
||||||
|
[path.relative(Instance.worktree, path.join(Global.Path.data, "plans/*.md"))]: "allow",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
user,
|
user,
|
||||||
@@ -124,6 +135,7 @@ export namespace Agent {
|
|||||||
read: "allow",
|
read: "allow",
|
||||||
external_directory: {
|
external_directory: {
|
||||||
[Truncate.DIR]: "allow",
|
[Truncate.DIR]: "allow",
|
||||||
|
[Truncate.GLOB]: "allow",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
user,
|
user,
|
||||||
@@ -213,14 +225,16 @@ export namespace Agent {
|
|||||||
// Ensure Truncate.DIR is allowed unless explicitly configured
|
// Ensure Truncate.DIR is allowed unless explicitly configured
|
||||||
for (const name in result) {
|
for (const name in result) {
|
||||||
const agent = result[name]
|
const agent = result[name]
|
||||||
const explicit = agent.permission.some(
|
const explicit = agent.permission.some((r) => {
|
||||||
(r) => r.permission === "external_directory" && r.pattern === Truncate.DIR && r.action === "deny",
|
if (r.permission !== "external_directory") return false
|
||||||
)
|
if (r.action !== "deny") return false
|
||||||
|
return r.pattern === Truncate.DIR || r.pattern === Truncate.GLOB
|
||||||
|
})
|
||||||
if (explicit) continue
|
if (explicit) continue
|
||||||
|
|
||||||
result[name].permission = PermissionNext.merge(
|
result[name].permission = PermissionNext.merge(
|
||||||
result[name].permission,
|
result[name].permission,
|
||||||
PermissionNext.fromConfig({ external_directory: { [Truncate.DIR]: "allow" } }),
|
PermissionNext.fromConfig({ external_directory: { [Truncate.DIR]: "allow", [Truncate.GLOB]: "allow" } }),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { basename } from "path"
|
import { basename } from "path"
|
||||||
import { Agent } from "../../../agent/agent"
|
import { Agent } from "../../../agent/agent"
|
||||||
|
import { Provider } from "../../../provider/provider"
|
||||||
|
import { Session } from "../../../session"
|
||||||
|
import type { MessageV2 } from "../../../session/message-v2"
|
||||||
|
import { Identifier } from "../../../id/id"
|
||||||
|
import { ToolRegistry } from "../../../tool/registry"
|
||||||
|
import { Instance } from "../../../project/instance"
|
||||||
|
import { PermissionNext } from "../../../permission/next"
|
||||||
|
import { iife } from "../../../util/iife"
|
||||||
import { bootstrap } from "../../bootstrap"
|
import { bootstrap } from "../../bootstrap"
|
||||||
import { cmd } from "../cmd"
|
import { cmd } from "../cmd"
|
||||||
|
|
||||||
@@ -8,11 +16,20 @@ export const AgentCommand = cmd({
|
|||||||
command: "agent <name>",
|
command: "agent <name>",
|
||||||
describe: "show agent configuration details",
|
describe: "show agent configuration details",
|
||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
yargs.positional("name", {
|
yargs
|
||||||
type: "string",
|
.positional("name", {
|
||||||
demandOption: true,
|
type: "string",
|
||||||
description: "Agent name",
|
demandOption: true,
|
||||||
}),
|
description: "Agent name",
|
||||||
|
})
|
||||||
|
.option("tool", {
|
||||||
|
type: "string",
|
||||||
|
description: "Tool id to execute",
|
||||||
|
})
|
||||||
|
.option("params", {
|
||||||
|
type: "string",
|
||||||
|
description: "Tool params as JSON or a JS object literal",
|
||||||
|
}),
|
||||||
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
|
||||||
@@ -23,7 +40,127 @@ export const AgentCommand = cmd({
|
|||||||
)
|
)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
process.stdout.write(JSON.stringify(agent, null, 2) + EOL)
|
const availableTools = await getAvailableTools(agent)
|
||||||
|
const resolvedTools = await resolveTools(agent, availableTools)
|
||||||
|
const toolID = args.tool as string | undefined
|
||||||
|
if (toolID) {
|
||||||
|
const tool = availableTools.find((item) => item.id === toolID)
|
||||||
|
if (!tool) {
|
||||||
|
process.stderr.write(`Tool ${toolID} not found for agent ${agentName}` + EOL)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
if (resolvedTools[toolID] === false) {
|
||||||
|
process.stderr.write(`Tool ${toolID} is disabled for agent ${agentName}` + EOL)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
const params = parseToolParams(args.params as string | undefined)
|
||||||
|
const ctx = await createToolContext(agent)
|
||||||
|
const result = await tool.execute(params, ctx)
|
||||||
|
process.stdout.write(JSON.stringify({ tool: toolID, input: params, result }, null, 2) + EOL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = {
|
||||||
|
...agent,
|
||||||
|
tools: resolvedTools,
|
||||||
|
}
|
||||||
|
process.stdout.write(JSON.stringify(output, null, 2) + EOL)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
async function getAvailableTools(agent: Agent.Info) {
|
||||||
|
const providerID = agent.model?.providerID ?? (await Provider.defaultModel()).providerID
|
||||||
|
return ToolRegistry.tools(providerID, agent)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveTools(agent: Agent.Info, availableTools: Awaited<ReturnType<typeof getAvailableTools>>) {
|
||||||
|
const disabled = PermissionNext.disabled(
|
||||||
|
availableTools.map((tool) => tool.id),
|
||||||
|
agent.permission,
|
||||||
|
)
|
||||||
|
const resolved: Record<string, boolean> = {}
|
||||||
|
for (const tool of availableTools) {
|
||||||
|
resolved[tool.id] = !disabled.has(tool.id)
|
||||||
|
}
|
||||||
|
return resolved
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseToolParams(input?: string) {
|
||||||
|
if (!input) return {}
|
||||||
|
const trimmed = input.trim()
|
||||||
|
if (trimmed.length === 0) return {}
|
||||||
|
|
||||||
|
const parsed = iife(() => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(trimmed)
|
||||||
|
} catch (jsonError) {
|
||||||
|
try {
|
||||||
|
return new Function(`return (${trimmed})`)()
|
||||||
|
} catch (evalError) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to parse --params. Use JSON or a JS object literal. JSON error: ${jsonError}. Eval error: ${evalError}.`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||||
|
throw new Error("Tool params must be an object.")
|
||||||
|
}
|
||||||
|
return parsed as Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createToolContext(agent: Agent.Info) {
|
||||||
|
const session = await Session.create({ title: `Debug tool run (${agent.name})` })
|
||||||
|
const messageID = Identifier.ascending("message")
|
||||||
|
const model = agent.model ?? (await Provider.defaultModel())
|
||||||
|
const now = Date.now()
|
||||||
|
const message: MessageV2.Assistant = {
|
||||||
|
id: messageID,
|
||||||
|
sessionID: session.id,
|
||||||
|
role: "assistant",
|
||||||
|
time: {
|
||||||
|
created: now,
|
||||||
|
},
|
||||||
|
parentID: messageID,
|
||||||
|
modelID: model.modelID,
|
||||||
|
providerID: model.providerID,
|
||||||
|
mode: "debug",
|
||||||
|
agent: agent.name,
|
||||||
|
path: {
|
||||||
|
cwd: Instance.directory,
|
||||||
|
root: Instance.worktree,
|
||||||
|
},
|
||||||
|
cost: 0,
|
||||||
|
tokens: {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
reasoning: 0,
|
||||||
|
cache: {
|
||||||
|
read: 0,
|
||||||
|
write: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await Session.updateMessage(message)
|
||||||
|
|
||||||
|
const ruleset = PermissionNext.merge(agent.permission, session.permission ?? [])
|
||||||
|
|
||||||
|
return {
|
||||||
|
sessionID: session.id,
|
||||||
|
messageID,
|
||||||
|
callID: Identifier.ascending("part"),
|
||||||
|
agent: agent.name,
|
||||||
|
abort: new AbortController().signal,
|
||||||
|
metadata: () => {},
|
||||||
|
async ask(req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) {
|
||||||
|
for (const pattern of req.patterns) {
|
||||||
|
const rule = PermissionNext.evaluate(req.permission, pattern, ruleset)
|
||||||
|
if (rule.action === "deny") {
|
||||||
|
throw new PermissionNext.DeniedError(ruleset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ const FileTreeCommand = cmd({
|
|||||||
}),
|
}),
|
||||||
async handler(args) {
|
async handler(args) {
|
||||||
const files = await Ripgrep.tree({ cwd: args.dir, limit: 200 })
|
const files = await Ripgrep.tree({ cwd: args.dir, limit: 200 })
|
||||||
console.log(files)
|
console.log(JSON.stringify(files, null, 2))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -394,6 +394,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Run opencode
|
- name: Run opencode
|
||||||
uses: anomalyco/opencode/github@latest${envStr}
|
uses: anomalyco/opencode/github@latest${envStr}
|
||||||
@@ -624,7 +626,7 @@ export const GithubRunCommand = cmd({
|
|||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
exitCode = 1
|
exitCode = 1
|
||||||
console.error(e)
|
console.error(e instanceof Error ? e.message : String(e))
|
||||||
let msg = e
|
let msg = e
|
||||||
if (e instanceof $.ShellError) {
|
if (e instanceof $.ShellError) {
|
||||||
msg = e.stderr.toString()
|
msg = e.stderr.toString()
|
||||||
@@ -915,7 +917,7 @@ export const GithubRunCommand = cmd({
|
|||||||
|
|
||||||
// result should always be assistant just satisfying type checker
|
// 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) {
|
||||||
console.error(result.info)
|
console.error("Agent error:", result.info.error)
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`${result.info.error.name}: ${"message" in result.info.error ? result.info.error.message : ""}`,
|
`${result.info.error.name}: ${"message" in result.info.error ? result.info.error.message : ""}`,
|
||||||
)
|
)
|
||||||
@@ -944,7 +946,7 @@ export const GithubRunCommand = cmd({
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (summary.info.role === "assistant" && summary.info.error) {
|
if (summary.info.role === "assistant" && summary.info.error) {
|
||||||
console.error(summary.info)
|
console.error("Summary agent error:", summary.info.error)
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`${summary.info.error.name}: ${"message" in summary.info.error ? summary.info.error.message : ""}`,
|
`${summary.info.error.name}: ${"message" in summary.info.error ? summary.info.error.message : ""}`,
|
||||||
)
|
)
|
||||||
@@ -962,7 +964,7 @@ export const GithubRunCommand = cmd({
|
|||||||
try {
|
try {
|
||||||
return await core.getIDToken("opencode-github-action")
|
return await core.getIDToken("opencode-github-action")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to get OIDC token:", error)
|
console.error("Failed to get OIDC token:", error instanceof Error ? error.message : error)
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Could not fetch an OIDC token. Make sure to add `id-token: write` to your workflow permissions.",
|
"Could not fetch an OIDC token. Make sure to add `id-token: write` to your workflow permissions.",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
|
||||||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
|
||||||
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
|
|
||||||
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"
|
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"
|
||||||
import * as prompts from "@clack/prompts"
|
import * as prompts from "@clack/prompts"
|
||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
@@ -13,6 +12,7 @@ import { Instance } from "../../project/instance"
|
|||||||
import { Installation } from "../../installation"
|
import { Installation } from "../../installation"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Global } from "../../global"
|
import { Global } from "../../global"
|
||||||
|
import { modify, applyEdits } from "jsonc-parser"
|
||||||
|
|
||||||
function getAuthStatusIcon(status: MCP.AuthStatus): string {
|
function getAuthStatusIcon(status: MCP.AuthStatus): string {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@@ -21,7 +21,7 @@ function getAuthStatusIcon(status: MCP.AuthStatus): string {
|
|||||||
case "expired":
|
case "expired":
|
||||||
return "⚠"
|
return "⚠"
|
||||||
case "not_authenticated":
|
case "not_authenticated":
|
||||||
return "○"
|
return "✗"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,133 +366,204 @@ export const McpLogoutCommand = cmd({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
async function resolveConfigPath(baseDir: string, global = false) {
|
||||||
|
// Check for existing config files (prefer .jsonc over .json, check .opencode/ subdirectory too)
|
||||||
|
const candidates = [path.join(baseDir, "opencode.json"), path.join(baseDir, "opencode.jsonc")]
|
||||||
|
|
||||||
|
if (!global) {
|
||||||
|
candidates.push(path.join(baseDir, ".opencode", "opencode.json"), path.join(baseDir, ".opencode", "opencode.jsonc"))
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (await Bun.file(candidate).exists()) {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to opencode.json if none exist
|
||||||
|
return candidates[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addMcpToConfig(name: string, mcpConfig: Config.Mcp, configPath: string) {
|
||||||
|
const file = Bun.file(configPath)
|
||||||
|
|
||||||
|
let text = "{}"
|
||||||
|
if (await file.exists()) {
|
||||||
|
text = await file.text()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use jsonc-parser to modify while preserving comments
|
||||||
|
const edits = modify(text, ["mcp", name], mcpConfig, {
|
||||||
|
formattingOptions: { tabSize: 2, insertSpaces: true },
|
||||||
|
})
|
||||||
|
const result = applyEdits(text, edits)
|
||||||
|
|
||||||
|
await Bun.write(configPath, result)
|
||||||
|
|
||||||
|
return configPath
|
||||||
|
}
|
||||||
|
|
||||||
export const McpAddCommand = cmd({
|
export const McpAddCommand = cmd({
|
||||||
command: "add",
|
command: "add",
|
||||||
describe: "add an MCP server",
|
describe: "add an MCP server",
|
||||||
async handler() {
|
async handler() {
|
||||||
UI.empty()
|
await Instance.provide({
|
||||||
prompts.intro("Add MCP server")
|
directory: process.cwd(),
|
||||||
|
async fn() {
|
||||||
|
UI.empty()
|
||||||
|
prompts.intro("Add MCP server")
|
||||||
|
|
||||||
const name = await prompts.text({
|
const project = Instance.project
|
||||||
message: "Enter MCP server name",
|
|
||||||
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
|
||||||
})
|
|
||||||
if (prompts.isCancel(name)) throw new UI.CancelledError()
|
|
||||||
|
|
||||||
const type = await prompts.select({
|
// Resolve config paths eagerly for hints
|
||||||
message: "Select MCP server type",
|
const [projectConfigPath, globalConfigPath] = await Promise.all([
|
||||||
options: [
|
resolveConfigPath(Instance.worktree),
|
||||||
{
|
resolveConfigPath(Global.Path.config, true),
|
||||||
label: "Local",
|
])
|
||||||
value: "local",
|
|
||||||
hint: "Run a local command",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Remote",
|
|
||||||
value: "remote",
|
|
||||||
hint: "Connect to a remote URL",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
if (prompts.isCancel(type)) throw new UI.CancelledError()
|
|
||||||
|
|
||||||
if (type === "local") {
|
// Determine scope
|
||||||
const command = await prompts.text({
|
let configPath = globalConfigPath
|
||||||
message: "Enter command to run",
|
if (project.vcs === "git") {
|
||||||
placeholder: "e.g., opencode x @modelcontextprotocol/server-filesystem",
|
const scopeResult = await prompts.select({
|
||||||
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
message: "Location",
|
||||||
})
|
options: [
|
||||||
if (prompts.isCancel(command)) throw new UI.CancelledError()
|
{
|
||||||
|
label: "Current project",
|
||||||
|
value: projectConfigPath,
|
||||||
|
hint: projectConfigPath,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Global",
|
||||||
|
value: globalConfigPath,
|
||||||
|
hint: globalConfigPath,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
if (prompts.isCancel(scopeResult)) throw new UI.CancelledError()
|
||||||
|
configPath = scopeResult
|
||||||
|
}
|
||||||
|
|
||||||
prompts.log.info(`Local MCP server "${name}" configured with command: ${command}`)
|
const name = await prompts.text({
|
||||||
prompts.outro("MCP server added successfully")
|
message: "Enter MCP server name",
|
||||||
return
|
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
||||||
}
|
|
||||||
|
|
||||||
if (type === "remote") {
|
|
||||||
const url = await prompts.text({
|
|
||||||
message: "Enter MCP server URL",
|
|
||||||
placeholder: "e.g., https://example.com/mcp",
|
|
||||||
validate: (x) => {
|
|
||||||
if (!x) return "Required"
|
|
||||||
if (x.length === 0) return "Required"
|
|
||||||
const isValid = URL.canParse(x)
|
|
||||||
return isValid ? undefined : "Invalid URL"
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if (prompts.isCancel(url)) throw new UI.CancelledError()
|
|
||||||
|
|
||||||
const useOAuth = await prompts.confirm({
|
|
||||||
message: "Does this server require OAuth authentication?",
|
|
||||||
initialValue: false,
|
|
||||||
})
|
|
||||||
if (prompts.isCancel(useOAuth)) throw new UI.CancelledError()
|
|
||||||
|
|
||||||
if (useOAuth) {
|
|
||||||
const hasClientId = await prompts.confirm({
|
|
||||||
message: "Do you have a pre-registered client ID?",
|
|
||||||
initialValue: false,
|
|
||||||
})
|
})
|
||||||
if (prompts.isCancel(hasClientId)) throw new UI.CancelledError()
|
if (prompts.isCancel(name)) throw new UI.CancelledError()
|
||||||
|
|
||||||
if (hasClientId) {
|
const type = await prompts.select({
|
||||||
const clientId = await prompts.text({
|
message: "Select MCP server type",
|
||||||
message: "Enter client ID",
|
options: [
|
||||||
|
{
|
||||||
|
label: "Local",
|
||||||
|
value: "local",
|
||||||
|
hint: "Run a local command",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Remote",
|
||||||
|
value: "remote",
|
||||||
|
hint: "Connect to a remote URL",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
if (prompts.isCancel(type)) throw new UI.CancelledError()
|
||||||
|
|
||||||
|
if (type === "local") {
|
||||||
|
const command = await prompts.text({
|
||||||
|
message: "Enter command to run",
|
||||||
|
placeholder: "e.g., opencode x @modelcontextprotocol/server-filesystem",
|
||||||
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
||||||
})
|
})
|
||||||
if (prompts.isCancel(clientId)) throw new UI.CancelledError()
|
if (prompts.isCancel(command)) throw new UI.CancelledError()
|
||||||
|
|
||||||
const hasSecret = await prompts.confirm({
|
const mcpConfig: Config.Mcp = {
|
||||||
message: "Do you have a client secret?",
|
type: "local",
|
||||||
initialValue: false,
|
command: command.split(" "),
|
||||||
})
|
|
||||||
if (prompts.isCancel(hasSecret)) throw new UI.CancelledError()
|
|
||||||
|
|
||||||
let clientSecret: string | undefined
|
|
||||||
if (hasSecret) {
|
|
||||||
const secret = await prompts.password({
|
|
||||||
message: "Enter client secret",
|
|
||||||
})
|
|
||||||
if (prompts.isCancel(secret)) throw new UI.CancelledError()
|
|
||||||
clientSecret = secret
|
|
||||||
}
|
}
|
||||||
|
|
||||||
prompts.log.info(`Remote MCP server "${name}" configured with OAuth (client ID: ${clientId})`)
|
await addMcpToConfig(name, mcpConfig, configPath)
|
||||||
prompts.log.info("Add this to your opencode.json:")
|
prompts.log.success(`MCP server "${name}" added to ${configPath}`)
|
||||||
prompts.log.info(`
|
prompts.outro("MCP server added successfully")
|
||||||
"mcp": {
|
return
|
||||||
"${name}": {
|
|
||||||
"type": "remote",
|
|
||||||
"url": "${url}",
|
|
||||||
"oauth": {
|
|
||||||
"clientId": "${clientId}"${clientSecret ? `,\n "clientSecret": "${clientSecret}"` : ""}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
} else {
|
|
||||||
prompts.log.info(`Remote MCP server "${name}" configured with OAuth (dynamic registration)`)
|
|
||||||
prompts.log.info("Add this to your opencode.json:")
|
|
||||||
prompts.log.info(`
|
|
||||||
"mcp": {
|
|
||||||
"${name}": {
|
|
||||||
"type": "remote",
|
|
||||||
"url": "${url}",
|
|
||||||
"oauth": {}
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
const client = new Client({
|
|
||||||
name: "opencode",
|
|
||||||
version: "1.0.0",
|
|
||||||
})
|
|
||||||
const transport = new StreamableHTTPClientTransport(new URL(url))
|
|
||||||
await client.connect(transport)
|
|
||||||
prompts.log.info(`Remote MCP server "${name}" configured with URL: ${url}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
prompts.outro("MCP server added successfully")
|
if (type === "remote") {
|
||||||
|
const url = await prompts.text({
|
||||||
|
message: "Enter MCP server URL",
|
||||||
|
placeholder: "e.g., https://example.com/mcp",
|
||||||
|
validate: (x) => {
|
||||||
|
if (!x) return "Required"
|
||||||
|
if (x.length === 0) return "Required"
|
||||||
|
const isValid = URL.canParse(x)
|
||||||
|
return isValid ? undefined : "Invalid URL"
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (prompts.isCancel(url)) throw new UI.CancelledError()
|
||||||
|
|
||||||
|
const useOAuth = await prompts.confirm({
|
||||||
|
message: "Does this server require OAuth authentication?",
|
||||||
|
initialValue: false,
|
||||||
|
})
|
||||||
|
if (prompts.isCancel(useOAuth)) throw new UI.CancelledError()
|
||||||
|
|
||||||
|
let mcpConfig: Config.Mcp
|
||||||
|
|
||||||
|
if (useOAuth) {
|
||||||
|
const hasClientId = await prompts.confirm({
|
||||||
|
message: "Do you have a pre-registered client ID?",
|
||||||
|
initialValue: false,
|
||||||
|
})
|
||||||
|
if (prompts.isCancel(hasClientId)) throw new UI.CancelledError()
|
||||||
|
|
||||||
|
if (hasClientId) {
|
||||||
|
const clientId = await prompts.text({
|
||||||
|
message: "Enter client ID",
|
||||||
|
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
||||||
|
})
|
||||||
|
if (prompts.isCancel(clientId)) throw new UI.CancelledError()
|
||||||
|
|
||||||
|
const hasSecret = await prompts.confirm({
|
||||||
|
message: "Do you have a client secret?",
|
||||||
|
initialValue: false,
|
||||||
|
})
|
||||||
|
if (prompts.isCancel(hasSecret)) throw new UI.CancelledError()
|
||||||
|
|
||||||
|
let clientSecret: string | undefined
|
||||||
|
if (hasSecret) {
|
||||||
|
const secret = await prompts.password({
|
||||||
|
message: "Enter client secret",
|
||||||
|
})
|
||||||
|
if (prompts.isCancel(secret)) throw new UI.CancelledError()
|
||||||
|
clientSecret = secret
|
||||||
|
}
|
||||||
|
|
||||||
|
mcpConfig = {
|
||||||
|
type: "remote",
|
||||||
|
url,
|
||||||
|
oauth: {
|
||||||
|
clientId,
|
||||||
|
...(clientSecret && { clientSecret }),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mcpConfig = {
|
||||||
|
type: "remote",
|
||||||
|
url,
|
||||||
|
oauth: {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mcpConfig = {
|
||||||
|
type: "remote",
|
||||||
|
url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await addMcpToConfig(name, mcpConfig, configPath)
|
||||||
|
prompts.log.success(`MCP server "${name}" added to ${configPath}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
prompts.outro("MCP server added successfully")
|
||||||
|
},
|
||||||
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -26,67 +26,82 @@ export function createDialogProviderOptions() {
|
|||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
|
const connected = createMemo(() => new Set(sync.data.provider_next.connected))
|
||||||
const options = createMemo(() => {
|
const options = createMemo(() => {
|
||||||
return pipe(
|
return pipe(
|
||||||
sync.data.provider_next.all,
|
sync.data.provider_next.all,
|
||||||
sortBy((x) => PROVIDER_PRIORITY[x.id] ?? 99),
|
sortBy((x) => PROVIDER_PRIORITY[x.id] ?? 99),
|
||||||
map((provider) => ({
|
map((provider) => {
|
||||||
title: provider.name,
|
const isConnected = connected().has(provider.id)
|
||||||
value: provider.id,
|
return {
|
||||||
description: {
|
title: provider.name,
|
||||||
opencode: "(Recommended)",
|
value: provider.id,
|
||||||
anthropic: "(Claude Max or API key)",
|
description: {
|
||||||
openai: "(ChatGPT Plus/Pro or API key)",
|
opencode: "(Recommended)",
|
||||||
}[provider.id],
|
anthropic: "(Claude Max or API key)",
|
||||||
category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other",
|
openai: "(ChatGPT Plus/Pro or API key)",
|
||||||
async onSelect() {
|
}[provider.id],
|
||||||
const methods = sync.data.provider_auth[provider.id] ?? [
|
category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other",
|
||||||
{
|
footer: isConnected ? "Connected" : undefined,
|
||||||
type: "api",
|
async onSelect() {
|
||||||
label: "API key",
|
const methods = sync.data.provider_auth[provider.id] ?? [
|
||||||
},
|
{
|
||||||
]
|
type: "api",
|
||||||
let index: number | null = 0
|
label: "API key",
|
||||||
if (methods.length > 1) {
|
},
|
||||||
index = await new Promise<number | null>((resolve) => {
|
]
|
||||||
dialog.replace(
|
let index: number | null = 0
|
||||||
() => (
|
if (methods.length > 1) {
|
||||||
<DialogSelect
|
index = await new Promise<number | null>((resolve) => {
|
||||||
title="Select auth method"
|
dialog.replace(
|
||||||
options={methods.map((x, index) => ({
|
() => (
|
||||||
title: x.label,
|
<DialogSelect
|
||||||
value: index,
|
title="Select auth method"
|
||||||
}))}
|
options={methods.map((x, index) => ({
|
||||||
onSelect={(option) => resolve(option.value)}
|
title: x.label,
|
||||||
|
value: index,
|
||||||
|
}))}
|
||||||
|
onSelect={(option) => resolve(option.value)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
() => resolve(null),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (index == null) return
|
||||||
|
const method = methods[index]
|
||||||
|
if (method.type === "oauth") {
|
||||||
|
const result = await sdk.client.provider.oauth.authorize({
|
||||||
|
providerID: provider.id,
|
||||||
|
method: index,
|
||||||
|
})
|
||||||
|
if (result.data?.method === "code") {
|
||||||
|
dialog.replace(() => (
|
||||||
|
<CodeMethod
|
||||||
|
providerID={provider.id}
|
||||||
|
title={method.label}
|
||||||
|
index={index}
|
||||||
|
authorization={result.data!}
|
||||||
/>
|
/>
|
||||||
),
|
))
|
||||||
() => resolve(null),
|
}
|
||||||
)
|
if (result.data?.method === "auto") {
|
||||||
})
|
dialog.replace(() => (
|
||||||
}
|
<AutoMethod
|
||||||
if (index == null) return
|
providerID={provider.id}
|
||||||
const method = methods[index]
|
title={method.label}
|
||||||
if (method.type === "oauth") {
|
index={index}
|
||||||
const result = await sdk.client.provider.oauth.authorize({
|
authorization={result.data!}
|
||||||
providerID: provider.id,
|
/>
|
||||||
method: index,
|
))
|
||||||
})
|
}
|
||||||
if (result.data?.method === "code") {
|
|
||||||
dialog.replace(() => (
|
|
||||||
<CodeMethod providerID={provider.id} title={method.label} index={index} authorization={result.data!} />
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
if (result.data?.method === "auto") {
|
if (method.type === "api") {
|
||||||
dialog.replace(() => (
|
return dialog.replace(() => <ApiMethod providerID={provider.id} title={method.label} />)
|
||||||
<AutoMethod providerID={provider.id} title={method.label} index={index} authorization={result.data!} />
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
if (method.type === "api") {
|
}
|
||||||
return dialog.replace(() => <ApiMethod providerID={provider.id} title={method.label} />)
|
}),
|
||||||
}
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
return options
|
return options
|
||||||
@@ -225,7 +240,7 @@ function ApiMethod(props: ApiMethodProps) {
|
|||||||
}
|
}
|
||||||
onConfirm={async (value) => {
|
onConfirm={async (value) => {
|
||||||
if (!value) return
|
if (!value) return
|
||||||
sdk.client.auth.set({
|
await sdk.client.auth.set({
|
||||||
providerID: props.providerID,
|
providerID: props.providerID,
|
||||||
auth: {
|
auth: {
|
||||||
type: "api",
|
type: "api",
|
||||||
|
|||||||
@@ -159,6 +159,26 @@ export function Autocomplete(props: {
|
|||||||
})
|
})
|
||||||
|
|
||||||
props.setPrompt((draft) => {
|
props.setPrompt((draft) => {
|
||||||
|
if (part.type === "file") {
|
||||||
|
const existingIndex = draft.parts.findIndex((p) => p.type === "file" && "url" in p && p.url === part.url)
|
||||||
|
if (existingIndex !== -1) {
|
||||||
|
const existing = draft.parts[existingIndex]
|
||||||
|
if (
|
||||||
|
part.source?.text &&
|
||||||
|
existing &&
|
||||||
|
"source" in existing &&
|
||||||
|
existing.source &&
|
||||||
|
"text" in existing.source &&
|
||||||
|
existing.source.text
|
||||||
|
) {
|
||||||
|
existing.source.text.start = extmarkStart
|
||||||
|
existing.source.text.end = extmarkEnd
|
||||||
|
existing.source.text.value = virtualText
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (part.type === "file" && part.source?.text) {
|
if (part.type === "file" && part.source?.text) {
|
||||||
part.source.text.start = extmarkStart
|
part.source.text.start = extmarkStart
|
||||||
part.source.text.end = extmarkEnd
|
part.source.text.end = extmarkEnd
|
||||||
|
|||||||
@@ -563,25 +563,27 @@ export function Prompt(props: PromptProps) {
|
|||||||
})),
|
})),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
sdk.client.session.prompt({
|
sdk.client.session
|
||||||
sessionID,
|
.prompt({
|
||||||
...selectedModel,
|
sessionID,
|
||||||
messageID,
|
...selectedModel,
|
||||||
agent: local.agent.current().name,
|
messageID,
|
||||||
model: selectedModel,
|
agent: local.agent.current().name,
|
||||||
variant,
|
model: selectedModel,
|
||||||
parts: [
|
variant,
|
||||||
{
|
parts: [
|
||||||
id: Identifier.ascending("part"),
|
{
|
||||||
type: "text",
|
id: Identifier.ascending("part"),
|
||||||
text: inputText,
|
type: "text",
|
||||||
},
|
text: inputText,
|
||||||
...nonTextParts.map((x) => ({
|
},
|
||||||
id: Identifier.ascending("part"),
|
...nonTextParts.map((x) => ({
|
||||||
...x,
|
id: Identifier.ascending("part"),
|
||||||
})),
|
...x,
|
||||||
],
|
})),
|
||||||
})
|
],
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
}
|
}
|
||||||
history.append({
|
history.append({
|
||||||
...store.prompt,
|
...store.prompt,
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ const TIPS = [
|
|||||||
"Press {highlight}Ctrl+X S{/highlight} or {highlight}/status{/highlight} to see system status info",
|
"Press {highlight}Ctrl+X S{/highlight} or {highlight}/status{/highlight} to see system status info",
|
||||||
"Enable {highlight}tui.scroll_acceleration{/highlight} for smooth macOS-style scrolling",
|
"Enable {highlight}tui.scroll_acceleration{/highlight} for smooth macOS-style scrolling",
|
||||||
"Toggle username display in chat via command palette ({highlight}Ctrl+P{/highlight})",
|
"Toggle username display in chat via command palette ({highlight}Ctrl+P{/highlight})",
|
||||||
"Run {highlight}docker run -it --rm ghcr.io/sst/opencode{/highlight} for containerized use",
|
"Run {highlight}docker run -it --rm ghcr.io/anomalyco/opencode{/highlight} for containerized use",
|
||||||
"Use {highlight}/connect{/highlight} with OpenCode Zen for curated, tested models",
|
"Use {highlight}/connect{/highlight} with OpenCode Zen for curated, tested models",
|
||||||
"Commit your project's {highlight}AGENTS.md{/highlight} file to Git for team sharing",
|
"Commit your project's {highlight}AGENTS.md{/highlight} file to Git for team sharing",
|
||||||
"Use {highlight}/review{/highlight} to review uncommitted changes, branches, or PRs",
|
"Use {highlight}/review{/highlight} to review uncommitted changes, branches, or PRs",
|
||||||
|
|||||||
@@ -25,24 +25,27 @@ export function Footer() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
// Track all timeouts to ensure proper cleanup
|
||||||
|
const timeouts: ReturnType<typeof setTimeout>[] = []
|
||||||
|
|
||||||
function tick() {
|
function tick() {
|
||||||
if (connected()) return
|
if (connected()) return
|
||||||
if (!store.welcome) {
|
if (!store.welcome) {
|
||||||
setStore("welcome", true)
|
setStore("welcome", true)
|
||||||
timeout = setTimeout(() => tick(), 5000)
|
timeouts.push(setTimeout(() => tick(), 5000))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (store.welcome) {
|
if (store.welcome) {
|
||||||
setStore("welcome", false)
|
setStore("welcome", false)
|
||||||
timeout = setTimeout(() => tick(), 10_000)
|
timeouts.push(setTimeout(() => tick(), 10_000))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let timeout = setTimeout(() => tick(), 10_000)
|
timeouts.push(setTimeout(() => tick(), 10_000))
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
clearTimeout(timeout)
|
timeouts.forEach(clearTimeout)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ import { Footer } from "./footer.tsx"
|
|||||||
import { usePromptRef } from "../../context/prompt"
|
import { usePromptRef } from "../../context/prompt"
|
||||||
import { useExit } from "../../context/exit"
|
import { useExit } from "../../context/exit"
|
||||||
import { Filesystem } from "@/util/filesystem"
|
import { Filesystem } from "@/util/filesystem"
|
||||||
|
import { Global } from "@/global"
|
||||||
import { PermissionPrompt } from "./permission"
|
import { PermissionPrompt } from "./permission"
|
||||||
import { QuestionPrompt } from "./question"
|
import { QuestionPrompt } from "./question"
|
||||||
import { DialogExportOptions } from "../../ui/dialog-export-options"
|
import { DialogExportOptions } from "../../ui/dialog-export-options"
|
||||||
@@ -195,6 +196,23 @@ export function Session() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let lastSwitch: string | undefined = undefined
|
||||||
|
sdk.event.on("message.part.updated", (evt) => {
|
||||||
|
const part = evt.properties.part
|
||||||
|
if (part.type !== "tool") return
|
||||||
|
if (part.sessionID !== route.sessionID) return
|
||||||
|
if (part.state.status !== "completed") return
|
||||||
|
if (part.id === lastSwitch) return
|
||||||
|
|
||||||
|
if (part.tool === "plan_exit") {
|
||||||
|
local.agent.set("build")
|
||||||
|
lastSwitch = part.id
|
||||||
|
} else if (part.tool === "plan_enter") {
|
||||||
|
local.agent.set("plan")
|
||||||
|
lastSwitch = part.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
let scroll: ScrollBoxRenderable
|
let scroll: ScrollBoxRenderable
|
||||||
let prompt: PromptRef
|
let prompt: PromptRef
|
||||||
const keybind = useKeybind()
|
const keybind = useKeybind()
|
||||||
@@ -1525,6 +1543,7 @@ function BlockTool(props: { title: string; children: JSX.Element; onClick?: () =
|
|||||||
|
|
||||||
function Bash(props: ToolProps<typeof BashTool>) {
|
function Bash(props: ToolProps<typeof BashTool>) {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
const sync = useSync()
|
||||||
const output = createMemo(() => stripAnsi(props.metadata.output?.trim() ?? ""))
|
const output = createMemo(() => stripAnsi(props.metadata.output?.trim() ?? ""))
|
||||||
const [expanded, setExpanded] = createSignal(false)
|
const [expanded, setExpanded] = createSignal(false)
|
||||||
const lines = createMemo(() => output().split("\n"))
|
const lines = createMemo(() => output().split("\n"))
|
||||||
@@ -1534,11 +1553,36 @@ function Bash(props: ToolProps<typeof BashTool>) {
|
|||||||
return [...lines().slice(0, 10), "…"].join("\n")
|
return [...lines().slice(0, 10), "…"].join("\n")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const workdirDisplay = createMemo(() => {
|
||||||
|
const workdir = props.input.workdir
|
||||||
|
if (!workdir || workdir === ".") return undefined
|
||||||
|
|
||||||
|
const base = sync.data.path.directory
|
||||||
|
if (!base) return undefined
|
||||||
|
|
||||||
|
const absolute = path.resolve(base, workdir)
|
||||||
|
if (absolute === base) return undefined
|
||||||
|
|
||||||
|
const home = Global.Path.home
|
||||||
|
if (!home) return absolute
|
||||||
|
|
||||||
|
const match = absolute === home || absolute.startsWith(home + path.sep)
|
||||||
|
return match ? absolute.replace(home, "~") : absolute
|
||||||
|
})
|
||||||
|
|
||||||
|
const title = createMemo(() => {
|
||||||
|
const desc = props.input.description ?? "Shell"
|
||||||
|
const wd = workdirDisplay()
|
||||||
|
if (!wd) return `# ${desc}`
|
||||||
|
if (desc.includes(wd)) return `# ${desc}`
|
||||||
|
return `# ${desc} in ${wd}`
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.metadata.output !== undefined}>
|
<Match when={props.metadata.output !== undefined}>
|
||||||
<BlockTool
|
<BlockTool
|
||||||
title={"# " + (props.input.description ?? "Shell")}
|
title={title()}
|
||||||
part={props.part}
|
part={props.part}
|
||||||
onClick={overflow() ? () => setExpanded((prev) => !prev) : undefined}
|
onClick={overflow() ? () => setExpanded((prev) => !prev) : undefined}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { createMemo, For, Match, Show, Switch } from "solid-js"
|
import { createMemo, For, Match, Show, Switch } from "solid-js"
|
||||||
import { useKeyboard, useTerminalDimensions, type JSX } from "@opentui/solid"
|
import { Portal, useKeyboard, useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid"
|
||||||
import type { TextareaRenderable } from "@opentui/core"
|
import type { TextareaRenderable } from "@opentui/core"
|
||||||
import { useKeybind } from "../../context/keybind"
|
import { useKeybind } from "../../context/keybind"
|
||||||
import { useTheme, selectedForeground } from "../../context/theme"
|
import { useTheme, selectedForeground } from "../../context/theme"
|
||||||
@@ -11,16 +11,28 @@ import { useSync } from "../../context/sync"
|
|||||||
import { useTextareaKeybindings } from "../../component/textarea-keybindings"
|
import { useTextareaKeybindings } from "../../component/textarea-keybindings"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
|
import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
|
||||||
|
import { Keybind } from "@/util/keybind"
|
||||||
import { Locale } from "@/util/locale"
|
import { Locale } from "@/util/locale"
|
||||||
|
import { Global } from "@/global"
|
||||||
|
|
||||||
type PermissionStage = "permission" | "always" | "reject"
|
type PermissionStage = "permission" | "always" | "reject"
|
||||||
|
|
||||||
function normalizePath(input?: string) {
|
function normalizePath(input?: string) {
|
||||||
if (!input) return ""
|
if (!input) return ""
|
||||||
if (path.isAbsolute(input)) {
|
|
||||||
return path.relative(process.cwd(), input) || "."
|
const cwd = process.cwd()
|
||||||
|
const home = Global.Path.home
|
||||||
|
const absolute = path.isAbsolute(input) ? input : path.resolve(cwd, input)
|
||||||
|
const relative = path.relative(cwd, absolute)
|
||||||
|
|
||||||
|
if (!relative) return "."
|
||||||
|
if (!relative.startsWith("..")) return relative
|
||||||
|
|
||||||
|
// outside cwd - use ~ or absolute
|
||||||
|
if (home && (absolute === home || absolute.startsWith(home + path.sep))) {
|
||||||
|
return absolute.replace(home, "~")
|
||||||
}
|
}
|
||||||
return input
|
return absolute
|
||||||
}
|
}
|
||||||
|
|
||||||
function filetype(input?: string) {
|
function filetype(input?: string) {
|
||||||
@@ -32,7 +44,9 @@ function filetype(input?: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function EditBody(props: { request: PermissionRequest }) {
|
function EditBody(props: { request: PermissionRequest }) {
|
||||||
const { theme, syntax } = useTheme()
|
const themeState = useTheme()
|
||||||
|
const theme = themeState.theme
|
||||||
|
const syntax = themeState.syntax
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const dimensions = useTerminalDimensions()
|
const dimensions = useTerminalDimensions()
|
||||||
|
|
||||||
@@ -54,7 +68,7 @@ function EditBody(props: { request: PermissionRequest }) {
|
|||||||
<text fg={theme.textMuted}>Edit {normalizePath(filepath())}</text>
|
<text fg={theme.textMuted}>Edit {normalizePath(filepath())}</text>
|
||||||
</box>
|
</box>
|
||||||
<Show when={diff()}>
|
<Show when={diff()}>
|
||||||
<box maxHeight={Math.floor(dimensions().height / 4)} overflow="scroll">
|
<scrollbox height="100%">
|
||||||
<diff
|
<diff
|
||||||
diff={diff()}
|
diff={diff()}
|
||||||
view={view()}
|
view={view()}
|
||||||
@@ -74,7 +88,7 @@ function EditBody(props: { request: PermissionRequest }) {
|
|||||||
addedLineNumberBg={theme.diffAddedLineNumberBg}
|
addedLineNumberBg={theme.diffAddedLineNumberBg}
|
||||||
removedLineNumberBg={theme.diffRemovedLineNumberBg}
|
removedLineNumberBg={theme.diffRemovedLineNumberBg}
|
||||||
/>
|
/>
|
||||||
</box>
|
</scrollbox>
|
||||||
</Show>
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
@@ -172,86 +186,111 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
|
|||||||
message: message || undefined,
|
message: message || undefined,
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
onCancel={() => setStore("stage", "permission")}
|
onCancel={() => {
|
||||||
|
setStore("stage", "permission")
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={store.stage === "permission"}>
|
<Match when={store.stage === "permission"}>
|
||||||
<Prompt
|
{(() => {
|
||||||
title="Permission required"
|
const body = (
|
||||||
body={
|
<Prompt
|
||||||
<Switch>
|
title="Permission required"
|
||||||
<Match when={props.request.permission === "edit"}>
|
body={
|
||||||
<EditBody request={props.request} />
|
<Switch>
|
||||||
</Match>
|
<Match when={props.request.permission === "edit"}>
|
||||||
<Match when={props.request.permission === "read"}>
|
<EditBody request={props.request} />
|
||||||
<TextBody icon="→" title={`Read ` + normalizePath(input().filePath as string)} />
|
</Match>
|
||||||
</Match>
|
<Match when={props.request.permission === "read"}>
|
||||||
<Match when={props.request.permission === "glob"}>
|
<TextBody icon="→" title={`Read ` + normalizePath(input().filePath as string)} />
|
||||||
<TextBody icon="✱" title={`Glob "` + (input().pattern ?? "") + `"`} />
|
</Match>
|
||||||
</Match>
|
<Match when={props.request.permission === "glob"}>
|
||||||
<Match when={props.request.permission === "grep"}>
|
<TextBody icon="✱" title={`Glob "` + (input().pattern ?? "") + `"`} />
|
||||||
<TextBody icon="✱" title={`Grep "` + (input().pattern ?? "") + `"`} />
|
</Match>
|
||||||
</Match>
|
<Match when={props.request.permission === "grep"}>
|
||||||
<Match when={props.request.permission === "list"}>
|
<TextBody icon="✱" title={`Grep "` + (input().pattern ?? "") + `"`} />
|
||||||
<TextBody icon="→" title={`List ` + normalizePath(input().path as string)} />
|
</Match>
|
||||||
</Match>
|
<Match when={props.request.permission === "list"}>
|
||||||
<Match when={props.request.permission === "bash"}>
|
<TextBody icon="→" title={`List ` + normalizePath(input().path as string)} />
|
||||||
<TextBody
|
</Match>
|
||||||
icon="#"
|
<Match when={props.request.permission === "bash"}>
|
||||||
title={(input().description as string) ?? ""}
|
<TextBody
|
||||||
description={("$ " + input().command) as string}
|
icon="#"
|
||||||
/>
|
title={(input().description as string) ?? ""}
|
||||||
</Match>
|
description={("$ " + input().command) as string}
|
||||||
<Match when={props.request.permission === "task"}>
|
/>
|
||||||
<TextBody
|
</Match>
|
||||||
icon="#"
|
<Match when={props.request.permission === "task"}>
|
||||||
title={`${Locale.titlecase((input().subagent_type as string) ?? "Unknown")} Task`}
|
<TextBody
|
||||||
description={"◉ " + input().description}
|
icon="#"
|
||||||
/>
|
title={`${Locale.titlecase((input().subagent_type as string) ?? "Unknown")} Task`}
|
||||||
</Match>
|
description={"◉ " + input().description}
|
||||||
<Match when={props.request.permission === "webfetch"}>
|
/>
|
||||||
<TextBody icon="%" title={`WebFetch ` + (input().url ?? "")} />
|
</Match>
|
||||||
</Match>
|
<Match when={props.request.permission === "webfetch"}>
|
||||||
<Match when={props.request.permission === "websearch"}>
|
<TextBody icon="%" title={`WebFetch ` + (input().url ?? "")} />
|
||||||
<TextBody icon="◈" title={`Exa Web Search "` + (input().query ?? "") + `"`} />
|
</Match>
|
||||||
</Match>
|
<Match when={props.request.permission === "websearch"}>
|
||||||
<Match when={props.request.permission === "codesearch"}>
|
<TextBody icon="◈" title={`Exa Web Search "` + (input().query ?? "") + `"`} />
|
||||||
<TextBody icon="◇" title={`Exa Code Search "` + (input().query ?? "") + `"`} />
|
</Match>
|
||||||
</Match>
|
<Match when={props.request.permission === "codesearch"}>
|
||||||
<Match when={props.request.permission === "external_directory"}>
|
<TextBody icon="◇" title={`Exa Code Search "` + (input().query ?? "") + `"`} />
|
||||||
<TextBody icon="←" title={`Access external directory ` + normalizePath(input().path as string)} />
|
</Match>
|
||||||
</Match>
|
<Match when={props.request.permission === "external_directory"}>
|
||||||
<Match when={props.request.permission === "doom_loop"}>
|
{(() => {
|
||||||
<TextBody icon="⟳" title="Continue after repeated failures" />
|
const meta = props.request.metadata ?? {}
|
||||||
</Match>
|
const parent = typeof meta["parentDir"] === "string" ? meta["parentDir"] : undefined
|
||||||
<Match when={true}>
|
const filepath = typeof meta["filepath"] === "string" ? meta["filepath"] : undefined
|
||||||
<TextBody icon="⚙" title={`Call tool ` + props.request.permission} />
|
const pattern = props.request.patterns?.[0]
|
||||||
</Match>
|
const derived =
|
||||||
</Switch>
|
typeof pattern === "string"
|
||||||
}
|
? pattern.includes("*")
|
||||||
options={{ once: "Allow once", always: "Allow always", reject: "Reject" }}
|
? path.dirname(pattern)
|
||||||
escapeKey="reject"
|
: pattern
|
||||||
onSelect={(option) => {
|
: undefined
|
||||||
if (option === "always") {
|
|
||||||
setStore("stage", "always")
|
const raw = parent ?? filepath ?? derived
|
||||||
return
|
const dir = normalizePath(raw)
|
||||||
}
|
|
||||||
if (option === "reject") {
|
return <TextBody icon="←" title={`Access external directory ` + dir} />
|
||||||
if (session()?.parentID) {
|
})()}
|
||||||
setStore("stage", "reject")
|
</Match>
|
||||||
return
|
<Match when={props.request.permission === "doom_loop"}>
|
||||||
|
<TextBody icon="⟳" title="Continue after repeated failures" />
|
||||||
|
</Match>
|
||||||
|
<Match when={true}>
|
||||||
|
<TextBody icon="⚙" title={`Call tool ` + props.request.permission} />
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
}
|
}
|
||||||
sdk.client.permission.reply({
|
options={{ once: "Allow once", always: "Allow always", reject: "Reject" }}
|
||||||
reply: "reject",
|
escapeKey="reject"
|
||||||
requestID: props.request.id,
|
fullscreen
|
||||||
})
|
onSelect={(option) => {
|
||||||
}
|
if (option === "always") {
|
||||||
sdk.client.permission.reply({
|
setStore("stage", "always")
|
||||||
reply: "once",
|
return
|
||||||
requestID: props.request.id,
|
}
|
||||||
})
|
if (option === "reject") {
|
||||||
}}
|
if (session()?.parentID) {
|
||||||
/>
|
setStore("stage", "reject")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sdk.client.permission.reply({
|
||||||
|
reply: "reject",
|
||||||
|
requestID: props.request.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sdk.client.permission.reply({
|
||||||
|
reply: "once",
|
||||||
|
requestID: props.request.id,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
return body
|
||||||
|
})()}
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
)
|
)
|
||||||
@@ -327,14 +366,18 @@ function Prompt<const T extends Record<string, string>>(props: {
|
|||||||
body: JSX.Element
|
body: JSX.Element
|
||||||
options: T
|
options: T
|
||||||
escapeKey?: keyof T
|
escapeKey?: keyof T
|
||||||
|
fullscreen?: boolean
|
||||||
onSelect: (option: keyof T) => void
|
onSelect: (option: keyof T) => void
|
||||||
}) {
|
}) {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const keybind = useKeybind()
|
const keybind = useKeybind()
|
||||||
|
const dimensions = useTerminalDimensions()
|
||||||
const keys = Object.keys(props.options) as (keyof T)[]
|
const keys = Object.keys(props.options) as (keyof T)[]
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
selected: keys[0],
|
selected: keys[0],
|
||||||
|
expanded: false,
|
||||||
})
|
})
|
||||||
|
const diffKey = Keybind.parse("ctrl+f")[0]
|
||||||
|
|
||||||
useKeyboard((evt) => {
|
useKeyboard((evt) => {
|
||||||
if (evt.name === "left" || evt.name == "h") {
|
if (evt.name === "left" || evt.name == "h") {
|
||||||
@@ -360,17 +403,36 @@ function Prompt<const T extends Record<string, string>>(props: {
|
|||||||
evt.preventDefault()
|
evt.preventDefault()
|
||||||
props.onSelect(props.escapeKey)
|
props.onSelect(props.escapeKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (props.fullscreen && diffKey && Keybind.match(diffKey, keybind.parse(evt))) {
|
||||||
|
evt.preventDefault()
|
||||||
|
evt.stopPropagation()
|
||||||
|
setStore("expanded", (v) => !v)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
const hint = createMemo(() => (store.expanded ? "minimize" : "fullscreen"))
|
||||||
|
const renderer = useRenderer()
|
||||||
|
|
||||||
|
const content = () => (
|
||||||
<box
|
<box
|
||||||
backgroundColor={theme.backgroundPanel}
|
backgroundColor={theme.backgroundPanel}
|
||||||
border={["left"]}
|
border={["left"]}
|
||||||
borderColor={theme.warning}
|
borderColor={theme.warning}
|
||||||
customBorderChars={SplitBorder.customBorderChars}
|
customBorderChars={SplitBorder.customBorderChars}
|
||||||
|
{...(store.expanded
|
||||||
|
? { top: dimensions().height * -1 + 1, bottom: 1, left: 2, right: 2, position: "absolute" }
|
||||||
|
: {
|
||||||
|
top: 0,
|
||||||
|
maxHeight: 15,
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
position: "relative",
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
<box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1}>
|
<box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1} flexGrow={1}>
|
||||||
<box flexDirection="row" gap={1} paddingLeft={1}>
|
<box flexDirection="row" gap={1} paddingLeft={1} flexShrink={0}>
|
||||||
<text fg={theme.warning}>{"△"}</text>
|
<text fg={theme.warning}>{"△"}</text>
|
||||||
<text fg={theme.text}>{props.title}</text>
|
<text fg={theme.text}>{props.title}</text>
|
||||||
</box>
|
</box>
|
||||||
@@ -403,6 +465,11 @@ function Prompt<const T extends Record<string, string>>(props: {
|
|||||||
</For>
|
</For>
|
||||||
</box>
|
</box>
|
||||||
<box flexDirection="row" gap={2}>
|
<box flexDirection="row" gap={2}>
|
||||||
|
<Show when={props.fullscreen}>
|
||||||
|
<text fg={theme.text}>
|
||||||
|
{"ctrl+f"} <span style={{ fg: theme.textMuted }}>{hint()}</span>
|
||||||
|
</text>
|
||||||
|
</Show>
|
||||||
<text fg={theme.text}>
|
<text fg={theme.text}>
|
||||||
{"⇆"} <span style={{ fg: theme.textMuted }}>select</span>
|
{"⇆"} <span style={{ fg: theme.textMuted }}>select</span>
|
||||||
</text>
|
</text>
|
||||||
@@ -413,4 +480,10 @@ function Prompt<const T extends Record<string, string>>(props: {
|
|||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show when={!store.expanded} fallback={<Portal>{content()}</Portal>}>
|
||||||
|
{content()}
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
|
|||||||
const question = createMemo(() => questions()[store.tab])
|
const question = createMemo(() => questions()[store.tab])
|
||||||
const confirm = createMemo(() => !single() && store.tab === questions().length)
|
const confirm = createMemo(() => !single() && store.tab === questions().length)
|
||||||
const options = createMemo(() => question()?.options ?? [])
|
const options = createMemo(() => question()?.options ?? [])
|
||||||
const other = createMemo(() => store.selected === options().length)
|
const custom = createMemo(() => question()?.custom !== false)
|
||||||
|
const other = createMemo(() => custom() && store.selected === options().length)
|
||||||
const input = createMemo(() => store.custom[store.tab] ?? "")
|
const input = createMemo(() => store.custom[store.tab] ?? "")
|
||||||
const multi = createMemo(() => question()?.multiple === true)
|
const multi = createMemo(() => question()?.multiple === true)
|
||||||
const customPicked = createMemo(() => {
|
const customPicked = createMemo(() => {
|
||||||
@@ -121,6 +122,9 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
|
|||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
|
|
||||||
useKeyboard((evt) => {
|
useKeyboard((evt) => {
|
||||||
|
// Skip processing if a dialog (e.g., command palette) is open
|
||||||
|
if (dialog.stack.length > 0) return
|
||||||
|
|
||||||
// When editing "Other" textarea
|
// When editing "Other" textarea
|
||||||
if (store.editing && !confirm()) {
|
if (store.editing && !confirm()) {
|
||||||
if (evt.name === "escape") {
|
if (evt.name === "escape") {
|
||||||
@@ -200,7 +204,7 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const opts = options()
|
const opts = options()
|
||||||
const total = opts.length + 1 // options + "Other"
|
const total = opts.length + (custom() ? 1 : 0)
|
||||||
|
|
||||||
if (evt.name === "up" || evt.name === "k") {
|
if (evt.name === "up" || evt.name === "k") {
|
||||||
evt.preventDefault()
|
evt.preventDefault()
|
||||||
@@ -295,35 +299,37 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</For>
|
</For>
|
||||||
<box onMouseOver={() => moveTo(options().length)} onMouseUp={() => selectOption()}>
|
<Show when={custom()}>
|
||||||
<box flexDirection="row" gap={1}>
|
<box onMouseOver={() => moveTo(options().length)} onMouseUp={() => selectOption()}>
|
||||||
<box backgroundColor={other() ? theme.backgroundElement : undefined}>
|
<box flexDirection="row" gap={1}>
|
||||||
<text fg={other() ? theme.secondary : customPicked() ? theme.success : theme.text}>
|
<box backgroundColor={other() ? theme.backgroundElement : undefined}>
|
||||||
{options().length + 1}. Type your own answer
|
<text fg={other() ? theme.secondary : customPicked() ? theme.success : theme.text}>
|
||||||
</text>
|
{options().length + 1}. Type your own answer
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
<text fg={theme.success}>{customPicked() ? "✓" : ""}</text>
|
||||||
</box>
|
</box>
|
||||||
<text fg={theme.success}>{customPicked() ? "✓" : ""}</text>
|
<Show when={store.editing}>
|
||||||
|
<box paddingLeft={3}>
|
||||||
|
<textarea
|
||||||
|
ref={(val: TextareaRenderable) => (textarea = val)}
|
||||||
|
focused
|
||||||
|
initialValue={input()}
|
||||||
|
placeholder="Type your own answer"
|
||||||
|
textColor={theme.text}
|
||||||
|
focusedTextColor={theme.text}
|
||||||
|
cursorColor={theme.primary}
|
||||||
|
keyBindings={bindings()}
|
||||||
|
/>
|
||||||
|
</box>
|
||||||
|
</Show>
|
||||||
|
<Show when={!store.editing && input()}>
|
||||||
|
<box paddingLeft={3}>
|
||||||
|
<text fg={theme.textMuted}>{input()}</text>
|
||||||
|
</box>
|
||||||
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
<Show when={store.editing}>
|
</Show>
|
||||||
<box paddingLeft={3}>
|
|
||||||
<textarea
|
|
||||||
ref={(val: TextareaRenderable) => (textarea = val)}
|
|
||||||
focused
|
|
||||||
initialValue={input()}
|
|
||||||
placeholder="Type your own answer"
|
|
||||||
textColor={theme.text}
|
|
||||||
focusedTextColor={theme.text}
|
|
||||||
cursorColor={theme.primary}
|
|
||||||
keyBindings={bindings()}
|
|
||||||
/>
|
|
||||||
</box>
|
|
||||||
</Show>
|
|
||||||
<Show when={!store.editing && input()}>
|
|
||||||
<box paddingLeft={3}>
|
|
||||||
<text fg={theme.textMuted}>{input()}</text>
|
|
||||||
</box>
|
|
||||||
</Show>
|
|
||||||
</box>
|
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -158,7 +158,8 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|||||||
if (evt.name === "return") {
|
if (evt.name === "return") {
|
||||||
const option = selected()
|
const option = selected()
|
||||||
if (option) {
|
if (option) {
|
||||||
// evt.preventDefault()
|
evt.preventDefault()
|
||||||
|
evt.stopPropagation()
|
||||||
if (option.onSelect) option.onSelect(dialog)
|
if (option.onSelect) option.onSelect(dialog)
|
||||||
props.onSelect?.(option)
|
props.onSelect?.(option)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Config } from "@/config/config"
|
|||||||
import { GlobalBus } from "@/bus/global"
|
import { GlobalBus } from "@/bus/global"
|
||||||
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2"
|
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2"
|
||||||
import type { BunWebSocketData } from "hono/bun"
|
import type { BunWebSocketData } from "hono/bun"
|
||||||
|
import { Flag } from "@/flag/flag"
|
||||||
|
|
||||||
await Log.init({
|
await Log.init({
|
||||||
print: process.argv.includes("--print-logs"),
|
print: process.argv.includes("--print-logs"),
|
||||||
@@ -50,6 +51,8 @@ const startEventStream = (directory: string) => {
|
|||||||
|
|
||||||
const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||||
const request = new Request(input, init)
|
const request = new Request(input, init)
|
||||||
|
const auth = getAuthorizationHeader()
|
||||||
|
if (auth) request.headers.set("Authorization", auth)
|
||||||
return Server.App().fetch(request)
|
return Server.App().fetch(request)
|
||||||
}) as typeof globalThis.fetch
|
}) as typeof globalThis.fetch
|
||||||
|
|
||||||
@@ -95,9 +98,14 @@ startEventStream(process.cwd())
|
|||||||
|
|
||||||
export const rpc = {
|
export const rpc = {
|
||||||
async fetch(input: { url: string; method: string; headers: Record<string, string>; body?: string }) {
|
async fetch(input: { url: string; method: string; headers: Record<string, string>; body?: string }) {
|
||||||
|
const headers = { ...input.headers }
|
||||||
|
const auth = getAuthorizationHeader()
|
||||||
|
if (auth && !headers["authorization"] && !headers["Authorization"]) {
|
||||||
|
headers["Authorization"] = auth
|
||||||
|
}
|
||||||
const request = new Request(input.url, {
|
const request = new Request(input.url, {
|
||||||
method: input.method,
|
method: input.method,
|
||||||
headers: input.headers,
|
headers,
|
||||||
body: input.body,
|
body: input.body,
|
||||||
})
|
})
|
||||||
const response = await Server.App().fetch(request)
|
const response = await Server.App().fetch(request)
|
||||||
@@ -135,3 +143,10 @@ export const rpc = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Rpc.listen(rpc)
|
Rpc.listen(rpc)
|
||||||
|
|
||||||
|
function getAuthorizationHeader(): string | undefined {
|
||||||
|
const password = Flag.OPENCODE_SERVER_PASSWORD
|
||||||
|
if (!password) return undefined
|
||||||
|
const username = Flag.OPENCODE_SERVER_USERNAME ?? "opencode"
|
||||||
|
return `Basic ${btoa(`${username}:${password}`)}`
|
||||||
|
}
|
||||||
|
|||||||
@@ -209,6 +209,19 @@ export namespace Config {
|
|||||||
await BunProc.run(["install"], { cwd: dir }).catch(() => {})
|
await BunProc.run(["install"], { cwd: dir }).catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rel(item: string, patterns: string[]) {
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
const index = item.indexOf(pattern)
|
||||||
|
if (index === -1) continue
|
||||||
|
return item.slice(index + pattern.length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function trim(file: string) {
|
||||||
|
const ext = path.extname(file)
|
||||||
|
return ext.length ? file.slice(0, -ext.length) : file
|
||||||
|
}
|
||||||
|
|
||||||
const COMMAND_GLOB = new Bun.Glob("{command,commands}/**/*.md")
|
const COMMAND_GLOB = new Bun.Glob("{command,commands}/**/*.md")
|
||||||
async function loadCommand(dir: string) {
|
async function loadCommand(dir: string) {
|
||||||
const result: Record<string, Command> = {}
|
const result: Record<string, Command> = {}
|
||||||
@@ -221,16 +234,9 @@ export namespace Config {
|
|||||||
const md = await ConfigMarkdown.parse(item)
|
const md = await ConfigMarkdown.parse(item)
|
||||||
if (!md.data) continue
|
if (!md.data) continue
|
||||||
|
|
||||||
const name = (() => {
|
const patterns = ["/.opencode/command/", "/.opencode/commands/", "/command/", "/commands/"]
|
||||||
const patterns = ["/.opencode/command/", "/command/"]
|
const file = rel(item, patterns) ?? path.basename(item)
|
||||||
const pattern = patterns.find((p) => item.includes(p))
|
const name = trim(file)
|
||||||
|
|
||||||
if (pattern) {
|
|
||||||
const index = item.indexOf(pattern)
|
|
||||||
return item.slice(index + pattern.length, -3)
|
|
||||||
}
|
|
||||||
return path.basename(item, ".md")
|
|
||||||
})()
|
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
name,
|
name,
|
||||||
@@ -260,20 +266,9 @@ export namespace Config {
|
|||||||
const md = await ConfigMarkdown.parse(item)
|
const md = await ConfigMarkdown.parse(item)
|
||||||
if (!md.data) continue
|
if (!md.data) continue
|
||||||
|
|
||||||
// Extract relative path from agent folder for nested agents
|
const patterns = ["/.opencode/agent/", "/.opencode/agents/", "/agent/", "/agents/"]
|
||||||
let agentName = path.basename(item, ".md")
|
const file = rel(item, patterns) ?? path.basename(item)
|
||||||
const agentFolderPath = item.includes("/.opencode/agent/")
|
const agentName = trim(file)
|
||||||
? item.split("/.opencode/agent/")[1]
|
|
||||||
: item.includes("/agent/")
|
|
||||||
? item.split("/agent/")[1]
|
|
||||||
: agentName + ".md"
|
|
||||||
|
|
||||||
// If agent is in a subfolder, include folder path in name
|
|
||||||
if (agentFolderPath.includes("/")) {
|
|
||||||
const relativePath = agentFolderPath.replace(".md", "")
|
|
||||||
const pathParts = relativePath.split("/")
|
|
||||||
agentName = pathParts.slice(0, -1).join("/") + "/" + pathParts[pathParts.length - 1]
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
name: agentName,
|
name: agentName,
|
||||||
@@ -1164,7 +1159,7 @@ export namespace Config {
|
|||||||
if (parsed.success) {
|
if (parsed.success) {
|
||||||
if (!parsed.data.$schema) {
|
if (!parsed.data.$schema) {
|
||||||
parsed.data.$schema = "https://opencode.ai/config.json"
|
parsed.data.$schema = "https://opencode.ai/config.json"
|
||||||
await Bun.write(configFilepath, JSON.stringify(parsed.data, null, 2))
|
await Bun.write(configFilepath, JSON.stringify(parsed.data, null, 2)).catch(() => {})
|
||||||
}
|
}
|
||||||
const data = parsed.data
|
const data = parsed.data
|
||||||
if (data.plugin) {
|
if (data.plugin) {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export namespace Flag {
|
|||||||
export const OPENCODE_EXPERIMENTAL_OXFMT = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_OXFMT")
|
export const OPENCODE_EXPERIMENTAL_OXFMT = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_OXFMT")
|
||||||
export const OPENCODE_EXPERIMENTAL_LSP_TY = truthy("OPENCODE_EXPERIMENTAL_LSP_TY")
|
export const OPENCODE_EXPERIMENTAL_LSP_TY = truthy("OPENCODE_EXPERIMENTAL_LSP_TY")
|
||||||
export const OPENCODE_EXPERIMENTAL_LSP_TOOL = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_LSP_TOOL")
|
export const OPENCODE_EXPERIMENTAL_LSP_TOOL = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_LSP_TOOL")
|
||||||
|
export const OPENCODE_EXPERIMENTAL_PLAN_MODE = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_PLAN_MODE")
|
||||||
|
|
||||||
function truthy(key: string) {
|
function truthy(key: string) {
|
||||||
const value = process.env[key]?.toLowerCase()
|
const value = process.env[key]?.toLowerCase()
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ export const oxfmt: Info = {
|
|||||||
|
|
||||||
export const biome: Info = {
|
export const biome: Info = {
|
||||||
name: "biome",
|
name: "biome",
|
||||||
command: [BunProc.which(), "x", "@biomejs/biome", "format", "--write", "$FILE"],
|
command: [BunProc.which(), "x", "@biomejs/biome", "check", "--write", "$FILE"],
|
||||||
environment: {
|
environment: {
|
||||||
BUN_BE_BUN: "1",
|
BUN_BE_BUN: "1",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ try {
|
|||||||
if (formatted) UI.error(formatted)
|
if (formatted) UI.error(formatted)
|
||||||
if (formatted === undefined) {
|
if (formatted === undefined) {
|
||||||
UI.error("Unexpected error, check log file at " + Log.file() + " for more details" + EOL)
|
UI.error("Unexpected error, check log file at " + Log.file() + " for more details" + EOL)
|
||||||
console.error(e)
|
console.error(e instanceof Error ? e.message : String(e))
|
||||||
}
|
}
|
||||||
process.exitCode = 1
|
process.exitCode = 1
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ export namespace Installation {
|
|||||||
break
|
break
|
||||||
case "brew": {
|
case "brew": {
|
||||||
const formula = await getBrewFormula()
|
const formula = await getBrewFormula()
|
||||||
cmd = $`brew install ${formula}`.env({
|
cmd = $`brew upgrade ${formula}`.env({
|
||||||
HOMEBREW_NO_AUTO_UPDATE: "1",
|
HOMEBREW_NO_AUTO_UPDATE: "1",
|
||||||
...process.env,
|
...process.env,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -266,6 +266,13 @@ export namespace MCP {
|
|||||||
status: s.status,
|
status: s.status,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Close existing client if present to prevent memory leaks
|
||||||
|
const existingClient = s.clients[name]
|
||||||
|
if (existingClient) {
|
||||||
|
await existingClient.close().catch((error) => {
|
||||||
|
log.error("Failed to close existing MCP client", { name, error })
|
||||||
|
})
|
||||||
|
}
|
||||||
s.clients[name] = result.mcpClient
|
s.clients[name] = result.mcpClient
|
||||||
s.status[name] = result.status
|
s.status[name] = result.status
|
||||||
|
|
||||||
@@ -523,6 +530,13 @@ export namespace MCP {
|
|||||||
const s = await state()
|
const s = await state()
|
||||||
s.status[name] = result.status
|
s.status[name] = result.status
|
||||||
if (result.mcpClient) {
|
if (result.mcpClient) {
|
||||||
|
// Close existing client if present to prevent memory leaks
|
||||||
|
const existingClient = s.clients[name]
|
||||||
|
if (existingClient) {
|
||||||
|
await existingClient.close().catch((error) => {
|
||||||
|
log.error("Failed to close existing MCP client", { name, error })
|
||||||
|
})
|
||||||
|
}
|
||||||
s.clients[name] = result.mcpClient
|
s.clients[name] = result.mcpClient
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -387,6 +387,7 @@ export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> {
|
|||||||
headers: {},
|
headers: {},
|
||||||
release_date: "2025-12-18",
|
release_date: "2025-12-18",
|
||||||
variants: {} as Record<string, Record<string, any>>,
|
variants: {} as Record<string, Record<string, any>>,
|
||||||
|
family: "gpt-codex",
|
||||||
}
|
}
|
||||||
model.variants = ProviderTransform.variants(model)
|
model.variants = ProviderTransform.variants(model)
|
||||||
provider.models["gpt-5.2-codex"] = model
|
provider.models["gpt-5.2-codex"] = model
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ import { NamedError } from "@opencode-ai/util/error"
|
|||||||
export namespace Plugin {
|
export namespace Plugin {
|
||||||
const log = Log.create({ service: "plugin" })
|
const log = Log.create({ service: "plugin" })
|
||||||
|
|
||||||
const BUILTIN = ["opencode-copilot-auth@0.0.12", "opencode-anthropic-auth@0.0.8"]
|
const BUILTIN = [
|
||||||
|
"opencode-copilot-auth@0.0.12",
|
||||||
|
"opencode-anthropic-auth@0.0.8",
|
||||||
|
"@gitlab/opencode-gitlab-auth@1.3.0",
|
||||||
|
]
|
||||||
|
|
||||||
// Built-in plugins that are directly imported (not installed from npm)
|
// Built-in plugins that are directly imported (not installed from npm)
|
||||||
const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin]
|
const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin]
|
||||||
@@ -46,6 +50,7 @@ export namespace Plugin {
|
|||||||
if (!Flag.OPENCODE_DISABLE_DEFAULT_PLUGINS) {
|
if (!Flag.OPENCODE_DISABLE_DEFAULT_PLUGINS) {
|
||||||
plugins.push(...BUILTIN)
|
plugins.push(...BUILTIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let plugin of plugins) {
|
for (let plugin of plugins) {
|
||||||
// ignore old codex plugin since it is supported first party now
|
// ignore old codex plugin since it is supported first party now
|
||||||
if (plugin.includes("opencode-openai-codex-auth")) continue
|
if (plugin.includes("opencode-openai-codex-auth")) continue
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export namespace State {
|
|||||||
tasks.push(task)
|
tasks.push(task)
|
||||||
}
|
}
|
||||||
entries.clear()
|
entries.clear()
|
||||||
|
recordsByKey.delete(key)
|
||||||
await Promise.all(tasks)
|
await Promise.all(tasks)
|
||||||
disposalFinished = true
|
disposalFinished = true
|
||||||
log.info("state disposal completed", { key })
|
log.info("state disposal completed", { key })
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import { createGateway } from "@ai-sdk/gateway"
|
|||||||
import { createTogetherAI } from "@ai-sdk/togetherai"
|
import { createTogetherAI } from "@ai-sdk/togetherai"
|
||||||
import { createPerplexity } from "@ai-sdk/perplexity"
|
import { createPerplexity } from "@ai-sdk/perplexity"
|
||||||
import { createVercel } from "@ai-sdk/vercel"
|
import { createVercel } from "@ai-sdk/vercel"
|
||||||
|
import { createGitLab } from "@gitlab/gitlab-ai-provider"
|
||||||
import { ProviderTransform } from "./transform"
|
import { ProviderTransform } from "./transform"
|
||||||
|
|
||||||
export namespace Provider {
|
export namespace Provider {
|
||||||
@@ -60,6 +61,7 @@ export namespace Provider {
|
|||||||
"@ai-sdk/togetherai": createTogetherAI,
|
"@ai-sdk/togetherai": createTogetherAI,
|
||||||
"@ai-sdk/perplexity": createPerplexity,
|
"@ai-sdk/perplexity": createPerplexity,
|
||||||
"@ai-sdk/vercel": createVercel,
|
"@ai-sdk/vercel": createVercel,
|
||||||
|
"@gitlab/gitlab-ai-provider": createGitLab,
|
||||||
// @ts-ignore (TODO: kill this code so we dont have to maintain it)
|
// @ts-ignore (TODO: kill this code so we dont have to maintain it)
|
||||||
"@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible,
|
"@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible,
|
||||||
}
|
}
|
||||||
@@ -237,6 +239,7 @@ export namespace Provider {
|
|||||||
"nova-lite",
|
"nova-lite",
|
||||||
"nova-pro",
|
"nova-pro",
|
||||||
"nova-premier",
|
"nova-premier",
|
||||||
|
"nova-2",
|
||||||
"claude",
|
"claude",
|
||||||
"deepseek",
|
"deepseek",
|
||||||
].some((m) => modelID.includes(m))
|
].some((m) => modelID.includes(m))
|
||||||
@@ -389,6 +392,43 @@ export namespace Provider {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async gitlab(input) {
|
||||||
|
const instanceUrl = Env.get("GITLAB_INSTANCE_URL") || "https://gitlab.com"
|
||||||
|
|
||||||
|
const auth = await Auth.get(input.id)
|
||||||
|
const apiKey = await (async () => {
|
||||||
|
if (auth?.type === "oauth") return auth.access
|
||||||
|
if (auth?.type === "api") return auth.key
|
||||||
|
return Env.get("GITLAB_TOKEN")
|
||||||
|
})()
|
||||||
|
|
||||||
|
const config = await Config.get()
|
||||||
|
const providerConfig = config.provider?.["gitlab"]
|
||||||
|
|
||||||
|
return {
|
||||||
|
autoload: !!apiKey,
|
||||||
|
options: {
|
||||||
|
instanceUrl,
|
||||||
|
apiKey,
|
||||||
|
featureFlags: {
|
||||||
|
duo_agent_platform_agentic_chat: true,
|
||||||
|
duo_agent_platform: true,
|
||||||
|
...(providerConfig?.options?.featureFlags || {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async getModel(sdk: ReturnType<typeof createGitLab>, modelID: string, options?: { anthropicModel?: string }) {
|
||||||
|
const anthropicModel = options?.anthropicModel
|
||||||
|
return sdk.agenticChat(modelID, {
|
||||||
|
anthropicModel,
|
||||||
|
featureFlags: {
|
||||||
|
duo_agent_platform_agentic_chat: true,
|
||||||
|
duo_agent_platform: true,
|
||||||
|
...(providerConfig?.options?.featureFlags || {}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
"cloudflare-ai-gateway": async (input) => {
|
"cloudflare-ai-gateway": async (input) => {
|
||||||
const accountId = Env.get("CLOUDFLARE_ACCOUNT_ID")
|
const accountId = Env.get("CLOUDFLARE_ACCOUNT_ID")
|
||||||
const gateway = Env.get("CLOUDFLARE_GATEWAY_ID")
|
const gateway = Env.get("CLOUDFLARE_GATEWAY_ID")
|
||||||
@@ -418,11 +458,26 @@ export namespace Provider {
|
|||||||
"HTTP-Referer": "https://opencode.ai/",
|
"HTTP-Referer": "https://opencode.ai/",
|
||||||
"X-Title": "opencode",
|
"X-Title": "opencode",
|
||||||
},
|
},
|
||||||
// Custom fetch to strip Authorization header - AI Gateway uses cf-aig-authorization instead
|
// Custom fetch to handle parameter transformation and auth
|
||||||
// Sending Authorization header with invalid value causes auth errors
|
|
||||||
fetch: async (input: RequestInfo | URL, init?: RequestInit) => {
|
fetch: async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||||
const headers = new Headers(init?.headers)
|
const headers = new Headers(init?.headers)
|
||||||
|
// Strip Authorization header - AI Gateway uses cf-aig-authorization instead
|
||||||
headers.delete("Authorization")
|
headers.delete("Authorization")
|
||||||
|
|
||||||
|
// Transform max_tokens to max_completion_tokens for newer models
|
||||||
|
if (init?.body && init.method === "POST") {
|
||||||
|
try {
|
||||||
|
const body = JSON.parse(init.body as string)
|
||||||
|
if (body.max_tokens !== undefined && !body.max_completion_tokens) {
|
||||||
|
body.max_completion_tokens = body.max_tokens
|
||||||
|
delete body.max_tokens
|
||||||
|
init = { ...init, body: JSON.stringify(body) }
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// If body parsing fails, continue with original request
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return fetch(input, { ...init, headers })
|
return fetch(input, { ...init, headers })
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -364,6 +364,25 @@ export namespace ProviderTransform {
|
|||||||
|
|
||||||
case "@ai-sdk/amazon-bedrock":
|
case "@ai-sdk/amazon-bedrock":
|
||||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/amazon-bedrock
|
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/amazon-bedrock
|
||||||
|
// For Anthropic models on Bedrock, use reasoningConfig with budgetTokens
|
||||||
|
if (model.api.id.includes("anthropic")) {
|
||||||
|
return {
|
||||||
|
high: {
|
||||||
|
reasoningConfig: {
|
||||||
|
type: "enabled",
|
||||||
|
budgetTokens: 16000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
max: {
|
||||||
|
reasoningConfig: {
|
||||||
|
type: "enabled",
|
||||||
|
budgetTokens: 31999,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Amazon Nova models, use reasoningConfig with maxReasoningEffort
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
WIDELY_SUPPORTED_EFFORTS.map((effort) => [
|
WIDELY_SUPPORTED_EFFORTS.map((effort) => [
|
||||||
effort,
|
effort,
|
||||||
@@ -457,6 +476,13 @@ export namespace ProviderTransform {
|
|||||||
result["chat_template_args"] = { enable_thinking: true }
|
result["chat_template_args"] = { enable_thinking: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (["zai", "zhipuai"].includes(model.providerID) && model.api.npm === "@ai-sdk/openai-compatible") {
|
||||||
|
result["thinking"] = {
|
||||||
|
type: "enabled",
|
||||||
|
clear_thinking: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (model.providerID === "openai" || providerOptions?.setCacheKey) {
|
if (model.providerID === "openai" || providerOptions?.setCacheKey) {
|
||||||
result["promptCacheKey"] = sessionID
|
result["promptCacheKey"] = sessionID
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export namespace Question {
|
|||||||
header: z.string().max(12).describe("Very short label (max 12 chars)"),
|
header: z.string().max(12).describe("Very short label (max 12 chars)"),
|
||||||
options: z.array(Option).describe("Available choices"),
|
options: z.array(Option).describe("Available choices"),
|
||||||
multiple: z.boolean().optional().describe("Allow selecting multiple choices"),
|
multiple: z.boolean().optional().describe("Allow selecting multiple choices"),
|
||||||
|
custom: z.boolean().optional().describe("Allow typing a custom answer (default: true)"),
|
||||||
})
|
})
|
||||||
.meta({
|
.meta({
|
||||||
ref: "QuestionInfo",
|
ref: "QuestionInfo",
|
||||||
|
|||||||
@@ -724,6 +724,8 @@ export namespace Server {
|
|||||||
validator(
|
validator(
|
||||||
"query",
|
"query",
|
||||||
z.object({
|
z.object({
|
||||||
|
directory: z.string().optional().meta({ description: "Filter sessions by project directory" }),
|
||||||
|
roots: z.coerce.boolean().optional().meta({ description: "Only return root sessions (no parentID)" }),
|
||||||
start: z.coerce
|
start: z.coerce
|
||||||
.number()
|
.number()
|
||||||
.optional()
|
.optional()
|
||||||
@@ -737,6 +739,8 @@ export namespace Server {
|
|||||||
const term = query.search?.toLowerCase()
|
const term = query.search?.toLowerCase()
|
||||||
const sessions: Session.Info[] = []
|
const sessions: Session.Info[] = []
|
||||||
for await (const session of Session.list()) {
|
for await (const session of Session.list()) {
|
||||||
|
if (query.directory !== undefined && session.directory !== query.directory) continue
|
||||||
|
if (query.roots && session.parentID) continue
|
||||||
if (query.start !== undefined && session.time.updated < query.start) continue
|
if (query.start !== undefined && session.time.updated < query.start) continue
|
||||||
if (term !== undefined && !session.title.toLowerCase().includes(term)) continue
|
if (term !== undefined && !session.title.toLowerCase().includes(term)) continue
|
||||||
sessions.push(session)
|
sessions.push(session)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Slug } from "@opencode-ai/util/slug"
|
||||||
|
import pat from "path"
|
||||||
import { BusEvent } from "@/bus/bus-event"
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
import { Bus } from "@/bus"
|
import { Bus } from "@/bus"
|
||||||
import { Decimal } from "decimal.js"
|
import { Decimal } from "decimal.js"
|
||||||
@@ -19,6 +21,8 @@ import { Snapshot } from "@/snapshot"
|
|||||||
|
|
||||||
import type { Provider } from "@/provider/provider"
|
import type { Provider } from "@/provider/provider"
|
||||||
import { PermissionNext } from "@/permission/next"
|
import { PermissionNext } from "@/permission/next"
|
||||||
|
import path from "path"
|
||||||
|
import { Global } from "@/global"
|
||||||
|
|
||||||
export namespace Session {
|
export namespace Session {
|
||||||
const log = Log.create({ service: "session" })
|
const log = Log.create({ service: "session" })
|
||||||
@@ -39,6 +43,7 @@ export namespace Session {
|
|||||||
export const Info = z
|
export const Info = z
|
||||||
.object({
|
.object({
|
||||||
id: Identifier.schema("session"),
|
id: Identifier.schema("session"),
|
||||||
|
slug: z.string(),
|
||||||
projectID: z.string(),
|
projectID: z.string(),
|
||||||
directory: z.string(),
|
directory: z.string(),
|
||||||
parentID: Identifier.schema("session").optional(),
|
parentID: Identifier.schema("session").optional(),
|
||||||
@@ -194,6 +199,7 @@ export namespace Session {
|
|||||||
}) {
|
}) {
|
||||||
const result: Info = {
|
const result: Info = {
|
||||||
id: Identifier.descending("session", input.id),
|
id: Identifier.descending("session", input.id),
|
||||||
|
slug: Slug.create(),
|
||||||
version: Installation.VERSION,
|
version: Installation.VERSION,
|
||||||
projectID: Instance.project.id,
|
projectID: Instance.project.id,
|
||||||
directory: input.directory,
|
directory: input.directory,
|
||||||
@@ -227,6 +233,13 @@ export namespace Session {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function plan(input: { slug: string; time: { created: number } }) {
|
||||||
|
const base = Instance.project.vcs
|
||||||
|
? path.join(Instance.worktree, ".opencode", "plans")
|
||||||
|
: path.join(Global.Path.data, "plans")
|
||||||
|
return path.join(base, [input.time.created, input.slug].join("-") + ".md")
|
||||||
|
}
|
||||||
|
|
||||||
export const get = fn(Identifier.schema("session"), async (id) => {
|
export const get = fn(Identifier.schema("session"), async (id) => {
|
||||||
const read = await Storage.read<Info>(["session", Instance.project.id, id])
|
const read = await Storage.read<Info>(["session", Instance.project.id, id])
|
||||||
return read as Info
|
return read as Info
|
||||||
|
|||||||
@@ -55,13 +55,20 @@ export namespace LLM {
|
|||||||
modelID: input.model.id,
|
modelID: input.model.id,
|
||||||
providerID: input.model.providerID,
|
providerID: input.model.providerID,
|
||||||
})
|
})
|
||||||
const [language, cfg] = await Promise.all([Provider.getLanguage(input.model), Config.get()])
|
const [language, cfg, provider, auth] = await Promise.all([
|
||||||
|
Provider.getLanguage(input.model),
|
||||||
|
Config.get(),
|
||||||
|
Provider.getProvider(input.model.providerID),
|
||||||
|
Auth.get(input.model.providerID),
|
||||||
|
])
|
||||||
|
const isCodex = provider.id === "openai" && auth?.type === "oauth"
|
||||||
|
|
||||||
const system = SystemPrompt.header(input.model.providerID)
|
const system = SystemPrompt.header(input.model.providerID)
|
||||||
system.push(
|
system.push(
|
||||||
[
|
[
|
||||||
// use agent prompt otherwise provider prompt
|
// use agent prompt otherwise provider prompt
|
||||||
...(input.agent.prompt ? [input.agent.prompt] : SystemPrompt.provider(input.model)),
|
// For Codex sessions, skip SystemPrompt.provider() since it's sent via options.instructions
|
||||||
|
...(input.agent.prompt ? [input.agent.prompt] : isCodex ? [] : SystemPrompt.provider(input.model)),
|
||||||
// any custom prompt passed into this call
|
// any custom prompt passed into this call
|
||||||
...input.system,
|
...input.system,
|
||||||
// any custom prompt from last user message
|
// any custom prompt from last user message
|
||||||
@@ -84,10 +91,6 @@ export namespace LLM {
|
|||||||
system.push(header, rest.join("\n"))
|
system.push(header, rest.join("\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
const provider = await Provider.getProvider(input.model.providerID)
|
|
||||||
const auth = await Auth.get(input.model.providerID)
|
|
||||||
const isCodex = provider.id === "openai" && auth?.type === "oauth"
|
|
||||||
|
|
||||||
const variant =
|
const variant =
|
||||||
!input.small && input.model.variants && input.user.variant ? input.model.variants[input.user.variant] : {}
|
!input.small && input.model.variants && input.user.variant ? input.model.variants[input.user.variant] : {}
|
||||||
const base = input.small
|
const base = input.small
|
||||||
@@ -110,7 +113,7 @@ export namespace LLM {
|
|||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
agent: input.agent,
|
agent: input.agent,
|
||||||
model: input.model,
|
model: input.model,
|
||||||
provider: Provider.getProvider(input.model.providerID),
|
provider,
|
||||||
message: input.user,
|
message: input.user,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -664,6 +664,7 @@ export namespace MessageV2 {
|
|||||||
return `${msg}: ${e.responseBody}`
|
return `${msg}: ${e.responseBody}`
|
||||||
}).trim()
|
}).trim()
|
||||||
|
|
||||||
|
const metadata = e.url ? { url: e.url } : undefined
|
||||||
return new MessageV2.APIError(
|
return new MessageV2.APIError(
|
||||||
{
|
{
|
||||||
message,
|
message,
|
||||||
@@ -671,6 +672,7 @@ export namespace MessageV2 {
|
|||||||
isRetryable: e.isRetryable,
|
isRetryable: e.isRetryable,
|
||||||
responseHeaders: e.responseHeaders,
|
responseHeaders: e.responseHeaders,
|
||||||
responseBody: e.responseBody,
|
responseBody: e.responseBody,
|
||||||
|
metadata,
|
||||||
},
|
},
|
||||||
{ cause: e },
|
{ cause: e },
|
||||||
).toObject()
|
).toObject()
|
||||||
|
|||||||
@@ -510,9 +510,10 @@ export namespace SessionPrompt {
|
|||||||
const agent = await Agent.get(lastUser.agent)
|
const agent = await Agent.get(lastUser.agent)
|
||||||
const maxSteps = agent.steps ?? Infinity
|
const maxSteps = agent.steps ?? Infinity
|
||||||
const isLastStep = step >= maxSteps
|
const isLastStep = step >= maxSteps
|
||||||
msgs = insertReminders({
|
msgs = await insertReminders({
|
||||||
messages: msgs,
|
messages: msgs,
|
||||||
agent,
|
agent,
|
||||||
|
session,
|
||||||
})
|
})
|
||||||
|
|
||||||
const processor = SessionProcessor.create({
|
const processor = SessionProcessor.create({
|
||||||
@@ -1185,30 +1186,142 @@ export namespace SessionPrompt {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertReminders(input: { messages: MessageV2.WithParts[]; agent: Agent.Info }) {
|
async function insertReminders(input: { messages: MessageV2.WithParts[]; agent: Agent.Info; session: Session.Info }) {
|
||||||
const userMessage = input.messages.findLast((msg) => msg.info.role === "user")
|
const userMessage = input.messages.findLast((msg) => msg.info.role === "user")
|
||||||
if (!userMessage) return input.messages
|
if (!userMessage) return input.messages
|
||||||
if (input.agent.name === "plan") {
|
|
||||||
userMessage.parts.push({
|
// Original logic when experimental plan mode is disabled
|
||||||
id: Identifier.ascending("part"),
|
if (!Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE) {
|
||||||
messageID: userMessage.info.id,
|
if (input.agent.name === "plan") {
|
||||||
sessionID: userMessage.info.sessionID,
|
userMessage.parts.push({
|
||||||
type: "text",
|
id: Identifier.ascending("part"),
|
||||||
// TODO (for mr dax): update to use the anthropic full fledged one (see plan-reminder-anthropic.txt)
|
messageID: userMessage.info.id,
|
||||||
text: PROMPT_PLAN,
|
sessionID: userMessage.info.sessionID,
|
||||||
synthetic: true,
|
type: "text",
|
||||||
})
|
text: PROMPT_PLAN,
|
||||||
|
synthetic: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.agent === "plan")
|
||||||
|
if (wasPlan && input.agent.name === "build") {
|
||||||
|
userMessage.parts.push({
|
||||||
|
id: Identifier.ascending("part"),
|
||||||
|
messageID: userMessage.info.id,
|
||||||
|
sessionID: userMessage.info.sessionID,
|
||||||
|
type: "text",
|
||||||
|
text: BUILD_SWITCH,
|
||||||
|
synthetic: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return input.messages
|
||||||
}
|
}
|
||||||
const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.agent === "plan")
|
|
||||||
if (wasPlan && input.agent.name === "build") {
|
// New plan mode logic when flag is enabled
|
||||||
userMessage.parts.push({
|
const assistantMessage = input.messages.findLast((msg) => msg.info.role === "assistant")
|
||||||
|
|
||||||
|
// Switching from plan mode to build mode
|
||||||
|
if (input.agent.name !== "plan" && assistantMessage?.info.agent === "plan") {
|
||||||
|
const plan = Session.plan(input.session)
|
||||||
|
const exists = await Bun.file(plan).exists()
|
||||||
|
if (exists) {
|
||||||
|
const part = await Session.updatePart({
|
||||||
|
id: Identifier.ascending("part"),
|
||||||
|
messageID: userMessage.info.id,
|
||||||
|
sessionID: userMessage.info.sessionID,
|
||||||
|
type: "text",
|
||||||
|
text:
|
||||||
|
BUILD_SWITCH + "\n\n" + `A plan file exists at ${plan}. You should execute on the plan defined within it`,
|
||||||
|
synthetic: true,
|
||||||
|
})
|
||||||
|
userMessage.parts.push(part)
|
||||||
|
}
|
||||||
|
return input.messages
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entering plan mode
|
||||||
|
if (input.agent.name === "plan" && assistantMessage?.info.agent !== "plan") {
|
||||||
|
const plan = Session.plan(input.session)
|
||||||
|
const exists = await Bun.file(plan).exists()
|
||||||
|
if (!exists) await fs.mkdir(path.dirname(plan), { recursive: true })
|
||||||
|
const part = await Session.updatePart({
|
||||||
id: Identifier.ascending("part"),
|
id: Identifier.ascending("part"),
|
||||||
messageID: userMessage.info.id,
|
messageID: userMessage.info.id,
|
||||||
sessionID: userMessage.info.sessionID,
|
sessionID: userMessage.info.sessionID,
|
||||||
type: "text",
|
type: "text",
|
||||||
text: BUILD_SWITCH,
|
text: `<system-reminder>
|
||||||
|
Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits (with the exception of the plan file mentioned below), run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supercedes any other instructions you have received.
|
||||||
|
|
||||||
|
## Plan File Info:
|
||||||
|
${exists ? `A plan file already exists at ${plan}. You can read it and make incremental edits using the edit tool.` : `No plan file exists yet. You should create your plan at ${plan} using the write tool.`}
|
||||||
|
You should build your plan incrementally by writing to or editing this file. NOTE that this is the only file you are allowed to edit - other than this you are only allowed to take READ-ONLY actions.
|
||||||
|
|
||||||
|
## Plan Workflow
|
||||||
|
|
||||||
|
### Phase 1: Initial Understanding
|
||||||
|
Goal: Gain a comprehensive understanding of the user's request by reading through code and asking them questions. Critical: In this phase you should only use the explore subagent type.
|
||||||
|
|
||||||
|
1. Focus on understanding the user's request and the code associated with their request
|
||||||
|
|
||||||
|
2. **Launch up to 3 explore agents IN PARALLEL** (single message, multiple tool calls) to efficiently explore the codebase.
|
||||||
|
- Use 1 agent when the task is isolated to known files, the user provided specific file paths, or you're making a small targeted change.
|
||||||
|
- Use multiple agents when: the scope is uncertain, multiple areas of the codebase are involved, or you need to understand existing patterns before planning.
|
||||||
|
- Quality over quantity - 3 agents maximum, but you should try to use the minimum number of agents necessary (usually just 1)
|
||||||
|
- If using multiple agents: Provide each agent with a specific search focus or area to explore. Example: One agent searches for existing implementations, another explores related components, a third investigates testing patterns
|
||||||
|
|
||||||
|
3. After exploring the code, use the question tool to clarify ambiguities in the user request up front.
|
||||||
|
|
||||||
|
### Phase 2: Design
|
||||||
|
Goal: Design an implementation approach.
|
||||||
|
|
||||||
|
Launch general agent(s) to design the implementation based on the user's intent and your exploration results from Phase 1.
|
||||||
|
|
||||||
|
You can launch up to 1 agent(s) in parallel.
|
||||||
|
|
||||||
|
**Guidelines:**
|
||||||
|
- **Default**: Launch at least 1 Plan agent for most tasks - it helps validate your understanding and consider alternatives
|
||||||
|
- **Skip agents**: Only for truly trivial tasks (typo fixes, single-line changes, simple renames)
|
||||||
|
|
||||||
|
Examples of when to use multiple agents:
|
||||||
|
- The task touches multiple parts of the codebase
|
||||||
|
- It's a large refactor or architectural change
|
||||||
|
- There are many edge cases to consider
|
||||||
|
- You'd benefit from exploring different approaches
|
||||||
|
|
||||||
|
Example perspectives by task type:
|
||||||
|
- New feature: simplicity vs performance vs maintainability
|
||||||
|
- Bug fix: root cause vs workaround vs prevention
|
||||||
|
- Refactoring: minimal change vs clean architecture
|
||||||
|
|
||||||
|
In the agent prompt:
|
||||||
|
- Provide comprehensive background context from Phase 1 exploration including filenames and code path traces
|
||||||
|
- Describe requirements and constraints
|
||||||
|
- Request a detailed implementation plan
|
||||||
|
|
||||||
|
### Phase 3: Review
|
||||||
|
Goal: Review the plan(s) from Phase 2 and ensure alignment with the user's intentions.
|
||||||
|
1. Read the critical files identified by agents to deepen your understanding
|
||||||
|
2. Ensure that the plans align with the user's original request
|
||||||
|
3. Use question tool to clarify any remaining questions with the user
|
||||||
|
|
||||||
|
### Phase 4: Final Plan
|
||||||
|
Goal: Write your final plan to the plan file (the only file you can edit).
|
||||||
|
- Include only your recommended approach, not all alternatives
|
||||||
|
- Ensure that the plan file is concise enough to scan quickly, but detailed enough to execute effectively
|
||||||
|
- Include the paths of critical files to be modified
|
||||||
|
- Include a verification section describing how to test the changes end-to-end (run the code, use MCP tools, run tests)
|
||||||
|
|
||||||
|
### Phase 5: Call plan_exit tool
|
||||||
|
At the very end of your turn, once you have asked the user questions and are happy with your final plan file - you should always call plan_exit to indicate to the user that you are done planning.
|
||||||
|
This is critical - your turn should only end with either asking the user a question or calling plan_exit. Do not stop unless it's for these 2 reasons.
|
||||||
|
|
||||||
|
**Important:** Use question tool to clarify requirements/approach, use plan_exit to request plan approval. Do NOT use question tool to ask "Is this plan okay?" - that's what plan_exit does.
|
||||||
|
|
||||||
|
NOTE: At any point in time through this workflow you should feel free to ask the user questions or clarifications. Don't make large assumptions about user intent. The goal is to present a well researched plan to the user, and tie any loose ends before implementation begins.
|
||||||
|
</system-reminder>`,
|
||||||
synthetic: true,
|
synthetic: true,
|
||||||
})
|
})
|
||||||
|
userMessage.parts.push(part)
|
||||||
|
return input.messages
|
||||||
}
|
}
|
||||||
return input.messages
|
return input.messages
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export const EditTool = Tool.define("edit", {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
let output = ""
|
let output = "Edit applied successfully."
|
||||||
await LSP.touchFile(filePath, true)
|
await LSP.touchFile(filePath, true)
|
||||||
const diagnostics = await LSP.diagnostics()
|
const diagnostics = await LSP.diagnostics()
|
||||||
const normalizedFilePath = Filesystem.normalizePath(filePath)
|
const normalizedFilePath = Filesystem.normalizePath(filePath)
|
||||||
@@ -129,7 +129,7 @@ export const EditTool = Tool.define("edit", {
|
|||||||
const limited = errors.slice(0, MAX_DIAGNOSTICS_PER_FILE)
|
const limited = errors.slice(0, MAX_DIAGNOSTICS_PER_FILE)
|
||||||
const suffix =
|
const suffix =
|
||||||
errors.length > MAX_DIAGNOSTICS_PER_FILE ? `\n... and ${errors.length - MAX_DIAGNOSTICS_PER_FILE} more` : ""
|
errors.length > MAX_DIAGNOSTICS_PER_FILE ? `\n... and ${errors.length - MAX_DIAGNOSTICS_PER_FILE} more` : ""
|
||||||
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n</file_diagnostics>\n`
|
output += `\n\nLSP errors detected in this file:\n<diagnostics file="${filePath}">\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n</diagnostics>`
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
Use this tool to suggest switching to plan agent when the user's request would benefit from planning before implementation.
|
||||||
|
|
||||||
|
If they explicitly mention wanting to create a plan ALWAYS call this tool first.
|
||||||
|
|
||||||
|
This tool will ask the user if they want to switch to plan agent.
|
||||||
|
|
||||||
|
Call this tool when:
|
||||||
|
- The user's request is complex and would benefit from planning first
|
||||||
|
- You want to research and design before making changes
|
||||||
|
- The task involves multiple files or significant architectural decisions
|
||||||
|
|
||||||
|
Do NOT call this tool:
|
||||||
|
- For simple, straightforward tasks
|
||||||
|
- When the user explicitly wants immediate implementation
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
Use this tool when you have completed the planning phase and are ready to exit plan agent.
|
||||||
|
|
||||||
|
This tool will ask the user if they want to switch to build agent to start implementing the plan.
|
||||||
|
|
||||||
|
Call this tool:
|
||||||
|
- After you have written a complete plan to the plan file
|
||||||
|
- After you have clarified any questions with the user
|
||||||
|
- When you are confident the plan is ready for implementation
|
||||||
|
|
||||||
|
Do NOT call this tool:
|
||||||
|
- Before you have created or finalized the plan
|
||||||
|
- If you still have unanswered questions about the implementation
|
||||||
|
- If the user has indicated they want to continue planning
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import z from "zod"
|
||||||
|
import path from "path"
|
||||||
|
import { Tool } from "./tool"
|
||||||
|
import { Question } from "../question"
|
||||||
|
import { Session } from "../session"
|
||||||
|
import { MessageV2 } from "../session/message-v2"
|
||||||
|
import { Identifier } from "../id/id"
|
||||||
|
import { Provider } from "../provider/provider"
|
||||||
|
import { Instance } from "../project/instance"
|
||||||
|
import EXIT_DESCRIPTION from "./plan-exit.txt"
|
||||||
|
import ENTER_DESCRIPTION from "./plan-enter.txt"
|
||||||
|
|
||||||
|
async function getLastModel(sessionID: string) {
|
||||||
|
for await (const item of MessageV2.stream(sessionID)) {
|
||||||
|
if (item.info.role === "user" && item.info.model) return item.info.model
|
||||||
|
}
|
||||||
|
return Provider.defaultModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PlanExitTool = Tool.define("plan_exit", {
|
||||||
|
description: EXIT_DESCRIPTION,
|
||||||
|
parameters: z.object({}),
|
||||||
|
async execute(_params, ctx) {
|
||||||
|
const session = await Session.get(ctx.sessionID)
|
||||||
|
const plan = path.relative(Instance.worktree, Session.plan(session))
|
||||||
|
const answers = await Question.ask({
|
||||||
|
sessionID: ctx.sessionID,
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
question: `Plan at ${plan} is complete. Would you like to switch to the build agent and start implementing?`,
|
||||||
|
header: "Build Agent",
|
||||||
|
custom: false,
|
||||||
|
options: [
|
||||||
|
{ label: "Yes", description: "Switch to build agent and start implementing the plan" },
|
||||||
|
{ label: "No", description: "Stay with plan agent to continue refining the plan" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tool: ctx.callID ? { messageID: ctx.messageID, callID: ctx.callID } : undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
const answer = answers[0]?.[0]
|
||||||
|
if (answer === "No") throw new Question.RejectedError()
|
||||||
|
|
||||||
|
const model = await getLastModel(ctx.sessionID)
|
||||||
|
|
||||||
|
const userMsg: MessageV2.User = {
|
||||||
|
id: Identifier.ascending("message"),
|
||||||
|
sessionID: ctx.sessionID,
|
||||||
|
role: "user",
|
||||||
|
time: {
|
||||||
|
created: Date.now(),
|
||||||
|
},
|
||||||
|
agent: "build",
|
||||||
|
model,
|
||||||
|
}
|
||||||
|
await Session.updateMessage(userMsg)
|
||||||
|
await Session.updatePart({
|
||||||
|
id: Identifier.ascending("part"),
|
||||||
|
messageID: userMsg.id,
|
||||||
|
sessionID: ctx.sessionID,
|
||||||
|
type: "text",
|
||||||
|
text: `The plan at ${plan} has been approved, you can now edit files. Execute the plan`,
|
||||||
|
synthetic: true,
|
||||||
|
} satisfies MessageV2.TextPart)
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: "Switching to build agent",
|
||||||
|
output: "User approved switching to build agent. Wait for further instructions.",
|
||||||
|
metadata: {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const PlanEnterTool = Tool.define("plan_enter", {
|
||||||
|
description: ENTER_DESCRIPTION,
|
||||||
|
parameters: z.object({}),
|
||||||
|
async execute(_params, ctx) {
|
||||||
|
const session = await Session.get(ctx.sessionID)
|
||||||
|
const plan = path.relative(Instance.worktree, Session.plan(session))
|
||||||
|
|
||||||
|
const answers = await Question.ask({
|
||||||
|
sessionID: ctx.sessionID,
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
question: `Would you like to switch to the plan agent and create a plan saved to ${plan}?`,
|
||||||
|
header: "Plan Mode",
|
||||||
|
custom: false,
|
||||||
|
options: [
|
||||||
|
{ label: "Yes", description: "Switch to plan agent for research and planning" },
|
||||||
|
{ label: "No", description: "Stay with build agent to continue making changes" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tool: ctx.callID ? { messageID: ctx.messageID, callID: ctx.callID } : undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
const answer = answers[0]?.[0]
|
||||||
|
|
||||||
|
if (answer === "No") throw new Question.RejectedError()
|
||||||
|
|
||||||
|
const model = await getLastModel(ctx.sessionID)
|
||||||
|
|
||||||
|
const userMsg: MessageV2.User = {
|
||||||
|
id: Identifier.ascending("message"),
|
||||||
|
sessionID: ctx.sessionID,
|
||||||
|
role: "user",
|
||||||
|
time: {
|
||||||
|
created: Date.now(),
|
||||||
|
},
|
||||||
|
agent: "plan",
|
||||||
|
model,
|
||||||
|
}
|
||||||
|
await Session.updateMessage(userMsg)
|
||||||
|
await Session.updatePart({
|
||||||
|
id: Identifier.ascending("part"),
|
||||||
|
messageID: userMsg.id,
|
||||||
|
sessionID: ctx.sessionID,
|
||||||
|
type: "text",
|
||||||
|
text: "User has requested to enter plan mode. Switch to plan mode and begin planning.",
|
||||||
|
synthetic: true,
|
||||||
|
} satisfies MessageV2.TextPart)
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: "Switching to plan agent",
|
||||||
|
output: `User confirmed to switch to plan mode. A new message has been created to switch you to plan mode. The plan file will be at ${plan}. Begin planning.`,
|
||||||
|
metadata: {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user