mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-15 07:48:24 -04:00
Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 60d099a996 | |||
| 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 |
@@ -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"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -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.94",
|
||||||
"@ai-sdk/anthropic": "3.0.67",
|
"@ai-sdk/anthropic": "3.0.70",
|
||||||
"@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.102",
|
||||||
"@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.111",
|
||||||
"@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",
|
||||||
@@ -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,7 +524,9 @@
|
|||||||
"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:",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -734,7 +738,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.94", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.70", "@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-XKE7wAjXejsIfNQvn3onvGUByhGHVM6W+xlL+1DAQLmjEb+ue4sOJIRehJ96rEvTXVVHRVyA6bSXx7ayxXfn5A=="],
|
||||||
|
|
||||||
"@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 +758,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.102", "", { "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-GrwDpaYJiVafrsA1MTbZtXPcQUI67g5AXiJo7Y1F8b+w+SiYHLk3ZIn1YmpQVoVAh2bjvxjj+Vo0AvfskuGH4g=="],
|
||||||
|
|
||||||
"@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.111", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.70", "@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-5gILpAWWI5idfal/MfoH3tlQeSnOJ9jfL8JB8m2fdc3ue/9xoXkYDpXpDL/nyJImFjMCi6eR0Fpvlo/IKEWDIg=="],
|
||||||
|
|
||||||
"@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=="],
|
||||||
|
|
||||||
@@ -1680,6 +1684,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=="],
|
||||||
@@ -4100,6 +4116,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 +5152,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.70", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hubTFcfnG3NbrlcDW0tU2fsZhRy/7dF5GCymu4DzBQUYliy2lb7tCeeMhDtFBaYa01qSBHRjkwGnsAdUtDPCwA=="],
|
||||||
|
|
||||||
|
"@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 +5170,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.70", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hubTFcfnG3NbrlcDW0tU2fsZhRy/7dF5GCymu4DzBQUYliy2lb7tCeeMhDtFBaYa01qSBHRjkwGnsAdUtDPCwA=="],
|
||||||
|
|
||||||
|
"@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=="],
|
||||||
|
|
||||||
@@ -5666,6 +5690,8 @@
|
|||||||
|
|
||||||
"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/@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=="],
|
||||||
|
|
||||||
"ai-gateway-provider/@ai-sdk/google": ["@ai-sdk/google@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-uz8tIlkDgQJG9Js2Wh9JHzd4kI9+hYJqf9XXJLx60vyN5mRIqhr49iwR5zGP5Gl8odp2PeR3Gh2k+5bh3Z1HHw=="],
|
"ai-gateway-provider/@ai-sdk/google": ["@ai-sdk/google@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-uz8tIlkDgQJG9Js2Wh9JHzd4kI9+hYJqf9XXJLx60vyN5mRIqhr49iwR5zGP5Gl8odp2PeR3Gh2k+5bh3Z1HHw=="],
|
||||||
@@ -5882,7 +5908,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.70", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hubTFcfnG3NbrlcDW0tU2fsZhRy/7dF5GCymu4DzBQUYliy2lb7tCeeMhDtFBaYa01qSBHRjkwGnsAdUtDPCwA=="],
|
||||||
|
|
||||||
"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-tYAb5Mo39UW1VEejYuo0jW0jzH2OyY/HrqgiZL3rmjY=",
|
||||||
"aarch64-linux": "sha256-9118AS1ED0nrliURgZYBRuF/18RqXpUouhYJRlZ6jeA=",
|
"aarch64-linux": "sha256-3zGKV5UwokXpmY0nT1mry3IhNf2EQYLKT7ac+/trmQA=",
|
||||||
"aarch64-darwin": "sha256-ppo3MfSIGKQHJCdYEZiLFRc61PtcJ9J0kAXH1pNIonA=",
|
"aarch64-darwin": "sha256-oKXAut7eu/eW5a43OT8+aFuH1F1tuIldTs+7PUXSCv4=",
|
||||||
"x86_64-darwin": "sha256-m+CZSOglBCTfNzbdBX6hXdDqqOzHNMzAddVp6BZVDtU="
|
"x86_64-darwin": "sha256-Az+9X1scOEhw3aOO8laKJoZjiuz3qlLTIk1bx25P/z4="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -87,6 +87,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",
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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?.()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -252,41 +252,48 @@ 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-0": !layout.sidebar.opened(),
|
||||||
"translate-x-0": !layout.sidebar.opened(),
|
"-translate-x-[36px]": layout.sidebar.opened(),
|
||||||
"-translate-x-[36px]": layout.sidebar.opened(),
|
"duration-180 ease-out": !layout.sidebar.opened(),
|
||||||
"duration-180 ease-out": !layout.sidebar.opened(),
|
"duration-180 ease-in": layout.sidebar.opened(),
|
||||||
"duration-180 ease-in": layout.sidebar.opened(),
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<Show when={hasProjects()}>
|
||||||
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
|
<div class="flex items-center gap-0 transition-transform">
|
||||||
<Button
|
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
|
||||||
variant="ghost"
|
<Button
|
||||||
icon="chevron-left"
|
variant="ghost"
|
||||||
class="titlebar-icon w-6 h-6 p-0 box-border"
|
icon="chevron-left"
|
||||||
disabled={!canBack()}
|
class="titlebar-icon w-6 h-6 p-0 box-border"
|
||||||
onClick={back}
|
disabled={!canBack()}
|
||||||
aria-label={language.t("common.goBack")}
|
onClick={back}
|
||||||
/>
|
aria-label={language.t("common.goBack")}
|
||||||
</Tooltip>
|
/>
|
||||||
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
|
</Tooltip>
|
||||||
<Button
|
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
|
||||||
variant="ghost"
|
<Button
|
||||||
icon="chevron-right"
|
variant="ghost"
|
||||||
class="titlebar-icon w-6 h-6 p-0 box-border"
|
icon="chevron-right"
|
||||||
disabled={!canForward()}
|
class="titlebar-icon w-6 h-6 p-0 box-border"
|
||||||
onClick={forward}
|
disabled={!canForward()}
|
||||||
aria-label={language.t("common.goForward")}
|
onClick={forward}
|
||||||
/>
|
aria-label={language.t("common.goForward")}
|
||||||
</Tooltip>
|
/>
|
||||||
</div>
|
</Tooltip>
|
||||||
</Show>
|
</div>
|
||||||
|
</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,
|
.ensureQueryData({
|
||||||
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,8 +262,9 @@ 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)
|
||||||
|
child[1]("bootstrapPromise", promise!)
|
||||||
const cache = children.vcsCache.get(directory)
|
const cache = children.vcsCache.get(directory)
|
||||||
if (!cache) return
|
if (!cache) return
|
||||||
const sdk = sdkFor(directory)
|
const sdk = sdkFor(directory)
|
||||||
@@ -269,11 +282,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 +331,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 +360,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 +374,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),
|
||||||
})
|
})
|
||||||
|
})()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ export function createChildStoreManager(input: {
|
|||||||
limit: 5,
|
limit: 5,
|
||||||
message: {},
|
message: {},
|
||||||
part: {},
|
part: {},
|
||||||
|
bootstrapPromise: Promise.resolve(),
|
||||||
})
|
})
|
||||||
children[directory] = child
|
children[directory] = child
|
||||||
disposers.set(directory, dispose)
|
disposers.set(directory, dispose)
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ export type State = {
|
|||||||
part: {
|
part: {
|
||||||
[messageID: string]: Part[]
|
[messageID: string]: Part[]
|
||||||
}
|
}
|
||||||
|
bootstrapPromise: Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type VcsCache = {
|
export type VcsCache = {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
+1
@@ -3,6 +3,7 @@ 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 OPENCODE_CHANNEL?: "dev" | "beta" | "prod"
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
|
|||||||
@@ -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": "보기",
|
||||||
|
|||||||
@@ -132,9 +132,11 @@ export default function Layout(props: ParentProps) {
|
|||||||
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 +958,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 +1096,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 +1362,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 +1455,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 +1827,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()
|
||||||
@@ -2110,7 +2112,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
onSave={(next) => {
|
onSave={(next) => {
|
||||||
const item = project()
|
const item = project()
|
||||||
if (!item) return
|
if (!item) return
|
||||||
renameProject(item, next)
|
void renameProject(item, next)
|
||||||
}}
|
}}
|
||||||
class="text-14-medium text-text-strong truncate"
|
class="text-14-medium text-text-strong truncate"
|
||||||
displayClass="text-14-medium text-text-strong truncate"
|
displayClass="text-14-medium text-text-strong truncate"
|
||||||
@@ -2242,7 +2244,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
const item = project()
|
const item = project()
|
||||||
if (!item) return
|
if (!item) return
|
||||||
createWorkspace(item)
|
void createWorkspace(item)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{language.t("workspace.new")}
|
{language.t("workspace.new")}
|
||||||
@@ -2353,8 +2355,14 @@ export default function Layout(props: ParentProps) {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const [loading] = createResource(
|
||||||
|
() => route()?.store?.[0]?.bootstrapPromise,
|
||||||
|
(p) => p,
|
||||||
|
)
|
||||||
|
|
||||||
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(), loading()) ?? ""}
|
||||||
<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()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -454,7 +455,8 @@ export const LocalWorkspace = (props: {
|
|||||||
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 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 loading = createMemo(() => query.isPending && count() === 0)
|
||||||
const hasMore = createMemo(() => workspace().store.sessionTotal > count())
|
const hasMore = createMemo(() => workspace().store.sessionTotal > count())
|
||||||
const loadMore = async () => {
|
const loadMore = async () => {
|
||||||
workspace().setStore("limit", (limit) => (limit ?? 0) + 5)
|
workspace().setStore("limit", (limit) => (limit ?? 0) + 5)
|
||||||
@@ -471,7 +473,7 @@ export const LocalWorkspace = (props: {
|
|||||||
mobile={props.mobile}
|
mobile={props.mobile}
|
||||||
ctx={props.ctx}
|
ctx={props.ctx}
|
||||||
showNew={() => false}
|
showNew={() => false}
|
||||||
loading={loading}
|
loading={() => query.isLoading}
|
||||||
sessions={sessions}
|
sessions={sessions}
|
||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
loadMore={loadMore}
|
loadMore={loadMore}
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -432,7 +433,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 +484,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(
|
||||||
@@ -805,8 +805,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 +818,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 +833,9 @@ export default function Page() {
|
|||||||
})
|
})
|
||||||
}, 0)
|
}, 0)
|
||||||
})
|
})
|
||||||
}),
|
|
||||||
|
return sync.session.sync(id)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
@@ -1882,6 +1882,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()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }),
|
||||||
|
|||||||
@@ -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,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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -39,6 +39,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
|
||||||
|
|
||||||
|
|||||||
@@ -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.94",
|
||||||
"@ai-sdk/anthropic": "3.0.67",
|
"@ai-sdk/anthropic": "3.0.70",
|
||||||
"@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.102",
|
||||||
"@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.111",
|
||||||
"@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",
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
/**
|
||||||
|
* Collapse a single-namespace barrel directory into a dir/index.ts module.
|
||||||
|
*
|
||||||
|
* Given a directory `src/foo/` that contains:
|
||||||
|
*
|
||||||
|
* - `index.ts` (exactly `export * as Foo from "./foo"`)
|
||||||
|
* - `foo.ts` (the real implementation)
|
||||||
|
* - zero or more sibling files
|
||||||
|
*
|
||||||
|
* this script:
|
||||||
|
*
|
||||||
|
* 1. Deletes the old `index.ts` barrel.
|
||||||
|
* 2. `git mv`s `foo.ts` → `index.ts` so the implementation IS the directory entry.
|
||||||
|
* 3. Appends `export * as Foo from "."` to the new `index.ts`.
|
||||||
|
* 4. Rewrites any same-directory sibling `*.ts` files that imported
|
||||||
|
* `./foo` (with or without the namespace name) to import `"."` instead.
|
||||||
|
*
|
||||||
|
* Consumer files outside the directory keep importing from the directory
|
||||||
|
* (`"@/foo"` / `"../foo"` / etc.) and continue to work, because
|
||||||
|
* `dir/index.ts` now provides the `Foo` named export directly.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
*
|
||||||
|
* bun script/collapse-barrel.ts src/bus
|
||||||
|
* bun script/collapse-barrel.ts src/bus --dry-run
|
||||||
|
*
|
||||||
|
* Notes:
|
||||||
|
*
|
||||||
|
* - Only works on directories whose barrel is a single
|
||||||
|
* `export * as Name from "./file"` line. Refuses otherwise.
|
||||||
|
* - Refuses if the implementation file name already conflicts with
|
||||||
|
* `index.ts`.
|
||||||
|
* - Safe to run repeatedly: a second run on an already-collapsed dir
|
||||||
|
* will exit with a clear message.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from "node:fs"
|
||||||
|
import path from "node:path"
|
||||||
|
import { spawnSync } from "node:child_process"
|
||||||
|
|
||||||
|
const args = process.argv.slice(2)
|
||||||
|
const dryRun = args.includes("--dry-run")
|
||||||
|
const targetArg = args.find((a) => !a.startsWith("--"))
|
||||||
|
|
||||||
|
if (!targetArg) {
|
||||||
|
console.error("Usage: bun script/collapse-barrel.ts <dir> [--dry-run]")
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const dir = path.resolve(targetArg)
|
||||||
|
const indexPath = path.join(dir, "index.ts")
|
||||||
|
|
||||||
|
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
||||||
|
console.error(`Not a directory: ${dir}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(indexPath)) {
|
||||||
|
console.error(`No index.ts in ${dir}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate barrel shape.
|
||||||
|
const indexContent = fs.readFileSync(indexPath, "utf-8").trim()
|
||||||
|
const match = indexContent.match(/^export\s+\*\s+as\s+(\w+)\s+from\s+["']\.\/([^"']+)["']\s*;?\s*$/)
|
||||||
|
if (!match) {
|
||||||
|
console.error(`Not a simple single-namespace barrel:\n${indexContent}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
const namespaceName = match[1]
|
||||||
|
const implRel = match[2].replace(/\.ts$/, "")
|
||||||
|
const implPath = path.join(dir, `${implRel}.ts`)
|
||||||
|
|
||||||
|
if (!fs.existsSync(implPath)) {
|
||||||
|
console.error(`Implementation file not found: ${implPath}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (implRel === "index") {
|
||||||
|
console.error(`Nothing to do — impl file is already index.ts`)
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Collapsing ${path.relative(process.cwd(), dir)}`)
|
||||||
|
console.log(` namespace: ${namespaceName}`)
|
||||||
|
console.log(` impl file: ${implRel}.ts → index.ts`)
|
||||||
|
|
||||||
|
// Figure out which sibling files need rewriting.
|
||||||
|
const siblings = fs
|
||||||
|
.readdirSync(dir)
|
||||||
|
.filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"))
|
||||||
|
.filter((f) => f !== "index.ts" && f !== `${implRel}.ts`)
|
||||||
|
.map((f) => path.join(dir, f))
|
||||||
|
|
||||||
|
type SiblingEdit = { file: string; content: string }
|
||||||
|
const siblingEdits: SiblingEdit[] = []
|
||||||
|
|
||||||
|
for (const sibling of siblings) {
|
||||||
|
const content = fs.readFileSync(sibling, "utf-8")
|
||||||
|
// Match any import or re-export referring to "./<implRel>" inside this directory.
|
||||||
|
const siblingRegex = new RegExp(`(from\\s*["'])\\.\\/${implRel.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&")}(["'])`, "g")
|
||||||
|
if (!siblingRegex.test(content)) continue
|
||||||
|
const updated = content.replace(siblingRegex, `$1.$2`)
|
||||||
|
siblingEdits.push({ file: sibling, content: updated })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (siblingEdits.length > 0) {
|
||||||
|
console.log(` sibling rewrites: ${siblingEdits.length}`)
|
||||||
|
for (const edit of siblingEdits) {
|
||||||
|
console.log(` ${path.relative(process.cwd(), edit.file)}`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(` sibling rewrites: none`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dryRun) {
|
||||||
|
console.log(`\n(dry run) would:`)
|
||||||
|
console.log(` - delete ${path.relative(process.cwd(), indexPath)}`)
|
||||||
|
console.log(` - git mv ${path.relative(process.cwd(), implPath)} ${path.relative(process.cwd(), indexPath)}`)
|
||||||
|
console.log(` - append \`export * as ${namespaceName} from "."\` to the new index.ts`)
|
||||||
|
for (const edit of siblingEdits) {
|
||||||
|
console.log(` - rewrite sibling: ${path.relative(process.cwd(), edit.file)}`)
|
||||||
|
}
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply: remove the old barrel, git-mv the impl onto it, then rewrite content.
|
||||||
|
// We can't git-mv on top of an existing tracked file, so we remove the barrel first.
|
||||||
|
function runGit(...cmd: string[]) {
|
||||||
|
const res = spawnSync("git", cmd, { stdio: "inherit" })
|
||||||
|
if (res.status !== 0) {
|
||||||
|
console.error(`git ${cmd.join(" ")} failed`)
|
||||||
|
process.exit(res.status ?? 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: remove the barrel
|
||||||
|
runGit("rm", "-f", indexPath)
|
||||||
|
|
||||||
|
// Step 2: rename the impl file into index.ts
|
||||||
|
runGit("mv", implPath, indexPath)
|
||||||
|
|
||||||
|
// Step 3: append the self-reexport to the new index.ts
|
||||||
|
const newContent = fs.readFileSync(indexPath, "utf-8")
|
||||||
|
const trimmed = newContent.endsWith("\n") ? newContent : newContent + "\n"
|
||||||
|
fs.writeFileSync(indexPath, `${trimmed}\nexport * as ${namespaceName} from "."\n`)
|
||||||
|
console.log(` appended: export * as ${namespaceName} from "."`)
|
||||||
|
|
||||||
|
// Step 4: rewrite siblings
|
||||||
|
for (const edit of siblingEdits) {
|
||||||
|
fs.writeFileSync(edit.file, edit.content)
|
||||||
|
}
|
||||||
|
if (siblingEdits.length > 0) {
|
||||||
|
console.log(` rewrote ${siblingEdits.length} sibling file(s)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\nDone. Verify with:`)
|
||||||
|
console.log(` cd packages/opencode`)
|
||||||
|
console.log(` bunx --bun tsgo --noEmit`)
|
||||||
|
console.log(` bun run --conditions=browser ./src/index.ts generate`)
|
||||||
|
console.log(` bun run test`)
|
||||||
@@ -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,444 +1,256 @@
|
|||||||
# Namespace → flat export migration
|
# Namespace → self-reexport migration
|
||||||
|
|
||||||
Migrate `export namespace` to the `export * as` / flat-export pattern used by
|
Migrate every `export namespace Foo { ... }` to flat top-level exports plus a
|
||||||
effect-smol. Primary goal: tree-shakeability. Secondary: consistency with Effect
|
single self-reexport line at the bottom of the same file:
|
||||||
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
|
```ts
|
||||||
// BEFORE — points at the file directly
|
export * as Foo from "./foo"
|
||||||
import { Provider } from "../provider/provider"
|
|
||||||
|
|
||||||
// AFTER — resolves to provider/index.ts, same Provider namespace
|
|
||||||
import { Provider } from "../provider"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Why this matters right now
|
No barrel `index.ts` files. No cross-directory indirection. Consumers keep the
|
||||||
|
exact same `import { Foo } from "../foo/foo"` ergonomics.
|
||||||
|
|
||||||
The CLI binary startup time (TOI) is too slow. Profiling shows we're loading
|
## Why this pattern
|
||||||
massive dependency graphs that are never actually used at runtime — because
|
|
||||||
bundlers cannot tree-shake TypeScript `export namespace` bodies.
|
|
||||||
|
|
||||||
### The problem in one sentence
|
We tested three options against Bun, esbuild, Rollup (what Vite uses under the
|
||||||
|
hood), Bun's runtime, and Node's native TypeScript runner.
|
||||||
`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)
|
heavy.ts loaded?
|
||||||
|
A. namespace B. barrel C. self-reexport
|
||||||
|
Bun bundler YES YES no
|
||||||
|
esbuild YES YES no
|
||||||
|
Rollup (Vite) YES YES no
|
||||||
|
Bun runtime YES YES no
|
||||||
|
Node --experimental-strip-types SYNTAX ERROR YES no
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`export namespace`** compiles to an IIFE. Bundlers see one opaque function
|
||||||
|
call and can't analyze what's used. Node's native TS runner rejects the
|
||||||
|
syntax outright: `SyntaxError: TypeScript namespace declaration is not
|
||||||
|
supported in strip-only mode`.
|
||||||
|
- **Barrel `index.ts`** files (`export * as Foo from "./foo"` in a separate
|
||||||
|
file) force every re-exported sibling to evaluate when you import one name.
|
||||||
|
Siblings with side effects (top-level imports of SDKs, etc.) always load.
|
||||||
|
- **Self-reexport** keeps the file as plain ESM. Bundlers see static named
|
||||||
|
exports. The module is only pulled in when something actually imports from
|
||||||
|
it. There is no barrel hop, so no sibling contamination and no circular
|
||||||
|
import hazard.
|
||||||
|
|
||||||
|
Bundle overhead for the self-reexport wrapper is roughly 240 bytes per module
|
||||||
|
(`Object.defineProperty` namespace proxy). At ~100 modules that's ~24KB —
|
||||||
|
negligible for a CLI binary.
|
||||||
|
|
||||||
|
## The pattern
|
||||||
|
|
||||||
|
### Before
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/permission/arity.ts
|
||||||
|
export namespace BashArity {
|
||||||
|
export function prefix(tokens: string[]) { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### After
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// src/permission/arity.ts
|
||||||
|
export function prefix(tokens: string[]) { ... }
|
||||||
|
|
||||||
|
export * as BashArity from "./arity"
|
||||||
|
```
|
||||||
|
|
||||||
|
Consumers don't change at all:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { BashArity } from "@/permission/arity"
|
||||||
|
BashArity.prefix(...) // still works
|
||||||
|
```
|
||||||
|
|
||||||
|
Editors still auto-import `BashArity` like any named export, because the file
|
||||||
|
does have a named `BashArity` export at the module top level.
|
||||||
|
|
||||||
|
### Odd but harmless
|
||||||
|
|
||||||
|
`BashArity.BashArity.BashArity.prefix(...)` compiles and runs because the
|
||||||
|
namespace contains a re-export of itself. Nobody would write that. Not a
|
||||||
|
problem.
|
||||||
|
|
||||||
|
## Why this is different from what we tried first
|
||||||
|
|
||||||
|
An earlier pass used sibling barrel files (`index.ts` with `export * as ...`).
|
||||||
|
That turned out to be wrong for our constraints:
|
||||||
|
|
||||||
|
1. The barrel file always loads all its sibling modules when you import
|
||||||
|
through it, even if you only need one. For our CLI this is exactly the
|
||||||
|
cost we're trying to avoid.
|
||||||
|
2. Barrel + sibling imports made it very easy to accidentally create circular
|
||||||
|
imports that only surface as `ReferenceError` at runtime, not at
|
||||||
|
typecheck.
|
||||||
|
|
||||||
|
The self-reexport has none of those issues. There is no indirection. The
|
||||||
|
file and the namespace are the same unit.
|
||||||
|
|
||||||
|
## Why this matters for startup
|
||||||
|
|
||||||
|
The worst import chain in the codebase looks like:
|
||||||
|
|
||||||
|
```
|
||||||
|
src/index.ts
|
||||||
└── FormatError from src/cli/error.ts
|
└── FormatError from src/cli/error.ts
|
||||||
├── { Provider } from provider/provider.ts (1709 lines)
|
├── { Provider } from provider/provider.ts (~1700 lines)
|
||||||
│ ├── 20+ @ai-sdk/* packages
|
│ ├── 20+ @ai-sdk/* packages
|
||||||
│ ├── @aws-sdk/credential-providers
|
│ ├── @aws-sdk/credential-providers
|
||||||
│ ├── google-auth-library
|
│ ├── google-auth-library
|
||||||
│ ├── gitlab-ai-provider, venice-ai-sdk-provider
|
│ └── more
|
||||||
│ └── fuzzysort, remeda, etc.
|
├── { Config } from config/config.ts (~1600 lines)
|
||||||
├── { Config } from config/config.ts (1663 lines)
|
└── { MCP } from mcp/mcp.ts (~900 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
|
All of that currently gets pulled in just to do `.isInstance()` on a handful
|
||||||
that needs maybe 200 bytes total. This inflates the binary, increases startup
|
of error classes. The namespace IIFE shape is the main reason bundlers cannot
|
||||||
memory, and slows down initial module evaluation.
|
strip the unused parts. Self-reexport + flat ESM fixes it.
|
||||||
|
|
||||||
### 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
|
## Automation
|
||||||
|
|
||||||
The transformation is scripted. From `packages/opencode`:
|
From `packages/opencode`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun script/unwrap-namespace.ts <file> [--dry-run]
|
bun script/unwrap-namespace.ts <file> [--dry-run]
|
||||||
```
|
```
|
||||||
|
|
||||||
The script uses ast-grep for accurate AST-based namespace boundary detection
|
The script:
|
||||||
(no false matches from braces in strings/templates/comments), then:
|
|
||||||
|
|
||||||
1. Removes the `export namespace Foo {` line and its closing `}`
|
1. Uses ast-grep to locate the `export namespace Foo { ... }` block accurately.
|
||||||
2. Dedents the body by one indent level (2 spaces)
|
2. Removes the `export namespace Foo {` line and the matching closing `}`.
|
||||||
3. If the file is `index.ts`, renames it to `<name>.ts` and creates a new
|
3. Dedents the body by one indent level (2 spaces).
|
||||||
`index.ts` barrel
|
4. Rewrites `Foo.Bar` self-references inside the file to just `Bar`.
|
||||||
4. If the file is NOT `index.ts`, rewrites it in place and creates `index.ts`
|
5. Appends `export * as Foo from "./<basename>"` at the bottom of the file.
|
||||||
5. Prints the exact commands to find and rewrite import paths
|
6. Never creates a barrel `index.ts`.
|
||||||
|
|
||||||
### Walkthrough: converting a module
|
### Typical flow for one file
|
||||||
|
|
||||||
Using `Provider` as an example:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Preview what will change
|
# 1. Preview
|
||||||
bun script/unwrap-namespace.ts src/provider/provider.ts --dry-run
|
bun script/unwrap-namespace.ts src/permission/arity.ts --dry-run
|
||||||
|
|
||||||
# 2. Apply the transformation
|
# 2. Apply
|
||||||
bun script/unwrap-namespace.ts src/provider/provider.ts
|
bun script/unwrap-namespace.ts src/permission/arity.ts
|
||||||
|
|
||||||
# 3. Rewrite import paths (script prints the exact command)
|
# 3. Verify
|
||||||
rg -l 'from.*provider/provider' src/ | xargs sed -i '' 's|provider/provider"|provider"|g'
|
cd packages/opencode
|
||||||
|
bunx --bun tsgo --noEmit
|
||||||
# 4. Verify
|
bun run --conditions=browser ./src/index.ts generate
|
||||||
bun typecheck
|
bun run test <affected test files>
|
||||||
bun run test
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**What changes on disk:**
|
### Consumer imports usually don't need to change
|
||||||
|
|
||||||
```
|
Most consumers already import straight from the file, e.g.:
|
||||||
# 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
|
```ts
|
||||||
// BEFORE
|
import { BashArity } from "@/permission/arity"
|
||||||
import { Provider } from "../provider/provider"
|
import { Config } from "@/config/config"
|
||||||
|
|
||||||
// AFTER — shorter path, same Provider object
|
|
||||||
import { Provider } from "../provider"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
All property access (`Provider.Service`, `Provider.ModelNotFoundError`, etc.)
|
Because the file itself now does `export * as Foo from "./foo"`, those imports
|
||||||
stays identical.
|
keep working with zero edits.
|
||||||
|
|
||||||
### Two cases the script handles
|
The only edits needed are when a consumer was importing through a previous
|
||||||
|
barrel (`"@/config"` or `"../config"` resolving to `config/index.ts`). In
|
||||||
|
that case, repoint it at the file:
|
||||||
|
|
||||||
**Case A: file is NOT `index.ts`** (e.g. `provider/provider.ts`)
|
```ts
|
||||||
|
// before
|
||||||
|
import { Config } from "@/config"
|
||||||
|
|
||||||
- Rewrites the file in place (unwrap + dedent)
|
// after
|
||||||
- Creates `provider/index.ts` as the barrel
|
import { Config } from "@/config/config"
|
||||||
- Import paths change: `"../provider/provider"` → `"../provider"`
|
```
|
||||||
|
|
||||||
**Case B: file IS `index.ts`** (e.g. `bus/index.ts`)
|
### Dynamic imports in tests
|
||||||
|
|
||||||
- Renames `index.ts` → `bus.ts` (kebab-case of namespace name)
|
If a test did `const { Foo } = await import("../../src/x/y")`, the destructure
|
||||||
- Creates new `index.ts` as the barrel
|
still works because of the self-reexport. No change required.
|
||||||
- **No import rewrites needed** — `"@/bus"` already resolves to `bus/index.ts`
|
|
||||||
|
|
||||||
## Do I need to split errors/schemas into subfiles?
|
## Verification checklist (per PR)
|
||||||
|
|
||||||
**No.** Once you do the `export * as` conversion, the bundler can tree-shake
|
Run all of these locally before pushing:
|
||||||
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
|
```bash
|
||||||
organization** — smaller files are easier to read and review. But it's not
|
cd packages/opencode
|
||||||
required for tree-shaking. The `export * as` conversion alone is sufficient.
|
bunx --bun tsgo --noEmit
|
||||||
|
bun run --conditions=browser ./src/index.ts generate
|
||||||
|
bun run test <affected test files>
|
||||||
|
```
|
||||||
|
|
||||||
The one case where subfile splitting provides extra tree-shake value is if an
|
Also do a quick grep in `src/`, `test/`, and `script/` to make sure no
|
||||||
imported package has module-level side effects that the bundler can't prove are
|
consumer is still importing the namespace from an old barrel path that no
|
||||||
unused. In practice this is rare — most npm packages are side-effect-free — and
|
longer exports it.
|
||||||
adding `"sideEffects": []` to package.json handles the common cases.
|
|
||||||
|
|
||||||
## Scope
|
The SDK build step (`bun run --conditions=browser ./src/index.ts generate`)
|
||||||
|
evaluates every module eagerly and is the most reliable way to catch circular
|
||||||
| Metric | Count |
|
import regressions at runtime — the typechecker does not catch these.
|
||||||
| ----------------------------------------------- | --------------- |
|
|
||||||
| 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
|
## Rules for new code
|
||||||
|
|
||||||
Going forward:
|
- No new `export namespace`.
|
||||||
|
- Every module directory has a single canonical file — typically
|
||||||
|
`dir/index.ts` — with flat top-level exports and a self-reexport at the
|
||||||
|
bottom:
|
||||||
|
`export * as Foo from "."`
|
||||||
|
- Consumers import from the directory:
|
||||||
|
`import { Foo } from "@/dir"` or `import { Foo } from "../dir"`.
|
||||||
|
- No sibling barrel files. If a directory has multiple independent
|
||||||
|
namespaces, they each get their own file (e.g. `config/config.ts`,
|
||||||
|
`config/plugin.ts`) and their own self-reexport; the `index.ts` in that
|
||||||
|
directory stays minimal or does not exist.
|
||||||
|
- If a file needs a sibling, import the sibling file directly:
|
||||||
|
`import * as Sibling from "./sibling"`, not `from "."`.
|
||||||
|
|
||||||
- **No new `export namespace`**. Use a file with flat named exports and
|
### Why `dir/index.ts` + `"."` is fine for us
|
||||||
`export * as` in the barrel.
|
|
||||||
- Keep the service, layer, errors, schemas, and runtime wiring together in one
|
A single-file module (e.g. `pty/`) can live entirely in `dir/index.ts`
|
||||||
file if you want — that's fine now. The `export * as` barrel makes everything
|
with `export * as Foo from "."` at the bottom. Consumers write the
|
||||||
individually shakeable regardless of file structure.
|
short form:
|
||||||
- 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
|
```ts
|
||||||
bundler handles that.
|
import { Pty } from "@/pty"
|
||||||
|
```
|
||||||
|
|
||||||
|
This works in Bun runtime, Bun build, esbuild, and Rollup. It does NOT
|
||||||
|
work under Node's `--experimental-strip-types` runner:
|
||||||
|
|
||||||
|
```
|
||||||
|
node --experimental-strip-types entry.ts
|
||||||
|
ERR_UNSUPPORTED_DIR_IMPORT: Directory import '/.../pty' is not supported
|
||||||
|
```
|
||||||
|
|
||||||
|
Node requires an explicit file or a `package.json#exports` map for ESM.
|
||||||
|
We don't care about that target right now because the opencode CLI is
|
||||||
|
built with Bun and the web apps are built with Vite/Rollup. If we ever
|
||||||
|
want to run raw `.ts` through Node, we'll need to either use explicit
|
||||||
|
`.ts` extensions everywhere or add per-directory `package.json` exports
|
||||||
|
maps.
|
||||||
|
|
||||||
|
### When NOT to collapse to `index.ts`
|
||||||
|
|
||||||
|
Some directories contain multiple independent namespaces where
|
||||||
|
`dir/index.ts` would be misleading. Examples:
|
||||||
|
|
||||||
|
- `config/` has `Config`, `ConfigPaths`, `ConfigMarkdown`, `ConfigPlugin`,
|
||||||
|
`ConfigKeybinds`. Each lives in its own file with its own self-reexport
|
||||||
|
(`config/config.ts`, `config/plugin.ts`, etc.). Consumers import the
|
||||||
|
specific one: `import { ConfigPlugin } from "@/config/plugin"`.
|
||||||
|
- Same shape for `session/`, `server/`, etc.
|
||||||
|
|
||||||
|
Collapsing one of those into `index.ts` would mean picking a single
|
||||||
|
"canonical" namespace for the directory, which breaks the symmetry and
|
||||||
|
hides the other files.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
There are still dozens of `export namespace` files left across the codebase.
|
||||||
|
Each one is its own small PR. Do them one at a time, verified locally, rather
|
||||||
|
than batching by directory.
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -31,9 +31,9 @@ import {
|
|||||||
type Usage,
|
type Usage,
|
||||||
} from "@agentclientprotocol/sdk"
|
} from "@agentclientprotocol/sdk"
|
||||||
|
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util"
|
||||||
import { pathToFileURL } from "url"
|
import { pathToFileURL } from "url"
|
||||||
import { Filesystem } from "../util/filesystem"
|
import { Filesystem } from "../util"
|
||||||
import { Hash } from "@opencode-ai/shared/util/hash"
|
import { Hash } from "@opencode-ai/shared/util/hash"
|
||||||
import { ACPSessionManager } from "./session"
|
import { ACPSessionManager } from "./session"
|
||||||
import type { ACPConfig } from "./types"
|
import type { ACPConfig } from "./types"
|
||||||
@@ -44,11 +44,13 @@ import { AppRuntime } from "@/effect/app-runtime"
|
|||||||
import { Installation } from "@/installation"
|
import { Installation } from "@/installation"
|
||||||
import { MessageV2 } from "@/session/message-v2"
|
import { MessageV2 } from "@/session/message-v2"
|
||||||
import { Config } from "@/config"
|
import { Config } from "@/config"
|
||||||
|
import { ConfigMCP } from "@/config/mcp"
|
||||||
import { Todo } from "@/session/todo"
|
import { Todo } from "@/session/todo"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { LoadAPIKeyError } from "ai"
|
import { LoadAPIKeyError } from "ai"
|
||||||
import type { AssistantMessage, Event, OpencodeClient, SessionMessageResponse, ToolPart } from "@opencode-ai/sdk/v2"
|
import type { AssistantMessage, Event, OpencodeClient, SessionMessageResponse, ToolPart } from "@opencode-ai/sdk/v2"
|
||||||
import { applyPatch } from "diff"
|
import { applyPatch } from "diff"
|
||||||
|
import { InstallationVersion } from "@/installation/version"
|
||||||
|
|
||||||
type ModeOption = { id: string; name: string; description?: string }
|
type ModeOption = { id: string; name: string; description?: string }
|
||||||
type ModelOption = { modelId: string; name: string }
|
type ModelOption = { modelId: string; name: string }
|
||||||
@@ -176,7 +178,7 @@ export namespace ACP {
|
|||||||
})
|
})
|
||||||
for await (const event of events.stream) {
|
for await (const event of events.stream) {
|
||||||
if (this.eventAbort.signal.aborted) return
|
if (this.eventAbort.signal.aborted) return
|
||||||
const payload = (event as any)?.payload
|
const payload = event?.payload
|
||||||
if (!payload) continue
|
if (!payload) continue
|
||||||
await this.handleEvent(payload as Event).catch((error) => {
|
await this.handleEvent(payload as Event).catch((error) => {
|
||||||
log.error("failed to handle event", { error, type: payload.type })
|
log.error("failed to handle event", { error, type: payload.type })
|
||||||
@@ -242,7 +244,7 @@ export namespace ACP {
|
|||||||
const newContent = getNewContent(content, diff)
|
const newContent = getNewContent(content, diff)
|
||||||
|
|
||||||
if (newContent) {
|
if (newContent) {
|
||||||
this.connection.writeTextFile({
|
void this.connection.writeTextFile({
|
||||||
sessionId: session.id,
|
sessionId: session.id,
|
||||||
path: filepath,
|
path: filepath,
|
||||||
content: newContent,
|
content: newContent,
|
||||||
@@ -570,7 +572,7 @@ export namespace ACP {
|
|||||||
authMethods: [authMethod],
|
authMethods: [authMethod],
|
||||||
agentInfo: {
|
agentInfo: {
|
||||||
name: "OpenCode",
|
name: "OpenCode",
|
||||||
version: Installation.VERSION,
|
version: InstallationVersion,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1212,7 +1214,7 @@ export namespace ACP {
|
|||||||
description: "compact the session",
|
description: "compact the session",
|
||||||
})
|
})
|
||||||
|
|
||||||
const mcpServers: Record<string, Config.Mcp> = {}
|
const mcpServers: Record<string, ConfigMCP.Info> = {}
|
||||||
for (const server of params.mcpServers) {
|
for (const server of params.mcpServers) {
|
||||||
if ("type" in server) {
|
if ("type" in server) {
|
||||||
mcpServers[server.name] = {
|
mcpServers[server.name] = {
|
||||||
@@ -1253,7 +1255,7 @@ export namespace ACP {
|
|||||||
)
|
)
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.connection.sessionUpdate({
|
void this.connection.sessionUpdate({
|
||||||
sessionId,
|
sessionId,
|
||||||
update: {
|
update: {
|
||||||
sessionUpdate: "available_commands_update",
|
sessionUpdate: "available_commands_update",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { RequestError, type McpServer } from "@agentclientprotocol/sdk"
|
import { RequestError, type McpServer } from "@agentclientprotocol/sdk"
|
||||||
import type { ACPSessionState } from "./types"
|
import type { ACPSessionState } from "./types"
|
||||||
import { Log } from "@/util/log"
|
import { Log } from "@/util"
|
||||||
import type { OpencodeClient } from "@opencode-ai/sdk/v2"
|
import type { OpencodeClient } from "@opencode-ai/sdk/v2"
|
||||||
|
|
||||||
const log = Log.create({ service: "acp-session-manager" })
|
const log = Log.create({ service: "acp-session-manager" })
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { Provider } from "../provider"
|
|||||||
import { ModelID, ProviderID } from "../provider/schema"
|
import { ModelID, ProviderID } from "../provider/schema"
|
||||||
import { generateObject, streamObject, type ModelMessage } from "ai"
|
import { generateObject, streamObject, type ModelMessage } from "ai"
|
||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
import { Truncate } from "../tool/truncate"
|
import { Truncate } from "../tool"
|
||||||
import { Auth } from "../auth"
|
import { Auth } from "../auth"
|
||||||
import { ProviderTransform } from "../provider/transform"
|
import { ProviderTransform } from "../provider"
|
||||||
|
|
||||||
import PROMPT_GENERATE from "./generate.txt"
|
import PROMPT_GENERATE from "./generate.txt"
|
||||||
import PROMPT_COMPACTION from "./prompt/compaction.txt"
|
import PROMPT_COMPACTION from "./prompt/compaction.txt"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Effect, Exit, Layer, PubSub, Scope, Context, Stream } from "effect"
|
import { Effect, Exit, Layer, PubSub, Scope, Context, Stream } from "effect"
|
||||||
import { EffectBridge } from "@/effect"
|
import { EffectBridge } from "@/effect"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util"
|
||||||
import { BusEvent } from "./bus-event"
|
import { BusEvent } from "./bus-event"
|
||||||
import { GlobalBus } from "./global"
|
import { GlobalBus } from "./global"
|
||||||
import { InstanceState } from "@/effect"
|
import { InstanceState } from "@/effect"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Log } from "@/util/log"
|
import { Log } from "@/util"
|
||||||
import { bootstrap } from "../bootstrap"
|
import { bootstrap } from "../bootstrap"
|
||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"
|
import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Agent } from "../../agent/agent"
|
|||||||
import { Provider } from "../../provider"
|
import { Provider } from "../../provider"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import { Filesystem } from "../../util/filesystem"
|
import { Filesystem } from "../../util"
|
||||||
import matter from "gray-matter"
|
import matter from "gray-matter"
|
||||||
import { Instance } from "../../project/instance"
|
import { Instance } from "../../project/instance"
|
||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { Argv } from "yargs"
|
import type { Argv } from "yargs"
|
||||||
import { spawn } from "child_process"
|
import { spawn } from "child_process"
|
||||||
import { Database } from "../../storage/db"
|
import { Database } from "../../storage"
|
||||||
import { drizzle } from "drizzle-orm/bun-sqlite"
|
import { drizzle } from "drizzle-orm/bun-sqlite"
|
||||||
import { Database as BunDatabase } from "bun:sqlite"
|
import { Database as BunDatabase } from "bun:sqlite"
|
||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
import { JsonMigration } from "../../storage/json-migration"
|
import { JsonMigration } from "../../storage"
|
||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { errorMessage } from "../../util/error"
|
import { errorMessage } from "../../util/error"
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Provider } from "../../../provider"
|
|||||||
import { Session } from "../../../session"
|
import { Session } from "../../../session"
|
||||||
import type { MessageV2 } from "../../../session/message-v2"
|
import type { MessageV2 } from "../../../session/message-v2"
|
||||||
import { MessageID, PartID } from "../../../session/schema"
|
import { MessageID, PartID } from "../../../session/schema"
|
||||||
import { ToolRegistry } from "../../../tool/registry"
|
import { ToolRegistry } from "../../../tool"
|
||||||
import { Instance } from "../../../project/instance"
|
import { Instance } from "../../../project/instance"
|
||||||
import { Permission } from "../../../permission"
|
import { Permission } from "../../../permission"
|
||||||
import { iife } from "../../../util/iife"
|
import { iife } from "../../../util/iife"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { AppRuntime } from "../../../effect/app-runtime"
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { bootstrap } from "../../bootstrap"
|
import { bootstrap } from "../../bootstrap"
|
||||||
import { cmd } from "../cmd"
|
import { cmd } from "../cmd"
|
||||||
import { Log } from "../../../util/log"
|
import { Log } from "../../../util"
|
||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
|
|
||||||
export const LSPCommand = cmd({
|
export const LSPCommand = cmd({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { Project } from "../../../project/project"
|
import { Project } from "../../../project"
|
||||||
import { Log } from "../../../util/log"
|
import { Log } from "../../../util"
|
||||||
import { cmd } from "../cmd"
|
import { cmd } from "../cmd"
|
||||||
|
|
||||||
export const ScrapCommand = cmd({
|
export const ScrapCommand = cmd({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { exec } from "child_process"
|
import { exec } from "child_process"
|
||||||
import { Filesystem } from "../../util/filesystem"
|
import { Filesystem } from "../../util"
|
||||||
import * as prompts from "@clack/prompts"
|
import * as prompts from "@clack/prompts"
|
||||||
import { map, pipe, sortBy, values } from "remeda"
|
import { map, pipe, sortBy, values } from "remeda"
|
||||||
import { Octokit } from "@octokit/rest"
|
import { Octokit } from "@octokit/rest"
|
||||||
@@ -18,10 +18,10 @@ import type {
|
|||||||
} from "@octokit/webhooks-types"
|
} from "@octokit/webhooks-types"
|
||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
import { ModelsDev } from "../../provider/models"
|
import { ModelsDev } from "../../provider"
|
||||||
import { Instance } from "@/project/instance"
|
import { Instance } from "@/project/instance"
|
||||||
import { bootstrap } from "../bootstrap"
|
import { bootstrap } from "../bootstrap"
|
||||||
import { SessionShare } from "@/share/session"
|
import { SessionShare } from "@/share"
|
||||||
import { Session } from "../../session"
|
import { Session } from "../../session"
|
||||||
import type { SessionID } from "../../session/schema"
|
import type { SessionID } from "../../session/schema"
|
||||||
import { MessageID, PartID } from "../../session/schema"
|
import { MessageID, PartID } from "../../session/schema"
|
||||||
@@ -32,7 +32,7 @@ import { SessionPrompt } from "@/session/prompt"
|
|||||||
import { AppRuntime } from "@/effect/app-runtime"
|
import { AppRuntime } from "@/effect/app-runtime"
|
||||||
import { Git } from "@/git"
|
import { Git } from "@/git"
|
||||||
import { setTimeout as sleep } from "node:timers/promises"
|
import { setTimeout as sleep } from "node:timers/promises"
|
||||||
import { Process } from "@/util/process"
|
import { Process } from "@/util"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
|
|
||||||
type GitHubAuthor = {
|
type GitHubAuthor = {
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import { Session } from "../../session"
|
|||||||
import { MessageV2 } from "../../session/message-v2"
|
import { MessageV2 } from "../../session/message-v2"
|
||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
import { bootstrap } from "../bootstrap"
|
import { bootstrap } from "../bootstrap"
|
||||||
import { Database } from "../../storage/db"
|
import { Database } from "../../storage"
|
||||||
import { SessionTable, MessageTable, PartTable } from "../../session/session.sql"
|
import { SessionTable, MessageTable, PartTable } from "../../session/session.sql"
|
||||||
import { Instance } from "../../project/instance"
|
import { Instance } from "../../project/instance"
|
||||||
import { ShareNext } from "../../share/share-next"
|
import { ShareNext } from "../../share"
|
||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { Filesystem } from "../../util/filesystem"
|
import { Filesystem } from "../../util"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
import { AppRuntime } from "@/effect/app-runtime"
|
||||||
|
|
||||||
/** Discriminated union returned by the ShareNext API (GET /api/shares/:id/data) */
|
/** Discriminated union returned by the ShareNext API (GET /api/shares/:id/data) */
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ import { MCP } from "../../mcp"
|
|||||||
import { McpAuth } from "../../mcp/auth"
|
import { McpAuth } from "../../mcp/auth"
|
||||||
import { McpOAuthProvider } from "../../mcp/oauth-provider"
|
import { McpOAuthProvider } from "../../mcp/oauth-provider"
|
||||||
import { Config } from "../../config"
|
import { Config } from "../../config"
|
||||||
|
import { ConfigMCP } from "../../config/mcp"
|
||||||
import { Instance } from "../../project/instance"
|
import { Instance } from "../../project/instance"
|
||||||
import { Installation } from "../../installation"
|
import { Installation } from "../../installation"
|
||||||
|
import { InstallationVersion } from "../../installation/version"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Global } from "../../global"
|
import { Global } from "../../global"
|
||||||
import { modify, applyEdits } from "jsonc-parser"
|
import { modify, applyEdits } from "jsonc-parser"
|
||||||
import { Filesystem } from "../../util/filesystem"
|
import { Filesystem } from "../../util"
|
||||||
import { Bus } from "../../bus"
|
import { Bus } from "../../bus"
|
||||||
import { AppRuntime } from "../../effect/app-runtime"
|
import { AppRuntime } from "../../effect/app-runtime"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
@@ -42,7 +44,7 @@ function getAuthStatusText(status: MCP.AuthStatus): string {
|
|||||||
|
|
||||||
type McpEntry = NonNullable<Config.Info["mcp"]>[string]
|
type McpEntry = NonNullable<Config.Info["mcp"]>[string]
|
||||||
|
|
||||||
type McpConfigured = Config.Mcp
|
type McpConfigured = ConfigMCP.Info
|
||||||
function isMcpConfigured(config: McpEntry): config is McpConfigured {
|
function isMcpConfigured(config: McpEntry): config is McpConfigured {
|
||||||
return typeof config === "object" && config !== null && "type" in config
|
return typeof config === "object" && config !== null && "type" in config
|
||||||
}
|
}
|
||||||
@@ -425,7 +427,7 @@ async function resolveConfigPath(baseDir: string, global = false) {
|
|||||||
return candidates[0]
|
return candidates[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addMcpToConfig(name: string, mcpConfig: Config.Mcp, configPath: string) {
|
async function addMcpToConfig(name: string, mcpConfig: ConfigMCP.Info, configPath: string) {
|
||||||
let text = "{}"
|
let text = "{}"
|
||||||
if (await Filesystem.exists(configPath)) {
|
if (await Filesystem.exists(configPath)) {
|
||||||
text = await Filesystem.readText(configPath)
|
text = await Filesystem.readText(configPath)
|
||||||
@@ -513,7 +515,7 @@ export const McpAddCommand = cmd({
|
|||||||
})
|
})
|
||||||
if (prompts.isCancel(command)) throw new UI.CancelledError()
|
if (prompts.isCancel(command)) throw new UI.CancelledError()
|
||||||
|
|
||||||
const mcpConfig: Config.Mcp = {
|
const mcpConfig: ConfigMCP.Info = {
|
||||||
type: "local",
|
type: "local",
|
||||||
command: command.split(" "),
|
command: command.split(" "),
|
||||||
}
|
}
|
||||||
@@ -543,7 +545,7 @@ export const McpAddCommand = cmd({
|
|||||||
})
|
})
|
||||||
if (prompts.isCancel(useOAuth)) throw new UI.CancelledError()
|
if (prompts.isCancel(useOAuth)) throw new UI.CancelledError()
|
||||||
|
|
||||||
let mcpConfig: Config.Mcp
|
let mcpConfig: ConfigMCP.Info
|
||||||
|
|
||||||
if (useOAuth) {
|
if (useOAuth) {
|
||||||
const hasClientId = await prompts.confirm({
|
const hasClientId = await prompts.confirm({
|
||||||
@@ -697,7 +699,7 @@ export const McpDebugCommand = cmd({
|
|||||||
params: {
|
params: {
|
||||||
protocolVersion: "2024-11-05",
|
protocolVersion: "2024-11-05",
|
||||||
capabilities: {},
|
capabilities: {},
|
||||||
clientInfo: { name: "opencode-debug", version: Installation.VERSION },
|
clientInfo: { name: "opencode-debug", version: InstallationVersion },
|
||||||
},
|
},
|
||||||
id: 1,
|
id: 1,
|
||||||
}),
|
}),
|
||||||
@@ -746,7 +748,7 @@ export const McpDebugCommand = cmd({
|
|||||||
try {
|
try {
|
||||||
const client = new Client({
|
const client = new Client({
|
||||||
name: "opencode-debug",
|
name: "opencode-debug",
|
||||||
version: Installation.VERSION,
|
version: InstallationVersion,
|
||||||
})
|
})
|
||||||
await client.connect(transport)
|
await client.connect(transport)
|
||||||
prompts.log.success("Connection successful (already authenticated)")
|
prompts.log.success("Connection successful (already authenticated)")
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { Argv } from "yargs"
|
|||||||
import { Instance } from "../../project/instance"
|
import { Instance } from "../../project/instance"
|
||||||
import { Provider } from "../../provider"
|
import { Provider } from "../../provider"
|
||||||
import { ProviderID } from "../../provider/schema"
|
import { ProviderID } from "../../provider/schema"
|
||||||
import { ModelsDev } from "../../provider/models"
|
import { ModelsDev } from "../../provider"
|
||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { intro, log, outro, spinner } from "@clack/prompts"
|
import { intro, log, outro, spinner } from "@clack/prompts"
|
||||||
import type { Argv } from "yargs"
|
import type { Argv } from "yargs"
|
||||||
|
|
||||||
import { ConfigPaths } from "../../config/paths"
|
import { ConfigPaths } from "../../config"
|
||||||
import { Global } from "../../global"
|
import { Global } from "../../global"
|
||||||
import { installPlugin, patchPluginConfig, readPluginManifest } from "../../plugin/install"
|
import { installPlugin, patchPluginConfig, readPluginManifest } from "../../plugin/install"
|
||||||
import { resolvePluginTarget } from "../../plugin/shared"
|
import { resolvePluginTarget } from "../../plugin/shared"
|
||||||
import { Instance } from "../../project/instance"
|
import { Instance } from "../../project/instance"
|
||||||
import { errorMessage } from "../../util/error"
|
import { errorMessage } from "../../util/error"
|
||||||
import { Filesystem } from "../../util/filesystem"
|
import { Filesystem } from "../../util"
|
||||||
import { Process } from "../../util/process"
|
import { Process } from "../../util"
|
||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { cmd } from "./cmd"
|
|||||||
import { AppRuntime } from "@/effect/app-runtime"
|
import { AppRuntime } from "@/effect/app-runtime"
|
||||||
import { Git } from "@/git"
|
import { Git } from "@/git"
|
||||||
import { Instance } from "@/project/instance"
|
import { Instance } from "@/project/instance"
|
||||||
import { Process } from "@/util/process"
|
import { Process } from "@/util"
|
||||||
|
|
||||||
export const PrCommand = cmd({
|
export const PrCommand = cmd({
|
||||||
command: "pr <number>",
|
command: "pr <number>",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { AppRuntime } from "../../effect/app-runtime"
|
|||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
import * as prompts from "@clack/prompts"
|
import * as prompts from "@clack/prompts"
|
||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import { ModelsDev } from "../../provider/models"
|
import { ModelsDev } from "../../provider"
|
||||||
import { map, pipe, sortBy, values } from "remeda"
|
import { map, pipe, sortBy, values } from "remeda"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import os from "os"
|
import os from "os"
|
||||||
@@ -12,7 +12,7 @@ import { Global } from "../../global"
|
|||||||
import { Plugin } from "../../plugin"
|
import { Plugin } from "../../plugin"
|
||||||
import { Instance } from "../../project/instance"
|
import { Instance } from "../../project/instance"
|
||||||
import type { Hooks } from "@opencode-ai/plugin"
|
import type { Hooks } from "@opencode-ai/plugin"
|
||||||
import { Process } from "../../util/process"
|
import { Process } from "../../util"
|
||||||
import { text } from "node:stream/consumers"
|
import { text } from "node:stream/consumers"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
|
|
||||||
@@ -297,7 +297,9 @@ export const ProvidersLoginCommand = cmd({
|
|||||||
prompts.intro("Add credential")
|
prompts.intro("Add credential")
|
||||||
if (args.url) {
|
if (args.url) {
|
||||||
const url = args.url.replace(/\/+$/, "")
|
const url = args.url.replace(/\/+$/, "")
|
||||||
const wellknown = await fetch(`${url}/.well-known/opencode`).then((x) => x.json() as any)
|
const wellknown = (await fetch(`${url}/.well-known/opencode`).then((x) => x.json())) as {
|
||||||
|
auth: { command: string[]; env: string }
|
||||||
|
}
|
||||||
prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``)
|
prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``)
|
||||||
const proc = Process.spawn(wellknown.auth.command, {
|
const proc = Process.spawn(wellknown.auth.command, {
|
||||||
stdout: "pipe",
|
stdout: "pipe",
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ import { cmd } from "./cmd"
|
|||||||
import { Flag } from "../../flag/flag"
|
import { Flag } from "../../flag/flag"
|
||||||
import { bootstrap } from "../bootstrap"
|
import { bootstrap } from "../bootstrap"
|
||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { Filesystem } from "../../util/filesystem"
|
import { Filesystem } from "../../util"
|
||||||
import { createOpencodeClient, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2"
|
import { createOpencodeClient, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2"
|
||||||
import { Server } from "../../server/server"
|
import { Server } from "../../server/server"
|
||||||
import { Provider } from "../../provider"
|
import { Provider } from "../../provider"
|
||||||
import { Agent } from "../../agent/agent"
|
import { Agent } from "../../agent/agent"
|
||||||
import { Permission } from "../../permission"
|
import { Permission } from "../../permission"
|
||||||
import { Tool } from "../../tool/tool"
|
import { Tool } from "../../tool"
|
||||||
import { GlobTool } from "../../tool/glob"
|
import { GlobTool } from "../../tool/glob"
|
||||||
import { GrepTool } from "../../tool/grep"
|
import { GrepTool } from "../../tool/grep"
|
||||||
import { ReadTool } from "../../tool/read"
|
import { ReadTool } from "../../tool/read"
|
||||||
@@ -25,7 +25,7 @@ import { TaskTool } from "../../tool/task"
|
|||||||
import { SkillTool } from "../../tool/skill"
|
import { SkillTool } from "../../tool/skill"
|
||||||
import { BashTool } from "../../tool/bash"
|
import { BashTool } from "../../tool/bash"
|
||||||
import { TodoWriteTool } from "../../tool/todo"
|
import { TodoWriteTool } from "../../tool/todo"
|
||||||
import { Locale } from "../../util/locale"
|
import { Locale } from "../../util"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
import { AppRuntime } from "@/effect/app-runtime"
|
||||||
|
|
||||||
type ToolProps<T> = {
|
type ToolProps<T> = {
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import { Session } from "../../session"
|
|||||||
import { SessionID } from "../../session/schema"
|
import { SessionID } from "../../session/schema"
|
||||||
import { bootstrap } from "../bootstrap"
|
import { bootstrap } from "../bootstrap"
|
||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import { Locale } from "../../util/locale"
|
import { Locale } from "../../util"
|
||||||
import { Flag } from "../../flag/flag"
|
import { Flag } from "../../flag/flag"
|
||||||
import { Filesystem } from "../../util/filesystem"
|
import { Filesystem } from "../../util"
|
||||||
import { Process } from "../../util/process"
|
import { Process } from "../../util"
|
||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { which } from "../../util/which"
|
import { which } from "../../util/which"
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import type { Argv } from "yargs"
|
|||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
import { Session } from "../../session"
|
import { Session } from "../../session"
|
||||||
import { bootstrap } from "../bootstrap"
|
import { bootstrap } from "../bootstrap"
|
||||||
import { Database } from "../../storage/db"
|
import { Database } from "../../storage"
|
||||||
import { SessionTable } from "../../session/session.sql"
|
import { SessionTable } from "../../session/session.sql"
|
||||||
import { Project } from "../../project/project"
|
import { Project } from "../../project"
|
||||||
import { Instance } from "../../project/instance"
|
import { Instance } from "../../project/instance"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
import { AppRuntime } from "@/effect/app-runtime"
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { render, TimeToFirstDraw, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
|
import { render, TimeToFirstDraw, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
|
||||||
import { Clipboard } from "@tui/util/clipboard"
|
import * as Clipboard from "@tui/util/clipboard"
|
||||||
import { Selection } from "@tui/util/selection"
|
import * as Selection from "@tui/util/selection"
|
||||||
import { Terminal } from "@tui/util/terminal"
|
import * as Terminal from "@tui/util/terminal"
|
||||||
import { createCliRenderer, MouseButton, type CliRendererConfig } from "@opentui/core"
|
import { createCliRenderer, MouseButton, type CliRendererConfig } from "@opentui/core"
|
||||||
import { RouteProvider, useRoute } from "@tui/context/route"
|
import { RouteProvider, useRoute } from "@tui/context/route"
|
||||||
import {
|
import {
|
||||||
@@ -57,7 +57,7 @@ import { ArgsProvider, useArgs, type Args } from "./context/args"
|
|||||||
import open from "open"
|
import open from "open"
|
||||||
import { PromptRefProvider, usePromptRef } from "./context/prompt"
|
import { PromptRefProvider, usePromptRef } from "./context/prompt"
|
||||||
import { TuiConfigProvider, useTuiConfig } from "./context/tui-config"
|
import { TuiConfigProvider, useTuiConfig } from "./context/tui-config"
|
||||||
import { TuiConfig } from "@/config/tui"
|
import { TuiConfig } from "@/cli/cmd/tui/config/tui"
|
||||||
import { createTuiApi, TuiPluginRuntime, type RouteMap } from "./plugin"
|
import { createTuiApi, TuiPluginRuntime, type RouteMap } from "./plugin"
|
||||||
import { FormatError, FormatUnknownError } from "@/cli/error"
|
import { FormatError, FormatUnknownError } from "@/cli/error"
|
||||||
|
|
||||||
@@ -235,7 +235,10 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
|||||||
renderer,
|
renderer,
|
||||||
})
|
})
|
||||||
const [ready, setReady] = createSignal(false)
|
const [ready, setReady] = createSignal(false)
|
||||||
TuiPluginRuntime.init(api)
|
TuiPluginRuntime.init({
|
||||||
|
api,
|
||||||
|
config: tuiConfig,
|
||||||
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("Failed to load TUI plugins", error)
|
console.error("Failed to load TUI plugins", error)
|
||||||
})
|
})
|
||||||
@@ -350,7 +353,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
|||||||
if (match) {
|
if (match) {
|
||||||
continued = true
|
continued = true
|
||||||
if (args.fork) {
|
if (args.fork) {
|
||||||
sdk.client.session.fork({ sessionID: match }).then((result) => {
|
void sdk.client.session.fork({ sessionID: match }).then((result) => {
|
||||||
if (result.data?.id) {
|
if (result.data?.id) {
|
||||||
route.navigate({ type: "session", sessionID: result.data.id })
|
route.navigate({ type: "session", sessionID: result.data.id })
|
||||||
} else {
|
} else {
|
||||||
@@ -370,7 +373,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
|||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (forked || sync.status !== "complete" || !args.sessionID || !args.fork) return
|
if (forked || sync.status !== "complete" || !args.sessionID || !args.fork) return
|
||||||
forked = true
|
forked = true
|
||||||
sdk.client.session.fork({ sessionID: args.sessionID }).then((result) => {
|
void sdk.client.session.fork({ sessionID: args.sessionID }).then((result) => {
|
||||||
if (result.data?.id) {
|
if (result.data?.id) {
|
||||||
route.navigate({ type: "session", sessionID: result.data.id })
|
route.navigate({ type: "session", sessionID: result.data.id })
|
||||||
} else {
|
} else {
|
||||||
@@ -818,7 +821,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
|||||||
`Successfully updated to OpenCode v${result.data.version}. Please restart the application.`,
|
`Successfully updated to OpenCode v${result.data.version}. Please restart the application.`,
|
||||||
)
|
)
|
||||||
|
|
||||||
exit()
|
void exit()
|
||||||
})
|
})
|
||||||
|
|
||||||
const plugin = createMemo(() => {
|
const plugin = createMemo(() => {
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ import { cmd } from "../cmd"
|
|||||||
import { UI } from "@/cli/ui"
|
import { UI } from "@/cli/ui"
|
||||||
import { tui } from "./app"
|
import { tui } from "./app"
|
||||||
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
|
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
|
||||||
import { TuiConfig } from "@/config/tui"
|
import { TuiConfig } from "@/cli/cmd/tui/config/tui"
|
||||||
import { Instance } from "@/project/instance"
|
|
||||||
import { existsSync } from "fs"
|
|
||||||
|
|
||||||
export const AttachCommand = cmd({
|
export const AttachCommand = cmd({
|
||||||
command: "attach <url>",
|
command: "attach <url>",
|
||||||
@@ -66,10 +64,7 @@ export const AttachCommand = cmd({
|
|||||||
const auth = `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`
|
const auth = `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`
|
||||||
return { Authorization: auth }
|
return { Authorization: auth }
|
||||||
})()
|
})()
|
||||||
const config = await Instance.provide({
|
const config = await TuiConfig.get()
|
||||||
directory: directory && existsSync(directory) ? directory : process.cwd(),
|
|
||||||
fn: () => TuiConfig.get(),
|
|
||||||
})
|
|
||||||
await tui({
|
await tui({
|
||||||
url: args.url,
|
url: args.url,
|
||||||
config,
|
config,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export function DialogAgent() {
|
|||||||
return (
|
return (
|
||||||
<DialogSelect
|
<DialogSelect
|
||||||
title="Select agent"
|
title="Select agent"
|
||||||
current={local.agent.current().name}
|
current={local.agent.current()?.name}
|
||||||
options={options()}
|
options={options()}
|
||||||
onSelect={(option) => {
|
onSelect={(option) => {
|
||||||
local.agent.set(option.value)
|
local.agent.set(option.value)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useSync } from "@tui/context/sync"
|
|||||||
import { map, pipe, entries, sortBy } from "remeda"
|
import { map, pipe, entries, sortBy } from "remeda"
|
||||||
import { DialogSelect, type DialogSelectRef, type DialogSelectOption } from "@tui/ui/dialog-select"
|
import { DialogSelect, type DialogSelectRef, type DialogSelectOption } from "@tui/ui/dialog-select"
|
||||||
import { useTheme } from "../context/theme"
|
import { useTheme } from "../context/theme"
|
||||||
import { Keybind } from "@/util/keybind"
|
import { Keybind } from "@/util"
|
||||||
import { TextAttributes } from "@opentui/core"
|
import { TextAttributes } from "@opentui/core"
|
||||||
import { useSDK } from "@tui/context/sdk"
|
import { useSDK } from "@tui/context/sdk"
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { TextAttributes } from "@opentui/core"
|
|||||||
import type { ProviderAuthAuthorization, ProviderAuthMethod } from "@opencode-ai/sdk/v2"
|
import type { ProviderAuthAuthorization, ProviderAuthMethod } from "@opencode-ai/sdk/v2"
|
||||||
import { DialogModel } from "./dialog-model"
|
import { DialogModel } from "./dialog-model"
|
||||||
import { useKeyboard } from "@opentui/solid"
|
import { useKeyboard } from "@opentui/solid"
|
||||||
import { Clipboard } from "@tui/util/clipboard"
|
import * as Clipboard from "@tui/util/clipboard"
|
||||||
import { useToast } from "../ui/toast"
|
import { useToast } from "../ui/toast"
|
||||||
import { isConsoleManagedProvider } from "@tui/util/provider-origin"
|
import { isConsoleManagedProvider } from "@tui/util/provider-origin"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { TextAttributes } from "@opentui/core"
|
||||||
|
import { useTheme } from "../context/theme"
|
||||||
|
import { useDialog } from "../ui/dialog"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
import { For } from "solid-js"
|
||||||
|
import { useKeyboard } from "@opentui/solid"
|
||||||
|
|
||||||
|
export function DialogSessionDeleteFailed(props: {
|
||||||
|
session: string
|
||||||
|
workspace: string
|
||||||
|
onDelete?: () => boolean | void | Promise<boolean | void>
|
||||||
|
onRestore?: () => boolean | void | Promise<boolean | void>
|
||||||
|
onDone?: () => void
|
||||||
|
}) {
|
||||||
|
const dialog = useDialog()
|
||||||
|
const { theme } = useTheme()
|
||||||
|
const [store, setStore] = createStore({
|
||||||
|
active: "delete" as "delete" | "restore",
|
||||||
|
})
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{
|
||||||
|
id: "delete" as const,
|
||||||
|
title: "Delete workspace",
|
||||||
|
description: "Delete the workspace and all sessions attached to it.",
|
||||||
|
run: props.onDelete,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "restore" as const,
|
||||||
|
title: "Restore to new workspace",
|
||||||
|
description: "Try to restore this session into a new workspace.",
|
||||||
|
run: props.onRestore,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
async function confirm() {
|
||||||
|
const result = await options.find((item) => item.id === store.active)?.run?.()
|
||||||
|
if (result === false) return
|
||||||
|
props.onDone?.()
|
||||||
|
if (!props.onDone) dialog.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
useKeyboard((evt) => {
|
||||||
|
if (evt.name === "return") {
|
||||||
|
void confirm()
|
||||||
|
}
|
||||||
|
if (evt.name === "left" || evt.name === "up") {
|
||||||
|
setStore("active", "delete")
|
||||||
|
}
|
||||||
|
if (evt.name === "right" || evt.name === "down") {
|
||||||
|
setStore("active", "restore")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box paddingLeft={2} paddingRight={2} gap={1}>
|
||||||
|
<box flexDirection="row" justifyContent="space-between">
|
||||||
|
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
||||||
|
Failed to Delete Session
|
||||||
|
</text>
|
||||||
|
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
|
||||||
|
esc
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
<text fg={theme.textMuted} wrapMode="word">
|
||||||
|
{`The session "${props.session}" could not be deleted because the workspace "${props.workspace}" is not available.`}
|
||||||
|
</text>
|
||||||
|
<text fg={theme.textMuted} wrapMode="word">
|
||||||
|
Choose how you want to recover this broken workspace session.
|
||||||
|
</text>
|
||||||
|
<box flexDirection="column" paddingBottom={1} gap={1}>
|
||||||
|
<For each={options}>
|
||||||
|
{(item) => (
|
||||||
|
<box
|
||||||
|
flexDirection="column"
|
||||||
|
paddingLeft={1}
|
||||||
|
paddingRight={1}
|
||||||
|
paddingTop={1}
|
||||||
|
paddingBottom={1}
|
||||||
|
backgroundColor={item.id === store.active ? theme.primary : undefined}
|
||||||
|
onMouseUp={() => {
|
||||||
|
setStore("active", item.id)
|
||||||
|
void confirm()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<text
|
||||||
|
attributes={TextAttributes.BOLD}
|
||||||
|
fg={item.id === store.active ? theme.selectedListItemText : theme.text}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</text>
|
||||||
|
<text fg={item.id === store.active ? theme.selectedListItemText : theme.textMuted} wrapMode="word">
|
||||||
|
{item.description}
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -3,18 +3,20 @@ import { DialogSelect } from "@tui/ui/dialog-select"
|
|||||||
import { useRoute } from "@tui/context/route"
|
import { useRoute } from "@tui/context/route"
|
||||||
import { useSync } from "@tui/context/sync"
|
import { useSync } from "@tui/context/sync"
|
||||||
import { createMemo, createResource, createSignal, onMount } from "solid-js"
|
import { createMemo, createResource, createSignal, onMount } from "solid-js"
|
||||||
import { Locale } from "@/util/locale"
|
import { Locale } from "@/util"
|
||||||
import { useProject } from "@tui/context/project"
|
import { useProject } from "@tui/context/project"
|
||||||
import { useKeybind } from "../context/keybind"
|
import { useKeybind } from "../context/keybind"
|
||||||
import { useTheme } from "../context/theme"
|
import { useTheme } from "../context/theme"
|
||||||
import { useSDK } from "../context/sdk"
|
import { useSDK } from "../context/sdk"
|
||||||
import { Flag } from "@/flag/flag"
|
import { Flag } from "@/flag/flag"
|
||||||
import { DialogSessionRename } from "./dialog-session-rename"
|
import { DialogSessionRename } from "./dialog-session-rename"
|
||||||
import { Keybind } from "@/util/keybind"
|
import { Keybind } from "@/util"
|
||||||
import { createDebouncedSignal } from "../util/signal"
|
import { createDebouncedSignal } from "../util/signal"
|
||||||
import { useToast } from "../ui/toast"
|
import { useToast } from "../ui/toast"
|
||||||
import { DialogWorkspaceCreate, openWorkspaceSession } from "./dialog-workspace-create"
|
import { DialogWorkspaceCreate, openWorkspaceSession, restoreWorkspaceSession } from "./dialog-workspace-create"
|
||||||
import { Spinner } from "./spinner"
|
import { Spinner } from "./spinner"
|
||||||
|
import { errorMessage } from "@/util/error"
|
||||||
|
import { DialogSessionDeleteFailed } from "./dialog-session-delete-failed"
|
||||||
|
|
||||||
type WorkspaceStatus = "connected" | "connecting" | "disconnected" | "error"
|
type WorkspaceStatus = "connected" | "connecting" | "disconnected" | "error"
|
||||||
|
|
||||||
@@ -30,7 +32,7 @@ export function DialogSessionList() {
|
|||||||
const [toDelete, setToDelete] = createSignal<string>()
|
const [toDelete, setToDelete] = createSignal<string>()
|
||||||
const [search, setSearch] = createDebouncedSignal("", 150)
|
const [search, setSearch] = createDebouncedSignal("", 150)
|
||||||
|
|
||||||
const [searchResults] = createResource(search, async (query) => {
|
const [searchResults, { refetch }] = createResource(search, async (query) => {
|
||||||
if (!query) return undefined
|
if (!query) return undefined
|
||||||
const result = await sdk.client.session.list({ search: query, limit: 30 })
|
const result = await sdk.client.session.list({ search: query, limit: 30 })
|
||||||
return result.data ?? []
|
return result.data ?? []
|
||||||
@@ -56,6 +58,57 @@ export function DialogSessionList() {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function recover(session: NonNullable<ReturnType<typeof sessions>[number]>) {
|
||||||
|
const workspace = project.workspace.get(session.workspaceID!)
|
||||||
|
const list = () => dialog.replace(() => <DialogSessionList />)
|
||||||
|
dialog.replace(() => (
|
||||||
|
<DialogSessionDeleteFailed
|
||||||
|
session={session.title}
|
||||||
|
workspace={workspace?.name ?? session.workspaceID!}
|
||||||
|
onDone={list}
|
||||||
|
onDelete={async () => {
|
||||||
|
const current = currentSessionID()
|
||||||
|
const info = current ? sync.data.session.find((item) => item.id === current) : undefined
|
||||||
|
const result = await sdk.client.experimental.workspace.remove({ id: session.workspaceID! })
|
||||||
|
if (result.error) {
|
||||||
|
toast.show({
|
||||||
|
variant: "error",
|
||||||
|
title: "Failed to delete workspace",
|
||||||
|
message: errorMessage(result.error),
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
await project.workspace.sync()
|
||||||
|
await sync.session.refresh()
|
||||||
|
if (search()) await refetch()
|
||||||
|
if (info?.workspaceID === session.workspaceID) {
|
||||||
|
route.navigate({ type: "home" })
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}}
|
||||||
|
onRestore={() => {
|
||||||
|
dialog.replace(() => (
|
||||||
|
<DialogWorkspaceCreate
|
||||||
|
onSelect={(workspaceID) =>
|
||||||
|
restoreWorkspaceSession({
|
||||||
|
dialog,
|
||||||
|
sdk,
|
||||||
|
sync,
|
||||||
|
project,
|
||||||
|
toast,
|
||||||
|
workspaceID,
|
||||||
|
sessionID: session.id,
|
||||||
|
done: list,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
return false
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
const options = createMemo(() => {
|
const options = createMemo(() => {
|
||||||
const today = new Date().toDateString()
|
const today = new Date().toDateString()
|
||||||
return sessions()
|
return sessions()
|
||||||
@@ -145,9 +198,43 @@ export function DialogSessionList() {
|
|||||||
title: "delete",
|
title: "delete",
|
||||||
onTrigger: async (option) => {
|
onTrigger: async (option) => {
|
||||||
if (toDelete() === option.value) {
|
if (toDelete() === option.value) {
|
||||||
sdk.client.session.delete({
|
const session = sessions().find((item) => item.id === option.value)
|
||||||
sessionID: option.value,
|
const status = session?.workspaceID ? project.workspace.status(session.workspaceID) : undefined
|
||||||
})
|
|
||||||
|
try {
|
||||||
|
const result = await sdk.client.session.delete({
|
||||||
|
sessionID: option.value,
|
||||||
|
})
|
||||||
|
if (result.error) {
|
||||||
|
if (session?.workspaceID) {
|
||||||
|
recover(session)
|
||||||
|
} else {
|
||||||
|
toast.show({
|
||||||
|
variant: "error",
|
||||||
|
title: "Failed to delete session",
|
||||||
|
message: errorMessage(result.error),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setToDelete(undefined)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (session?.workspaceID) {
|
||||||
|
recover(session)
|
||||||
|
} else {
|
||||||
|
toast.show({
|
||||||
|
variant: "error",
|
||||||
|
title: "Failed to delete session",
|
||||||
|
message: errorMessage(err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setToDelete(undefined)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (status && status !== "connected") {
|
||||||
|
await sync.session.refresh()
|
||||||
|
}
|
||||||
|
if (search()) await refetch()
|
||||||
setToDelete(undefined)
|
setToDelete(undefined)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export function DialogSessionRename(props: DialogSessionRenameProps) {
|
|||||||
title="Rename Session"
|
title="Rename Session"
|
||||||
value={session()?.title}
|
value={session()?.title}
|
||||||
onConfirm={(value) => {
|
onConfirm={(value) => {
|
||||||
sdk.client.session.update({
|
void sdk.client.session.update({
|
||||||
sessionID: props.session,
|
sessionID: props.session,
|
||||||
title: value,
|
title: value,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useDialog } from "@tui/ui/dialog"
|
import { useDialog } from "@tui/ui/dialog"
|
||||||
import { DialogSelect } from "@tui/ui/dialog-select"
|
import { DialogSelect } from "@tui/ui/dialog-select"
|
||||||
import { createMemo, createSignal } from "solid-js"
|
import { createMemo, createSignal } from "solid-js"
|
||||||
import { Locale } from "@/util/locale"
|
import { Locale } from "@/util"
|
||||||
import { useTheme } from "../context/theme"
|
import { useTheme } from "../context/theme"
|
||||||
import { useKeybind } from "../context/keybind"
|
import { useKeybind } from "../context/keybind"
|
||||||
import { usePromptStash, type StashEntry } from "./prompt/stash"
|
import { usePromptStash, type StashEntry } from "./prompt/stash"
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { useSync } from "@tui/context/sync"
|
|||||||
import { useProject } from "@tui/context/project"
|
import { useProject } from "@tui/context/project"
|
||||||
import { createMemo, createSignal, onMount } from "solid-js"
|
import { createMemo, createSignal, onMount } from "solid-js"
|
||||||
import { setTimeout as sleep } from "node:timers/promises"
|
import { setTimeout as sleep } from "node:timers/promises"
|
||||||
|
import { errorData, errorMessage } from "@/util/error"
|
||||||
|
import * as Log from "@/util/log"
|
||||||
import { useSDK } from "../context/sdk"
|
import { useSDK } from "../context/sdk"
|
||||||
import { useToast } from "../ui/toast"
|
import { useToast } from "../ui/toast"
|
||||||
|
|
||||||
@@ -15,6 +17,8 @@ type Adaptor = {
|
|||||||
description: string
|
description: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const log = Log.Default.clone().tag("service", "tui-workspace")
|
||||||
|
|
||||||
function scoped(sdk: ReturnType<typeof useSDK>, sync: ReturnType<typeof useSync>, workspaceID: string) {
|
function scoped(sdk: ReturnType<typeof useSDK>, sync: ReturnType<typeof useSync>, workspaceID: string) {
|
||||||
return createOpencodeClient({
|
return createOpencodeClient({
|
||||||
baseUrl: sdk.url,
|
baseUrl: sdk.url,
|
||||||
@@ -33,8 +37,20 @@ export async function openWorkspaceSession(input: {
|
|||||||
workspaceID: string
|
workspaceID: string
|
||||||
}) {
|
}) {
|
||||||
const client = scoped(input.sdk, input.sync, input.workspaceID)
|
const client = scoped(input.sdk, input.sync, input.workspaceID)
|
||||||
|
log.info("workspace session create requested", {
|
||||||
|
workspaceID: input.workspaceID,
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log("opening!")
|
||||||
while (true) {
|
while (true) {
|
||||||
const result = await client.session.create({ workspaceID: input.workspaceID }).catch(() => undefined)
|
console.log("creating")
|
||||||
|
const result = await client.session.create({ workspace: input.workspaceID }).catch((err) => {
|
||||||
|
log.error("workspace session create request failed", {
|
||||||
|
workspaceID: input.workspaceID,
|
||||||
|
error: errorData(err),
|
||||||
|
})
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
if (!result) {
|
if (!result) {
|
||||||
input.toast.show({
|
input.toast.show({
|
||||||
message: "Failed to create workspace session",
|
message: "Failed to create workspace session",
|
||||||
@@ -42,26 +58,113 @@ export async function openWorkspaceSession(input: {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (result.response.status >= 500 && result.response.status < 600) {
|
log.info("workspace session create response", {
|
||||||
|
workspaceID: input.workspaceID,
|
||||||
|
status: result.response?.status,
|
||||||
|
sessionID: result.data?.id,
|
||||||
|
})
|
||||||
|
if (result.response?.status && result.response.status >= 500 && result.response.status < 600) {
|
||||||
|
log.warn("workspace session create retrying after server error", {
|
||||||
|
workspaceID: input.workspaceID,
|
||||||
|
status: result.response.status,
|
||||||
|
})
|
||||||
await sleep(1000)
|
await sleep(1000)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (!result.data) {
|
if (!result.data) {
|
||||||
|
log.error("workspace session create returned no data", {
|
||||||
|
workspaceID: input.workspaceID,
|
||||||
|
status: result.response?.status,
|
||||||
|
})
|
||||||
input.toast.show({
|
input.toast.show({
|
||||||
message: "Failed to create workspace session",
|
message: "Failed to create workspace session",
|
||||||
variant: "error",
|
variant: "error",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
input.route.navigate({
|
input.route.navigate({
|
||||||
type: "session",
|
type: "session",
|
||||||
sessionID: result.data.id,
|
sessionID: result.data.id,
|
||||||
})
|
})
|
||||||
|
log.info("workspace session create complete", {
|
||||||
|
workspaceID: input.workspaceID,
|
||||||
|
sessionID: result.data.id,
|
||||||
|
})
|
||||||
input.dialog.clear()
|
input.dialog.clear()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function restoreWorkspaceSession(input: {
|
||||||
|
dialog: ReturnType<typeof useDialog>
|
||||||
|
sdk: ReturnType<typeof useSDK>
|
||||||
|
sync: ReturnType<typeof useSync>
|
||||||
|
project: ReturnType<typeof useProject>
|
||||||
|
toast: ReturnType<typeof useToast>
|
||||||
|
workspaceID: string
|
||||||
|
sessionID: string
|
||||||
|
done?: () => void
|
||||||
|
}) {
|
||||||
|
log.info("session restore requested", {
|
||||||
|
workspaceID: input.workspaceID,
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
})
|
||||||
|
const result = await input.sdk.client.experimental.workspace
|
||||||
|
.sessionRestore({ id: input.workspaceID, sessionID: input.sessionID })
|
||||||
|
.catch((err) => {
|
||||||
|
log.error("session restore request failed", {
|
||||||
|
workspaceID: input.workspaceID,
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
error: errorData(err),
|
||||||
|
})
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
if (!result?.data) {
|
||||||
|
log.error("session restore failed", {
|
||||||
|
workspaceID: input.workspaceID,
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
status: result?.response?.status,
|
||||||
|
error: result?.error ? errorData(result.error) : undefined,
|
||||||
|
})
|
||||||
|
input.toast.show({
|
||||||
|
message: `Failed to restore session: ${errorMessage(result?.error ?? "no response")}`,
|
||||||
|
variant: "error",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("session restore response", {
|
||||||
|
workspaceID: input.workspaceID,
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
status: result.response?.status,
|
||||||
|
total: result.data.total,
|
||||||
|
})
|
||||||
|
|
||||||
|
await Promise.all([input.project.workspace.sync(), input.sync.session.refresh()]).catch((err) => {
|
||||||
|
log.error("session restore refresh failed", {
|
||||||
|
workspaceID: input.workspaceID,
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
error: errorData(err),
|
||||||
|
})
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
|
||||||
|
log.info("session restore complete", {
|
||||||
|
workspaceID: input.workspaceID,
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
total: result.data.total,
|
||||||
|
})
|
||||||
|
|
||||||
|
input.toast.show({
|
||||||
|
message: "Session restored into the new workspace",
|
||||||
|
variant: "success",
|
||||||
|
})
|
||||||
|
input.done?.()
|
||||||
|
if (input.done) return
|
||||||
|
input.dialog.clear()
|
||||||
|
}
|
||||||
|
|
||||||
export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) => Promise<void> | void }) {
|
export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) => Promise<void> | void }) {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
@@ -123,18 +226,43 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) =
|
|||||||
const create = async (type: string) => {
|
const create = async (type: string) => {
|
||||||
if (creating()) return
|
if (creating()) return
|
||||||
setCreating(type)
|
setCreating(type)
|
||||||
|
log.info("workspace create requested", {
|
||||||
|
type,
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await sdk.client.experimental.workspace.create({ type, branch: null }).catch((err) => {
|
||||||
|
log.error("workspace create request failed", {
|
||||||
|
type,
|
||||||
|
error: errorData(err),
|
||||||
|
})
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
|
||||||
const result = await sdk.client.experimental.workspace.create({ type, branch: null }).catch(() => undefined)
|
|
||||||
const workspace = result?.data
|
const workspace = result?.data
|
||||||
if (!workspace) {
|
if (!workspace) {
|
||||||
setCreating(undefined)
|
setCreating(undefined)
|
||||||
|
log.error("workspace create failed", {
|
||||||
|
type,
|
||||||
|
status: result?.response.status,
|
||||||
|
error: result?.error ? errorData(result.error) : undefined,
|
||||||
|
})
|
||||||
toast.show({
|
toast.show({
|
||||||
message: "Failed to create workspace",
|
message: `Failed to create workspace: ${errorMessage(result?.error ?? "no response")}`,
|
||||||
variant: "error",
|
variant: "error",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
log.info("workspace create response", {
|
||||||
|
type,
|
||||||
|
workspaceID: workspace.id,
|
||||||
|
status: result.response?.status,
|
||||||
|
})
|
||||||
|
|
||||||
await project.workspace.sync()
|
await project.workspace.sync()
|
||||||
|
log.info("workspace create synced", {
|
||||||
|
type,
|
||||||
|
workspaceID: workspace.id,
|
||||||
|
})
|
||||||
await props.onSelect(workspace.id)
|
await props.onSelect(workspace.id)
|
||||||
setCreating(undefined)
|
setCreating(undefined)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { TextAttributes } from "@opentui/core"
|
import { TextAttributes } from "@opentui/core"
|
||||||
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
|
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
|
||||||
import { Clipboard } from "@tui/util/clipboard"
|
import * as Clipboard from "@tui/util/clipboard"
|
||||||
import { createSignal } from "solid-js"
|
import { createSignal } from "solid-js"
|
||||||
import { Installation } from "@/installation"
|
import { InstallationVersion } from "@/installation/version"
|
||||||
import { win32FlushInputBuffer } from "../win32"
|
import { win32FlushInputBuffer } from "../win32"
|
||||||
import { getScrollAcceleration } from "../util/scroll"
|
import { getScrollAcceleration } from "../util/scroll"
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ export function ErrorComponent(props: {
|
|||||||
|
|
||||||
useKeyboard((evt) => {
|
useKeyboard((evt) => {
|
||||||
if (evt.ctrl && evt.name === "c") {
|
if (evt.ctrl && evt.name === "c") {
|
||||||
handleExit()
|
void handleExit()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const [copied, setCopied] = createSignal(false)
|
const [copied, setCopied] = createSignal(false)
|
||||||
@@ -53,10 +53,10 @@ export function ErrorComponent(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
issueURL.searchParams.set("opencode-version", Installation.VERSION)
|
issueURL.searchParams.set("opencode-version", InstallationVersion)
|
||||||
|
|
||||||
const copyIssueURL = () => {
|
const copyIssueURL = () => {
|
||||||
Clipboard.copy(issueURL.toString()).then(() => {
|
void Clipboard.copy(issueURL.toString()).then(() => {
|
||||||
setCopied(true)
|
setCopied(true)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { BoxRenderable, MouseButton, MouseEvent, RGBA, TextAttributes } from "@opentui/core"
|
import { BoxRenderable, MouseButton, MouseEvent, RGBA, TextAttributes } from "@opentui/core"
|
||||||
import { For, createMemo, createSignal, onCleanup, type JSX } from "solid-js"
|
import { For, createMemo, createSignal, onCleanup, type JSX } from "solid-js"
|
||||||
import { useTheme, tint } from "@tui/context/theme"
|
import { useTheme, tint } from "@tui/context/theme"
|
||||||
import { Sound } from "@tui/util/sound"
|
import * as Sound from "@tui/util/sound"
|
||||||
import { logo } from "@/cli/logo"
|
import { logo } from "@/cli/logo"
|
||||||
|
|
||||||
// Shadow markers (rendered chars in parens):
|
// Shadow markers (rendered chars in parens):
|
||||||
@@ -520,7 +520,7 @@ export function Logo() {
|
|||||||
const shadow = tint(theme.background, ink, 0.25)
|
const shadow = tint(theme.background, ink, 0.25)
|
||||||
const attrs = bold ? TextAttributes.BOLD : undefined
|
const attrs = bold ? TextAttributes.BOLD : undefined
|
||||||
|
|
||||||
return [...line].map((char, i) => {
|
return Array.from(line).map((char, i) => {
|
||||||
const h = field(off + i, y, frame)
|
const h = field(off + i, y, frame)
|
||||||
const n = wave(off + i, y, frame, lit(char)) + h
|
const n = wave(off + i, y, frame, lit(char)) + h
|
||||||
const s = wave(off + i, y, dusk, false) + h
|
const s = wave(off + i, y, dusk, false) + h
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { useTheme, selectedForeground } from "@tui/context/theme"
|
|||||||
import { SplitBorder } from "@tui/component/border"
|
import { SplitBorder } from "@tui/component/border"
|
||||||
import { useCommandDialog } from "@tui/component/dialog-command"
|
import { useCommandDialog } from "@tui/component/dialog-command"
|
||||||
import { useTerminalDimensions } from "@opentui/solid"
|
import { useTerminalDimensions } from "@opentui/solid"
|
||||||
import { Locale } from "@/util/locale"
|
import { Locale } from "@/util"
|
||||||
import type { PromptInfo } from "./history"
|
import type { PromptInfo } from "./history"
|
||||||
import { useFrecency } from "./frecency"
|
import { useFrecency } from "./frecency"
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user