Compare commits
258
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13dc12fe6d | ||
|
|
c74350dc22 | ||
|
|
5fa1673341 | ||
|
|
daaa1c7e26 | ||
|
|
1fae784b81 | ||
|
|
81b7b58a5e | ||
|
|
866188a643 | ||
|
|
e6fd57165e | ||
|
|
a5d99e7a3c | ||
|
|
a92c75e5f4 | ||
|
|
826fd3350c | ||
|
|
23a2d01282 | ||
|
|
5181f9b4e1 | ||
|
|
f52ae28432 | ||
|
|
36119ff173 | ||
|
|
bb90f3bbf9 | ||
|
|
05cdb7c107 | ||
|
|
b493dabfe6 | ||
|
|
c4816f944e | ||
|
|
211136e3a8 | ||
|
|
cf0a53c501 | ||
|
|
2899984819 | ||
|
|
eafbe5c57c | ||
|
|
7b98f544ff | ||
|
|
b5aba5807c | ||
|
|
d5c4c26b4b | ||
|
|
a35b8a95c2 | ||
|
|
cded68a2e2 | ||
|
|
0068ccec35 | ||
|
|
89e8994fd1 | ||
|
|
5980b0a5ee | ||
|
|
89029a20ef | ||
|
|
ce69bd97b9 | ||
|
|
999d8651aa | ||
|
|
ed0f022502 | ||
|
|
b1307d5c2a | ||
|
|
dc16013b4f | ||
|
|
e7686dbd64 | ||
|
|
47f553f9ba | ||
|
|
d11268ece7 | ||
|
|
650a13a690 | ||
|
|
54435325b6 | ||
|
|
11fa257549 | ||
|
|
6af8ab0df2 | ||
|
|
984f5ed6eb | ||
|
|
c2061c6bbf | ||
|
|
b708e8431e | ||
|
|
9b6c397171 | ||
|
|
9b0659d4f9 | ||
|
|
f83cecaaf6 | ||
|
|
aa05b9abe5 | ||
|
|
68834cfcc3 | ||
|
|
5621373bc2 | ||
|
|
88582566bf | ||
|
|
d6e1362fee | ||
|
|
b275b8580d | ||
|
|
467be08e67 | ||
|
|
bbb422d125 | ||
|
|
b1f076558c | ||
|
|
992435aaf8 | ||
|
|
2f73e73e9d | ||
|
|
4c30a78cd9 | ||
|
|
a8c78fc005 | ||
|
|
fcb473ff64 | ||
|
|
797953c88d | ||
|
|
ce0cfb0ea5 | ||
|
|
13dfe569ef | ||
|
|
c491161c0c | ||
|
|
fde3d9133b | ||
|
|
0d582f9d3f | ||
|
|
1a59133168 | ||
|
|
803d9eb7ad | ||
|
|
a27d3c1623 | ||
|
|
551216a452 | ||
|
|
38cd3979f2 | ||
|
|
3fe602cda3 | ||
|
|
3a4b49095c | ||
|
|
ac5b395c5d | ||
|
|
8fbbca5f4b | ||
|
|
2415820ecd | ||
|
|
20103eb97b | ||
|
|
10c4ab9a3d | ||
|
|
7e39c9b950 | ||
|
|
cc063d4c32 | ||
|
|
3707e4a49c | ||
|
|
cb425ac927 | ||
|
|
0f80c827ed | ||
|
|
fffc496f41 | ||
|
|
06ae43920b | ||
|
|
e78d75a003 | ||
|
|
ec3ac0c4b0 | ||
|
|
c57c5315c1 | ||
|
|
a726530735 | ||
|
|
d9950598d0 | ||
|
|
81f0885879 | ||
|
|
65b2a10e97 | ||
|
|
7605acff65 | ||
|
|
e7f8f7fa3b | ||
|
|
72d7cb717d | ||
|
|
f0caeb9b25 | ||
|
|
76a141090e | ||
|
|
4bd5a158a5 | ||
|
|
dfaae14544 | ||
|
|
79e9baf55a | ||
|
|
9ee89f7868 | ||
|
|
67dbb3cf18 | ||
|
|
4260c40efa | ||
|
|
0bedea52b1 | ||
|
|
fbbab9d6c8 | ||
|
|
cccb907a9b | ||
|
|
ee7339f2c6 | ||
|
|
c51f3e35ca | ||
|
|
7b3bb9a761 | ||
|
|
dc38f22bd8 | ||
|
|
220e3e9a2b | ||
|
|
f135c0b5ee | ||
|
|
ebe6ea580d | ||
|
|
ee708040f6 | ||
|
|
61c4815a37 | ||
|
|
01bb54a94d | ||
|
|
f592c3846b | ||
|
|
c026e25088 | ||
|
|
8ba73bed23 | ||
|
|
4f8986aa48 | ||
|
|
9c87a144e8 | ||
|
|
5b9fa32255 | ||
|
|
f13778215a | ||
|
|
326471a25c | ||
|
|
6405e3a7b1 | ||
|
|
8afb625bab | ||
|
|
c59df636cc | ||
|
|
94878d76f8 | ||
|
|
5022895e2b | ||
|
|
54046e0b98 | ||
|
|
d2cb1613ac | ||
|
|
266fb93422 | ||
|
|
51d8219c46 | ||
|
|
d6af5a686c | ||
|
|
39342b0e75 | ||
|
|
54078c4cae | ||
|
|
c0bfccc15e | ||
|
|
53dc7b1649 | ||
|
|
635970b0a1 | ||
|
|
059b32c212 | ||
|
|
2704ad9110 | ||
|
|
06d247c709 | ||
|
|
974fa1b8b1 | ||
|
|
fb02744460 | ||
|
|
79732ab175 | ||
|
|
f6dbb2f3e0 | ||
|
|
fdd5b77bfd | ||
|
|
cde105e7a8 | ||
|
|
1291e82bb4 | ||
|
|
19d15d9ff7 | ||
|
|
4e27804160 | ||
|
|
bae80af1b4 | ||
|
|
f9aa3d77cd | ||
|
|
5d47ea0918 | ||
|
|
c03fa36257 | ||
|
|
1089fa0415 | ||
|
|
715786bbf9 | ||
|
|
218eca7c2b | ||
|
|
30fc791480 | ||
|
|
e2d161dfdd | ||
|
|
23d48a7cf1 | ||
|
|
cb18f2ef40 | ||
|
|
dbe2ff52b2 | ||
|
|
9db40996cc | ||
|
|
9f201d6370 | ||
|
|
0e86466f99 | ||
|
|
32548bcb4a | ||
|
|
86c54c5acc | ||
|
|
ae584332b3 | ||
|
|
1694c5bfe1 | ||
|
|
cdfbb26c00 | ||
|
|
610c036ef1 | ||
|
|
2638e2acfa | ||
|
|
49bbea5aed | ||
|
|
5fccdc9fc7 | ||
|
|
664b2c36e8 | ||
|
|
964474a1b1 | ||
|
|
ab15fc1575 | ||
|
|
99d392a4fb | ||
|
|
ae9a696607 | ||
|
|
bd51a0d35b | ||
|
|
8c191b10c2 | ||
|
|
cb6a9253fe | ||
|
|
23f97ac49d | ||
|
|
021ab50fb1 | ||
|
|
3fe906f517 | ||
|
|
a8d8a35cd3 | ||
|
|
9b77430d0d | ||
|
|
1045a43603 | ||
|
|
26af77cd1e | ||
|
|
25a9de301a | ||
|
|
e0d71f124e | ||
|
|
1c33b866ba | ||
|
|
5e650fd9e2 | ||
|
|
76275fc3ab | ||
|
|
6c3b28db64 | ||
|
|
2fe9d94470 | ||
|
|
219b473e66 | ||
|
|
7c1b30291c | ||
|
|
47e0e2342c | ||
|
|
bf4c107829 | ||
|
|
9afbdc102c | ||
|
|
370770122c | ||
|
|
143817d44e | ||
|
|
c60862fc9e | ||
|
|
bee5f919fc | ||
|
|
cefa7f04c6 | ||
|
|
03e20e6ac1 | ||
|
|
c5deeee8c7 | ||
|
|
8b1f0e2d90 | ||
|
|
9bf2dfea35 | ||
|
|
33bb847a1d | ||
|
|
bfffc3c2c6 | ||
|
|
b28956f0db | ||
|
|
d82bc3a421 | ||
|
|
06afd33291 | ||
|
|
305460b25f | ||
|
|
8c0205a84a | ||
|
|
378c05f202 | ||
|
|
cc7acd90ab | ||
|
|
a200f6fb8b | ||
|
|
2b1696f1d1 | ||
|
|
8ab17f5ce0 | ||
|
|
6ce481e95b | ||
|
|
7341718f92 | ||
|
|
ef90b93205 | ||
|
|
3f7df08be9 | ||
|
|
ef6c26c730 | ||
|
|
8b3b608ba9 | ||
|
|
97918500d4 | ||
|
|
e2c0803962 | ||
|
|
f418fd5632 | ||
|
|
675a46e23e | ||
|
|
150ab07a83 | ||
|
|
6b20838981 | ||
|
|
c8af8f96ce | ||
|
|
5011465c81 | ||
|
|
f6cc228684 | ||
|
|
9f4b73b6a3 | ||
|
|
bd29004831 | ||
|
|
8aa0f9fe95 | ||
|
|
c802695ee9 | ||
|
|
225a769411 | ||
|
|
0e20382396 | ||
|
|
509bc11f81 | ||
|
|
f24207844f | ||
|
|
1ca257e356 | ||
|
|
d4cfbd020d | ||
|
|
581d5208ca | ||
|
|
a427a28fa9 | ||
|
|
0beaf04df5 | ||
|
|
80f1f1b5b8 | ||
|
|
343a564183 | ||
|
|
b0eae5e12f |
@@ -594,7 +594,6 @@ OPENCODE_DISABLE_CLAUDE_CODE
|
|||||||
OPENCODE_DISABLE_CLAUDE_CODE_PROMPT
|
OPENCODE_DISABLE_CLAUDE_CODE_PROMPT
|
||||||
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS
|
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS
|
||||||
OPENCODE_DISABLE_DEFAULT_PLUGINS
|
OPENCODE_DISABLE_DEFAULT_PLUGINS
|
||||||
OPENCODE_DISABLE_FILETIME_CHECK
|
|
||||||
OPENCODE_DISABLE_LSP_DOWNLOAD
|
OPENCODE_DISABLE_LSP_DOWNLOAD
|
||||||
OPENCODE_DISABLE_MODELS_FETCH
|
OPENCODE_DISABLE_MODELS_FETCH
|
||||||
OPENCODE_DISABLE_PRUNE
|
OPENCODE_DISABLE_PRUNE
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://opencode.ai/config.json",
|
"$schema": "https://opencode.ai/config.json",
|
||||||
"provider": {
|
"provider": {},
|
||||||
"opencode": {
|
|
||||||
"options": {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"permission": {
|
"permission": {
|
||||||
"edit": {
|
"edit": {
|
||||||
"packages/opencode/migration/*": "deny",
|
"packages/opencode/migration/*": "deny",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ async function githubFetch(endpoint: string, options: RequestInit = {}) {
|
|||||||
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
|
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
|
||||||
Accept: "application/vnd.github+json",
|
Accept: "application/vnd.github+json",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...options.headers,
|
...(options.headers instanceof Headers ? Object.fromEntries(options.headers.entries()) : options.headers),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ async function githubFetch(endpoint: string, options: RequestInit = {}) {
|
|||||||
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
|
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
|
||||||
Accept: "application/vnd.github+json",
|
Accept: "application/vnd.github+json",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...options.headers,
|
...(options.headers instanceof Headers ? Object.fromEntries(options.headers.entries()) : options.headers),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
+17
-2
@@ -1,9 +1,13 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/nicolo-ribaudo/oxc-project.github.io/refs/heads/json-schema/src/public/.oxlintrc.schema.json",
|
"$schema": "https://raw.githubusercontent.com/nicolo-ribaudo/oxc-project.github.io/refs/heads/json-schema/src/public/.oxlintrc.schema.json",
|
||||||
|
"options": {
|
||||||
|
"typeAware": true
|
||||||
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"suspicious": "warn"
|
"suspicious": "warn"
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
|
"typescript/no-base-to-string": "warn",
|
||||||
// Effect uses `function*` with Effect.gen/Effect.fnUntraced that don't always yield
|
// Effect uses `function*` with Effect.gen/Effect.fnUntraced that don't always yield
|
||||||
"require-yield": "off",
|
"require-yield": "off",
|
||||||
// SolidJS uses `let ref: T | undefined` for JSX ref bindings assigned at runtime
|
// SolidJS uses `let ref: T | undefined` for JSX ref bindings assigned at runtime
|
||||||
@@ -30,7 +34,18 @@
|
|||||||
// postMessage target origin not relevant for this codebase
|
// postMessage target origin not relevant for this codebase
|
||||||
"unicorn/require-post-message-target-origin": "off",
|
"unicorn/require-post-message-target-origin": "off",
|
||||||
// Side-effectful constructors are intentional in some places
|
// Side-effectful constructors are intentional in some places
|
||||||
"no-new": "off"
|
"no-new": "off",
|
||||||
|
|
||||||
|
// Type-aware: catch unhandled promises
|
||||||
|
"typescript/no-floating-promises": "warn",
|
||||||
|
// Warn when spreading non-plain objects (Headers, class instances, etc.)
|
||||||
|
"typescript/no-misused-spread": "warn"
|
||||||
},
|
},
|
||||||
"ignorePatterns": ["**/node_modules", "**/dist", "**/.build", "**/.sst", "**/*.d.ts"]
|
"options": {
|
||||||
|
"typeAware": true
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"typeAware": true
|
||||||
|
},
|
||||||
|
"ignorePatterns": ["**/node_modules", "**/dist", "**/.build", "**/.sst", "**/*.d.ts", "**/sdk.gen.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
- Use Bun APIs when possible, like `Bun.file()`
|
- Use Bun APIs when possible, like `Bun.file()`
|
||||||
- Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity
|
- Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity
|
||||||
- Prefer functional array methods (flatMap, filter, map) over for loops; use type guards on filter to maintain type inference downstream
|
- Prefer functional array methods (flatMap, filter, map) over for loops; use type guards on filter to maintain type inference downstream
|
||||||
|
- In `src/config`, follow the existing self-export pattern at the top of the file (for example `export * as ConfigAgent from "./agent"`) when adding a new config module.
|
||||||
|
|
||||||
Reduce total variable count by inlining when a value is only used once.
|
Reduce total variable count by inlining when a value is only used once.
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"glob": "13.0.5",
|
"glob": "13.0.5",
|
||||||
"husky": "9.1.7",
|
"husky": "9.1.7",
|
||||||
"oxlint": "1.60.0",
|
"oxlint": "1.60.0",
|
||||||
|
"oxlint-tsgolint": "0.21.0",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"semver": "^7.6.0",
|
"semver": "^7.6.0",
|
||||||
"sst": "3.18.10",
|
"sst": "3.18.10",
|
||||||
@@ -28,7 +29,7 @@
|
|||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.4.6",
|
"version": "1.4.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -82,7 +83,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.4.6",
|
"version": "1.4.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
@@ -116,7 +117,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.4.6",
|
"version": "1.4.11",
|
||||||
"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",
|
||||||
@@ -143,7 +144,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.4.6",
|
"version": "1.4.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "3.0.64",
|
"@ai-sdk/anthropic": "3.0.64",
|
||||||
"@ai-sdk/openai": "3.0.48",
|
"@ai-sdk/openai": "3.0.48",
|
||||||
@@ -167,7 +168,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.4.6",
|
"version": "1.4.11",
|
||||||
"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",
|
||||||
@@ -191,7 +192,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.4.6",
|
"version": "1.4.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/app": "workspace:*",
|
"@opencode-ai/app": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
@@ -224,7 +225,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop-electron": {
|
"packages/desktop-electron": {
|
||||||
"name": "@opencode-ai/desktop-electron",
|
"name": "@opencode-ai/desktop-electron",
|
||||||
"version": "1.4.6",
|
"version": "1.4.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"electron-context-menu": "4.1.2",
|
"electron-context-menu": "4.1.2",
|
||||||
@@ -267,7 +268,7 @@
|
|||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.4.6",
|
"version": "1.4.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/shared": "workspace:*",
|
"@opencode-ai/shared": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
@@ -296,7 +297,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.4.6",
|
"version": "1.4.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
@@ -312,7 +313,7 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.4.6",
|
"version": "1.4.11",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -321,15 +322,15 @@
|
|||||||
"@actions/github": "6.0.1",
|
"@actions/github": "6.0.1",
|
||||||
"@agentclientprotocol/sdk": "0.16.1",
|
"@agentclientprotocol/sdk": "0.16.1",
|
||||||
"@ai-sdk/alibaba": "1.0.17",
|
"@ai-sdk/alibaba": "1.0.17",
|
||||||
"@ai-sdk/amazon-bedrock": "4.0.93",
|
"@ai-sdk/amazon-bedrock": "4.0.95",
|
||||||
"@ai-sdk/anthropic": "3.0.67",
|
"@ai-sdk/anthropic": "3.0.71",
|
||||||
"@ai-sdk/azure": "3.0.49",
|
"@ai-sdk/azure": "3.0.49",
|
||||||
"@ai-sdk/cerebras": "2.0.41",
|
"@ai-sdk/cerebras": "2.0.41",
|
||||||
"@ai-sdk/cohere": "3.0.27",
|
"@ai-sdk/cohere": "3.0.27",
|
||||||
"@ai-sdk/deepinfra": "2.0.41",
|
"@ai-sdk/deepinfra": "2.0.41",
|
||||||
"@ai-sdk/gateway": "3.0.97",
|
"@ai-sdk/gateway": "3.0.104",
|
||||||
"@ai-sdk/google": "3.0.63",
|
"@ai-sdk/google": "3.0.63",
|
||||||
"@ai-sdk/google-vertex": "4.0.109",
|
"@ai-sdk/google-vertex": "4.0.112",
|
||||||
"@ai-sdk/groq": "3.0.31",
|
"@ai-sdk/groq": "3.0.31",
|
||||||
"@ai-sdk/mistral": "3.0.27",
|
"@ai-sdk/mistral": "3.0.27",
|
||||||
"@ai-sdk/openai": "3.0.53",
|
"@ai-sdk/openai": "3.0.53",
|
||||||
@@ -364,8 +365,8 @@
|
|||||||
"@opentelemetry/exporter-trace-otlp-http": "0.214.0",
|
"@opentelemetry/exporter-trace-otlp-http": "0.214.0",
|
||||||
"@opentelemetry/sdk-trace-base": "2.6.1",
|
"@opentelemetry/sdk-trace-base": "2.6.1",
|
||||||
"@opentelemetry/sdk-trace-node": "2.6.1",
|
"@opentelemetry/sdk-trace-node": "2.6.1",
|
||||||
"@opentui/core": "0.1.99",
|
"@opentui/core": "catalog:",
|
||||||
"@opentui/solid": "0.1.99",
|
"@opentui/solid": "catalog:",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
@@ -385,7 +386,7 @@
|
|||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
"gitlab-ai-provider": "6.4.2",
|
"gitlab-ai-provider": "6.6.0",
|
||||||
"glob": "13.0.5",
|
"glob": "13.0.5",
|
||||||
"google-auth-library": "10.5.0",
|
"google-auth-library": "10.5.0",
|
||||||
"gray-matter": "4.0.3",
|
"gray-matter": "4.0.3",
|
||||||
@@ -457,23 +458,23 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.4.6",
|
"version": "1.4.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@opentui/core": "0.1.99",
|
"@opentui/core": "catalog:",
|
||||||
"@opentui/solid": "0.1.99",
|
"@opentui/solid": "catalog:",
|
||||||
"@tsconfig/node22": "catalog:",
|
"@tsconfig/node22": "catalog:",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@opentui/core": ">=0.1.99",
|
"@opentui/core": ">=0.1.100",
|
||||||
"@opentui/solid": ">=0.1.99",
|
"@opentui/solid": ">=0.1.100",
|
||||||
},
|
},
|
||||||
"optionalPeers": [
|
"optionalPeers": [
|
||||||
"@opentui/core",
|
"@opentui/core",
|
||||||
@@ -492,7 +493,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.4.6",
|
"version": "1.4.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-spawn": "catalog:",
|
"cross-spawn": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -507,7 +508,7 @@
|
|||||||
},
|
},
|
||||||
"packages/shared": {
|
"packages/shared": {
|
||||||
"name": "@opencode-ai/shared",
|
"name": "@opencode-ai/shared",
|
||||||
"version": "1.4.6",
|
"version": "1.4.11",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -515,6 +516,7 @@
|
|||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
"@npmcli/arborist": "catalog:",
|
"@npmcli/arborist": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
|
"glob": "13.0.5",
|
||||||
"mime-types": "3.0.2",
|
"mime-types": "3.0.2",
|
||||||
"minimatch": "10.2.5",
|
"minimatch": "10.2.5",
|
||||||
"semver": "catalog:",
|
"semver": "catalog:",
|
||||||
@@ -522,13 +524,15 @@
|
|||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@tsconfig/bun": "catalog:",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
|
"@types/npmcli__arborist": "6.3.3",
|
||||||
"@types/semver": "catalog:",
|
"@types/semver": "catalog:",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.4.6",
|
"version": "1.4.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -563,7 +567,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.4.6",
|
"version": "1.4.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -612,7 +616,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.4.6",
|
"version": "1.4.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
@@ -671,6 +675,8 @@
|
|||||||
"@npmcli/arborist": "9.4.0",
|
"@npmcli/arborist": "9.4.0",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||||
|
"@opentui/core": "0.1.99",
|
||||||
|
"@opentui/solid": "0.1.99",
|
||||||
"@pierre/diffs": "1.1.0-beta.18",
|
"@pierre/diffs": "1.1.0-beta.18",
|
||||||
"@playwright/test": "1.59.1",
|
"@playwright/test": "1.59.1",
|
||||||
"@solid-primitives/storage": "4.3.3",
|
"@solid-primitives/storage": "4.3.3",
|
||||||
@@ -686,7 +692,7 @@
|
|||||||
"@types/node": "22.13.9",
|
"@types/node": "22.13.9",
|
||||||
"@types/semver": "7.7.1",
|
"@types/semver": "7.7.1",
|
||||||
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
||||||
"ai": "6.0.158",
|
"ai": "6.0.168",
|
||||||
"cross-spawn": "7.0.6",
|
"cross-spawn": "7.0.6",
|
||||||
"diff": "8.0.2",
|
"diff": "8.0.2",
|
||||||
"dompurify": "3.3.1",
|
"dompurify": "3.3.1",
|
||||||
@@ -734,7 +740,7 @@
|
|||||||
|
|
||||||
"@ai-sdk/alibaba": ["@ai-sdk/alibaba@1.0.17", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZbE+U5bWz2JBc5DERLowx5+TKbjGBE93LqKZAWvuEn7HOSQMraxFMZuc0ST335QZJAyfBOzh7m1mPQ+y7EaaoA=="],
|
"@ai-sdk/alibaba": ["@ai-sdk/alibaba@1.0.17", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZbE+U5bWz2JBc5DERLowx5+TKbjGBE93LqKZAWvuEn7HOSQMraxFMZuc0ST335QZJAyfBOzh7m1mPQ+y7EaaoA=="],
|
||||||
|
|
||||||
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.93", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.69", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hcXDU8QDwpAzLVTuY932TQVlIij9+iaVTxc5mPGY6yb//JMAAC5hMVhg93IrxlrxWLvMgjezNgoZGwquR+SGnw=="],
|
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.95", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-qJKWEy+cNx3bLSJi/XpIVhv0P8KO0JFB1SvEroNWN8gKm820SIglBmXS10DTeXJdM5PPbQX4i/wJj5BHEk2LRQ=="],
|
||||||
|
|
||||||
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rwLi/Rsuj2pYniQXIrvClHvXDzgM4UQHHnvHTWEF14efnlKclG/1ghpNC+adsRujAbCTr6gRsSbDE2vEqriV7g=="],
|
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rwLi/Rsuj2pYniQXIrvClHvXDzgM4UQHHnvHTWEF14efnlKclG/1ghpNC+adsRujAbCTr6gRsSbDE2vEqriV7g=="],
|
||||||
|
|
||||||
@@ -754,11 +760,11 @@
|
|||||||
|
|
||||||
"@ai-sdk/fireworks": ["@ai-sdk/fireworks@2.0.46", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XRKR0zgRyegdmtK5CDUEjlyRp0Fo+XVCdoG+301U1SGtgRIAYG3ObVtgzVJBVpJdHFSLHuYeLTnNiQoUxD7+FQ=="],
|
"@ai-sdk/fireworks": ["@ai-sdk/fireworks@2.0.46", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XRKR0zgRyegdmtK5CDUEjlyRp0Fo+XVCdoG+301U1SGtgRIAYG3ObVtgzVJBVpJdHFSLHuYeLTnNiQoUxD7+FQ=="],
|
||||||
|
|
||||||
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.97", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ERHmVGX30YKTwxObuHQzNqoOf8Nb5WwYMDBn34e3TGGVn0vLEXwMimo7uRVTbhhi4gfu9WtwYTE4x1+csZok1w=="],
|
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.104", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZKX5n74io8VIRlhIMSLWVlvT3sXC8Z7cZ9GHuWBWZDVi96+62AIsWuLGvMfcBA1STYuSoDrp6rIziZmvrTq0TA=="],
|
||||||
|
|
||||||
"@ai-sdk/google": ["@ai-sdk/google@3.0.63", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-RfOZWVMYSPu2sPRfGajrauWAZ9BSaRopSn+AszkKWQ1MFj8nhaXvCqRHB5pBQUaHTfZKagvOmMpNfa/s3gPLgQ=="],
|
"@ai-sdk/google": ["@ai-sdk/google@3.0.63", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-RfOZWVMYSPu2sPRfGajrauWAZ9BSaRopSn+AszkKWQ1MFj8nhaXvCqRHB5pBQUaHTfZKagvOmMpNfa/s3gPLgQ=="],
|
||||||
|
|
||||||
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.109", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.69", "@ai-sdk/google": "3.0.63", "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-QzQ+DgOoSYlkU4mK0H+iaCaW1bl5zOimH9X2E2oylcVyUtAdCuduQ959Uw1ygW3l09J2K/ceEDtK8OUPHyOA7g=="],
|
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.112", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/google": "3.0.64", "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cSfHCkM+9ZrFtQWIN1WlV93JPD+isGSdFxKj7u1L9m2aLVZajlXdcE41GL9hMt7ld7bZYE4NnZ+4VLxBAHE+Eg=="],
|
||||||
|
|
||||||
"@ai-sdk/groq": ["@ai-sdk/groq@3.0.31", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XbbugpnFmXGu2TlXiq8KUJskP6/VVbuFcnFIGDzDIB/Chg6XHsNnqrTF80Zxkh0Pd3+NvbM+2Uqrtsndk6bDAg=="],
|
"@ai-sdk/groq": ["@ai-sdk/groq@3.0.31", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XbbugpnFmXGu2TlXiq8KUJskP6/VVbuFcnFIGDzDIB/Chg6XHsNnqrTF80Zxkh0Pd3+NvbM+2Uqrtsndk6bDAg=="],
|
||||||
|
|
||||||
@@ -1582,7 +1588,7 @@
|
|||||||
|
|
||||||
"@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-logs": "0.214.0", "@opentelemetry/sdk-metrics": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1", "protobufjs": "^7.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DSaYcuBRh6uozfsWN3R8HsN0yDhCuWP7tOFdkUOVaWD1KVJg8m4qiLUsg/tNhTLS9HUYUcwNpwL2eroLtsZZ/w=="],
|
"@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-logs": "0.214.0", "@opentelemetry/sdk-metrics": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1", "protobufjs": "^7.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DSaYcuBRh6uozfsWN3R8HsN0yDhCuWP7tOFdkUOVaWD1KVJg8m4qiLUsg/tNhTLS9HUYUcwNpwL2eroLtsZZ/w=="],
|
||||||
|
|
||||||
"@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
|
"@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="],
|
||||||
|
|
||||||
"@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA=="],
|
"@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA=="],
|
||||||
|
|
||||||
@@ -1680,6 +1686,18 @@
|
|||||||
|
|
||||||
"@oxc-transform/binding-win32-x64-msvc": ["@oxc-transform/binding-win32-x64-msvc@0.96.0", "", { "os": "win32", "cpu": "x64" }, "sha512-0fI0P0W7bSO/GCP/N5dkmtB9vBqCA4ggo1WmXTnxNJVmFFOtcA1vYm1I9jl8fxo+sucW2WnlpnI4fjKdo3JKxA=="],
|
"@oxc-transform/binding-win32-x64-msvc": ["@oxc-transform/binding-win32-x64-msvc@0.96.0", "", { "os": "win32", "cpu": "x64" }, "sha512-0fI0P0W7bSO/GCP/N5dkmtB9vBqCA4ggo1WmXTnxNJVmFFOtcA1vYm1I9jl8fxo+sucW2WnlpnI4fjKdo3JKxA=="],
|
||||||
|
|
||||||
|
"@oxlint-tsgolint/darwin-arm64": ["@oxlint-tsgolint/darwin-arm64@0.21.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-P20j3MLqfwIT+94qGU3htC7dWp4pXGZW1p1p7FRUzu1aopq7c9nPCgf0W/WjktqQ57+iuTq9mbSlwWinl6+H1A=="],
|
||||||
|
|
||||||
|
"@oxlint-tsgolint/darwin-x64": ["@oxlint-tsgolint/darwin-x64@0.21.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-81TmmuBcPedEA0MwRmObuQuXnCprS1UiHQWGe7pseqNAJzUWXeAPrayqKTACX92VpruJI+yvY0XJrFp11PpcTA=="],
|
||||||
|
|
||||||
|
"@oxlint-tsgolint/linux-arm64": ["@oxlint-tsgolint/linux-arm64@0.21.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-sbjBr6zDduX8rNO0PTjhf7VYLCPWqdijWiMPp8e10qu6Tam1GdaVLaLlX8QrNupTgglO1GvqqgY/jcacWL8a6g=="],
|
||||||
|
|
||||||
|
"@oxlint-tsgolint/linux-x64": ["@oxlint-tsgolint/linux-x64@0.21.0", "", { "os": "linux", "cpu": "x64" }, "sha512-jNrOcy53R5TJQfrK444Cm60bW9437xDoxPbm3AdvFSo/fhdFMllawc7uZC2Wzr+EAjTkW13K8R4QHzsUdBG9fQ=="],
|
||||||
|
|
||||||
|
"@oxlint-tsgolint/win32-arm64": ["@oxlint-tsgolint/win32-arm64@0.21.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-xWeRxJJILDE4b9UqHEWGBxcBc1TUS6zWHhxcyxTZMwf4q3wdKeu0OHYAcwLGJzoSjEIf6FTjyfPiRNil2oqsdg=="],
|
||||||
|
|
||||||
|
"@oxlint-tsgolint/win32-x64": ["@oxlint-tsgolint/win32-x64@0.21.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Ob9AA9teI8ckPo1whV1smLr5NrqwgBv/8boDbK0YZG+fKgNGRwr1hBj1ORgFWOQaUBv+5njp5A0RAfJJjQ95QQ=="],
|
||||||
|
|
||||||
"@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.60.0", "", { "os": "android", "cpu": "arm" }, "sha512-YdeJKaZckDQL1qa62a1aKq/goyq48aX3yOxaaWqWb4sau4Ee4IiLbamftNLU3zbePky6QsDj6thnSSzHRBjDfA=="],
|
"@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.60.0", "", { "os": "android", "cpu": "arm" }, "sha512-YdeJKaZckDQL1qa62a1aKq/goyq48aX3yOxaaWqWb4sau4Ee4IiLbamftNLU3zbePky6QsDj6thnSSzHRBjDfA=="],
|
||||||
|
|
||||||
"@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.60.0", "", { "os": "android", "cpu": "arm64" }, "sha512-7ANS7PpXCfq84xZQ8E5WPs14gwcuPcl+/8TFNXfpSu0CQBXz3cUo2fDpHT8v8HJN+Ut02eacvMAzTnc9s6X4tw=="],
|
"@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.60.0", "", { "os": "android", "cpu": "arm64" }, "sha512-7ANS7PpXCfq84xZQ8E5WPs14gwcuPcl+/8TFNXfpSu0CQBXz3cUo2fDpHT8v8HJN+Ut02eacvMAzTnc9s6X4tw=="],
|
||||||
@@ -2438,7 +2456,7 @@
|
|||||||
|
|
||||||
"@valibot/to-json-schema": ["@valibot/to-json-schema@1.6.0", "", { "peerDependencies": { "valibot": "^1.3.0" } }, "sha512-d6rYyK5KVa2XdqamWgZ4/Nr+cXhxjy7lmpe6Iajw15J/jmU+gyxl2IEd1Otg1d7Rl3gOQL5reulnSypzBtYy1A=="],
|
"@valibot/to-json-schema": ["@valibot/to-json-schema@1.6.0", "", { "peerDependencies": { "valibot": "^1.3.0" } }, "sha512-d6rYyK5KVa2XdqamWgZ4/Nr+cXhxjy7lmpe6Iajw15J/jmU+gyxl2IEd1Otg1d7Rl3gOQL5reulnSypzBtYy1A=="],
|
||||||
|
|
||||||
"@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="],
|
"@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="],
|
||||||
|
|
||||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
|
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
|
||||||
|
|
||||||
@@ -2498,7 +2516,7 @@
|
|||||||
|
|
||||||
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
|
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
|
||||||
|
|
||||||
"ai": ["ai@6.0.158", "", { "dependencies": { "@ai-sdk/gateway": "3.0.95", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-gLTp1UXFtMqKUi3XHs33K7UFglbvojkxF/aq337TxnLGOhHIW9+GyP2jwW4hYX87f1es+wId3VQoPRRu9zEStQ=="],
|
"ai": ["ai@6.0.168", "", { "dependencies": { "@ai-sdk/gateway": "3.0.104", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2HqCJuO+1V2aV7vfYs5LFEUfxbkGX+5oa54q/gCCTL7KLTdbxcCu5D7TdLA5kwsrs3Szgjah9q6D9tpjHM3hUQ=="],
|
||||||
|
|
||||||
"ai-gateway-provider": ["ai-gateway-provider@3.1.2", "", { "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^4.0.62", "@ai-sdk/anthropic": "^3.0.46", "@ai-sdk/azure": "^3.0.31", "@ai-sdk/cerebras": "^2.0.34", "@ai-sdk/cohere": "^3.0.21", "@ai-sdk/deepgram": "^2.0.20", "@ai-sdk/deepseek": "^2.0.20", "@ai-sdk/elevenlabs": "^2.0.20", "@ai-sdk/fireworks": "^2.0.34", "@ai-sdk/google": "^3.0.30", "@ai-sdk/google-vertex": "^4.0.61", "@ai-sdk/groq": "^3.0.24", "@ai-sdk/mistral": "^3.0.20", "@ai-sdk/openai": "^3.0.30", "@ai-sdk/perplexity": "^3.0.19", "@ai-sdk/xai": "^3.0.57", "@openrouter/ai-sdk-provider": "^2.2.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^2.0.0", "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.0", "ai": "^6.0.0" } }, "sha512-krGNnJSoO/gJ7Hbe5nQDlsBpDUGIBGtMQTRUaW7s1MylsfvLduba0TLWzQaGtOmNRkP0pGhtGlwsnS6FNQMlyw=="],
|
"ai-gateway-provider": ["ai-gateway-provider@3.1.2", "", { "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^4.0.62", "@ai-sdk/anthropic": "^3.0.46", "@ai-sdk/azure": "^3.0.31", "@ai-sdk/cerebras": "^2.0.34", "@ai-sdk/cohere": "^3.0.21", "@ai-sdk/deepgram": "^2.0.20", "@ai-sdk/deepseek": "^2.0.20", "@ai-sdk/elevenlabs": "^2.0.20", "@ai-sdk/fireworks": "^2.0.34", "@ai-sdk/google": "^3.0.30", "@ai-sdk/google-vertex": "^4.0.61", "@ai-sdk/groq": "^3.0.24", "@ai-sdk/mistral": "^3.0.20", "@ai-sdk/openai": "^3.0.30", "@ai-sdk/perplexity": "^3.0.19", "@ai-sdk/xai": "^3.0.57", "@openrouter/ai-sdk-provider": "^2.2.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^2.0.0", "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.0", "ai": "^6.0.0" } }, "sha512-krGNnJSoO/gJ7Hbe5nQDlsBpDUGIBGtMQTRUaW7s1MylsfvLduba0TLWzQaGtOmNRkP0pGhtGlwsnS6FNQMlyw=="],
|
||||||
|
|
||||||
@@ -3296,7 +3314,7 @@
|
|||||||
|
|
||||||
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
|
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
|
||||||
|
|
||||||
"gitlab-ai-provider": ["gitlab-ai-provider@6.4.2", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-Wyw6uslCuipBOr/NYwAtpgXEUJj68iJY5aekad2DjePN99JetKVQBqkLgAy9PZp2EA4OuscfRQu9qKIBN/evNw=="],
|
"gitlab-ai-provider": ["gitlab-ai-provider@6.6.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-jUxYnKA4XQaPc3wxACDZ8bPDXO0Mzx7cZaBDxbT2uGgLqtGZmSi+9tVNIg7louSS+s/ioVra3SoUz3iOFVhKPA=="],
|
||||||
|
|
||||||
"glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="],
|
"glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="],
|
||||||
|
|
||||||
@@ -4100,6 +4118,8 @@
|
|||||||
|
|
||||||
"oxlint": ["oxlint@1.60.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.60.0", "@oxlint/binding-android-arm64": "1.60.0", "@oxlint/binding-darwin-arm64": "1.60.0", "@oxlint/binding-darwin-x64": "1.60.0", "@oxlint/binding-freebsd-x64": "1.60.0", "@oxlint/binding-linux-arm-gnueabihf": "1.60.0", "@oxlint/binding-linux-arm-musleabihf": "1.60.0", "@oxlint/binding-linux-arm64-gnu": "1.60.0", "@oxlint/binding-linux-arm64-musl": "1.60.0", "@oxlint/binding-linux-ppc64-gnu": "1.60.0", "@oxlint/binding-linux-riscv64-gnu": "1.60.0", "@oxlint/binding-linux-riscv64-musl": "1.60.0", "@oxlint/binding-linux-s390x-gnu": "1.60.0", "@oxlint/binding-linux-x64-gnu": "1.60.0", "@oxlint/binding-linux-x64-musl": "1.60.0", "@oxlint/binding-openharmony-arm64": "1.60.0", "@oxlint/binding-win32-arm64-msvc": "1.60.0", "@oxlint/binding-win32-ia32-msvc": "1.60.0", "@oxlint/binding-win32-x64-msvc": "1.60.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.18.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-tnRzTWiWJ9pg3ftRWnD0+Oqh78L6ZSwcEudvCZaER0PIqiAnNyXj5N1dPwjmNpDalkKS9m/WMLN1CTPUBPmsgw=="],
|
"oxlint": ["oxlint@1.60.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.60.0", "@oxlint/binding-android-arm64": "1.60.0", "@oxlint/binding-darwin-arm64": "1.60.0", "@oxlint/binding-darwin-x64": "1.60.0", "@oxlint/binding-freebsd-x64": "1.60.0", "@oxlint/binding-linux-arm-gnueabihf": "1.60.0", "@oxlint/binding-linux-arm-musleabihf": "1.60.0", "@oxlint/binding-linux-arm64-gnu": "1.60.0", "@oxlint/binding-linux-arm64-musl": "1.60.0", "@oxlint/binding-linux-ppc64-gnu": "1.60.0", "@oxlint/binding-linux-riscv64-gnu": "1.60.0", "@oxlint/binding-linux-riscv64-musl": "1.60.0", "@oxlint/binding-linux-s390x-gnu": "1.60.0", "@oxlint/binding-linux-x64-gnu": "1.60.0", "@oxlint/binding-linux-x64-musl": "1.60.0", "@oxlint/binding-openharmony-arm64": "1.60.0", "@oxlint/binding-win32-arm64-msvc": "1.60.0", "@oxlint/binding-win32-ia32-msvc": "1.60.0", "@oxlint/binding-win32-x64-msvc": "1.60.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.18.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-tnRzTWiWJ9pg3ftRWnD0+Oqh78L6ZSwcEudvCZaER0PIqiAnNyXj5N1dPwjmNpDalkKS9m/WMLN1CTPUBPmsgw=="],
|
||||||
|
|
||||||
|
"oxlint-tsgolint": ["oxlint-tsgolint@0.21.0", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "0.21.0", "@oxlint-tsgolint/darwin-x64": "0.21.0", "@oxlint-tsgolint/linux-arm64": "0.21.0", "@oxlint-tsgolint/linux-x64": "0.21.0", "@oxlint-tsgolint/win32-arm64": "0.21.0", "@oxlint-tsgolint/win32-x64": "0.21.0" }, "bin": { "tsgolint": "bin/tsgolint.js" } }, "sha512-HiWPhANwRnN1pZJQ2SgNB3WRR+1etLJHmRzQ/MJhyINsEIaOUCjxhlXJKbEaVUwdnyXwRWqo/P9Fx21lz0/mSg=="],
|
||||||
|
|
||||||
"p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="],
|
"p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="],
|
||||||
|
|
||||||
"p-defer": ["p-defer@3.0.0", "", {}, "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw=="],
|
"p-defer": ["p-defer@3.0.0", "", {}, "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw=="],
|
||||||
@@ -5134,7 +5154,11 @@
|
|||||||
|
|
||||||
"@ai-sdk/alibaba/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
"@ai-sdk/alibaba/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
||||||
|
|
||||||
"@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.69", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LshR7X3pFugY0o41G2VKTmg1XoGpSl7uoYWfzk6zjVZLhCfeFiwgpOga+eTV4XY1VVpZwKVqRnkDbIL7K2eH5g=="],
|
"@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.71", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bUWOzrzR0gJKJO/PLGMR4uH2dqEgqGhrsCV+sSpk4KtOEnUQlfjZI/F7BFlqSvVpFbjdgYRRLysAeEZpJ6S1lg=="],
|
||||||
|
|
||||||
|
"@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.13", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.0", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-vYahwBAtRaAcFbOmE9aLr12z7RiHYDSLcnogSdxfm7kKfsNa3wH+NU5r7vTeB5rKvLsWyPjVX8iH94brP7umiQ=="],
|
||||||
|
|
||||||
|
"@ai-sdk/amazon-bedrock/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
||||||
|
|
||||||
"@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
"@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
|
||||||
|
|
||||||
@@ -5148,7 +5172,9 @@
|
|||||||
|
|
||||||
"@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
"@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
||||||
|
|
||||||
"@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.69", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LshR7X3pFugY0o41G2VKTmg1XoGpSl7uoYWfzk6zjVZLhCfeFiwgpOga+eTV4XY1VVpZwKVqRnkDbIL7K2eH5g=="],
|
"@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.71", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bUWOzrzR0gJKJO/PLGMR4uH2dqEgqGhrsCV+sSpk4KtOEnUQlfjZI/F7BFlqSvVpFbjdgYRRLysAeEZpJ6S1lg=="],
|
||||||
|
|
||||||
|
"@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CbR82EgGPNrj/6q0HtclwuCqe0/pDShyv3nWDP/A9DroujzWXnLMlUJVrgPOsg4b40zQCwwVs2XSKCxvt/4QaA=="],
|
||||||
|
|
||||||
"@ai-sdk/google-vertex/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
"@ai-sdk/google-vertex/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
|
||||||
|
|
||||||
@@ -5558,6 +5584,18 @@
|
|||||||
|
|
||||||
"@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="],
|
"@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="],
|
||||||
|
|
||||||
|
"@opentelemetry/exporter-trace-otlp-http/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
|
||||||
|
|
||||||
|
"@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
|
||||||
|
|
||||||
|
"@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="],
|
||||||
|
|
||||||
|
"@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
|
||||||
|
|
||||||
|
"@opentelemetry/sdk-metrics/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
|
||||||
|
|
||||||
|
"@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
|
||||||
|
|
||||||
"@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
|
"@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
|
||||||
|
|
||||||
"@opentui/solid/babel-preset-solid": ["babel-preset-solid@1.9.10", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.3" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.10" }, "optionalPeers": ["solid-js"] }, "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ=="],
|
"@opentui/solid/babel-preset-solid": ["babel-preset-solid@1.9.10", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.3" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.10" }, "optionalPeers": ["solid-js"] }, "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ=="],
|
||||||
@@ -5664,7 +5702,7 @@
|
|||||||
|
|
||||||
"accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
"accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
||||||
|
|
||||||
"ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.95", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZmUNNbZl3V42xwQzPaNUi+s8eqR2lnrxf0bvB6YbLXpLjHYv0k2Y78t12cNOfY0bxGeuVVTLyk856uLuQIuXEQ=="],
|
"ai-gateway-provider/@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.93", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.69", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hcXDU8QDwpAzLVTuY932TQVlIij9+iaVTxc5mPGY6yb//JMAAC5hMVhg93IrxlrxWLvMgjezNgoZGwquR+SGnw=="],
|
||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.69", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LshR7X3pFugY0o41G2VKTmg1XoGpSl7uoYWfzk6zjVZLhCfeFiwgpOga+eTV4XY1VVpZwKVqRnkDbIL7K2eH5g=="],
|
"ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.69", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LshR7X3pFugY0o41G2VKTmg1XoGpSl7uoYWfzk6zjVZLhCfeFiwgpOga+eTV4XY1VVpZwKVqRnkDbIL7K2eH5g=="],
|
||||||
|
|
||||||
@@ -5882,7 +5920,7 @@
|
|||||||
|
|
||||||
"nypm/tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="],
|
"nypm/tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="],
|
||||||
|
|
||||||
"opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.67", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-FFX4P5Fd6lcQJc2OLngZQkbbJHa0IDDZi087Edb8qRZx6h90krtM61ArbMUL8us/7ZUwojCXnyJ/wQ2Eflx2jQ=="],
|
"opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.71", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bUWOzrzR0gJKJO/PLGMR4uH2dqEgqGhrsCV+sSpk4KtOEnUQlfjZI/F7BFlqSvVpFbjdgYRRLysAeEZpJ6S1lg=="],
|
||||||
|
|
||||||
"opencode/@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="],
|
"opencode/@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="],
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -513,7 +513,7 @@ async function subscribeSessionEvents() {
|
|||||||
const decoder = new TextDecoder()
|
const decoder = new TextDecoder()
|
||||||
|
|
||||||
let text = ""
|
let text = ""
|
||||||
;(async () => {
|
void (async () => {
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
const { done, value } = await reader.read()
|
const { done, value } = await reader.read()
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-VIgTxIjmZ4Bfwwdj/YFmRJdBpPHYhJSY31kh06EXX+0=",
|
"x86_64-linux": "sha256-GjpBQhvGLTM6NWX29b/mS+KjrQPl0w9VjQHH5jaK9SM=",
|
||||||
"aarch64-linux": "sha256-9118AS1ED0nrliURgZYBRuF/18RqXpUouhYJRlZ6jeA=",
|
"aarch64-linux": "sha256-F5h9p+iZ8CASdUYaYR7O22NwBRa/iT+ZinUxO8lbPTc=",
|
||||||
"aarch64-darwin": "sha256-ppo3MfSIGKQHJCdYEZiLFRc61PtcJ9J0kAXH1pNIonA=",
|
"aarch64-darwin": "sha256-jWo5yvCtjVKRf9i5XUcTTaLtj2+G6+T1Td2llO/cT5I=",
|
||||||
"x86_64-darwin": "sha256-m+CZSOglBCTfNzbdBX6hXdDqqOzHNMzAddVp6BZVDtU="
|
"x86_64-darwin": "sha256-LzV+5/8P2mkiFHmt+a8zDeJjRbU8z9nssSA4tzv1HxA="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ stdenvNoCC.mkDerivation {
|
|||||||
--filter './packages/opencode' \
|
--filter './packages/opencode' \
|
||||||
--filter './packages/desktop' \
|
--filter './packages/desktop' \
|
||||||
--filter './packages/app' \
|
--filter './packages/app' \
|
||||||
|
--filter './packages/shared' \
|
||||||
--frozen-lockfile \
|
--frozen-lockfile \
|
||||||
--ignore-scripts \
|
--ignore-scripts \
|
||||||
--no-progress
|
--no-progress
|
||||||
|
|||||||
+4
-1
@@ -34,6 +34,8 @@
|
|||||||
"@types/cross-spawn": "6.0.6",
|
"@types/cross-spawn": "6.0.6",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
"@hono/zod-validator": "0.4.2",
|
"@hono/zod-validator": "0.4.2",
|
||||||
|
"@opentui/core": "0.1.99",
|
||||||
|
"@opentui/solid": "0.1.99",
|
||||||
"ulid": "3.0.1",
|
"ulid": "3.0.1",
|
||||||
"@kobalte/core": "0.13.11",
|
"@kobalte/core": "0.13.11",
|
||||||
"@types/luxon": "3.7.1",
|
"@types/luxon": "3.7.1",
|
||||||
@@ -51,7 +53,7 @@
|
|||||||
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
|
||||||
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
|
||||||
"effect": "4.0.0-beta.48",
|
"effect": "4.0.0-beta.48",
|
||||||
"ai": "6.0.158",
|
"ai": "6.0.168",
|
||||||
"cross-spawn": "7.0.6",
|
"cross-spawn": "7.0.6",
|
||||||
"hono": "4.10.7",
|
"hono": "4.10.7",
|
||||||
"hono-openapi": "1.1.2",
|
"hono-openapi": "1.1.2",
|
||||||
@@ -87,6 +89,7 @@
|
|||||||
"glob": "13.0.5",
|
"glob": "13.0.5",
|
||||||
"husky": "9.1.7",
|
"husky": "9.1.7",
|
||||||
"oxlint": "1.60.0",
|
"oxlint": "1.60.0",
|
||||||
|
"oxlint-tsgolint": "0.21.0",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"semver": "^7.6.0",
|
"semver": "^7.6.0",
|
||||||
"sst": "3.18.10",
|
"sst": "3.18.10",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.4.6",
|
"version": "1.4.11",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -121,10 +121,10 @@ function SessionProviders(props: ParentProps) {
|
|||||||
function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
|
function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
|
||||||
return (
|
return (
|
||||||
<AppShellProviders>
|
<AppShellProviders>
|
||||||
<Suspense fallback={<Loading />}>
|
{/*<Suspense fallback={<Loading />}>*/}
|
||||||
{props.appChildren}
|
{props.appChildren}
|
||||||
{props.children}
|
{props.children}
|
||||||
</Suspense>
|
{/*</Suspense>*/}
|
||||||
</AppShellProviders>
|
</AppShellProviders>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -184,32 +184,41 @@ function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show
|
<Suspense
|
||||||
when={checkMode() === "blocking" ? !startupHealthCheck.loading : startupHealthCheck.state !== "pending"}
|
|
||||||
fallback={
|
fallback={
|
||||||
<div class="h-dvh w-screen flex flex-col items-center justify-center bg-background-base">
|
<div class="h-dvh w-screen flex flex-col items-center justify-center bg-background-base">
|
||||||
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
|
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
{/*<Show
|
||||||
|
when={checkMode() === "blocking" ? !startupHealthCheck.loading : startupHealthCheck.state !== "pending"}
|
||||||
|
fallback={
|
||||||
|
<div class="h-dvh w-screen flex flex-col items-center justify-center bg-background-base">
|
||||||
|
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>*/}
|
||||||
|
{checkMode() === "blocking" ? startupHealthCheck() : startupHealthCheck.latest}
|
||||||
<Show
|
<Show
|
||||||
when={startupHealthCheck()}
|
when={startupHealthCheck()}
|
||||||
fallback={
|
fallback={
|
||||||
<ConnectionError
|
<ConnectionError
|
||||||
onRetry={() => {
|
onRetry={() => {
|
||||||
if (checkMode() === "background") healthCheckActions.refetch()
|
if (checkMode() === "background") void healthCheckActions.refetch()
|
||||||
}}
|
}}
|
||||||
onServerSelected={(key) => {
|
onServerSelected={(key) => {
|
||||||
setCheckMode("blocking")
|
setCheckMode("blocking")
|
||||||
server.setActive(key)
|
server.setActive(key)
|
||||||
healthCheckActions.refetch()
|
void healthCheckActions.refetch()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
{/*</Show>*/}
|
||||||
|
</Suspense>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -327,7 +327,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
if (loading()) return
|
if (loading()) return
|
||||||
if (methods().length === 1) {
|
if (methods().length === 1) {
|
||||||
auto = true
|
auto = true
|
||||||
selectMethod(0)
|
void selectMethod(0)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -373,7 +373,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
|||||||
key={(m) => m?.label}
|
key={(m) => m?.label}
|
||||||
onSelect={async (selected, index) => {
|
onSelect={async (selected, index) => {
|
||||||
if (!selected) return
|
if (!selected) return
|
||||||
selectMethod(index)
|
void selectMethod(index)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(i) => (
|
{(i) => (
|
||||||
|
|||||||
@@ -348,8 +348,8 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
|||||||
|
|
||||||
const open = (path: string) => {
|
const open = (path: string) => {
|
||||||
const value = file.tab(path)
|
const value = file.tab(path)
|
||||||
tabs().open(value)
|
void tabs().open(value)
|
||||||
file.load(path)
|
void file.load(path)
|
||||||
if (!view().reviewPanel.opened()) view().reviewPanel.open()
|
if (!view().reviewPanel.opened()) view().reviewPanel.open()
|
||||||
layout.fileTree.setTab("all")
|
layout.fileTree.setTab("all")
|
||||||
props.onOpenFile?.(path)
|
props.onOpenFile?.(path)
|
||||||
|
|||||||
@@ -344,7 +344,7 @@ export function DialogSelectServer() {
|
|||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
items()
|
items()
|
||||||
refreshHealth()
|
void refreshHealth()
|
||||||
const interval = setInterval(refreshHealth, 10_000)
|
const interval = setInterval(refreshHealth, 10_000)
|
||||||
onCleanup(() => clearInterval(interval))
|
onCleanup(() => clearInterval(interval))
|
||||||
})
|
})
|
||||||
@@ -498,7 +498,7 @@ export function DialogSelectServer() {
|
|||||||
async function handleRemove(url: ServerConnection.Key) {
|
async function handleRemove(url: ServerConnection.Key) {
|
||||||
server.remove(url)
|
server.remove(url)
|
||||||
if ((await platform.getDefaultServer?.()) === url) {
|
if ((await platform.getDefaultServer?.()) === url) {
|
||||||
platform.setDefaultServer?.(null)
|
void platform.setDefaultServer?.(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -536,7 +536,7 @@ export function DialogSelectServer() {
|
|||||||
items={sortedItems}
|
items={sortedItems}
|
||||||
key={(x) => x.http.url}
|
key={(x) => x.http.url}
|
||||||
onSelect={(x) => {
|
onSelect={(x) => {
|
||||||
if (x) select(x)
|
if (x) void select(x)
|
||||||
}}
|
}}
|
||||||
divider={true}
|
divider={true}
|
||||||
class="px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]h-[300px] [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:min-h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent"
|
class="px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]h-[300px] [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:min-h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent"
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ import { PromptImageAttachments } from "./prompt-input/image-attachments"
|
|||||||
import { PromptDragOverlay } from "./prompt-input/drag-overlay"
|
import { PromptDragOverlay } from "./prompt-input/drag-overlay"
|
||||||
import { promptPlaceholder } from "./prompt-input/placeholder"
|
import { promptPlaceholder } from "./prompt-input/placeholder"
|
||||||
import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
||||||
|
import { useQuery } from "@tanstack/solid-query"
|
||||||
|
import { loadAgentsQuery, loadProvidersQuery } from "@/context/global-sync/bootstrap"
|
||||||
|
|
||||||
interface PromptInputProps {
|
interface PromptInputProps {
|
||||||
class?: string
|
class?: string
|
||||||
@@ -100,6 +102,7 @@ const NON_EMPTY_TEXT = /[^\s\u200B]/
|
|||||||
|
|
||||||
export const PromptInput: Component<PromptInputProps> = (props) => {
|
export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
|
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
const files = useFile()
|
const files = useFile()
|
||||||
@@ -212,9 +215,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
if (!view().reviewPanel.opened()) view().reviewPanel.open()
|
if (!view().reviewPanel.opened()) view().reviewPanel.open()
|
||||||
layout.fileTree.setTab("all")
|
layout.fileTree.setTab("all")
|
||||||
const tab = files.tab(item.path)
|
const tab = files.tab(item.path)
|
||||||
tabs().open(tab)
|
void tabs().open(tab)
|
||||||
tabs().setActive(tab)
|
tabs().setActive(tab)
|
||||||
Promise.resolve(files.load(item.path)).finally(() => queueCommentFocus())
|
void Promise.resolve(files.load(item.path)).finally(() => queueCommentFocus())
|
||||||
}
|
}
|
||||||
|
|
||||||
const recent = createMemo(() => {
|
const recent = createMemo(() => {
|
||||||
@@ -1139,7 +1142,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (working()) {
|
if (working()) {
|
||||||
abort()
|
void abort()
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
return
|
return
|
||||||
@@ -1205,7 +1208,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (working()) {
|
if (working()) {
|
||||||
abort()
|
void abort()
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -1245,10 +1248,18 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
handleSubmit(event)
|
void handleSubmit(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const agentsQuery = useQuery(() => loadAgentsQuery(sdk.directory))
|
||||||
|
const agentsLoading = () => agentsQuery.isLoading
|
||||||
|
|
||||||
|
const globalProvidersQuery = useQuery(() => loadProvidersQuery(null))
|
||||||
|
const providersQuery = useQuery(() => loadProvidersQuery(sdk.directory))
|
||||||
|
|
||||||
|
const providersLoading = () => agentsLoading() || providersQuery.isLoading || globalProvidersQuery.isLoading
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="relative size-full _max-h-[320px] flex flex-col gap-0">
|
<div class="relative size-full _max-h-[320px] flex flex-col gap-0">
|
||||||
<PromptPopover
|
<PromptPopover
|
||||||
@@ -1444,53 +1455,89 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
<span class="truncate text-13-medium text-text-strong">{language.t("prompt.mode.shell")}</span>
|
<span class="truncate text-13-medium text-text-strong">{language.t("prompt.mode.shell")}</span>
|
||||||
<div class="size-4 shrink-0" />
|
<div class="size-4 shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1.5 min-w-0 flex-1">
|
<div class="flex items-center gap-1.5 min-w-0 flex-1 h-7">
|
||||||
<div data-component="prompt-agent-control">
|
<Show when={!agentsLoading()}>
|
||||||
<TooltipKeybind
|
<div data-component="prompt-agent-control">
|
||||||
placement="top"
|
<TooltipKeybind
|
||||||
gutter={4}
|
placement="top"
|
||||||
title={language.t("command.agent.cycle")}
|
gutter={4}
|
||||||
keybind={command.keybind("agent.cycle")}
|
title={language.t("command.agent.cycle")}
|
||||||
>
|
keybind={command.keybind("agent.cycle")}
|
||||||
<Select
|
>
|
||||||
size="normal"
|
<Select
|
||||||
options={agentNames()}
|
size="normal"
|
||||||
current={local.agent.current()?.name ?? ""}
|
options={agentNames()}
|
||||||
onSelect={(value) => {
|
current={local.agent.current()?.name ?? ""}
|
||||||
local.agent.set(value)
|
onSelect={(value) => {
|
||||||
restoreFocus()
|
local.agent.set(value)
|
||||||
}}
|
restoreFocus()
|
||||||
class="capitalize max-w-[160px] text-text-base"
|
}}
|
||||||
valueClass="truncate text-13-regular text-text-base"
|
class="capitalize max-w-[160px] text-text-base"
|
||||||
triggerStyle={control()}
|
valueClass="truncate text-13-regular text-text-base"
|
||||||
triggerProps={{ "data-action": "prompt-agent" }}
|
triggerStyle={control()}
|
||||||
variant="ghost"
|
triggerProps={{ "data-action": "prompt-agent" }}
|
||||||
/>
|
variant="ghost"
|
||||||
</TooltipKeybind>
|
/>
|
||||||
</div>
|
</TooltipKeybind>
|
||||||
<Show when={store.mode !== "shell"}>
|
</div>
|
||||||
<div data-component="prompt-model-control">
|
</Show>
|
||||||
<Show
|
<Show when={!providersLoading()}>
|
||||||
when={providers.paid().length > 0}
|
<Show when={store.mode !== "shell"}>
|
||||||
fallback={
|
<div data-component="prompt-model-control">
|
||||||
|
<Show
|
||||||
|
when={providers.paid().length > 0}
|
||||||
|
fallback={
|
||||||
|
<TooltipKeybind
|
||||||
|
placement="top"
|
||||||
|
gutter={4}
|
||||||
|
title={language.t("command.model.choose")}
|
||||||
|
keybind={command.keybind("model.choose")}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
data-action="prompt-model"
|
||||||
|
as="div"
|
||||||
|
variant="ghost"
|
||||||
|
size="normal"
|
||||||
|
class="min-w-0 max-w-[320px] text-13-regular text-text-base group"
|
||||||
|
style={control()}
|
||||||
|
onClick={() => {
|
||||||
|
void import("@/components/dialog-select-model-unpaid").then((x) => {
|
||||||
|
dialog.show(() => <x.DialogSelectModelUnpaid model={local.model} />)
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Show when={local.model.current()?.provider?.id}>
|
||||||
|
<ProviderIcon
|
||||||
|
id={local.model.current()?.provider?.id ?? ""}
|
||||||
|
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
||||||
|
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
<span class="truncate">
|
||||||
|
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
||||||
|
</span>
|
||||||
|
<Icon name="chevron-down" size="small" class="shrink-0" />
|
||||||
|
</Button>
|
||||||
|
</TooltipKeybind>
|
||||||
|
}
|
||||||
|
>
|
||||||
<TooltipKeybind
|
<TooltipKeybind
|
||||||
placement="top"
|
placement="top"
|
||||||
gutter={4}
|
gutter={4}
|
||||||
title={language.t("command.model.choose")}
|
title={language.t("command.model.choose")}
|
||||||
keybind={command.keybind("model.choose")}
|
keybind={command.keybind("model.choose")}
|
||||||
>
|
>
|
||||||
<Button
|
<ModelSelectorPopover
|
||||||
data-action="prompt-model"
|
model={local.model}
|
||||||
as="div"
|
triggerAs={Button}
|
||||||
variant="ghost"
|
triggerProps={{
|
||||||
size="normal"
|
variant: "ghost",
|
||||||
class="min-w-0 max-w-[320px] text-13-regular text-text-base group"
|
size: "normal",
|
||||||
style={control()}
|
style: control(),
|
||||||
onClick={() => {
|
class: "min-w-0 max-w-[320px] text-13-regular text-text-base group",
|
||||||
void import("@/components/dialog-select-model-unpaid").then((x) => {
|
"data-action": "prompt-model",
|
||||||
dialog.show(() => <x.DialogSelectModelUnpaid model={local.model} />)
|
|
||||||
})
|
|
||||||
}}
|
}}
|
||||||
|
onClose={restoreFocus}
|
||||||
>
|
>
|
||||||
<Show when={local.model.current()?.provider?.id}>
|
<Show when={local.model.current()?.provider?.id}>
|
||||||
<ProviderIcon
|
<ProviderIcon
|
||||||
@@ -1503,67 +1550,35 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
||||||
</span>
|
</span>
|
||||||
<Icon name="chevron-down" size="small" class="shrink-0" />
|
<Icon name="chevron-down" size="small" class="shrink-0" />
|
||||||
</Button>
|
</ModelSelectorPopover>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
}
|
</Show>
|
||||||
>
|
</div>
|
||||||
|
<div data-component="prompt-variant-control">
|
||||||
<TooltipKeybind
|
<TooltipKeybind
|
||||||
placement="top"
|
placement="top"
|
||||||
gutter={4}
|
gutter={4}
|
||||||
title={language.t("command.model.choose")}
|
title={language.t("command.model.variant.cycle")}
|
||||||
keybind={command.keybind("model.choose")}
|
keybind={command.keybind("model.variant.cycle")}
|
||||||
>
|
>
|
||||||
<ModelSelectorPopover
|
<Select
|
||||||
model={local.model}
|
size="normal"
|
||||||
triggerAs={Button}
|
options={variants()}
|
||||||
triggerProps={{
|
current={local.model.variant.current() ?? "default"}
|
||||||
variant: "ghost",
|
label={(x) => (x === "default" ? language.t("common.default") : x)}
|
||||||
size: "normal",
|
onSelect={(value) => {
|
||||||
style: control(),
|
local.model.variant.set(value === "default" ? undefined : value)
|
||||||
class: "min-w-0 max-w-[320px] text-13-regular text-text-base group",
|
restoreFocus()
|
||||||
"data-action": "prompt-model",
|
|
||||||
}}
|
}}
|
||||||
onClose={restoreFocus}
|
class="capitalize max-w-[160px] text-text-base"
|
||||||
>
|
valueClass="truncate text-13-regular text-text-base"
|
||||||
<Show when={local.model.current()?.provider?.id}>
|
triggerStyle={control()}
|
||||||
<ProviderIcon
|
triggerProps={{ "data-action": "prompt-model-variant" }}
|
||||||
id={local.model.current()?.provider?.id ?? ""}
|
variant="ghost"
|
||||||
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
/>
|
||||||
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
<span class="truncate">
|
|
||||||
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
|
||||||
</span>
|
|
||||||
<Icon name="chevron-down" size="small" class="shrink-0" />
|
|
||||||
</ModelSelectorPopover>
|
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
</Show>
|
</div>
|
||||||
</div>
|
</Show>
|
||||||
<div data-component="prompt-variant-control">
|
|
||||||
<TooltipKeybind
|
|
||||||
placement="top"
|
|
||||||
gutter={4}
|
|
||||||
title={language.t("command.model.variant.cycle")}
|
|
||||||
keybind={command.keybind("model.variant.cycle")}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
size="normal"
|
|
||||||
options={variants()}
|
|
||||||
current={local.model.variant.current() ?? "default"}
|
|
||||||
label={(x) => (x === "default" ? language.t("common.default") : x)}
|
|
||||||
onSelect={(value) => {
|
|
||||||
local.model.variant.set(value === "default" ? undefined : value)
|
|
||||||
restoreFocus()
|
|
||||||
}}
|
|
||||||
class="capitalize max-w-[160px] text-text-base"
|
|
||||||
valueClass="truncate text-13-regular text-text-base"
|
|
||||||
triggerStyle={control()}
|
|
||||||
triggerProps={{ "data-action": "prompt-model-variant" }}
|
|
||||||
variant="ghost"
|
|
||||||
/>
|
|
||||||
</TooltipKeybind>
|
|
||||||
</div>
|
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -295,7 +295,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
const mode = input.mode()
|
const mode = input.mode()
|
||||||
|
|
||||||
if (text.trim().length === 0 && images.length === 0 && input.commentCount() === 0) {
|
if (text.trim().length === 0 && images.length === 0 && input.commentCount() === 0) {
|
||||||
if (input.working()) abort()
|
if (input.working()) void abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ function openSessionContext(args: {
|
|||||||
}) {
|
}) {
|
||||||
if (!args.view.reviewPanel.opened()) args.view.reviewPanel.open()
|
if (!args.view.reviewPanel.opened()) args.view.reviewPanel.open()
|
||||||
if (args.layout.fileTree.opened() && args.layout.fileTree.tab() !== "all") args.layout.fileTree.setTab("all")
|
if (args.layout.fileTree.opened() && args.layout.fileTree.tab() !== "all") args.layout.fileTree.setTab("all")
|
||||||
args.tabs.open("context")
|
void args.tabs.open("context")
|
||||||
args.tabs.setActive("context")
|
args.tabs.setActive("context")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Spinner } from "@opencode-ai/ui/spinner"
|
|||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||||
import { getFilename } from "@opencode-ai/shared/util/path"
|
import { getFilename } from "@opencode-ai/shared/util/path"
|
||||||
import { createEffect, createMemo, For, Show } from "solid-js"
|
import { createEffect, createMemo, createSignal, For, onMount, Show } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { Portal } from "solid-js/web"
|
import { Portal } from "solid-js/web"
|
||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
@@ -16,6 +16,7 @@ import { useLanguage } from "@/context/language"
|
|||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useServer } from "@/context/server"
|
import { useServer } from "@/context/server"
|
||||||
|
import { useSettings } from "@/context/settings"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useTerminal } from "@/context/terminal"
|
import { useTerminal } from "@/context/terminal"
|
||||||
import { focusTerminalById } from "@/pages/session/helpers"
|
import { focusTerminalById } from "@/pages/session/helpers"
|
||||||
@@ -134,6 +135,7 @@ export function SessionHeader() {
|
|||||||
const server = useServer()
|
const server = useServer()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
const settings = useSettings()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const terminal = useTerminal()
|
const terminal = useTerminal()
|
||||||
const { params, view } = useSessionLayout()
|
const { params, view } = useSessionLayout()
|
||||||
@@ -151,6 +153,11 @@ export function SessionHeader() {
|
|||||||
})
|
})
|
||||||
const hotkey = createMemo(() => command.keybind("file.open"))
|
const hotkey = createMemo(() => command.keybind("file.open"))
|
||||||
const os = createMemo(() => detectOS(platform))
|
const os = createMemo(() => detectOS(platform))
|
||||||
|
const isDesktopBeta = platform.platform === "desktop" && import.meta.env.VITE_OPENCODE_CHANNEL === "beta"
|
||||||
|
const search = createMemo(() => !isDesktopBeta || settings.general.showSearch())
|
||||||
|
const tree = createMemo(() => !isDesktopBeta || settings.general.showFileTree())
|
||||||
|
const term = createMemo(() => !isDesktopBeta || settings.general.showTerminal())
|
||||||
|
const status = createMemo(() => !isDesktopBeta || settings.general.showStatus())
|
||||||
|
|
||||||
const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({
|
const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({
|
||||||
finder: true,
|
finder: true,
|
||||||
@@ -262,12 +269,16 @@ export function SessionHeader() {
|
|||||||
.catch((err: unknown) => showRequestError(language, err))
|
.catch((err: unknown) => showRequestError(language, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
const centerMount = createMemo(() => document.getElementById("opencode-titlebar-center"))
|
const [centerMount, setCenterMount] = createSignal<HTMLElement | null>(null)
|
||||||
const rightMount = createMemo(() => document.getElementById("opencode-titlebar-right"))
|
const [rightMount, setRightMount] = createSignal<HTMLElement | null>(null)
|
||||||
|
onMount(() => {
|
||||||
|
setCenterMount(document.getElementById("opencode-titlebar-center"))
|
||||||
|
setRightMount(document.getElementById("opencode-titlebar-right"))
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Show when={centerMount()}>
|
<Show when={search() && centerMount()}>
|
||||||
{(mount) => (
|
{(mount) => (
|
||||||
<Portal mount={mount()}>
|
<Portal mount={mount()}>
|
||||||
<Button
|
<Button
|
||||||
@@ -415,24 +426,28 @@ export function SessionHeader() {
|
|||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<Tooltip placement="bottom" value={language.t("status.popover.trigger")}>
|
<Show when={status()}>
|
||||||
<StatusPopover />
|
<Tooltip placement="bottom" value={language.t("status.popover.trigger")}>
|
||||||
</Tooltip>
|
<StatusPopover />
|
||||||
<TooltipKeybind
|
</Tooltip>
|
||||||
title={language.t("command.terminal.toggle")}
|
</Show>
|
||||||
keybind={command.keybind("terminal.toggle")}
|
<Show when={term()}>
|
||||||
>
|
<TooltipKeybind
|
||||||
<Button
|
title={language.t("command.terminal.toggle")}
|
||||||
variant="ghost"
|
keybind={command.keybind("terminal.toggle")}
|
||||||
class="group/terminal-toggle titlebar-icon w-8 h-6 p-0 box-border shrink-0"
|
|
||||||
onClick={toggleTerminal}
|
|
||||||
aria-label={language.t("command.terminal.toggle")}
|
|
||||||
aria-expanded={view().terminal.opened()}
|
|
||||||
aria-controls="terminal-panel"
|
|
||||||
>
|
>
|
||||||
<Icon size="small" name={view().terminal.opened() ? "terminal-active" : "terminal"} />
|
<Button
|
||||||
</Button>
|
variant="ghost"
|
||||||
</TooltipKeybind>
|
class="group/terminal-toggle titlebar-icon w-8 h-6 p-0 box-border shrink-0"
|
||||||
|
onClick={toggleTerminal}
|
||||||
|
aria-label={language.t("command.terminal.toggle")}
|
||||||
|
aria-expanded={view().terminal.opened()}
|
||||||
|
aria-controls="terminal-panel"
|
||||||
|
>
|
||||||
|
<Icon size="small" name={view().terminal.opened() ? "terminal-active" : "terminal"} />
|
||||||
|
</Button>
|
||||||
|
</TooltipKeybind>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<div class="hidden md:flex items-center gap-1 shrink-0">
|
<div class="hidden md:flex items-center gap-1 shrink-0">
|
||||||
<TooltipKeybind
|
<TooltipKeybind
|
||||||
@@ -451,30 +466,32 @@ export function SessionHeader() {
|
|||||||
</Button>
|
</Button>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
|
|
||||||
<TooltipKeybind
|
<Show when={tree()}>
|
||||||
title={language.t("command.fileTree.toggle")}
|
<TooltipKeybind
|
||||||
keybind={command.keybind("fileTree.toggle")}
|
title={language.t("command.fileTree.toggle")}
|
||||||
>
|
keybind={command.keybind("fileTree.toggle")}
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
class="titlebar-icon w-8 h-6 p-0 box-border"
|
|
||||||
onClick={() => layout.fileTree.toggle()}
|
|
||||||
aria-label={language.t("command.fileTree.toggle")}
|
|
||||||
aria-expanded={layout.fileTree.opened()}
|
|
||||||
aria-controls="file-tree-panel"
|
|
||||||
>
|
>
|
||||||
<div class="relative flex items-center justify-center size-4">
|
<Button
|
||||||
<Icon
|
variant="ghost"
|
||||||
size="small"
|
class="titlebar-icon w-8 h-6 p-0 box-border"
|
||||||
name={layout.fileTree.opened() ? "file-tree-active" : "file-tree"}
|
onClick={() => layout.fileTree.toggle()}
|
||||||
classList={{
|
aria-label={language.t("command.fileTree.toggle")}
|
||||||
"text-icon-strong": layout.fileTree.opened(),
|
aria-expanded={layout.fileTree.opened()}
|
||||||
"text-icon-weak": !layout.fileTree.opened(),
|
aria-controls="file-tree-panel"
|
||||||
}}
|
>
|
||||||
/>
|
<div class="relative flex items-center justify-center size-4">
|
||||||
</div>
|
<Icon
|
||||||
</Button>
|
size="small"
|
||||||
</TooltipKeybind>
|
name={layout.fileTree.opened() ? "file-tree-active" : "file-tree"}
|
||||||
|
classList={{
|
||||||
|
"text-icon-strong": layout.fileTree.opened(),
|
||||||
|
"text-icon-weak": !layout.fileTree.opened(),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</TooltipKeybind>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
|
|||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
const count = terminal.all().length
|
const count = terminal.all().length
|
||||||
terminal.close(props.terminal.id)
|
void terminal.close(props.terminal.id)
|
||||||
if (count === 1) {
|
if (count === 1) {
|
||||||
props.onClose?.()
|
props.onClose?.()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
|
|
||||||
permission.disableAutoAccept(params.id, value)
|
permission.disableAutoAccept(params.id, value)
|
||||||
}
|
}
|
||||||
|
const desktop = createMemo(() => platform.platform === "desktop")
|
||||||
|
|
||||||
const check = () => {
|
const check = () => {
|
||||||
if (!platform.checkUpdate) return
|
if (!platform.checkUpdate) return
|
||||||
@@ -279,6 +280,74 @@ export const SettingsGeneral: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const AdvancedSection = () => (
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.advanced")}</h3>
|
||||||
|
|
||||||
|
<SettingsList>
|
||||||
|
<SettingsRow
|
||||||
|
title={language.t("settings.general.row.showFileTree.title")}
|
||||||
|
description={language.t("settings.general.row.showFileTree.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-show-file-tree">
|
||||||
|
<Switch
|
||||||
|
checked={settings.general.showFileTree()}
|
||||||
|
onChange={(checked) => settings.general.setShowFileTree(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRow>
|
||||||
|
|
||||||
|
<SettingsRow
|
||||||
|
title={language.t("settings.general.row.showNavigation.title")}
|
||||||
|
description={language.t("settings.general.row.showNavigation.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-show-navigation">
|
||||||
|
<Switch
|
||||||
|
checked={settings.general.showNavigation()}
|
||||||
|
onChange={(checked) => settings.general.setShowNavigation(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRow>
|
||||||
|
|
||||||
|
<SettingsRow
|
||||||
|
title={language.t("settings.general.row.showSearch.title")}
|
||||||
|
description={language.t("settings.general.row.showSearch.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-show-search">
|
||||||
|
<Switch
|
||||||
|
checked={settings.general.showSearch()}
|
||||||
|
onChange={(checked) => settings.general.setShowSearch(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRow>
|
||||||
|
|
||||||
|
<SettingsRow
|
||||||
|
title={language.t("settings.general.row.showTerminal.title")}
|
||||||
|
description={language.t("settings.general.row.showTerminal.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-show-terminal">
|
||||||
|
<Switch
|
||||||
|
checked={settings.general.showTerminal()}
|
||||||
|
onChange={(checked) => settings.general.setShowTerminal(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRow>
|
||||||
|
|
||||||
|
<SettingsRow
|
||||||
|
title={language.t("settings.general.row.showStatus.title")}
|
||||||
|
description={language.t("settings.general.row.showStatus.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-show-status">
|
||||||
|
<Switch
|
||||||
|
checked={settings.general.showStatus()}
|
||||||
|
onChange={(checked) => settings.general.setShowStatus(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRow>
|
||||||
|
</SettingsList>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
const AppearanceSection = () => (
|
const AppearanceSection = () => (
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.appearance")}</h3>
|
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.appearance")}</h3>
|
||||||
@@ -527,6 +596,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
console.log(import.meta.env)
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
||||||
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
||||||
@@ -609,6 +679,10 @@ export const SettingsGeneral: Component = () => {
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
<Show when={desktop() && import.meta.env.VITE_OPENCODE_CHANNEL === "beta"}>
|
||||||
|
<AdvancedSection />
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
const scrollY = typeof local.pty.scrollY === "number" ? local.pty.scrollY : undefined
|
const scrollY = typeof local.pty.scrollY === "number" ? local.pty.scrollY : undefined
|
||||||
let ws: WebSocket | undefined
|
let ws: WebSocket | undefined
|
||||||
let term: Term | undefined
|
let term: Term | undefined
|
||||||
let ghostty: Ghostty
|
let _ghostty: Ghostty
|
||||||
let serializeAddon: SerializeAddon
|
let serializeAddon: SerializeAddon
|
||||||
let fitAddon: FitAddon
|
let fitAddon: FitAddon
|
||||||
let handleResize: () => void
|
let handleResize: () => void
|
||||||
@@ -372,7 +372,7 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
cleanup()
|
cleanup()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ghostty = g
|
_ghostty = g
|
||||||
term = t
|
term = t
|
||||||
output = terminalWriter((data, done) =>
|
output = terminalWriter((data, done) =>
|
||||||
t.write(data, () => {
|
t.write(data, () => {
|
||||||
@@ -415,7 +415,7 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
if (local.autoFocus !== false) focusTerminal()
|
if (local.autoFocus !== false) focusTerminal()
|
||||||
|
|
||||||
if (typeof document !== "undefined" && document.fonts) {
|
if (typeof document !== "undefined" && document.fonts) {
|
||||||
document.fonts.ready.then(scheduleFit)
|
void document.fonts.ready.then(scheduleFit)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onResize = t.onResize((size) => {
|
const onResize = t.onResize((size) => {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { useLayout } from "@/context/layout"
|
|||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { useSettings } from "@/context/settings"
|
||||||
import { applyPath, backPath, forwardPath } from "./titlebar-history"
|
import { applyPath, backPath, forwardPath } from "./titlebar-history"
|
||||||
|
|
||||||
type TauriDesktopWindow = {
|
type TauriDesktopWindow = {
|
||||||
@@ -40,6 +41,7 @@ export function Titlebar() {
|
|||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const command = useCommand()
|
const command = useCommand()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
const settings = useSettings()
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
@@ -78,6 +80,7 @@ export function Titlebar() {
|
|||||||
const canBack = createMemo(() => history.index > 0)
|
const canBack = createMemo(() => history.index > 0)
|
||||||
const canForward = createMemo(() => history.index < history.stack.length - 1)
|
const canForward = createMemo(() => history.index < history.stack.length - 1)
|
||||||
const hasProjects = createMemo(() => layout.projects.list().length > 0)
|
const hasProjects = createMemo(() => layout.projects.list().length > 0)
|
||||||
|
const nav = createMemo(() => import.meta.env.VITE_OPENCODE_CHANNEL !== "beta" || settings.general.showNavigation())
|
||||||
|
|
||||||
const back = () => {
|
const back = () => {
|
||||||
const next = backPath(history)
|
const next = backPath(history)
|
||||||
@@ -252,41 +255,47 @@ export function Titlebar() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={hasProjects()}>
|
<div
|
||||||
<div
|
class="flex items-center shrink-0"
|
||||||
class="flex items-center gap-0 transition-transform"
|
classList={{
|
||||||
classList={{
|
"-translate-x-[36px]": layout.sidebar.opened() && !!params.dir,
|
||||||
"translate-x-0": !layout.sidebar.opened(),
|
"duration-180 ease-out": !layout.sidebar.opened(),
|
||||||
"-translate-x-[36px]": layout.sidebar.opened(),
|
"duration-180 ease-in": layout.sidebar.opened(),
|
||||||
"duration-180 ease-out": !layout.sidebar.opened(),
|
}}
|
||||||
"duration-180 ease-in": layout.sidebar.opened(),
|
>
|
||||||
}}
|
<Show when={hasProjects() && nav()}>
|
||||||
>
|
<div class="flex items-center gap-0 transition-transform">
|
||||||
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
|
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
icon="chevron-left"
|
icon="chevron-left"
|
||||||
class="titlebar-icon w-6 h-6 p-0 box-border"
|
class="titlebar-icon w-6 h-6 p-0 box-border"
|
||||||
disabled={!canBack()}
|
disabled={!canBack()}
|
||||||
onClick={back}
|
onClick={back}
|
||||||
aria-label={language.t("common.goBack")}
|
aria-label={language.t("common.goBack")}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
|
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
icon="chevron-right"
|
icon="chevron-right"
|
||||||
class="titlebar-icon w-6 h-6 p-0 box-border"
|
class="titlebar-icon w-6 h-6 p-0 box-border"
|
||||||
disabled={!canForward()}
|
disabled={!canForward()}
|
||||||
onClick={forward}
|
onClick={forward}
|
||||||
aria-label={language.t("common.goForward")}
|
aria-label={language.t("common.goForward")}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
|
||||||
|
{["beta", "dev"].includes(import.meta.env.VITE_OPENCODE_CHANNEL) && (
|
||||||
|
<div class="bg-icon-interactive-base text-[#FFF] font-medium px-2 rounded-sm uppercase font-mono">
|
||||||
|
{import.meta.env.VITE_OPENCODE_CHANNEL.toUpperCase()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="min-w-0 flex items-center justify-center pointer-events-none">
|
<div class="min-w-0 flex items-center justify-center pointer-events-none">
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import type { ProjectMeta } from "./global-sync/types"
|
|||||||
import { SESSION_RECENT_LIMIT } from "./global-sync/types"
|
import { SESSION_RECENT_LIMIT } from "./global-sync/types"
|
||||||
import { sanitizeProject } from "./global-sync/utils"
|
import { sanitizeProject } from "./global-sync/utils"
|
||||||
import { formatServerError } from "@/utils/server-errors"
|
import { formatServerError } from "@/utils/server-errors"
|
||||||
|
import { queryOptions, skipToken, useQueryClient } from "@tanstack/solid-query"
|
||||||
|
|
||||||
type GlobalStore = {
|
type GlobalStore = {
|
||||||
ready: boolean
|
ready: boolean
|
||||||
@@ -41,6 +42,9 @@ type GlobalStore = {
|
|||||||
reload: undefined | "pending" | "complete"
|
reload: undefined | "pending" | "complete"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const loadSessionsQuery = (directory: string) =>
|
||||||
|
queryOptions<null>({ queryKey: [directory, "loadSessions"], queryFn: skipToken })
|
||||||
|
|
||||||
function createGlobalSync() {
|
function createGlobalSync() {
|
||||||
const globalSDK = useGlobalSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
@@ -67,6 +71,7 @@ function createGlobalSync() {
|
|||||||
config: {},
|
config: {},
|
||||||
reload: undefined,
|
reload: undefined,
|
||||||
})
|
})
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
let active = true
|
let active = true
|
||||||
let projectWritten = false
|
let projectWritten = false
|
||||||
@@ -198,46 +203,53 @@ function createGlobalSync() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const limit = Math.max(store.limit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT)
|
const limit = Math.max(store.limit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT)
|
||||||
const promise = loadRootSessionsWithFallback({
|
const promise = queryClient
|
||||||
directory,
|
.fetchQuery({
|
||||||
limit,
|
...loadSessionsQuery(directory),
|
||||||
list: (query) => globalSDK.client.session.list(query),
|
queryFn: () =>
|
||||||
})
|
loadRootSessionsWithFallback({
|
||||||
.then((x) => {
|
directory,
|
||||||
const nonArchived = (x.data ?? [])
|
limit,
|
||||||
.filter((s) => !!s?.id)
|
list: (query) => globalSDK.client.session.list(query),
|
||||||
.filter((s) => !s.time?.archived)
|
})
|
||||||
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
.then((x) => {
|
||||||
const limit = store.limit
|
const nonArchived = (x.data ?? [])
|
||||||
const childSessions = store.session.filter((s) => !!s.parentID)
|
.filter((s) => !!s?.id)
|
||||||
const sessions = trimSessions([...nonArchived, ...childSessions], {
|
.filter((s) => !s.time?.archived)
|
||||||
limit,
|
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
||||||
permission: store.permission,
|
const limit = store.limit
|
||||||
})
|
const childSessions = store.session.filter((s) => !!s.parentID)
|
||||||
setStore(
|
const sessions = trimSessions([...nonArchived, ...childSessions], {
|
||||||
"sessionTotal",
|
limit,
|
||||||
estimateRootSessionTotal({
|
permission: store.permission,
|
||||||
count: nonArchived.length,
|
})
|
||||||
limit: x.limit,
|
setStore(
|
||||||
limited: x.limited,
|
"sessionTotal",
|
||||||
}),
|
estimateRootSessionTotal({
|
||||||
)
|
count: nonArchived.length,
|
||||||
setStore("session", reconcile(sessions, { key: "id" }))
|
limit: x.limit,
|
||||||
cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo)
|
limited: x.limited,
|
||||||
sessionMeta.set(directory, { limit })
|
}),
|
||||||
})
|
)
|
||||||
.catch((err) => {
|
setStore("session", reconcile(sessions, { key: "id" }))
|
||||||
console.error("Failed to load sessions", err)
|
cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo)
|
||||||
const project = getFilename(directory)
|
sessionMeta.set(directory, { limit })
|
||||||
showToast({
|
})
|
||||||
variant: "error",
|
.catch((err) => {
|
||||||
title: language.t("toast.session.listFailed.title", { project }),
|
console.error("Failed to load sessions", err)
|
||||||
description: formatServerError(err, language.t),
|
const project = getFilename(directory)
|
||||||
})
|
showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: language.t("toast.session.listFailed.title", { project }),
|
||||||
|
description: formatServerError(err, language.t),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(() => null),
|
||||||
})
|
})
|
||||||
|
.then(() => {})
|
||||||
|
|
||||||
sessionLoads.set(directory, promise)
|
sessionLoads.set(directory, promise)
|
||||||
promise.finally(() => {
|
void promise.finally(() => {
|
||||||
sessionLoads.delete(directory)
|
sessionLoads.delete(directory)
|
||||||
children.unpin(directory)
|
children.unpin(directory)
|
||||||
})
|
})
|
||||||
@@ -250,7 +262,7 @@ function createGlobalSync() {
|
|||||||
if (pending) return pending
|
if (pending) return pending
|
||||||
|
|
||||||
children.pin(directory)
|
children.pin(directory)
|
||||||
const promise = (async () => {
|
const promise = Promise.resolve().then(async () => {
|
||||||
const child = children.ensureChild(directory)
|
const child = children.ensureChild(directory)
|
||||||
const cache = children.vcsCache.get(directory)
|
const cache = children.vcsCache.get(directory)
|
||||||
if (!cache) return
|
if (!cache) return
|
||||||
@@ -269,11 +281,12 @@ function createGlobalSync() {
|
|||||||
vcsCache: cache,
|
vcsCache: cache,
|
||||||
loadSessions,
|
loadSessions,
|
||||||
translate: language.t,
|
translate: language.t,
|
||||||
|
queryClient,
|
||||||
})
|
})
|
||||||
})()
|
})
|
||||||
|
|
||||||
booting.set(directory, promise)
|
booting.set(directory, promise)
|
||||||
promise.finally(() => {
|
void promise.finally(() => {
|
||||||
booting.delete(directory)
|
booting.delete(directory)
|
||||||
children.unpin(directory)
|
children.unpin(directory)
|
||||||
})
|
})
|
||||||
@@ -317,7 +330,7 @@ function createGlobalSync() {
|
|||||||
setSessionTodo,
|
setSessionTodo,
|
||||||
vcsCache: children.vcsCache.get(directory),
|
vcsCache: children.vcsCache.get(directory),
|
||||||
loadLsp: () => {
|
loadLsp: () => {
|
||||||
sdkFor(directory)
|
void sdkFor(directory)
|
||||||
.lsp.status()
|
.lsp.status()
|
||||||
.then((x) => {
|
.then((x) => {
|
||||||
setStore("lsp", x.data ?? [])
|
setStore("lsp", x.data ?? [])
|
||||||
@@ -346,6 +359,7 @@ function createGlobalSync() {
|
|||||||
translate: language.t,
|
translate: language.t,
|
||||||
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
|
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
|
||||||
setGlobalStore: setBootStore,
|
setGlobalStore: setBootStore,
|
||||||
|
queryClient,
|
||||||
})
|
})
|
||||||
bootedAt = Date.now()
|
bootedAt = Date.now()
|
||||||
} finally {
|
} finally {
|
||||||
@@ -359,13 +373,13 @@ function createGlobalSync() {
|
|||||||
eventFrame = undefined
|
eventFrame = undefined
|
||||||
eventTimer = setTimeout(() => {
|
eventTimer = setTimeout(() => {
|
||||||
eventTimer = undefined
|
eventTimer = undefined
|
||||||
globalSDK.event.start()
|
void globalSDK.event.start()
|
||||||
}, 0)
|
}, 0)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
eventTimer = setTimeout(() => {
|
eventTimer = setTimeout(() => {
|
||||||
eventTimer = undefined
|
eventTimer = undefined
|
||||||
globalSDK.event.start()
|
void globalSDK.event.start()
|
||||||
}, 0)
|
}, 0)
|
||||||
}
|
}
|
||||||
void bootstrap()
|
void bootstrap()
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import { reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
|||||||
import type { State, VcsCache } from "./types"
|
import type { State, VcsCache } from "./types"
|
||||||
import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
|
import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
|
||||||
import { formatServerError } from "@/utils/server-errors"
|
import { formatServerError } from "@/utils/server-errors"
|
||||||
|
import { QueryClient, queryOptions, skipToken } from "@tanstack/solid-query"
|
||||||
|
import { loadSessionsQuery } from "../global-sync"
|
||||||
|
|
||||||
type GlobalStore = {
|
type GlobalStore = {
|
||||||
ready: boolean
|
ready: boolean
|
||||||
@@ -71,6 +73,7 @@ export async function bootstrapGlobal(input: {
|
|||||||
translate: (key: string, vars?: Record<string, string | number>) => string
|
translate: (key: string, vars?: Record<string, string | number>) => string
|
||||||
formatMoreCount: (count: number) => string
|
formatMoreCount: (count: number) => string
|
||||||
setGlobalStore: SetStoreFunction<GlobalStore>
|
setGlobalStore: SetStoreFunction<GlobalStore>
|
||||||
|
queryClient: QueryClient
|
||||||
}) {
|
}) {
|
||||||
const fast = [
|
const fast = [
|
||||||
() =>
|
() =>
|
||||||
@@ -80,11 +83,16 @@ export async function bootstrapGlobal(input: {
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
() =>
|
() =>
|
||||||
retry(() =>
|
input.queryClient.fetchQuery({
|
||||||
input.globalSDK.provider.list().then((x) => {
|
...loadProvidersQuery(null),
|
||||||
input.setGlobalStore("provider", normalizeProviderList(x.data!))
|
queryFn: () =>
|
||||||
}),
|
retry(() =>
|
||||||
),
|
input.globalSDK.provider.list().then((x) => {
|
||||||
|
input.setGlobalStore("provider", normalizeProviderList(x.data!))
|
||||||
|
return null
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
|
|
||||||
const slow = [
|
const slow = [
|
||||||
@@ -172,6 +180,12 @@ function warmSessions(input: {
|
|||||||
).then(() => undefined)
|
).then(() => undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const loadProvidersQuery = (directory: string | null) =>
|
||||||
|
queryOptions<null>({ queryKey: [directory, "providers"], queryFn: skipToken })
|
||||||
|
|
||||||
|
export const loadAgentsQuery = (directory: string | null) =>
|
||||||
|
queryOptions<null>({ queryKey: [directory, "agents"], queryFn: skipToken })
|
||||||
|
|
||||||
export async function bootstrapDirectory(input: {
|
export async function bootstrapDirectory(input: {
|
||||||
directory: string
|
directory: string
|
||||||
sdk: OpencodeClient
|
sdk: OpencodeClient
|
||||||
@@ -186,6 +200,7 @@ export async function bootstrapDirectory(input: {
|
|||||||
project: Project[]
|
project: Project[]
|
||||||
provider: ProviderListResponse
|
provider: ProviderListResponse
|
||||||
}
|
}
|
||||||
|
queryClient: QueryClient
|
||||||
}) {
|
}) {
|
||||||
const loading = input.store.status !== "complete"
|
const loading = input.store.status !== "complete"
|
||||||
const seededProject = projectID(input.directory, input.global.project)
|
const seededProject = projectID(input.directory, input.global.project)
|
||||||
@@ -207,97 +222,7 @@ export async function bootstrapDirectory(input: {
|
|||||||
input.setStore("lsp", [])
|
input.setStore("lsp", [])
|
||||||
if (loading) input.setStore("status", "partial")
|
if (loading) input.setStore("status", "partial")
|
||||||
|
|
||||||
const fast = [
|
const fast = [() => Promise.resolve(input.loadSessions(input.directory))]
|
||||||
() => retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", normalizeAgentList(x.data)))),
|
|
||||||
() => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))),
|
|
||||||
() => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
|
|
||||||
]
|
|
||||||
|
|
||||||
const slow = [
|
|
||||||
() =>
|
|
||||||
seededProject
|
|
||||||
? Promise.resolve()
|
|
||||||
: retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)),
|
|
||||||
() =>
|
|
||||||
seededPath
|
|
||||||
? Promise.resolve()
|
|
||||||
: retry(() =>
|
|
||||||
input.sdk.path.get().then((x) => {
|
|
||||||
input.setStore("path", x.data!)
|
|
||||||
const next = projectID(x.data?.directory ?? input.directory, input.global.project)
|
|
||||||
if (next) input.setStore("project", next)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
() =>
|
|
||||||
retry(() =>
|
|
||||||
input.sdk.vcs.get().then((x) => {
|
|
||||||
const next = x.data ?? input.store.vcs
|
|
||||||
input.setStore("vcs", next)
|
|
||||||
if (next) input.vcsCache.setStore("value", next)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
() => retry(() => input.sdk.command.list().then((x) => input.setStore("command", x.data ?? []))),
|
|
||||||
() =>
|
|
||||||
retry(() =>
|
|
||||||
input.sdk.permission.list().then((x) => {
|
|
||||||
const ids = (x.data ?? []).map((perm) => perm?.sessionID).filter((id): id is string => !!id)
|
|
||||||
const grouped = groupBySession(
|
|
||||||
(x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm.sessionID),
|
|
||||||
)
|
|
||||||
return warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk }).then(() =>
|
|
||||||
batch(() => {
|
|
||||||
for (const sessionID of Object.keys(input.store.permission)) {
|
|
||||||
if (grouped[sessionID]) continue
|
|
||||||
input.setStore("permission", sessionID, [])
|
|
||||||
}
|
|
||||||
for (const [sessionID, permissions] of Object.entries(grouped)) {
|
|
||||||
input.setStore(
|
|
||||||
"permission",
|
|
||||||
sessionID,
|
|
||||||
reconcile(
|
|
||||||
permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)),
|
|
||||||
{ key: "id" },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
() =>
|
|
||||||
retry(() =>
|
|
||||||
input.sdk.question.list().then((x) => {
|
|
||||||
const ids = (x.data ?? []).map((question) => question?.sessionID).filter((id): id is string => !!id)
|
|
||||||
const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID))
|
|
||||||
return warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk }).then(() =>
|
|
||||||
batch(() => {
|
|
||||||
for (const sessionID of Object.keys(input.store.question)) {
|
|
||||||
if (grouped[sessionID]) continue
|
|
||||||
input.setStore("question", sessionID, [])
|
|
||||||
}
|
|
||||||
for (const [sessionID, questions] of Object.entries(grouped)) {
|
|
||||||
input.setStore(
|
|
||||||
"question",
|
|
||||||
sessionID,
|
|
||||||
reconcile(
|
|
||||||
questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)),
|
|
||||||
{ key: "id" },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
() => Promise.resolve(input.loadSessions(input.directory)),
|
|
||||||
() =>
|
|
||||||
retry(() =>
|
|
||||||
input.sdk.mcp.status().then((x) => {
|
|
||||||
input.setStore("mcp", x.data!)
|
|
||||||
input.setStore("mcp_ready", true)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
const errs = errors(await runAll(fast))
|
const errs = errors(await runAll(fast))
|
||||||
if (errs.length > 0) {
|
if (errs.length > 0) {
|
||||||
@@ -310,36 +235,138 @@ export async function bootstrapDirectory(input: {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
await waitForPaint()
|
;(async () => {
|
||||||
const slowErrs = errors(await runAll(slow))
|
const slow = [
|
||||||
if (slowErrs.length > 0) {
|
() =>
|
||||||
console.error("Failed to finish bootstrap instance", slowErrs[0])
|
input.queryClient.ensureQueryData({
|
||||||
const project = getFilename(input.directory)
|
...loadAgentsQuery(input.directory),
|
||||||
showToast({
|
queryFn: () =>
|
||||||
variant: "error",
|
retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", normalizeAgentList(x.data)))).then(
|
||||||
title: input.translate("toast.project.reloadFailed.title", { project }),
|
() => null,
|
||||||
description: formatServerError(slowErrs[0], input.translate),
|
),
|
||||||
})
|
}),
|
||||||
}
|
() => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))),
|
||||||
|
() => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
|
||||||
|
() =>
|
||||||
|
seededProject
|
||||||
|
? Promise.resolve()
|
||||||
|
: retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)),
|
||||||
|
() =>
|
||||||
|
seededPath
|
||||||
|
? Promise.resolve()
|
||||||
|
: retry(() =>
|
||||||
|
input.sdk.path.get().then((x) => {
|
||||||
|
input.setStore("path", x.data!)
|
||||||
|
const next = projectID(x.data?.directory ?? input.directory, input.global.project)
|
||||||
|
if (next) input.setStore("project", next)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
() =>
|
||||||
|
retry(() =>
|
||||||
|
input.sdk.vcs.get().then((x) => {
|
||||||
|
const next = x.data ?? input.store.vcs
|
||||||
|
input.setStore("vcs", next)
|
||||||
|
if (next) input.vcsCache.setStore("value", next)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
() => retry(() => input.sdk.command.list().then((x) => input.setStore("command", x.data ?? []))),
|
||||||
|
() =>
|
||||||
|
retry(() =>
|
||||||
|
input.sdk.permission.list().then((x) => {
|
||||||
|
const ids = (x.data ?? []).map((perm) => perm?.sessionID).filter((id): id is string => !!id)
|
||||||
|
const grouped = groupBySession(
|
||||||
|
(x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm.sessionID),
|
||||||
|
)
|
||||||
|
return warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk }).then(() =>
|
||||||
|
batch(() => {
|
||||||
|
for (const sessionID of Object.keys(input.store.permission)) {
|
||||||
|
if (grouped[sessionID]) continue
|
||||||
|
input.setStore("permission", sessionID, [])
|
||||||
|
}
|
||||||
|
for (const [sessionID, permissions] of Object.entries(grouped)) {
|
||||||
|
input.setStore(
|
||||||
|
"permission",
|
||||||
|
sessionID,
|
||||||
|
reconcile(
|
||||||
|
permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)),
|
||||||
|
{ key: "id" },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
() =>
|
||||||
|
retry(() =>
|
||||||
|
input.sdk.question.list().then((x) => {
|
||||||
|
const ids = (x.data ?? []).map((question) => question?.sessionID).filter((id): id is string => !!id)
|
||||||
|
const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID))
|
||||||
|
return warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk }).then(() =>
|
||||||
|
batch(() => {
|
||||||
|
for (const sessionID of Object.keys(input.store.question)) {
|
||||||
|
if (grouped[sessionID]) continue
|
||||||
|
input.setStore("question", sessionID, [])
|
||||||
|
}
|
||||||
|
for (const [sessionID, questions] of Object.entries(grouped)) {
|
||||||
|
input.setStore(
|
||||||
|
"question",
|
||||||
|
sessionID,
|
||||||
|
reconcile(
|
||||||
|
questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)),
|
||||||
|
{ key: "id" },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
() => Promise.resolve(input.loadSessions(input.directory)),
|
||||||
|
() =>
|
||||||
|
retry(() =>
|
||||||
|
input.sdk.mcp.status().then((x) => {
|
||||||
|
input.setStore("mcp", x.data!)
|
||||||
|
input.setStore("mcp_ready", true)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
if (loading && errs.length === 0 && slowErrs.length === 0) input.setStore("status", "complete")
|
await waitForPaint()
|
||||||
|
const slowErrs = errors(await runAll(slow))
|
||||||
const rev = (providerRev.get(input.directory) ?? 0) + 1
|
if (slowErrs.length > 0) {
|
||||||
providerRev.set(input.directory, rev)
|
console.error("Failed to finish bootstrap instance", slowErrs[0])
|
||||||
void retry(() => input.sdk.provider.list())
|
|
||||||
.then((x) => {
|
|
||||||
if (providerRev.get(input.directory) !== rev) return
|
|
||||||
input.setStore("provider", normalizeProviderList(x.data!))
|
|
||||||
input.setStore("provider_ready", true)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
if (providerRev.get(input.directory) !== rev) return
|
|
||||||
console.error("Failed to refresh provider list", err)
|
|
||||||
const project = getFilename(input.directory)
|
const project = getFilename(input.directory)
|
||||||
showToast({
|
showToast({
|
||||||
variant: "error",
|
variant: "error",
|
||||||
title: input.translate("toast.project.reloadFailed.title", { project }),
|
title: input.translate("toast.project.reloadFailed.title", { project }),
|
||||||
description: formatServerError(err, input.translate),
|
description: formatServerError(slowErrs[0], input.translate),
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading && errs.length === 0 && slowErrs.length === 0) input.setStore("status", "complete")
|
||||||
|
|
||||||
|
const rev = (providerRev.get(input.directory) ?? 0) + 1
|
||||||
|
providerRev.set(input.directory, rev)
|
||||||
|
void input.queryClient.ensureQueryData({
|
||||||
|
...loadSessionsQuery(input.directory),
|
||||||
|
queryFn: () =>
|
||||||
|
retry(() => input.sdk.provider.list())
|
||||||
|
.then((x) => {
|
||||||
|
if (providerRev.get(input.directory) !== rev) return
|
||||||
|
input.setStore("provider", normalizeProviderList(x.data!))
|
||||||
|
input.setStore("provider_ready", true)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (providerRev.get(input.directory) !== rev) console.error("Failed to refresh provider list", err)
|
||||||
|
const project = getFilename(input.directory)
|
||||||
|
showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: input.translate("toast.project.reloadFailed.title", { project }),
|
||||||
|
description: formatServerError(err, input.translate),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(() => null),
|
||||||
})
|
})
|
||||||
|
})()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -582,7 +582,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
open(directory: string) {
|
open(directory: string) {
|
||||||
const root = rootFor(directory)
|
const root = rootFor(directory)
|
||||||
if (server.projects.list().find((x) => x.worktree === root)) return
|
if (server.projects.list().find((x) => x.worktree === root)) return
|
||||||
globalSync.project.loadSessions(root)
|
void globalSync.project.loadSessions(root)
|
||||||
server.projects.open(root)
|
server.projects.open(root)
|
||||||
},
|
},
|
||||||
close(directory: string) {
|
close(directory: string) {
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ export interface Settings {
|
|||||||
autoSave: boolean
|
autoSave: boolean
|
||||||
releaseNotes: boolean
|
releaseNotes: boolean
|
||||||
followup: "queue" | "steer"
|
followup: "queue" | "steer"
|
||||||
|
showFileTree: boolean
|
||||||
|
showNavigation: boolean
|
||||||
|
showSearch: boolean
|
||||||
|
showStatus: boolean
|
||||||
|
showTerminal: boolean
|
||||||
showReasoningSummaries: boolean
|
showReasoningSummaries: boolean
|
||||||
shellToolPartsExpanded: boolean
|
shellToolPartsExpanded: boolean
|
||||||
editToolPartsExpanded: boolean
|
editToolPartsExpanded: boolean
|
||||||
@@ -89,6 +94,11 @@ const defaultSettings: Settings = {
|
|||||||
autoSave: true,
|
autoSave: true,
|
||||||
releaseNotes: true,
|
releaseNotes: true,
|
||||||
followup: "steer",
|
followup: "steer",
|
||||||
|
showFileTree: false,
|
||||||
|
showNavigation: false,
|
||||||
|
showSearch: false,
|
||||||
|
showStatus: false,
|
||||||
|
showTerminal: false,
|
||||||
showReasoningSummaries: false,
|
showReasoningSummaries: false,
|
||||||
shellToolPartsExpanded: false,
|
shellToolPartsExpanded: false,
|
||||||
editToolPartsExpanded: false,
|
editToolPartsExpanded: false,
|
||||||
@@ -162,6 +172,26 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
|||||||
setFollowup(value: "queue" | "steer") {
|
setFollowup(value: "queue" | "steer") {
|
||||||
setStore("general", "followup", value === "queue" ? "steer" : value)
|
setStore("general", "followup", value === "queue" ? "steer" : value)
|
||||||
},
|
},
|
||||||
|
showFileTree: withFallback(() => store.general?.showFileTree, defaultSettings.general.showFileTree),
|
||||||
|
setShowFileTree(value: boolean) {
|
||||||
|
setStore("general", "showFileTree", value)
|
||||||
|
},
|
||||||
|
showNavigation: withFallback(() => store.general?.showNavigation, defaultSettings.general.showNavigation),
|
||||||
|
setShowNavigation(value: boolean) {
|
||||||
|
setStore("general", "showNavigation", value)
|
||||||
|
},
|
||||||
|
showSearch: withFallback(() => store.general?.showSearch, defaultSettings.general.showSearch),
|
||||||
|
setShowSearch(value: boolean) {
|
||||||
|
setStore("general", "showSearch", value)
|
||||||
|
},
|
||||||
|
showStatus: withFallback(() => store.general?.showStatus, defaultSettings.general.showStatus),
|
||||||
|
setShowStatus(value: boolean) {
|
||||||
|
setStore("general", "showStatus", value)
|
||||||
|
},
|
||||||
|
showTerminal: withFallback(() => store.general?.showTerminal, defaultSettings.general.showTerminal),
|
||||||
|
setShowTerminal(value: boolean) {
|
||||||
|
setStore("general", "showTerminal", value)
|
||||||
|
},
|
||||||
showReasoningSummaries: withFallback(
|
showReasoningSummaries: withFallback(
|
||||||
() => store.general?.showReasoningSummaries,
|
() => store.general?.showReasoningSummaries,
|
||||||
defaultSettings.general.showReasoningSummaries,
|
defaultSettings.general.showReasoningSummaries,
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], plat
|
|||||||
entry?.value.clear()
|
entry?.value.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
removePersisted(Persist.workspace(dir, "terminal"), platform)
|
void removePersisted(Persist.workspace(dir, "terminal"), platform)
|
||||||
|
|
||||||
const legacy = new Set(getLegacyTerminalStorageKeys(dir))
|
const legacy = new Set(getLegacyTerminalStorageKeys(dir))
|
||||||
for (const id of sessionIDs ?? []) {
|
for (const id of sessionIDs ?? []) {
|
||||||
@@ -126,7 +126,7 @@ export function clearWorkspaceTerminals(dir: string, sessionIDs?: string[], plat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const key of legacy) {
|
for (const key of legacy) {
|
||||||
removePersisted({ key }, platform)
|
void removePersisted({ key }, platform)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+2
-3
@@ -1,15 +1,14 @@
|
|||||||
import "solid-js"
|
|
||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_OPENCODE_SERVER_HOST: string
|
readonly VITE_OPENCODE_SERVER_HOST: string
|
||||||
readonly VITE_OPENCODE_SERVER_PORT: string
|
readonly VITE_OPENCODE_SERVER_PORT: string
|
||||||
|
readonly VITE_OPENCODE_CHANNEL?: "dev" | "beta" | "prod"
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
readonly env: ImportMetaEnv
|
readonly env: ImportMetaEnv
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module "solid-js" {
|
export declare module "solid-js" {
|
||||||
namespace JSX {
|
namespace JSX {
|
||||||
interface Directives {
|
interface Directives {
|
||||||
sortable: true
|
sortable: true
|
||||||
|
|||||||
@@ -719,6 +719,7 @@ export const dict = {
|
|||||||
"settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.",
|
"settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.",
|
||||||
|
|
||||||
"settings.general.section.appearance": "Appearance",
|
"settings.general.section.appearance": "Appearance",
|
||||||
|
"settings.general.section.advanced": "Advanced",
|
||||||
"settings.general.section.notifications": "System notifications",
|
"settings.general.section.notifications": "System notifications",
|
||||||
"settings.general.section.updates": "Updates",
|
"settings.general.section.updates": "Updates",
|
||||||
"settings.general.section.sounds": "Sound effects",
|
"settings.general.section.sounds": "Sound effects",
|
||||||
@@ -741,6 +742,16 @@ export const dict = {
|
|||||||
"settings.general.row.followup.description": "Choose whether follow-up prompts steer immediately or wait in a queue",
|
"settings.general.row.followup.description": "Choose whether follow-up prompts steer immediately or wait in a queue",
|
||||||
"settings.general.row.followup.option.queue": "Queue",
|
"settings.general.row.followup.option.queue": "Queue",
|
||||||
"settings.general.row.followup.option.steer": "Steer",
|
"settings.general.row.followup.option.steer": "Steer",
|
||||||
|
"settings.general.row.showFileTree.title": "File tree",
|
||||||
|
"settings.general.row.showFileTree.description": "Show the file tree toggle and panel in desktop sessions",
|
||||||
|
"settings.general.row.showNavigation.title": "Navigation controls",
|
||||||
|
"settings.general.row.showNavigation.description": "Show the back and forward buttons in the desktop title bar",
|
||||||
|
"settings.general.row.showSearch.title": "Command palette",
|
||||||
|
"settings.general.row.showSearch.description": "Show the search and command palette button in the desktop title bar",
|
||||||
|
"settings.general.row.showTerminal.title": "Terminal",
|
||||||
|
"settings.general.row.showTerminal.description": "Show the terminal button in the desktop title bar",
|
||||||
|
"settings.general.row.showStatus.title": "Server status",
|
||||||
|
"settings.general.row.showStatus.description": "Show the server status button in the desktop title bar",
|
||||||
"settings.general.row.reasoningSummaries.title": "Show reasoning summaries",
|
"settings.general.row.reasoningSummaries.title": "Show reasoning summaries",
|
||||||
"settings.general.row.reasoningSummaries.description": "Display model reasoning summaries in the timeline",
|
"settings.general.row.reasoningSummaries.description": "Display model reasoning summaries in the timeline",
|
||||||
"settings.general.row.shellToolPartsExpanded.title": "Expand shell tool parts",
|
"settings.general.row.shellToolPartsExpanded.title": "Expand shell tool parts",
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { dict as en } from "./en"
|
|
||||||
|
|
||||||
export const dict = {
|
export const dict = {
|
||||||
"command.category.suggested": "추천",
|
"command.category.suggested": "추천",
|
||||||
"command.category.view": "보기",
|
"command.category.view": "보기",
|
||||||
|
|||||||
+190
-184
@@ -13,7 +13,7 @@ import {
|
|||||||
type Accessor,
|
type Accessor,
|
||||||
} from "solid-js"
|
} from "solid-js"
|
||||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||||
import { useNavigate, useParams } from "@solidjs/router"
|
import { useLocation, useNavigate, useParams } from "@solidjs/router"
|
||||||
import { useLayout, LocalProject } from "@/context/layout"
|
import { useLayout, LocalProject } from "@/context/layout"
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
@@ -127,14 +127,17 @@ export default function Layout(props: ParentProps) {
|
|||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const initialDirectory = decode64(params.dir)
|
const initialDirectory = decode64(params.dir)
|
||||||
|
const location = useLocation()
|
||||||
const route = createMemo(() => {
|
const route = createMemo(() => {
|
||||||
const slug = params.dir
|
const slug = params.dir
|
||||||
if (!slug) return { slug, dir: "" }
|
if (!slug) return { slug, dir: "" }
|
||||||
const dir = decode64(slug)
|
const dir = decode64(slug)
|
||||||
if (!dir) return { slug, dir: "" }
|
if (!dir) return { slug, dir: "" }
|
||||||
|
const store = globalSync.peek(dir, { bootstrap: false })
|
||||||
return {
|
return {
|
||||||
slug,
|
slug,
|
||||||
dir: globalSync.peek(dir, { bootstrap: false })[0].path.directory || dir,
|
store,
|
||||||
|
dir: store[0].path.directory || dir,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const availableThemeEntries = createMemo(() => theme.ids().map((id) => [id, theme.themes()[id]] as const))
|
const availableThemeEntries = createMemo(() => theme.ids().map((id) => [id, theme.themes()[id]] as const))
|
||||||
@@ -956,7 +959,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
// warm up child store to prevent flicker
|
// warm up child store to prevent flicker
|
||||||
globalSync.child(target.worktree)
|
globalSync.child(target.worktree)
|
||||||
openProject(target.worktree)
|
void openProject(target.worktree)
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigateSessionByUnseen(offset: number) {
|
function navigateSessionByUnseen(offset: number) {
|
||||||
@@ -1094,7 +1097,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
disabled: !params.dir || !params.id,
|
disabled: !params.dir || !params.id,
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
const session = currentSessions().find((s) => s.id === params.id)
|
const session = currentSessions().find((s) => s.id === params.id)
|
||||||
if (session) archiveSession(session)
|
if (session) void archiveSession(session)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1360,11 +1363,11 @@ export default function Layout(props: ParentProps) {
|
|||||||
if (!server.isLocal()) return
|
if (!server.isLocal()) return
|
||||||
|
|
||||||
for (const directory of collectOpenProjectDeepLinks(urls)) {
|
for (const directory of collectOpenProjectDeepLinks(urls)) {
|
||||||
openProject(directory)
|
void openProject(directory)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const link of collectNewSessionDeepLinks(urls)) {
|
for (const link of collectNewSessionDeepLinks(urls)) {
|
||||||
openProject(link.directory, false)
|
void openProject(link.directory, false)
|
||||||
const slug = base64Encode(link.directory)
|
const slug = base64Encode(link.directory)
|
||||||
if (link.prompt) {
|
if (link.prompt) {
|
||||||
setSessionHandoff(slug, { prompt: link.prompt })
|
setSessionHandoff(slug, { prompt: link.prompt })
|
||||||
@@ -1453,11 +1456,11 @@ export default function Layout(props: ParentProps) {
|
|||||||
function resolve(result: string | string[] | null) {
|
function resolve(result: string | string[] | null) {
|
||||||
if (Array.isArray(result)) {
|
if (Array.isArray(result)) {
|
||||||
for (const directory of result) {
|
for (const directory of result) {
|
||||||
openProject(directory, false)
|
void openProject(directory, false)
|
||||||
}
|
}
|
||||||
navigateToProject(result[0])
|
void navigateToProject(result[0])
|
||||||
} else if (result) {
|
} else if (result) {
|
||||||
openProject(result)
|
void openProject(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1825,7 +1828,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
const next = new Set(dirs)
|
const next = new Set(dirs)
|
||||||
for (const directory of next) {
|
for (const directory of next) {
|
||||||
if (loadedSessionDirs.has(directory)) continue
|
if (loadedSessionDirs.has(directory)) continue
|
||||||
globalSync.project.loadSessions(directory)
|
void globalSync.project.loadSessions(directory)
|
||||||
}
|
}
|
||||||
|
|
||||||
loadedSessionDirs.clear()
|
loadedSessionDirs.clear()
|
||||||
@@ -2100,196 +2103,198 @@ export default function Layout(props: ParentProps) {
|
|||||||
</Show>
|
</Show>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<>
|
{(project) => (
|
||||||
<div class="shrink-0 pl-1 py-1">
|
<>
|
||||||
<div class="group/project flex items-start justify-between gap-2 py-2 pl-2 pr-0">
|
<div class="shrink-0 pl-1 py-1">
|
||||||
<div class="flex flex-col min-w-0">
|
<div class="group/project flex items-start justify-between gap-2 py-2 pl-2 pr-0">
|
||||||
<InlineEditor
|
<div class="flex flex-col min-w-0">
|
||||||
id={`project:${projectId()}`}
|
<InlineEditor
|
||||||
value={projectName}
|
id={`project:${projectId()}`}
|
||||||
onSave={(next) => {
|
value={projectName}
|
||||||
const item = project()
|
onSave={(next) => {
|
||||||
if (!item) return
|
const item = project()
|
||||||
renameProject(item, next)
|
if (!item) return
|
||||||
}}
|
void renameProject(item, next)
|
||||||
class="text-14-medium text-text-strong truncate"
|
}}
|
||||||
displayClass="text-14-medium text-text-strong truncate"
|
class="text-14-medium text-text-strong truncate"
|
||||||
stopPropagation
|
displayClass="text-14-medium text-text-strong truncate"
|
||||||
/>
|
stopPropagation
|
||||||
|
/>
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
gutter={2}
|
gutter={2}
|
||||||
value={worktree()}
|
value={worktree()}
|
||||||
class="shrink-0"
|
class="shrink-0"
|
||||||
contentStyle={{
|
contentStyle={{
|
||||||
"max-width": "640px",
|
"max-width": "640px",
|
||||||
transform: "translate3d(52px, 0, 0)",
|
transform: "translate3d(52px, 0, 0)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span class="text-12-regular text-text-base truncate select-text">
|
<span class="text-12-regular text-text-base truncate select-text">
|
||||||
{worktree().replace(homedir(), "~")}
|
{worktree().replace(homedir(), "~")}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DropdownMenu modal={!sidebarHovering()}>
|
||||||
|
<DropdownMenu.Trigger
|
||||||
|
as={IconButton}
|
||||||
|
icon="dot-grid"
|
||||||
|
variant="ghost"
|
||||||
|
data-action="project-menu"
|
||||||
|
data-project={slug()}
|
||||||
|
class="shrink-0 size-6 rounded-md transition-opacity data-[expanded]:bg-surface-base-active"
|
||||||
|
classList={{
|
||||||
|
"opacity-100": panelProps.mobile || merged(),
|
||||||
|
"opacity-0 group-hover/project:opacity-100 group-focus-within/project:opacity-100 data-[expanded]:opacity-100":
|
||||||
|
!panelProps.mobile && !merged(),
|
||||||
|
}}
|
||||||
|
aria-label={language.t("common.moreOptions")}
|
||||||
|
/>
|
||||||
|
<DropdownMenu.Portal>
|
||||||
|
<DropdownMenu.Content class="mt-1">
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onSelect={() => {
|
||||||
|
const item = project()
|
||||||
|
if (!item) return
|
||||||
|
showEditProjectDialog(item)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
data-action="project-workspaces-toggle"
|
||||||
|
data-project={slug()}
|
||||||
|
disabled={!canToggle()}
|
||||||
|
onSelect={() => {
|
||||||
|
const item = project()
|
||||||
|
if (!item) return
|
||||||
|
toggleProjectWorkspaces(item)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenu.ItemLabel>
|
||||||
|
{workspacesEnabled()
|
||||||
|
? language.t("sidebar.workspaces.disable")
|
||||||
|
: language.t("sidebar.workspaces.enable")}
|
||||||
|
</DropdownMenu.ItemLabel>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
data-action="project-clear-notifications"
|
||||||
|
data-project={slug()}
|
||||||
|
disabled={unseenCount() === 0}
|
||||||
|
onSelect={clearNotifications}
|
||||||
|
>
|
||||||
|
<DropdownMenu.ItemLabel>
|
||||||
|
{language.t("sidebar.project.clearNotifications")}
|
||||||
|
</DropdownMenu.ItemLabel>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item
|
||||||
|
data-action="project-close-menu"
|
||||||
|
data-project={slug()}
|
||||||
|
onSelect={() => {
|
||||||
|
const dir = worktree()
|
||||||
|
if (!dir) return
|
||||||
|
closeProject(dir)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenu.ItemLabel>{language.t("common.close")}</DropdownMenu.ItemLabel>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Portal>
|
||||||
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DropdownMenu modal={!sidebarHovering()}>
|
|
||||||
<DropdownMenu.Trigger
|
|
||||||
as={IconButton}
|
|
||||||
icon="dot-grid"
|
|
||||||
variant="ghost"
|
|
||||||
data-action="project-menu"
|
|
||||||
data-project={slug()}
|
|
||||||
class="shrink-0 size-6 rounded-md transition-opacity data-[expanded]:bg-surface-base-active"
|
|
||||||
classList={{
|
|
||||||
"opacity-100": panelProps.mobile || merged(),
|
|
||||||
"opacity-0 group-hover/project:opacity-100 group-focus-within/project:opacity-100 data-[expanded]:opacity-100":
|
|
||||||
!panelProps.mobile && !merged(),
|
|
||||||
}}
|
|
||||||
aria-label={language.t("common.moreOptions")}
|
|
||||||
/>
|
|
||||||
<DropdownMenu.Portal>
|
|
||||||
<DropdownMenu.Content class="mt-1">
|
|
||||||
<DropdownMenu.Item
|
|
||||||
onSelect={() => {
|
|
||||||
const item = project()
|
|
||||||
if (!item) return
|
|
||||||
showEditProjectDialog(item)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Item
|
|
||||||
data-action="project-workspaces-toggle"
|
|
||||||
data-project={slug()}
|
|
||||||
disabled={!canToggle()}
|
|
||||||
onSelect={() => {
|
|
||||||
const item = project()
|
|
||||||
if (!item) return
|
|
||||||
toggleProjectWorkspaces(item)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DropdownMenu.ItemLabel>
|
|
||||||
{workspacesEnabled()
|
|
||||||
? language.t("sidebar.workspaces.disable")
|
|
||||||
: language.t("sidebar.workspaces.enable")}
|
|
||||||
</DropdownMenu.ItemLabel>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Item
|
|
||||||
data-action="project-clear-notifications"
|
|
||||||
data-project={slug()}
|
|
||||||
disabled={unseenCount() === 0}
|
|
||||||
onSelect={clearNotifications}
|
|
||||||
>
|
|
||||||
<DropdownMenu.ItemLabel>
|
|
||||||
{language.t("sidebar.project.clearNotifications")}
|
|
||||||
</DropdownMenu.ItemLabel>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Separator />
|
|
||||||
<DropdownMenu.Item
|
|
||||||
data-action="project-close-menu"
|
|
||||||
data-project={slug()}
|
|
||||||
onSelect={() => {
|
|
||||||
const dir = worktree()
|
|
||||||
if (!dir) return
|
|
||||||
closeProject(dir)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DropdownMenu.ItemLabel>{language.t("common.close")}</DropdownMenu.ItemLabel>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu.Portal>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex-1 min-h-0 flex flex-col">
|
<div class="flex-1 min-h-0 flex flex-col">
|
||||||
<Show
|
<Show
|
||||||
when={workspacesEnabled()}
|
when={workspacesEnabled()}
|
||||||
fallback={
|
fallback={
|
||||||
|
<>
|
||||||
|
<div class="shrink-0 py-4">
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
icon="new-session"
|
||||||
|
class="w-full"
|
||||||
|
onClick={() => {
|
||||||
|
const dir = worktree()
|
||||||
|
if (!dir) return
|
||||||
|
navigateWithSidebarReset(`/${base64Encode(dir)}/session`)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{language.t("command.session.new")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-h-0">
|
||||||
|
<LocalWorkspace
|
||||||
|
ctx={workspaceSidebarCtx}
|
||||||
|
project={project()}
|
||||||
|
sortNow={sortNow}
|
||||||
|
mobile={panelProps.mobile}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<>
|
<>
|
||||||
<div class="shrink-0 py-4">
|
<div class="shrink-0 py-4">
|
||||||
<Button
|
<Button
|
||||||
size="large"
|
size="large"
|
||||||
icon="new-session"
|
icon="plus-small"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const dir = worktree()
|
const item = project()
|
||||||
if (!dir) return
|
if (!item) return
|
||||||
navigateWithSidebarReset(`/${base64Encode(dir)}/session`)
|
void createWorkspace(item)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{language.t("command.session.new")}
|
{language.t("workspace.new")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-h-0">
|
<div class="relative flex-1 min-h-0">
|
||||||
<LocalWorkspace
|
<DragDropProvider
|
||||||
ctx={workspaceSidebarCtx}
|
onDragStart={handleWorkspaceDragStart}
|
||||||
project={project()!}
|
onDragEnd={handleWorkspaceDragEnd}
|
||||||
sortNow={sortNow}
|
onDragOver={handleWorkspaceDragOver}
|
||||||
mobile={panelProps.mobile}
|
collisionDetector={closestCenter}
|
||||||
/>
|
>
|
||||||
|
<DragDropSensors />
|
||||||
|
<ConstrainDragXAxis />
|
||||||
|
<div
|
||||||
|
ref={(el) => {
|
||||||
|
if (!panelProps.mobile) scrollContainerRef = el
|
||||||
|
}}
|
||||||
|
class="size-full flex flex-col py-2 gap-4 overflow-y-auto no-scrollbar [overflow-anchor:none]"
|
||||||
|
>
|
||||||
|
<SortableProvider ids={workspaces()}>
|
||||||
|
<For each={workspaces()}>
|
||||||
|
{(directory) => (
|
||||||
|
<SortableWorkspace
|
||||||
|
ctx={workspaceSidebarCtx}
|
||||||
|
directory={directory}
|
||||||
|
project={project()}
|
||||||
|
sortNow={sortNow}
|
||||||
|
mobile={panelProps.mobile}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</SortableProvider>
|
||||||
|
</div>
|
||||||
|
<DragOverlay>
|
||||||
|
<WorkspaceDragOverlay
|
||||||
|
sidebarProject={sidebarProject}
|
||||||
|
activeWorkspace={() => store.activeWorkspace}
|
||||||
|
workspaceLabel={workspaceLabel}
|
||||||
|
/>
|
||||||
|
</DragOverlay>
|
||||||
|
</DragDropProvider>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
</Show>
|
||||||
>
|
</div>
|
||||||
<>
|
</>
|
||||||
<div class="shrink-0 py-4">
|
)}
|
||||||
<Button
|
|
||||||
size="large"
|
|
||||||
icon="plus-small"
|
|
||||||
class="w-full"
|
|
||||||
onClick={() => {
|
|
||||||
const item = project()
|
|
||||||
if (!item) return
|
|
||||||
createWorkspace(item)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{language.t("workspace.new")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div class="relative flex-1 min-h-0">
|
|
||||||
<DragDropProvider
|
|
||||||
onDragStart={handleWorkspaceDragStart}
|
|
||||||
onDragEnd={handleWorkspaceDragEnd}
|
|
||||||
onDragOver={handleWorkspaceDragOver}
|
|
||||||
collisionDetector={closestCenter}
|
|
||||||
>
|
|
||||||
<DragDropSensors />
|
|
||||||
<ConstrainDragXAxis />
|
|
||||||
<div
|
|
||||||
ref={(el) => {
|
|
||||||
if (!panelProps.mobile) scrollContainerRef = el
|
|
||||||
}}
|
|
||||||
class="size-full flex flex-col py-2 gap-4 overflow-y-auto no-scrollbar [overflow-anchor:none]"
|
|
||||||
>
|
|
||||||
<SortableProvider ids={workspaces()}>
|
|
||||||
<For each={workspaces()}>
|
|
||||||
{(directory) => (
|
|
||||||
<SortableWorkspace
|
|
||||||
ctx={workspaceSidebarCtx}
|
|
||||||
directory={directory}
|
|
||||||
project={project()!}
|
|
||||||
sortNow={sortNow}
|
|
||||||
mobile={panelProps.mobile}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</SortableProvider>
|
|
||||||
</div>
|
|
||||||
<DragOverlay>
|
|
||||||
<WorkspaceDragOverlay
|
|
||||||
sidebarProject={sidebarProject}
|
|
||||||
activeWorkspace={() => store.activeWorkspace}
|
|
||||||
workspaceLabel={workspaceLabel}
|
|
||||||
/>
|
|
||||||
</DragOverlay>
|
|
||||||
</DragDropProvider>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -2355,6 +2360,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
|
<div class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
|
||||||
|
{autoselecting() ?? ""}
|
||||||
<Titlebar />
|
<Titlebar />
|
||||||
<div class="flex-1 min-h-0 min-w-0 flex">
|
<div class="flex-1 min-h-0 min-w-0 flex">
|
||||||
<div class="flex-1 min-h-0 relative">
|
<div class="flex-1 min-h-0 relative">
|
||||||
|
|||||||
@@ -14,10 +14,11 @@ import { Spinner } from "@opencode-ai/ui/spinner"
|
|||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
import { type Session } from "@opencode-ai/sdk/v2/client"
|
import { type Session } from "@opencode-ai/sdk/v2/client"
|
||||||
import { type LocalProject } from "@/context/layout"
|
import { type LocalProject } from "@/context/layout"
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
import { loadSessionsQuery, useGlobalSync } from "@/context/global-sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items"
|
import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items"
|
||||||
import { sortedRootSessions, workspaceKey } from "./helpers"
|
import { sortedRootSessions, workspaceKey } from "./helpers"
|
||||||
|
import { useQuery } from "@tanstack/solid-query"
|
||||||
|
|
||||||
type InlineEditorComponent = (props: {
|
type InlineEditorComponent = (props: {
|
||||||
id: string
|
id: string
|
||||||
@@ -277,7 +278,7 @@ const WorkspaceSessionList = (props: {
|
|||||||
class="flex w-full text-left justify-start text-14-regular text-text-weak pl-2 pr-10"
|
class="flex w-full text-left justify-start text-14-regular text-text-weak pl-2 pr-10"
|
||||||
size="large"
|
size="large"
|
||||||
onClick={(e: MouseEvent) => {
|
onClick={(e: MouseEvent) => {
|
||||||
props.loadMore()
|
void props.loadMore()
|
||||||
;(e.currentTarget as HTMLButtonElement).blur()
|
;(e.currentTarget as HTMLButtonElement).blur()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -316,12 +317,11 @@ export const SortableWorkspace = (props: {
|
|||||||
})
|
})
|
||||||
const open = createMemo(() => props.ctx.workspaceExpanded(props.directory, local()))
|
const open = createMemo(() => props.ctx.workspaceExpanded(props.directory, local()))
|
||||||
const boot = createMemo(() => open() || active())
|
const boot = createMemo(() => open() || active())
|
||||||
const booted = createMemo((prev) => prev || workspaceStore.status === "complete", false)
|
|
||||||
const count = createMemo(() => sessions()?.length ?? 0)
|
const count = createMemo(() => sessions()?.length ?? 0)
|
||||||
const hasMore = createMemo(() => workspaceStore.sessionTotal > count())
|
const hasMore = createMemo(() => workspaceStore.sessionTotal > count())
|
||||||
|
const query = useQuery(() => ({ ...loadSessionsQuery(props.project.worktree) }))
|
||||||
const busy = createMemo(() => props.ctx.isBusy(props.directory))
|
const busy = createMemo(() => props.ctx.isBusy(props.directory))
|
||||||
const wasBusy = createMemo((prev) => prev || busy(), false)
|
const loading = () => query.isLoading
|
||||||
const loading = createMemo(() => open() && !booted() && count() === 0 && !wasBusy())
|
|
||||||
const touch = createMediaQuery("(hover: none)")
|
const touch = createMediaQuery("(hover: none)")
|
||||||
const showNew = createMemo(() => !loading() && (touch() || count() === 0 || (active() && !params.id)))
|
const showNew = createMemo(() => !loading() && (touch() || count() === 0 || (active() && !params.id)))
|
||||||
const loadMore = async () => {
|
const loadMore = async () => {
|
||||||
@@ -426,7 +426,7 @@ export const SortableWorkspace = (props: {
|
|||||||
mobile={props.mobile}
|
mobile={props.mobile}
|
||||||
ctx={props.ctx}
|
ctx={props.ctx}
|
||||||
showNew={showNew}
|
showNew={showNew}
|
||||||
loading={loading}
|
loading={() => query.isLoading && count() === 0}
|
||||||
sessions={sessions}
|
sessions={sessions}
|
||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
loadMore={loadMore}
|
loadMore={loadMore}
|
||||||
@@ -452,10 +452,10 @@ export const LocalWorkspace = (props: {
|
|||||||
})
|
})
|
||||||
const slug = createMemo(() => base64Encode(props.project.worktree))
|
const slug = createMemo(() => base64Encode(props.project.worktree))
|
||||||
const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow()))
|
const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow()))
|
||||||
const booted = createMemo((prev) => prev || workspace().store.status === "complete", false)
|
|
||||||
const count = createMemo(() => sessions()?.length ?? 0)
|
const count = createMemo(() => sessions()?.length ?? 0)
|
||||||
const loading = createMemo(() => !booted() && count() === 0)
|
const query = useQuery(() => ({ ...loadSessionsQuery(props.project.worktree) }))
|
||||||
const hasMore = createMemo(() => workspace().store.sessionTotal > count())
|
const hasMore = createMemo(() => workspace().store.sessionTotal > count())
|
||||||
|
const loading = () => query.isLoading && count() === 0
|
||||||
const loadMore = async () => {
|
const loadMore = async () => {
|
||||||
workspace().setStore("limit", (limit) => (limit ?? 0) + 5)
|
workspace().setStore("limit", (limit) => (limit ?? 0) + 5)
|
||||||
await globalSync.project.loadSessions(props.project.worktree)
|
await globalSync.project.loadSessions(props.project.worktree)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Project, UserMessage, VcsFileDiff } from "@opencode-ai/sdk/v2"
|
import type { Project, UserMessage } from "@opencode-ai/sdk/v2"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { useMutation } from "@tanstack/solid-query"
|
import { createQuery, skipToken, useMutation, useQueryClient } from "@tanstack/solid-query"
|
||||||
import {
|
import {
|
||||||
batch,
|
batch,
|
||||||
onCleanup,
|
onCleanup,
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
on,
|
on,
|
||||||
onMount,
|
onMount,
|
||||||
untrack,
|
untrack,
|
||||||
|
createResource,
|
||||||
} from "solid-js"
|
} from "solid-js"
|
||||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||||
import { createMediaQuery } from "@solid-primitives/media"
|
import { createMediaQuery } from "@solid-primitives/media"
|
||||||
@@ -323,6 +324,7 @@ export default function Page() {
|
|||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
const file = useFile()
|
const file = useFile()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
@@ -432,7 +434,6 @@ export default function Page() {
|
|||||||
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
||||||
const isChildSession = createMemo(() => !!info()?.parentID)
|
const isChildSession = createMemo(() => !!info()?.parentID)
|
||||||
const diffs = createMemo(() => (params.id ? list(sync.data.session_diff[params.id]) : []))
|
const diffs = createMemo(() => (params.id ? list(sync.data.session_diff[params.id]) : []))
|
||||||
const sessionCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length))
|
|
||||||
const canReview = createMemo(() => !!sync.project)
|
const canReview = createMemo(() => !!sync.project)
|
||||||
const reviewTab = createMemo(() => isDesktop())
|
const reviewTab = createMemo(() => isDesktop())
|
||||||
const tabState = createSessionTabs({
|
const tabState = createSessionTabs({
|
||||||
@@ -484,7 +485,7 @@ export default function Page() {
|
|||||||
if (!tab) return
|
if (!tab) return
|
||||||
|
|
||||||
const path = file.pathFromTab(tab)
|
const path = file.pathFromTab(tab)
|
||||||
if (path) file.load(path)
|
if (path) void file.load(path)
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
@@ -518,26 +519,6 @@ export default function Page() {
|
|||||||
deferRender: false,
|
deferRender: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const [vcs, setVcs] = createStore<{
|
|
||||||
diff: {
|
|
||||||
git: VcsFileDiff[]
|
|
||||||
branch: VcsFileDiff[]
|
|
||||||
}
|
|
||||||
ready: {
|
|
||||||
git: boolean
|
|
||||||
branch: boolean
|
|
||||||
}
|
|
||||||
}>({
|
|
||||||
diff: {
|
|
||||||
git: [] as VcsFileDiff[],
|
|
||||||
branch: [] as VcsFileDiff[],
|
|
||||||
},
|
|
||||||
ready: {
|
|
||||||
git: false,
|
|
||||||
branch: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const [followup, setFollowup] = persisted(
|
const [followup, setFollowup] = persisted(
|
||||||
Persist.workspace(sdk.directory, "followup", ["followup.v1"]),
|
Persist.workspace(sdk.directory, "followup", ["followup.v1"]),
|
||||||
createStore<{
|
createStore<{
|
||||||
@@ -571,68 +552,6 @@ export default function Page() {
|
|||||||
let todoTimer: number | undefined
|
let todoTimer: number | undefined
|
||||||
let diffFrame: number | undefined
|
let diffFrame: number | undefined
|
||||||
let diffTimer: number | undefined
|
let diffTimer: number | undefined
|
||||||
const vcsTask = new Map<VcsMode, Promise<void>>()
|
|
||||||
const vcsRun = new Map<VcsMode, number>()
|
|
||||||
|
|
||||||
const bumpVcs = (mode: VcsMode) => {
|
|
||||||
const next = (vcsRun.get(mode) ?? 0) + 1
|
|
||||||
vcsRun.set(mode, next)
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetVcs = (mode?: VcsMode) => {
|
|
||||||
const list = mode ? [mode] : (["git", "branch"] as const)
|
|
||||||
list.forEach((item) => {
|
|
||||||
bumpVcs(item)
|
|
||||||
vcsTask.delete(item)
|
|
||||||
setVcs("diff", item, [])
|
|
||||||
setVcs("ready", item, false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadVcs = (mode: VcsMode, force = false) => {
|
|
||||||
if (sync.project?.vcs !== "git") return Promise.resolve()
|
|
||||||
if (!force && vcs.ready[mode]) return Promise.resolve()
|
|
||||||
|
|
||||||
if (force) {
|
|
||||||
if (vcsTask.has(mode)) bumpVcs(mode)
|
|
||||||
vcsTask.delete(mode)
|
|
||||||
setVcs("ready", mode, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const current = vcsTask.get(mode)
|
|
||||||
if (current) return current
|
|
||||||
|
|
||||||
const run = bumpVcs(mode)
|
|
||||||
|
|
||||||
const task = sdk.client.vcs
|
|
||||||
.diff({ mode })
|
|
||||||
.then((result) => {
|
|
||||||
if (vcsRun.get(mode) !== run) return
|
|
||||||
setVcs("diff", mode, list(result.data))
|
|
||||||
setVcs("ready", mode, true)
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
if (vcsRun.get(mode) !== run) return
|
|
||||||
console.debug("[session-review] failed to load vcs diff", { mode, error })
|
|
||||||
setVcs("diff", mode, [])
|
|
||||||
setVcs("ready", mode, true)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
if (vcsTask.get(mode) === task) vcsTask.delete(mode)
|
|
||||||
})
|
|
||||||
|
|
||||||
vcsTask.set(mode, task)
|
|
||||||
return task
|
|
||||||
}
|
|
||||||
|
|
||||||
const refreshVcs = () => {
|
|
||||||
resetVcs()
|
|
||||||
const mode = untrack(vcsMode)
|
|
||||||
if (!mode) return
|
|
||||||
if (!untrack(wantsReview)) return
|
|
||||||
void loadVcs(mode, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
createComputed((prev) => {
|
createComputed((prev) => {
|
||||||
const open = desktopReviewOpen()
|
const open = desktopReviewOpen()
|
||||||
@@ -663,21 +582,52 @@ export default function Page() {
|
|||||||
list.push("turn")
|
list.push("turn")
|
||||||
return list
|
return list
|
||||||
})
|
})
|
||||||
|
const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes")
|
||||||
|
const wantsReview = createMemo(() =>
|
||||||
|
isDesktop()
|
||||||
|
? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review")
|
||||||
|
: store.mobileTab === "changes",
|
||||||
|
)
|
||||||
const vcsMode = createMemo<VcsMode | undefined>(() => {
|
const vcsMode = createMemo<VcsMode | undefined>(() => {
|
||||||
if (store.changes === "git" || store.changes === "branch") return store.changes
|
if (store.changes === "git" || store.changes === "branch") return store.changes
|
||||||
})
|
})
|
||||||
const reviewDiffs = createMemo(() => {
|
const vcsKey = createMemo(
|
||||||
if (store.changes === "git") return list(vcs.diff.git)
|
() => ["session-vcs", sdk.directory, sync.data.vcs?.branch ?? "", sync.data.vcs?.default_branch ?? ""] as const,
|
||||||
if (store.changes === "branch") return list(vcs.diff.branch)
|
)
|
||||||
|
const vcsQuery = createQuery(() => {
|
||||||
|
const mode = vcsMode()
|
||||||
|
const enabled = wantsReview() && sync.project?.vcs === "git"
|
||||||
|
|
||||||
|
return {
|
||||||
|
queryKey: [...vcsKey(), mode] as const,
|
||||||
|
enabled,
|
||||||
|
staleTime: Number.POSITIVE_INFINITY,
|
||||||
|
gcTime: 60 * 1000,
|
||||||
|
queryFn: mode
|
||||||
|
? () =>
|
||||||
|
sdk.client.vcs
|
||||||
|
.diff({ mode })
|
||||||
|
.then((result) => list(result.data))
|
||||||
|
.catch((error) => {
|
||||||
|
console.debug("[session-review] failed to load vcs diff", { mode, error })
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
: skipToken,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const refreshVcs = () => void queryClient.invalidateQueries({ queryKey: vcsKey() })
|
||||||
|
const reviewDiffs = () => {
|
||||||
|
if (store.changes === "git" || store.changes === "branch")
|
||||||
|
// avoids suspense
|
||||||
|
return vcsQuery.isFetched ? (vcsQuery.data ?? []) : []
|
||||||
return turnDiffs()
|
return turnDiffs()
|
||||||
})
|
}
|
||||||
const reviewCount = createMemo(() => reviewDiffs().length)
|
const reviewCount = () => reviewDiffs().length
|
||||||
const hasReview = createMemo(() => reviewCount() > 0)
|
const hasReview = () => reviewCount() > 0
|
||||||
const reviewReady = createMemo(() => {
|
const reviewReady = () => {
|
||||||
if (store.changes === "git") return vcs.ready.git
|
if (store.changes === "git" || store.changes === "branch") return !vcsQuery.isPending
|
||||||
if (store.changes === "branch") return vcs.ready.branch
|
|
||||||
return true
|
return true
|
||||||
})
|
}
|
||||||
|
|
||||||
const newSessionWorktree = createMemo(() => {
|
const newSessionWorktree = createMemo(() => {
|
||||||
if (store.newSessionWorktree === "create") return "create"
|
if (store.newSessionWorktree === "create") return "create"
|
||||||
@@ -805,8 +755,9 @@ export default function Page() {
|
|||||||
|
|
||||||
const hasScrollGesture = () => Date.now() - ui.scrollGesture < scrollGestureWindowMs
|
const hasScrollGesture = () => Date.now() - ui.scrollGesture < scrollGestureWindowMs
|
||||||
|
|
||||||
createEffect(
|
const [sessionSync] = createResource(
|
||||||
on([() => sdk.directory, () => params.id] as const, ([, id]) => {
|
() => [sdk.directory, params.id] as const,
|
||||||
|
([directory, id]) => {
|
||||||
if (refreshFrame !== undefined) cancelAnimationFrame(refreshFrame)
|
if (refreshFrame !== undefined) cancelAnimationFrame(refreshFrame)
|
||||||
if (refreshTimer !== undefined) window.clearTimeout(refreshTimer)
|
if (refreshTimer !== undefined) window.clearTimeout(refreshTimer)
|
||||||
refreshFrame = undefined
|
refreshFrame = undefined
|
||||||
@@ -817,13 +768,10 @@ export default function Page() {
|
|||||||
const stale = !cached
|
const stale = !cached
|
||||||
? false
|
? false
|
||||||
: (() => {
|
: (() => {
|
||||||
const info = getSessionPrefetch(sdk.directory, id)
|
const info = getSessionPrefetch(directory, id)
|
||||||
if (!info) return true
|
if (!info) return true
|
||||||
return Date.now() - info.at > SESSION_PREFETCH_TTL
|
return Date.now() - info.at > SESSION_PREFETCH_TTL
|
||||||
})()
|
})()
|
||||||
untrack(() => {
|
|
||||||
void sync.session.sync(id)
|
|
||||||
})
|
|
||||||
|
|
||||||
refreshFrame = requestAnimationFrame(() => {
|
refreshFrame = requestAnimationFrame(() => {
|
||||||
refreshFrame = undefined
|
refreshFrame = undefined
|
||||||
@@ -835,7 +783,9 @@ export default function Page() {
|
|||||||
})
|
})
|
||||||
}, 0)
|
}, 0)
|
||||||
})
|
})
|
||||||
}),
|
|
||||||
|
return sync.session.sync(id)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
@@ -897,27 +847,6 @@ export default function Page() {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
createEffect(
|
|
||||||
on(
|
|
||||||
() => sdk.directory,
|
|
||||||
() => {
|
|
||||||
resetVcs()
|
|
||||||
},
|
|
||||||
{ defer: true },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
createEffect(
|
|
||||||
on(
|
|
||||||
() => [sync.data.vcs?.branch, sync.data.vcs?.default_branch] as const,
|
|
||||||
(next, prev) => {
|
|
||||||
if (prev === undefined || same(next, prev)) return
|
|
||||||
refreshVcs()
|
|
||||||
},
|
|
||||||
{ defer: true },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const stopVcs = sdk.event.listen((evt) => {
|
const stopVcs = sdk.event.listen((evt) => {
|
||||||
if (evt.details.type !== "file.watcher.updated") return
|
if (evt.details.type !== "file.watcher.updated") return
|
||||||
const props =
|
const props =
|
||||||
@@ -1051,13 +980,6 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes")
|
|
||||||
const wantsReview = createMemo(() =>
|
|
||||||
isDesktop()
|
|
||||||
? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review")
|
|
||||||
: store.mobileTab === "changes",
|
|
||||||
)
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const list = changesOptions()
|
const list = changesOptions()
|
||||||
if (list.includes(store.changes)) return
|
if (list.includes(store.changes)) return
|
||||||
@@ -1066,22 +988,12 @@ export default function Page() {
|
|||||||
setStore("changes", next)
|
setStore("changes", next)
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const mode = vcsMode()
|
|
||||||
if (!mode) return
|
|
||||||
if (!wantsReview()) return
|
|
||||||
void loadVcs(mode)
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => sync.data.session_status[params.id ?? ""]?.type,
|
() => sync.data.session_status[params.id ?? ""]?.type,
|
||||||
(next, prev) => {
|
(next, prev) => {
|
||||||
const mode = vcsMode()
|
|
||||||
if (!mode) return
|
|
||||||
if (!wantsReview()) return
|
|
||||||
if (next !== "idle" || prev === undefined || prev === "idle") return
|
if (next !== "idle" || prev === undefined || prev === "idle") return
|
||||||
void loadVcs(mode, true)
|
refreshVcs()
|
||||||
},
|
},
|
||||||
{ defer: true },
|
{ defer: true },
|
||||||
),
|
),
|
||||||
@@ -1882,6 +1794,7 @@ export default function Page() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="relative bg-background-base size-full overflow-hidden flex flex-col">
|
<div class="relative bg-background-base size-full overflow-hidden flex flex-col">
|
||||||
|
{sessionSync() ?? ""}
|
||||||
<SessionHeader />
|
<SessionHeader />
|
||||||
<div class="flex-1 min-h-0 flex flex-col md:flex-row">
|
<div class="flex-1 min-h-0 flex flex-col md:flex-row">
|
||||||
<Show when={!isDesktop() && !!params.id}>
|
<Show when={!isDesktop() && !!params.id}>
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export const createOpenReviewFile = (input: {
|
|||||||
input.openTab(tab)
|
input.openTab(tab)
|
||||||
input.setActive(tab)
|
input.setActive(tab)
|
||||||
}
|
}
|
||||||
if (maybePromise instanceof Promise) maybePromise.then(open)
|
if (maybePromise instanceof Promise) void maybePromise.then(open)
|
||||||
else open()
|
else open()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ import { useCommand } from "@/context/command"
|
|||||||
import { useFile, type SelectedLineRange } from "@/context/file"
|
import { useFile, type SelectedLineRange } from "@/context/file"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
|
import { usePlatform } from "@/context/platform"
|
||||||
|
import { useSettings } from "@/context/settings"
|
||||||
|
import { useSync } from "@/context/sync"
|
||||||
import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
|
import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
|
||||||
import { FileTabContent } from "@/pages/session/file-tabs"
|
import { FileTabContent } from "@/pages/session/file-tabs"
|
||||||
import { createOpenSessionFileTab, createSessionTabs, getTabReorderIndex, type Sizing } from "@/pages/session/helpers"
|
import { createOpenSessionFileTab, createSessionTabs, getTabReorderIndex, type Sizing } from "@/pages/session/helpers"
|
||||||
@@ -39,6 +42,9 @@ export function SessionSidePanel(props: {
|
|||||||
size: Sizing
|
size: Sizing
|
||||||
}) {
|
}) {
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
|
const platform = usePlatform()
|
||||||
|
const settings = useSettings()
|
||||||
|
const sync = useSync()
|
||||||
const file = useFile()
|
const file = useFile()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const command = useCommand()
|
const command = useCommand()
|
||||||
@@ -46,9 +52,15 @@ export function SessionSidePanel(props: {
|
|||||||
const { sessionKey, tabs, view } = useSessionLayout()
|
const { sessionKey, tabs, view } = useSessionLayout()
|
||||||
|
|
||||||
const isDesktop = createMediaQuery("(min-width: 768px)")
|
const isDesktop = createMediaQuery("(min-width: 768px)")
|
||||||
|
const shown = createMemo(
|
||||||
|
() =>
|
||||||
|
platform.platform !== "desktop" ||
|
||||||
|
import.meta.env.VITE_OPENCODE_CHANNEL !== "beta" ||
|
||||||
|
settings.general.showFileTree(),
|
||||||
|
)
|
||||||
|
|
||||||
const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
|
const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
|
||||||
const fileOpen = createMemo(() => isDesktop() && layout.fileTree.opened())
|
const fileOpen = createMemo(() => isDesktop() && shown() && layout.fileTree.opened())
|
||||||
const open = createMemo(() => reviewOpen() || fileOpen())
|
const open = createMemo(() => reviewOpen() || fileOpen())
|
||||||
const reviewTab = createMemo(() => isDesktop())
|
const reviewTab = createMemo(() => isDesktop())
|
||||||
const panelWidth = createMemo(() => {
|
const panelWidth = createMemo(() => {
|
||||||
@@ -341,98 +353,99 @@ export function SessionSidePanel(props: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<Show when={shown()}>
|
||||||
id="file-tree-panel"
|
|
||||||
aria-hidden={!fileOpen()}
|
|
||||||
inert={!fileOpen()}
|
|
||||||
class="relative min-w-0 h-full shrink-0 overflow-hidden"
|
|
||||||
classList={{
|
|
||||||
"pointer-events-none": !fileOpen(),
|
|
||||||
"transition-[width] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
|
|
||||||
!props.size.active(),
|
|
||||||
}}
|
|
||||||
style={{ width: treeWidth() }}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="h-full flex flex-col overflow-hidden group/filetree"
|
id="file-tree-panel"
|
||||||
classList={{ "border-l border-border-weaker-base": reviewOpen() }}
|
aria-hidden={!fileOpen()}
|
||||||
|
inert={!fileOpen()}
|
||||||
|
class="relative min-w-0 h-full shrink-0 overflow-hidden"
|
||||||
|
classList={{
|
||||||
|
"pointer-events-none": !fileOpen(),
|
||||||
|
"transition-[width] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
|
||||||
|
!props.size.active(),
|
||||||
|
}}
|
||||||
|
style={{ width: treeWidth() }}
|
||||||
>
|
>
|
||||||
<Tabs
|
<div
|
||||||
variant="pill"
|
class="h-full flex flex-col overflow-hidden group/filetree"
|
||||||
value={fileTreeTab()}
|
classList={{ "border-l border-border-weaker-base": reviewOpen() }}
|
||||||
onChange={setFileTreeTabValue}
|
|
||||||
class="h-full"
|
|
||||||
data-scope="filetree"
|
|
||||||
>
|
>
|
||||||
<Tabs.List>
|
<Tabs
|
||||||
<Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}>
|
variant="pill"
|
||||||
{props.reviewCount()}{" "}
|
value={fileTreeTab()}
|
||||||
{language.t(
|
onChange={setFileTreeTabValue}
|
||||||
props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other",
|
class="h-full"
|
||||||
)}
|
data-scope="filetree"
|
||||||
</Tabs.Trigger>
|
>
|
||||||
<Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}>
|
<Tabs.List>
|
||||||
{language.t("session.files.all")}
|
<Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}>
|
||||||
</Tabs.Trigger>
|
{props.reviewCount()}{" "}
|
||||||
</Tabs.List>
|
{language.t(
|
||||||
<Tabs.Content value="changes" class="bg-background-stronger px-3 py-0">
|
props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other",
|
||||||
<Switch>
|
)}
|
||||||
<Match when={props.hasReview() || !props.diffsReady()}>
|
</Tabs.Trigger>
|
||||||
<Show
|
<Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}>
|
||||||
when={props.diffsReady()}
|
{language.t("session.files.all")}
|
||||||
fallback={
|
</Tabs.Trigger>
|
||||||
<div class="px-2 py-2 text-12-regular text-text-weak">
|
</Tabs.List>
|
||||||
{language.t("common.loading")}
|
<Tabs.Content value="changes" class="bg-background-stronger px-3 py-0">
|
||||||
{language.t("common.loading.ellipsis")}
|
<Switch>
|
||||||
</div>
|
<Match when={props.hasReview() || !props.diffsReady()}>
|
||||||
}
|
<Show
|
||||||
>
|
when={props.diffsReady()}
|
||||||
|
fallback={
|
||||||
|
<div class="px-2 py-2 text-12-regular text-text-weak">
|
||||||
|
{language.t("common.loading")}
|
||||||
|
{language.t("common.loading.ellipsis")}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FileTree
|
||||||
|
path=""
|
||||||
|
class="pt-3"
|
||||||
|
allowed={diffFiles()}
|
||||||
|
kinds={kinds()}
|
||||||
|
draggable={false}
|
||||||
|
active={props.activeDiff}
|
||||||
|
onFileClick={(node) => props.focusReviewDiff(node.path)}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</Tabs.Content>
|
||||||
|
<Tabs.Content value="all" class="bg-background-stronger px-3 py-0">
|
||||||
|
<Switch>
|
||||||
|
<Match when={nofiles()}>{empty(language.t("session.files.empty"))}</Match>
|
||||||
|
<Match when={true}>
|
||||||
<FileTree
|
<FileTree
|
||||||
path=""
|
path=""
|
||||||
class="pt-3"
|
class="pt-3"
|
||||||
allowed={diffFiles()}
|
modified={diffFiles()}
|
||||||
kinds={kinds()}
|
kinds={kinds()}
|
||||||
draggable={false}
|
onFileClick={(node) => openTab(file.tab(node.path))}
|
||||||
active={props.activeDiff}
|
|
||||||
onFileClick={(node) => props.focusReviewDiff(node.path)}
|
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Match>
|
||||||
</Match>
|
</Switch>
|
||||||
<Match when={true}>{empty(props.empty())}</Match>
|
</Tabs.Content>
|
||||||
</Switch>
|
</Tabs>
|
||||||
</Tabs.Content>
|
|
||||||
<Tabs.Content value="all" class="bg-background-stronger px-3 py-0">
|
|
||||||
<Switch>
|
|
||||||
<Match when={nofiles()}>{empty(language.t("session.files.empty"))}</Match>
|
|
||||||
<Match when={true}>
|
|
||||||
<FileTree
|
|
||||||
path=""
|
|
||||||
class="pt-3"
|
|
||||||
modified={diffFiles()}
|
|
||||||
kinds={kinds()}
|
|
||||||
onFileClick={(node) => openTab(file.tab(node.path))}
|
|
||||||
/>
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
</Tabs.Content>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
<Show when={fileOpen()}>
|
|
||||||
<div onPointerDown={() => props.size.start()}>
|
|
||||||
<ResizeHandle
|
|
||||||
direction="horizontal"
|
|
||||||
edge="start"
|
|
||||||
size={layout.fileTree.width()}
|
|
||||||
min={200}
|
|
||||||
max={480}
|
|
||||||
onResize={(width) => {
|
|
||||||
props.size.touch()
|
|
||||||
layout.fileTree.resize(width)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
<Show when={fileOpen()}>
|
||||||
</div>
|
<div onPointerDown={() => props.size.start()}>
|
||||||
|
<ResizeHandle
|
||||||
|
direction="horizontal"
|
||||||
|
edge="start"
|
||||||
|
size={layout.fileTree.width()}
|
||||||
|
min={200}
|
||||||
|
max={480}
|
||||||
|
onResize={(width) => {
|
||||||
|
props.size.touch()
|
||||||
|
layout.fileTree.resize(width)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import { useLanguage } from "@/context/language"
|
|||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { useLocal } from "@/context/local"
|
import { useLocal } from "@/context/local"
|
||||||
import { usePermission } from "@/context/permission"
|
import { usePermission } from "@/context/permission"
|
||||||
|
import { usePlatform } from "@/context/platform"
|
||||||
import { usePrompt } from "@/context/prompt"
|
import { usePrompt } from "@/context/prompt"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
|
import { useSettings } from "@/context/settings"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useTerminal } from "@/context/terminal"
|
import { useTerminal } from "@/context/terminal"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
@@ -39,8 +41,10 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
const permission = usePermission()
|
const permission = usePermission()
|
||||||
|
const platform = usePlatform()
|
||||||
const prompt = usePrompt()
|
const prompt = usePrompt()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
|
const settings = useSettings()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const terminal = useTerminal()
|
const terminal = useTerminal()
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
@@ -66,6 +70,10 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
})
|
})
|
||||||
const activeFileTab = tabState.activeFileTab
|
const activeFileTab = tabState.activeFileTab
|
||||||
const closableTab = tabState.closableTab
|
const closableTab = tabState.closableTab
|
||||||
|
const shown = () =>
|
||||||
|
platform.platform !== "desktop" ||
|
||||||
|
import.meta.env.VITE_OPENCODE_CHANNEL !== "beta" ||
|
||||||
|
settings.general.showFileTree()
|
||||||
|
|
||||||
const idle = { type: "idle" as const }
|
const idle = { type: "idle" as const }
|
||||||
const status = () => sync.data.session_status[params.id ?? ""] ?? idle
|
const status = () => sync.data.session_status[params.id ?? ""] ?? idle
|
||||||
@@ -457,12 +465,16 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|||||||
keybind: "mod+shift+r",
|
keybind: "mod+shift+r",
|
||||||
onSelect: () => view().reviewPanel.toggle(),
|
onSelect: () => view().reviewPanel.toggle(),
|
||||||
}),
|
}),
|
||||||
viewCommand({
|
...(shown()
|
||||||
id: "fileTree.toggle",
|
? [
|
||||||
title: language.t("command.fileTree.toggle"),
|
viewCommand({
|
||||||
keybind: "mod+\\",
|
id: "fileTree.toggle",
|
||||||
onSelect: () => layout.fileTree.toggle(),
|
title: language.t("command.fileTree.toggle"),
|
||||||
}),
|
keybind: "mod+\\",
|
||||||
|
onSelect: () => layout.fileTree.toggle(),
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
: []),
|
||||||
viewCommand({
|
viewCommand({
|
||||||
id: "input.focus",
|
id: "input.focus",
|
||||||
title: language.t("command.input.focus"),
|
title: language.t("command.input.focus"),
|
||||||
|
|||||||
@@ -469,7 +469,7 @@ export function persisted<T>(
|
|||||||
state,
|
state,
|
||||||
setState,
|
setState,
|
||||||
init,
|
init,
|
||||||
Object.assign(() => ready() === true, {
|
Object.assign(() => (ready.loading ? false : ready.latest === true), {
|
||||||
promise: init instanceof Promise ? init : undefined,
|
promise: init instanceof Promise ? init : undefined,
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ export function createSdkForServer({
|
|||||||
|
|
||||||
return createOpencodeClient({
|
return createOpencodeClient({
|
||||||
...config,
|
...config,
|
||||||
headers: { ...config.headers, ...auth },
|
headers: {
|
||||||
|
...(config.headers instanceof Headers ? Object.fromEntries(config.headers.entries()) : config.headers),
|
||||||
|
...auth,
|
||||||
|
},
|
||||||
baseUrl: server.url,
|
baseUrl: server.url,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.4.6",
|
"version": "1.4.11",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -105,4 +105,4 @@ async function main() {
|
|||||||
console.log(`✓ Sitemap generated at ${outputPath}`)
|
console.log(`✓ Sitemap generated at ${outputPath}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
void main()
|
||||||
|
|||||||
@@ -766,7 +766,7 @@ export default function Spotlight(props: SpotlightProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeWebGPU()
|
void initializeWebGPU()
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
if (cleanupFunctionRef) {
|
if (cleanupFunctionRef) {
|
||||||
|
|||||||
@@ -298,7 +298,7 @@ export default function BlackSubscribe() {
|
|||||||
|
|
||||||
// Resolve stripe promise once
|
// Resolve stripe promise once
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
stripePromise.then((s) => {
|
void stripePromise.then((s) => {
|
||||||
if (s) setStripe(s)
|
if (s) setStripe(s)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { APIEvent } from "@solidjs/start"
|
import type { APIEvent } from "@solidjs/start"
|
||||||
import type { DownloadPlatform } from "../types"
|
import type { DownloadPlatform } from "../types"
|
||||||
|
|
||||||
const assetNames: Record<string, string> = {
|
const prodAssetNames: Record<string, string> = {
|
||||||
"darwin-aarch64-dmg": "opencode-desktop-darwin-aarch64.dmg",
|
"darwin-aarch64-dmg": "opencode-desktop-darwin-aarch64.dmg",
|
||||||
"darwin-x64-dmg": "opencode-desktop-darwin-x64.dmg",
|
"darwin-x64-dmg": "opencode-desktop-darwin-x64.dmg",
|
||||||
"windows-x64-nsis": "opencode-desktop-windows-x64.exe",
|
"windows-x64-nsis": "opencode-desktop-windows-x64.exe",
|
||||||
@@ -10,6 +10,15 @@ const assetNames: Record<string, string> = {
|
|||||||
"linux-x64-rpm": "opencode-desktop-linux-x86_64.rpm",
|
"linux-x64-rpm": "opencode-desktop-linux-x86_64.rpm",
|
||||||
} satisfies Record<DownloadPlatform, string>
|
} satisfies Record<DownloadPlatform, string>
|
||||||
|
|
||||||
|
const betaAssetNames: Record<string, string> = {
|
||||||
|
"darwin-aarch64-dmg": "opencode-electron-mac-arm64.dmg",
|
||||||
|
"darwin-x64-dmg": "opencode-electron-mac-x64.dmg",
|
||||||
|
"windows-x64-nsis": "opencode-electron-win-x64.exe",
|
||||||
|
"linux-x64-deb": "opencode-electron-linux-amd64.deb",
|
||||||
|
"linux-x64-appimage": "opencode-electron-linux-x86_64.AppImage",
|
||||||
|
"linux-x64-rpm": "opencode-electron-linux-x86_64.rpm",
|
||||||
|
} satisfies Record<DownloadPlatform, string>
|
||||||
|
|
||||||
// Doing this on the server lets us preserve the original name for platforms we don't care to rename for
|
// Doing this on the server lets us preserve the original name for platforms we don't care to rename for
|
||||||
const downloadNames: Record<string, string> = {
|
const downloadNames: Record<string, string> = {
|
||||||
"darwin-aarch64-dmg": "OpenCode Desktop.dmg",
|
"darwin-aarch64-dmg": "OpenCode Desktop.dmg",
|
||||||
@@ -18,7 +27,7 @@ const downloadNames: Record<string, string> = {
|
|||||||
} satisfies { [K in DownloadPlatform]?: string }
|
} satisfies { [K in DownloadPlatform]?: string }
|
||||||
|
|
||||||
export async function GET({ params: { platform, channel } }: APIEvent) {
|
export async function GET({ params: { platform, channel } }: APIEvent) {
|
||||||
const assetName = assetNames[platform]
|
const assetName = channel === "stable" ? prodAssetNames[platform] : betaAssetNames[platform]
|
||||||
if (!assetName) return new Response(null, { status: 404 })
|
if (!assetName) return new Response(null, { status: 404 })
|
||||||
|
|
||||||
const resp = await fetch(
|
const resp = await fetch(
|
||||||
@@ -37,5 +46,5 @@ export async function GET({ params: { platform, channel } }: APIEvent) {
|
|||||||
const headers = new Headers(resp.headers)
|
const headers = new Headers(resp.headers)
|
||||||
if (downloadName) headers.set("content-disposition", `attachment; filename="${downloadName}"`)
|
if (downloadName) headers.set("content-disposition", `attachment; filename="${downloadName}"`)
|
||||||
|
|
||||||
return new Response(resp.body, { ...resp, headers })
|
return new Response(resp.body, { status: resp.status, statusText: resp.statusText, headers })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export default function Download() {
|
|||||||
|
|
||||||
const handleCopyClick = (command: string) => (event: Event) => {
|
const handleCopyClick = (command: string) => (event: Event) => {
|
||||||
const button = event.currentTarget as HTMLButtonElement
|
const button = event.currentTarget as HTMLButtonElement
|
||||||
navigator.clipboard.writeText(command)
|
void navigator.clipboard.writeText(command)
|
||||||
button.setAttribute("data-copied", "")
|
button.setAttribute("data-copied", "")
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
button.removeAttribute("data-copied")
|
button.removeAttribute("data-copied")
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { Header } from "~/component/header"
|
|||||||
import { Footer } from "~/component/footer"
|
import { Footer } from "~/component/footer"
|
||||||
import { Legal } from "~/component/legal"
|
import { Legal } from "~/component/legal"
|
||||||
import { github } from "~/lib/github"
|
import { github } from "~/lib/github"
|
||||||
import { createMemo } from "solid-js"
|
|
||||||
import { config } from "~/config"
|
import { config } from "~/config"
|
||||||
import { useI18n } from "~/context/i18n"
|
import { useI18n } from "~/context/i18n"
|
||||||
import { useLanguage } from "~/context/language"
|
import { useLanguage } from "~/context/language"
|
||||||
@@ -30,12 +29,12 @@ function CopyStatus() {
|
|||||||
export default function Home() {
|
export default function Home() {
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const githubData = createAsync(() => github())
|
const _githubData = createAsync(() => github())
|
||||||
const handleCopyClick = (event: Event) => {
|
const handleCopyClick = (event: Event) => {
|
||||||
const button = event.currentTarget as HTMLButtonElement
|
const button = event.currentTarget as HTMLButtonElement
|
||||||
const text = button.textContent
|
const text = button.textContent
|
||||||
if (text) {
|
if (text) {
|
||||||
navigator.clipboard.writeText(text)
|
void navigator.clipboard.writeText(text)
|
||||||
button.setAttribute("data-copied", "")
|
button.setAttribute("data-copied", "")
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
button.removeAttribute("data-copied")
|
button.removeAttribute("data-copied")
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export default function Home() {
|
|||||||
const callback = () => {
|
const callback = () => {
|
||||||
const text = button.textContent
|
const text = button.textContent
|
||||||
if (text) {
|
if (text) {
|
||||||
navigator.clipboard.writeText(text)
|
void navigator.clipboard.writeText(text)
|
||||||
button.setAttribute("data-copied", "")
|
button.setAttribute("data-copied", "")
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
button.removeAttribute("data-copied")
|
button.removeAttribute("data-copied")
|
||||||
|
|||||||
@@ -116,9 +116,9 @@ const createSessionUrl = action(async (workspaceID: string, returnUrl: string) =
|
|||||||
|
|
||||||
const setUseBalance = action(async (form: FormData) => {
|
const setUseBalance = action(async (form: FormData) => {
|
||||||
"use server"
|
"use server"
|
||||||
const workspaceID = form.get("workspaceID")?.toString()
|
const workspaceID = form.get("workspaceID") as string | null
|
||||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||||
const useBalance = form.get("useBalance")?.toString() === "true"
|
const useBalance = (form.get("useBalance") as string | null) === "true"
|
||||||
|
|
||||||
return json(
|
return json(
|
||||||
await withActor(async () => {
|
await withActor(async () => {
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import { formError, localizeError } from "~/lib/form-error"
|
|||||||
|
|
||||||
const setMonthlyLimit = action(async (form: FormData) => {
|
const setMonthlyLimit = action(async (form: FormData) => {
|
||||||
"use server"
|
"use server"
|
||||||
const limit = form.get("limit")?.toString()
|
const limit = form.get("limit") as string | null
|
||||||
if (!limit) return { error: formError.limitRequired }
|
if (!limit) return { error: formError.limitRequired }
|
||||||
const numericLimit = parseInt(limit)
|
const numericLimit = parseInt(limit)
|
||||||
if (numericLimit < 0) return { error: formError.monthlyLimitInvalid }
|
if (numericLimit < 0) return { error: formError.monthlyLimitInvalid }
|
||||||
const workspaceID = form.get("workspaceID")?.toString()
|
const workspaceID = form.get("workspaceID") as string | null
|
||||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||||
return json(
|
return json(
|
||||||
await withActor(
|
await withActor(
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { formError, formErrorReloadAmountMin, formErrorReloadTriggerMin, localiz
|
|||||||
|
|
||||||
const reload = action(async (form: FormData) => {
|
const reload = action(async (form: FormData) => {
|
||||||
"use server"
|
"use server"
|
||||||
const workspaceID = form.get("workspaceID")?.toString()
|
const workspaceID = form.get("workspaceID") as string | null
|
||||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||||
return json(await withActor(() => Billing.reload(), workspaceID), {
|
return json(await withActor(() => Billing.reload(), workspaceID), {
|
||||||
revalidate: queryBillingInfo.key,
|
revalidate: queryBillingInfo.key,
|
||||||
@@ -21,11 +21,11 @@ const reload = action(async (form: FormData) => {
|
|||||||
|
|
||||||
const setReload = action(async (form: FormData) => {
|
const setReload = action(async (form: FormData) => {
|
||||||
"use server"
|
"use server"
|
||||||
const workspaceID = form.get("workspaceID")?.toString()
|
const workspaceID = form.get("workspaceID") as string | null
|
||||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||||
const reloadValue = form.get("reload")?.toString() === "true"
|
const reloadValue = (form.get("reload") as string | null) === "true"
|
||||||
const amountStr = form.get("reloadAmount")?.toString()
|
const amountStr = form.get("reloadAmount") as string | null
|
||||||
const triggerStr = form.get("reloadTrigger")?.toString()
|
const triggerStr = form.get("reloadTrigger") as string | null
|
||||||
|
|
||||||
const reloadAmount = amountStr && amountStr.trim() !== "" ? parseInt(amountStr) : null
|
const reloadAmount = amountStr && amountStr.trim() !== "" ? parseInt(amountStr) : null
|
||||||
const reloadTrigger = triggerStr && triggerStr.trim() !== "" ? parseInt(triggerStr) : null
|
const reloadTrigger = triggerStr && triggerStr.trim() !== "" ? parseInt(triggerStr) : null
|
||||||
@@ -91,8 +91,8 @@ export function ReloadSection() {
|
|||||||
const info = billingInfo()!
|
const info = billingInfo()!
|
||||||
setStore("show", true)
|
setStore("show", true)
|
||||||
setStore("reload", true)
|
setStore("reload", true)
|
||||||
setStore("reloadAmount", info.reloadAmount.toString())
|
setStore("reloadAmount", String(info.reloadAmount))
|
||||||
setStore("reloadTrigger", info.reloadTrigger.toString())
|
setStore("reloadTrigger", String(info.reloadTrigger))
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
@@ -152,11 +152,11 @@ export function ReloadSection() {
|
|||||||
data-component="input"
|
data-component="input"
|
||||||
name="reloadAmount"
|
name="reloadAmount"
|
||||||
type="number"
|
type="number"
|
||||||
min={billingInfo()?.reloadAmountMin.toString()}
|
min={String(billingInfo()?.reloadAmountMin ?? "")}
|
||||||
step="1"
|
step="1"
|
||||||
value={store.reloadAmount}
|
value={store.reloadAmount}
|
||||||
onInput={(e) => setStore("reloadAmount", e.currentTarget.value)}
|
onInput={(e) => setStore("reloadAmount", e.currentTarget.value)}
|
||||||
placeholder={billingInfo()?.reloadAmount.toString()}
|
placeholder={String(billingInfo()?.reloadAmount ?? "")}
|
||||||
disabled={!store.reload}
|
disabled={!store.reload}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -166,11 +166,11 @@ export function ReloadSection() {
|
|||||||
data-component="input"
|
data-component="input"
|
||||||
name="reloadTrigger"
|
name="reloadTrigger"
|
||||||
type="number"
|
type="number"
|
||||||
min={billingInfo()?.reloadTriggerMin.toString()}
|
min={String(billingInfo()?.reloadTriggerMin ?? "")}
|
||||||
step="1"
|
step="1"
|
||||||
value={store.reloadTrigger}
|
value={store.reloadTrigger}
|
||||||
onInput={(e) => setStore("reloadTrigger", e.currentTarget.value)}
|
onInput={(e) => setStore("reloadTrigger", e.currentTarget.value)}
|
||||||
placeholder={billingInfo()?.reloadTrigger.toString()}
|
placeholder={String(billingInfo()?.reloadTrigger ?? "")}
|
||||||
disabled={!store.reload}
|
disabled={!store.reload}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -120,9 +120,9 @@ const createSessionUrl = action(async (workspaceID: string, returnUrl: string) =
|
|||||||
|
|
||||||
const setLiteUseBalance = action(async (form: FormData) => {
|
const setLiteUseBalance = action(async (form: FormData) => {
|
||||||
"use server"
|
"use server"
|
||||||
const workspaceID = form.get("workspaceID")?.toString()
|
const workspaceID = form.get("workspaceID") as string | null
|
||||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||||
const useBalance = form.get("useBalance")?.toString() === "true"
|
const useBalance = (form.get("useBalance") as string | null) === "true"
|
||||||
|
|
||||||
return json(
|
return json(
|
||||||
await withActor(async () => {
|
await withActor(async () => {
|
||||||
|
|||||||
@@ -12,18 +12,18 @@ import { formError, localizeError } from "~/lib/form-error"
|
|||||||
|
|
||||||
const removeKey = action(async (form: FormData) => {
|
const removeKey = action(async (form: FormData) => {
|
||||||
"use server"
|
"use server"
|
||||||
const id = form.get("id")?.toString()
|
const id = form.get("id") as string | null
|
||||||
if (!id) return { error: formError.idRequired }
|
if (!id) return { error: formError.idRequired }
|
||||||
const workspaceID = form.get("workspaceID")?.toString()
|
const workspaceID = form.get("workspaceID") as string | null
|
||||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||||
return json(await withActor(() => Key.remove({ id }), workspaceID), { revalidate: listKeys.key })
|
return json(await withActor(() => Key.remove({ id }), workspaceID), { revalidate: listKeys.key })
|
||||||
}, "key.remove")
|
}, "key.remove")
|
||||||
|
|
||||||
const createKey = action(async (form: FormData) => {
|
const createKey = action(async (form: FormData) => {
|
||||||
"use server"
|
"use server"
|
||||||
const name = form.get("name")?.toString().trim()
|
const name = (form.get("name") as string | null)?.trim()
|
||||||
if (!name) return { error: formError.nameRequired }
|
if (!name) return { error: formError.nameRequired }
|
||||||
const workspaceID = form.get("workspaceID")?.toString()
|
const workspaceID = form.get("workspaceID") as string | null
|
||||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||||
return json(
|
return json(
|
||||||
await withActor(
|
await withActor(
|
||||||
|
|||||||
@@ -24,13 +24,13 @@ const listMembers = query(async (workspaceID: string) => {
|
|||||||
|
|
||||||
const inviteMember = action(async (form: FormData) => {
|
const inviteMember = action(async (form: FormData) => {
|
||||||
"use server"
|
"use server"
|
||||||
const email = form.get("email")?.toString().trim()
|
const email = (form.get("email") as string | null)?.trim()
|
||||||
if (!email) return { error: formError.emailRequired }
|
if (!email) return { error: formError.emailRequired }
|
||||||
const workspaceID = form.get("workspaceID")?.toString()
|
const workspaceID = form.get("workspaceID") as string | null
|
||||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||||
const role = form.get("role")?.toString() as (typeof UserRole)[number]
|
const role = form.get("role") as (typeof UserRole)[number] | null
|
||||||
if (!role) return { error: formError.roleRequired }
|
if (!role) return { error: formError.roleRequired }
|
||||||
const limit = form.get("limit")?.toString()
|
const limit = form.get("limit") as string | null
|
||||||
const monthlyLimit = limit && limit.trim() !== "" ? parseInt(limit) : null
|
const monthlyLimit = limit && limit.trim() !== "" ? parseInt(limit) : null
|
||||||
if (monthlyLimit !== null && monthlyLimit < 0) return { error: formError.monthlyLimitInvalid }
|
if (monthlyLimit !== null && monthlyLimit < 0) return { error: formError.monthlyLimitInvalid }
|
||||||
return json(
|
return json(
|
||||||
@@ -47,9 +47,9 @@ const inviteMember = action(async (form: FormData) => {
|
|||||||
|
|
||||||
const removeMember = action(async (form: FormData) => {
|
const removeMember = action(async (form: FormData) => {
|
||||||
"use server"
|
"use server"
|
||||||
const id = form.get("id")?.toString()
|
const id = form.get("id") as string | null
|
||||||
if (!id) return { error: formError.idRequired }
|
if (!id) return { error: formError.idRequired }
|
||||||
const workspaceID = form.get("workspaceID")?.toString()
|
const workspaceID = form.get("workspaceID") as string | null
|
||||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||||
return json(
|
return json(
|
||||||
await withActor(
|
await withActor(
|
||||||
@@ -66,13 +66,13 @@ const removeMember = action(async (form: FormData) => {
|
|||||||
const updateMember = action(async (form: FormData) => {
|
const updateMember = action(async (form: FormData) => {
|
||||||
"use server"
|
"use server"
|
||||||
|
|
||||||
const id = form.get("id")?.toString()
|
const id = form.get("id") as string | null
|
||||||
if (!id) return { error: formError.idRequired }
|
if (!id) return { error: formError.idRequired }
|
||||||
const workspaceID = form.get("workspaceID")?.toString()
|
const workspaceID = form.get("workspaceID") as string | null
|
||||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||||
const role = form.get("role")?.toString() as (typeof UserRole)[number]
|
const role = form.get("role") as (typeof UserRole)[number] | null
|
||||||
if (!role) return { error: formError.roleRequired }
|
if (!role) return { error: formError.roleRequired }
|
||||||
const limit = form.get("limit")?.toString()
|
const limit = form.get("limit") as string | null
|
||||||
const monthlyLimit = limit && limit.trim() !== "" ? parseInt(limit) : null
|
const monthlyLimit = limit && limit.trim() !== "" ? parseInt(limit) : null
|
||||||
if (monthlyLimit !== null && monthlyLimit < 0) return { error: formError.monthlyLimitInvalid }
|
if (monthlyLimit !== null && monthlyLimit < 0) return { error: formError.monthlyLimitInvalid }
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@ function MemberRow(props: {
|
|||||||
}
|
}
|
||||||
setStore("editing", true)
|
setStore("editing", true)
|
||||||
setStore("selectedRole", props.member.role)
|
setStore("selectedRole", props.member.role)
|
||||||
setStore("limit", props.member.monthlyLimit?.toString() ?? "")
|
setStore("limit", props.member.monthlyLimit != null ? String(props.member.monthlyLimit) : "")
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
|
|||||||
@@ -67,11 +67,11 @@ const getModelsInfo = query(async (workspaceID: string) => {
|
|||||||
|
|
||||||
const updateModel = action(async (form: FormData) => {
|
const updateModel = action(async (form: FormData) => {
|
||||||
"use server"
|
"use server"
|
||||||
const model = form.get("model")?.toString()
|
const model = form.get("model") as string | null
|
||||||
if (!model) return { error: formError.modelRequired }
|
if (!model) return { error: formError.modelRequired }
|
||||||
const workspaceID = form.get("workspaceID")?.toString()
|
const workspaceID = form.get("workspaceID") as string | null
|
||||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||||
const enabled = form.get("enabled")?.toString() === "true"
|
const enabled = (form.get("enabled") as string | null) === "true"
|
||||||
return json(
|
return json(
|
||||||
withActor(async () => {
|
withActor(async () => {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
@@ -163,7 +163,7 @@ export function ModelSection() {
|
|||||||
<form action={updateModel} method="post">
|
<form action={updateModel} method="post">
|
||||||
<input type="hidden" name="model" value={id} />
|
<input type="hidden" name="model" value={id} />
|
||||||
<input type="hidden" name="workspaceID" value={params.id} />
|
<input type="hidden" name="workspaceID" value={params.id} />
|
||||||
<input type="hidden" name="enabled" value={isEnabled().toString()} />
|
<input type="hidden" name="enabled" value={String(isEnabled())} />
|
||||||
<label data-slot="model-toggle-label">
|
<label data-slot="model-toggle-label">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ function maskCredentials(credentials: string) {
|
|||||||
|
|
||||||
const removeProvider = action(async (form: FormData) => {
|
const removeProvider = action(async (form: FormData) => {
|
||||||
"use server"
|
"use server"
|
||||||
const provider = form.get("provider")?.toString()
|
const provider = form.get("provider") as string | null
|
||||||
if (!provider) return { error: formError.providerRequired }
|
if (!provider) return { error: formError.providerRequired }
|
||||||
const workspaceID = form.get("workspaceID")?.toString()
|
const workspaceID = form.get("workspaceID") as string | null
|
||||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||||
return json(await withActor(() => Provider.remove({ provider }), workspaceID), {
|
return json(await withActor(() => Provider.remove({ provider }), workspaceID), {
|
||||||
revalidate: listProviders.key,
|
revalidate: listProviders.key,
|
||||||
@@ -32,11 +32,11 @@ const removeProvider = action(async (form: FormData) => {
|
|||||||
|
|
||||||
const saveProvider = action(async (form: FormData) => {
|
const saveProvider = action(async (form: FormData) => {
|
||||||
"use server"
|
"use server"
|
||||||
const provider = form.get("provider")?.toString()
|
const provider = form.get("provider") as string | null
|
||||||
const credentials = form.get("credentials")?.toString()
|
const credentials = form.get("credentials") as string | null
|
||||||
if (!provider) return { error: formError.providerRequired }
|
if (!provider) return { error: formError.providerRequired }
|
||||||
if (!credentials) return { error: formError.apiKeyRequired }
|
if (!credentials) return { error: formError.apiKeyRequired }
|
||||||
const workspaceID = form.get("workspaceID")?.toString()
|
const workspaceID = form.get("workspaceID") as string | null
|
||||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||||
return json(
|
return json(
|
||||||
await withActor(
|
await withActor(
|
||||||
@@ -59,10 +59,13 @@ function ProviderRow(props: { provider: Provider }) {
|
|||||||
const params = useParams()
|
const params = useParams()
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
const providers = createAsync(() => listProviders(params.id!))
|
const providers = createAsync(() => listProviders(params.id!))
|
||||||
const saveSubmission = useSubmission(saveProvider, ([fd]) => fd.get("provider")?.toString() === props.provider.key)
|
const saveSubmission = useSubmission(
|
||||||
|
saveProvider,
|
||||||
|
([fd]) => (fd.get("provider") as string | null) === props.provider.key,
|
||||||
|
)
|
||||||
const removeSubmission = useSubmission(
|
const removeSubmission = useSubmission(
|
||||||
removeProvider,
|
removeProvider,
|
||||||
([fd]) => fd.get("provider")?.toString() === props.provider.key,
|
([fd]) => (fd.get("provider") as string | null) === props.provider.key,
|
||||||
)
|
)
|
||||||
const [store, setStore] = createStore({ editing: false })
|
const [store, setStore] = createStore({ editing: false })
|
||||||
|
|
||||||
|
|||||||
@@ -30,10 +30,10 @@ const getWorkspaceInfo = query(async (workspaceID: string) => {
|
|||||||
|
|
||||||
const updateWorkspace = action(async (form: FormData) => {
|
const updateWorkspace = action(async (form: FormData) => {
|
||||||
"use server"
|
"use server"
|
||||||
const name = form.get("name")?.toString().trim()
|
const name = (form.get("name") as string | null)?.trim()
|
||||||
if (!name) return { error: formError.workspaceNameRequired }
|
if (!name) return { error: formError.workspaceNameRequired }
|
||||||
if (name.length > 255) return { error: formError.nameTooLong }
|
if (name.length > 255) return { error: formError.nameTooLong }
|
||||||
const workspaceID = form.get("workspaceID")?.toString()
|
const workspaceID = form.get("workspaceID") as string | null
|
||||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||||
return json(
|
return json(
|
||||||
await withActor(
|
await withActor(
|
||||||
|
|||||||
@@ -26,14 +26,14 @@ export function createDataDumper(sessionId: string, requestId: string, projectId
|
|||||||
const minute = timestamp.substring(10, 12)
|
const minute = timestamp.substring(10, 12)
|
||||||
const second = timestamp.substring(12, 14)
|
const second = timestamp.substring(12, 14)
|
||||||
|
|
||||||
waitUntil(
|
void waitUntil(
|
||||||
Resource.ZenDataNew.put(
|
Resource.ZenDataNew.put(
|
||||||
`data/${data.modelName}/${year}/${month}/${day}/${hour}/${minute}/${second}/${requestId}.json`,
|
`data/${data.modelName}/${year}/${month}/${day}/${hour}/${minute}/${second}/${requestId}.json`,
|
||||||
JSON.stringify({ timestamp, ...data }),
|
JSON.stringify({ timestamp, ...data }),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
waitUntil(
|
void waitUntil(
|
||||||
Resource.ZenDataNew.put(
|
Resource.ZenDataNew.put(
|
||||||
`meta/${data.modelName}/${sessionId}/${requestId}.json`,
|
`meta/${data.modelName}/${sessionId}/${requestId}.json`,
|
||||||
JSON.stringify({ timestamp, ...metadata }),
|
JSON.stringify({ timestamp, ...metadata }),
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import { LiteData } from "@opencode-ai/console-core/lite.js"
|
|||||||
import { Resource } from "@opencode-ai/console-resource"
|
import { Resource } from "@opencode-ai/console-resource"
|
||||||
import { i18n, type Key } from "~/i18n"
|
import { i18n, type Key } from "~/i18n"
|
||||||
import { localeFromRequest } from "~/lib/language"
|
import { localeFromRequest } from "~/lib/language"
|
||||||
|
import { createModelTpmLimiter } from "./modelTpmLimiter"
|
||||||
|
|
||||||
type ZenData = Awaited<ReturnType<typeof ZenData.list>>
|
type ZenData = Awaited<ReturnType<typeof ZenData.list>>
|
||||||
type RetryOptions = {
|
type RetryOptions = {
|
||||||
@@ -121,6 +122,8 @@ export async function handler(
|
|||||||
const authInfo = await authenticate(modelInfo, zenApiKey)
|
const authInfo = await authenticate(modelInfo, zenApiKey)
|
||||||
const billingSource = validateBilling(authInfo, modelInfo)
|
const billingSource = validateBilling(authInfo, modelInfo)
|
||||||
logger.metric({ source: billingSource })
|
logger.metric({ source: billingSource })
|
||||||
|
const modelTpmLimiter = createModelTpmLimiter(modelInfo.providers)
|
||||||
|
const modelTpmLimits = await modelTpmLimiter?.check()
|
||||||
|
|
||||||
const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => {
|
const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => {
|
||||||
const providerInfo = selectProvider(
|
const providerInfo = selectProvider(
|
||||||
@@ -133,6 +136,7 @@ export async function handler(
|
|||||||
trialProviders,
|
trialProviders,
|
||||||
retry,
|
retry,
|
||||||
stickyProvider,
|
stickyProvider,
|
||||||
|
modelTpmLimits,
|
||||||
)
|
)
|
||||||
validateModelSettings(billingSource, authInfo)
|
validateModelSettings(billingSource, authInfo)
|
||||||
updateProviderKey(authInfo, providerInfo)
|
updateProviderKey(authInfo, providerInfo)
|
||||||
@@ -229,6 +233,7 @@ export async function handler(
|
|||||||
const usageInfo = providerInfo.normalizeUsage(json.usage)
|
const usageInfo = providerInfo.normalizeUsage(json.usage)
|
||||||
const costInfo = calculateCost(modelInfo, usageInfo)
|
const costInfo = calculateCost(modelInfo, usageInfo)
|
||||||
await trialLimiter?.track(usageInfo)
|
await trialLimiter?.track(usageInfo)
|
||||||
|
await modelTpmLimiter?.track(providerInfo.id, providerInfo.model, usageInfo)
|
||||||
await trackUsage(sessionId, billingSource, authInfo, modelInfo, providerInfo, usageInfo, costInfo)
|
await trackUsage(sessionId, billingSource, authInfo, modelInfo, providerInfo, usageInfo, costInfo)
|
||||||
await reload(billingSource, authInfo, costInfo)
|
await reload(billingSource, authInfo, costInfo)
|
||||||
json.cost = calculateOccurredCost(billingSource, costInfo)
|
json.cost = calculateOccurredCost(billingSource, costInfo)
|
||||||
@@ -278,6 +283,7 @@ export async function handler(
|
|||||||
const usageInfo = providerInfo.normalizeUsage(usage)
|
const usageInfo = providerInfo.normalizeUsage(usage)
|
||||||
const costInfo = calculateCost(modelInfo, usageInfo)
|
const costInfo = calculateCost(modelInfo, usageInfo)
|
||||||
await trialLimiter?.track(usageInfo)
|
await trialLimiter?.track(usageInfo)
|
||||||
|
await modelTpmLimiter?.track(providerInfo.id, providerInfo.model, usageInfo)
|
||||||
await trackUsage(sessionId, billingSource, authInfo, modelInfo, providerInfo, usageInfo, costInfo)
|
await trackUsage(sessionId, billingSource, authInfo, modelInfo, providerInfo, usageInfo, costInfo)
|
||||||
await reload(billingSource, authInfo, costInfo)
|
await reload(billingSource, authInfo, costInfo)
|
||||||
const cost = calculateOccurredCost(billingSource, costInfo)
|
const cost = calculateOccurredCost(billingSource, costInfo)
|
||||||
@@ -433,12 +439,16 @@ export async function handler(
|
|||||||
trialProviders: string[] | undefined,
|
trialProviders: string[] | undefined,
|
||||||
retry: RetryOptions,
|
retry: RetryOptions,
|
||||||
stickyProvider: string | undefined,
|
stickyProvider: string | undefined,
|
||||||
|
modelTpmLimits: Record<string, number> | undefined,
|
||||||
) {
|
) {
|
||||||
const modelProvider = (() => {
|
const modelProvider = (() => {
|
||||||
|
// Byok is top priority b/c if user set their own API key, we should use it
|
||||||
|
// instead of using the sticky provider for the same session
|
||||||
if (authInfo?.provider?.credentials) {
|
if (authInfo?.provider?.credentials) {
|
||||||
return modelInfo.providers.find((provider) => provider.id === modelInfo.byokProvider)
|
return modelInfo.providers.find((provider) => provider.id === modelInfo.byokProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always use the same provider for the same session
|
||||||
if (stickyProvider) {
|
if (stickyProvider) {
|
||||||
const provider = modelInfo.providers.find((provider) => provider.id === stickyProvider)
|
const provider = modelInfo.providers.find((provider) => provider.id === stickyProvider)
|
||||||
if (provider) return provider
|
if (provider) return provider
|
||||||
@@ -451,10 +461,20 @@ export async function handler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (retry.retryCount !== MAX_FAILOVER_RETRIES) {
|
if (retry.retryCount !== MAX_FAILOVER_RETRIES) {
|
||||||
const providers = modelInfo.providers
|
const allProviders = modelInfo.providers
|
||||||
.filter((provider) => !provider.disabled)
|
.filter((provider) => !provider.disabled)
|
||||||
|
.filter((provider) => provider.weight !== 0)
|
||||||
.filter((provider) => !retry.excludeProviders.includes(provider.id))
|
.filter((provider) => !retry.excludeProviders.includes(provider.id))
|
||||||
.flatMap((provider) => Array<typeof provider>(provider.weight ?? 1).fill(provider))
|
.filter((provider) => {
|
||||||
|
if (!provider.tpmLimit) return true
|
||||||
|
const usage = modelTpmLimits?.[`${provider.id}/${provider.model}`] ?? 0
|
||||||
|
return usage < provider.tpmLimit * 1_000_000
|
||||||
|
})
|
||||||
|
|
||||||
|
const topPriority = Math.min(...allProviders.map((p) => p.priority))
|
||||||
|
const providers = allProviders
|
||||||
|
.filter((p) => p.priority <= topPriority)
|
||||||
|
.flatMap((provider) => Array<typeof provider>(provider.weight).fill(provider))
|
||||||
|
|
||||||
// Use the last 4 characters of session ID to select a provider
|
// Use the last 4 characters of session ID to select a provider
|
||||||
const identifier = sessionId.length ? sessionId : ip
|
const identifier = sessionId.length ? sessionId : ip
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { and, Database, eq, inArray, sql } from "@opencode-ai/console-core/drizzle/index.js"
|
||||||
|
import { ModelRateLimitTable } from "@opencode-ai/console-core/schema/ip.sql.js"
|
||||||
|
import { UsageInfo } from "./provider/provider"
|
||||||
|
|
||||||
|
export function createModelTpmLimiter(providers: { id: string; model: string; tpmLimit?: number }[]) {
|
||||||
|
const keys = providers.filter((p) => p.tpmLimit).map((p) => `${p.id}/${p.model}`)
|
||||||
|
if (keys.length === 0) return
|
||||||
|
|
||||||
|
const yyyyMMddHHmm = new Date(Date.now())
|
||||||
|
.toISOString()
|
||||||
|
.replace(/[^0-9]/g, "")
|
||||||
|
.substring(0, 12)
|
||||||
|
|
||||||
|
return {
|
||||||
|
check: async () => {
|
||||||
|
const data = await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.select()
|
||||||
|
.from(ModelRateLimitTable)
|
||||||
|
.where(and(inArray(ModelRateLimitTable.key, keys), eq(ModelRateLimitTable.interval, yyyyMMddHHmm))),
|
||||||
|
)
|
||||||
|
|
||||||
|
// convert to map of model to count
|
||||||
|
return data.reduce(
|
||||||
|
(acc, curr) => {
|
||||||
|
acc[curr.key] = curr.count
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{} as Record<string, number>,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
track: async (id: string, model: string, usageInfo: UsageInfo) => {
|
||||||
|
const key = `${id}/${model}`
|
||||||
|
if (!keys.includes(key)) return
|
||||||
|
const usage =
|
||||||
|
usageInfo.inputTokens +
|
||||||
|
usageInfo.outputTokens +
|
||||||
|
(usageInfo.reasoningTokens ?? 0) +
|
||||||
|
(usageInfo.cacheReadTokens ?? 0) +
|
||||||
|
(usageInfo.cacheWrite5mTokens ?? 0) +
|
||||||
|
(usageInfo.cacheWrite1hTokens ?? 0)
|
||||||
|
if (usage <= 0) return
|
||||||
|
await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.insert(ModelRateLimitTable)
|
||||||
|
.values({ key, interval: yyyyMMddHHmm, count: usage })
|
||||||
|
.onDuplicateKeyUpdate({ set: { count: sql`${ModelRateLimitTable.count} + ${usage}` } }),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
CREATE TABLE `model_rate_limit` (
|
||||||
|
`key` varchar(255) NOT NULL,
|
||||||
|
`interval` varchar(40) NOT NULL,
|
||||||
|
`count` int NOT NULL,
|
||||||
|
CONSTRAINT PRIMARY KEY(`key`,`interval`)
|
||||||
|
);
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.4.6",
|
"version": "1.4.11",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ export namespace ZenData {
|
|||||||
z.object({
|
z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
model: z.string(),
|
model: z.string(),
|
||||||
|
priority: z.number().optional(),
|
||||||
|
tpmLimit: z.number().optional(),
|
||||||
weight: z.number().optional(),
|
weight: z.number().optional(),
|
||||||
disabled: z.boolean().optional(),
|
disabled: z.boolean().optional(),
|
||||||
storeModel: z.string().optional(),
|
storeModel: z.string().optional(),
|
||||||
@@ -123,10 +125,16 @@ export namespace ZenData {
|
|||||||
),
|
),
|
||||||
models: (() => {
|
models: (() => {
|
||||||
const normalize = (model: z.infer<typeof ModelSchema>) => {
|
const normalize = (model: z.infer<typeof ModelSchema>) => {
|
||||||
const composite = model.providers.find((p) => compositeProviders[p.id].length > 1)
|
const providers = model.providers.map((p) => ({
|
||||||
|
...p,
|
||||||
|
priority: p.priority ?? Infinity,
|
||||||
|
weight: p.weight ?? 1,
|
||||||
|
}))
|
||||||
|
const composite = providers.find((p) => compositeProviders[p.id].length > 1)
|
||||||
if (!composite)
|
if (!composite)
|
||||||
return {
|
return {
|
||||||
trialProvider: model.trialProvider ? [model.trialProvider] : undefined,
|
trialProvider: model.trialProvider ? [model.trialProvider] : undefined,
|
||||||
|
providers,
|
||||||
}
|
}
|
||||||
|
|
||||||
const weightMulti = compositeProviders[composite.id].length
|
const weightMulti = compositeProviders[composite.id].length
|
||||||
@@ -137,17 +145,16 @@ export namespace ZenData {
|
|||||||
if (model.trialProvider === composite.id) return compositeProviders[composite.id].map((p) => p.id)
|
if (model.trialProvider === composite.id) return compositeProviders[composite.id].map((p) => p.id)
|
||||||
return [model.trialProvider]
|
return [model.trialProvider]
|
||||||
})(),
|
})(),
|
||||||
providers: model.providers.flatMap((p) =>
|
providers: providers.flatMap((p) =>
|
||||||
p.id === composite.id
|
p.id === composite.id
|
||||||
? compositeProviders[p.id].map((sub) => ({
|
? compositeProviders[p.id].map((sub) => ({
|
||||||
...p,
|
...p,
|
||||||
id: sub.id,
|
id: sub.id,
|
||||||
weight: p.weight ?? 1,
|
|
||||||
}))
|
}))
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
...p,
|
...p,
|
||||||
weight: (p.weight ?? 1) * weightMulti,
|
weight: p.weight * weightMulti,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -30,3 +30,13 @@ export const KeyRateLimitTable = mysqlTable(
|
|||||||
},
|
},
|
||||||
(table) => [primaryKey({ columns: [table.key, table.interval] })],
|
(table) => [primaryKey({ columns: [table.key, table.interval] })],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const ModelRateLimitTable = mysqlTable(
|
||||||
|
"model_rate_limit",
|
||||||
|
{
|
||||||
|
key: varchar("key", { length: 255 }).notNull(),
|
||||||
|
interval: varchar("interval", { length: 40 }).notNull(),
|
||||||
|
count: int("count").notNull(),
|
||||||
|
},
|
||||||
|
(table) => [primaryKey({ columns: [table.key, table.interval] })],
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.4.6",
|
"version": "1.4.11",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.4.6",
|
"version": "1.4.11",
|
||||||
"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",
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ export default defineConfig({
|
|||||||
plugins: [appPlugin],
|
plugins: [appPlugin],
|
||||||
publicDir: "../../../app/public",
|
publicDir: "../../../app/public",
|
||||||
root: "src/renderer",
|
root: "src/renderer",
|
||||||
|
define: {
|
||||||
|
"import.meta.env.VITE_OPENCODE_CHANNEL": JSON.stringify(channel),
|
||||||
|
},
|
||||||
build: {
|
build: {
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: {
|
input: {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/desktop-electron",
|
"name": "@opencode-ai/desktop-electron",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.4.6",
|
"version": "1.4.11",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://opencode.ai",
|
"homepage": "https://opencode.ai",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.4.6",
|
"version": "1.4.11",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
if (location.pathname === "/loading") {
|
if (location.pathname === "/loading") {
|
||||||
import("./loading")
|
void import("./loading")
|
||||||
} else {
|
} else {
|
||||||
import("./")
|
void import("./")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -410,7 +410,7 @@ const createPlatform = (): Platform => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let menuTrigger = null as null | ((id: string) => void)
|
let menuTrigger = null as null | ((id: string) => void)
|
||||||
createMenu((id) => {
|
void createMenu((id) => {
|
||||||
menuTrigger?.(id)
|
menuTrigger?.(id)
|
||||||
})
|
})
|
||||||
void listenForDeepLinks()
|
void listenForDeepLinks()
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ render(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
listener.then((cb) => cb())
|
void listener.then((cb) => cb())
|
||||||
timers.forEach(clearTimeout)
|
timers.forEach(clearTimeout)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -186,5 +186,5 @@ export async function createMenu(trigger: (id: string) => void) {
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
menu.setAsAppMenu()
|
void menu.setAsAppMenu()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const clamp = (value: number) => Math.min(Math.max(value, MIN_ZOOM_LEVEL), MAX_Z
|
|||||||
|
|
||||||
const applyZoom = (next: number) => {
|
const applyZoom = (next: number) => {
|
||||||
setWebviewZoom(next)
|
setWebviewZoom(next)
|
||||||
invoke("plugin:webview|set_webview_zoom", {
|
void invoke("plugin:webview|set_webview_zoom", {
|
||||||
value: next,
|
value: next,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.4.6",
|
"version": "1.4.11",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -37,4 +37,4 @@ async function test() {
|
|||||||
await Share.remove({ id: shareInfo.id, secret: shareInfo.secret })
|
await Share.remove({ id: shareInfo.id, secret: shareInfo.secret })
|
||||||
}
|
}
|
||||||
|
|
||||||
test()
|
void test()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
id = "opencode"
|
id = "opencode"
|
||||||
name = "OpenCode"
|
name = "OpenCode"
|
||||||
description = "The open source coding agent."
|
description = "The open source coding agent."
|
||||||
version = "1.4.6"
|
version = "1.4.11"
|
||||||
schema_version = 1
|
schema_version = 1
|
||||||
authors = ["Anomaly"]
|
authors = ["Anomaly"]
|
||||||
repository = "https://github.com/anomalyco/opencode"
|
repository = "https://github.com/anomalyco/opencode"
|
||||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
|||||||
icon = "./icons/opencode.svg"
|
icon = "./icons/opencode.svg"
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-aarch64]
|
[agent_servers.opencode.targets.darwin-aarch64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.6/opencode-darwin-arm64.zip"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.11/opencode-darwin-arm64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-x86_64]
|
[agent_servers.opencode.targets.darwin-x86_64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.6/opencode-darwin-x64.zip"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.11/opencode-darwin-x64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-aarch64]
|
[agent_servers.opencode.targets.linux-aarch64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.6/opencode-linux-arm64.tar.gz"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.11/opencode-linux-arm64.tar.gz"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-x86_64]
|
[agent_servers.opencode.targets.linux-x86_64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.6/opencode-linux-x64.tar.gz"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.11/opencode-linux-x64.tar.gz"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.windows-x86_64]
|
[agent_servers.opencode.targets.windows-x86_64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.6/opencode-windows-x64.zip"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.11/opencode-windows-x64.zip"
|
||||||
cmd = "./opencode.exe"
|
cmd = "./opencode.exe"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.4.6",
|
"version": "1.4.11",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
research
|
research
|
||||||
dist
|
dist
|
||||||
|
dist-*
|
||||||
gen
|
gen
|
||||||
app.log
|
app.log
|
||||||
src/provider/models-snapshot.js
|
src/provider/models-snapshot.js
|
||||||
src/provider/models-snapshot.d.ts
|
src/provider/models-snapshot.d.ts
|
||||||
|
script/build-*.ts
|
||||||
|
temporary-*.md
|
||||||
|
|||||||
-31
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"name": ".opencode",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"dependencies": {
|
|
||||||
"@opencode-ai/plugin": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@opencode-ai/plugin": {
|
|
||||||
"version": "1.2.6",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@opencode-ai/sdk": "1.2.6",
|
|
||||||
"zod": "4.1.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@opencode-ai/sdk": {
|
|
||||||
"version": "1.2.6",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/zod": {
|
|
||||||
"version": "4.1.8",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,6 +9,63 @@
|
|||||||
- **Output**: creates `migration/<timestamp>_<slug>/migration.sql` and `snapshot.json`.
|
- **Output**: creates `migration/<timestamp>_<slug>/migration.sql` and `snapshot.json`.
|
||||||
- **Tests**: migration tests should read the per-folder layout (no `_journal.json`).
|
- **Tests**: migration tests should read the per-folder layout (no `_journal.json`).
|
||||||
|
|
||||||
|
# Module shape
|
||||||
|
|
||||||
|
Do not use `export namespace Foo { ... }` for module organization. It is not
|
||||||
|
standard ESM, it prevents tree-shaking, and it breaks Node's native TypeScript
|
||||||
|
runner. Use flat top-level exports combined with a self-reexport at the bottom
|
||||||
|
of the file:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/foo/foo.ts
|
||||||
|
export interface Interface { ... }
|
||||||
|
export class Service extends Context.Service<Service, Interface>()("@opencode/Foo") {}
|
||||||
|
export const layer = Layer.effect(Service, ...)
|
||||||
|
export const defaultLayer = layer.pipe(...)
|
||||||
|
|
||||||
|
export * as Foo from "./foo"
|
||||||
|
```
|
||||||
|
|
||||||
|
Consumers import the namespace projection:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Foo } from "@/foo/foo"
|
||||||
|
|
||||||
|
yield * Foo.Service
|
||||||
|
Foo.layer
|
||||||
|
Foo.defaultLayer
|
||||||
|
```
|
||||||
|
|
||||||
|
Namespace-private helpers stay as non-exported top-level declarations in the
|
||||||
|
same file — they remain inaccessible to consumers (they are not projected by
|
||||||
|
`export * as`) but are usable by the file's own code.
|
||||||
|
|
||||||
|
## When the file is an `index.ts`
|
||||||
|
|
||||||
|
If the module is `foo/index.ts` (single-namespace directory), use `"."` for
|
||||||
|
the self-reexport source rather than `"./index"`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/foo/index.ts
|
||||||
|
export const thing = ...
|
||||||
|
|
||||||
|
export * as Foo from "."
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multi-sibling directories
|
||||||
|
|
||||||
|
For directories with several independent modules (e.g. `src/session/`,
|
||||||
|
`src/config/`), keep each sibling as its own file with its own self-reexport,
|
||||||
|
and do not add a barrel `index.ts`. Consumers import the specific sibling:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { SessionRetry } from "@/session/retry"
|
||||||
|
import { SessionStatus } from "@/session/status"
|
||||||
|
```
|
||||||
|
|
||||||
|
Barrels in multi-sibling directories force every import through the barrel to
|
||||||
|
evaluate every sibling, which defeats tree-shaking and slows module load.
|
||||||
|
|
||||||
# opencode Effect rules
|
# opencode Effect rules
|
||||||
|
|
||||||
Use these rules when writing or migrating Effect code.
|
Use these rules when writing or migrating Effect code.
|
||||||
@@ -23,6 +80,10 @@ See `specs/effect/migration.md` for the compact pattern reference and examples.
|
|||||||
- Use `Effect.callback` for callback-based APIs.
|
- Use `Effect.callback` for callback-based APIs.
|
||||||
- Prefer `DateTime.nowAsDate` over `new Date(yield* Clock.currentTimeMillis)` when you need a `Date`.
|
- Prefer `DateTime.nowAsDate` over `new Date(yield* Clock.currentTimeMillis)` when you need a `Date`.
|
||||||
|
|
||||||
|
## Module conventions
|
||||||
|
|
||||||
|
- In `src/config`, follow the existing self-export pattern at the top of the file (for example `export * as ConfigAgent from "./agent"`) when adding a new config module.
|
||||||
|
|
||||||
## Schemas and errors
|
## Schemas and errors
|
||||||
|
|
||||||
- Use `Schema.Class` for multi-field data.
|
- Use `Schema.Class` for multi-field data.
|
||||||
@@ -39,6 +100,12 @@ See `specs/effect/migration.md` for the compact pattern reference and examples.
|
|||||||
- Do the work directly in the `InstanceState.make` closure — `ScopedCache` handles run-once semantics. Don't add fibers, `ensure()` callbacks, or `started` flags on top.
|
- Do the work directly in the `InstanceState.make` closure — `ScopedCache` handles run-once semantics. Don't add fibers, `ensure()` callbacks, or `started` flags on top.
|
||||||
- Use `Effect.addFinalizer` or `Effect.acquireRelease` inside the `InstanceState.make` closure for cleanup (subscriptions, process teardown, etc.).
|
- Use `Effect.addFinalizer` or `Effect.acquireRelease` inside the `InstanceState.make` closure for cleanup (subscriptions, process teardown, etc.).
|
||||||
- Use `Effect.forkScoped` inside the closure for background stream consumers — the fiber is interrupted when the instance is disposed.
|
- Use `Effect.forkScoped` inside the closure for background stream consumers — the fiber is interrupted when the instance is disposed.
|
||||||
|
- To make a service's `init()` non-blocking, fork `InstanceState.get(state)` at the `init()` call site (e.g. `Effect.forkIn(scope)`), not by forking work inside the `InstanceState.make` closure. Forking inside the closure leaves state incomplete for other methods that read it.
|
||||||
|
- `src/project/bootstrap.ts` already wraps every service `init()` in `Effect.forkDetach`, so `init()` is fire-and-forget in production. Keep `init()` methods synchronous internally; the caller controls concurrency.
|
||||||
|
|
||||||
|
## Effect v4 beta API
|
||||||
|
|
||||||
|
- `Effect.fork` and `Effect.forkDaemon` do not exist. Use `Effect.forkIn(scope)` to fork a fiber into a specific scope.
|
||||||
|
|
||||||
## Preferred Effect services
|
## Preferred Effect services
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.4.6",
|
"version": "1.4.11",
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
"fix-node-pty": "bun run script/fix-node-pty.ts",
|
"fix-node-pty": "bun run script/fix-node-pty.ts",
|
||||||
"upgrade-opentui": "bun run script/upgrade-opentui.ts",
|
"upgrade-opentui": "bun run script/upgrade-opentui.ts",
|
||||||
"dev": "bun run --conditions=browser ./src/index.ts",
|
"dev": "bun run --conditions=browser ./src/index.ts",
|
||||||
|
"dev:temporary": "bun run --conditions=browser ./src/temporary.ts",
|
||||||
"db": "bun drizzle-kit"
|
"db": "bun drizzle-kit"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -78,15 +79,15 @@
|
|||||||
"@actions/github": "6.0.1",
|
"@actions/github": "6.0.1",
|
||||||
"@agentclientprotocol/sdk": "0.16.1",
|
"@agentclientprotocol/sdk": "0.16.1",
|
||||||
"@ai-sdk/alibaba": "1.0.17",
|
"@ai-sdk/alibaba": "1.0.17",
|
||||||
"@ai-sdk/amazon-bedrock": "4.0.93",
|
"@ai-sdk/amazon-bedrock": "4.0.95",
|
||||||
"@ai-sdk/anthropic": "3.0.67",
|
"@ai-sdk/anthropic": "3.0.71",
|
||||||
"@ai-sdk/azure": "3.0.49",
|
"@ai-sdk/azure": "3.0.49",
|
||||||
"@ai-sdk/cerebras": "2.0.41",
|
"@ai-sdk/cerebras": "2.0.41",
|
||||||
"@ai-sdk/cohere": "3.0.27",
|
"@ai-sdk/cohere": "3.0.27",
|
||||||
"@ai-sdk/deepinfra": "2.0.41",
|
"@ai-sdk/deepinfra": "2.0.41",
|
||||||
"@ai-sdk/gateway": "3.0.97",
|
"@ai-sdk/gateway": "3.0.104",
|
||||||
"@ai-sdk/google": "3.0.63",
|
"@ai-sdk/google": "3.0.63",
|
||||||
"@ai-sdk/google-vertex": "4.0.109",
|
"@ai-sdk/google-vertex": "4.0.112",
|
||||||
"@ai-sdk/groq": "3.0.31",
|
"@ai-sdk/groq": "3.0.31",
|
||||||
"@ai-sdk/mistral": "3.0.27",
|
"@ai-sdk/mistral": "3.0.27",
|
||||||
"@ai-sdk/openai": "3.0.53",
|
"@ai-sdk/openai": "3.0.53",
|
||||||
@@ -121,8 +122,8 @@
|
|||||||
"@opentelemetry/exporter-trace-otlp-http": "0.214.0",
|
"@opentelemetry/exporter-trace-otlp-http": "0.214.0",
|
||||||
"@opentelemetry/sdk-trace-base": "2.6.1",
|
"@opentelemetry/sdk-trace-base": "2.6.1",
|
||||||
"@opentelemetry/sdk-trace-node": "2.6.1",
|
"@opentelemetry/sdk-trace-node": "2.6.1",
|
||||||
"@opentui/core": "0.1.99",
|
"@opentui/core": "catalog:",
|
||||||
"@opentui/solid": "0.1.99",
|
"@opentui/solid": "catalog:",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
"@pierre/diffs": "catalog:",
|
"@pierre/diffs": "catalog:",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
@@ -142,7 +143,7 @@
|
|||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
"gitlab-ai-provider": "6.4.2",
|
"gitlab-ai-provider": "6.6.0",
|
||||||
"glob": "13.0.5",
|
"glob": "13.0.5",
|
||||||
"google-auth-library": "10.5.0",
|
"google-auth-library": "10.5.0",
|
||||||
"gray-matter": "4.0.3",
|
"gray-matter": "4.0.3",
|
||||||
|
|||||||
@@ -68,23 +68,6 @@ function findBinary() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepareBinDirectory(binaryName) {
|
|
||||||
const binDir = path.join(__dirname, "bin")
|
|
||||||
const targetPath = path.join(binDir, binaryName)
|
|
||||||
|
|
||||||
// Ensure bin directory exists
|
|
||||||
if (!fs.existsSync(binDir)) {
|
|
||||||
fs.mkdirSync(binDir, { recursive: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove existing binary/symlink if it exists
|
|
||||||
if (fs.existsSync(targetPath)) {
|
|
||||||
fs.unlinkSync(targetPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
return { binDir, targetPath }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
try {
|
try {
|
||||||
if (os.platform() === "win32") {
|
if (os.platform() === "win32") {
|
||||||
@@ -112,7 +95,7 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
main()
|
void main()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Postinstall script error:", error.message)
|
console.error("Postinstall script error:", error.message)
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { Config } from "../src/config"
|
import { Config } from "../src/config"
|
||||||
import { TuiConfig } from "../src/config/tui"
|
import { TuiConfig } from "../src/cli/cmd/tui/config/tui"
|
||||||
|
|
||||||
function generate(schema: z.ZodType) {
|
function generate(schema: z.ZodType) {
|
||||||
const result = z.toJSONSchema(schema, {
|
const result = z.toJSONSchema(schema, {
|
||||||
@@ -33,7 +33,7 @@ function generate(schema: z.ZodType) {
|
|||||||
schema.examples = [schema.default]
|
schema.examples = [schema.default]
|
||||||
}
|
}
|
||||||
|
|
||||||
schema.description = [schema.description || "", `default: \`${schema.default}\``]
|
schema.description = [schema.description || "", `default: \`${String(schema.default)}\``]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join("\n\n")
|
.join("\n\n")
|
||||||
.trim()
|
.trim()
|
||||||
|
|||||||
Executable
+6
@@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
import path from "path"
|
||||||
|
const toDynamicallyImport = path.join(process.cwd(), process.argv[2])
|
||||||
|
await import(toDynamicallyImport)
|
||||||
|
console.log(performance.now())
|
||||||
Executable
+153
@@ -0,0 +1,153 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
import * as path from "path"
|
||||||
|
import * as ts from "typescript"
|
||||||
|
|
||||||
|
const BASE_DIR = "/home/thdxr/dev/projects/anomalyco/opencode/packages/opencode"
|
||||||
|
|
||||||
|
// Get entry file from command line arg or use default
|
||||||
|
const ENTRY_FILE = process.argv[2] || "src/cli/cmd/tui/plugin/index.ts"
|
||||||
|
|
||||||
|
const visited = new Set<string>()
|
||||||
|
|
||||||
|
function resolveImport(importPath: string, fromFile: string): string | null {
|
||||||
|
if (importPath.startsWith("@/")) {
|
||||||
|
return path.join(BASE_DIR, "src", importPath.slice(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (importPath.startsWith("./") || importPath.startsWith("../")) {
|
||||||
|
const dir = path.dirname(fromFile)
|
||||||
|
return path.resolve(dir, importPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInternalImport(importPath: string): boolean {
|
||||||
|
return importPath.startsWith("@/") || importPath.startsWith("./") || importPath.startsWith("../")
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryExtensions(filePath: string): Promise<string | null> {
|
||||||
|
const extensions = [".ts", ".tsx", ".js", ".jsx"]
|
||||||
|
|
||||||
|
try {
|
||||||
|
const file = Bun.file(filePath)
|
||||||
|
const stat = await file.stat()
|
||||||
|
|
||||||
|
if (stat?.isDirectory()) {
|
||||||
|
for (const ext of extensions) {
|
||||||
|
const indexPath = path.join(filePath, "index" + ext)
|
||||||
|
const indexFile = Bun.file(indexPath)
|
||||||
|
if (await indexFile.exists()) return indexPath
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// It's a file
|
||||||
|
return filePath
|
||||||
|
} catch {
|
||||||
|
// Path doesn't exist, try adding extensions
|
||||||
|
for (const ext of extensions) {
|
||||||
|
const withExt = filePath + ext
|
||||||
|
const extFile = Bun.file(withExt)
|
||||||
|
if (await extFile.exists()) return withExt
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractImports(sourceFile: ts.SourceFile): string[] {
|
||||||
|
const imports: string[] = []
|
||||||
|
|
||||||
|
function visit(node: ts.Node) {
|
||||||
|
// import x from "path" or import { x } from "path"
|
||||||
|
if (ts.isImportDeclaration(node)) {
|
||||||
|
// Skip type-only imports
|
||||||
|
if (node.importClause?.isTypeOnly) return
|
||||||
|
|
||||||
|
const moduleSpec = node.moduleSpecifier
|
||||||
|
if (ts.isStringLiteral(moduleSpec)) {
|
||||||
|
imports.push(moduleSpec.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// export { x } from "path"
|
||||||
|
if (ts.isExportDeclaration(node) && node.moduleSpecifier) {
|
||||||
|
if (ts.isStringLiteral(node.moduleSpecifier)) {
|
||||||
|
imports.push(node.moduleSpecifier.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic import: import("path")
|
||||||
|
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
||||||
|
const arg = node.arguments[0]
|
||||||
|
if (arg && ts.isStringLiteral(arg)) {
|
||||||
|
imports.push(arg.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ts.forEachChild(node, visit)
|
||||||
|
}
|
||||||
|
|
||||||
|
visit(sourceFile)
|
||||||
|
return imports
|
||||||
|
}
|
||||||
|
|
||||||
|
async function traceFile(filePath: string, depth = 0): Promise<void> {
|
||||||
|
const normalizedPath = path.relative(BASE_DIR, filePath)
|
||||||
|
|
||||||
|
if (visited.has(filePath)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only trace TypeScript/JavaScript files
|
||||||
|
if (!filePath.match(/\.(ts|tsx|js|jsx)$/)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
visited.add(filePath)
|
||||||
|
console.log("\t".repeat(depth) + normalizedPath)
|
||||||
|
|
||||||
|
let content: string
|
||||||
|
try {
|
||||||
|
content = await Bun.file(filePath).text()
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true)
|
||||||
|
|
||||||
|
const imports = extractImports(sourceFile)
|
||||||
|
const internalImports = imports.filter(isInternalImport)
|
||||||
|
const externalImports = imports.filter((imp) => !isInternalImport(imp))
|
||||||
|
|
||||||
|
// Print external imports
|
||||||
|
for (const imp of externalImports) {
|
||||||
|
console.log("\t".repeat(depth + 1) + `[ext] ${imp}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const imp of internalImports) {
|
||||||
|
const resolved = resolveImport(imp, filePath)
|
||||||
|
if (!resolved) continue
|
||||||
|
|
||||||
|
const actualPath = await tryExtensions(resolved)
|
||||||
|
if (!actualPath) continue
|
||||||
|
|
||||||
|
await traceFile(actualPath, depth + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const entryPath = path.join(BASE_DIR, ENTRY_FILE)
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
const file = Bun.file(entryPath)
|
||||||
|
if (!(await file.exists())) {
|
||||||
|
console.error(`File not found: ${ENTRY_FILE}`)
|
||||||
|
console.error(`Resolved to: ${entryPath}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
await traceFile(entryPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error)
|
||||||
@@ -1,305 +0,0 @@
|
|||||||
#!/usr/bin/env bun
|
|
||||||
/**
|
|
||||||
* Unwrap a TypeScript `export namespace` into flat exports + barrel.
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* bun script/unwrap-namespace.ts src/bus/index.ts
|
|
||||||
* bun script/unwrap-namespace.ts src/bus/index.ts --dry-run
|
|
||||||
* bun script/unwrap-namespace.ts src/pty/index.ts --name service # avoid collision with pty.ts
|
|
||||||
*
|
|
||||||
* What it does:
|
|
||||||
* 1. Reads the file and finds the `export namespace Foo { ... }` block
|
|
||||||
* (uses ast-grep for accurate AST-based boundary detection)
|
|
||||||
* 2. Removes the namespace wrapper and dedents the body
|
|
||||||
* 3. Fixes self-references (e.g. Config.PermissionAction → PermissionAction)
|
|
||||||
* 4. If the file is index.ts, renames it to <lowercase-name>.ts
|
|
||||||
* 5. Creates/updates index.ts with `export * as Foo from "./<file>"`
|
|
||||||
* 6. Rewrites import paths across src/, test/, and script/
|
|
||||||
* 7. Fixes sibling imports within the same directory
|
|
||||||
*
|
|
||||||
* Requires: ast-grep (`brew install ast-grep` or `cargo install ast-grep`)
|
|
||||||
*/
|
|
||||||
|
|
||||||
import path from "path"
|
|
||||||
import fs from "fs"
|
|
||||||
|
|
||||||
const args = process.argv.slice(2)
|
|
||||||
const dryRun = args.includes("--dry-run")
|
|
||||||
const nameFlag = args.find((a, i) => args[i - 1] === "--name")
|
|
||||||
const filePath = args.find((a) => !a.startsWith("--") && args[args.indexOf(a) - 1] !== "--name")
|
|
||||||
|
|
||||||
if (!filePath) {
|
|
||||||
console.error("Usage: bun script/unwrap-namespace.ts <file> [--dry-run] [--name <impl-name>]")
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const absPath = path.resolve(filePath)
|
|
||||||
if (!fs.existsSync(absPath)) {
|
|
||||||
console.error(`File not found: ${absPath}`)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const src = fs.readFileSync(absPath, "utf-8")
|
|
||||||
const lines = src.split("\n")
|
|
||||||
|
|
||||||
// Use ast-grep to find the namespace boundaries accurately.
|
|
||||||
// This avoids false matches from braces in strings, templates, comments, etc.
|
|
||||||
const astResult = Bun.spawnSync(
|
|
||||||
["ast-grep", "run", "--pattern", "export namespace $NAME { $$$BODY }", "--lang", "typescript", "--json", absPath],
|
|
||||||
{ stdout: "pipe", stderr: "pipe" },
|
|
||||||
)
|
|
||||||
|
|
||||||
if (astResult.exitCode !== 0) {
|
|
||||||
console.error("ast-grep failed:", astResult.stderr.toString())
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const matches = JSON.parse(astResult.stdout.toString()) as Array<{
|
|
||||||
text: string
|
|
||||||
range: { start: { line: number; column: number }; end: { line: number; column: number } }
|
|
||||||
metaVariables: { single: Record<string, { text: string }>; multi: Record<string, Array<{ text: string }>> }
|
|
||||||
}>
|
|
||||||
|
|
||||||
if (matches.length === 0) {
|
|
||||||
console.error("No `export namespace Foo { ... }` found in file")
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matches.length > 1) {
|
|
||||||
console.error(`Found ${matches.length} namespaces — this script handles one at a time`)
|
|
||||||
console.error("Namespaces found:")
|
|
||||||
for (const m of matches) console.error(` ${m.metaVariables.single.NAME.text} (line ${m.range.start.line + 1})`)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const match = matches[0]
|
|
||||||
const nsName = match.metaVariables.single.NAME.text
|
|
||||||
const nsLine = match.range.start.line // 0-indexed
|
|
||||||
const closeLine = match.range.end.line // 0-indexed, the line with closing `}`
|
|
||||||
|
|
||||||
console.log(`Found: export namespace ${nsName} { ... }`)
|
|
||||||
console.log(` Lines ${nsLine + 1}–${closeLine + 1} (${closeLine - nsLine + 1} lines)`)
|
|
||||||
|
|
||||||
// Build the new file content:
|
|
||||||
// 1. Everything before the namespace declaration (imports, etc.)
|
|
||||||
// 2. The namespace body, dedented by one level (2 spaces)
|
|
||||||
// 3. Everything after the closing brace (rare, but possible)
|
|
||||||
const before = lines.slice(0, nsLine)
|
|
||||||
const body = lines.slice(nsLine + 1, closeLine)
|
|
||||||
const after = lines.slice(closeLine + 1)
|
|
||||||
|
|
||||||
// Dedent: remove exactly 2 leading spaces from each line
|
|
||||||
const dedented = body.map((line) => {
|
|
||||||
if (line === "") return ""
|
|
||||||
if (line.startsWith(" ")) return line.slice(2)
|
|
||||||
return line
|
|
||||||
})
|
|
||||||
|
|
||||||
let newContent = [...before, ...dedented, ...after].join("\n")
|
|
||||||
|
|
||||||
// --- Fix self-references ---
|
|
||||||
// After unwrapping, references like `Config.PermissionAction` inside the same file
|
|
||||||
// need to become just `PermissionAction`. Only fix code positions, not strings.
|
|
||||||
const exportedNames = new Set<string>()
|
|
||||||
const exportRegex = /export\s+(?:const|function|class|interface|type|enum|abstract\s+class)\s+(\w+)/g
|
|
||||||
for (const line of dedented) {
|
|
||||||
for (const m of line.matchAll(exportRegex)) exportedNames.add(m[1])
|
|
||||||
}
|
|
||||||
const reExportRegex = /export\s*\{\s*([^}]+)\}/g
|
|
||||||
for (const line of dedented) {
|
|
||||||
for (const m of line.matchAll(reExportRegex)) {
|
|
||||||
for (const name of m[1].split(",")) {
|
|
||||||
const trimmed = name
|
|
||||||
.trim()
|
|
||||||
.split(/\s+as\s+/)
|
|
||||||
.pop()!
|
|
||||||
.trim()
|
|
||||||
if (trimmed) exportedNames.add(trimmed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let selfRefCount = 0
|
|
||||||
if (exportedNames.size > 0) {
|
|
||||||
const fixedLines = newContent.split("\n").map((line) => {
|
|
||||||
// Split line into string-literal and code segments to avoid replacing inside strings
|
|
||||||
const segments: Array<{ text: string; isString: boolean }> = []
|
|
||||||
let i = 0
|
|
||||||
let current = ""
|
|
||||||
let inString: string | null = null
|
|
||||||
|
|
||||||
while (i < line.length) {
|
|
||||||
const ch = line[i]
|
|
||||||
if (inString) {
|
|
||||||
current += ch
|
|
||||||
if (ch === "\\" && i + 1 < line.length) {
|
|
||||||
current += line[i + 1]
|
|
||||||
i += 2
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (ch === inString) {
|
|
||||||
segments.push({ text: current, isString: true })
|
|
||||||
current = ""
|
|
||||||
inString = null
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (ch === '"' || ch === "'" || ch === "`") {
|
|
||||||
if (current) segments.push({ text: current, isString: false })
|
|
||||||
current = ch
|
|
||||||
inString = ch
|
|
||||||
i++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (ch === "/" && i + 1 < line.length && line[i + 1] === "/") {
|
|
||||||
current += line.slice(i)
|
|
||||||
segments.push({ text: current, isString: true })
|
|
||||||
current = ""
|
|
||||||
i = line.length
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
current += ch
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
if (current) segments.push({ text: current, isString: !!inString })
|
|
||||||
|
|
||||||
return segments
|
|
||||||
.map((seg) => {
|
|
||||||
if (seg.isString) return seg.text
|
|
||||||
let result = seg.text
|
|
||||||
for (const name of exportedNames) {
|
|
||||||
const pattern = `${nsName}.${name}`
|
|
||||||
while (result.includes(pattern)) {
|
|
||||||
const idx = result.indexOf(pattern)
|
|
||||||
const charBefore = idx > 0 ? result[idx - 1] : " "
|
|
||||||
const charAfter = idx + pattern.length < result.length ? result[idx + pattern.length] : " "
|
|
||||||
if (/\w/.test(charBefore) || /\w/.test(charAfter)) break
|
|
||||||
result = result.slice(0, idx) + name + result.slice(idx + pattern.length)
|
|
||||||
selfRefCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
.join("")
|
|
||||||
})
|
|
||||||
newContent = fixedLines.join("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Figure out file naming
|
|
||||||
const dir = path.dirname(absPath)
|
|
||||||
const basename = path.basename(absPath, ".ts")
|
|
||||||
const isIndex = basename === "index"
|
|
||||||
const implName = nameFlag ?? (isIndex ? nsName.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase() : basename)
|
|
||||||
const implFile = path.join(dir, `${implName}.ts`)
|
|
||||||
const indexFile = path.join(dir, "index.ts")
|
|
||||||
const barrelLine = `export * as ${nsName} from "./${implName}"\n`
|
|
||||||
|
|
||||||
console.log("")
|
|
||||||
if (isIndex) {
|
|
||||||
console.log(`Plan: rename ${basename}.ts → ${implName}.ts, create new index.ts barrel`)
|
|
||||||
} else {
|
|
||||||
console.log(`Plan: rewrite ${basename}.ts in place, create index.ts barrel`)
|
|
||||||
}
|
|
||||||
if (selfRefCount > 0) console.log(`Fixed ${selfRefCount} self-reference(s) (${nsName}.X → X)`)
|
|
||||||
console.log("")
|
|
||||||
|
|
||||||
if (dryRun) {
|
|
||||||
console.log("--- DRY RUN ---")
|
|
||||||
console.log("")
|
|
||||||
console.log(`=== ${implName}.ts (first 30 lines) ===`)
|
|
||||||
newContent
|
|
||||||
.split("\n")
|
|
||||||
.slice(0, 30)
|
|
||||||
.forEach((l, i) => console.log(` ${i + 1}: ${l}`))
|
|
||||||
console.log(" ...")
|
|
||||||
console.log("")
|
|
||||||
console.log(`=== index.ts ===`)
|
|
||||||
console.log(` ${barrelLine.trim()}`)
|
|
||||||
console.log("")
|
|
||||||
if (!isIndex) {
|
|
||||||
const relDir = path.relative(path.resolve("src"), dir)
|
|
||||||
console.log(`=== Import rewrites (would apply) ===`)
|
|
||||||
console.log(` ${relDir}/${basename}" → ${relDir}" across src/, test/, script/`)
|
|
||||||
} else {
|
|
||||||
console.log("No import rewrites needed (was index.ts)")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (isIndex) {
|
|
||||||
fs.writeFileSync(implFile, newContent)
|
|
||||||
fs.writeFileSync(indexFile, barrelLine)
|
|
||||||
console.log(`Wrote ${implName}.ts (${newContent.split("\n").length} lines)`)
|
|
||||||
console.log(`Wrote index.ts (barrel)`)
|
|
||||||
} else {
|
|
||||||
fs.writeFileSync(absPath, newContent)
|
|
||||||
if (fs.existsSync(indexFile)) {
|
|
||||||
const existing = fs.readFileSync(indexFile, "utf-8")
|
|
||||||
if (!existing.includes(`export * as ${nsName}`)) {
|
|
||||||
fs.appendFileSync(indexFile, barrelLine)
|
|
||||||
console.log(`Appended to existing index.ts`)
|
|
||||||
} else {
|
|
||||||
console.log(`index.ts already has ${nsName} export`)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fs.writeFileSync(indexFile, barrelLine)
|
|
||||||
console.log(`Wrote index.ts (barrel)`)
|
|
||||||
}
|
|
||||||
console.log(`Rewrote ${basename}.ts (${newContent.split("\n").length} lines)`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Rewrite import paths across src/, test/, script/ ---
|
|
||||||
const relDir = path.relative(path.resolve("src"), dir)
|
|
||||||
if (!isIndex) {
|
|
||||||
const oldTail = `${relDir}/${basename}`
|
|
||||||
const searchDirs = ["src", "test", "script"].filter((d) => fs.existsSync(d))
|
|
||||||
const rgResult = Bun.spawnSync(["rg", "-l", `from.*${oldTail}"`, ...searchDirs], {
|
|
||||||
stdout: "pipe",
|
|
||||||
stderr: "pipe",
|
|
||||||
})
|
|
||||||
const filesToRewrite = rgResult.stdout
|
|
||||||
.toString()
|
|
||||||
.trim()
|
|
||||||
.split("\n")
|
|
||||||
.filter((f) => f.length > 0)
|
|
||||||
|
|
||||||
if (filesToRewrite.length > 0) {
|
|
||||||
console.log(`\nRewriting imports in ${filesToRewrite.length} file(s)...`)
|
|
||||||
for (const file of filesToRewrite) {
|
|
||||||
const content = fs.readFileSync(file, "utf-8")
|
|
||||||
fs.writeFileSync(file, content.replaceAll(`${oldTail}"`, `${relDir}"`))
|
|
||||||
}
|
|
||||||
console.log(` Done: ${oldTail}" → ${relDir}"`)
|
|
||||||
} else {
|
|
||||||
console.log("\nNo import rewrites needed")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log("\nNo import rewrites needed (was index.ts)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Fix sibling imports within the same directory ---
|
|
||||||
const siblingFiles = fs.readdirSync(dir).filter((f) => {
|
|
||||||
if (!f.endsWith(".ts")) return false
|
|
||||||
if (f === "index.ts" || f === `${implName}.ts`) return false
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
let siblingFixCount = 0
|
|
||||||
for (const sibFile of siblingFiles) {
|
|
||||||
const sibPath = path.join(dir, sibFile)
|
|
||||||
const content = fs.readFileSync(sibPath, "utf-8")
|
|
||||||
const pattern = new RegExp(`from\\s+["']\\./${basename}["']`, "g")
|
|
||||||
if (pattern.test(content)) {
|
|
||||||
fs.writeFileSync(sibPath, content.replace(pattern, `from "."`))
|
|
||||||
siblingFixCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (siblingFixCount > 0) {
|
|
||||||
console.log(`Fixed ${siblingFixCount} sibling import(s) in ${path.basename(dir)}/ (./${basename} → .)`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("")
|
|
||||||
console.log("=== Verify ===")
|
|
||||||
console.log("")
|
|
||||||
console.log("bunx --bun tsgo --noEmit # typecheck")
|
|
||||||
console.log("bun run test # run tests")
|
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
# Facade removal checklist
|
# Facade removal checklist
|
||||||
|
|
||||||
Concrete inventory of the remaining `makeRuntime(...)`-backed service facades in `packages/opencode`.
|
Concrete inventory of the remaining `makeRuntime(...)`-backed facades in `packages/opencode`.
|
||||||
|
|
||||||
As of 2026-04-13, latest `origin/dev`:
|
Current status on this branch:
|
||||||
|
|
||||||
- `src/` still has 15 `makeRuntime(...)` call sites.
|
- `src/` has 5 `makeRuntime(...)` call sites total.
|
||||||
- 13 of those are still in scope for facade removal.
|
- 2 are intentionally excluded from this checklist: `src/bus/index.ts` and `src/effect/cross-spawn-spawner.ts`.
|
||||||
- 2 are excluded from this checklist: `bus/index.ts` and `effect/cross-spawn-spawner.ts`.
|
- 1 is tracked primarily by the instance-context migration rather than facade removal: `src/project/instance.ts`.
|
||||||
|
- That leaves 2 live runtime-backed service facades still worth tracking here: `src/npm/index.ts` and `src/cli/cmd/tui/config/tui.ts`.
|
||||||
|
|
||||||
Recent progress:
|
Recent progress:
|
||||||
|
|
||||||
@@ -15,8 +16,9 @@ Recent progress:
|
|||||||
|
|
||||||
## Priority hotspots
|
## Priority hotspots
|
||||||
|
|
||||||
- `server/instance/session.ts` still depends on `Session`, `SessionPrompt`, `SessionRevert`, `SessionCompaction`, `SessionSummary`, `ShareSession`, `Agent`, and `Permission` facades.
|
- `src/cli/cmd/tui/config/tui.ts` still exports `makeRuntime(...)` plus async facade helpers for `get()` and `waitForDependencies()`.
|
||||||
- `src/effect/app-runtime.ts` still references many facade namespaces directly, so it should stay in view during each deletion.
|
- `src/npm/index.ts` still exports `makeRuntime(...)` plus async facade helpers for `install()`, `add()`, `outdated()`, and `which()`.
|
||||||
|
- `src/project/instance.ts` still uses a dedicated runtime for project boot, but that file is really part of the broader legacy instance-context transition tracked in `instance-context.md`.
|
||||||
|
|
||||||
## Completed Batches
|
## Completed Batches
|
||||||
|
|
||||||
@@ -184,53 +186,34 @@ These were the recurring mistakes and useful corrections from the first two batc
|
|||||||
5. For CLI readability, extract file-local preload helpers when the handler starts doing config load + service load + batched effect fanout inline.
|
5. For CLI readability, extract file-local preload helpers when the handler starts doing config load + service load + batched effect fanout inline.
|
||||||
6. When rebasing a facade branch after nearby merges, prefer the already-cleaned service/test version over older inline facade-era code.
|
6. When rebasing a facade branch after nearby merges, prefer the already-cleaned service/test version over older inline facade-era code.
|
||||||
|
|
||||||
## Next batch
|
## Remaining work
|
||||||
|
|
||||||
Recommended next five, in order:
|
Most of the original facade-removal backlog is already done. The practical remaining work is narrower now:
|
||||||
|
|
||||||
1. `src/permission/index.ts`
|
1. remove the `Npm` runtime-backed facade from `src/npm/index.ts`
|
||||||
2. `src/agent/agent.ts`
|
2. remove the `TuiConfig` runtime-backed facade from `src/cli/cmd/tui/config/tui.ts`
|
||||||
3. `src/session/summary.ts`
|
3. keep `src/project/instance.ts` in the separate instance-context migration, not this checklist
|
||||||
4. `src/session/revert.ts`
|
|
||||||
5. `src/mcp/auth.ts`
|
|
||||||
|
|
||||||
Why this batch:
|
|
||||||
|
|
||||||
- It keeps pushing the session-adjacent cleanup without jumping straight into `session/index.ts` or `session/prompt.ts`.
|
|
||||||
- `Permission`, `Agent`, `SessionSummary`, and `SessionRevert` all reduce fanout in `server/instance/session.ts`.
|
|
||||||
- `McpAuth` is small and closely related to the just-landed `MCP` cleanup.
|
|
||||||
|
|
||||||
After that batch, the expected follow-up is the main session cluster:
|
|
||||||
|
|
||||||
1. `src/session/index.ts`
|
|
||||||
2. `src/session/prompt.ts`
|
|
||||||
3. `src/session/compaction.ts`
|
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
- [ ] `src/session/index.ts` (`Session`) - facades: `create`, `fork`, `get`, `setTitle`, `setArchived`, `setPermission`, `setRevert`, `messages`, `children`, `remove`, `updateMessage`, `removeMessage`, `removePart`, `updatePart`; main callers: `server/instance/session.ts`, `cli/cmd/session.ts`, `cli/cmd/export.ts`, `cli/cmd/github.ts`; tests: `test/server/session-actions.test.ts`, `test/server/session-list.test.ts`, `test/server/global-session-list.test.ts`
|
- [ ] `src/npm/index.ts` (`Npm`) - still exports runtime-backed async facade helpers on top of `Npm.Service`
|
||||||
- [ ] `src/session/prompt.ts` (`SessionPrompt`) - facades: `prompt`, `resolvePromptParts`, `cancel`, `loop`, `shell`, `command`; main callers: `server/instance/session.ts`, `cli/cmd/github.ts`; tests: `test/session/prompt.test.ts`, `test/session/prompt-effect.test.ts`, `test/session/structured-output-integration.test.ts`
|
- [ ] `src/cli/cmd/tui/config/tui.ts` (`TuiConfig`) - still exports runtime-backed async facade helpers on top of `TuiConfig.Service`
|
||||||
- [ ] `src/session/revert.ts` (`SessionRevert`) - facades: `revert`, `unrevert`, `cleanup`; main callers: `server/instance/session.ts`; tests: `test/session/revert-compact.test.ts`
|
- [x] `src/session/session.ts` / `src/session/prompt.ts` / `src/session/revert.ts` / `src/session/summary.ts` - service-local facades removed
|
||||||
- [ ] `src/session/compaction.ts` (`SessionCompaction`) - facades: `isOverflow`, `prune`, `create`; main callers: `server/instance/session.ts`; tests: `test/session/compaction.test.ts`
|
- [x] `src/agent/agent.ts` (`Agent`) - service-local facades removed
|
||||||
- [ ] `src/session/summary.ts` (`SessionSummary`) - facades: `summarize`, `diff`; main callers: `session/prompt.ts`, `session/processor.ts`, `server/instance/session.ts`; tests: `test/session/snapshot-tool-race.test.ts`
|
- [x] `src/permission/index.ts` (`Permission`) - service-local facades removed
|
||||||
- [ ] `src/share/session.ts` (`ShareSession`) - facades: `create`, `share`, `unshare`; main callers: `server/instance/session.ts`, `cli/cmd/github.ts`
|
- [x] `src/worktree/index.ts` (`Worktree`) - service-local facades removed
|
||||||
- [ ] `src/agent/agent.ts` (`Agent`) - facades: `get`, `list`, `defaultAgent`, `generate`; main callers: `cli/cmd/agent.ts`, `server/instance/session.ts`, `server/instance/experimental.ts`; tests: `test/agent/agent.test.ts`
|
- [x] `src/plugin/index.ts` (`Plugin`) - service-local facades removed
|
||||||
- [ ] `src/permission/index.ts` (`Permission`) - facades: `ask`, `reply`, `list`; main callers: `server/instance/permission.ts`, `server/instance/session.ts`, `session/llm.ts`; tests: `test/permission/next.test.ts`
|
- [x] `src/snapshot/index.ts` (`Snapshot`) - service-local facades removed
|
||||||
- [x] `src/file/index.ts` (`File`) - facades removed and merged.
|
- [x] `src/file/index.ts` (`File`) - facades removed and merged
|
||||||
- [x] `src/lsp/index.ts` (`LSP`) - facades removed and merged.
|
- [x] `src/lsp/index.ts` (`LSP`) - facades removed and merged
|
||||||
- [x] `src/mcp/index.ts` (`MCP`) - facades removed and merged.
|
- [x] `src/mcp/index.ts` (`MCP`) - facades removed and merged
|
||||||
- [x] `src/config/config.ts` (`Config`) - facades removed and merged.
|
- [x] `src/config/config.ts` (`Config`) - facades removed and merged
|
||||||
- [x] `src/provider/provider.ts` (`Provider`) - facades removed and merged.
|
- [x] `src/provider/provider.ts` (`Provider`) - facades removed and merged
|
||||||
- [x] `src/pty/index.ts` (`Pty`) - facades removed and merged.
|
- [x] `src/pty/index.ts` (`Pty`) - facades removed and merged
|
||||||
- [x] `src/skill/index.ts` (`Skill`) - facades removed and merged.
|
- [x] `src/skill/index.ts` (`Skill`) - facades removed and merged
|
||||||
- [x] `src/project/vcs.ts` (`Vcs`) - facades removed and merged.
|
- [x] `src/project/vcs.ts` (`Vcs`) - facades removed and merged
|
||||||
- [x] `src/tool/registry.ts` (`ToolRegistry`) - facades removed and merged.
|
- [x] `src/tool/registry.ts` (`ToolRegistry`) - facades removed and merged
|
||||||
- [ ] `src/worktree/index.ts` (`Worktree`) - facades: `makeWorktreeInfo`, `createFromInfo`, `create`, `remove`, `reset`; main callers: `control-plane/adaptors/worktree.ts`, `server/instance/experimental.ts`; tests: `test/project/worktree.test.ts`, `test/project/worktree-remove.test.ts`
|
- [x] `src/auth/index.ts` (`Auth`) - facades removed and merged
|
||||||
- [x] `src/auth/index.ts` (`Auth`) - facades removed and merged.
|
|
||||||
- [ ] `src/mcp/auth.ts` (`McpAuth`) - facades: `get`, `getForUrl`, `all`, `set`, `remove`, `updateTokens`, `updateClientInfo`, `updateCodeVerifier`, `updateOAuthState`; main callers: `mcp/oauth-provider.ts`, `cli/cmd/mcp.ts`; tests: `test/mcp/oauth-auto-connect.test.ts`
|
|
||||||
- [ ] `src/plugin/index.ts` (`Plugin`) - facades: `trigger`, `list`, `init`; main callers: `agent/agent.ts`, `session/llm.ts`, `project/bootstrap.ts`; tests: `test/plugin/trigger.test.ts`, `test/provider/provider.test.ts`
|
|
||||||
- [ ] `src/project/project.ts` (`Project`) - facades: `fromDirectory`, `discover`, `initGit`, `update`, `sandboxes`, `addSandbox`, `removeSandbox`; main callers: `project/instance.ts`, `server/instance/project.ts`, `server/instance/experimental.ts`; tests: `test/project/project.test.ts`, `test/project/migrate-global.test.ts`
|
|
||||||
- [ ] `src/snapshot/index.ts` (`Snapshot`) - facades: `init`, `track`, `patch`, `restore`, `revert`, `diff`, `diffFull`; main callers: `project/bootstrap.ts`, `cli/cmd/debug/snapshot.ts`; tests: `test/snapshot/snapshot.test.ts`, `test/session/revert-compact.test.ts`
|
|
||||||
|
|
||||||
## Excluded `makeRuntime(...)` sites
|
## Excluded `makeRuntime(...)` sites
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ Many route boundaries still use Zod-first validators. That does not block all ex
|
|||||||
|
|
||||||
### Mixed handler styles
|
### Mixed handler styles
|
||||||
|
|
||||||
Many current `server/instance/*.ts` handlers still call async facades directly. Migrating those to composed `Effect.gen(...)` handlers is the low-risk step to do first.
|
Many current `server/routes/instance/*.ts` handlers still mix composed Effect code with smaller Promise- or ALS-backed seams. Migrating those to consistent `Effect.gen(...)` handlers is the low-risk step to do first.
|
||||||
|
|
||||||
### Non-JSON routes
|
### Non-JSON routes
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ The current server composition, middleware, and docs flow are Hono-centered toda
|
|||||||
|
|
||||||
### 1. Finish the prerequisites first
|
### 1. Finish the prerequisites first
|
||||||
|
|
||||||
- continue route-handler effectification in `server/instance/*.ts`
|
- continue route-handler effectification in `server/routes/instance/*.ts`
|
||||||
- continue schema migration toward Effect Schema-first DTOs and errors
|
- continue schema migration toward Effect Schema-first DTOs and errors
|
||||||
- keep removing service facades
|
- keep removing service facades
|
||||||
|
|
||||||
@@ -98,9 +98,9 @@ The current server composition, middleware, and docs flow are Hono-centered toda
|
|||||||
|
|
||||||
Introduce one small `HttpApi` group for plain JSON endpoints only. Good initial candidates are the least stateful endpoints in:
|
Introduce one small `HttpApi` group for plain JSON endpoints only. Good initial candidates are the least stateful endpoints in:
|
||||||
|
|
||||||
- `server/instance/question.ts`
|
- `server/routes/instance/question.ts`
|
||||||
- `server/instance/provider.ts`
|
- `server/routes/instance/provider.ts`
|
||||||
- `server/instance/permission.ts`
|
- `server/routes/instance/permission.ts`
|
||||||
|
|
||||||
Avoid `session.ts`, SSE, websocket, and TUI-facing routes first.
|
Avoid `session.ts`, SSE, websocket, and TUI-facing routes first.
|
||||||
|
|
||||||
@@ -121,17 +121,46 @@ Why `question` first:
|
|||||||
|
|
||||||
Do not re-architect business logic during the HTTP migration. `HttpApi` handlers should call the same Effect services already used by the Hono handlers.
|
Do not re-architect business logic during the HTTP migration. `HttpApi` handlers should call the same Effect services already used by the Hono handlers.
|
||||||
|
|
||||||
### 4. Build in parallel, do not bridge into Hono
|
### 4. Bridge into Hono behind a feature flag
|
||||||
|
|
||||||
The `HttpApi` implementation lives under `src/server/instance/httpapi/` as a standalone Effect HTTP server. It is **not mounted into the Hono app**. There is no `toWebHandler` bridge, no Hono `Handler` export, and no `.route()` call wiring it into `experimental.ts`.
|
The `HttpApi` routes are bridged into the Hono server via `HttpRouter.toWebHandler` with a shared `memoMap`. This means:
|
||||||
|
|
||||||
The standalone server (`httpapi/server.ts`) can be started independently and proves the routes work. Tests exercise it via `HttpRouter.serve` with `NodeHttpServer.layerTest`.
|
- one process, one port — no separate server
|
||||||
|
- the Effect handler shares layer instances with `AppRuntime` (same `Question.Service`, etc.)
|
||||||
|
- Effect middleware handles auth and instance lookup independently from Hono middleware
|
||||||
|
- Hono's `.all()` catch-all intercepts matching paths before the Hono route handlers
|
||||||
|
|
||||||
The goal is to build enough route coverage in the Effect server that the Hono server can eventually be replaced entirely. Until then, the two implementations exist side by side but are completely separate processes.
|
The bridge is gated behind `OPENCODE_EXPERIMENTAL_HTTPAPI` (or `OPENCODE_EXPERIMENTAL`). When the flag is off (default), all requests go through the original Hono handlers unchanged.
|
||||||
|
|
||||||
### 5. Migrate JSON route groups gradually
|
```ts
|
||||||
|
// in instance/index.ts
|
||||||
|
if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) {
|
||||||
|
const handler = ExperimentalHttpApiServer.webHandler().handler
|
||||||
|
app.all("/question", (c) => handler(c.req.raw)).all("/question/*", (c) => handler(c.req.raw))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
If the parallel slice works well, migrate additional JSON route groups one at a time. Leave streaming-style endpoints on Hono until there is a clear reason to move them.
|
The Hono route handlers are always registered (after the bridge) so `hono-openapi` generates the OpenAPI spec entries that feed SDK codegen. When the flag is on, these handlers are dead code — the `.all()` bridge matches first.
|
||||||
|
|
||||||
|
### 5. Observability
|
||||||
|
|
||||||
|
The `webHandler` provides `Observability.layer` via `Layer.provideMerge`. Since the `memoMap` is shared with `AppRuntime`, the tracing provider is deduplicated — no extra initialization cost.
|
||||||
|
|
||||||
|
This gives:
|
||||||
|
|
||||||
|
- **spans**: `Effect.fn("QuestionHttpApi.list")` etc. appear in traces alongside service-layer spans
|
||||||
|
- **HTTP logs**: `HttpMiddleware.logger` emits structured `Effect.log` entries with `http.method`, `http.url`, `http.status` annotations, flowing to motel via `OtlpLogger`
|
||||||
|
|
||||||
|
### 6. Migrate JSON route groups gradually
|
||||||
|
|
||||||
|
As each route group is ported to `HttpApi`:
|
||||||
|
|
||||||
|
1. add `.get(...)` / `.post(...)` bridge entries to the flag block in `server/routes/instance/index.ts`
|
||||||
|
2. for partial ports (e.g. only `GET /provider/auth`), bridge only the specific path
|
||||||
|
3. keep the legacy Hono route registered behind it for OpenAPI / SDK generation until the spec pipeline changes
|
||||||
|
4. verify SDK output is unchanged
|
||||||
|
|
||||||
|
Leave streaming-style endpoints on Hono until there is a clear reason to move them.
|
||||||
|
|
||||||
## Schema rule for HttpApi work
|
## Schema rule for HttpApi work
|
||||||
|
|
||||||
@@ -160,10 +189,46 @@ Ordering for a route-group migration:
|
|||||||
|
|
||||||
SDK shape rule:
|
SDK shape rule:
|
||||||
|
|
||||||
- every schema migration must preserve the generated SDK output byte-for-byte
|
- every schema migration must preserve the generated SDK output byte-for-byte **unless the new ref is intentional** (see Schema.Class vs Schema.Struct below)
|
||||||
- `Schema.Class` emits a named `$ref` in OpenAPI via its identifier — use it only for types that already had `.meta({ ref })` in the old Zod schema
|
- if an unintended diff appears in `packages/sdk/js/src/v2/gen/types.gen.ts`, the migration introduced an unintended API surface change — fix it before merging
|
||||||
- inner / nested types that were anonymous in the old Zod schema should stay as `Schema.Struct` (not `Schema.Class`) to avoid introducing new named components in the OpenAPI spec
|
|
||||||
- if a diff appears in `packages/sdk/js/src/v2/gen/types.gen.ts`, the migration introduced an unintended API surface change — fix it before merging
|
### Schema.Class vs Schema.Struct
|
||||||
|
|
||||||
|
The pattern choice determines whether a schema becomes a **named** export in the SDK or stays **anonymous inline**.
|
||||||
|
|
||||||
|
**Schema.Class** emits a named `$ref` in OpenAPI via its identifier → produces a named `export type Foo = ...` in `types.gen.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export class Info extends Schema.Class<Info>("FooConfig")({ ... }) {
|
||||||
|
static readonly zod = zod(this)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Schema.Struct** stays anonymous and is inlined everywhere it is referenced:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const Info = Schema.Struct({ ... }).pipe(
|
||||||
|
withStatics((s) => ({ zod: zod(s) })),
|
||||||
|
)
|
||||||
|
export type Info = Schema.Schema.Type<typeof Info>
|
||||||
|
```
|
||||||
|
|
||||||
|
When to use each:
|
||||||
|
|
||||||
|
- Use **Schema.Class** when:
|
||||||
|
- the original Zod had `.meta({ ref: ... })` (preserve the existing named SDK type byte-for-byte)
|
||||||
|
- the schema is a top-level endpoint request or response (SDK consumers benefit from a stable importable name)
|
||||||
|
- Use **Schema.Struct** when:
|
||||||
|
- the type is only used as a nested field inside another named schema
|
||||||
|
- the original Zod was anonymous and promoting it would bloat SDK types with no import value
|
||||||
|
|
||||||
|
Promoting a previously-anonymous schema to Schema.Class is acceptable when it is top-level or endpoint-facing, but call it out in the PR — it is an additive SDK change (`export type Foo = ...` newly appears) even if it preserves the JSON shape.
|
||||||
|
|
||||||
|
Schemas that are **not** pure objects (enums, unions, records, tuples) cannot use Schema.Class. For those, add `.annotate({ identifier: "FooName" })` to get the same named-ref behavior:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const Action = Schema.Literals(["ask", "allow", "deny"]).annotate({ identifier: "PermissionActionConfig" })
|
||||||
|
```
|
||||||
|
|
||||||
Temporary exception:
|
Temporary exception:
|
||||||
|
|
||||||
@@ -202,7 +267,7 @@ Use the same sequence for each route group.
|
|||||||
3. Apply the schema migration ordering above so those types are Effect Schema-first.
|
3. Apply the schema migration ordering above so those types are Effect Schema-first.
|
||||||
4. Define the `HttpApi` contract separately from the handlers.
|
4. Define the `HttpApi` contract separately from the handlers.
|
||||||
5. Implement handlers by yielding the existing service from context.
|
5. Implement handlers by yielding the existing service from context.
|
||||||
6. Mount the new surface in parallel under an experimental prefix.
|
6. Mount the new surface in parallel behind the `OPENCODE_EXPERIMENTAL_HTTPAPI` bridge.
|
||||||
7. Regenerate the SDK and verify zero diff against `dev` (see SDK shape rule above).
|
7. Regenerate the SDK and verify zero diff against `dev` (see SDK shape rule above).
|
||||||
8. Add one end-to-end test and one OpenAPI-focused test.
|
8. Add one end-to-end test and one OpenAPI-focused test.
|
||||||
9. Compare ergonomics before migrating the next endpoint.
|
9. Compare ergonomics before migrating the next endpoint.
|
||||||
@@ -221,20 +286,20 @@ Placement rule:
|
|||||||
- keep `HttpApi` code under `src/server`, not `src/effect`
|
- keep `HttpApi` code under `src/server`, not `src/effect`
|
||||||
- `src/effect` should stay focused on runtimes, layers, instance state, and shared Effect plumbing
|
- `src/effect` should stay focused on runtimes, layers, instance state, and shared Effect plumbing
|
||||||
- place each `HttpApi` slice next to the HTTP boundary it serves
|
- place each `HttpApi` slice next to the HTTP boundary it serves
|
||||||
- for instance-scoped routes, prefer `src/server/instance/httpapi/*`
|
- for instance-scoped routes, prefer `src/server/routes/instance/httpapi/*`
|
||||||
- if control-plane routes ever migrate, prefer `src/server/control/httpapi/*`
|
- if control-plane routes ever migrate, prefer `src/server/routes/control/httpapi/*`
|
||||||
|
|
||||||
Suggested file layout for a repeatable spike:
|
Suggested file layout for a repeatable spike:
|
||||||
|
|
||||||
- `src/server/instance/httpapi/question.ts` — contract and handler layer for one route group
|
- `src/server/routes/instance/httpapi/question.ts` — contract and handler layer for one route group
|
||||||
- `src/server/instance/httpapi/server.ts` — standalone Effect HTTP server that composes all groups
|
- `src/server/routes/instance/httpapi/server.ts` — bridged Effect HTTP layer that composes all groups
|
||||||
- `test/server/question-httpapi.test.ts` — end-to-end test against the real service
|
- route or OpenAPI verification should live alongside the existing server tests; there is no dedicated `question-httpapi` test file on this branch
|
||||||
|
|
||||||
Suggested responsibilities:
|
Suggested responsibilities:
|
||||||
|
|
||||||
- `question.ts` defines the `HttpApi` contract and `HttpApiBuilder.group(...)` handlers
|
- `question.ts` defines the `HttpApi` contract and `HttpApiBuilder.group(...)` handlers
|
||||||
- `server.ts` composes all route groups into one `HttpRouter.serve` layer with shared middleware (auth, instance lookup)
|
- `server.ts` composes all route groups into one `HttpRouter.toWebHandler(...)` bridge with shared middleware (auth, instance lookup)
|
||||||
- tests use `ExperimentalHttpApiServer.layerTest` to run against a real in-process HTTP server
|
- tests should verify the bridged routes through the normal server surface
|
||||||
|
|
||||||
## Example migration shape
|
## Example migration shape
|
||||||
|
|
||||||
@@ -254,33 +319,33 @@ Each route-group spike should follow the same shape.
|
|||||||
- keep handler bodies thin
|
- keep handler bodies thin
|
||||||
- keep transport mapping at the HTTP boundary only
|
- keep transport mapping at the HTTP boundary only
|
||||||
|
|
||||||
### 3. Standalone server
|
### 3. Bridged server
|
||||||
|
|
||||||
- the Effect HTTP server is self-contained in `httpapi/server.ts`
|
- the Effect HTTP layer is composed in `httpapi/server.ts`
|
||||||
- it is **not** mounted into the Hono app — no bridge, no `toWebHandler`
|
- it is mounted into the Hono app via `HttpRouter.toWebHandler(...)`
|
||||||
- route paths use the `/experimental/httpapi` prefix so they match the eventual cutover
|
- routes keep their normal instance paths and are gated by the `OPENCODE_EXPERIMENTAL_HTTPAPI` flag
|
||||||
- each route group exposes its own OpenAPI doc endpoint
|
- the legacy Hono handlers stay registered after the bridge so current OpenAPI / SDK generation still works
|
||||||
|
|
||||||
### 4. Verification
|
### 4. Verification
|
||||||
|
|
||||||
- seed real state through the existing service
|
- seed real state through the existing service
|
||||||
- call the experimental endpoints
|
- call the bridged endpoints with the flag enabled
|
||||||
- assert that the service behavior is unchanged
|
- assert that the service behavior is unchanged
|
||||||
- assert that the generated OpenAPI contains the migrated paths and schemas
|
- assert that the generated OpenAPI contains the migrated paths and schemas
|
||||||
|
|
||||||
## Boundary composition
|
## Boundary composition
|
||||||
|
|
||||||
The standalone Effect server owns its own middleware stack. It does not share middleware with the Hono server.
|
The Effect `HttpApi` layer owns its own auth and instance middleware, but it is currently mounted inside the existing Hono server.
|
||||||
|
|
||||||
### Auth
|
### Auth
|
||||||
|
|
||||||
- the standalone server implements auth as an `HttpApiMiddleware.Service` using `HttpApiSecurity.basic`
|
- the bridged `HttpApi` layer implements auth as an `HttpApiMiddleware.Service` using `HttpApiSecurity.basic`
|
||||||
- each route group's `HttpApi` is wrapped with `.middleware(Authorization)` before being served
|
- each route group's `HttpApi` is wrapped with `.middleware(Authorization)` before being served
|
||||||
- this is independent of the Hono `AuthMiddleware` — when the Effect server eventually replaces Hono, this becomes the only auth layer
|
- this is independent of the Hono auth layer; the current bridge keeps the responsibility local to the `HttpApi` slice
|
||||||
|
|
||||||
### Instance and workspace lookup
|
### Instance and workspace lookup
|
||||||
|
|
||||||
- the standalone server resolves instance context via an `HttpRouter.middleware` that reads `x-opencode-directory` headers and `directory` query params
|
- the bridged `HttpApi` layer resolves instance context via an `HttpRouter.middleware` that reads `x-opencode-directory` headers and `directory` query params
|
||||||
- this is the Effect equivalent of the Hono `WorkspaceRouterMiddleware`
|
- this is the Effect equivalent of the Hono `WorkspaceRouterMiddleware`
|
||||||
- `HttpApi` handlers yield services from context and assume the correct instance has already been provided
|
- `HttpApi` handlers yield services from context and assume the correct instance has already been provided
|
||||||
|
|
||||||
@@ -295,51 +360,57 @@ The standalone Effect server owns its own middleware stack. It does not share mi
|
|||||||
|
|
||||||
The first slice is successful if:
|
The first slice is successful if:
|
||||||
|
|
||||||
- the standalone Effect server starts and serves the endpoints independently of the Hono server
|
- the bridged endpoints serve correctly through the existing Hono host when the flag is enabled
|
||||||
- the handlers reuse the existing Effect service
|
- the handlers reuse the existing Effect service
|
||||||
- request decoding and response shapes are schema-defined from canonical Effect schemas
|
- request decoding and response shapes are schema-defined from canonical Effect schemas
|
||||||
- any remaining Zod boundary usage is derived from `.zod` or clearly temporary
|
- any remaining Zod boundary usage is derived from `.zod` or clearly temporary
|
||||||
- OpenAPI is generated from the `HttpApi` contract
|
- OpenAPI is generated from the `HttpApi` contract
|
||||||
- the tests are straightforward enough that the next slice feels mechanical
|
- the tests are straightforward enough that the next slice feels mechanical
|
||||||
|
|
||||||
## Learnings from the question slice
|
## Learnings
|
||||||
|
|
||||||
The first parallel `question` spike gave us a concrete pattern to reuse.
|
### Schema
|
||||||
|
|
||||||
- `Schema.Class` works well for route DTOs such as `Question.Request`, `Question.Info`, and `Question.Reply`.
|
- `Schema.Class` works well for route DTOs such as `Question.Request`, `Question.Info`, and `Question.Reply`.
|
||||||
- scalar or collection schemas such as `Question.Answer` should stay as schemas and use helpers like `withStatics(...)` instead of being forced into classes.
|
- scalar or collection schemas such as `Question.Answer` should stay as schemas and use helpers like `withStatics(...)` instead of being forced into classes.
|
||||||
- if an `HttpApi` success schema uses `Schema.Class`, the handler or underlying service needs to return real schema instances rather than plain objects.
|
- if an `HttpApi` success schema uses `Schema.Class`, the handler or underlying service needs to return real schema instances rather than plain objects.
|
||||||
- internal event payloads can stay anonymous when we want to avoid adding extra named OpenAPI component churn for non-route shapes.
|
- internal event payloads can stay anonymous when we want to avoid adding extra named OpenAPI component churn for non-route shapes.
|
||||||
- the experimental slice should stay as a standalone Effect server and keep calling the existing service layer unchanged.
|
- `Schema.Class` emits named `$ref` in OpenAPI — only use it for types that already had `.meta({ ref })` in the old Zod schema. Inner/nested types should stay as `Schema.Struct` to avoid SDK shape changes.
|
||||||
- compare generated OpenAPI semantically at the route and schema level.
|
|
||||||
|
### Integration
|
||||||
|
|
||||||
|
- `HttpRouter.toWebHandler` with the shared `memoMap` from `run-service.ts` cleanly bridges Effect routes into Hono — one process, one port, shared layer instances.
|
||||||
|
- `Observability.layer` must be explicitly provided via `Layer.provideMerge` in the routes layer for OTEL spans and HTTP logs to flow. The `memoMap` deduplicates it with `AppRuntime` — no extra cost.
|
||||||
|
- `HttpMiddleware.logger` (enabled by default when `disableLogger` is not set) emits structured `Effect.log` entries with `http.method`, `http.url`, `http.status` — these flow through `OtlpLogger` to motel.
|
||||||
|
- Hono OpenAPI stubs must remain registered for SDK codegen until the SDK pipeline reads from the Effect OpenAPI spec instead.
|
||||||
|
- the `OPENCODE_EXPERIMENTAL_HTTPAPI` flag gates the bridge at the Hono router level — default off, no behavior change unless opted in.
|
||||||
|
|
||||||
## Route inventory
|
## Route inventory
|
||||||
|
|
||||||
Status legend:
|
Status legend:
|
||||||
|
|
||||||
- `done` - parallel `HttpApi` slice exists
|
- `bridged` - Effect HttpApi slice exists and is bridged into Hono behind the flag
|
||||||
|
- `done` - Effect HttpApi slice exists but not yet bridged
|
||||||
- `next` - good near-term candidate
|
- `next` - good near-term candidate
|
||||||
- `later` - possible, but not first wave
|
- `later` - possible, but not first wave
|
||||||
- `defer` - not a good early `HttpApi` target
|
- `defer` - not a good early `HttpApi` target
|
||||||
|
|
||||||
Current instance route inventory:
|
Current instance route inventory:
|
||||||
|
|
||||||
- `question` - `done`
|
- `question` - `bridged`
|
||||||
endpoints in slice: `GET /question`, `POST /question/:requestID/reply`
|
endpoints: `GET /question`, `POST /question/:requestID/reply`, `POST /question/:requestID/reject`
|
||||||
- `permission` - `done`
|
- `permission` - `bridged`
|
||||||
endpoints in slice: `GET /permission`, `POST /permission/:requestID/reply`
|
endpoints: `GET /permission`, `POST /permission/:requestID/reply`
|
||||||
- `provider` - `next`
|
- `provider` - `bridged`
|
||||||
best next endpoint: `GET /provider/auth`
|
endpoints: `GET /provider`, `GET /provider/auth`, `POST /provider/:providerID/oauth/authorize`, `POST /provider/:providerID/oauth/callback`
|
||||||
later endpoint: `GET /provider`
|
- `config` - `bridged` (partial)
|
||||||
defer first-wave OAuth mutations
|
bridged endpoint: `GET /config/providers`
|
||||||
- `config` - `next`
|
|
||||||
best next endpoint: `GET /config/providers`
|
|
||||||
later endpoint: `GET /config`
|
later endpoint: `GET /config`
|
||||||
defer `PATCH /config` for now
|
defer `PATCH /config` for now
|
||||||
- `project` - `later`
|
- `project` - `bridged` (partial)
|
||||||
best small reads: `GET /project`, `GET /project/current`
|
bridged endpoints: `GET /project`, `GET /project/current`
|
||||||
defer git-init mutation first
|
defer git-init mutation first
|
||||||
- `workspace` - `later`
|
- `workspace` - `next`
|
||||||
best small reads: `GET /experimental/workspace/adaptor`, `GET /experimental/workspace`, `GET /experimental/workspace/status`
|
best small reads: `GET /experimental/workspace/adaptor`, `GET /experimental/workspace`, `GET /experimental/workspace/status`
|
||||||
defer create/remove mutations first
|
defer create/remove mutations first
|
||||||
- `file` - `later`
|
- `file` - `later`
|
||||||
@@ -357,12 +428,12 @@ Current instance route inventory:
|
|||||||
- `tui` - `defer`
|
- `tui` - `defer`
|
||||||
queue-style UI bridge, weak early `HttpApi` fit
|
queue-style UI bridge, weak early `HttpApi` fit
|
||||||
|
|
||||||
Recommended near-term sequence after the first spike:
|
Recommended near-term sequence:
|
||||||
|
|
||||||
1. `provider` auth read endpoint
|
1. `workspace` read endpoints (`GET /experimental/workspace/adaptor`, `GET /experimental/workspace`, `GET /experimental/workspace/status`)
|
||||||
2. `config` providers read endpoint
|
2. `config` full read endpoint (`GET /config`)
|
||||||
3. `project` read endpoints
|
3. `file` JSON read endpoints
|
||||||
4. `workspace` read endpoints
|
4. `mcp` JSON read endpoints
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
@@ -371,7 +442,17 @@ Recommended near-term sequence after the first spike:
|
|||||||
- [x] keep the underlying service calls identical to the current handlers
|
- [x] keep the underlying service calls identical to the current handlers
|
||||||
- [x] compare generated OpenAPI against the current Hono/OpenAPI setup
|
- [x] compare generated OpenAPI against the current Hono/OpenAPI setup
|
||||||
- [x] document how auth, instance lookup, and error mapping would compose in the new stack
|
- [x] document how auth, instance lookup, and error mapping would compose in the new stack
|
||||||
- [ ] decide after the spike whether `HttpApi` should stay parallel, replace only some groups, or become the long-term default
|
- [x] bridge Effect routes into Hono via `toWebHandler` with shared `memoMap`
|
||||||
|
- [x] gate behind `OPENCODE_EXPERIMENTAL_HTTPAPI` flag
|
||||||
|
- [x] verify OTEL spans and HTTP logs flow to motel
|
||||||
|
- [x] bridge question, permission, and provider auth routes
|
||||||
|
- [x] port remaining provider endpoints (`GET /provider`, OAuth mutations)
|
||||||
|
- [x] port `config` providers read endpoint
|
||||||
|
- [x] port `project` read endpoints (`GET /project`, `GET /project/current`)
|
||||||
|
- [ ] port `workspace` read endpoints
|
||||||
|
- [ ] port `GET /config` full read endpoint
|
||||||
|
- [ ] port `file` JSON read endpoints
|
||||||
|
- [ ] decide when to remove the flag and make Effect routes the default
|
||||||
|
|
||||||
## Rule of thumb
|
## Rule of thumb
|
||||||
|
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ Direct legacy usage means any source file that still calls one of:
|
|||||||
- `Instance.reload(...)`
|
- `Instance.reload(...)`
|
||||||
- `Instance.dispose()` / `Instance.disposeAll()`
|
- `Instance.dispose()` / `Instance.disposeAll()`
|
||||||
|
|
||||||
Current total: `54` files in `packages/opencode/src`.
|
Current total: `56` files in `packages/opencode/src`.
|
||||||
|
|
||||||
### Core bridge and plumbing
|
### Core bridge and plumbing
|
||||||
|
|
||||||
@@ -177,13 +177,13 @@ Migration rule:
|
|||||||
|
|
||||||
These are the current request-entry seams that still create or consume instance context through the legacy helper.
|
These are the current request-entry seams that still create or consume instance context through the legacy helper.
|
||||||
|
|
||||||
- `src/server/instance/middleware.ts`
|
- `src/server/routes/instance/middleware.ts`
|
||||||
- `src/server/instance/index.ts`
|
- `src/server/routes/instance/index.ts`
|
||||||
- `src/server/instance/project.ts`
|
- `src/server/routes/instance/project.ts`
|
||||||
- `src/server/instance/workspace.ts`
|
- `src/server/routes/control/workspace.ts`
|
||||||
- `src/server/instance/file.ts`
|
- `src/server/routes/instance/file.ts`
|
||||||
- `src/server/instance/experimental.ts`
|
- `src/server/routes/instance/experimental.ts`
|
||||||
- `src/server/instance/global.ts`
|
- `src/server/routes/global.ts`
|
||||||
|
|
||||||
Migration rule:
|
Migration rule:
|
||||||
|
|
||||||
@@ -239,7 +239,7 @@ Migration rule:
|
|||||||
These modules are already the best near-term migration targets because they are in Effect code but still read sync getters from the legacy helper.
|
These modules are already the best near-term migration targets because they are in Effect code but still read sync getters from the legacy helper.
|
||||||
|
|
||||||
- `src/agent/agent.ts`
|
- `src/agent/agent.ts`
|
||||||
- `src/config/tui-migrate.ts`
|
- `src/cli/cmd/tui/config/tui-migrate.ts`
|
||||||
- `src/file/index.ts`
|
- `src/file/index.ts`
|
||||||
- `src/file/watcher.ts`
|
- `src/file/watcher.ts`
|
||||||
- `src/format/formatter.ts`
|
- `src/format/formatter.ts`
|
||||||
@@ -250,7 +250,7 @@ These modules are already the best near-term migration targets because they are
|
|||||||
- `src/project/vcs.ts`
|
- `src/project/vcs.ts`
|
||||||
- `src/provider/provider.ts`
|
- `src/provider/provider.ts`
|
||||||
- `src/pty/index.ts`
|
- `src/pty/index.ts`
|
||||||
- `src/session/index.ts`
|
- `src/session/session.ts`
|
||||||
- `src/session/instruction.ts`
|
- `src/session/instruction.ts`
|
||||||
- `src/session/llm.ts`
|
- `src/session/llm.ts`
|
||||||
- `src/session/system.ts`
|
- `src/session/system.ts`
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ Small follow-ups that do not fit neatly into the main facade, route, tool, or sc
|
|||||||
|
|
||||||
## Config / TUI
|
## Config / TUI
|
||||||
|
|
||||||
- [ ] `config/tui.ts` - finish the internal Effect migration after the `Instance.state(...)` removal.
|
- [ ] `cli/cmd/tui/config/tui.ts` - finish the internal Effect migration.
|
||||||
Keep the current precedence and migration semantics intact while converting the remaining internal async helpers (`loadState`, `mergeFile`, `loadFile`, `load`) to `Effect.gen(...)` / `Effect.fn(...)`.
|
Keep the current precedence and migration semantics intact while converting the remaining internal async helpers (`loadState`, `mergeFile`, `loadFile`, `load`) to `Effect.gen(...)` / `Effect.fn(...)`.
|
||||||
- [ ] `config/tui.ts` callers - once the internal service is stable, migrate plain async callers to use `TuiConfig.Service` directly where that actually simplifies the code.
|
- [ ] `cli/cmd/tui/config/tui.ts` callers - once the internal service is stable, migrate plain async callers to use `TuiConfig.Service` directly where that actually simplifies the code.
|
||||||
Likely first callers: `cli/cmd/tui/attach.ts`, `cli/cmd/tui/thread.ts`, `cli/cmd/tui/plugin/runtime.ts`.
|
Likely first callers: `cli/cmd/tui/attach.ts`, `cli/cmd/tui/thread.ts`, `cli/cmd/tui/plugin/runtime.ts`.
|
||||||
- [ ] `env/index.ts` - move the last production `Instance.state(...)` usage onto `InstanceState` (or its replacement) so `Instance.state` can be deleted.
|
- [x] `env/index.ts` - already uses `InstanceState.make(...)`.
|
||||||
|
|
||||||
## ConfigPaths
|
## ConfigPaths
|
||||||
|
|
||||||
@@ -21,14 +21,12 @@ Small follow-ups that do not fit neatly into the main facade, route, tool, or sc
|
|||||||
- `readFile(...)`
|
- `readFile(...)`
|
||||||
- `parseText(...)`
|
- `parseText(...)`
|
||||||
- [ ] `config/config.ts` - switch internal config loading from `Effect.promise(() => ConfigPaths.*(...))` to `yield* paths.*(...)` once the service exists.
|
- [ ] `config/config.ts` - switch internal config loading from `Effect.promise(() => ConfigPaths.*(...))` to `yield* paths.*(...)` once the service exists.
|
||||||
- [ ] `config/tui.ts` - switch TUI config loading from async `ConfigPaths.*` wrappers to the `ConfigPaths.Service` once that service exists.
|
- [ ] `cli/cmd/tui/config/tui.ts` - switch TUI config loading from async `ConfigPaths.*` wrappers to the `ConfigPaths.Service` once that service exists.
|
||||||
- [ ] `config/tui-migrate.ts` - decide whether to leave this as a plain async module using wrapper functions or effectify it fully after `ConfigPaths.Service` lands.
|
- [ ] `cli/cmd/tui/config/tui-migrate.ts` - decide whether to leave this as a plain async module using wrapper functions or effectify it fully after `ConfigPaths.Service` lands.
|
||||||
|
|
||||||
## Instance cleanup
|
## Instance cleanup
|
||||||
|
|
||||||
- [ ] `project/instance.ts` - remove `Instance.state(...)` once `env/index.ts` is migrated.
|
- [ ] `project/instance.ts` - keep shrinking the legacy ALS / Promise cache after the remaining `Instance.*` callers move over.
|
||||||
- [ ] `project/state.ts` - delete the bespoke per-instance state helper after the last production caller is gone.
|
|
||||||
- [ ] `test/project/state.test.ts` - replace or delete the old `Instance.state(...)` tests after the removal.
|
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Use `InstanceState` (from `src/effect/instance-state.ts`) for services that need
|
|||||||
Use `makeRuntime` (from `src/effect/run-service.ts`) to create a per-service `ManagedRuntime` that lazily initializes and shares layers via a global `memoMap`. Returns `{ runPromise, runFork, runCallback }`.
|
Use `makeRuntime` (from `src/effect/run-service.ts`) to create a per-service `ManagedRuntime` that lazily initializes and shares layers via a global `memoMap`. Returns `{ runPromise, runFork, runCallback }`.
|
||||||
|
|
||||||
- Global services (no per-directory state): Account, Auth, AppFileSystem, Installation, Truncate, Worktree
|
- Global services (no per-directory state): Account, Auth, AppFileSystem, Installation, Truncate, Worktree
|
||||||
- Instance-scoped (per-directory state via InstanceState): Agent, Bus, Command, Config, File, FileTime, FileWatcher, Format, LSP, MCP, Permission, Plugin, ProviderAuth, Pty, Question, SessionStatus, Skill, Snapshot, ToolRegistry, Vcs
|
- Instance-scoped (per-directory state via InstanceState): Agent, Bus, Command, Config, File, FileWatcher, Format, LSP, MCP, Permission, Plugin, ProviderAuth, Pty, Question, SessionStatus, Skill, Snapshot, ToolRegistry, Vcs
|
||||||
|
|
||||||
Rule of thumb: if two open directories should not share one copy of the service, it needs `InstanceState`.
|
Rule of thumb: if two open directories should not share one copy of the service, it needs `InstanceState`.
|
||||||
|
|
||||||
@@ -19,53 +19,43 @@ See `instance-context.md` for the phased plan to remove the legacy ALS / promise
|
|||||||
|
|
||||||
## Service shape
|
## Service shape
|
||||||
|
|
||||||
Every service follows the same pattern — a single namespace with the service definition, layer, `runPromise`, and async facade functions:
|
Every service follows the same pattern: one module, flat top-level exports, traced Effect methods, and a self-reexport at the bottom when the file is the public module.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
export namespace Foo {
|
export interface Interface {
|
||||||
export interface Interface {
|
readonly get: (id: FooID) => Effect.Effect<FooInfo, FooError>
|
||||||
readonly get: (id: FooID) => Effect.Effect<FooInfo, FooError>
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Foo") {}
|
|
||||||
|
|
||||||
export const layer = Layer.effect(
|
|
||||||
Service,
|
|
||||||
Effect.gen(function* () {
|
|
||||||
// For instance-scoped services:
|
|
||||||
const state = yield* InstanceState.make<State>(
|
|
||||||
Effect.fn("Foo.state")(() => Effect.succeed({ ... })),
|
|
||||||
)
|
|
||||||
|
|
||||||
const get = Effect.fn("Foo.get")(function* (id: FooID) {
|
|
||||||
const s = yield* InstanceState.get(state)
|
|
||||||
// ...
|
|
||||||
})
|
|
||||||
|
|
||||||
return Service.of({ get })
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Optional: wire dependencies
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(FooDep.layer))
|
|
||||||
|
|
||||||
// Per-service runtime (inside the namespace)
|
|
||||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
|
||||||
|
|
||||||
// Async facade functions
|
|
||||||
export async function get(id: FooID) {
|
|
||||||
return runPromise((svc) => svc.get(id))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class Service extends Context.Service<Service, Interface>()("@opencode/Foo") {}
|
||||||
|
|
||||||
|
export const layer = Layer.effect(
|
||||||
|
Service,
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const state = yield* InstanceState.make<State>(
|
||||||
|
Effect.fn("Foo.state")(() => Effect.succeed({ ... })),
|
||||||
|
)
|
||||||
|
|
||||||
|
const get = Effect.fn("Foo.get")(function* (id: FooID) {
|
||||||
|
const s = yield* InstanceState.get(state)
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
|
||||||
|
return Service.of({ get })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const defaultLayer = layer.pipe(Layer.provide(FooDep.layer))
|
||||||
|
|
||||||
|
export * as Foo from "."
|
||||||
```
|
```
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
|
|
||||||
- Keep everything in one namespace, one file — no separate `service.ts` / `index.ts` split
|
- Keep the service surface in one module; prefer flat top-level exports over `export namespace Foo { ... }`
|
||||||
- `runPromise` goes inside the namespace (not exported unless tests need it)
|
- Use `Effect.fn("Foo.method")` for Effect methods
|
||||||
- Facade functions are plain `async function` — no `fn()` wrappers
|
- Use a self-reexport (`export * as Foo from "."` or `"./foo"`) for the public namespace projection
|
||||||
- Use `Effect.fn("Namespace.method")` for all Effect functions (for tracing)
|
- Avoid service-local `makeRuntime(...)` facades unless a file is still intentionally in the older migration phase
|
||||||
- No `Layer.fresh` — InstanceState handles per-directory isolation
|
- No `Layer.fresh` for normal per-directory isolation; use `InstanceState`
|
||||||
|
|
||||||
## Schema → Zod interop
|
## Schema → Zod interop
|
||||||
|
|
||||||
@@ -195,7 +185,6 @@ This checklist is only about the service shape migration. Many of these services
|
|||||||
- [x] `Config` — `config/config.ts`
|
- [x] `Config` — `config/config.ts`
|
||||||
- [x] `Discovery` — `skill/discovery.ts` (dependency-only layer, no standalone runtime)
|
- [x] `Discovery` — `skill/discovery.ts` (dependency-only layer, no standalone runtime)
|
||||||
- [x] `File` — `file/index.ts`
|
- [x] `File` — `file/index.ts`
|
||||||
- [x] `FileTime` — `file/time.ts`
|
|
||||||
- [x] `FileWatcher` — `file/watcher.ts`
|
- [x] `FileWatcher` — `file/watcher.ts`
|
||||||
- [x] `Format` — `format/index.ts`
|
- [x] `Format` — `format/index.ts`
|
||||||
- [x] `Installation` — `installation/index.ts`
|
- [x] `Installation` — `installation/index.ts`
|
||||||
@@ -267,7 +256,7 @@ Tool-specific filesystem cleanup notes live in `tools.md`.
|
|||||||
|
|
||||||
## Destroying the facades
|
## Destroying the facades
|
||||||
|
|
||||||
This phase is still broadly open. As of 2026-04-13 there are still 15 `makeRuntime(...)` call sites under `src/`, with 13 still in scope for facade removal. The live checklist now lives in `facades.md`.
|
This phase is no longer broadly open. There are 5 `makeRuntime(...)` call sites under `src/`, and only a small subset are still ordinary facade-removal targets. The live checklist now lives in `facades.md`.
|
||||||
|
|
||||||
These facades exist because cyclic imports used to force each service to build its own independent runtime. Now that the layer DAG is acyclic and `AppRuntime` (`src/effect/app-runtime.ts`) composes everything into one `ManagedRuntime`, we're removing them.
|
These facades exist because cyclic imports used to force each service to build its own independent runtime. Now that the layer DAG is acyclic and `AppRuntime` (`src/effect/app-runtime.ts`) composes everything into one `ManagedRuntime`, we're removing them.
|
||||||
|
|
||||||
@@ -298,12 +287,11 @@ For each service, the migration is roughly:
|
|||||||
- `ShareNext` — migrated 2026-04-11. Swapped remaining async callers to `AppRuntime.runPromise(ShareNext.Service.use(...))`, removed the `makeRuntime(...)` facade, and kept instance bootstrap on the shared app runtime.
|
- `ShareNext` — migrated 2026-04-11. Swapped remaining async callers to `AppRuntime.runPromise(ShareNext.Service.use(...))`, removed the `makeRuntime(...)` facade, and kept instance bootstrap on the shared app runtime.
|
||||||
- `SessionTodo` — migrated 2026-04-10. Already matched the target service shape in `session/todo.ts`: single namespace, traced Effect methods, and no `makeRuntime(...)` facade remained; checklist updated to reflect the completed migration.
|
- `SessionTodo` — migrated 2026-04-10. Already matched the target service shape in `session/todo.ts`: single namespace, traced Effect methods, and no `makeRuntime(...)` facade remained; checklist updated to reflect the completed migration.
|
||||||
- `Storage` — migrated 2026-04-10. One production caller (`Session.diff`) and all storage.test.ts tests converted to effectful style. Facades and `makeRuntime` removed.
|
- `Storage` — migrated 2026-04-10. One production caller (`Session.diff`) and all storage.test.ts tests converted to effectful style. Facades and `makeRuntime` removed.
|
||||||
- `SessionRunState` — migrated 2026-04-11. Single caller in `server/instance/session.ts` converted; facade removed.
|
- `SessionRunState` — migrated 2026-04-11. Single caller in `server/routes/instance/session.ts` converted; facade removed.
|
||||||
- `Account` — migrated 2026-04-11. Callers in `server/instance/experimental.ts` and `cli/cmd/account.ts` converted; facade removed.
|
- `Account` — migrated 2026-04-11. Callers in `server/routes/instance/experimental.ts` and `cli/cmd/account.ts` converted; facade removed.
|
||||||
- `Instruction` — migrated 2026-04-11. Test-only callers converted; facade removed.
|
- `Instruction` — migrated 2026-04-11. Test-only callers converted; facade removed.
|
||||||
- `FileTime` — migrated 2026-04-11. Test-only callers converted; facade removed.
|
|
||||||
- `FileWatcher` — migrated 2026-04-11. Callers in `project/bootstrap.ts` and test converted; facade removed.
|
- `FileWatcher` — migrated 2026-04-11. Callers in `project/bootstrap.ts` and test converted; facade removed.
|
||||||
- `Question` — migrated 2026-04-11. Callers in `server/instance/question.ts` and test converted; facade removed.
|
- `Question` — migrated 2026-04-11. Callers in `server/routes/instance/question.ts` and test converted; facade removed.
|
||||||
- `Truncate` — migrated 2026-04-11. Caller in `tool/tool.ts` and test converted; facade removed.
|
- `Truncate` — migrated 2026-04-11. Caller in `tool/tool.ts` and test converted; facade removed.
|
||||||
|
|
||||||
## Route handler effectification
|
## Route handler effectification
|
||||||
|
|||||||
@@ -1,444 +0,0 @@
|
|||||||
# Namespace → flat export migration
|
|
||||||
|
|
||||||
Migrate `export namespace` to the `export * as` / flat-export pattern used by
|
|
||||||
effect-smol. Primary goal: tree-shakeability. Secondary: consistency with Effect
|
|
||||||
conventions, LLM-friendliness for future migrations.
|
|
||||||
|
|
||||||
## What changes and what doesn't
|
|
||||||
|
|
||||||
The **consumer API stays the same**. You still write `Provider.ModelNotFoundError`,
|
|
||||||
`Config.JsonError`, `Bus.publish`, etc. The namespace ergonomics are preserved.
|
|
||||||
|
|
||||||
What changes is **how** the namespace is constructed — the TypeScript
|
|
||||||
`export namespace` keyword is replaced by `export * as` in a barrel file. This
|
|
||||||
is a mechanical change: unwrap the namespace body into flat exports, add a
|
|
||||||
one-line barrel. Consumers that import `{ Provider }` don't notice.
|
|
||||||
|
|
||||||
Import paths actually get **nicer**. Today most consumers import from the
|
|
||||||
explicit file (`"../provider/provider"`). After the migration, each module has a
|
|
||||||
barrel `index.ts`, so imports become `"../provider"` or `"@/provider"`:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// BEFORE — points at the file directly
|
|
||||||
import { Provider } from "../provider/provider"
|
|
||||||
|
|
||||||
// AFTER — resolves to provider/index.ts, same Provider namespace
|
|
||||||
import { Provider } from "../provider"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Why this matters right now
|
|
||||||
|
|
||||||
The CLI binary startup time (TOI) is too slow. Profiling shows we're loading
|
|
||||||
massive dependency graphs that are never actually used at runtime — because
|
|
||||||
bundlers cannot tree-shake TypeScript `export namespace` bodies.
|
|
||||||
|
|
||||||
### The problem in one sentence
|
|
||||||
|
|
||||||
`cli/error.ts` needs 6 lightweight `.isInstance()` checks on error classes, but
|
|
||||||
importing `{ Provider }` from `provider.ts` forces the bundler to include **all
|
|
||||||
20+ `@ai-sdk/*` packages**, `@aws-sdk/credential-providers`,
|
|
||||||
`google-auth-library`, and every other top-level import in that 1709-line file.
|
|
||||||
|
|
||||||
### Why `export namespace` defeats tree-shaking
|
|
||||||
|
|
||||||
TypeScript compiles `export namespace Foo { ... }` to an IIFE:
|
|
||||||
|
|
||||||
```js
|
|
||||||
// TypeScript output
|
|
||||||
export var Provider;
|
|
||||||
(function (Provider) {
|
|
||||||
Provider.ModelNotFoundError = NamedError.create(...)
|
|
||||||
// ... 1600 more lines of assignments ...
|
|
||||||
})(Provider || (Provider = {}))
|
|
||||||
```
|
|
||||||
|
|
||||||
This is **opaque to static analysis**. The bundler sees one big function call
|
|
||||||
whose return value populates an object. It cannot determine which properties are
|
|
||||||
used downstream, so it keeps everything. Every `import` statement at the top of
|
|
||||||
`provider.ts` executes unconditionally — that's 20+ AI SDK packages loaded into
|
|
||||||
memory just so the CLI can check `Provider.ModelNotFoundError.isInstance(x)`.
|
|
||||||
|
|
||||||
### What `export * as` does differently
|
|
||||||
|
|
||||||
`export * as Provider from "./provider"` compiles to a static re-export. The
|
|
||||||
bundler knows the exact shape of `Provider` at compile time — it's the named
|
|
||||||
export list of `./provider.ts`. When it sees `Provider.ModelNotFoundError` used
|
|
||||||
but `Provider.layer` unused, it can trace that `ModelNotFoundError` doesn't
|
|
||||||
reference `createAnthropic` or any AI SDK import, and drop them. The namespace
|
|
||||||
object still exists at runtime — same API — but the bundler can see inside it.
|
|
||||||
|
|
||||||
### Concrete impact
|
|
||||||
|
|
||||||
The worst import chain in the codebase:
|
|
||||||
|
|
||||||
```
|
|
||||||
src/index.ts (entry point)
|
|
||||||
└── FormatError from src/cli/error.ts
|
|
||||||
├── { Provider } from provider/provider.ts (1709 lines)
|
|
||||||
│ ├── 20+ @ai-sdk/* packages
|
|
||||||
│ ├── @aws-sdk/credential-providers
|
|
||||||
│ ├── google-auth-library
|
|
||||||
│ ├── gitlab-ai-provider, venice-ai-sdk-provider
|
|
||||||
│ └── fuzzysort, remeda, etc.
|
|
||||||
├── { Config } from config/config.ts (1663 lines)
|
|
||||||
│ ├── jsonc-parser
|
|
||||||
│ ├── LSPServer (all server definitions)
|
|
||||||
│ └── Plugin, Auth, Env, Account, etc.
|
|
||||||
└── { MCP } from mcp/index.ts (930 lines)
|
|
||||||
├── @modelcontextprotocol/sdk (3 transports)
|
|
||||||
└── open (browser launcher)
|
|
||||||
```
|
|
||||||
|
|
||||||
All of this gets pulled in to check `.isInstance()` on 6 error classes — code
|
|
||||||
that needs maybe 200 bytes total. This inflates the binary, increases startup
|
|
||||||
memory, and slows down initial module evaluation.
|
|
||||||
|
|
||||||
### Why this also hurts memory
|
|
||||||
|
|
||||||
Every module-level import is eagerly evaluated. Even with Bun's fast module
|
|
||||||
loader, evaluating 20+ AI SDK factory functions, the AWS credential chain, and
|
|
||||||
Google's auth library allocates objects, closures, and prototype chains that
|
|
||||||
persist for the lifetime of the process. Most CLI commands never use a provider
|
|
||||||
at all.
|
|
||||||
|
|
||||||
## What effect-smol does
|
|
||||||
|
|
||||||
effect-smol achieves tree-shakeable namespaced APIs via three structural choices.
|
|
||||||
|
|
||||||
### 1. Each module is a separate file with flat named exports
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// Effect.ts — no namespace wrapper, just flat exports
|
|
||||||
export const gen: { ... } = internal.gen
|
|
||||||
export const fail: <E>(error: E) => Effect<never, E> = internal.fail
|
|
||||||
export const succeed: <A>(value: A) => Effect<A> = internal.succeed
|
|
||||||
// ... 230+ individual named exports
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Barrel file uses `export * as` (not `export namespace`)
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// index.ts
|
|
||||||
export * as Effect from "./Effect.ts"
|
|
||||||
export * as Schema from "./Schema.ts"
|
|
||||||
export * as Stream from "./Stream.ts"
|
|
||||||
// ~134 modules
|
|
||||||
```
|
|
||||||
|
|
||||||
This creates a namespace-like API (`Effect.gen`, `Schema.parse`) but the
|
|
||||||
bundler knows the **exact shape** at compile time — it's the static export list
|
|
||||||
of that file. It can trace property accesses (`Effect.gen` → keep `gen`,
|
|
||||||
drop `timeout` if unused). With `export namespace`, the IIFE is opaque and
|
|
||||||
nothing can be dropped.
|
|
||||||
|
|
||||||
### 3. `sideEffects: []` and deep imports
|
|
||||||
|
|
||||||
```jsonc
|
|
||||||
// package.json
|
|
||||||
{ "sideEffects": [] }
|
|
||||||
```
|
|
||||||
|
|
||||||
Plus `"./*": "./src/*.ts"` in the exports map, enabling
|
|
||||||
`import * as Effect from "effect/Effect"` to bypass the barrel entirely.
|
|
||||||
|
|
||||||
### 4. Errors as flat exports, not class declarations
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// Cause.ts
|
|
||||||
export const NoSuchElementErrorTypeId = core.NoSuchElementErrorTypeId
|
|
||||||
export interface NoSuchElementError extends YieldableError { ... }
|
|
||||||
export const NoSuchElementError: new(msg?: string) => NoSuchElementError = core.NoSuchElementError
|
|
||||||
export const isNoSuchElementError: (u: unknown) => u is NoSuchElementError = core.isNoSuchElementError
|
|
||||||
```
|
|
||||||
|
|
||||||
Each error is 4 independent exports: TypeId, interface, constructor (as const),
|
|
||||||
type guard. All individually shakeable.
|
|
||||||
|
|
||||||
## The plan
|
|
||||||
|
|
||||||
The core migration is **Phase 1** — convert `export namespace` to
|
|
||||||
`export * as`. Once that's done, the bundler can tree-shake individual exports
|
|
||||||
within each module. You do NOT need to break things into subfiles for
|
|
||||||
tree-shaking to work — the bundler traces which exports you actually access on
|
|
||||||
the namespace object and drops the rest, including their transitive imports.
|
|
||||||
|
|
||||||
Splitting errors/schemas into separate files (Phase 0) is optional — it's a
|
|
||||||
lower-risk warmup step that can be done before or after the main conversion, and
|
|
||||||
it provides extra resilience against bundler edge cases. But the big win comes
|
|
||||||
from Phase 1.
|
|
||||||
|
|
||||||
### Phase 0 (optional): Pre-split errors into subfiles
|
|
||||||
|
|
||||||
This is a low-risk warmup that provides immediate benefit even before the full
|
|
||||||
`export * as` conversion. It's optional because Phase 1 alone is sufficient for
|
|
||||||
tree-shaking. But it's a good starting point if you want incremental progress:
|
|
||||||
|
|
||||||
**For each namespace that defines errors** (15 files, ~30 error classes total):
|
|
||||||
|
|
||||||
1. Create a sibling `errors.ts` file (e.g. `provider/errors.ts`) with the error
|
|
||||||
definitions as top-level named exports:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// provider/errors.ts
|
|
||||||
import z from "zod"
|
|
||||||
import { NamedError } from "@opencode-ai/shared/util/error"
|
|
||||||
import { ProviderID, ModelID } from "./schema"
|
|
||||||
|
|
||||||
export const ModelNotFoundError = NamedError.create(
|
|
||||||
"ProviderModelNotFoundError",
|
|
||||||
z.object({
|
|
||||||
providerID: ProviderID.zod,
|
|
||||||
modelID: ModelID.zod,
|
|
||||||
suggestions: z.array(z.string()).optional(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const InitError = NamedError.create("ProviderInitError", z.object({ providerID: ProviderID.zod }))
|
|
||||||
```
|
|
||||||
|
|
||||||
2. In the namespace file, re-export from the errors file to maintain backward
|
|
||||||
compatibility:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// provider/provider.ts — inside the namespace
|
|
||||||
export { ModelNotFoundError, InitError } from "./errors"
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Update `cli/error.ts` (and any other light consumers) to import directly:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// BEFORE
|
|
||||||
import { Provider } from "../provider/provider"
|
|
||||||
Provider.ModelNotFoundError.isInstance(input)
|
|
||||||
|
|
||||||
// AFTER
|
|
||||||
import { ModelNotFoundError as ProviderModelNotFoundError } from "../provider/errors"
|
|
||||||
ProviderModelNotFoundError.isInstance(input)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Files to split (Phase 0):**
|
|
||||||
|
|
||||||
| Current file | New errors file | Errors to extract |
|
|
||||||
| ----------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| `provider/provider.ts` | `provider/errors.ts` | ModelNotFoundError, InitError |
|
|
||||||
| `provider/auth.ts` | `provider/auth-errors.ts` | OauthMissing, OauthCodeMissing, OauthCallbackFailed, ValidationFailed |
|
|
||||||
| `config/config.ts` | (already has `config/paths.ts`) | ConfigDirectoryTypoError → move to paths.ts |
|
|
||||||
| `config/markdown.ts` | `config/markdown-errors.ts` | FrontmatterError |
|
|
||||||
| `mcp/index.ts` | `mcp/errors.ts` | Failed |
|
|
||||||
| `session/message-v2.ts` | `session/message-errors.ts` | OutputLengthError, AbortedError, StructuredOutputError, AuthError, APIError, ContextOverflowError |
|
|
||||||
| `session/message.ts` | (shares with message-v2) | OutputLengthError, AuthError |
|
|
||||||
| `cli/ui.ts` | `cli/ui-errors.ts` | CancelledError |
|
|
||||||
| `skill/index.ts` | `skill/errors.ts` | InvalidError, NameMismatchError |
|
|
||||||
| `worktree/index.ts` | `worktree/errors.ts` | NotGitError, NameGenerationFailedError, CreateFailedError, StartCommandFailedError, RemoveFailedError, ResetFailedError |
|
|
||||||
| `storage/storage.ts` | `storage/errors.ts` | NotFoundError |
|
|
||||||
| `npm/index.ts` | `npm/errors.ts` | InstallFailedError |
|
|
||||||
| `ide/index.ts` | `ide/errors.ts` | AlreadyInstalledError, InstallFailedError |
|
|
||||||
| `lsp/client.ts` | `lsp/errors.ts` | InitializeError |
|
|
||||||
|
|
||||||
### Phase 1: The real migration — `export namespace` → `export * as`
|
|
||||||
|
|
||||||
This is the phase that actually fixes tree-shaking. For each module:
|
|
||||||
|
|
||||||
1. **Unwrap** the `export namespace Foo { ... }` — remove the namespace wrapper,
|
|
||||||
keep all the members as top-level `export const` / `export function` / etc.
|
|
||||||
2. **Rename** the file if it's currently `index.ts` (e.g. `bus/index.ts` →
|
|
||||||
`bus/bus.ts`), so the barrel can take `index.ts`.
|
|
||||||
3. **Create the barrel** `index.ts` with one line: `export * as Foo from "./foo"`
|
|
||||||
|
|
||||||
The file structure change for a module that's currently a single file:
|
|
||||||
|
|
||||||
```
|
|
||||||
# BEFORE
|
|
||||||
provider/
|
|
||||||
provider.ts ← 1709-line file with `export namespace Provider { ... }`
|
|
||||||
|
|
||||||
# AFTER
|
|
||||||
provider/
|
|
||||||
index.ts ← NEW: `export * as Provider from "./provider"`
|
|
||||||
provider.ts ← SAME file, same name, just unwrap the namespace
|
|
||||||
```
|
|
||||||
|
|
||||||
And the code change is purely removing the wrapper:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// BEFORE: provider/provider.ts
|
|
||||||
export namespace Provider {
|
|
||||||
export class Service extends Context.Service<...>()("@opencode/Provider") {}
|
|
||||||
export const layer = Layer.effect(Service, ...)
|
|
||||||
export const ModelNotFoundError = NamedError.create(...)
|
|
||||||
export function parseModel(model: string) { ... }
|
|
||||||
}
|
|
||||||
|
|
||||||
// AFTER: provider/provider.ts — identical exports, no namespace keyword
|
|
||||||
export class Service extends Context.Service<...>()("@opencode/Provider") {}
|
|
||||||
export const layer = Layer.effect(Service, ...)
|
|
||||||
export const ModelNotFoundError = NamedError.create(...)
|
|
||||||
export function parseModel(model: string) { ... }
|
|
||||||
```
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// NEW: provider/index.ts
|
|
||||||
export * as Provider from "./provider"
|
|
||||||
```
|
|
||||||
|
|
||||||
Consumer code barely changes — import path gets shorter:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// BEFORE
|
|
||||||
import { Provider } from "../provider/provider"
|
|
||||||
|
|
||||||
// AFTER — resolves to provider/index.ts, same Provider object
|
|
||||||
import { Provider } from "../provider"
|
|
||||||
```
|
|
||||||
|
|
||||||
All access like `Provider.ModelNotFoundError`, `Provider.Service`,
|
|
||||||
`Provider.layer` works exactly as before. The difference is invisible to
|
|
||||||
consumers but lets the bundler see inside the namespace.
|
|
||||||
|
|
||||||
**Once this is done, you don't need to break anything into subfiles for
|
|
||||||
tree-shaking.** The bundler traces that `Provider.ModelNotFoundError` only
|
|
||||||
depends on `NamedError` + `zod` + the schema file, and drops
|
|
||||||
`Provider.layer` + all 20 AI SDK imports when they're unused. This works because
|
|
||||||
`export * as` gives the bundler a static export list it can do inner-graph
|
|
||||||
analysis on — it knows which exports reference which imports.
|
|
||||||
|
|
||||||
**Order of conversion** (by risk / size, do small modules first):
|
|
||||||
|
|
||||||
1. Tiny utilities: `Archive`, `Color`, `Token`, `Rpc`, `LocalContext` (~7-66 lines each)
|
|
||||||
2. Small services: `Auth`, `Env`, `BusEvent`, `SessionStatus`, `SessionRunState`, `Editor`, `Selection` (~25-91 lines)
|
|
||||||
3. Medium services: `Bus`, `Format`, `FileTime`, `FileWatcher`, `Command`, `Question`, `Permission`, `Vcs`, `Project`
|
|
||||||
4. Large services: `Config`, `Provider`, `MCP`, `Session`, `SessionProcessor`, `SessionPrompt`, `ACP`
|
|
||||||
|
|
||||||
### Phase 2: Build configuration
|
|
||||||
|
|
||||||
After the module structure supports tree-shaking:
|
|
||||||
|
|
||||||
1. Add `"sideEffects": []` to `packages/opencode/package.json` (or
|
|
||||||
`"sideEffects": false`) — this is safe because our services use explicit
|
|
||||||
layer composition, not import-time side effects.
|
|
||||||
2. Verify Bun's bundler respects the new structure. If Bun's tree-shaking is
|
|
||||||
insufficient, evaluate whether the compiled binary path needs an esbuild
|
|
||||||
pre-pass.
|
|
||||||
3. Consider adding `/*#__PURE__*/` annotations to `NamedError.create(...)` calls
|
|
||||||
— these are factory functions that return classes, and bundlers may not know
|
|
||||||
they're side-effect-free without the annotation.
|
|
||||||
|
|
||||||
## Automation
|
|
||||||
|
|
||||||
The transformation is scripted. From `packages/opencode`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bun script/unwrap-namespace.ts <file> [--dry-run]
|
|
||||||
```
|
|
||||||
|
|
||||||
The script uses ast-grep for accurate AST-based namespace boundary detection
|
|
||||||
(no false matches from braces in strings/templates/comments), then:
|
|
||||||
|
|
||||||
1. Removes the `export namespace Foo {` line and its closing `}`
|
|
||||||
2. Dedents the body by one indent level (2 spaces)
|
|
||||||
3. If the file is `index.ts`, renames it to `<name>.ts` and creates a new
|
|
||||||
`index.ts` barrel
|
|
||||||
4. If the file is NOT `index.ts`, rewrites it in place and creates `index.ts`
|
|
||||||
5. Prints the exact commands to find and rewrite import paths
|
|
||||||
|
|
||||||
### Walkthrough: converting a module
|
|
||||||
|
|
||||||
Using `Provider` as an example:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Preview what will change
|
|
||||||
bun script/unwrap-namespace.ts src/provider/provider.ts --dry-run
|
|
||||||
|
|
||||||
# 2. Apply the transformation
|
|
||||||
bun script/unwrap-namespace.ts src/provider/provider.ts
|
|
||||||
|
|
||||||
# 3. Rewrite import paths (script prints the exact command)
|
|
||||||
rg -l 'from.*provider/provider' src/ | xargs sed -i '' 's|provider/provider"|provider"|g'
|
|
||||||
|
|
||||||
# 4. Verify
|
|
||||||
bun typecheck
|
|
||||||
bun run test
|
|
||||||
```
|
|
||||||
|
|
||||||
**What changes on disk:**
|
|
||||||
|
|
||||||
```
|
|
||||||
# BEFORE
|
|
||||||
provider/
|
|
||||||
provider.ts ← 1709 lines, `export namespace Provider { ... }`
|
|
||||||
|
|
||||||
# AFTER
|
|
||||||
provider/
|
|
||||||
index.ts ← NEW: `export * as Provider from "./provider"`
|
|
||||||
provider.ts ← same file, namespace unwrapped to flat exports
|
|
||||||
```
|
|
||||||
|
|
||||||
**What changes in consumer code:**
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// BEFORE
|
|
||||||
import { Provider } from "../provider/provider"
|
|
||||||
|
|
||||||
// AFTER — shorter path, same Provider object
|
|
||||||
import { Provider } from "../provider"
|
|
||||||
```
|
|
||||||
|
|
||||||
All property access (`Provider.Service`, `Provider.ModelNotFoundError`, etc.)
|
|
||||||
stays identical.
|
|
||||||
|
|
||||||
### Two cases the script handles
|
|
||||||
|
|
||||||
**Case A: file is NOT `index.ts`** (e.g. `provider/provider.ts`)
|
|
||||||
|
|
||||||
- Rewrites the file in place (unwrap + dedent)
|
|
||||||
- Creates `provider/index.ts` as the barrel
|
|
||||||
- Import paths change: `"../provider/provider"` → `"../provider"`
|
|
||||||
|
|
||||||
**Case B: file IS `index.ts`** (e.g. `bus/index.ts`)
|
|
||||||
|
|
||||||
- Renames `index.ts` → `bus.ts` (kebab-case of namespace name)
|
|
||||||
- Creates new `index.ts` as the barrel
|
|
||||||
- **No import rewrites needed** — `"@/bus"` already resolves to `bus/index.ts`
|
|
||||||
|
|
||||||
## Do I need to split errors/schemas into subfiles?
|
|
||||||
|
|
||||||
**No.** Once you do the `export * as` conversion, the bundler can tree-shake
|
|
||||||
individual exports within the file. If `cli/error.ts` only accesses
|
|
||||||
`Provider.ModelNotFoundError`, the bundler traces that `ModelNotFoundError`
|
|
||||||
doesn't reference `createAnthropic` and drops the AI SDK imports.
|
|
||||||
|
|
||||||
Splitting into subfiles (errors.ts, schema.ts) is still a fine idea for **code
|
|
||||||
organization** — smaller files are easier to read and review. But it's not
|
|
||||||
required for tree-shaking. The `export * as` conversion alone is sufficient.
|
|
||||||
|
|
||||||
The one case where subfile splitting provides extra tree-shake value is if an
|
|
||||||
imported package has module-level side effects that the bundler can't prove are
|
|
||||||
unused. In practice this is rare — most npm packages are side-effect-free — and
|
|
||||||
adding `"sideEffects": []` to package.json handles the common cases.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
| Metric | Count |
|
|
||||||
| ----------------------------------------------- | --------------- |
|
|
||||||
| Files with `export namespace` | 106 |
|
|
||||||
| Total namespace declarations | 118 (12 nested) |
|
|
||||||
| Files with `NamedError.create` inside namespace | 15 |
|
|
||||||
| Total error classes to extract | ~30 |
|
|
||||||
| Files using `export * as` today | 0 |
|
|
||||||
|
|
||||||
Phase 1 (the `export * as` conversion) is the main change. It's mechanical and
|
|
||||||
LLM-friendly but touches every import site, so it should be done module by
|
|
||||||
module with type-checking between each step. Each module is an independent PR.
|
|
||||||
|
|
||||||
## Rules for new code
|
|
||||||
|
|
||||||
Going forward:
|
|
||||||
|
|
||||||
- **No new `export namespace`**. Use a file with flat named exports and
|
|
||||||
`export * as` in the barrel.
|
|
||||||
- Keep the service, layer, errors, schemas, and runtime wiring together in one
|
|
||||||
file if you want — that's fine now. The `export * as` barrel makes everything
|
|
||||||
individually shakeable regardless of file structure.
|
|
||||||
- If a file grows large enough that it's hard to navigate, split by concern
|
|
||||||
(errors.ts, schema.ts, etc.) for readability. Not for tree-shaking — the
|
|
||||||
bundler handles that.
|
|
||||||
@@ -39,28 +39,26 @@ This eliminates multiple `runPromise` round-trips and lets handlers compose natu
|
|||||||
|
|
||||||
## Current route files
|
## Current route files
|
||||||
|
|
||||||
Current instance route files live under `src/server/instance`, not `server/routes`.
|
Current instance route files live under `src/server/routes/instance`.
|
||||||
|
|
||||||
The main migration targets are:
|
Files that are already mostly on the intended service-yielding shape:
|
||||||
|
|
||||||
- [ ] `server/instance/session.ts` — heaviest; still has many direct facade calls for Session, SessionPrompt, SessionRevert, SessionCompaction, SessionShare, SessionSummary, Agent, Bus
|
- [x] `server/routes/instance/question.ts` — handlers yield `Question.Service`
|
||||||
- [ ] `server/instance/global.ts` — still has direct facade calls for Config and instance lifecycle actions
|
- [x] `server/routes/instance/provider.ts` — handlers yield `Provider.Service`, `ProviderAuth.Service`, and `Config.Service`
|
||||||
- [ ] `server/instance/provider.ts` — still has direct facade calls for Config and Provider
|
- [x] `server/routes/instance/permission.ts` — handlers yield `Permission.Service`
|
||||||
- [ ] `server/instance/question.ts` — partially converted; still worth tracking here until it consistently uses the composed style
|
- [x] `server/routes/instance/mcp.ts` — handlers mostly yield `MCP.Service`
|
||||||
- [ ] `server/instance/pty.ts` — still calls Pty facades directly
|
- [x] `server/routes/instance/pty.ts` — handlers yield `Pty.Service`
|
||||||
- [ ] `server/instance/experimental.ts` — mixed state; some handlers are already composed, others still use facades
|
|
||||||
|
|
||||||
Additional route files that still participate in the migration:
|
Files still worth tracking here:
|
||||||
|
|
||||||
- [ ] `server/instance/index.ts` — Vcs, Agent, Skill, LSP, Format
|
- [ ] `server/routes/instance/session.ts` — still the heaviest mixed file; many handlers are composed, but the file still mixes patterns and has direct `Bus.publish(...)` / `Session.list(...)` usage
|
||||||
- [ ] `server/instance/file.ts` — Ripgrep, File, LSP
|
- [ ] `server/routes/instance/index.ts` — mostly converted, but still has direct `Instance.dispose()` / `Instance.*` reads for `/instance/dispose` and `/path`
|
||||||
- [ ] `server/instance/mcp.ts` — MCP facade-heavy
|
- [ ] `server/routes/instance/file.ts` — most handlers yield services, but `/find` still passes `Instance.directory` directly into ripgrep and `/find/symbol` is still stubbed
|
||||||
- [ ] `server/instance/permission.ts` — Permission
|
- [ ] `server/routes/instance/experimental.ts` — mixed state; many handlers are composed, but some still rely on `runRequest(...)` or direct `Instance.project` reads
|
||||||
- [ ] `server/instance/workspace.ts` — Workspace
|
- [ ] `server/routes/instance/middleware.ts` — still enters the instance via `Instance.provide(...)`
|
||||||
- [ ] `server/instance/tui.ts` — Bus and Session
|
- [ ] `server/routes/global.ts` — still uses `Instance.disposeAll()` and remains partly outside the fully-composed style
|
||||||
- [ ] `server/instance/middleware.ts` — Session and Workspace lookups
|
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Some handlers already use `AppRuntime.runPromise(Effect.gen(...))` in isolated places. Keep pushing those files toward one consistent style.
|
- Route conversion is now less about facade removal and more about removing the remaining direct `Instance.*` reads, `Instance.provide(...)` boundaries, and small Promise-style bridges inside route files.
|
||||||
- Route conversion is closely tied to facade removal. As services lose `makeRuntime`-backed async exports, route handlers should switch to yielding the service directly.
|
- `jsonRequest(...)` / `runRequest(...)` already provide a good intermediate shape for many handlers. The remaining cleanup is mostly consistency work in the heavier files.
|
||||||
|
|||||||
@@ -40,13 +40,13 @@ Everything still lives in `packages/opencode`.
|
|||||||
Important current facts:
|
Important current facts:
|
||||||
|
|
||||||
- there is no `packages/core` or `packages/cli` workspace yet
|
- there is no `packages/core` or `packages/cli` workspace yet
|
||||||
- `packages/server` now exists as a minimal scaffold package, but it does not own any real route contracts, handlers, or runtime composition yet
|
- there is no `packages/server` workspace yet on this branch
|
||||||
- the main host server is still Hono-based in `src/server/server.ts`
|
- the main host server is still Hono-based in `src/server/server.ts`
|
||||||
- current OpenAPI generation is Hono-based through `Server.openapi()` and `cli/cmd/generate.ts`
|
- current OpenAPI generation is Hono-based through `Server.openapi()` and `cli/cmd/generate.ts`
|
||||||
- the Effect runtime and app layer are centralized in `src/effect/app-runtime.ts` and `src/effect/run-service.ts`
|
- the Effect runtime and app layer are centralized in `src/effect/app-runtime.ts` and `src/effect/run-service.ts`
|
||||||
- there is already one experimental Effect `HttpApi` slice at `src/server/instance/httpapi/question.ts`
|
- there are already bridged Effect `HttpApi` slices under `src/server/routes/instance/httpapi/*`
|
||||||
- that experimental slice is mounted under `/experimental/httpapi/question`
|
- those slices are mounted into the Hono server behind `OPENCODE_EXPERIMENTAL_HTTPAPI`
|
||||||
- that experimental slice already has an end-to-end test at `test/server/question-httpapi.test.ts`
|
- the bridge currently covers `question`, `permission`, `provider`, partial `config`, and partial `project` routes
|
||||||
|
|
||||||
This means the package split should start from an extraction path, not from greenfield package ownership.
|
This means the package split should start from an extraction path, not from greenfield package ownership.
|
||||||
|
|
||||||
@@ -209,17 +209,19 @@ Current host and route composition:
|
|||||||
|
|
||||||
- `src/server/server.ts`
|
- `src/server/server.ts`
|
||||||
- `src/server/control/index.ts`
|
- `src/server/control/index.ts`
|
||||||
- `src/server/instance/index.ts`
|
- `src/server/routes/instance/index.ts`
|
||||||
- `src/server/middleware.ts`
|
- `src/server/middleware.ts`
|
||||||
- `src/server/adapter.bun.ts`
|
- `src/server/adapter.bun.ts`
|
||||||
- `src/server/adapter.node.ts`
|
- `src/server/adapter.node.ts`
|
||||||
|
|
||||||
Current experimental `HttpApi` slice:
|
Current bridged `HttpApi` slices:
|
||||||
|
|
||||||
- `src/server/instance/httpapi/question.ts`
|
- `src/server/routes/instance/httpapi/question.ts`
|
||||||
- `src/server/instance/httpapi/index.ts`
|
- `src/server/routes/instance/httpapi/permission.ts`
|
||||||
- `src/server/instance/experimental.ts`
|
- `src/server/routes/instance/httpapi/provider.ts`
|
||||||
- `test/server/question-httpapi.test.ts`
|
- `src/server/routes/instance/httpapi/config.ts`
|
||||||
|
- `src/server/routes/instance/httpapi/project.ts`
|
||||||
|
- `src/server/routes/instance/httpapi/server.ts`
|
||||||
|
|
||||||
Current OpenAPI flow:
|
Current OpenAPI flow:
|
||||||
|
|
||||||
@@ -245,7 +247,7 @@ Keep in `packages/opencode` for now:
|
|||||||
|
|
||||||
- `src/server/server.ts`
|
- `src/server/server.ts`
|
||||||
- `src/server/control/index.ts`
|
- `src/server/control/index.ts`
|
||||||
- `src/server/instance/*.ts`
|
- `src/server/routes/**/*.ts`
|
||||||
- `src/server/middleware.ts`
|
- `src/server/middleware.ts`
|
||||||
- `src/server/adapter.*.ts`
|
- `src/server/adapter.*.ts`
|
||||||
- `src/effect/app-runtime.ts`
|
- `src/effect/app-runtime.ts`
|
||||||
@@ -305,14 +307,13 @@ Bad early migration targets:
|
|||||||
|
|
||||||
## First vertical slice
|
## First vertical slice
|
||||||
|
|
||||||
The first slice for the package split is the existing experimental `question` group.
|
The first slice for the package split is still the existing `question` `HttpApi` group.
|
||||||
|
|
||||||
Why `question` first:
|
Why `question` first:
|
||||||
|
|
||||||
- it already exists as an experimental `HttpApi` slice
|
- it already exists as an experimental `HttpApi` slice
|
||||||
- it already follows the desired contract and implementation split in one file
|
- it already follows the desired contract and implementation split in one file
|
||||||
- it is already mounted through the current Hono host
|
- it is already mounted through the current Hono host
|
||||||
- it already has an end-to-end test
|
|
||||||
- it is JSON-only
|
- it is JSON-only
|
||||||
- it has low blast radius
|
- it has low blast radius
|
||||||
|
|
||||||
@@ -357,7 +358,7 @@ Done means:
|
|||||||
|
|
||||||
Scope:
|
Scope:
|
||||||
|
|
||||||
- extract the pure `HttpApi` contract from `src/server/instance/httpapi/question.ts`
|
- extract the pure `HttpApi` contract from `src/server/routes/instance/httpapi/question.ts`
|
||||||
- place it in `packages/server/src/definition/question.ts`
|
- place it in `packages/server/src/definition/question.ts`
|
||||||
- aggregate it in `packages/server/src/definition/api.ts`
|
- aggregate it in `packages/server/src/definition/api.ts`
|
||||||
- generate OpenAPI in `packages/server/src/openapi.ts`
|
- generate OpenAPI in `packages/server/src/openapi.ts`
|
||||||
@@ -399,8 +400,9 @@ Scope:
|
|||||||
|
|
||||||
- replace local experimental question route wiring in `packages/opencode`
|
- replace local experimental question route wiring in `packages/opencode`
|
||||||
- keep the same mount path:
|
- keep the same mount path:
|
||||||
- `/experimental/httpapi/question`
|
- `/question`
|
||||||
- `/experimental/httpapi/question/doc`
|
- `/question/:requestID/reply`
|
||||||
|
- `/question/:requestID/reject`
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
|
|
||||||
@@ -569,7 +571,7 @@ For package-split PRs, validate the smallest useful thing.
|
|||||||
Typical validation for the first waves:
|
Typical validation for the first waves:
|
||||||
|
|
||||||
- `bun typecheck` in the touched package directory or directories
|
- `bun typecheck` in the touched package directory or directories
|
||||||
- the relevant route test, especially `test/server/question-httpapi.test.ts`
|
- the relevant server / route coverage for the migrated slice
|
||||||
- merged OpenAPI coverage if the PR touches spec generation
|
- merged OpenAPI coverage if the PR touches spec generation
|
||||||
|
|
||||||
Do not run tests from repo root.
|
Do not run tests from repo root.
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ This keeps tool tests aligned with the production service graph and makes follow
|
|||||||
|
|
||||||
## Exported tools
|
## Exported tools
|
||||||
|
|
||||||
These exported tool definitions already exist in `src/tool` and are on the current Effect-native `Tool.define(...)` path:
|
These exported tool definitions currently use `Tool.define(...)` in `src/tool`:
|
||||||
|
|
||||||
- [x] `apply_patch.ts`
|
- [x] `apply_patch.ts`
|
||||||
- [x] `bash.ts`
|
- [x] `bash.ts`
|
||||||
@@ -45,7 +45,6 @@ These exported tool definitions already exist in `src/tool` and are on the curre
|
|||||||
- [x] `glob.ts`
|
- [x] `glob.ts`
|
||||||
- [x] `grep.ts`
|
- [x] `grep.ts`
|
||||||
- [x] `invalid.ts`
|
- [x] `invalid.ts`
|
||||||
- [x] `ls.ts`
|
|
||||||
- [x] `lsp.ts`
|
- [x] `lsp.ts`
|
||||||
- [x] `multiedit.ts`
|
- [x] `multiedit.ts`
|
||||||
- [x] `plan.ts`
|
- [x] `plan.ts`
|
||||||
@@ -60,7 +59,7 @@ These exported tool definitions already exist in `src/tool` and are on the curre
|
|||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
- `batch.ts` is no longer a current tool file and should not be tracked here.
|
- There is no current `ls.ts` tool file on this branch.
|
||||||
- `truncate.ts` is an Effect service used by tools, not a tool definition itself.
|
- `truncate.ts` is an Effect service used by tools, not a tool definition itself.
|
||||||
- `mcp-exa.ts`, `external-directory.ts`, and `schema.ts` are support modules, not standalone tool definitions.
|
- `mcp-exa.ts`, `external-directory.ts`, and `schema.ts` are support modules, not standalone tool definitions.
|
||||||
|
|
||||||
@@ -73,7 +72,7 @@ Current spot cleanups worth tracking:
|
|||||||
- [ ] `read.ts` — still bridges to Node stream / `readline` helpers and Promise-based binary detection
|
- [ ] `read.ts` — still bridges to Node stream / `readline` helpers and Promise-based binary detection
|
||||||
- [ ] `bash.ts` — already uses Effect child-process primitives; only keep tracking shell-specific platform bridges and parser/loading details as they come up
|
- [ ] `bash.ts` — already uses Effect child-process primitives; only keep tracking shell-specific platform bridges and parser/loading details as they come up
|
||||||
- [ ] `webfetch.ts` — already uses `HttpClient`; remaining work is limited to smaller boundary helpers like HTML text extraction
|
- [ ] `webfetch.ts` — already uses `HttpClient`; remaining work is limited to smaller boundary helpers like HTML text extraction
|
||||||
- [ ] `file/ripgrep.ts` — adjacent to tool migration; still has raw fs/process usage that affects `grep.ts` and `ls.ts`
|
- [ ] `file/ripgrep.ts` — adjacent to tool migration; still has raw fs/process usage that affects `grep.ts` and file-search routes
|
||||||
- [ ] `patch/index.ts` — adjacent to tool migration; still has raw fs usage behind patch application
|
- [ ] `patch/index.ts` — adjacent to tool migration; still has raw fs usage behind patch application
|
||||||
|
|
||||||
Notable items that are already effectively on the target path and do not need separate migration bullets right now:
|
Notable items that are already effectively on the target path and do not need separate migration bullets right now:
|
||||||
@@ -83,7 +82,6 @@ Notable items that are already effectively on the target path and do not need se
|
|||||||
- `write.ts`
|
- `write.ts`
|
||||||
- `codesearch.ts`
|
- `codesearch.ts`
|
||||||
- `websearch.ts`
|
- `websearch.ts`
|
||||||
- `ls.ts`
|
|
||||||
- `multiedit.ts`
|
- `multiedit.ts`
|
||||||
- `edit.ts`
|
- `edit.ts`
|
||||||
|
|
||||||
|
|||||||
@@ -181,10 +181,10 @@ export interface Interface {
|
|||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Account") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/Account") {}
|
||||||
|
|
||||||
export const layer: Layer.Layer<Service, never, AccountRepo | HttpClient.HttpClient> = Layer.effect(
|
export const layer: Layer.Layer<Service, never, AccountRepo.Service | HttpClient.HttpClient> = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const repo = yield* AccountRepo
|
const repo = yield* AccountRepo.Service
|
||||||
const http = yield* HttpClient.HttpClient
|
const http = yield* HttpClient.HttpClient
|
||||||
const httpRead = withTransientReadRetry(http)
|
const httpRead = withTransientReadRetry(http)
|
||||||
const httpOk = HttpClient.filterStatusOk(http)
|
const httpOk = HttpClient.filterStatusOk(http)
|
||||||
@@ -452,3 +452,5 @@ export const layer: Layer.Layer<Service, never, AccountRepo | HttpClient.HttpCli
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(Layer.provide(AccountRepo.layer), Layer.provide(FetchHttpClient.layer))
|
export const defaultLayer = layer.pipe(Layer.provide(AccountRepo.layer), Layer.provide(FetchHttpClient.layer))
|
||||||
|
|
||||||
|
export * as Account from "./account"
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
export * as Account from "./account"
|
|
||||||
export {
|
|
||||||
AccountID,
|
|
||||||
type AccountError,
|
|
||||||
AccountRepoError,
|
|
||||||
AccountServiceError,
|
|
||||||
AccountTransportError,
|
|
||||||
AccessToken,
|
|
||||||
RefreshToken,
|
|
||||||
DeviceCode,
|
|
||||||
UserCode,
|
|
||||||
Info,
|
|
||||||
Org,
|
|
||||||
OrgID,
|
|
||||||
Login,
|
|
||||||
PollSuccess,
|
|
||||||
PollPending,
|
|
||||||
PollSlow,
|
|
||||||
PollExpired,
|
|
||||||
PollDenied,
|
|
||||||
PollError,
|
|
||||||
type PollResult,
|
|
||||||
} from "./schema"
|
|
||||||
export type { AccountOrgs, ActiveOrg } from "./account"
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { eq } from "drizzle-orm"
|
import { eq } from "drizzle-orm"
|
||||||
import { Effect, Layer, Option, Schema, Context } from "effect"
|
import { Effect, Layer, Option, Schema, Context } from "effect"
|
||||||
|
|
||||||
import { Database } from "@/storage/db"
|
import { Database } from "@/storage"
|
||||||
import { AccountStateTable, AccountTable } from "./account.sql"
|
import { AccountStateTable, AccountTable } from "./account.sql"
|
||||||
import { AccessToken, AccountID, AccountRepoError, Info, OrgID, RefreshToken } from "./schema"
|
import { AccessToken, AccountID, AccountRepoError, Info, OrgID, RefreshToken } from "./schema"
|
||||||
import { normalizeServerUrl } from "./url"
|
import { normalizeServerUrl } from "./url"
|
||||||
@@ -13,154 +13,154 @@ type DbTransactionCallback<A> = Parameters<typeof Database.transaction<A>>[0]
|
|||||||
|
|
||||||
const ACCOUNT_STATE_ID = 1
|
const ACCOUNT_STATE_ID = 1
|
||||||
|
|
||||||
export namespace AccountRepo {
|
export interface Interface {
|
||||||
export interface Service {
|
readonly active: () => Effect.Effect<Option.Option<Info>, AccountRepoError>
|
||||||
readonly active: () => Effect.Effect<Option.Option<Info>, AccountRepoError>
|
readonly list: () => Effect.Effect<Info[], AccountRepoError>
|
||||||
readonly list: () => Effect.Effect<Info[], AccountRepoError>
|
readonly remove: (accountID: AccountID) => Effect.Effect<void, AccountRepoError>
|
||||||
readonly remove: (accountID: AccountID) => Effect.Effect<void, AccountRepoError>
|
readonly use: (accountID: AccountID, orgID: Option.Option<OrgID>) => Effect.Effect<void, AccountRepoError>
|
||||||
readonly use: (accountID: AccountID, orgID: Option.Option<OrgID>) => Effect.Effect<void, AccountRepoError>
|
readonly getRow: (accountID: AccountID) => Effect.Effect<Option.Option<AccountRow>, AccountRepoError>
|
||||||
readonly getRow: (accountID: AccountID) => Effect.Effect<Option.Option<AccountRow>, AccountRepoError>
|
readonly persistToken: (input: {
|
||||||
readonly persistToken: (input: {
|
accountID: AccountID
|
||||||
accountID: AccountID
|
accessToken: AccessToken
|
||||||
accessToken: AccessToken
|
refreshToken: RefreshToken
|
||||||
refreshToken: RefreshToken
|
expiry: Option.Option<number>
|
||||||
expiry: Option.Option<number>
|
}) => Effect.Effect<void, AccountRepoError>
|
||||||
}) => Effect.Effect<void, AccountRepoError>
|
readonly persistAccount: (input: {
|
||||||
readonly persistAccount: (input: {
|
id: AccountID
|
||||||
id: AccountID
|
email: string
|
||||||
email: string
|
url: string
|
||||||
url: string
|
accessToken: AccessToken
|
||||||
accessToken: AccessToken
|
refreshToken: RefreshToken
|
||||||
refreshToken: RefreshToken
|
expiry: number
|
||||||
expiry: number
|
orgID: Option.Option<OrgID>
|
||||||
orgID: Option.Option<OrgID>
|
}) => Effect.Effect<void, AccountRepoError>
|
||||||
}) => Effect.Effect<void, AccountRepoError>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AccountRepo extends Context.Service<AccountRepo, AccountRepo.Service>()("@opencode/AccountRepo") {
|
export class Service extends Context.Service<Service, Interface>()("@opencode/AccountRepo") {}
|
||||||
static readonly layer: Layer.Layer<AccountRepo> = Layer.effect(
|
|
||||||
AccountRepo,
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const decode = Schema.decodeUnknownSync(Info)
|
|
||||||
|
|
||||||
const query = <A>(f: DbTransactionCallback<A>) =>
|
export const layer: Layer.Layer<Service> = Layer.effect(
|
||||||
Effect.try({
|
Service,
|
||||||
try: () => Database.use(f),
|
Effect.gen(function* () {
|
||||||
catch: (cause) => new AccountRepoError({ message: "Database operation failed", cause }),
|
const decode = Schema.decodeUnknownSync(Info)
|
||||||
|
|
||||||
|
const query = <A>(f: DbTransactionCallback<A>) =>
|
||||||
|
Effect.try({
|
||||||
|
try: () => Database.use(f),
|
||||||
|
catch: (cause) => new AccountRepoError({ message: "Database operation failed", cause }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const tx = <A>(f: DbTransactionCallback<A>) =>
|
||||||
|
Effect.try({
|
||||||
|
try: () => Database.transaction(f),
|
||||||
|
catch: (cause) => new AccountRepoError({ message: "Database operation failed", cause }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const current = (db: DbClient) => {
|
||||||
|
const state = db.select().from(AccountStateTable).where(eq(AccountStateTable.id, ACCOUNT_STATE_ID)).get()
|
||||||
|
if (!state?.active_account_id) return
|
||||||
|
const account = db.select().from(AccountTable).where(eq(AccountTable.id, state.active_account_id)).get()
|
||||||
|
if (!account) return
|
||||||
|
return { ...account, active_org_id: state.active_org_id ?? null }
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = (db: DbClient, accountID: AccountID, orgID: Option.Option<OrgID>) => {
|
||||||
|
const id = Option.getOrNull(orgID)
|
||||||
|
return db
|
||||||
|
.insert(AccountStateTable)
|
||||||
|
.values({ id: ACCOUNT_STATE_ID, active_account_id: accountID, active_org_id: id })
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: AccountStateTable.id,
|
||||||
|
set: { active_account_id: accountID, active_org_id: id },
|
||||||
})
|
})
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
const tx = <A>(f: DbTransactionCallback<A>) =>
|
const active = Effect.fn("AccountRepo.active")(() =>
|
||||||
Effect.try({
|
query((db) => current(db)).pipe(Effect.map((row) => (row ? Option.some(decode(row)) : Option.none()))),
|
||||||
try: () => Database.transaction(f),
|
)
|
||||||
catch: (cause) => new AccountRepoError({ message: "Database operation failed", cause }),
|
|
||||||
})
|
|
||||||
|
|
||||||
const current = (db: DbClient) => {
|
const list = Effect.fn("AccountRepo.list")(() =>
|
||||||
const state = db.select().from(AccountStateTable).where(eq(AccountStateTable.id, ACCOUNT_STATE_ID)).get()
|
query((db) =>
|
||||||
if (!state?.active_account_id) return
|
db
|
||||||
const account = db.select().from(AccountTable).where(eq(AccountTable.id, state.active_account_id)).get()
|
.select()
|
||||||
if (!account) return
|
.from(AccountTable)
|
||||||
return { ...account, active_org_id: state.active_org_id ?? null }
|
.all()
|
||||||
}
|
.map((row: AccountRow) => decode({ ...row, active_org_id: null })),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
const state = (db: DbClient, accountID: AccountID, orgID: Option.Option<OrgID>) => {
|
const remove = Effect.fn("AccountRepo.remove")((accountID: AccountID) =>
|
||||||
const id = Option.getOrNull(orgID)
|
tx((db) => {
|
||||||
return db
|
db.update(AccountStateTable)
|
||||||
.insert(AccountStateTable)
|
.set({ active_account_id: null, active_org_id: null })
|
||||||
.values({ id: ACCOUNT_STATE_ID, active_account_id: accountID, active_org_id: id })
|
.where(eq(AccountStateTable.active_account_id, accountID))
|
||||||
.onConflictDoUpdate({
|
|
||||||
target: AccountStateTable.id,
|
|
||||||
set: { active_account_id: accountID, active_org_id: id },
|
|
||||||
})
|
|
||||||
.run()
|
.run()
|
||||||
}
|
db.delete(AccountTable).where(eq(AccountTable.id, accountID)).run()
|
||||||
|
}).pipe(Effect.asVoid),
|
||||||
|
)
|
||||||
|
|
||||||
const active = Effect.fn("AccountRepo.active")(() =>
|
const use = Effect.fn("AccountRepo.use")((accountID: AccountID, orgID: Option.Option<OrgID>) =>
|
||||||
query((db) => current(db)).pipe(Effect.map((row) => (row ? Option.some(decode(row)) : Option.none()))),
|
query((db) => state(db, accountID, orgID)).pipe(Effect.asVoid),
|
||||||
)
|
)
|
||||||
|
|
||||||
const list = Effect.fn("AccountRepo.list")(() =>
|
const getRow = Effect.fn("AccountRepo.getRow")((accountID: AccountID) =>
|
||||||
query((db) =>
|
query((db) => db.select().from(AccountTable).where(eq(AccountTable.id, accountID)).get()).pipe(
|
||||||
db
|
Effect.map(Option.fromNullishOr),
|
||||||
.select()
|
),
|
||||||
.from(AccountTable)
|
)
|
||||||
.all()
|
|
||||||
.map((row: AccountRow) => decode({ ...row, active_org_id: null })),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const remove = Effect.fn("AccountRepo.remove")((accountID: AccountID) =>
|
const persistToken = Effect.fn("AccountRepo.persistToken")((input) =>
|
||||||
tx((db) => {
|
query((db) =>
|
||||||
db.update(AccountStateTable)
|
db
|
||||||
.set({ active_account_id: null, active_org_id: null })
|
.update(AccountTable)
|
||||||
.where(eq(AccountStateTable.active_account_id, accountID))
|
.set({
|
||||||
.run()
|
access_token: input.accessToken,
|
||||||
db.delete(AccountTable).where(eq(AccountTable.id, accountID)).run()
|
refresh_token: input.refreshToken,
|
||||||
}).pipe(Effect.asVoid),
|
token_expiry: Option.getOrNull(input.expiry),
|
||||||
)
|
})
|
||||||
|
.where(eq(AccountTable.id, input.accountID))
|
||||||
|
.run(),
|
||||||
|
).pipe(Effect.asVoid),
|
||||||
|
)
|
||||||
|
|
||||||
const use = Effect.fn("AccountRepo.use")((accountID: AccountID, orgID: Option.Option<OrgID>) =>
|
const persistAccount = Effect.fn("AccountRepo.persistAccount")((input) =>
|
||||||
query((db) => state(db, accountID, orgID)).pipe(Effect.asVoid),
|
tx((db) => {
|
||||||
)
|
const url = normalizeServerUrl(input.url)
|
||||||
|
|
||||||
const getRow = Effect.fn("AccountRepo.getRow")((accountID: AccountID) =>
|
db.insert(AccountTable)
|
||||||
query((db) => db.select().from(AccountTable).where(eq(AccountTable.id, accountID)).get()).pipe(
|
.values({
|
||||||
Effect.map(Option.fromNullishOr),
|
id: input.id,
|
||||||
),
|
email: input.email,
|
||||||
)
|
url,
|
||||||
|
access_token: input.accessToken,
|
||||||
const persistToken = Effect.fn("AccountRepo.persistToken")((input) =>
|
refresh_token: input.refreshToken,
|
||||||
query((db) =>
|
token_expiry: input.expiry,
|
||||||
db
|
})
|
||||||
.update(AccountTable)
|
.onConflictDoUpdate({
|
||||||
.set({
|
target: AccountTable.id,
|
||||||
access_token: input.accessToken,
|
set: {
|
||||||
refresh_token: input.refreshToken,
|
|
||||||
token_expiry: Option.getOrNull(input.expiry),
|
|
||||||
})
|
|
||||||
.where(eq(AccountTable.id, input.accountID))
|
|
||||||
.run(),
|
|
||||||
).pipe(Effect.asVoid),
|
|
||||||
)
|
|
||||||
|
|
||||||
const persistAccount = Effect.fn("AccountRepo.persistAccount")((input) =>
|
|
||||||
tx((db) => {
|
|
||||||
const url = normalizeServerUrl(input.url)
|
|
||||||
|
|
||||||
db.insert(AccountTable)
|
|
||||||
.values({
|
|
||||||
id: input.id,
|
|
||||||
email: input.email,
|
email: input.email,
|
||||||
url,
|
url,
|
||||||
access_token: input.accessToken,
|
access_token: input.accessToken,
|
||||||
refresh_token: input.refreshToken,
|
refresh_token: input.refreshToken,
|
||||||
token_expiry: input.expiry,
|
token_expiry: input.expiry,
|
||||||
})
|
},
|
||||||
.onConflictDoUpdate({
|
})
|
||||||
target: AccountTable.id,
|
.run()
|
||||||
set: {
|
void state(db, input.id, input.orgID)
|
||||||
email: input.email,
|
}).pipe(Effect.asVoid),
|
||||||
url,
|
)
|
||||||
access_token: input.accessToken,
|
|
||||||
refresh_token: input.refreshToken,
|
|
||||||
token_expiry: input.expiry,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.run()
|
|
||||||
void state(db, input.id, input.orgID)
|
|
||||||
}).pipe(Effect.asVoid),
|
|
||||||
)
|
|
||||||
|
|
||||||
return AccountRepo.of({
|
return Service.of({
|
||||||
active,
|
active,
|
||||||
list,
|
list,
|
||||||
remove,
|
remove,
|
||||||
use,
|
use,
|
||||||
getRow,
|
getRow,
|
||||||
persistToken,
|
persistToken,
|
||||||
persistAccount,
|
persistAccount,
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
export * as AccountRepo from "./repo"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user