Compare commits

..

52 Commits

Author SHA1 Message Date
Kit Langton 2bb1fc34a5 fix(cli): drop removed workspace filter from session export 2026-08-06 19:29:21 -04:00
Kit Langton 9a5b015126 Merge remote-tracking branch 'origin/v2' into workspace-plan
# ------------------------ >8 ------------------------
# Do not modify or remove the line above.
# Everything below it will be ignored.
#
# Conflicts:
#	packages/core/test/session-create.test.ts
2026-08-06 19:27:34 -04:00
Kit Langton 38e7d2302c refactor(core): trim hosted read round trips and share graph utilities 2026-08-06 18:12:44 -04:00
Kit Langton e4c603d631 test(core): guard hosted graph against host primitives 2026-08-06 18:00:07 -04:00
Kit Langton eaa196d574 fix(core): route hosted read tool through the workspace environment 2026-08-06 17:43:44 -04:00
Kit Langton d7ecb3af37 refactor(core): promote slash helper to FSUtil 2026-08-06 17:11:31 -04:00
Kit Langton 13a415b68a refactor(core): own environment error wrapping in WorkspaceEnvironment 2026-08-06 17:09:46 -04:00
Kit Langton dee3f88737 refactor(core): dedupe shell cwd checks and search plumbing 2026-08-06 17:08:30 -04:00
Kit Langton 39dab7199f refactor(core): share FileSystem entry operations across backends 2026-08-06 17:03:56 -04:00
Kit Langton 4729a3f9ae refactor(core): share LocationMutation ancestor walk 2026-08-06 17:00:56 -04:00
Kit Langton 9f05331ac4 refactor(core): share FileMutation orchestration via backend seam 2026-08-06 16:58:56 -04:00
Kit Langton 0b3732aa7c refactor(core): unify FileMutation write and add move 2026-08-06 16:41:33 -04:00
Kit Langton 248bfb64d4 feat(core): enable hosted search tools 2026-08-06 16:20:47 -04:00
Kit Langton cbec0989e0 Merge branch 'v2' into workspace-plan 2026-08-06 15:12:18 -04:00
Kit Langton 9d284040de fix(core): tighten workspace mutation boundaries 2026-08-06 12:43:49 -04:00
Kit Langton 4e03ad95a7 chore: regenerate workspace lockfile 2026-08-06 12:05:39 -04:00
Kit Langton 8d70adab50 chore(app): drop unrelated branch changes 2026-08-06 12:04:29 -04:00
Kit Langton 8b8e2d97dc Merge remote-tracking branch 'origin/v2' into workspace-plan 2026-08-06 12:04:19 -04:00
Kit Langton 716258d97a fix(core): preserve mutation boundary semantics 2026-08-06 11:57:46 -04:00
Kit Langton d6ab618155 refactor(core): make tools hosting-blind 2026-08-06 11:45:50 -04:00
Kit Langton 1fa3d9dd12 docs: target architecture for hosting-blind tools 2026-08-06 11:23:32 -04:00
Kit Langton 92442bb7ff chore: merge origin/v2 2026-08-06 11:12:37 -04:00
Kit Langton 64c82bedec chore: format workspace files 2026-08-05 22:40:59 -04:00
Kit Langton 6cbc71a2eb feat(core): hosted apply_patch through the workspace environment 2026-08-05 22:38:35 -04:00
Kit Langton 756ac88039 test(core): scripted-model session e2e on hosted locations 2026-08-05 22:27:56 -04:00
Kit Langton a4eeaefd5b fix(core): stat shell workdir through the workspace environment 2026-08-05 22:27:55 -04:00
Kit Langton 6e0066247c fix(server): route hosted sessions through workspace location 2026-08-05 21:48:28 -04:00
Kit Langton 65ac161373 test(server): run hosted location graph live on modal 2026-08-05 21:43:07 -04:00
Kit Langton 9c0883054b feat(server): add modal workspace driver 2026-08-05 21:39:09 -04:00
Kit Langton 79eed3d3a0 refactor(core): apply simplify review to hosted workspace slice 2026-08-05 21:29:19 -04:00
Kit Langton be74fad688 test(core): prove hosted workspace end to end with directory-backed fake driver 2026-08-05 21:15:16 -04:00
Kit Langton 5d93a35a1d feat(core): exclude host-search tools from hosted locations 2026-08-05 21:08:26 -04:00
Kit Langton 395eb8a9c8 feat(core): run hosted shell commands through workspace environment 2026-08-05 21:06:48 -04:00
Kit Langton 14e7b547c5 feat(core): route hosted mutation resolution and writes through workspace environment 2026-08-05 21:03:24 -04:00
Kit Langton 97a5497664 feat(core): serve hosted filesystem reads through workspace environment 2026-08-05 20:58:39 -04:00
Kit Langton e544b8927b feat(core): branch hosted location graph construction 2026-08-05 20:42:29 -04:00
Kit Langton 44481e1ecc docs: record hosted project identity decisions 2026-08-05 20:16:26 -04:00
Kit Langton 5168652082 feat(core): admit hosted sessions without host project discovery 2026-08-05 19:33:27 -04:00
Kit Langton 977a35c222 refactor(core): use struct for workspace info 2026-08-05 19:28:57 -04:00
Kit Langton 5d64dc8bfd feat(core): add workspace domain service 2026-08-05 19:25:06 -04:00
Kit Langton 479821e621 feat(core): add workspace driver seam, environment contract, and table 2026-08-05 19:23:09 -04:00
Kit Langton 5ce6626a44 Merge remote-tracking branch 'origin/remove-old-workspaces' into workspace-plan 2026-08-05 19:06:22 -04:00
Kit Langton eef60bcd64 docs: add workspaces.get metadata read 2026-08-05 18:55:54 -04:00
Kit Langton f840b3e3a2 fix: complete legacy workspace removal 2026-08-05 18:52:44 -04:00
Kit Langton b1abfd4bd9 docs: require explicit workspace provider 2026-08-05 18:51:40 -04:00
Kit Langton 71251dd246 docs: tighten workspaces proposal to code-first form 2026-08-05 18:35:12 -04:00
Kit Langton 8461226ccd docs: add driver registration example and drop ripgrep seam field 2026-08-05 18:35:12 -04:00
Kit Langton e1b863dcb3 docs: name environment files interface and explain ripgrep seam 2026-08-05 18:35:12 -04:00
Kit Langton 56405af7d6 docs: propose workspace domain model and driver seam 2026-08-05 18:35:11 -04:00
Kit Langton 05c3866788 fix(cli): remove legacy workspace request fields 2026-08-05 18:33:39 -04:00
Kit Langton 3eca11a7da chore(client): regenerate workspace-free API 2026-08-05 18:32:05 -04:00
Kit Langton 794283057a refactor: remove legacy workspace control plane 2026-08-05 18:29:26 -04:00
313 changed files with 12567 additions and 6635 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

+40 -1
View File
@@ -395,7 +395,6 @@
"ignore": "7.0.5", "ignore": "7.0.5",
"immer": "11.1.4", "immer": "11.1.4",
"jsonc-parser": "3.3.1", "jsonc-parser": "3.3.1",
"mime-types": "3.0.2",
"tree-sitter-bash": "0.25.0", "tree-sitter-bash": "0.25.0",
"tree-sitter-powershell": "0.25.10", "tree-sitter-powershell": "0.25.10",
"turndown": "7.2.0", "turndown": "7.2.0",
@@ -679,8 +678,10 @@
"@opencode-ai/util": "workspace:*", "@opencode-ai/util": "workspace:*",
"drizzle-orm": "catalog:", "drizzle-orm": "catalog:",
"effect": "catalog:", "effect": "catalog:",
"modal": "0.9.0",
}, },
"devDependencies": { "devDependencies": {
"@opencode-ai/ai": "workspace:*",
"@tsconfig/bun": "catalog:", "@tsconfig/bun": "catalog:",
"@types/bun": "catalog:", "@types/bun": "catalog:",
"@typescript/native-preview": "catalog:", "@typescript/native-preview": "catalog:",
@@ -1512,6 +1513,18 @@
"@capsizecss/unpack": ["@capsizecss/unpack@2.4.0", "", { "dependencies": { "blob-to-buffer": "^1.2.8", "cross-fetch": "^3.0.4", "fontkit": "^2.0.2" } }, "sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q=="], "@capsizecss/unpack": ["@capsizecss/unpack@2.4.0", "", { "dependencies": { "blob-to-buffer": "^1.2.8", "cross-fetch": "^3.0.4", "fontkit": "^2.0.2" } }, "sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q=="],
"@cbor-extract/cbor-extract-darwin-arm64": ["@cbor-extract/cbor-extract-darwin-arm64@2.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZKZ/F8US7JR92J4DMct6cLW/Y66o2K576+zjlEN/MevH70bFIsB10wkZEQPLzl2oNh2SMGy55xpJ9JoBRl5DOA=="],
"@cbor-extract/cbor-extract-darwin-x64": ["@cbor-extract/cbor-extract-darwin-x64@2.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-32b1mgc+P61Js+KW9VZv/c+xRw5EfmOcPx990JbCBSkYJFY0l25VinvyyWfl+3KjibQmAcYwmyzKF9J4DyKP/Q=="],
"@cbor-extract/cbor-extract-linux-arm": ["@cbor-extract/cbor-extract-linux-arm@2.2.2", "", { "os": "linux", "cpu": "arm" }, "sha512-tNg0za41TpQfkhWjptD+0gSD2fggMiDCSacuIeELyb2xZhr7PrhPe5h66Jc67B/5dmpIhI2QOUtv4SBsricyYQ=="],
"@cbor-extract/cbor-extract-linux-arm64": ["@cbor-extract/cbor-extract-linux-arm64@2.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-wfqgzqCAy/Vn8i6WVIh7qZd0DdBFaWBjPdB6ma+Wihcjv0gHqD/mw3ouVv7kbbUNrab6dKEx/w3xQZEdeXIlzg=="],
"@cbor-extract/cbor-extract-linux-x64": ["@cbor-extract/cbor-extract-linux-x64@2.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-rpiLnVEsqtPJ+mXTdx1rfz4RtUGYIUg2rUAZgd1KjiC1SehYUSkJN7Yh+aVfSjvCGtVP0/bfkQkXpPXKbmSUaA=="],
"@cbor-extract/cbor-extract-win32-x64": ["@cbor-extract/cbor-extract-win32-x64@2.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA=="],
"@chevrotain/types": ["@chevrotain/types@11.1.2", "", {}, "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw=="], "@chevrotain/types": ["@chevrotain/types@11.1.2", "", {}, "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw=="],
"@clack/core": ["@clack/core@1.0.0-alpha.1", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-rFbCU83JnN7l3W1nfgCqqme4ZZvTTgsiKQ6FM0l+r0P+o2eJpExcocBUWUIwnDzL76Aca9VhUdWmB2MbUv+Qyg=="], "@clack/core": ["@clack/core@1.0.0-alpha.1", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-rFbCU83JnN7l3W1nfgCqqme4ZZvTTgsiKQ6FM0l+r0P+o2eJpExcocBUWUIwnDzL76Aca9VhUdWmB2MbUv+Qyg=="],
@@ -1720,6 +1733,10 @@
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="], "@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
"@grpc/grpc-js": ["@grpc/grpc-js@1.14.4", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ=="],
"@grpc/proto-loader": ["@grpc/proto-loader@0.8.1", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg=="],
"@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="], "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="],
"@hono/node-server": ["@hono/node-server@1.19.15", "", { "peerDependencies": { "hono": "^4" } }, "sha512-Za2ai6TLdKjUvnur+eenO6nuYYipVAEhyCAdaV8IRvmU9kK8crOZUSYvIXn72E4f8fJqyAbpcJuTsYYmZp9Deg=="], "@hono/node-server": ["@hono/node-server@1.19.15", "", { "peerDependencies": { "hono": "^4" } }, "sha512-Za2ai6TLdKjUvnur+eenO6nuYYipVAEhyCAdaV8IRvmU9kK8crOZUSYvIXn72E4f8fJqyAbpcJuTsYYmZp9Deg=="],
@@ -1814,6 +1831,8 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="],
"@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="], "@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="],
"@jsx-email/all": ["@jsx-email/all@2.2.3", "", { "dependencies": { "@jsx-email/body": "1.0.2", "@jsx-email/button": "1.0.4", "@jsx-email/column": "1.0.3", "@jsx-email/container": "1.0.2", "@jsx-email/font": "1.0.3", "@jsx-email/head": "1.0.2", "@jsx-email/heading": "1.0.2", "@jsx-email/hr": "1.0.2", "@jsx-email/html": "1.0.2", "@jsx-email/img": "1.0.2", "@jsx-email/link": "1.0.2", "@jsx-email/markdown": "2.0.4", "@jsx-email/preview": "1.0.2", "@jsx-email/render": "1.1.1", "@jsx-email/row": "1.0.2", "@jsx-email/section": "1.0.2", "@jsx-email/tailwind": "2.4.4", "@jsx-email/text": "1.0.2" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-OBvLe/hVSQc0LlMSTJnkjFoqs3bmxcC4zpy/5pT5agPCSKMvAKQjzmsc2xJ2wO73jSpRV1K/g38GmvdCfrhSoQ=="], "@jsx-email/all": ["@jsx-email/all@2.2.3", "", { "dependencies": { "@jsx-email/body": "1.0.2", "@jsx-email/button": "1.0.4", "@jsx-email/column": "1.0.3", "@jsx-email/container": "1.0.2", "@jsx-email/font": "1.0.3", "@jsx-email/head": "1.0.2", "@jsx-email/heading": "1.0.2", "@jsx-email/hr": "1.0.2", "@jsx-email/html": "1.0.2", "@jsx-email/img": "1.0.2", "@jsx-email/link": "1.0.2", "@jsx-email/markdown": "2.0.4", "@jsx-email/preview": "1.0.2", "@jsx-email/render": "1.1.1", "@jsx-email/row": "1.0.2", "@jsx-email/section": "1.0.2", "@jsx-email/tailwind": "2.4.4", "@jsx-email/text": "1.0.2" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-OBvLe/hVSQc0LlMSTJnkjFoqs3bmxcC4zpy/5pT5agPCSKMvAKQjzmsc2xJ2wO73jSpRV1K/g38GmvdCfrhSoQ=="],
@@ -3298,6 +3317,8 @@
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
"abort-controller-x": ["abort-controller-x@0.5.0", "", {}, "sha512-yTt9CI0x+nRfX6BFMenEGP8ooPvErGH6AbFz20C2IeOLIlDsrw/VHpgne3GsCEuTA410IiFiaLVFKmgM4bKEPQ=="],
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
@@ -3538,6 +3559,10 @@
"caniuse-lite": ["caniuse-lite@1.0.30001806", "", {}, "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw=="], "caniuse-lite": ["caniuse-lite@1.0.30001806", "", {}, "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw=="],
"cbor-extract": ["cbor-extract@2.2.2", "", { "dependencies": { "node-gyp-build-optional-packages": "5.1.1" }, "optionalDependencies": { "@cbor-extract/cbor-extract-darwin-arm64": "2.2.2", "@cbor-extract/cbor-extract-darwin-x64": "2.2.2", "@cbor-extract/cbor-extract-linux-arm": "2.2.2", "@cbor-extract/cbor-extract-linux-arm64": "2.2.2", "@cbor-extract/cbor-extract-linux-x64": "2.2.2", "@cbor-extract/cbor-extract-win32-x64": "2.2.2" }, "bin": { "download-cbor-prebuilds": "bin/download-prebuilds.js" } }, "sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng=="],
"cbor-x": ["cbor-x@1.6.5", "", { "optionalDependencies": { "cbor-extract": "^2.2.2" } }, "sha512-yO64CxnSh6kp+pHNRK9IfwnMvCB+c8HvmUjQY/9l9YRF0/cAPka/tUHLwS64QqUpFCq3/OtbKziVJYXH2EaRig=="],
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
"chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
@@ -4570,6 +4595,8 @@
"lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="], "lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="],
"lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="],
"lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="], "lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="],
"lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
@@ -4796,6 +4823,8 @@
"mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
"modal": ["modal@0.9.0", "", { "dependencies": { "cbor-x": "^1.6.0", "long": "^5.3.1", "nice-grpc": "^2.1.12", "protobufjs": "^7.5.0", "smol-toml": "^1.3.3", "uuid": "^11.1.0" } }, "sha512-kCXcdJkhbJorf/q/6T9Wdlg6in9JmRnCNQnV6rVBMyeqNV/iXI6BYk4IzY4cvZ6dbauNeDMjk/Q08cbxvoIaXg=="],
"morphdom": ["morphdom@2.7.8", "", {}, "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg=="], "morphdom": ["morphdom@2.7.8", "", {}, "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg=="],
"motion": ["motion@12.34.5", "", { "dependencies": { "framer-motion": "^12.34.5", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-N06NLJ9IeBHeielRqIvYvjPfXuRdyTxa+9++BgpGa+hY2D7TcMkI6QzV3jaRuv0aZRXgMa7cPy9YcBUBisPzAQ=="], "motion": ["motion@12.34.5", "", { "dependencies": { "framer-motion": "^12.34.5", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-N06NLJ9IeBHeielRqIvYvjPfXuRdyTxa+9++BgpGa+hY2D7TcMkI6QzV3jaRuv0aZRXgMa7cPy9YcBUBisPzAQ=="],
@@ -4834,6 +4863,10 @@
"nf3": ["nf3@0.1.12", "", {}, "sha512-qbMXT7RTGh74MYWPeqTIED8nDW70NXOULVHpdWcdZ7IVHVnAsMV9fNugSNnvooipDc1FMOzpis7T9nXJEbJhvQ=="], "nf3": ["nf3@0.1.12", "", {}, "sha512-qbMXT7RTGh74MYWPeqTIED8nDW70NXOULVHpdWcdZ7IVHVnAsMV9fNugSNnvooipDc1FMOzpis7T9nXJEbJhvQ=="],
"nice-grpc": ["nice-grpc@2.1.16", "", { "dependencies": { "@grpc/grpc-js": "^1.14.0", "abort-controller-x": "^0.5.0", "nice-grpc-common": "^2.0.3" } }, "sha512-Cl3Pn00212Hl8/U6bpgMxmhZj5lyv3nWoJov4cd3FjWarktrMHP4DNvSjCnDwkMWYx4W1tyscEia4JX6Y4GVCQ=="],
"nice-grpc-common": ["nice-grpc-common@2.0.3", "", { "dependencies": { "ts-error": "^1.0.6" } }, "sha512-MEhnD3JMah0mgyivpb9hpRDbOBuXBxI/TVO+OK1h6rC97WM42HsPMR+zzRNQ0C5BqYJTw1nyWiQRD0DucO+pjQ=="],
"nitro": ["nitro@3.0.1-alpha.1", "", { "dependencies": { "consola": "^3.4.2", "crossws": "^0.4.1", "db0": "^0.3.4", "h3": "2.0.1-rc.5", "jiti": "^2.6.1", "nf3": "^0.1.10", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "oxc-minify": "^0.96.0", "oxc-transform": "^0.96.0", "srvx": "^0.9.5", "undici": "^7.16.0", "unenv": "^2.0.0-rc.24", "unstorage": "^2.0.0-alpha.4" }, "peerDependencies": { "rolldown": "*", "rollup": "^4", "vite": "^7", "xml2js": "^0.6.2" }, "optionalPeers": ["rolldown", "rollup", "vite", "xml2js"], "bin": { "nitro": "dist/cli/index.mjs" } }, "sha512-U4AxIsXxdkxzkFrK0XAw0e5Qbojk8jQ50MjjRBtBakC4HurTtQoiZvF+lSe382jhuQZCfAyywGWOFa9QzXLFaw=="], "nitro": ["nitro@3.0.1-alpha.1", "", { "dependencies": { "consola": "^3.4.2", "crossws": "^0.4.1", "db0": "^0.3.4", "h3": "2.0.1-rc.5", "jiti": "^2.6.1", "nf3": "^0.1.10", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "oxc-minify": "^0.96.0", "oxc-transform": "^0.96.0", "srvx": "^0.9.5", "undici": "^7.16.0", "unenv": "^2.0.0-rc.24", "unstorage": "^2.0.0-alpha.4" }, "peerDependencies": { "rolldown": "*", "rollup": "^4", "vite": "^7", "xml2js": "^0.6.2" }, "optionalPeers": ["rolldown", "rollup", "vite", "xml2js"], "bin": { "nitro": "dist/cli/index.mjs" } }, "sha512-U4AxIsXxdkxzkFrK0XAw0e5Qbojk8jQ50MjjRBtBakC4HurTtQoiZvF+lSe382jhuQZCfAyywGWOFa9QzXLFaw=="],
"nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="], "nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="],
@@ -5632,6 +5665,8 @@
"ts-dedent": ["ts-dedent@2.3.0", "", {}, "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg=="], "ts-dedent": ["ts-dedent@2.3.0", "", {}, "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg=="],
"ts-error": ["ts-error@1.0.6", "", {}, "sha512-tLJxacIQUM82IR7JO1UUkKlYuUTmoY9HBJAmNWFzheSlDS5SPMcNIepejHJa4BpPQLAcbRhRf3GDJzyj6rbKvA=="],
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
"tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="], "tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="],
@@ -6590,6 +6625,8 @@
"builder-util/js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="], "builder-util/js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="],
"cbor-extract/node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.1.1", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-test": "build-test.js", "node-gyp-build-optional-packages-optional": "optional.js" } }, "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw=="],
"cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
@@ -6734,6 +6771,8 @@
"minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"modal/uuid": ["uuid@11.1.1", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ=="],
"motion/framer-motion": ["framer-motion@12.42.2", "", { "dependencies": { "motion-dom": "^12.42.2", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw=="], "motion/framer-motion": ["framer-motion@12.42.2", "", { "dependencies": { "motion-dom": "^12.42.2", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw=="],
"nitro/h3": ["h3@2.0.1-rc.5", "", { "dependencies": { "rou3": "^0.7.9", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-qkohAzCab0nLzXNm78tBjZDvtKMTmtygS8BJLT3VPczAQofdqlFXDPkXdLMJN4r05+xqneG8snZJ0HgkERCZTg=="], "nitro/h3": ["h3@2.0.1-rc.5", "", { "dependencies": { "rou3": "^0.7.9", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-qkohAzCab0nLzXNm78tBjZDvtKMTmtygS8BJLT3VPczAQofdqlFXDPkXdLMJN4r05+xqneG8snZJ0HgkERCZTg=="],
+3 -4
View File
@@ -368,12 +368,11 @@ Other provider exports listed above remain direct facades until they explicitly
## Provider options & HTTP overlays ## Provider options & HTTP overlays
Request options in order of stability: Three escape hatches in order of stability:
1. **`generation`** — portable knobs (`maxTokens`, `temperature`, `topP`, `topK`, penalties, seed, stop). 1. **`generation`** — portable knobs (`maxTokens`, `temperature`, `topP`, `topK`, penalties, seed, stop).
2. **`promptCacheKey`** — stable cache affinity lowered by every protocol that supports it. 2. **`providerOptions: { <provider>: {...} }`** — typed-at-the-facade provider-specific knobs (OpenAI `promptCacheKey`, Anthropic `thinking`, Gemini `thinkingConfig`, OpenRouter routing).
3. **`providerOptions: { <provider>: {...} }`** — typed-at-the-facade provider-specific knobs (OpenAI `store`, Anthropic `thinking`, Gemini `thinkingConfig`, OpenRouter routing). 3. **`http: { body, headers, query }`** — last-resort serializable overlays merged into the final HTTP request. Reach for this only when a stable typed path doesn't yet exist.
4. **`http: { body, headers, query }`** — last-resort serializable overlays merged into the final HTTP request. Reach for this only when a stable typed path doesn't yet exist.
Route/provider defaults are overridden by request-level values for each axis. Route/provider defaults are overridden by request-level values for each axis.
+5 -4
View File
@@ -33,10 +33,9 @@ const model = OpenAI.configure({
// //
// - `generation`: common controls such as max tokens, temperature, topP/topK, // - `generation`: common controls such as max tokens, temperature, topP/topK,
// penalties, seed, and stop sequences. // penalties, seed, and stop sequences.
// - `promptCacheKey`: stable cache affinity for protocols that support it.
// - `providerOptions`: namespaced provider-native behavior. For example, // - `providerOptions`: namespaced provider-native behavior. For example,
// OpenAI store behavior, Anthropic thinking, Gemini thinking config, or // OpenAI cache keys and store behavior, Anthropic thinking, Gemini thinking
// OpenRouter routing/reasoning. // config, or OpenRouter routing/reasoning.
// - `http`: last-resort serializable overlays for final request body, headers, // - `http`: last-resort serializable overlays for final request body, headers,
// and query params. Prefer typed `providerOptions` when a field is stable. // and query params. Prefer typed `providerOptions` when a field is stable.
// //
@@ -46,7 +45,9 @@ const request = LLM.request({
system: "You are concise and practical.", system: "You are concise and practical.",
prompt: "Tell me a joke", prompt: "Tell me a joke",
generation: { maxTokens: 80, temperature: 0.7 }, generation: { maxTokens: 80, temperature: 0.7 },
promptCacheKey: "tutorial-joke", providerOptions: {
openai: { promptCacheKey: "tutorial-joke" },
},
}) })
// 3. `generate` sends the request and collects the event stream into one // 3. `generate` sends the request and collects the event stream into one
+5 -37
View File
@@ -25,20 +25,8 @@ import { ToolSchemaProjection } from "./utils/tool-schema"
const ADAPTER = "gemini" const ADAPTER = "gemini"
const MEDIA_MIMES = new Set<string>(ProviderShared.MEDIA_MIMES) const MEDIA_MIMES = new Set<string>(ProviderShared.MEDIA_MIMES)
// Google documents this sentinel for replaying Gemini 3 function calls after their original signature was lost.
const SKIP_THOUGHT_SIGNATURE_VALIDATOR = "skip_thought_signature_validator"
export const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta" export const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta"
// Gemini 3 rejects replayed function calls without a thought signature. Google's SDKs avoid that in normal chats by
// retaining complete model responses, but OpenCode reconstructs durable history and may encounter an unsigned call
// from an older or external session. Model IDs are open-ended, so unknown Gemini aliases inherit current behavior.
const requiresThoughtSignatureFallback = (modelID: string) => {
if (!/(^|\/)gemini-/i.test(modelID)) return false
if (/(^|\/)gemini-(?:1|2)(?:[.-]|$)/i.test(modelID)) return false
if (/(^|\/)gemini-pro(?:-vision)?$/i.test(modelID)) return false
return !/(^|\/)gemini-robotics-er-1\.5(?:[.-]|$)/i.test(modelID)
}
export interface OptionsInput { export interface OptionsInput {
readonly [key: string]: unknown readonly [key: string]: unknown
readonly cachedContent?: string readonly cachedContent?: string
@@ -157,9 +145,6 @@ const GeminiGenerationConfig = Schema.Struct({
temperature: Schema.optional(Schema.Number), temperature: Schema.optional(Schema.Number),
topP: Schema.optional(Schema.Number), topP: Schema.optional(Schema.Number),
topK: Schema.optional(Schema.Number), topK: Schema.optional(Schema.Number),
frequencyPenalty: Schema.optional(Schema.Number),
presencePenalty: Schema.optional(Schema.Number),
seed: Schema.optional(Schema.Number),
stopSequences: optionalArray(Schema.String), stopSequences: optionalArray(Schema.String),
thinkingConfig: Schema.optional(GeminiThinkingConfig), thinkingConfig: Schema.optional(GeminiThinkingConfig),
}) })
@@ -217,13 +202,11 @@ interface ParserState {
// keys on non-object scalars. Mirrors OpenCode's historical Gemini rules. // keys on non-object scalars. Mirrors OpenCode's historical Gemini rules.
// //
// 2. Project — lossy mapping from JSON Schema to Gemini's schema dialect: // 2. Project — lossy mapping from JSON Schema to Gemini's schema dialect:
// drop empty root parameter schemas while preserving nested empty objects, // drop empty objects, derive `nullable: true` from `type: [..., "null"]`,
// expand type arrays into `anyOf`, derive `nullable: true` from null members, // coerce `const` to `[const]` enum, recurse properties/items, propagate
// coerce `const` to `[const]` enum, recurse properties/items, and propagate
// only an allowlisted set of keys (description, required, format, type, // only an allowlisted set of keys (description, required, format, type,
// nullable, enum, properties, items, allOf, anyOf, oneOf, minLength). // properties, items, allOf, anyOf, oneOf, minLength). Anything outside the
// Anything outside the allowlist (e.g. `additionalProperties`, `$ref`) is // allowlist (e.g. `additionalProperties`, `$ref`) is silently dropped.
// silently dropped.
// //
// Sanitize runs first, then project. The implementation lives in // Sanitize runs first, then project. The implementation lives in
// `utils/gemini-tool-schema` so this protocol keeps the same shape as the other // `utils/gemini-tool-schema` so this protocol keeps the same shape as the other
@@ -299,8 +282,6 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMR
if (message.role === "assistant") { if (message.role === "assistant") {
const parts: Array<Schema.Schema.Type<typeof GeminiContentPart>> = [] const parts: Array<Schema.Schema.Type<typeof GeminiContentPart>> = []
// Parallel Gemini 3 calls may carry one signature on the first call; unsigned sibling calls are valid.
let hasSignedToolCall = false
for (const part of message.content) { for (const part of message.content) {
if (!ProviderShared.supportsContent(part, ["text", "reasoning", "tool-call"])) if (!ProviderShared.supportsContent(part, ["text", "reasoning", "tool-call"]))
return yield* ProviderShared.unsupportedContent("Gemini", "assistant", ["text", "reasoning", "tool-call"]) return yield* ProviderShared.unsupportedContent("Gemini", "assistant", ["text", "reasoning", "tool-call"])
@@ -313,17 +294,7 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMR
continue continue
} }
if (part.type === "tool-call") { if (part.type === "tool-call") {
const lowered = lowerToolCall(part) parts.push(lowerToolCall(part))
const signature = lowered.thoughtSignature
parts.push({
...lowered,
thoughtSignature:
signature ??
(requiresThoughtSignatureFallback(request.model.id) && !hasSignedToolCall
? SKIP_THOUGHT_SIGNATURE_VALIDATOR
: undefined),
})
if (signature !== undefined) hasSignedToolCall = true
continue continue
} }
} }
@@ -417,9 +388,6 @@ const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request: LLMReque
temperature: generation?.temperature, temperature: generation?.temperature,
topP: generation?.topP, topP: generation?.topP,
topK: generation?.topK, topK: generation?.topK,
frequencyPenalty: generation?.frequencyPenalty,
presencePenalty: generation?.presencePenalty,
seed: generation?.seed,
stopSequences: generation?.stop, stopSequences: generation?.stop,
thinkingConfig: options.thinkingConfig, thinkingConfig: options.thinkingConfig,
} }
+1 -1
View File
@@ -539,7 +539,7 @@ const lowerOptions = (request: LLMRequest) => {
return { return {
...(options.instructions ? { instructions: options.instructions } : {}), ...(options.instructions ? { instructions: options.instructions } : {}),
...(options.store !== undefined ? { store: options.store } : {}), ...(options.store !== undefined ? { store: options.store } : {}),
...(request.promptCacheKey ? { prompt_cache_key: request.promptCacheKey } : {}), ...(options.promptCacheKey ? { prompt_cache_key: options.promptCacheKey } : {}),
...(options.include ? { include: options.include } : {}), ...(options.include ? { include: options.include } : {}),
...(options.reasoningEffort || options.reasoningSummary ...(options.reasoningEffort || options.reasoningSummary
? { reasoning: { effort: options.reasoningEffort, summary: options.reasoningSummary } } ? { reasoning: { effort: options.reasoningEffort, summary: options.reasoningSummary } }
-2
View File
@@ -132,7 +132,6 @@ export const bodyFields = {
stream: Schema.Literal(true), stream: Schema.Literal(true),
stream_options: Schema.optional(Schema.Struct({ include_usage: Schema.Boolean })), stream_options: Schema.optional(Schema.Struct({ include_usage: Schema.Boolean })),
store: Schema.optional(Schema.Boolean), store: Schema.optional(Schema.Boolean),
prompt_cache_key: Schema.optional(Schema.String),
reasoning_effort: Schema.optional(OpenAIOptions.OpenAIReasoningEffort), reasoning_effort: Schema.optional(OpenAIOptions.OpenAIReasoningEffort),
max_completion_tokens: Schema.optional(Schema.Number), max_completion_tokens: Schema.optional(Schema.Number),
max_tokens: Schema.optional(Schema.Number), max_tokens: Schema.optional(Schema.Number),
@@ -510,7 +509,6 @@ const lowerOptions = (request: LLMRequest) => {
const options = OpenAIOptions.resolve(request) const options = OpenAIOptions.resolve(request)
return { return {
...(options.store !== undefined ? { store: options.store } : {}), ...(options.store !== undefined ? { store: options.store } : {}),
...(request.promptCacheKey ? { prompt_cache_key: request.promptCacheKey } : {}),
...(options.reasoningEffort ? { reasoning_effort: options.reasoningEffort } : {}), ...(options.reasoningEffort ? { reasoning_effort: options.reasoningEffort } : {}),
} }
} }
@@ -61,57 +61,37 @@ const emptyObjectSchema = (schema: Record<string, unknown>) =>
(!isRecord(schema.properties) || Object.keys(schema.properties).length === 0) && (!isRecord(schema.properties) || Object.keys(schema.properties).length === 0) &&
!schema.additionalProperties !schema.additionalProperties
const projectNode = (schema: unknown, nested = false): Record<string, unknown> | undefined => { const projectNode = (schema: unknown): Record<string, unknown> | undefined => {
if (!isRecord(schema)) return undefined if (!isRecord(schema)) return undefined
if (!nested && emptyObjectSchema(schema)) return undefined if (emptyObjectSchema(schema)) return undefined
const types = Array.isArray(schema.type) ? schema.type.filter((type) => type !== "null") : undefined return Object.fromEntries(
const anyOf = Array.isArray(schema.anyOf) ? schema.anyOf : undefined
const hasNullAnyOf = anyOf?.some((item) => isRecord(item) && item.type === "null") ?? false
const anyOfTypes = hasNullAnyOf ? anyOf?.filter((item) => !isRecord(item) || item.type !== "null") : anyOf
const flattenedAnyOf = hasNullAnyOf && anyOfTypes?.length === 1 ? projectNode(anyOfTypes[0], true) : undefined
const result = Object.fromEntries(
[ [
["description", schema.description], ["description", schema.description],
["required", schema.required], ["required", schema.required],
["format", schema.format], ["format", schema.format],
["type", types ? (types.length === 0 ? "null" : undefined) : schema.type], ["type", Array.isArray(schema.type) ? schema.type.filter((type) => type !== "null")[0] : schema.type],
[ ["nullable", Array.isArray(schema.type) && schema.type.includes("null") ? true : undefined],
"nullable",
(Array.isArray(schema.type) && schema.type.includes("null") && types && types.length > 0) || hasNullAnyOf
? true
: undefined,
],
["enum", schema.const !== undefined ? [schema.const] : schema.enum], ["enum", schema.const !== undefined ? [schema.const] : schema.enum],
[ [
"properties", "properties",
isRecord(schema.properties) isRecord(schema.properties)
? Object.fromEntries(Object.entries(schema.properties).map(([key, value]) => [key, projectNode(value, true)])) ? Object.fromEntries(Object.entries(schema.properties).map(([key, value]) => [key, projectNode(value)]))
: undefined, : undefined,
], ],
[ [
"items", "items",
Array.isArray(schema.items) Array.isArray(schema.items)
? schema.items.map((item) => projectNode(item, true)) ? schema.items.map(projectNode)
: schema.items === undefined : schema.items === undefined
? undefined ? undefined
: projectNode(schema.items, true), : projectNode(schema.items),
], ],
["allOf", Array.isArray(schema.allOf) ? schema.allOf.map((item) => projectNode(item, true)) : undefined], ["allOf", Array.isArray(schema.allOf) ? schema.allOf.map(projectNode) : undefined],
[ ["anyOf", Array.isArray(schema.anyOf) ? schema.anyOf.map(projectNode) : undefined],
"anyOf", ["oneOf", Array.isArray(schema.oneOf) ? schema.oneOf.map(projectNode) : undefined],
anyOfTypes
? hasNullAnyOf && anyOfTypes.length === 1
? undefined
: anyOfTypes.map((item) => projectNode(item, true))
: types && types.length > 0
? types.map((type) => ({ type }))
: undefined,
],
["oneOf", Array.isArray(schema.oneOf) ? schema.oneOf.map((item) => projectNode(item, true)) : undefined],
["minLength", schema.minLength], ["minLength", schema.minLength],
].filter((entry) => entry[1] !== undefined), ].filter((entry) => entry[1] !== undefined),
) )
return flattenedAnyOf ? { ...result, ...flattenedAnyOf } : result
} }
export const convert = (schema: unknown) => projectNode(sanitizeNode(schema)) export const convert = (schema: unknown) => projectNode(sanitizeNode(schema))
@@ -33,6 +33,7 @@ export const ServiceTierSchema = Schema.Literals(ServiceTiers)
export interface Resolved { export interface Resolved {
readonly instructions?: string readonly instructions?: string
readonly store?: boolean readonly store?: boolean
readonly promptCacheKey?: string
readonly reasoningEffort?: string readonly reasoningEffort?: string
readonly reasoningSummary?: "auto" | "concise" | "detailed" readonly reasoningSummary?: "auto" | "concise" | "detailed"
readonly include?: ReadonlyArray<ResponseIncludable> readonly include?: ReadonlyArray<ResponseIncludable>
@@ -49,6 +50,7 @@ export const resolve = (request: LLMRequest): Resolved => {
return { return {
instructions: typeof input?.instructions === "string" ? input.instructions : undefined, instructions: typeof input?.instructions === "string" ? input.instructions : undefined,
store: typeof input?.store === "boolean" ? input.store : undefined, store: typeof input?.store === "boolean" ? input.store : undefined,
promptCacheKey: typeof input?.promptCacheKey === "string" ? input.promptCacheKey : undefined,
reasoningEffort: typeof input?.reasoningEffort === "string" ? input.reasoningEffort : undefined, reasoningEffort: typeof input?.reasoningEffort === "string" ? input.reasoningEffort : undefined,
reasoningSummary: reasoningSummary:
reasoningSummary === "auto" || reasoningSummary === "concise" || reasoningSummary === "detailed" reasoningSummary === "auto" || reasoningSummary === "concise" || reasoningSummary === "detailed"
@@ -5,6 +5,7 @@ export interface OpenResponsesOptionsInput {
readonly [key: string]: unknown readonly [key: string]: unknown
readonly instructions?: string readonly instructions?: string
readonly store?: boolean readonly store?: boolean
readonly promptCacheKey?: string
readonly reasoningEffort?: ReasoningEffort readonly reasoningEffort?: ReasoningEffort
readonly reasoningSummary?: "auto" | "concise" | "detailed" readonly reasoningSummary?: "auto" | "concise" | "detailed"
readonly include?: ReadonlyArray<ResponseIncludable> readonly include?: ReadonlyArray<ResponseIncludable>
@@ -17,6 +17,7 @@ const openAIProviderOptions = (options: OpenAIOptionsInput | undefined): Provide
const openai = Object.fromEntries( const openai = Object.fromEntries(
definedEntries({ definedEntries({
store: options?.store, store: options?.store,
promptCacheKey: options?.promptCacheKey,
reasoningEffort: options?.reasoningEffort, reasoningEffort: options?.reasoningEffort,
reasoningSummary: options?.reasoningSummary, reasoningSummary: options?.reasoningSummary,
include: options?.include, include: options?.include,
+2 -1
View File
@@ -55,6 +55,7 @@ export interface OpenRouterOptions {
readonly debug?: Readonly<{ echo_upstream_body?: boolean }> readonly debug?: Readonly<{ echo_upstream_body?: boolean }>
readonly models?: ReadonlyArray<string> readonly models?: ReadonlyArray<string>
readonly plugins?: ReadonlyArray<OpenRouterPlugin> readonly plugins?: ReadonlyArray<OpenRouterPlugin>
readonly promptCacheKey?: string
readonly provider?: OpenRouterProviderRouting readonly provider?: OpenRouterProviderRouting
readonly reasoning?: Readonly<{ readonly reasoning?: Readonly<{
enabled?: boolean enabled?: boolean
@@ -121,7 +122,6 @@ export const protocol = Protocol.make({
...body, ...body,
messages, messages,
...bodyOptions(request.providerOptions?.openrouter), ...bodyOptions(request.providerOptions?.openrouter),
...(request.promptCacheKey ? { prompt_cache_key: request.promptCacheKey } : {}),
} as OpenRouterBody } as OpenRouterBody
}), }),
), ),
@@ -161,6 +161,7 @@ const bodyOptions = (input: unknown) => {
...(isRecord(debug) ? { debug } : {}), ...(isRecord(debug) ? { debug } : {}),
...(typeof user === "string" ? { user } : {}), ...(typeof user === "string" ? { user } : {}),
...(isRecord(reasoning) ? { reasoning } : {}), ...(isRecord(reasoning) ? { reasoning } : {}),
...(typeof promptCacheKey === "string" ? { prompt_cache_key: promptCacheKey } : {}),
} }
} }
-2
View File
@@ -47,8 +47,6 @@ const chatRoute = Route.make({
protocol: OpenAIChat.protocol, protocol: OpenAIChat.protocol,
endpoint: Endpoint.path("/chat/completions", { baseURL: OpenAICompatibleProfiles.profiles.xai.baseURL }), endpoint: Endpoint.path("/chat/completions", { baseURL: OpenAICompatibleProfiles.profiles.xai.baseURL }),
transport: OpenAICompatibleChat.route.transport, transport: OpenAICompatibleChat.route.transport,
headers: ({ request }): Record<string, string> =>
request.promptCacheKey ? { "x-grok-conv-id": request.promptCacheKey } : {},
}) })
export const routes = [responsesRoute, chatRoute] export const routes = [responsesRoute, chatRoute]
-3
View File
@@ -272,8 +272,6 @@ export class LLMRequest extends Schema.Class<LLMRequest>("LLM.Request")({
providerOptions: Schema.optional(ProviderOptions), providerOptions: Schema.optional(ProviderOptions),
http: Schema.optional(HttpOptions), http: Schema.optional(HttpOptions),
cache: Schema.optional(CachePolicy), cache: Schema.optional(CachePolicy),
// Stable cache affinity for protocols that support provider-managed prompt caching.
promptCacheKey: Schema.optional(Schema.String),
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
}) {} }) {}
@@ -291,7 +289,6 @@ export namespace LLMRequest {
providerOptions: request.providerOptions, providerOptions: request.providerOptions,
http: request.http, http: request.http,
cache: request.cache, cache: request.cache,
promptCacheKey: request.promptCacheKey,
metadata: request.metadata, metadata: request.metadata,
}) })
@@ -3,11 +3,11 @@ import { CloudflareWorkersAI } from "../../src/providers"
const model = CloudflareWorkersAI.configure({ accountId: "account", apiKey: "test" }).model("model") const model = CloudflareWorkersAI.configure({ accountId: "account", apiKey: "test" }).model("model")
LLM.request({ model, prompt: "Hello", promptCacheKey: "cache" }) LLM.request({ model, prompt: "Hello", providerOptions: { openai: { promptCacheKey: "cache" } } })
LLM.request({ LLM.request({
model, model,
prompt: "Hello", prompt: "Hello",
// @ts-expect-error Prompt cache keys must be strings. // @ts-expect-error Cloudflare's OpenAI-compatible prompt cache key must be a string.
promptCacheKey: 1, providerOptions: { openai: { promptCacheKey: 1 } },
}) })
-172
View File
@@ -16,13 +16,6 @@ const model = Gemini.route
}) })
.model({ id: "gemini-2.5-flash" }) .model({ id: "gemini-2.5-flash" })
const gemini3 = Gemini.route
.with({
endpoint: { baseURL: "https://generativelanguage.test/v1beta/" },
auth: Auth.header("x-goog-api-key", "test"),
})
.model({ id: "gemini-3-flash-preview" })
const request = LLM.request({ const request = LLM.request({
id: "req_1", id: "req_1",
model, model,
@@ -93,39 +86,6 @@ describe("Gemini route", () => {
}), }),
) )
it.effect("forwards standard Gemini generation options", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model,
prompt: "Say hello.",
generation: {
maxTokens: 40,
temperature: 0.2,
topP: 0.8,
topK: 12,
frequencyPenalty: 0.3,
presencePenalty: 0.4,
seed: 42,
stop: ["done"],
},
}),
)
expect(prepared.body.generationConfig).toEqual({
maxOutputTokens: 40,
temperature: 0.2,
topP: 0.8,
topK: 12,
frequencyPenalty: 0.3,
presencePenalty: 0.4,
seed: 42,
stopSequences: ["done"],
thinkingConfig: undefined,
})
}),
)
it.effect("lowers chronological system updates to wrapped user text in order", () => it.effect("lowers chronological system updates to wrapped user text in order", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* compileRequest(
@@ -390,100 +350,6 @@ describe("Gemini route", () => {
}), }),
) )
it.effect("preserves nested empty object tool schemas", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model,
prompt: "Use the tool.",
tools: [
{
name: "configure",
description: "Configure the operation",
inputSchema: {
type: "object",
required: ["options"],
properties: {
options: { type: "object", description: "Optional provider settings", properties: {} },
},
},
},
],
}),
)
expect(prepared.body.tools).toEqual([
{
functionDeclarations: [
{
name: "configure",
description: "Configure the operation",
parameters: {
type: "object",
required: ["options"],
properties: {
options: { type: "object", description: "Optional provider settings", properties: {} },
},
},
},
],
},
])
}),
)
it.effect("projects Gemini type arrays without narrowing their allowed values", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model,
prompt: "Use the tool.",
tools: [
{
name: "filter",
description: "Filter values",
inputSchema: {
type: "object",
properties: {
status: { type: ["number", "string"], description: "Status filter" },
maybe: { type: ["string", "null"] },
nothing: { type: ["null"] },
explicit: { anyOf: [{ type: "string" }, { type: "null" }] },
choice: { anyOf: [{ type: "string" }, { type: "number" }, { type: "null" }] },
},
},
},
],
}),
)
expect(prepared.body.tools?.[0]?.functionDeclarations[0]?.parameters).toEqual({
type: "object",
properties: {
status: {
description: "Status filter",
anyOf: [{ type: "number" }, { type: "string" }],
},
maybe: {
nullable: true,
anyOf: [{ type: "string" }],
},
nothing: {
type: "null",
},
explicit: {
type: "string",
nullable: true,
},
choice: {
anyOf: [{ type: "string" }, { type: "number" }],
nullable: true,
},
},
})
}),
)
it.effect("parses text, reasoning, and usage stream fixtures", () => it.effect("parses text, reasoning, and usage stream fixtures", () =>
Effect.gen(function* () { Effect.gen(function* () {
const body = sseEvents( const body = sseEvents(
@@ -670,44 +536,6 @@ describe("Gemini route", () => {
}), }),
) )
it.effect("replays unsigned Gemini 3 tool calls with the validator bypass sentinel", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: gemini3,
messages: [
Message.assistant([ToolCallPart.make({ id: "tool_0", name: "lookup", input: { query: "weather" } })]),
Message.tool({ id: "tool_0", name: "lookup", result: "done", resultType: "text" }),
],
}),
)
expect(prepared.body.contents).toEqual([
{
role: "model",
parts: [
{
functionCall: { id: undefined, name: "lookup", args: { query: "weather" } },
thoughtSignature: "skip_thought_signature_validator",
},
],
},
{
role: "user",
parts: [
{
functionResponse: {
id: undefined,
name: "lookup",
response: { name: "lookup", content: "done" },
},
},
],
},
])
}),
)
it.effect("emits streamed tool calls and maps finish reason", () => it.effect("emits streamed tool calls and maps finish reason", () =>
Effect.gen(function* () { Effect.gen(function* () {
const body = sseEvents({ const body = sseEvents({
@@ -15,8 +15,6 @@ import {
} from "../../src" } from "../../src"
import * as Azure from "../../src/providers/azure" import * as Azure from "../../src/providers/azure"
import * as OpenAI from "../../src/providers/openai" import * as OpenAI from "../../src/providers/openai"
import * as OpenAICompatible from "../../src/providers/openai-compatible"
import * as XAI from "../../src/providers/xai"
import * as OpenAIChat from "../../src/protocols/openai-chat" import * as OpenAIChat from "../../src/protocols/openai-chat"
import { ProviderShared } from "../../src/protocols/shared" import { ProviderShared } from "../../src/protocols/shared"
import { Auth, LLMClient } from "../../src/route" import { Auth, LLMClient } from "../../src/route"
@@ -156,47 +154,6 @@ describe("OpenAI Chat route", () => {
}), }),
) )
it.effect("maps the request prompt cache key", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: OpenAICompatible.configure({
baseURL: "https://api.compatible.test/v1",
apiKey: "test",
}).model("compatible-model"),
prompt: "Hello",
promptCacheKey: "session_123",
}),
)
expect(prepared.body.prompt_cache_key).toBe("session_123")
}),
)
it.effect("maps the xAI Chat prompt cache key to conversation affinity", () =>
LLMClient.generate(
LLM.request({
model: XAI.configure({ apiKey: "test", baseURL: "https://api.x.ai/v1" }).chat("grok-4.5"),
prompt: "Hello",
promptCacheKey: "session_123",
}),
).pipe(
Effect.provide(
dynamicResponse((input) =>
Effect.gen(function* () {
const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
expect(web.headers.get("x-grok-conv-id")).toBe("session_123")
const body = decodeJson(yield* Effect.promise(() => web.text()))
expect(ProviderShared.isRecord(body) ? body.prompt_cache_key : undefined).toBe("session_123")
return input.respond(sseEvents(deltaChunk({}, "stop")), {
headers: { "content-type": "text/event-stream" },
})
}),
),
),
),
)
it.effect("passes through custom OpenAI-compatible reasoning effort strings", () => it.effect("passes through custom OpenAI-compatible reasoning effort strings", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* compileRequest(
@@ -20,7 +20,7 @@ const cacheRequest = LLM.request({
system: LARGE_CACHEABLE_SYSTEM, system: LARGE_CACHEABLE_SYSTEM,
prompt: "Say hi.", prompt: "Say hi.",
generation: { maxTokens: 16, temperature: 0 }, generation: { maxTokens: 16, temperature: 0 },
promptCacheKey: "recorded-cache-test", providerOptions: { openai: { promptCacheKey: "recorded-cache-test" } },
}) })
const recorded = recordedTests({ const recorded = recordedTests({
@@ -682,9 +682,9 @@ describe("OpenAI Responses route", () => {
LLM.request({ LLM.request({
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).model("gpt-5.2"), model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).model("gpt-5.2"),
prompt: "think", prompt: "think",
promptCacheKey: "session_123",
providerOptions: { providerOptions: {
openai: { openai: {
promptCacheKey: "session_123",
reasoningEffort: "high", reasoningEffort: "high",
reasoningSummary: "auto", reasoningSummary: "auto",
include: ["reasoning.encrypted_content"], include: ["reasoning.encrypted_content"],
@@ -803,16 +803,17 @@ describe("OpenAI Responses route", () => {
}), }),
) )
it.effect("maps the request prompt cache key", () => it.effect("request OpenAI provider options override route defaults", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* compileRequest(
LLM.request({ LLM.request({
model: OpenAI.configure({ model: OpenAI.configure({
baseURL: "https://api.openai.test/v1/", baseURL: "https://api.openai.test/v1/",
apiKey: "test", apiKey: "test",
providerOptions: { openai: { promptCacheKey: "model_cache" } },
}).model("gpt-4.1-mini"), }).model("gpt-4.1-mini"),
prompt: "no cache", prompt: "no cache",
promptCacheKey: "request_cache", providerOptions: { openai: { promptCacheKey: "request_cache" } },
}), }),
) )
+1 -1
View File
@@ -162,6 +162,7 @@ describe("OpenRouter", () => {
openrouter: { openrouter: {
usage: true, usage: true,
reasoning: { effort: "high" }, reasoning: { effort: "high" },
promptCacheKey: "session_123",
models: ["anthropic/claude-sonnet-4.6", "google/gemini-3.1-pro"], models: ["anthropic/claude-sonnet-4.6", "google/gemini-3.1-pro"],
provider: { order: ["anthropic", "google"], require_parameters: true }, provider: { order: ["anthropic", "google"], require_parameters: true },
plugins: [{ id: "response-healing" }], plugins: [{ id: "response-healing" }],
@@ -173,7 +174,6 @@ describe("OpenRouter", () => {
}, },
}).model("anthropic/claude-3.7-sonnet:thinking"), }).model("anthropic/claude-3.7-sonnet:thinking"),
prompt: "Think briefly.", prompt: "Think briefly.",
promptCacheKey: "session_123",
}), }),
) )
@@ -1,18 +1,19 @@
import { expect, test } from "@playwright/test" import { expect, test } from "@playwright/test"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { mockOpenCodeServer } from "../utils/mock-server" import { mockOpenCodeServer } from "../utils/mock-server"
const draftID = "draft_removed_layout_preference" const draftID = "draft_legacy_new_session"
const directory = "C:/OpenCode/RemovedLayoutPreference" const directory = "C:/OpenCode/LegacyNewSession"
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}` const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
test("ignores persisted old layout preferences when opening drafts", async ({ page }) => { test("redirects a draft to the legacy new-session route", async ({ page }) => {
await mockOpenCodeServer(page, { await mockOpenCodeServer(page, {
directory, directory,
project: { project: {
id: "proj_removed_layout_preference", id: "proj_legacy_new_session",
worktree: directory, worktree: directory,
vcs: "git", vcs: "git",
name: "removed-layout-preference", name: "legacy-new-session",
time: { created: 1700000000000, updated: 1700000000000 }, time: { created: 1700000000000, updated: 1700000000000 },
sandboxes: [], sandboxes: [],
}, },
@@ -23,6 +24,7 @@ test("ignores persisted old layout preferences when opening drafts", async ({ pa
await page.addInitScript( await page.addInitScript(
({ directory, draftID, server }) => { ({ directory, draftID, server }) => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: false } })) localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: false } }))
localStorage.setItem("app-version.v1", JSON.stringify({ version: "1.17.20" }))
localStorage.setItem( localStorage.setItem(
"opencode.window.browser.dat:tabs", "opencode.window.browser.dat:tabs",
JSON.stringify([{ type: "draft", draftID, server, directory }]), JSON.stringify([{ type: "draft", draftID, server, directory }]),
@@ -33,7 +35,7 @@ test("ignores persisted old layout preferences when opening drafts", async ({ pa
await page.goto(`/new-session?draftId=${draftID}`) await page.goto(`/new-session?draftId=${draftID}`)
await expect(page).toHaveURL(`/new-session?draftId=${draftID}`) await expect(page).toHaveURL(`/${base64Encode(directory)}/session`)
await expect(page.locator("body")).toHaveAttribute("data-new-layout", "") await expect(page.locator("header[data-tauri-drag-region]")).toBeVisible()
await expect(page.getByRole("textbox", { name: "Prompt" })).toBeVisible() await expect(page.locator('[data-component="prompt-input"]')).toBeVisible()
}) })
+1 -1
View File
@@ -20,7 +20,7 @@
<meta property="twitter:image" content="/social-share.png" /> <meta property="twitter:image" content="/social-share.png" />
<script id="oc-theme-preload-script" src="/oc-theme-preload.js"></script> <script id="oc-theme-preload-script" src="/oc-theme-preload.js"></script>
</head> </head>
<body data-new-layout class="antialiased overscroll-none font-(family-name:--font-family-text) text-[13px] font-[440] overflow-hidden bg-v2-background-bg-deep"> <body class="antialiased overscroll-none text-12-regular overflow-hidden bg-v2-background-bg-deep">
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root" class="flex flex-col h-dvh bg-v2-background-bg-deep p-px"></div> <div id="root" class="flex flex-col h-dvh bg-v2-background-bg-deep p-px"></div>
<script src="/src/entry.tsx" type="module"></script> <script src="/src/entry.tsx" type="module"></script>
+161 -46
View File
@@ -14,11 +14,14 @@ import {
Navigate, Navigate,
Route, Route,
Router, Router,
useLocation,
useNavigate,
useParams, useParams,
useSearchParams, useSearchParams,
} from "@solidjs/router" } from "@solidjs/router"
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query" import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
import { Effect } from "effect" import { Effect } from "effect"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { import {
type Component, type Component,
createEffect, createEffect,
@@ -40,7 +43,7 @@ import { CommandProvider, useCommand, type CommandOption } from "@/context/comma
import { CommentsProvider } from "@/context/comments" import { CommentsProvider } from "@/context/comments"
import { FileProvider } from "@/context/file" import { FileProvider } from "@/context/file"
import { ServerSDKProvider } from "@/context/server-sdk" import { ServerSDKProvider } from "@/context/server-sdk"
import { ServerSyncProvider } from "@/context/server-sync" import { ServerSyncProvider, useServerSync } from "@/context/server-sync"
import { GlobalProvider, useGlobal } from "@/context/global" import { GlobalProvider, useGlobal } from "@/context/global"
import { HighlightsProvider } from "@/context/highlights" import { HighlightsProvider } from "@/context/highlights"
import { LanguageProvider, type Locale, useLanguage } from "@/context/language" import { LanguageProvider, type Locale, useLanguage } from "@/context/language"
@@ -51,36 +54,58 @@ import { PermissionProvider } from "@/context/permission"
import { usePlatform } from "@/context/platform" import { usePlatform } from "@/context/platform"
import { PromptProvider } from "@/context/prompt" import { PromptProvider } from "@/context/prompt"
import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server" import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server"
import { SettingsProvider } from "@/context/settings" import { SettingsProvider, useSettings } from "@/context/settings"
import { TabsProvider, useTabs, type DraftTab } from "@/context/tabs" import { TabsProvider, useTabs, type DraftTab } from "@/context/tabs"
import { SDKProvider } from "@/context/sdk" import { SDKProvider, useSDK } from "@/context/sdk"
import { WslServersProvider } from "@/wsl/context" import { WslServersProvider } from "@/wsl/context"
import { DirectoryDataProvider } from "@/pages/directory-layout" import DirectoryLayout, { DirectoryDataProvider } from "@/pages/directory-layout"
import Layout from "@/pages/layout" import LegacyLayout from "@/pages/layout"
import NewLayout from "@/pages/layout-new"
import { ErrorPage } from "./pages/error" import { ErrorPage } from "./pages/error"
import { useCheckServerHealth } from "./utils/server-health" import { useCheckServerHealth } from "./utils/server-health"
import { legacySessionServer, requireServerKey, sessionHref } from "./utils/session-route" import { legacySessionHref, legacySessionServer, requireServerKey, sessionHref } from "./utils/session-route"
import { decode64 } from "@/utils/base64" import { createSessionLineage } from "@/pages/session/session-lineage"
import { TargetSessionRouteContent } from "@/pages/session" import { SessionPage, SessionRouteErrorBoundary, TargetSessionRouteContent } from "@/pages/session"
import { Home } from "@/pages/home" import { NewHome } from "@/pages/home"
import { LegacyHome } from "@/pages/home/legacy-home"
const NewSession = lazy(() => import("@/pages/new-session")) const NewSession = lazy(() => import("@/pages/new-session"))
const DirectoryDraftRedirect = () => { const SessionRoute = () => {
const settings = useSettings()
const params = useParams() const params = useParams()
const [search] = useSearchParams<{ draftId?: string; prompt?: string }>() const [search] = useSearchParams<{ draftId?: string; prompt?: string }>()
const sdk = useSDK()
const server = useServer() const server = useServer()
const tabs = useTabs() const tabs = useTabs()
if (params.id && settings.general.newLayoutDesigns()) {
const sessionID = params.id
return (
<Show when={tabs.ready()}>
{(_) => {
const persisted = tabs.store.filter((item) => item.type === "session")
return <Navigate href={sessionHref(legacySessionServer(persisted, sessionID, server.key), sessionID)} />
}}
</Show>
)
}
// When the new layout is enabled, the legacy new-session route (/:dir/session with no id)
// is replaced by a draft at /new-session?draftId=…
createEffect(() => { createEffect(() => {
if (search.draftId || !tabs.ready()) return if (!settings.general.newLayoutDesigns()) return
const directory = decode64(params.dir) if (params.id || search.draftId) return
if (!directory) return if (!tabs.ready() || !sdk().directory) return
tabs.newDraft({ server: server.key, directory }, search.prompt) tabs.newDraft({ server: server.key, directory: sdk().directory }, search.prompt)
}) })
return null return (
<SessionRouteErrorBoundary sessionID={params.id}>
<SessionPage />
</SessionRouteErrorBoundary>
)
} }
function TargetServerRoute(props: ParentProps) { function TargetServerRoute(props: ParentProps) {
@@ -92,7 +117,9 @@ function TargetServerRoute(props: ParentProps) {
}) })
return ( return (
// Owns the server-identity remount. Session changes must not remount this subtree. // Owns the server-identity remount. Session changes must NOT remount this
// subtree (SessionRouteErrorBoundary resets and createSessionLineage
// re-resolves reactively instead); both rely on this key for server changes.
<Show when={requireServerKey(params.serverKey)} keyed> <Show when={requireServerKey(params.serverKey)} keyed>
<ServerSDKProvider server={conn}> <ServerSDKProvider server={conn}>
<ServerSyncProvider server={conn}>{props.children}</ServerSyncProvider> <ServerSyncProvider server={conn}>{props.children}</ServerSyncProvider>
@@ -107,6 +134,35 @@ const TargetSessionRoute = () => (
</TargetServerRoute> </TargetServerRoute>
) )
function LegacyTargetSessionRoute() {
const params = useParams<{ serverKey: string; id: string }>()
return (
<TargetServerRoute>
<SessionRouteErrorBoundary sessionID={params.id} serverKey={requireServerKey(params.serverKey)}>
<LegacyTargetSessionRedirect />
</SessionRouteErrorBoundary>
</TargetServerRoute>
)
}
function LegacyTargetSessionRedirect() {
const params = useParams<{ id: string }>()
const navigate = useNavigate()
const sync = useServerSync()
const current = createSessionLineage(
() => params.id,
() => sync().session.lineage,
)
createEffect(() => {
const directory = current()?.session.location.directory
if (!directory) return
navigate(legacySessionHref(directory, params.id), { replace: true })
})
return null
}
// Wraps the non-draft routes. They are gated on (and keyed to) the globally selected // Wraps the non-draft routes. They are gated on (and keyed to) the globally selected
// server via ServerKey, then provide the server-scoped shell for that server. // server via ServerKey, then provide the server-scoped shell for that server.
function SelectedServerProviders(props: ParentProps) { function SelectedServerProviders(props: ParentProps) {
@@ -119,8 +175,17 @@ function SelectedServerProviders(props: ParentProps) {
) )
} }
function LegacyServerLayout(props: ParentProps<{ serverScoped?: JSX.Element }>) {
return (
<SelectedServerProviders>
<LegacyServerScopedShell serverScoped={props.serverScoped}>{props.children}</LegacyServerScopedShell>
</SelectedServerProviders>
)
}
function DraftRoute() { function DraftRoute() {
const [search] = useSearchParams<{ draftId?: string }>() const [search] = useSearchParams<{ draftId?: string }>()
const settings = useSettings()
const tabs = useTabs() const tabs = useTabs()
return ( return (
<Show when={tabs.ready()}> <Show when={tabs.ready()}>
@@ -129,7 +194,14 @@ function DraftRoute() {
keyed keyed
fallback={<Navigate href="/" />} fallback={<Navigate href="/" />}
> >
{(draft) => <ResolvedDraftRoute draft={draft} />} {(draft) => (
<Show
when={settings.general.newLayoutDesigns()}
fallback={<Navigate href={`/${base64Encode(draft.directory)}/session`} />}
>
<ResolvedDraftRoute draft={draft} />
</Show>
)}
</Show> </Show>
</Show> </Show>
) )
@@ -165,6 +237,10 @@ function UiI18nBridge(props: ParentProps) {
return <I18nProvider value={{ locale: language.intl, t: language.t }}>{props.children}</I18nProvider> return <I18nProvider value={{ locale: language.intl, t: language.t }}>{props.children}</I18nProvider>
} }
function LayoutCompatibility(props: ParentProps) {
return <>{props.children}</>
}
declare global { declare global {
interface Window { interface Window {
__OPENCODE__?: { __OPENCODE__?: {
@@ -191,11 +267,17 @@ function QueryProvider(props: ParentProps) {
} }
function BodyDesignClass() { function BodyDesignClass() {
const settings = useSettings()
createRenderEffect(() => { createRenderEffect(() => {
if (typeof document === "undefined") return if (typeof document === "undefined") return
document.body.toggleAttribute("data-new-layout", true)
document.body.classList.remove("text-12-regular") const enabled = settings.general.newLayoutDesigns()
document.body.classList.add("font-(family-name:--font-family-text)", "text-[13px]", "font-[440]") document.body.toggleAttribute("data-new-layout", enabled)
document.body.classList.toggle("text-12-regular", !enabled)
document.body.classList.toggle("font-(family-name:--font-family-text)", enabled)
document.body.classList.toggle("text-[13px]", enabled)
document.body.classList.toggle("font-[440]", enabled)
}) })
return null return null
@@ -238,6 +320,7 @@ function DesktopCommands() {
return null return null
} }
// Server-scoped providers shared by the legacy shell and the top-level new shell.
type ServerScopedShellProps = ParentProps<{ type ServerScopedShellProps = ParentProps<{
directory?: () => string | undefined directory?: () => string | undefined
serverScoped?: JSX.Element serverScoped?: JSX.Element
@@ -252,11 +335,19 @@ function ServerScopedProviders(props: ServerScopedShellProps) {
) )
} }
function AppLayout(props: ParentProps<{ serverScoped?: JSX.Element }>) { function LegacyServerScopedShell(props: ServerScopedShellProps) {
return (
<ServerScopedProviders directory={props.directory} serverScoped={props.serverScoped}>
<LegacyLayout>{props.children}</LegacyLayout>
</ServerScopedProviders>
)
}
function NewAppLayout(props: ParentProps<{ serverScoped?: JSX.Element }>) {
return ( return (
<SelectedServerProviders> <SelectedServerProviders>
<ServerScopedProviders serverScoped={props.serverScoped}> <ServerScopedProviders serverScoped={props.serverScoped}>
<Layout>{props.children}</Layout> <NewLayout>{props.children}</NewLayout>
</ServerScopedProviders> </ServerScopedProviders>
</SelectedServerProviders> </SelectedServerProviders>
) )
@@ -445,7 +536,7 @@ export function AppInterface(props: {
startup?: Promise<void> startup?: Promise<void>
serverScoped?: JSX.Element serverScoped?: JSX.Element
}) { }) {
// The visual layout lives in the router root so it remains mounted across // The visual new layout lives in the router root so it remains mounted across
// route changes. Draft and session routes override only their server-bound data // route changes. Draft and session routes override only their server-bound data
// providers beneath it. // providers beneath it.
const ServerShell = (shellProps: ParentProps) => ( const ServerShell = (shellProps: ParentProps) => (
@@ -466,22 +557,26 @@ export function AppInterface(props: {
<GlobalProvider> <GlobalProvider>
<SettingsProvider> <SettingsProvider>
<ConnectionGate disableHealthCheck={props.disableHealthCheck} startup={props.startup}> <ConnectionGate disableHealthCheck={props.disableHealthCheck} startup={props.startup}>
<Dynamic <Show when={useSettings().general.newLayoutDesigns().toString()} keyed>
component={props.router ?? Router} <Dynamic
root={(routerProps) => ( component={props.router ?? Router}
<TabsProvider> root={(routerProps) => (
<PermissionProvider> <TabsProvider>
<NotificationProvider> <PermissionProvider>
<ServerShell> <NotificationProvider>
<AppLayout serverScoped={props.serverScoped}>{routerProps.children}</AppLayout> <ServerShell>
</ServerShell> <Show when={useSettings().general.newLayoutDesigns()} fallback={routerProps.children}>
</NotificationProvider> <NewAppLayout serverScoped={props.serverScoped}>{routerProps.children}</NewAppLayout>
</PermissionProvider> </Show>
</TabsProvider> </ServerShell>
)} </NotificationProvider>
> </PermissionProvider>
<Routes /> </TabsProvider>
</Dynamic> )}
>
<Routes serverScoped={props.serverScoped} />
</Dynamic>
</Show>
</ConnectionGate> </ConnectionGate>
</SettingsProvider> </SettingsProvider>
</GlobalProvider> </GlobalProvider>
@@ -489,20 +584,40 @@ export function AppInterface(props: {
) )
} }
function Routes() { function Routes(props: { serverScoped?: JSX.Element }) {
const settings = useSettings()
return ( return (
<> <>
<Route path="/" component={Home} /> <Route
<Route path="/:dir" component={DirectoryDraftRedirect} /> component={(routeProps) => (
<Route path="/:dir/session" component={DirectoryDraftRedirect} /> <LegacyServerLayout serverScoped={props.serverScoped}>{routeProps.children}</LegacyServerLayout>
<Route path="/:dir/session/:id" component={LegacySessionRedirect} /> )}
<Route path="/server/:serverKey/session/:id" component={TargetSessionRoute} /> >
<Show when={!settings.general.newLayoutDesigns()}>
{
<>
<Route path="/" component={LegacyHome} />
<Route path="/server/:serverKey/session/:id" component={LegacyTargetSessionRoute} />
</>
}
</Show>
<Route path="/:dir" component={DirectoryLayout}>
<Route path="/" component={() => <Navigate href="session" />} />
<Route path="/session/:id?" component={SessionRoute} />
</Route>
</Route>
<Show when={settings.general.newLayoutDesigns()}>
<Route path="/" component={NewHome} />
<Route path="/:dir/session/:id" component={NewLayoutLegacySessionRedirect} />
<Route path="/server/:serverKey/session/:id" component={TargetSessionRoute} />
</Show>
<Route path="/new-session" component={DraftRoute} /> <Route path="/new-session" component={DraftRoute} />
</> </>
) )
} }
function LegacySessionRedirect() { function NewLayoutLegacySessionRedirect() {
const server = useServer() const server = useServer()
const tabs = useTabs() const tabs = useTabs()
const params = useParams<{ id: string }>() const params = useParams<{ id: string }>()
Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

@@ -0,0 +1,170 @@
import { Button } from "@opencode-ai/ui/button"
import { Dialog } from "@opencode-ai/ui/dialog"
import { TextField } from "@opencode-ai/ui/text-field"
import { Icon } from "@opencode-ai/ui/icon"
import { For, Show } from "solid-js"
import { type LocalProject, getAvatarColors } from "@/context/layout"
import { Avatar } from "@opencode-ai/ui/avatar"
import { useLanguage } from "@/context/language"
import { getProjectAvatarSource } from "@/pages/layout/helpers"
import { ServerConnection } from "@/context/server"
import { createEditProjectModel } from "./edit-project"
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
export function DialogEditProject(props: { project: LocalProject; server: ServerConnection.Any }) {
const language = useLanguage()
const model = createEditProjectModel(props)
return (
<Dialog title={language.t("dialog.project.edit.title")} class="w-full max-w-[480px] mx-auto">
<form onSubmit={model.submit} class="flex flex-col gap-6 p-6 pt-0">
<div class="flex flex-col gap-4">
<TextField
autofocus
type="text"
label={language.t("dialog.project.edit.name")}
placeholder={model.folderName()}
value={model.store.name}
onChange={(v) => model.setStore("name", v)}
/>
<div class="flex flex-col gap-2">
<label class="text-12-medium text-text-weak">{language.t("dialog.project.edit.icon")}</label>
<div class="flex gap-3 items-start">
<div
class="relative"
onMouseEnter={() => model.setStore("iconHover", true)}
onMouseLeave={() => model.setStore("iconHover", false)}
>
<div
class="relative size-16 rounded-md transition-colors cursor-pointer"
classList={{
"border-text-interactive-base bg-surface-info-base/20": model.store.dragOver,
"border-border-base hover:border-border-strong": !model.store.dragOver,
"overflow-hidden": !!model.store.iconOverride,
}}
onDrop={model.drop}
onDragOver={model.dragOver}
onDragLeave={model.dragLeave}
onClick={model.iconClick}
>
<Show
when={getProjectAvatarSource(props.project.id, {
color: model.store.color,
url: props.project.icon?.url,
override: model.store.iconOverride,
})}
fallback={
<div class="size-full flex items-center justify-center">
<Avatar
fallback={model.store.name || model.defaultName()}
{...getAvatarColors(model.store.color)}
class="size-full text-[32px]"
/>
</div>
}
>
{(src) => (
<img
src={src()}
alt={language.t("dialog.project.edit.icon.alt")}
class="size-full object-cover"
/>
)}
</Show>
</div>
<div
class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
classList={{
"opacity-100": model.store.iconHover && !model.store.iconOverride,
"opacity-0": !(model.store.iconHover && !model.store.iconOverride),
}}
>
<Icon name="cloud-upload" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
</div>
<div
class="absolute inset-0 size-16 bg-surface-raised-stronger-non-alpha/90 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
classList={{
"opacity-100": model.store.iconHover && !!model.store.iconOverride,
"opacity-0": !(model.store.iconHover && !!model.store.iconOverride),
}}
>
<Icon name="trash" size="large" class="text-icon-on-interactive-base drop-shadow-sm" />
</div>
</div>
<input
id="icon-upload"
ref={(el) => {
model.setIconInput(el)
}}
type="file"
accept="image/*"
class="hidden"
onChange={model.inputChange}
/>
<div class="flex flex-col gap-1.5 text-12-regular text-text-weak self-center">
<span>{language.t("dialog.project.edit.icon.hint")}</span>
<span>{language.t("dialog.project.edit.icon.recommended")}</span>
</div>
</div>
</div>
<Show when={!model.store.iconOverride}>
<div class="flex flex-col gap-2">
<label class="text-12-medium text-text-weak">{language.t("dialog.project.edit.color")}</label>
<div class="flex gap-1.5">
<For each={AVATAR_COLOR_KEYS}>
{(color) => (
<button
type="button"
aria-label={language.t("dialog.project.edit.color.select", { color })}
aria-pressed={model.store.color === color}
classList={{
"flex items-center justify-center size-10 p-0.5 rounded-lg overflow-hidden transition-colors cursor-default": true,
"bg-transparent border-2 border-icon-strong-base hover:bg-surface-base-hover":
model.store.color === color,
"bg-transparent border border-transparent hover:bg-surface-base-hover hover:border-border-weak-base":
model.store.color !== color,
}}
onClick={() => {
if (model.store.color === color && !props.project.icon?.url) return
model.setStore("color", model.store.color === color ? undefined : color)
}}
>
<Avatar
fallback={model.store.name || model.defaultName()}
{...getAvatarColors(color)}
class="size-full rounded"
/>
</button>
)}
</For>
</div>
</div>
</Show>
<TextField
multiline
label={language.t("dialog.project.edit.worktree.startup")}
description={language.t("dialog.project.edit.worktree.startup.description")}
placeholder={language.t("dialog.project.edit.worktree.startup.placeholder")}
value={model.store.startup}
onChange={(v) => model.setStore("startup", v)}
spellcheck={false}
class="max-h-14 w-full overflow-y-auto font-mono text-xs"
/>
</div>
<div class="flex justify-end gap-2">
<Button type="button" variant="ghost" size="large" onClick={model.close}>
{language.t("common.cancel")}
</Button>
<Button type="submit" variant="primary" size="large" disabled={!model.supported || model.save.isPending}>
{model.save.isPending ? language.t("common.saving") : language.t("common.save")}
</Button>
</div>
</form>
</Dialog>
)
}
@@ -0,0 +1,94 @@
import { Component, createSignal, startTransition } from "solid-js"
import { Dialog } from "@opencode-ai/ui/dialog"
import { Tabs } from "@opencode-ai/ui/tabs"
import { Icon } from "@opencode-ai/ui/icon"
import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { SettingsGeneral } from "./settings-general"
import { SettingsKeybinds } from "./settings-keybinds"
import { SettingsProviders } from "./settings-providers"
import { SettingsModels } from "./settings-models"
import { SettingsServers } from "./settings-servers"
export const DialogSettings: Component<{ defaultValue?: string }> = (props) => {
const language = useLanguage()
const platform = usePlatform()
const dialog = useDialog()
const [tab, setTab] = createSignal(props.defaultValue ?? "general")
const showProviders = () => {
void dialog.show(() => <DialogSettings defaultValue="providers" />)
}
return (
<Dialog size="x-large" transition>
<Tabs
orientation="vertical"
variant="settings"
value={tab()}
onChange={(value) => void startTransition(() => setTab(value))}
class="h-full settings-dialog"
>
<Tabs.List>
<div class="flex flex-col justify-between h-full w-full gap-4">
<div class="flex flex-col gap-3 w-full pt-3">
<div class="flex flex-col gap-3">
<div class="flex flex-col gap-1.5">
<Tabs.SectionTitle>{language.t("settings.section.desktop")}</Tabs.SectionTitle>
<div class="flex flex-col gap-1.5 w-full">
<Tabs.Trigger value="general">
<Icon name="sliders" />
{language.t("settings.tab.general")}
</Tabs.Trigger>
<Tabs.Trigger value="shortcuts">
<Icon name="keyboard" />
{language.t("settings.tab.shortcuts")}
</Tabs.Trigger>
<Tabs.Trigger value="servers">
<Icon name="server" />
{language.t("status.popover.tab.servers")}
</Tabs.Trigger>
</div>
</div>
<div class="flex flex-col gap-1.5">
<Tabs.SectionTitle>{language.t("settings.section.server")}</Tabs.SectionTitle>
<div class="flex flex-col gap-1.5 w-full">
<Tabs.Trigger value="providers">
<Icon name="providers" />
{language.t("settings.providers.title")}
</Tabs.Trigger>
<Tabs.Trigger value="models">
<Icon name="models" />
{language.t("settings.models.title")}
</Tabs.Trigger>
</div>
</div>
</div>
</div>
<div class="flex flex-col gap-1 pl-1 py-1 text-12-medium text-text-weak">
<span>{language.t("app.name.desktop")}</span>
<span class="text-11-regular">v{platform.version}</span>
</div>
</div>
</Tabs.List>
<Tabs.Content value="general" class="no-scrollbar">
<SettingsGeneral />
</Tabs.Content>
<Tabs.Content value="shortcuts" class="no-scrollbar">
<SettingsKeybinds />
</Tabs.Content>
<Tabs.Content value="servers" class="no-scrollbar">
<SettingsServers />
</Tabs.Content>
<Tabs.Content value="providers" class="no-scrollbar">
<SettingsProviders onBack={showProviders} />
</Tabs.Content>
<Tabs.Content value="models" class="no-scrollbar">
<SettingsModels />
</Tabs.Content>
</Tabs>
</Dialog>
)
}
+148
View File
@@ -0,0 +1,148 @@
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { createSignal, Show } from "solid-js"
import { Drawer, DrawerClose, DrawerContent } from "@/components/ui/drawer"
import { usePlatform } from "@/context/platform"
import { useSettings } from "@/context/settings"
import introducingTabsVideo from "@/assets/help/introducing-tabs.mp4"
import homeImage from "@/assets/help/home.png"
import tabsImage from "@/assets/help/tabs.png"
// TODO: wire to changelog / seen-state when available
const showPopover = () => true
// can remove this after the tabs rollout has been out for a while
export function TabsInfoPopup() {
const settings = useSettings()
const platform = usePlatform()
const [drawerOpen, setDrawerOpen] = createSignal(false)
const windows = () => platform.platform === "desktop" && platform.os === "windows"
return (
<Drawer open={drawerOpen()} onOpenChange={setDrawerOpen} side="right">
<Show when={settings.general.shouldDisplayTabsToast()}>
<div
class="fixed bottom-5 right-5 z-50 h-[240px] w-[192px] rounded-[8px] bg-v2-background-bg-base p-1 shadow-[var(--v2-elevation-floating)]"
aria-label="Introducing Tabs. Organize your work and active sessions with tabs"
>
<button
type="button"
aria-label="Dismiss Tabs information"
class="absolute top-3 right-3 z-10 size-5 flex items-center justify-center rounded-[4px] bg-[rgba(0,0,0,0.4)]"
onClick={settings.general.dismissTabsToast}
>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path d="M4.25 11.75L11.75 4.25M11.75 11.75L4.25 4.25" stroke="white" />
</svg>
</button>
<button
type="button"
class="relative block h-[232px] w-[184px] cursor-pointer overflow-hidden rounded-[4px] text-left"
onClick={() => {
settings.general.dismissTabsToast()
setDrawerOpen(true)
}}
>
<video
src={introducingTabsVideo}
class="absolute inset-0 h-full w-full object-cover"
loop
muted
autoplay
playsinline
aria-hidden="true"
onContextMenu={(event) => event.preventDefault()}
/>
<div class="absolute inset-x-0 bottom-0 flex w-full flex-col items-start gap-1.5 bg-[linear-gradient(180deg,rgba(0,0,0,0)_0%,#000000_100%)] px-3 py-5">
<p class="w-full select-none text-[13px] font-[530] leading-none tracking-[-0.04px] text-[#FFFFFF]">
Introducing Tabs
</p>
<p class="w-full select-none text-[13px] font-[440] leading-[140%] tracking-[-0.04px] text-[#808080]">
Organize your work and active sessions with tabs
</p>
</div>
</button>
</div>
</Show>
<DrawerContent
style={
windows()
? {
inset: "0 0 0 auto",
"max-height": "100vh",
"max-width": "100vw",
"border-radius": "0",
}
: undefined
}
>
<Show when={windows()}>
<DrawerClose
as={IconButtonV2}
type="button"
size="small"
variant="neutral"
aria-label="Close"
icon={<IconV2 name="xmark-small" />}
class="absolute top-[10px] left-[-36px]"
/>
</Show>
<div
class="flex w-full shrink-0 items-center gap-4 self-stretch border-b border-v2-border-border-muted"
classList={{
"h-[40px] px-4": windows(),
"h-[52px] p-4": !windows(),
}}
>
<p class="min-h-0 min-w-0 flex-1 text-[13px] font-[530] leading-5 tracking-[-0.04px] tabular-nums text-v2-text-text-muted">
July 14
</p>
<Show when={!windows()}>
<DrawerClose
as={IconButtonV2}
type="button"
size="small"
variant="ghost-muted"
aria-label="Close"
icon={<IconV2 name="xmark-small" />}
/>
</Show>
</div>
<div class="relative flex min-h-0 w-full flex-1 flex-col items-start gap-6 overflow-y-auto p-8">
<p class="w-full shrink-0 self-stretch text-[21px] font-[610] leading-6 tracking-[-0.37px] tabular-nums text-v2-text-text-base">
Introducing Tabs
</p>
<div class="flex w-full flex-1 flex-col gap-4 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-base">
<p>OpenCode Desktop is now built around tabs.</p>
<img src={tabsImage} alt="" class="aspect-video w-full rounded-[6px] object-cover" />
<p>
Start a new session in a tab, or open an existing session from any of your projects. Open a new tab when
you're starting something new, and close it when you're done.
</p>
<p>
Keeping a few tabs open makes it easier to organize your active sessions. Rename tabs to something
memorable if you plan to keep them around.
</p>
<p>
You'll find all your sessions and projects on the new Home screen. Selecting a session opens it in a tab.
</p>
<img src={homeImage} alt="" class="aspect-video w-full rounded-[6px] object-cover" />
<p>When you reopen the app, your tabs are still open.</p>
<p>
The new design does not support Git Worktrees yet, it's coming soon. So if you'd prefer to continue using
the previous layout, you can switch between layouts in Settings. Just keep in mind that the new layout
will become permanent in a few weeks.
</p>
</div>
</div>
</DrawerContent>
</Drawer>
)
}
@@ -0,0 +1,793 @@
import { Component, Show, createMemo, createResource, onMount, type JSX } from "solid-js"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { Select } from "@opencode-ai/ui/select"
import { Switch } from "@opencode-ai/ui/switch"
import { TextField } from "@opencode-ai/ui/text-field"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { Tag } from "@opencode-ai/ui/v2/badge-v2"
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useParams } from "@solidjs/router"
import { useLanguage } from "@/context/language"
import { usePermission } from "@/context/permission"
import { usePlatform, type DisplayBackend } from "@/context/platform"
import { useServerSync } from "@/context/server-sync"
import { useServerSDK } from "@/context/server-sdk"
import { useUpdaterAction } from "./updater-action"
import {
monoDefault,
monoFontFamily,
monoInput,
sansDefault,
sansFontFamily,
sansInput,
terminalDefault,
terminalFontFamily,
terminalInput,
useSettings,
} from "@/context/settings"
import { decode64 } from "@/utils/base64"
import { playSoundById, SOUND_OPTIONS } from "@/utils/sound"
import { Link } from "./link"
import { SettingsList } from "./settings-list"
let demoSoundState = {
cleanup: undefined as (() => void) | undefined,
timeout: undefined as NodeJS.Timeout | undefined,
run: 0,
}
type ThemeOption = {
id: string
name: string
}
type ShellOption = {
path: string
name: string
acceptable: boolean
}
type ShellSelectOption = {
id: string
value: string
label: string
}
// To prevent audio from overlapping/playing very quickly when navigating the settings menus,
// delay the playback by 100ms during quick selection changes and pause existing sounds.
const stopDemoSound = () => {
demoSoundState.run += 1
if (demoSoundState.cleanup) {
demoSoundState.cleanup()
}
clearTimeout(demoSoundState.timeout)
demoSoundState.cleanup = undefined
}
const playDemoSound = (id: string | undefined) => {
stopDemoSound()
if (!id) return
const run = ++demoSoundState.run
demoSoundState.timeout = setTimeout(() => {
void playSoundById(id).then((cleanup) => {
if (demoSoundState.run !== run) {
cleanup?.()
return
}
demoSoundState.cleanup = cleanup
})
}, 100)
}
export const SettingsGeneral: Component = () => {
const theme = useTheme()
const language = useLanguage()
const permission = usePermission()
const platform = usePlatform()
const dialog = useDialog()
const params = useParams()
const settings = useSettings()
const updater = useUpdaterAction()
const linux = createMemo(() => platform.platform === "desktop" && platform.os === "linux")
const dir = createMemo(() => decode64(params.dir))
const accepting = createMemo(() => {
const value = dir()
if (!value) return false
if (!params.id) return permission.isAutoAcceptingDirectory(value)
return permission.isAutoAccepting(params.id, value)
})
const toggleAccept = (checked: boolean) => {
const value = dir()
if (!value) return
if (!params.id) {
if (permission.isAutoAcceptingDirectory(value) === checked) return
permission.toggleAutoAcceptDirectory(value)
return
}
if (checked) {
permission.enableAutoAccept(params.id, value)
return
}
permission.disableAutoAccept(params.id, value)
}
const desktop = createMemo(() => platform.platform === "desktop")
const themeOptions = createMemo<ThemeOption[]>(() => theme.ids().map((id) => ({ id, name: theme.name(id) })))
const serverSync = useServerSync()
const serverSdk = useServerSDK()
const [shells] = createResource(
async () => {
// TODO: Restore executable shell discovery; V2 shell.list only lists shell processes.
return [] as ShellOption[]
},
{ initialValue: [] as ShellOption[] },
)
const [displayBackend, { refetch: refetchDisplayBackend }] = createResource(
() => (linux() && platform.getDisplayBackend ? true : false),
() => Promise.resolve(platform.getDisplayBackend?.() ?? null).catch(() => null as DisplayBackend | null),
{ initialValue: null as DisplayBackend | null },
)
const [pinchZoom, { mutate: setPinchZoom }] = createResource(
() => (desktop() && platform.getPinchZoomEnabled ? true : false),
() => Promise.resolve(platform.getPinchZoomEnabled?.() ?? false).catch(() => false),
{ initialValue: false },
)
onMount(() => {
void theme.loadThemes()
})
const autoOption = { id: "auto", value: "", label: language.t("settings.general.row.shell.autoDefault") }
const currentShell = createMemo(() => serverSync().data.config.shell ?? "")
const shellOptions = createMemo<ShellSelectOption[]>(() => {
const list = shells.latest
const current = serverSync().data.config.shell
const nameCounts = new Map<string, number>()
for (const s of list) {
nameCounts.set(s.name, (nameCounts.get(s.name) || 0) + 1)
}
const options = [
autoOption,
...list.map((s) => {
const ambiguousName = (nameCounts.get(s.name) || 0) > 1
const text = ambiguousName ? s.path : s.name
const label = s.acceptable ? text : `${text} (${language.t("settings.general.row.shell.terminalOnly")})`
return {
id: s.path,
// Prefer name over path - "bash" is much cleaner than the explicit full route even when it may change due to PATH.
value: ambiguousName ? s.path : s.name,
label,
}
}),
]
if (current && !options.some((o) => o.value === current)) {
options.push({ id: current, value: current, label: current })
}
return options
})
const onDisplayBackendChange = (checked: boolean) => {
const update = platform.setDisplayBackend?.(checked ? "wayland" : "auto")
if (!update) return
void update.finally(() => {
void refetchDisplayBackend()
})
}
const onPinchZoomChange = (checked: boolean) => {
setPinchZoom(checked)
const update = platform.setPinchZoomEnabled?.(checked)
if (!update) return
void update.catch(() => setPinchZoom(!checked))
}
const colorSchemeOptions = createMemo((): { value: ColorScheme; label: string }[] => [
{ value: "system", label: language.t("theme.scheme.system") },
{ value: "light", label: language.t("theme.scheme.light") },
{ value: "dark", label: language.t("theme.scheme.dark") },
])
const languageOptions = createMemo(() =>
language.locales.map((locale) => ({
value: locale,
label: language.label(locale),
})),
)
const noneSound = { id: "none", label: "sound.option.none" } as const
const soundOptions = [noneSound, ...SOUND_OPTIONS]
const mono = () => monoInput(settings.appearance.font())
const sans = () => sansInput(settings.appearance.uiFont())
const terminal = () => terminalInput(settings.appearance.terminalFont())
const soundSelectProps = (
enabled: () => boolean,
current: () => string,
setEnabled: (value: boolean) => void,
set: (id: string) => void,
) => ({
options: soundOptions,
current: enabled() ? (soundOptions.find((o) => o.id === current()) ?? noneSound) : noneSound,
value: (o: (typeof soundOptions)[number]) => o.id,
label: (o: (typeof soundOptions)[number]) => language.t(o.label),
onHighlight: (option: (typeof soundOptions)[number] | undefined) => {
if (!option) return
playDemoSound(option.id === "none" ? undefined : option.id)
},
onSelect: (option: (typeof soundOptions)[number] | undefined) => {
if (!option) return
if (option.id === "none") {
setEnabled(false)
stopDemoSound()
return
}
setEnabled(true)
set(option.id)
playDemoSound(option.id)
},
variant: "secondary" as const,
size: "small" as const,
triggerVariant: "settings" as const,
})
const InterfaceSection = () => (
<div class="flex flex-col gap-1">
<SettingsList>
<SettingsRow
title={
<span class="flex items-center gap-2">
{language.t("settings.general.row.newInterface.title")}
<Tag variant="accent">{language.t("settings.general.row.newInterface.badge")}</Tag>
</span>
}
description={language.t("settings.general.row.newInterface.description")}
>
<div data-action="settings-new-layout-designs">
<Switch
checked={settings.general.newLayoutDesigns()}
onChange={(checked) => {
settings.general.setNewLayoutDesigns(checked)
if (!checked) return
void import("@/components/settings-v2").then((module) => {
void dialog.show(() => <module.DialogSettings />)
})
}}
/>
</div>
</SettingsRow>
</SettingsList>
</div>
)
const InterfaceNoticeSection = () => (
<div class="flex flex-col gap-1">
<SettingsList>
<SettingsRow
title={language.t("settings.general.row.newInterfaceNotice.title")}
description={language.t("settings.general.row.newInterfaceNotice.description")}
>
<Button size="small" variant="ghost" onClick={settings.general.dismissNewInterfaceNotice}>
{language.t("settings.general.row.newInterfaceNotice.dismiss")}
</Button>
</SettingsRow>
</SettingsList>
</div>
)
const GeneralSection = () => (
<div class="flex flex-col gap-1">
<SettingsList>
<SettingsRow
title={language.t("settings.general.row.language.title")}
description={language.t("settings.general.row.language.description")}
>
<Select
data-action="settings-language"
options={languageOptions()}
current={languageOptions().find((o) => o.value === language.locale())}
value={(o) => o.value}
label={(o) => o.label}
onSelect={(option) => option && language.setLocale(option.value)}
variant="secondary"
size="small"
triggerVariant="settings"
/>
</SettingsRow>
<SettingsRow
title={language.t("command.permissions.autoaccept.enable")}
description={language.t("toast.permissions.autoaccept.on.description")}
>
<div data-action="settings-auto-accept-permissions">
<Switch checked={accepting()} disabled={!dir()} onChange={toggleAccept} />
</div>
</SettingsRow>
<SettingsRow
title={language.t("settings.general.row.shell.title")}
description={language.t("settings.general.row.shell.description")}
>
<Select
data-action="settings-shell"
disabled
options={shellOptions()}
current={shellOptions().find((o) => o.value === currentShell()) ?? autoOption}
value={(o) => o.id}
label={(o) => o.label}
onSelect={(option) => {
if (!option) return
if (option.value === currentShell()) return
// TODO: Restore config writes when the V2 client exposes a config API.
// void serverSync().updateConfig({ shell: option.value })
}}
variant="secondary"
size="small"
triggerVariant="settings"
triggerStyle={{ "min-width": "180px" }}
/>
</SettingsRow>
<SettingsRow
title={language.t("settings.general.row.reasoningSummaries.title")}
description={language.t("settings.general.row.reasoningSummaries.description")}
>
<div data-action="settings-feed-reasoning-summaries">
<Switch
checked={settings.general.showReasoningSummaries()}
onChange={(checked) => settings.general.setShowReasoningSummaries(checked)}
/>
</div>
</SettingsRow>
<SettingsRow
title={language.t("settings.general.row.shellToolPartsExpanded.title")}
description={language.t("settings.general.row.shellToolPartsExpanded.description")}
>
<div data-action="settings-feed-shell-tool-parts-expanded">
<Switch
checked={settings.general.shellToolPartsExpanded()}
onChange={(checked) => settings.general.setShellToolPartsExpanded(checked)}
/>
</div>
</SettingsRow>
<SettingsRow
title={language.t("settings.general.row.editToolPartsExpanded.title")}
description={language.t("settings.general.row.editToolPartsExpanded.description")}
>
<div data-action="settings-feed-edit-tool-parts-expanded">
<Switch
checked={settings.general.editToolPartsExpanded()}
onChange={(checked) => settings.general.setEditToolPartsExpanded(checked)}
/>
</div>
</SettingsRow>
</SettingsList>
</div>
)
const AdvancedSection = () => (
<div class="flex flex-col gap-1">
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.advanced")}</h3>
<SettingsList>
<SettingsRow
title={language.t("settings.general.row.showFileTree.title")}
description={language.t("settings.general.row.showFileTree.description")}
>
<div data-action="settings-show-file-tree">
<Switch
checked={settings.general.showFileTree()}
onChange={(checked) => settings.general.setShowFileTree(checked)}
/>
</div>
</SettingsRow>
<SettingsRow
title={language.t("settings.general.row.showNavigation.title")}
description={language.t("settings.general.row.showNavigation.description")}
>
<div data-action="settings-show-navigation">
<Switch
checked={settings.general.showNavigation()}
onChange={(checked) => settings.general.setShowNavigation(checked)}
/>
</div>
</SettingsRow>
<SettingsRow
title={language.t("settings.general.row.showSearch.title")}
description={language.t("settings.general.row.showSearch.description")}
>
<div data-action="settings-show-search">
<Switch
checked={settings.general.showSearch()}
onChange={(checked) => settings.general.setShowSearch(checked)}
/>
</div>
</SettingsRow>
<SettingsRow
title={language.t("settings.general.row.showStatus.title")}
description={language.t("settings.general.row.showStatus.description")}
>
<div data-action="settings-show-status">
<Switch
checked={settings.general.showStatus()}
onChange={(checked) => settings.general.setShowStatus(checked)}
/>
</div>
</SettingsRow>
<SettingsRow
title={language.t("settings.general.row.showCustomAgents.title")}
description={language.t("settings.general.row.showCustomAgents.description")}
>
<div data-action="settings-show-custom-agents">
<Switch
checked={settings.general.showCustomAgents()}
onChange={(checked) => settings.general.setShowCustomAgents(checked)}
/>
</div>
</SettingsRow>
</SettingsList>
</div>
)
const AppearanceSection = () => (
<div class="flex flex-col gap-1">
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.appearance")}</h3>
<SettingsList>
<SettingsRow
title={language.t("settings.general.row.colorScheme.title")}
description={language.t("settings.general.row.colorScheme.description")}
>
<Select
data-action="settings-color-scheme"
options={colorSchemeOptions()}
current={colorSchemeOptions().find((o) => o.value === theme.colorScheme())}
value={(o) => o.value}
label={(o) => o.label}
onSelect={(option) => option && theme.setColorScheme(option.value)}
variant="secondary"
size="small"
triggerVariant="settings"
triggerStyle={{ "min-width": "220px" }}
/>
</SettingsRow>
<SettingsRow
title={language.t("settings.general.row.theme.title")}
description={
<>
{language.t("settings.general.row.theme.description")}{" "}
<Link href="https://opencode.ai/docs/themes/">{language.t("common.learnMore")}</Link>
</>
}
>
<Select
data-action="settings-theme"
options={themeOptions()}
current={themeOptions().find((o) => o.id === theme.themeId())}
value={(o) => o.id}
label={(o) => o.name}
onSelect={(option) => {
if (!option) return
theme.setTheme(option.id)
}}
variant="secondary"
size="small"
triggerVariant="settings"
/>
</SettingsRow>
<SettingsRow
title={language.t("settings.general.row.uiFont.title")}
description={language.t("settings.general.row.uiFont.description")}
>
<div class="w-full sm:w-[220px]">
<TextField
data-action="settings-ui-font"
label={language.t("settings.general.row.uiFont.title")}
hideLabel
type="text"
value={sans()}
onChange={(value) => settings.appearance.setUIFont(value)}
placeholder={sansDefault}
spellcheck={false}
autocorrect="off"
autocomplete="off"
autocapitalize="off"
class="text-12-regular"
style={{ "font-family": sansFontFamily(settings.appearance.uiFont()) }}
/>
</div>
</SettingsRow>
<SettingsRow
title={language.t("settings.general.row.font.title")}
description={language.t("settings.general.row.font.description")}
>
<div class="w-full sm:w-[220px]">
<TextField
data-action="settings-code-font"
label={language.t("settings.general.row.font.title")}
hideLabel
type="text"
value={mono()}
onChange={(value) => settings.appearance.setFont(value)}
placeholder={monoDefault}
spellcheck={false}
autocorrect="off"
autocomplete="off"
autocapitalize="off"
class="text-12-regular"
style={{ "font-family": monoFontFamily(settings.appearance.font()) }}
/>
</div>
</SettingsRow>
<SettingsRow
title={language.t("settings.general.row.terminalFont.title")}
description={language.t("settings.general.row.terminalFont.description")}
>
<div class="w-full sm:w-[220px]">
<TextField
data-action="settings-terminal-font"
label={language.t("settings.general.row.terminalFont.title")}
hideLabel
type="text"
value={terminal()}
onChange={(value) => settings.appearance.setTerminalFont(value)}
placeholder={terminalDefault}
spellcheck={false}
autocorrect="off"
autocomplete="off"
autocapitalize="off"
class="text-12-regular"
style={{ "font-family": terminalFontFamily(settings.appearance.terminalFont()) }}
/>
</div>
</SettingsRow>
</SettingsList>
</div>
)
const NotificationsSection = () => (
<div class="flex flex-col gap-1">
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.notifications")}</h3>
<SettingsList>
<SettingsRow
title={language.t("settings.general.notifications.agent.title")}
description={language.t("settings.general.notifications.agent.description")}
>
<div data-action="settings-notifications-agent">
<Switch
checked={settings.notifications.agent()}
onChange={(checked) => settings.notifications.setAgent(checked)}
/>
</div>
</SettingsRow>
<SettingsRow
title={language.t("settings.general.notifications.permissions.title")}
description={language.t("settings.general.notifications.permissions.description")}
>
<div data-action="settings-notifications-permissions">
<Switch
checked={settings.notifications.permissions()}
onChange={(checked) => settings.notifications.setPermissions(checked)}
/>
</div>
</SettingsRow>
<SettingsRow
title={language.t("settings.general.notifications.errors.title")}
description={language.t("settings.general.notifications.errors.description")}
>
<div data-action="settings-notifications-errors">
<Switch
checked={settings.notifications.errors()}
onChange={(checked) => settings.notifications.setErrors(checked)}
/>
</div>
</SettingsRow>
</SettingsList>
</div>
)
const SoundsSection = () => (
<div class="flex flex-col gap-1">
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.sounds")}</h3>
<SettingsList>
<SettingsRow
title={language.t("settings.general.sounds.agent.title")}
description={language.t("settings.general.sounds.agent.description")}
>
<Select
data-action="settings-sounds-agent"
{...soundSelectProps(
() => settings.sounds.agentEnabled(),
() => settings.sounds.agent(),
(value) => settings.sounds.setAgentEnabled(value),
(id) => settings.sounds.setAgent(id),
)}
/>
</SettingsRow>
<SettingsRow
title={language.t("settings.general.sounds.permissions.title")}
description={language.t("settings.general.sounds.permissions.description")}
>
<Select
data-action="settings-sounds-permissions"
{...soundSelectProps(
() => settings.sounds.permissionsEnabled(),
() => settings.sounds.permissions(),
(value) => settings.sounds.setPermissionsEnabled(value),
(id) => settings.sounds.setPermissions(id),
)}
/>
</SettingsRow>
<SettingsRow
title={language.t("settings.general.sounds.errors.title")}
description={language.t("settings.general.sounds.errors.description")}
>
<Select
data-action="settings-sounds-errors"
{...soundSelectProps(
() => settings.sounds.errorsEnabled(),
() => settings.sounds.errors(),
(value) => settings.sounds.setErrorsEnabled(value),
(id) => settings.sounds.setErrors(id),
)}
/>
</SettingsRow>
</SettingsList>
</div>
)
const UpdatesSection = () => (
<div class="flex flex-col gap-1">
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.updates")}</h3>
<SettingsList>
<SettingsRow
title={language.t("settings.general.row.releaseNotes.title")}
description={language.t("settings.general.row.releaseNotes.description")}
>
<div data-action="settings-release-notes">
<Switch
checked={settings.general.releaseNotes()}
onChange={(checked) => settings.general.setReleaseNotes(checked)}
/>
</div>
</SettingsRow>
<SettingsRow
title={language.t("settings.updates.row.check.title")}
description={language.t("settings.updates.row.check.description")}
>
<Button size="small" variant="secondary" disabled={!updater.action().run} onClick={updater.run}>
{language.t(updater.action().label)}
</Button>
</SettingsRow>
</SettingsList>
</div>
)
const DisplaySection = () => (
<Show when={desktop()}>
<div class="flex flex-col gap-1">
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.display")}</h3>
<SettingsList>
<SettingsRow
title={language.t("settings.general.row.pinchZoom.title")}
description={language.t("settings.general.row.pinchZoom.description")}
>
<div data-action="settings-pinch-zoom">
<Switch checked={pinchZoom.latest} onChange={onPinchZoomChange} />
</div>
</SettingsRow>
<Show when={linux()}>
<SettingsRow
title={
<div class="flex items-center gap-2">
<span>{language.t("settings.general.row.wayland.title")}</span>
<Tooltip value={language.t("settings.general.row.wayland.tooltip")} placement="top">
<span class="text-text-weak">
<Icon name="help" size="small" />
</span>
</Tooltip>
</div>
}
description={language.t("settings.general.row.wayland.description")}
>
<div data-action="settings-wayland">
<Switch checked={displayBackend.latest === "wayland"} onChange={onDisplayBackendChange} />
</div>
</SettingsRow>
</Show>
</SettingsList>
</div>
</Show>
)
return (
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
<div class="flex flex-col gap-1 pt-6 pb-8">
<h2 class="text-16-medium text-text-strong">{language.t("settings.tab.general")}</h2>
</div>
</div>
<div class="flex flex-col gap-8 w-full">
<Show when={settings.general.layoutTransitionAvailable()}>
<InterfaceSection />
</Show>
<Show when={settings.general.newInterfaceNoticeVisible()}>
<InterfaceNoticeSection />
</Show>
<GeneralSection />
<AppearanceSection />
<NotificationsSection />
<SoundsSection />
<UpdatesSection />
<DisplaySection />
<Show when={desktop()}>
<AdvancedSection />
</Show>
</div>
</div>
)
}
interface SettingsRowProps {
title: string | JSX.Element
description: string | JSX.Element
children: JSX.Element
}
const SettingsRow: Component<SettingsRowProps> = (props) => {
return (
<div class="flex flex-wrap items-center gap-4 py-3 border-b border-border-weak-base last:border-none sm:flex-nowrap">
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
<span class="text-14-medium text-text-strong">{props.title}</span>
<span class="text-12-regular text-text-weak">{props.description}</span>
</div>
<div class="flex w-full justify-end sm:w-auto sm:shrink-0">{props.children}</div>
</div>
)
}
@@ -0,0 +1,149 @@
import { useFilteredList } from "@opencode-ai/ui/hooks"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { Switch } from "@opencode-ai/ui/switch"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { TextField } from "@opencode-ai/ui/text-field"
import { type Component, For, Show } from "solid-js"
import { useLanguage } from "@/context/language"
import { useModels } from "@/context/models"
import { popularProviders } from "@/hooks/use-providers"
import { SettingsList } from "./settings-list"
import { SettingsServerPicker, SettingsServerScope } from "./settings-server-picker"
type ModelItem = ReturnType<ReturnType<typeof useModels>["list"]>[number]
const ListLoadingState: Component<{ label: string }> = (props) => {
return (
<div class="flex flex-col items-center justify-center py-12 text-center">
<span class="text-14-regular text-text-weak">{props.label}</span>
</div>
)
}
const ListEmptyState: Component<{ message: string; filter: string }> = (props) => {
return (
<div class="flex flex-col items-center justify-center py-12 text-center">
<span class="text-14-regular text-text-weak">{props.message}</span>
<Show when={props.filter}>
<span class="text-14-regular text-text-strong mt-1">&quot;{props.filter}&quot;</span>
</Show>
</div>
)
}
export const SettingsModels: Component = () => {
return (
<SettingsServerScope>
<SettingsModelsContent />
</SettingsServerScope>
)
}
const SettingsModelsContent: Component = () => {
const language = useLanguage()
const models = useModels()
const list = useFilteredList<ModelItem>({
items: (_filter) => models.list(),
key: (x) => `${x.provider.id}:${x.id}`,
filterKeys: ["provider.name", "name", "id"],
sortBy: (a, b) => a.name.localeCompare(b.name),
groupBy: (x) => x.provider.id,
sortGroupsBy: (a, b) => {
const aIndex = popularProviders.indexOf(a.category)
const bIndex = popularProviders.indexOf(b.category)
const aPopular = aIndex >= 0
const bPopular = bIndex >= 0
if (aPopular && !bPopular) return -1
if (!aPopular && bPopular) return 1
if (aPopular && bPopular) return aIndex - bIndex
const aName = a.items[0].provider.name
const bName = b.items[0].provider.name
return aName.localeCompare(bName)
},
})
return (
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
<div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
<div class="flex items-center justify-between gap-4">
<h2 class="text-16-medium text-text-strong">{language.t("settings.models.title")}</h2>
<SettingsServerPicker />
</div>
<div class="flex items-center gap-2 px-3 h-9 rounded-lg bg-surface-base">
<Icon name="magnifying-glass" class="text-icon-weak-base flex-shrink-0" />
<TextField
variant="ghost"
type="text"
value={list.filter()}
onChange={list.onInput}
placeholder={language.t("dialog.model.search.placeholder")}
spellcheck={false}
autocorrect="off"
autocomplete="off"
autocapitalize="off"
class="flex-1"
/>
<Show when={list.filter()}>
<IconButton icon="circle-x" variant="ghost" onClick={list.clear} />
</Show>
</div>
</div>
</div>
<div class="flex flex-col gap-8 max-w-[720px]">
<Show
when={!list.grouped.loading}
fallback={
<ListLoadingState label={`${language.t("common.loading")}${language.t("common.loading.ellipsis")}`} />
}
>
<Show
when={list.flat().length > 0}
fallback={<ListEmptyState message={language.t("dialog.model.empty")} filter={list.filter()} />}
>
<For each={list.grouped.latest}>
{(group) => (
<div class="flex flex-col gap-1">
<div class="flex items-center gap-2 pb-2">
<ProviderIcon id={group.category} class="size-5 shrink-0 icon-strong-base" />
<span class="text-14-medium text-text-strong">{group.items[0].provider.name}</span>
</div>
<SettingsList>
<For each={group.items}>
{(item) => {
const key = { providerID: item.provider.id, modelID: item.id }
return (
<div class="flex flex-wrap items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
<div class="min-w-0">
<span class="text-14-regular text-text-strong truncate block">{item.name}</span>
</div>
<div class="flex-shrink-0">
<Switch
checked={models.visible(key)}
onChange={(checked) => {
models.setVisibility(key, checked)
}}
hideLabel
>
{item.name}
</Switch>
</div>
</div>
)
}}
</For>
</SettingsList>
</div>
)}
</For>
</Show>
</Show>
</div>
</div>
)
}
@@ -0,0 +1,259 @@
import { Button } from "@opencode-ai/ui/button"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { Tag } from "@opencode-ai/ui/tag"
import { showToast } from "@/utils/toast"
import { popularProviders, useProviders } from "@/hooks/use-providers"
import { createMemo, type Component, For, Show } from "solid-js"
import { useLanguage } from "@/context/language"
import { useServerSDK } from "@/context/server-sdk"
import { useServerSync } from "@/context/server-sync"
import { DialogConnectProvider, useProviderConnectController } from "./dialog-connect-provider"
import { DialogCustomProvider } from "./dialog-custom-provider"
import { SettingsList } from "./settings-list"
import { SettingsServerPicker, SettingsServerScope } from "./settings-server-picker"
type ProviderSource = "env" | "api" | "config" | "custom"
type ProviderItem = ReturnType<ReturnType<typeof useProviders>["connected"]>[number]
const PROVIDER_NOTES = [
{ match: (id: string) => id === "opencode", key: "dialog.provider.opencode.note" },
{ match: (id: string) => id === "opencode-go", key: "dialog.provider.opencodeGo.tagline" },
{ match: (id: string) => id === "anthropic", key: "dialog.provider.anthropic.note" },
{ match: (id: string) => id.startsWith("github-copilot"), key: "dialog.provider.copilot.note" },
{ match: (id: string) => id === "openai", key: "dialog.provider.openai.note" },
{ match: (id: string) => id === "google", key: "dialog.provider.google.note" },
{ match: (id: string) => id === "openrouter", key: "dialog.provider.openrouter.note" },
{ match: (id: string) => id === "vercel", key: "dialog.provider.vercel.note" },
] as const
export const SettingsProviders: Component<{ onBack?: () => void }> = (props) => {
return (
<SettingsServerScope>
<SettingsProvidersContent onBack={props.onBack} />
</SettingsServerScope>
)
}
const SettingsProvidersContent: Component<{ onBack?: () => void }> = (props) => {
const dialog = useDialog()
const language = useLanguage()
const serverSDK = useServerSDK()
const serverSync = useServerSync()
const providers = useProviders(() => undefined)
const providerConnect = useProviderConnectController({ onBack: props.onBack })
const connect = (provider?: string) => {
providerConnect.select(provider)
void dialog.show(() => <DialogConnectProvider controller={providerConnect} />)
}
const connected = createMemo(() => {
return providers
.connected()
.filter((p) => p.id !== "opencode" || Object.values(p.models).find((m) => m.cost?.input))
})
const popular = createMemo(() => {
const connectedIDs = new Set(connected().map((p) => p.id))
const items = providers
.popular()
.filter((p) => !connectedIDs.has(p.id))
.slice()
items.sort((a, b) => popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id))
return items
})
const source = (item: ProviderItem): ProviderSource | undefined => {
if (!("source" in item)) return
const value = item.source
if (value === "env" || value === "api" || value === "config" || value === "custom") return value
return
}
const type = (item: ProviderItem) => {
const current = source(item)
if (current === "env") return language.t("settings.providers.tag.environment")
if (current === "api") return language.t("provider.connect.method.apiKey")
if (current === "config") {
if (isConfigCustom(item.id)) return language.t("settings.providers.tag.custom")
return language.t("settings.providers.tag.config")
}
if (current === "custom") return language.t("settings.providers.tag.custom")
return language.t("settings.providers.tag.other")
}
const canDisconnect = (item: ProviderItem) => source(item) !== "env" && !isConfigCustom(item.id)
const note = (id: string) => PROVIDER_NOTES.find((item) => item.match(id))?.key
const isConfigCustom = (providerID: string) => {
const provider = serverSync().data.config.provider?.[providerID]
if (!provider) return false
if (provider.npm !== "@ai-sdk/openai-compatible") return false
if (!provider.models || Object.keys(provider.models).length === 0) return false
return true
}
const disableProvider = async (providerID: string, name: string) => {
return
const before = serverSync().data.config.disabled_providers ?? []
const next = before.includes(providerID) ? before : [...before, providerID]
serverSync().set("config", "disabled_providers", next)
await serverSync()
.updateConfig({ disabled_providers: next })
.then(() => {
showToast({
variant: "success",
icon: "circle-check",
title: language.t("provider.disconnect.toast.disconnected.title", { provider: name }),
description: language.t("provider.disconnect.toast.disconnected.description", { provider: name }),
})
})
.catch((err: unknown) => {
serverSync().set("config", "disabled_providers", before)
const message = err instanceof Error ? err.message : String(err)
showToast({ title: language.t("common.requestFailed"), description: message })
})
}
const disconnect = async (providerID: string, name: string) => {
await serverSDK()
.api.integration.get({ integrationID: providerID })
.then(async (integration) => {
const credentials = integration.data?.connections.filter((item) => item.type === "credential") ?? []
if (credentials.length === 0) throw new Error(`No removable credentials found for ${name}`)
await Promise.all(
credentials.map((credential) => serverSDK().api.credential.remove({ credentialID: credential.id })),
)
showToast({
variant: "success",
icon: "circle-check",
title: language.t("provider.disconnect.toast.disconnected.title", { provider: name }),
description: language.t("provider.disconnect.toast.disconnected.description", { provider: name }),
})
})
.catch((err: unknown) => {
const message = err instanceof Error ? err.message : String(err)
showToast({ title: language.t("common.requestFailed"), description: message })
})
}
return (
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
<div class="flex items-center justify-between gap-4 pt-6 pb-8 max-w-[720px]">
<h2 class="text-16-medium text-text-strong">{language.t("settings.providers.title")}</h2>
<SettingsServerPicker />
</div>
</div>
<div class="flex flex-col gap-8 max-w-[720px]">
<div class="flex flex-col gap-1" data-component="connected-providers-section">
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.providers.section.connected")}</h3>
<SettingsList>
<Show
when={connected().length > 0}
fallback={
<div class="py-4 text-14-regular text-text-weak">
{language.t("settings.providers.connected.empty")}
</div>
}
>
<For each={connected()}>
{(item) => (
<div class="group flex flex-wrap items-center justify-between gap-4 min-h-16 py-3 border-b border-border-weak-base last:border-none">
<div class="flex items-center gap-3 min-w-0">
<ProviderIcon id={item.id} class="size-5 shrink-0 icon-strong-base" />
<span class="text-14-medium text-text-strong truncate">{item.name}</span>
<Tag>{type(item)}</Tag>
</div>
<Show
when={canDisconnect(item)}
fallback={
<span class="text-14-regular text-text-base opacity-0 group-hover:opacity-100 transition-opacity duration-200 pr-3 cursor-default">
{language.t("settings.providers.connected.environmentDescription")}
</span>
}
>
<Button size="large" variant="ghost" onClick={() => void disconnect(item.id, item.name)}>
{language.t("common.disconnect")}
</Button>
</Show>
</div>
)}
</For>
</Show>
</SettingsList>
</div>
<div class="flex flex-col gap-1">
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.providers.section.popular")}</h3>
<SettingsList>
<For each={popular()}>
{(item) => (
<div class="flex flex-wrap items-center justify-between gap-4 min-h-16 py-3 border-b border-border-weak-base last:border-none">
<div class="flex flex-col min-w-0">
<div class="flex items-center gap-x-3">
<ProviderIcon id={item.id} class="size-5 shrink-0 icon-strong-base" />
<span class="text-14-medium text-text-strong">{item.name}</span>
<Show when={item.id === "opencode"}>
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
</Show>
<Show when={item.id === "opencode-go"}>
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
</Show>
</div>
<Show when={note(item.id)}>
{(key) => <span class="text-12-regular text-text-weak pl-8">{language.t(key())}</span>}
</Show>
</div>
<Button size="large" variant="secondary" icon="plus-small" onClick={() => connect(item.id)}>
{language.t("common.connect")}
</Button>
</div>
)}
</For>
<Show when={false}>
<div
class="flex items-center justify-between gap-4 min-h-16 border-b border-border-weak-base last:border-none flex-wrap py-3"
data-component="custom-provider-section"
>
<div class="flex flex-col min-w-0">
<div class="flex flex-wrap items-center gap-x-3 gap-y-1">
<ProviderIcon id="synthetic" class="size-5 shrink-0 icon-strong-base" />
<span class="text-14-medium text-text-strong">{language.t("provider.custom.title")}</span>
<Tag>{language.t("settings.providers.tag.custom")}</Tag>
</div>
<span class="text-12-regular text-text-weak pl-8">
{language.t("settings.providers.custom.description")}
</span>
</div>
<Button
size="large"
variant="secondary"
icon="plus-small"
onClick={() => {
dialog.show(() => <DialogCustomProvider onBack={dialog.close} />)
}}
>
{language.t("common.connect")}
</Button>
</div>
</Show>
</SettingsList>
<Button
variant="ghost"
class="px-0 py-0 mt-5 text-14-medium text-text-interactive-base text-left justify-start hover:bg-transparent active:bg-transparent"
onClick={() => connect()}
>
{language.t("dialog.provider.viewAll")}
</Button>
</div>
</div>
</div>
)
}
@@ -0,0 +1,106 @@
import { Button } from "@opencode-ai/ui/button"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { Icon } from "@opencode-ai/ui/icon"
import { QueryClientProvider } from "@tanstack/solid-query"
import { createMemo, For, type ParentProps, Show } from "solid-js"
import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
import { ModelsProvider } from "@/context/models"
import { ServerConnection } from "@/context/server"
import { ServerSDKProvider } from "@/context/server-sdk"
import { ServerSyncProvider } from "@/context/server-sync"
import { useGlobal } from "@/context/global"
import { useSettings } from "@/context/settings"
export function SettingsServerScope(props: ParentProps) {
const global = useGlobal()
const settings = useSettings()
return (
<Show when={settings.general.newLayoutDesigns()} fallback={props.children}>
<Show when={global.settings.server.selected()}>
{(server) => <SettingsServerDataProviders server={server()}>{props.children}</SettingsServerDataProviders>}
</Show>
</Show>
)
}
function SettingsServerDataProviders(props: ParentProps<{ server: ServerConnection.Any }>) {
const global = useGlobal()
const serverCtx = () => global.ensureServerCtx(props.server)
return (
<QueryClientProvider client={serverCtx().queryClient}>
<ServerSDKProvider server={() => props.server}>
<ServerSyncProvider>
<ModelsProvider>{props.children}</ModelsProvider>
</ServerSyncProvider>
</ServerSDKProvider>
</QueryClientProvider>
)
}
export function SettingsServerPicker() {
const global = useGlobal()
const settings = useSettings()
const selected = createMemo(() =>
settings.general.newLayoutDesigns() ? global.settings.server.selected() : undefined,
)
return (
<Show when={selected()}>
{(conn) => (
<DropdownMenu gutter={4} placement="bottom-end">
<DropdownMenu.Trigger
as={Button}
variant="secondary"
size="large"
class="h-8 max-w-[260px] gap-2 px-2 py-1.5 data-[expanded]:bg-surface-base-active"
>
<ServerHealthIndicator health={global.servers.health[ServerConnection.key(conn())]} />
<ServerRow
conn={conn()}
status={global.servers.health[ServerConnection.key(conn())]}
class="flex items-center gap-2 min-w-0 flex-1"
nameClass="text-14-regular text-text-base truncate"
versionClass="hidden"
/>
<Icon name="chevron-down" size="small" class="text-icon-weak shrink-0" />
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content class="w-[320px] mt-1 [&_[data-slot=dropdown-menu-radio-item]]:pl-2 [&_[data-slot=dropdown-menu-radio-item]]:pr-2">
<DropdownMenu.RadioGroup
value={global.settings.server.key}
onChange={(key) => {
if (typeof key === "string") global.settings.server.set(ServerConnection.Key.make(key))
}}
>
<For each={global.servers.list()}>
{(item) => {
const key = ServerConnection.key(item)
const blocked = () => global.servers.health[key]?.healthy === false
return (
<DropdownMenu.RadioItem value={key} disabled={blocked()}>
<ServerHealthIndicator health={global.servers.health[key]} />
<ServerRow
conn={item}
dimmed={blocked()}
status={global.servers.health[key]}
class="flex items-center gap-2 min-w-0 flex-1"
nameClass="text-14-regular text-text-base truncate"
versionClass="text-12-regular text-text-weak truncate"
/>
<DropdownMenu.ItemIndicator>
<Icon name="check-small" size="small" class="text-icon-weak" />
</DropdownMenu.ItemIndicator>
</DropdownMenu.RadioItem>
)
}}
</For>
</DropdownMenu.RadioGroup>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
)}
</Show>
)
}
@@ -0,0 +1,33 @@
import { Show, type Component } from "solid-js"
import { useLanguage } from "@/context/language"
import { ServerConnectionForm, ServerConnectionList, useServerManagementController } from "./dialog-select-server"
export const SettingsServers: Component = () => {
const language = useLanguage()
const controller = useServerManagementController()
return (
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
<div class="flex flex-col flex-1 min-h-0 max-w-[720px]">
<Show
when={controller.isFormMode()}
fallback={
<>
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
<div class="flex flex-col gap-1 pt-6 pb-8">
<h2 class="text-16-medium text-text-strong">{language.t("status.popover.tab.servers")}</h2>
</div>
</div>
<ServerConnectionList controller={controller} />
</>
}
>
<div class="flex flex-1 min-h-0 flex-col gap-4 pt-6">
<div class="text-16-medium text-text-strong">{controller.formTitle()}</div>
<ServerConnectionForm controller={controller} />
</div>
</Show>
</div>
</div>
)
}
@@ -5,6 +5,7 @@ import { SelectV2 } from "@opencode-ai/ui/v2/select-v2"
import { Switch } from "@opencode-ai/ui/v2/switch-v2" import { Switch } from "@opencode-ai/ui/v2/switch-v2"
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2" import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context" import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { usePermission } from "@/context/permission" import { usePermission } from "@/context/permission"
import { usePlatform } from "@/context/platform" import { usePlatform } from "@/context/platform"
@@ -26,6 +27,7 @@ import { playSoundById, SOUND_OPTIONS } from "@/utils/sound"
import { Link } from "../link" import { Link } from "../link"
import { SettingsListV2 } from "./parts/list" import { SettingsListV2 } from "./parts/list"
import { SettingsRowV2 } from "./parts/row" import { SettingsRowV2 } from "./parts/row"
import { LayoutRetirementNotice, LayoutTransitionToggle } from "./interface-transition"
import "./settings-v2.css" import "./settings-v2.css"
let demoSoundState = { let demoSoundState = {
@@ -85,6 +87,7 @@ export const SettingsGeneralV2: Component<{
const language = useLanguage() const language = useLanguage()
const permission = usePermission() const permission = usePermission()
const platform = usePlatform() const platform = usePlatform()
const dialog = useDialog()
const settings = useSettings() const settings = useSettings()
const serverSync = useServerSync() const serverSync = useServerSync()
const mobile = createMediaQuery("(max-width: 767px)") const mobile = createMediaQuery("(max-width: 767px)")
@@ -222,6 +225,31 @@ export const SettingsGeneralV2: Component<{
}, },
}) })
const InterfaceSection = () => (
<LayoutTransitionToggle
title={language.t("settings.general.row.newInterface.title")}
badge={language.t("settings.general.row.newInterface.badge")}
description={language.t("settings.general.row.newInterface.description")}
checked={settings.general.newLayoutDesigns()}
onChange={(checked) => {
settings.general.setNewLayoutDesigns(checked)
if (checked) return
void import("@/components/dialog-settings").then((module) => {
void dialog.show(() => <module.DialogSettings />)
})
}}
/>
)
const InterfaceNoticeSection = () => (
<LayoutRetirementNotice
title={language.t("settings.general.row.newInterfaceNotice.title")}
description={language.t("settings.general.row.newInterfaceNotice.description")}
dismiss={language.t("settings.general.row.newInterfaceNotice.dismiss")}
onDismiss={settings.general.dismissNewInterfaceNotice}
/>
)
const GeneralSection = () => ( const GeneralSection = () => (
<div class="settings-v2-section"> <div class="settings-v2-section">
<SettingsListV2> <SettingsListV2>
@@ -661,6 +689,14 @@ export const SettingsGeneralV2: Component<{
</div> </div>
<div class="settings-v2-tab-body"> <div class="settings-v2-tab-body">
<Show when={settings.general.layoutTransitionAvailable()}>
<InterfaceSection />
</Show>
<Show when={settings.general.newInterfaceNoticeVisible()}>
<InterfaceNoticeSection />
</Show>
<GeneralSection /> <GeneralSection />
<AppearanceSection /> <AppearanceSection />
@@ -0,0 +1,76 @@
// @ts-nocheck
import { Show } from "solid-js"
import { createStore } from "solid-js/store"
import { LayoutRetirementNotice, LayoutTransitionToggle } from "./interface-transition"
const copy = {
title: "New layout",
badge: "New",
description: "Use the new tabs and home layout. Switch between layouts for a limited time.",
noticeTitle: "You're now using new layout",
noticeDescription: "The previous layout is no longer available",
dismiss: "Dismiss",
}
function Frame(props) {
return <div class="w-[640px] max-w-full">{props.children}</div>
}
function ToggleExample(props) {
const [state, setState] = createStore({ checked: props.checked })
return (
<Frame>
<LayoutTransitionToggle
title={copy.title}
badge={copy.badge}
description={copy.description}
checked={state.checked}
onChange={(checked) => setState("checked", checked)}
/>
</Frame>
)
}
function NoticeExample() {
const [state, setState] = createStore({ dismissed: false })
return (
<Frame>
<Show when={!state.dismissed} fallback={<span class="text-v2-text-text-muted">Notice dismissed</span>}>
<LayoutRetirementNotice
title={copy.noticeTitle}
description={copy.noticeDescription}
dismiss={copy.dismiss}
onDismiss={() => setState("dismissed", true)}
/>
</Show>
</Frame>
)
}
export default {
title: "App/Settings/Layout transition",
id: "app-settings-layout-transition",
component: LayoutTransitionToggle,
}
export const NewLayoutEnabled = {
render: () => <ToggleExample checked />,
}
export const PreviousLayoutEnabled = {
render: () => <ToggleExample checked={false} />,
}
export const PreviousLayoutRetired = {
render: () => <NoticeExample />,
}
export const AllStates = {
render: () => (
<div class="flex flex-col gap-8">
<ToggleExample checked />
<ToggleExample checked={false} />
<NoticeExample />
</div>
),
}
@@ -0,0 +1,54 @@
import { Tag } from "@opencode-ai/ui/v2/badge-v2"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { Switch } from "@opencode-ai/ui/v2/switch-v2"
import { SettingsListV2 } from "./parts/list"
import { SettingsRowV2 } from "./parts/row"
export function LayoutTransitionToggle(props: {
title: string
badge: string
description: string
checked: boolean
onChange: (checked: boolean) => void
}) {
return (
<div class="settings-v2-section">
<div class="settings-v2-interface-feature">
<SettingsListV2>
<SettingsRowV2
title={
<span class="flex items-center gap-2">
{props.title}
<Tag variant="accent">{props.badge}</Tag>
</span>
}
description={props.description}
>
<div data-action="settings-new-layout-designs">
<Switch checked={props.checked} onChange={props.onChange} />
</div>
</SettingsRowV2>
</SettingsListV2>
</div>
</div>
)
}
export function LayoutRetirementNotice(props: {
title: string
description: string
dismiss: string
onDismiss: () => void
}) {
return (
<div class="settings-v2-section">
<SettingsListV2>
<SettingsRowV2 title={props.title} description={props.description}>
<ButtonV2 size="small" variant="ghost-muted" onClick={props.onDismiss}>
{props.dismiss}
</ButtonV2>
</SettingsRowV2>
</SettingsListV2>
</div>
)
}
@@ -153,88 +153,4 @@ describe("v2 session reducer", () => {
expect(result).toMatchObject({ sessionID: "ses_1", missing: "msg_user", touched: [] }) expect(result).toMatchObject({ sessionID: "ses_1", missing: "msg_user", touched: [] })
}) })
test("removes cancelled input from the pending promotion fold", () => {
const reducer = createV2SessionReducer()
reducer.reduce(
[],
event({
...base,
id: "evt_admitted",
type: "session.input.admitted",
data: {
sessionID: "ses_1",
inputID: "msg_user",
input: { type: "user", delivery: "queue", data: { text: "cancel me" } },
},
}),
)
reducer.reduce(
[],
event({
...base,
id: "evt_cancelled",
type: "session.input.cancelled",
data: { sessionID: "ses_1", inputID: "msg_user" },
}),
)
const result = reducer.reduce(
[],
event({
...base,
id: "evt_promoted",
type: "session.input.promoted",
data: { sessionID: "ses_1", inputID: "msg_user" },
}),
)
expect(result).toMatchObject({ missing: "msg_user" })
})
test("keeps steered input available to the promotion fold", () => {
const reducer = createV2SessionReducer()
reducer.reduce(
[],
event({
...base,
id: "evt_admitted",
type: "session.input.admitted",
data: {
sessionID: "ses_1",
inputID: "msg_user",
input: { type: "user", delivery: "queue", data: { text: "steer me" } },
},
}),
)
reducer.reduce(
[],
event({
...base,
id: "evt_steered",
type: "session.input.steered",
data: { sessionID: "ses_1", inputID: "msg_user" },
}),
)
reducer.reduce(
[],
event({
...base,
id: "evt_queued",
type: "session.input.queued",
data: { sessionID: "ses_1", inputID: "msg_user" },
}),
)
const result = reducer.reduce(
[],
event({
...base,
id: "evt_promoted",
type: "session.input.promoted",
data: { sessionID: "ses_1", inputID: "msg_user" },
}),
)
expect(result?.messages).toMatchObject([{ id: "msg_user", type: "user", text: "steer me" }])
})
}) })
@@ -29,9 +29,6 @@ export function createV2SessionReducer() {
case "session.input.admitted": case "session.input.admitted":
pending.set(key(sessionID, event.data.inputID), event.data.input) pending.set(key(sessionID, event.data.inputID), event.data.input)
return result([...source]) return result([...source])
case "session.input.cancelled":
pending.delete(key(sessionID, event.data.inputID))
return
case "session.input.promoted": { case "session.input.promoted": {
const input = pending.get(key(sessionID, event.data.inputID)) const input = pending.get(key(sessionID, event.data.inputID))
pending.delete(key(sessionID, event.data.inputID)) pending.delete(key(sessionID, event.data.inputID))
+79
View File
@@ -0,0 +1,79 @@
import { describe, expect, test } from "bun:test"
import {
hasExistingWebState,
isAppUpgrade,
layoutTransitionState,
maximumSunsetTimeout,
newLayoutDesignsDefault,
nextSunsetCheckDelay,
resolveNewLayoutDesigns,
shouldDisplayTabsToast,
shouldEnableNewLayout,
} from "./settings"
describe("layout transition", () => {
test("blank profiles default to the new layout", () => {
expect(newLayoutDesignsDefault).toBe(true)
})
test("hides the transition until a sunset is scheduled", () => {
expect(layoutTransitionState(false, true, false, false)).toEqual({ available: false, notice: false })
})
test("existing profiles can switch before sunset", () => {
expect(layoutTransitionState(true, true, false, false)).toEqual({ available: true, notice: false })
})
test("classifies web profiles from existing settings or a recorded version", () => {
expect(hasExistingWebState("{}", undefined)).toBe(true)
expect(hasExistingWebState(null, "1.17.19")).toBe(true)
expect(hasExistingWebState(null, undefined)).toBe(false)
})
test("preserves explicit and default layout preferences", () => {
expect(resolveNewLayoutDesigns(false, false, true)).toBe(false)
expect(resolveNewLayoutDesigns(false, undefined, false)).toBe(false)
expect(resolveNewLayoutDesigns(false, undefined, true)).toBe(true)
})
test("sunset replaces the toggle with a dismissible notice", () => {
expect(layoutTransitionState(true, true, true, false)).toEqual({ available: false, notice: true })
expect(layoutTransitionState(true, true, true, true)).toEqual({ available: false, notice: false })
expect(resolveNewLayoutDesigns(true, false)).toBe(true)
})
test("caps checks for sunsets beyond the browser timeout limit", () => {
expect(nextSunsetCheckDelay(maximumSunsetTimeout + 1_000, 0)).toBe(maximumSunsetTimeout)
expect(nextSunsetCheckDelay(10_000, 9_000)).toBe(1_000)
expect(nextSunsetCheckDelay(9_000, 10_000)).toBe(0)
})
test("enables the new layout when upgrading from 1.17.19 or earlier", () => {
expect(shouldEnableNewLayout("v1.17.19", "1.17.20")).toBe(true)
expect(shouldEnableNewLayout("1.16.9", "2.0.0")).toBe(true)
})
test("enables the new layout when no previous version was recorded", () => {
expect(shouldEnableNewLayout(undefined, "1.17.20")).toBe(true)
})
test("detects upgrades only when a previous version is older", () => {
expect(isAppUpgrade("1.17.19", "1.17.20")).toBe(true)
expect(isAppUpgrade(undefined, "1.17.20")).toBe(false)
expect(isAppUpgrade("1.17.20", "1.17.20")).toBe(false)
expect(isAppUpgrade("1.17.21", "1.17.20")).toBe(false)
})
test("shows the tabs toast for upgrades and existing installs without a recorded version", () => {
expect(shouldDisplayTabsToast("1.17.19", "1.17.20", false)).toBe(true)
expect(shouldDisplayTabsToast(undefined, "1.17.20", true)).toBe(true)
expect(shouldDisplayTabsToast(undefined, "1.17.20", false)).toBe(false)
})
test("does not enable the new layout without a qualifying upgrade", () => {
expect(shouldEnableNewLayout("1.17.19", "1.17.19")).toBe(false)
expect(shouldEnableNewLayout("1.17.20", "1.17.21")).toBe(false)
expect(shouldEnableNewLayout(undefined, "1.17.19")).toBe(false)
expect(shouldEnableNewLayout("dev", "1.17.20")).toBe(false)
})
})
+199 -7
View File
@@ -1,7 +1,8 @@
import { createStore, reconcile } from "solid-js/store" import { createStore, reconcile } from "solid-js/store"
import { createEffect, createMemo } from "solid-js" import { createEffect, createMemo, createSignal, onCleanup } from "solid-js"
import { createSimpleContext } from "@opencode-ai/ui/context" import { createSimpleContext } from "@opencode-ai/ui/context"
import { persisted } from "@/utils/persist" import { persisted } from "@/utils/persist"
import { usePlatform } from "@/context/platform"
export interface NotificationSettings { export interface NotificationSettings {
agent: boolean agent: boolean
@@ -33,6 +34,10 @@ export interface Settings {
editToolPartsExpanded: boolean editToolPartsExpanded: boolean
showCustomAgents: boolean showCustomAgents: boolean
mobileTitlebarPosition: "top" | "bottom" mobileTitlebarPosition: "top" | "bottom"
newLayoutDesigns?: boolean
layoutTransitionEligible?: boolean
newInterfaceNoticeDismissed?: boolean
shouldDisplayTabsToast?: boolean
} }
appearance: { appearance: {
fontSize: number fontSize: number
@@ -51,6 +56,75 @@ export interface Settings {
export const monoDefault = "System Mono" export const monoDefault = "System Mono"
export const sansDefault = "System Sans" export const sansDefault = "System Sans"
export const terminalDefault = "JetBrainsMono Nerd Font Mono" export const terminalDefault = "JetBrainsMono Nerd Font Mono"
const legacyNewLayoutDesignsDefault = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
export const newLayoutDesignsDefault = true
// Existing users can switch layouts until local midnight on this date. Set new Date(YYYY, M-1, D) to show.
export const oldInterfaceSunset = new Date(2026, 8, 14)
const newLayoutDesignsUpgradeCutoff = "1.17.19"
function compareVersions(a: string, b: string) {
const parse = (version: string) => {
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/i.exec(version.trim())
if (!match) return
return match.slice(1).map(Number)
}
const left = parse(a)
const right = parse(b)
if (!left || !right) return
const index = left.findIndex((part, index) => part !== right[index])
return index === -1 ? 0 : left[index]! - right[index]!
}
export function isAppUpgrade(previous: string | undefined, current: string | undefined) {
if (!previous || !current) return false
const comparison = compareVersions(current, previous)
return comparison !== undefined && comparison > 0
}
export function shouldDisplayTabsToast(
previous: string | undefined,
current: string | undefined,
existingInstall: boolean,
) {
return isAppUpgrade(previous, current) || (!previous && existingInstall)
}
export function hasExistingWebState(settings: Promise<string> | string | null, previousVersion: string | undefined) {
return settings !== null || previousVersion !== undefined
}
export function shouldEnableNewLayout(previous: string | undefined, current: string | undefined) {
if (!current) return false
const currentComparison = compareVersions(current, newLayoutDesignsUpgradeCutoff)
if (!previous) return currentComparison !== undefined && currentComparison > 0
if (!isAppUpgrade(previous, current)) return false
const previousComparison = compareVersions(previous, newLayoutDesignsUpgradeCutoff)
return (
previousComparison !== undefined &&
currentComparison !== undefined &&
previousComparison <= 0 &&
currentComparison > 0
)
}
export function layoutTransitionState(scheduled: boolean, eligible: boolean, retired: boolean, dismissed: boolean) {
return {
available: scheduled && eligible && !retired,
notice: scheduled && eligible && retired && !dismissed,
}
}
export const maximumSunsetTimeout = 2_147_483_647
export function nextSunsetCheckDelay(sunset: number, now: number) {
return Math.min(Math.max(0, sunset - now), maximumSunsetTimeout)
}
export function resolveNewLayoutDesigns(retired: boolean, preference: boolean | undefined, fallback = true) {
if (retired) return true
return preference ?? fallback
}
const monoFallback = const monoFallback =
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace' 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
const sansFallback = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif' const sansFallback = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
@@ -149,7 +223,17 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
name: "Settings", name: "Settings",
gate: false, gate: false,
init: () => { init: () => {
const [store, setStore, , ready] = persisted("settings.v3", createStore<Settings>(defaultSettings)) const platform = usePlatform()
const [store, setStore, settingsInit, ready] = persisted("settings.v3", createStore<Settings>(defaultSettings))
const [launch, setLaunch, , launchReady] = persisted(
"app-version.v1",
createStore<{ version?: string }>({ version: undefined }),
)
const [launchState, setLaunchState] = createStore({
classified: false,
migrationApplied: false,
previous: undefined as string | undefined,
})
const showFileTree = withFallback(() => store.general?.showFileTree, defaultSettings.general.showFileTree) const showFileTree = withFallback(() => store.general?.showFileTree, defaultSettings.general.showFileTree)
const showSearch = withFallback(() => store.general?.showSearch, defaultSettings.general.showSearch) const showSearch = withFallback(() => store.general?.showSearch, defaultSettings.general.showSearch)
const showStatus = withFallback(() => store.general?.showStatus, defaultSettings.general.showStatus) const showStatus = withFallback(() => store.general?.showStatus, defaultSettings.general.showStatus)
@@ -157,6 +241,93 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
() => store.general?.showCustomAgents, () => store.general?.showCustomAgents,
defaultSettings.general.showCustomAgents, defaultSettings.general.showCustomAgents,
) )
const sunset = oldInterfaceSunset
const [oldInterfaceRetired, setOldInterfaceRetired] = createSignal(sunset ? Date.now() >= sunset.getTime() : false)
const layoutTransitionClassified = createMemo(() => typeof store.general?.layoutTransitionEligible === "boolean")
const layoutTransitionEligible = withFallback(() => store.general?.layoutTransitionEligible, false)
const newInterfaceNoticeDismissed = withFallback(() => store.general?.newInterfaceNoticeDismissed, false)
const layoutUpgrade = createMemo(() =>
launchState.classified && !launchState.migrationApplied
? shouldEnableNewLayout(launchState.previous, platform.version)
: false,
)
const layoutTransition = createMemo(() =>
layoutTransitionState(!!sunset, layoutTransitionEligible(), oldInterfaceRetired(), newInterfaceNoticeDismissed()),
)
const newLayoutDesigns = createMemo(() => {
if (layoutUpgrade()) return true
if (!ready() && !oldInterfaceRetired()) return legacyNewLayoutDesignsDefault
if (!layoutTransitionClassified()) {
return resolveNewLayoutDesigns(
oldInterfaceRetired(),
store.general?.newLayoutDesigns,
legacyNewLayoutDesignsDefault,
)
}
return resolveNewLayoutDesigns(
oldInterfaceRetired(),
store.general?.newLayoutDesigns,
layoutTransitionEligible() ? legacyNewLayoutDesignsDefault : newLayoutDesignsDefault,
)
})
const visible = (preference: () => boolean) => createMemo(() => !newLayoutDesigns() || preference())
if (sunset && !oldInterfaceRetired()) {
const timeout = { current: undefined as ReturnType<typeof setTimeout> | undefined }
const checkSunset = () => {
if (Date.now() >= sunset.getTime()) {
setOldInterfaceRetired(true)
return
}
timeout.current = setTimeout(checkSunset, nextSunsetCheckDelay(sunset.getTime(), Date.now()))
}
checkSunset()
onCleanup(() => {
if (timeout.current !== undefined) clearTimeout(timeout.current)
})
}
createEffect(() => {
if (!launchReady() || launchState.classified) return
setLaunchState({
classified: true,
previous: launch.version,
})
if (!platform.version || launch.version === platform.version) return
setLaunch("version", platform.version)
})
createEffect(() => {
if (!ready() || !launchState.classified || platform.platform !== "web") return
if (layoutTransitionClassified()) return
setStore("general", "layoutTransitionEligible", hasExistingWebState(settingsInit, launchState.previous))
})
createEffect(() => {
if (!ready() || !launchState.classified || launchState.migrationApplied) return
if (layoutUpgrade() && store.general?.newLayoutDesigns !== true) {
setStore("general", "newLayoutDesigns", true)
}
setLaunchState("migrationApplied", true)
})
createEffect(() => {
if (!ready() || !launchState.classified) return
if (typeof store.general?.shouldDisplayTabsToast === "boolean") return
if (!launchState.previous && !layoutTransitionClassified()) return
setStore(
"general",
"shouldDisplayTabsToast",
shouldDisplayTabsToast(launchState.previous, platform.version, layoutTransitionEligible()),
)
})
createEffect(() => {
if (!ready() || !oldInterfaceRetired()) return
if (store.general?.newLayoutDesigns === true) return
setStore("general", "newLayoutDesigns", true)
})
createEffect(() => { createEffect(() => {
if (typeof document === "undefined") return if (typeof document === "undefined") return
const root = document.documentElement const root = document.documentElement
@@ -242,13 +413,34 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
setMobileTitlebarPosition(value: "top" | "bottom") { setMobileTitlebarPosition(value: "top" | "bottom") {
setStore("general", "mobileTitlebarPosition", value) setStore("general", "mobileTitlebarPosition", value)
}, },
newLayoutDesigns: () => true, newLayoutDesigns,
setNewLayoutDesigns(value: boolean) {
const next = oldInterfaceRetired() ? true : value
if (newLayoutDesigns() === next) return
setStore("general", "newLayoutDesigns", next)
if (typeof window !== "undefined") setTimeout(() => window.location.reload())
},
layoutTransitionClassified,
setOldLayoutEligible(eligible: boolean) {
const current = store.general?.layoutTransitionEligible
if (typeof current === "boolean") return
setStore("general", "layoutTransitionEligible", eligible)
},
layoutTransitionAvailable: createMemo(() => ready() && layoutTransition().available),
newInterfaceNoticeVisible: createMemo(() => ready() && layoutTransition().notice),
dismissNewInterfaceNotice() {
setStore("general", "newInterfaceNoticeDismissed", true)
},
shouldDisplayTabsToast: withFallback(() => store.general?.shouldDisplayTabsToast, false),
dismissTabsToast() {
setStore("general", "shouldDisplayTabsToast", false)
},
}, },
visibility: { visibility: {
fileTree: showFileTree, fileTree: visible(showFileTree),
search: showSearch, search: visible(showSearch),
status: showStatus, status: visible(showStatus),
customAgents: showCustomAgents, customAgents: visible(showCustomAgents),
}, },
appearance: { appearance: {
fontSize: withFallback(() => store.appearance?.fontSize, defaultSettings.appearance.fontSize), fontSize: withFallback(() => store.appearance?.fontSize, defaultSettings.appearance.fontSize),
+1 -1
View File
@@ -8,7 +8,7 @@ import { createHomeSessionSearchController } from "./home/home-session-search-co
import { createHomeSessionsController } from "./home/home-sessions-controller" import { createHomeSessionsController } from "./home/home-sessions-controller"
import { HomeSessions } from "./home/home-sessions" import { HomeSessions } from "./home/home-sessions"
export function Home() { export function NewHome() {
const home = createHomeController() const home = createHomeController()
const projects = createHomeProjectsController(home) const projects = createHomeProjectsController(home)
const sessions = createHomeSessionsController(home) const sessions = createHomeSessionsController(home)
+142
View File
@@ -0,0 +1,142 @@
import { DialogSelectServer } from "@/components/dialog-select-server"
import { useDirectoryPicker } from "@/components/directory-picker"
import { useGlobal } from "@/context/global"
import { useLanguage } from "@/context/language"
import { type ServerConnection, useServer } from "@/context/server"
import { useServerSync } from "@/context/server-sync"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { Button } from "@opencode-ai/ui/button"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Icon } from "@opencode-ai/ui/icon"
import { Logo } from "@opencode-ai/ui/logo"
import { useNavigate } from "@solidjs/router"
import { DateTime } from "luxon"
import { createMemo, For, Match, Switch } from "solid-js"
export function LegacyHome() {
const sync = useServerSync()
const pickDirectory = useDirectoryPicker()
const dialog = useDialog()
const navigate = useNavigate()
const global = useGlobal()
const server = useServer()
const language = useLanguage()
const homedir = createMemo(() => sync().data.path.home)
const serverUnreachable = createMemo(() => global.servers.health[server.key]?.healthy === false)
const recent = createMemo(() => {
return sync()
.data.project.slice()
.sort((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
.slice(0, 5)
})
const serverDotClass = createMemo(() => {
const healthy = global.servers.health[server.key]?.healthy
if (healthy === true) return "bg-icon-success-base"
if (healthy === false) return "bg-icon-critical-base"
return "bg-border-weak-base"
})
function openProject(conn: ServerConnection.Any, directory: string) {
const serverCtx = global.ensureServerCtx(conn)
serverCtx.projects.open(directory)
serverCtx.projects.touch(directory)
navigate(`/${base64Encode(directory)}`)
}
function chooseProject() {
if (serverUnreachable()) return
const conn = server.current
if (!conn) return
const resolve = (result: string | string[] | null) => {
if (Array.isArray(result)) {
result.forEach((directory) => openProject(conn, directory))
return
}
if (result) openProject(conn, result)
}
pickDirectory({
server: conn,
title: language.t("command.project.open"),
multiple: true,
onSelect: resolve,
})
}
return (
<div class="mx-auto mt-55 w-full md:w-auto px-4">
<Logo class="md:w-xl opacity-12" />
<Button
size="large"
variant="ghost"
class="mt-4 mx-auto text-14-regular text-text-weak"
onClick={() => dialog.show(() => <DialogSelectServer />)}
>
<div
classList={{
"size-2 rounded-full": true,
[serverDotClass()]: true,
}}
/>
{server.name}
</Button>
<Switch>
<Match when={sync().data.project.length > 0}>
<div class="mt-20 w-full flex flex-col gap-4">
<div class="flex gap-2 items-center justify-between pl-3">
<div class="text-14-medium text-text-strong">{language.t("home.recentProjects")}</div>
<Button
icon="folder-add-left"
size="normal"
class="pl-2 pr-3"
disabled={serverUnreachable()}
onClick={chooseProject}
>
{language.t("command.project.open")}
</Button>
</div>
<ul class="flex flex-col gap-2">
<For each={recent()}>
{(project) => (
<Button
size="large"
variant="ghost"
class="text-14-mono text-left justify-between px-3"
onClick={() => openProject(server.current!, project.worktree)}
>
{project.worktree.replace(homedir(), "~")}
<div class="text-14-regular text-text-weak">
{DateTime.fromMillis(project.time.updated ?? project.time.created).toRelative()}
</div>
</Button>
)}
</For>
</ul>
</div>
</Match>
<Match when={!sync().ready}>
<div class="mt-30 mx-auto flex flex-col items-center gap-3">
<div class="text-12-regular text-text-weak">{language.t("common.loading")}</div>
<Button class="px-3" disabled={serverUnreachable()} onClick={chooseProject}>
{language.t("command.project.open")}
</Button>
</div>
</Match>
<Match when={true}>
<div class="mt-30 mx-auto flex flex-col items-center gap-3">
<Icon name="folder-add-left" size="large" />
<div class="flex flex-col gap-1 items-center justify-center">
<div class="text-14-medium text-text-strong">{language.t("home.empty.title")}</div>
<div class="text-12-regular text-text-weak">{language.t("home.empty.description")}</div>
</div>
<Button class="px-3 mt-1" disabled={serverUnreachable()} onClick={chooseProject}>
{language.t("command.project.open")}
</Button>
</div>
</Match>
</Switch>
</div>
)
}
+53
View File
@@ -0,0 +1,53 @@
import { createEffect, Suspense, type ParentProps } from "solid-js"
import { createStore } from "solid-js/store"
import { useNavigate } from "@solidjs/router"
import { DebugBar } from "@/components/debug-bar"
import { TabsInfoPopup } from "@/components/help-button"
import { Titlebar, type TitlebarUpdate } from "@/components/titlebar"
import { usePlatform } from "@/context/platform"
import { setNavigate } from "@/utils/notification-click"
import { setV2Toast, ToastRegion } from "@/utils/toast"
export default function NewLayout(props: ParentProps) {
const platform = usePlatform()
const navigate = useNavigate()
setNavigate(navigate)
const [state, setState] = createStore({ debugTools: true })
createEffect(() => setV2Toast(true))
const update: TitlebarUpdate = {
version: () => {
const state = platform.updater?.state()
if (state?.status !== "ready") return
return state.version
},
installing: () => platform.updater?.state().status === "installing",
install: () => void platform.updater?.install(),
}
return (
<div
class="relative bg-v2-background-bg-deep flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text"
style={{
"padding-top": "env(safe-area-inset-top, 0px)",
"padding-bottom": "env(safe-area-inset-bottom, 0px)",
}}
>
<Titlebar
update={update}
debugTools={
import.meta.env.DEV
? { visible: state.debugTools, toggle: () => setState("debugTools", (value) => !value) }
: undefined
}
/>
<main class="flex-1 min-h-0 min-w-0 overflow-x-hidden flex flex-col items-start contain-strict">
<Suspense>{props.children}</Suspense>
</main>
{import.meta.env.DEV && state.debugTools && <DebugBar inline />}
<TabsInfoPopup />
<ToastRegion v2 />
</div>
)
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,126 @@
import { createStore } from "solid-js/store"
import { onCleanup, Show, type Accessor } from "solid-js"
import { InlineInput } from "@opencode-ai/ui/inline-input"
export function createInlineEditorController() {
// This controller intentionally supports one active inline editor at a time.
const [editor, setEditor] = createStore({
active: "" as string,
value: "",
})
const editorOpen = (id: string) => editor.active === id
const editorValue = () => editor.value
const openEditor = (id: string, value: string) => {
if (!id) return
setEditor({ active: id, value })
}
const closeEditor = () => setEditor({ active: "", value: "" })
const saveEditor = (callback: (next: string) => void) => {
const next = editor.value.trim()
if (!next) {
closeEditor()
return
}
closeEditor()
callback(next)
}
const editorKeyDown = (event: KeyboardEvent, callback: (next: string) => void) => {
if (event.key === "Enter") {
event.preventDefault()
saveEditor(callback)
return
}
if (event.key !== "Escape") return
event.preventDefault()
closeEditor()
}
const InlineEditor = (props: {
id: string
value: Accessor<string>
onSave: (next: string) => void
class?: string
displayClass?: string
editing?: boolean
stopPropagation?: boolean
openOnDblClick?: boolean
}) => {
let frame: number | undefined
onCleanup(() => {
if (frame === undefined) return
cancelAnimationFrame(frame)
})
const isEditing = () => props.editing ?? editorOpen(props.id)
const stopEvents = () => props.stopPropagation ?? false
const allowDblClick = () => props.openOnDblClick ?? true
const stopPropagation = (event: Event) => {
if (!stopEvents()) return
event.stopPropagation()
}
const handleDblClick = (event: MouseEvent) => {
if (!allowDblClick()) return
stopPropagation(event)
openEditor(props.id, props.value())
}
return (
<Show
when={isEditing()}
fallback={
<span
class={props.displayClass ?? props.class}
onDblClick={handleDblClick}
onPointerDown={stopPropagation}
onMouseDown={stopPropagation}
onClick={stopPropagation}
onTouchStart={stopPropagation}
>
{props.value()}
</span>
}
>
<InlineInput
ref={(el) => {
if (frame !== undefined) cancelAnimationFrame(frame)
frame = requestAnimationFrame(() => {
frame = undefined
if (!el.isConnected) return
el.focus()
})
}}
value={editorValue()}
class={props.class}
onInput={(event) => setEditor("value", event.currentTarget.value)}
onKeyDown={(event) => {
event.stopPropagation()
editorKeyDown(event, props.onSave)
}}
onBlur={closeEditor}
onPointerDown={stopPropagation}
onClick={stopPropagation}
onDblClick={stopPropagation}
onMouseDown={stopPropagation}
onMouseUp={stopPropagation}
onTouchStart={stopPropagation}
/>
</Show>
)
}
return {
editor,
editorOpen,
editorValue,
openEditor,
closeEditor,
saveEditor,
editorKeyDown,
setEditor,
InlineEditor,
}
}
@@ -0,0 +1,340 @@
import type { SessionInfo } from "@opencode-ai/client/promise"
import { Avatar } from "@opencode-ai/ui/avatar"
import { Icon } from "@opencode-ai/ui/icon"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Spinner } from "@opencode-ai/ui/spinner"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { getFilename } from "@opencode-ai/core/util/path"
import { A, useParams } from "@solidjs/router"
import { type Accessor, createMemo, For, type JSX, Match, Show, Switch } from "solid-js"
import { useServerSync } from "@/context/server-sync"
import { useLanguage } from "@/context/language"
import { getAvatarColors, type LocalProject, useLayout } from "@/context/layout"
import { useNotification } from "@/context/notification"
import { usePermission } from "@/context/permission"
import { messageAgentColor } from "@/utils/agent"
import { sessionLabel } from "@/utils/session-title"
import { sessionPermissionRequest } from "../session/composer/session-request-tree"
import { childSessionOnPath, getProjectAvatarSource, hasProjectPermissions } from "./helpers"
export const ProjectIcon = (props: {
project: LocalProject
class?: string
notify?: boolean
working?: boolean
}): JSX.Element => {
const serverSync = useServerSync()
const notification = useNotification()
const permission = usePermission()
const dirs = createMemo(() => [props.project.worktree, ...(props.project.sandboxes ?? [])])
const unseenCount = createMemo(() =>
dirs().reduce((total, directory) => total + notification.project.unseenCount(directory), 0),
)
const hasError = createMemo(() => dirs().some((directory) => notification.project.unseenHasError(directory)))
const hasPermissions = createMemo(() =>
dirs().some((directory) => {
return hasProjectPermissions(serverSync().session.data.permission, (item) => {
if (serverSync().session.get(item.sessionID)?.location.directory !== directory) return false
return !permission.autoResponds(item, directory)
})
}),
)
const notify = createMemo(() => props.notify && (hasPermissions() || unseenCount() > 0))
const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
return (
<div class={`relative size-8 shrink-0 rounded ${props.class ?? ""}`}>
<div class="size-full rounded overflow-clip">
<Avatar
fallback={name()}
src={getProjectAvatarSource(props.project.id, props.project.icon)}
{...getAvatarColors(props.project.icon?.color)}
class="size-full rounded"
classList={{ "badge-mask": notify() }}
/>
</div>
<Show when={notify()}>
<div
classList={{
"absolute top-px right-px size-1.5 rounded-full z-10": true,
"bg-surface-warning-strong": hasPermissions(),
"bg-icon-critical-base": !hasPermissions() && hasError(),
"bg-text-interactive-base": !hasPermissions() && !hasError(),
}}
/>
</Show>
<Show when={props.working}>
<div class="absolute bottom-px right-px size-3 rounded-full bg-background-base z-10 flex items-center justify-center">
<Spinner class="size-[9px]" />
</div>
</Show>
</div>
)
}
export type SessionItemProps = {
session: SessionInfo
list: SessionInfo[]
navList?: Accessor<SessionInfo[]>
slug: string
mobile?: boolean
dense?: boolean
showTooltip?: boolean
showChild?: boolean
level?: number
sidebarExpanded: Accessor<boolean>
clearHoverProjectSoon: () => void
prefetchSession: (session: SessionInfo, priority?: "high" | "low") => void
archiveSession: (session: SessionInfo) => Promise<void>
}
const SessionRow = (props: {
session: SessionInfo
slug: string
mobile?: boolean
dense?: boolean
tint: Accessor<string | undefined>
isWorking: Accessor<boolean>
hasPermissions: Accessor<boolean>
hasError: Accessor<boolean>
unseenCount: Accessor<number>
clearHoverProjectSoon: () => void
sidebarOpened: Accessor<boolean>
warmPress: () => void
warmFocus: () => void
}): JSX.Element => {
const title = () => sessionLabel(props.session)
return (
<A
href={`/${props.slug}/session/${props.session.id}`}
class={`flex items-center gap-2 min-w-0 w-full text-left focus:outline-none ${props.dense ? "py-0.5" : "py-1"}`}
onPointerDown={props.warmPress}
onFocus={props.warmFocus}
onClick={() => {
if (props.sidebarOpened()) return
props.clearHoverProjectSoon()
}}
>
<Show when={props.isWorking() || props.hasPermissions() || props.hasError() || props.unseenCount() > 0}>
<div
class="shrink-0 size-6 flex items-center justify-center"
style={{ color: props.tint() ?? "var(--icon-interactive-base)" }}
>
<Switch>
<Match when={props.isWorking()}>
<Spinner class="size-[15px]" />
</Match>
<Match when={props.hasPermissions()}>
<div class="size-1.5 rounded-full bg-surface-warning-strong" />
</Match>
<Match when={props.hasError()}>
<div class="size-1.5 rounded-full bg-text-diff-delete-base" />
</Match>
<Match when={props.unseenCount() > 0}>
<div class="size-1.5 rounded-full bg-text-interactive-base" />
</Match>
</Switch>
</div>
</Show>
<span class="text-14-regular text-text-strong min-w-0 flex-1 truncate">{title()}</span>
</A>
)
}
export const SessionItem = (props: SessionItemProps): JSX.Element => {
const params = useParams()
const layout = useLayout()
const language = useLanguage()
const notification = useNotification()
const permission = usePermission()
const serverSync = useServerSync()
const unseenCount = createMemo(() => notification.session.unseenCount(props.session.id))
const hasError = createMemo(() => notification.session.unseenHasError(props.session.id))
const [sessionStore] = serverSync().child(props.session.location.directory)
const hasPermissions = createMemo(() => {
return !!sessionPermissionRequest(
sessionStore.session,
serverSync().session.data.permission,
props.session.id,
(item) => {
return !permission.autoResponds(item, props.session.location.directory)
},
)
})
const isWorking = createMemo(() => {
if (hasPermissions()) return false
return serverSync().session.data.session_working(props.session.id)
})
const tint = createMemo(() =>
messageAgentColor(serverSync().session.data.message[props.session.id], sessionStore.agent),
)
const tooltip = createMemo(() => props.showTooltip ?? (props.mobile || !props.sidebarExpanded()))
const currentChild = createMemo(() => {
if (!props.showChild) return
return childSessionOnPath(sessionStore.session, props.session.id, params.id)
})
const warm = (span: number, priority: "high" | "low") => {
const nav = props.navList?.()
const list = nav?.some(
(item) => item.id === props.session.id && item.location.directory === props.session.location.directory,
)
? nav
: props.list
props.prefetchSession(props.session, priority)
const idx = list.findIndex(
(item) => item.id === props.session.id && item.location.directory === props.session.location.directory,
)
if (idx === -1) return
for (let step = 1; step <= span; step++) {
const next = list[idx + step]
if (next) props.prefetchSession(next, step === 1 ? "high" : priority)
const prev = list[idx - step]
if (prev) props.prefetchSession(prev, step === 1 ? "high" : priority)
}
}
const item = (
<SessionRow
session={props.session}
slug={props.slug}
mobile={props.mobile}
dense={props.dense}
tint={tint}
isWorking={isWorking}
hasPermissions={hasPermissions}
hasError={hasError}
unseenCount={unseenCount}
clearHoverProjectSoon={props.clearHoverProjectSoon}
sidebarOpened={layout.sidebar.opened}
warmPress={() => warm(2, "high")}
warmFocus={() => warm(2, "high")}
/>
)
return (
<>
<div
data-session-id={props.session.id}
class="group/session relative w-full min-w-0 rounded-md cursor-default pr-3 transition-colors hover:bg-surface-raised-base-hover [&:has(:focus-visible)]:bg-surface-raised-base-hover has-[[data-expanded]]:bg-surface-raised-base-hover has-[.active]:bg-surface-base-active"
style={{ "padding-left": `${8 + (props.level ?? 0) * 16}px` }}
>
<div class="flex min-w-0 items-center gap-1">
<div class="min-w-0 flex-1">
<Show
when={!tooltip()}
fallback={
<Tooltip
placement={props.mobile ? "bottom" : "right"}
value={sessionLabel(props.session)}
gutter={10}
class="min-w-0 w-full"
>
{item}
</Tooltip>
}
>
{item}
</Show>
</div>
{/* TODO: Restore the archive action when the V2 client exposes session archive. */}
<Show when={false}>
<div
class="shrink-0 overflow-hidden transition-[width,opacity]"
classList={{
"w-6 opacity-100 pointer-events-auto": !!props.mobile,
"w-0 opacity-0 pointer-events-none": !props.mobile,
"group-hover/session:w-6 group-hover/session:opacity-100 group-hover/session:pointer-events-auto": true,
"group-focus-within/session:w-6 group-focus-within/session:opacity-100 group-focus-within/session:pointer-events-auto": true,
}}
>
<Tooltip value={language.t("common.archive")} placement="top">
<IconButton
icon="archive"
variant="ghost"
class="size-6 rounded-md"
aria-label={language.t("common.archive")}
onClick={(event) => {
event.preventDefault()
event.stopPropagation()
void props.archiveSession(props.session)
}}
/>
</Tooltip>
</div>
</Show>
</div>
</div>
<Show when={currentChild()} keyed>
{(child) => (
<div class="w-full">
<SessionItem {...props} session={child} level={(props.level ?? 0) + 1} />
</div>
)}
</Show>
</>
)
}
export const NewSessionItem = (props: {
slug: string
mobile?: boolean
dense?: boolean
sidebarExpanded: Accessor<boolean>
clearHoverProjectSoon: () => void
}): JSX.Element => {
const layout = useLayout()
const language = useLanguage()
const label = language.t("command.session.new")
const tooltip = () => props.mobile || !props.sidebarExpanded()
const item = (
<A
href={`/${props.slug}/session`}
end
class={`flex items-center gap-2 min-w-0 w-full text-left focus:outline-none ${props.dense ? "py-0.5" : "py-1"}`}
onClick={() => {
if (layout.sidebar.opened()) return
props.clearHoverProjectSoon()
}}
>
<div class="shrink-0 size-6 flex items-center justify-center">
<IconV2 name="edit" size="small" class="text-icon-weak" />
</div>
<span class="text-14-regular text-text-strong min-w-0 flex-1 truncate">{label}</span>
</A>
)
return (
<div class="group/session relative w-full min-w-0 rounded-md cursor-default transition-colors pl-2 pr-3 hover:bg-surface-raised-base-hover [&:has(:focus-visible)]:bg-surface-raised-base-hover has-[.active]:bg-surface-base-active">
<Show
when={!tooltip()}
fallback={
<Tooltip placement={props.mobile ? "bottom" : "right"} value={label} gutter={10} class="min-w-0 w-full">
{item}
</Tooltip>
}
>
{item}
</Show>
</div>
)
}
export const SessionSkeleton = (props: { count?: number }): JSX.Element => {
const items = Array.from({ length: props.count ?? 4 }, (_, index) => index)
return (
<div class="flex flex-col gap-1">
<For each={items}>
{() => <div class="h-8 w-full rounded-md bg-surface-raised-base opacity-60 animate-pulse" />}
</For>
</div>
)
}
@@ -0,0 +1,377 @@
import { createMemo, For, Show, type Accessor, type JSX } from "solid-js"
import { createStore } from "solid-js/store"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { Button } from "@opencode-ai/ui/button"
import { ContextMenu } from "@opencode-ai/ui/context-menu"
import { HoverCard } from "@opencode-ai/ui/hover-card"
import { Icon } from "@opencode-ai/ui/icon"
import { createSortable } from "@thisbeyond/solid-dnd"
import { useLayout, type LocalProject } from "@/context/layout"
import { useServerSync } from "@/context/server-sync"
import { useLanguage } from "@/context/language"
import { useNotification } from "@/context/notification"
import { ProjectIcon, SessionItem, type SessionItemProps } from "./sidebar-items"
import { displayName, sortedRootSessions } from "./helpers"
export type ProjectSidebarContext = {
currentDir: Accessor<string>
currentProject: Accessor<LocalProject | undefined>
sidebarOpened: Accessor<boolean>
sidebarHovering: Accessor<boolean>
hoverProject: Accessor<string | undefined>
onProjectMouseEnter: (worktree: string, event: MouseEvent) => void
onProjectMouseLeave: (worktree: string) => void
onProjectFocus: (worktree: string) => void
onHoverOpenChanged: (worktree: string, hovered: boolean) => void
navigateToProject: (directory: string) => void
openSidebar: () => void
closeProject: (directory: string) => void
showEditProjectDialog: (project: LocalProject) => void
toggleProjectWorkspaces: (project: LocalProject) => void
workspacesEnabled: (project: LocalProject) => boolean
workspaceIds: (project: LocalProject) => string[]
workspaceLabel: (directory: string, branch?: string, projectId?: string) => string
sessionProps: Omit<SessionItemProps, "session" | "list" | "slug" | "mobile" | "dense">
}
export const ProjectDragOverlay = (props: {
projects: Accessor<LocalProject[]>
activeProject: Accessor<string | undefined>
}): JSX.Element => {
const project = createMemo(() => props.projects().find((p) => p.worktree === props.activeProject()))
return (
<Show when={project()}>
{(p) => (
<div class="bg-background-base rounded-xl p-1">
<ProjectIcon project={p()} />
</div>
)}
</Show>
)
}
const ProjectTile = (props: {
project: LocalProject
mobile?: boolean
sidebarHovering: Accessor<boolean>
selected: Accessor<boolean>
active: Accessor<boolean>
isWorking: Accessor<boolean>
overlay: Accessor<boolean>
suppressHover: Accessor<boolean>
dirs: Accessor<string[]>
onProjectMouseEnter: (worktree: string, event: MouseEvent) => void
onProjectMouseLeave: (worktree: string) => void
onProjectFocus: (worktree: string) => void
navigateToProject: (directory: string) => void
showEditProjectDialog: (project: LocalProject) => void
toggleProjectWorkspaces: (project: LocalProject) => void
workspacesEnabled: (project: LocalProject) => boolean
closeProject: (directory: string) => void
setMenu: (value: boolean) => void
setOpen: (value: boolean) => void
setSuppressHover: (value: boolean) => void
language: ReturnType<typeof useLanguage>
}): JSX.Element => {
const notification = useNotification()
const layout = useLayout()
const unseenCount = createMemo(() =>
props.dirs().reduce((total, directory) => total + notification.project.unseenCount(directory), 0),
)
const clear = () =>
props
.dirs()
.filter((directory) => notification.project.unseenCount(directory) > 0)
.forEach((directory) => notification.project.markViewed(directory))
return (
<ContextMenu
modal={!props.sidebarHovering()}
onOpenChange={(value) => {
props.setMenu(value)
props.setSuppressHover(value)
if (value) props.setOpen(false)
}}
>
<ContextMenu.Trigger
as="button"
type="button"
aria-label={displayName(props.project)}
data-action="project-switch"
data-project={base64Encode(props.project.worktree)}
classList={{
"flex items-center justify-center size-10 p-1 rounded-lg overflow-hidden transition-colors cursor-default": true,
"bg-transparent border-2 border-icon-strong-base hover:bg-surface-base-hover": props.selected(),
"bg-transparent border border-transparent hover:bg-surface-base-hover hover:border-border-weak-base":
!props.selected() && !props.active(),
"bg-surface-base-hover border border-border-weak-base": !props.selected() && props.active(),
}}
onPointerDown={(event) => {
if (event.button === 0 && !event.ctrlKey) {
props.setOpen(false)
props.setSuppressHover(true)
return
}
if (!props.overlay()) return
if (event.button !== 2 && !(event.button === 0 && event.ctrlKey)) return
props.setOpen(false)
props.setSuppressHover(true)
event.preventDefault()
}}
onMouseEnter={(event: MouseEvent) => {
if (!props.overlay()) return
if (props.suppressHover()) return
props.onProjectMouseEnter(props.project.worktree, event)
}}
onMouseLeave={() => {
if (props.suppressHover()) props.setSuppressHover(false)
if (!props.overlay()) return
props.onProjectMouseLeave(props.project.worktree)
}}
onFocus={() => {
if (!props.overlay()) return
if (props.suppressHover()) return
props.onProjectFocus(props.project.worktree)
}}
onClick={() => {
props.setOpen(false)
if (props.selected()) {
layout.sidebar.toggle()
return
}
props.navigateToProject(props.project.worktree)
}}
onBlur={() => props.setOpen(false)}
>
<ProjectIcon project={props.project} notify working={props.isWorking()} />
</ContextMenu.Trigger>
<ContextMenu.Portal>
<ContextMenu.Content>
<ContextMenu.Item onSelect={() => props.showEditProjectDialog(props.project)}>
<ContextMenu.ItemLabel>{props.language.t("common.edit")}</ContextMenu.ItemLabel>
</ContextMenu.Item>
<ContextMenu.Item
data-action="project-workspaces-toggle"
data-project={base64Encode(props.project.worktree)}
disabled={props.project.vcs !== "git" && !props.workspacesEnabled(props.project)}
onSelect={() => props.toggleProjectWorkspaces(props.project)}
>
<ContextMenu.ItemLabel>
{props.workspacesEnabled(props.project)
? props.language.t("sidebar.workspaces.disable")
: props.language.t("sidebar.workspaces.enable")}
</ContextMenu.ItemLabel>
</ContextMenu.Item>
<ContextMenu.Item
data-action="project-clear-notifications"
data-project={base64Encode(props.project.worktree)}
disabled={unseenCount() === 0}
onSelect={clear}
>
<ContextMenu.ItemLabel>{props.language.t("sidebar.project.clearNotifications")}</ContextMenu.ItemLabel>
</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Item
data-action="project-close-menu"
data-project={base64Encode(props.project.worktree)}
onSelect={() => props.closeProject(props.project.worktree)}
>
<ContextMenu.ItemLabel>{props.language.t("common.close")}</ContextMenu.ItemLabel>
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu>
)
}
const ProjectPreviewPanel = (props: {
project: LocalProject
mobile?: boolean
selected: Accessor<boolean>
workspaceEnabled: Accessor<boolean>
workspaces: Accessor<string[]>
label: (directory: string) => string
projectSessions: Accessor<ReturnType<typeof sortedRootSessions>>
workspaceSessions: (directory: string) => ReturnType<typeof sortedRootSessions>
ctx: ProjectSidebarContext
language: ReturnType<typeof useLanguage>
}): JSX.Element => (
<div class="-m-3 p-2 flex flex-col w-72">
<div class="px-4 pt-2 pb-1 flex items-center gap-2">
<div class="text-14-medium text-text-strong truncate grow">{displayName(props.project)}</div>
</div>
<div class="px-4 pb-2 text-12-medium text-text-weak">{props.language.t("sidebar.project.recentSessions")}</div>
<div class="px-2 pb-2 flex flex-col gap-2">
<Show
when={props.workspaceEnabled()}
fallback={
<For each={props.projectSessions().slice(0, 2)}>
{(session) => (
<SessionItem
{...props.ctx.sessionProps}
session={session}
list={props.projectSessions()}
slug={base64Encode(props.project.worktree)}
dense
showTooltip
mobile={props.mobile}
/>
)}
</For>
}
>
<For each={props.workspaces()}>
{(directory) => {
const sessions = createMemo(() => props.workspaceSessions(directory))
return (
<div class="flex flex-col gap-1">
<div class="px-2 py-0.5 flex items-center gap-1 min-w-0">
<div class="shrink-0 size-6 flex items-center justify-center">
<Icon name="branch" size="small" class="text-icon-base" />
</div>
<span class="truncate text-14-medium text-text-base">{props.label(directory)}</span>
</div>
<For each={sessions().slice(0, 2)}>
{(session) => (
<SessionItem
{...props.ctx.sessionProps}
session={session}
list={sessions()}
slug={base64Encode(directory)}
dense
showTooltip
mobile={props.mobile}
/>
)}
</For>
</div>
)
}}
</For>
</Show>
</div>
<div class="px-2 py-2 border-t border-border-weak-base">
<Button
variant="ghost"
class="flex w-full text-left justify-start text-text-base px-2 hover:bg-transparent active:bg-transparent"
onClick={() => {
props.ctx.openSidebar()
props.ctx.onHoverOpenChanged(props.project.worktree, false)
if (props.selected()) return
props.ctx.navigateToProject(props.project.worktree)
}}
>
{props.language.t("sidebar.project.viewAllSessions")}
</Button>
</div>
</div>
)
export const SortableProject = (props: {
project: LocalProject
mobile?: boolean
ctx: ProjectSidebarContext
sortNow: Accessor<number>
}): JSX.Element => {
const serverSync = useServerSync()
const language = useLanguage()
const sortable = createSortable(props.project.worktree)
const selected = createMemo(() => props.ctx.currentProject()?.worktree === props.project.worktree)
const workspaces = createMemo(() => props.ctx.workspaceIds(props.project).slice(0, 2))
const workspaceEnabled = createMemo(() => props.ctx.workspacesEnabled(props.project))
const dirs = createMemo(() => props.ctx.workspaceIds(props.project))
const [state, setState] = createStore({
menu: false,
suppressHover: false,
})
const isHoverProject = () => props.ctx.hoverProject() === props.project.worktree
const preview = createMemo(() => !props.mobile && props.ctx.sidebarOpened())
const overlay = createMemo(() => !props.mobile && !props.ctx.sidebarOpened())
const active = createMemo(() => state.menu || (preview() ? isHoverProject() : overlay() && isHoverProject()))
const hoverOpen = () => isHoverProject() && preview() && !selected() && !state.menu
const label = (directory: string) => {
const [data] = serverSync().child(directory, { bootstrap: false })
const kind =
directory === props.project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox")
const name = props.ctx.workspaceLabel(directory, data.vcs?.branch, props.project.id)
return `${kind} : ${name}`
}
const projectStore = createMemo(() => serverSync().child(props.project.worktree, { bootstrap: false })[0])
const isWorking = createMemo(() =>
dirs().some((directory) => {
return Object.keys(serverSync().session.data.session_status).some((id) => {
if (serverSync().session.get(id)?.location.directory !== directory) return false
return serverSync().session.data.session_working(id)
})
}),
)
const projectSessions = createMemo(() => sortedRootSessions(projectStore(), props.sortNow()))
const workspaceSessions = (directory: string) => {
const [data] = serverSync().child(directory, { bootstrap: false })
return sortedRootSessions(data, props.sortNow())
}
const tile = () => (
<ProjectTile
project={props.project}
mobile={props.mobile}
sidebarHovering={props.ctx.sidebarHovering}
selected={selected}
active={active}
isWorking={isWorking}
overlay={overlay}
suppressHover={() => state.suppressHover}
dirs={dirs}
onProjectMouseEnter={props.ctx.onProjectMouseEnter}
onProjectMouseLeave={props.ctx.onProjectMouseLeave}
onProjectFocus={props.ctx.onProjectFocus}
navigateToProject={props.ctx.navigateToProject}
showEditProjectDialog={props.ctx.showEditProjectDialog}
toggleProjectWorkspaces={props.ctx.toggleProjectWorkspaces}
workspacesEnabled={props.ctx.workspacesEnabled}
closeProject={props.ctx.closeProject}
setMenu={(value) => setState("menu", value)}
setOpen={(value) => props.ctx.onHoverOpenChanged(props.project.worktree, value)}
setSuppressHover={(value) => setState("suppressHover", value)}
language={language}
/>
)
return (
// @ts-ignore
<div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
<Show when={preview() && !selected()} fallback={tile()}>
<HoverCard
open={!state.suppressHover && hoverOpen() && !state.menu}
openDelay={0}
closeDelay={0}
placement="right-start"
gutter={6}
trigger={tile()}
onOpenChange={(value) => {
if (state.menu) return
if (value && state.suppressHover) return
props.ctx.onHoverOpenChanged(props.project.worktree, value)
}}
>
<ProjectPreviewPanel
project={props.project}
mobile={props.mobile}
selected={selected}
workspaceEnabled={workspaceEnabled}
workspaces={workspaces}
label={label}
projectSessions={projectSessions}
workspaceSessions={workspaceSessions}
ctx={props.ctx}
language={language}
/>
</HoverCard>
</Show>
</div>
)
}
@@ -0,0 +1,125 @@
import { createEffect, createMemo, For, Show, type Accessor, type JSX } from "solid-js"
import {
DragDropProvider,
DragDropSensors,
DragOverlay,
SortableProvider,
closestCenter,
type DragEvent,
} from "@thisbeyond/solid-dnd"
import { ConstrainDragXAxis } from "@/utils/solid-dnd"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { type LocalProject } from "@/context/layout"
export const SidebarContent = (props: {
mobile?: boolean
opened: Accessor<boolean>
aimMove: (event: MouseEvent) => void
projects: Accessor<LocalProject[]>
renderProject: (project: LocalProject) => JSX.Element
handleDragStart: (event: unknown) => void
handleDragEnd: () => void
handleDragOver: (event: DragEvent) => void
openProjectLabel: JSX.Element
openProjectKeybind: Accessor<string | undefined>
onOpenProject: () => void
renderProjectOverlay: () => JSX.Element
settingsLabel: Accessor<string>
settingsKeybind: Accessor<string | undefined>
onOpenSettings: () => void
helpLabel: Accessor<string>
onOpenHelp: () => void
renderPanel: () => JSX.Element
}): JSX.Element => {
const expanded = createMemo(() => !!props.mobile || props.opened())
const placement = () => (props.mobile ? "bottom" : "right")
let panel: HTMLDivElement | undefined
createEffect(() => {
const el = panel
if (!el) return
if (expanded()) {
el.removeAttribute("inert")
return
}
el.setAttribute("inert", "")
})
return (
<div class="flex h-full w-full min-w-0 overflow-hidden">
<div
data-component="sidebar-rail"
class="w-16 shrink-0 bg-background-base flex flex-col items-center overflow-hidden"
onMouseMove={props.aimMove}
>
<div class="flex-1 min-h-0 w-full">
<DragDropProvider
onDragStart={props.handleDragStart}
onDragEnd={props.handleDragEnd}
onDragOver={props.handleDragOver}
collisionDetector={closestCenter}
>
<DragDropSensors />
<ConstrainDragXAxis />
<div class="h-full w-full flex flex-col items-center gap-3 px-3 py-3 overflow-y-auto no-scrollbar">
<SortableProvider ids={props.projects().map((p) => p.worktree)}>
<For each={props.projects()}>{(project) => props.renderProject(project)}</For>
</SortableProvider>
<Tooltip
placement={placement()}
value={
<div class="flex items-center gap-2">
<span>{props.openProjectLabel}</span>
<Show when={!props.mobile && !!props.openProjectKeybind()}>
<span class="text-icon-base text-12-medium">{props.openProjectKeybind()}</span>
</Show>
</div>
}
>
<IconButton
icon="plus"
variant="ghost"
size="large"
onClick={props.onOpenProject}
aria-label={typeof props.openProjectLabel === "string" ? props.openProjectLabel : undefined}
/>
</Tooltip>
</div>
<DragOverlay>{props.renderProjectOverlay()}</DragOverlay>
</DragDropProvider>
</div>
<div class="shrink-0 w-full pt-3 pb-6 flex flex-col items-center gap-2">
<TooltipKeybind placement={placement()} title={props.settingsLabel()} keybind={props.settingsKeybind() ?? ""}>
<IconButton
icon="settings-gear"
variant="ghost"
size="large"
onClick={props.onOpenSettings}
aria-label={props.settingsLabel()}
/>
</TooltipKeybind>
<Tooltip placement={placement()} value={props.helpLabel()}>
<IconButton
icon="help"
variant="ghost"
size="large"
onClick={props.onOpenHelp}
aria-label={props.helpLabel()}
/>
</Tooltip>
</div>
</div>
<div
ref={(el) => {
panel = el
}}
classList={{ "flex-1 flex h-full min-h-0 min-w-0 overflow-hidden": true, "pointer-events-none": !expanded() }}
aria-hidden={!expanded()}
>
{props.renderPanel()}
</div>
</div>
)
}
@@ -0,0 +1,489 @@
import { useNavigate, useParams } from "@solidjs/router"
import { createEffect, createMemo, For, Show, type Accessor, type JSX } from "solid-js"
import { createStore } from "solid-js/store"
import { createSortable } from "@thisbeyond/solid-dnd"
import { createMediaQuery } from "@solid-primitives/media"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { getFilename } from "@opencode-ai/core/util/path"
import { Button } from "@opencode-ai/ui/button"
import { Collapsible } from "@opencode-ai/ui/collapsible"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { Spinner } from "@opencode-ai/ui/spinner"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import type { SessionInfo } from "@opencode-ai/client/promise"
import { type LocalProject } from "@/context/layout"
import { useServerSync, useQueryOptions } from "@/context/server-sync"
import { useLanguage } from "@/context/language"
import { pathKey } from "@/utils/path-key"
import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items"
import { sortedRootSessions } from "./helpers"
import { useIsFetching } from "@tanstack/solid-query"
type InlineEditorComponent = (props: {
id: string
value: Accessor<string>
onSave: (next: string) => void
class?: string
displayClass?: string
editing?: boolean
stopPropagation?: boolean
openOnDblClick?: boolean
}) => JSX.Element
export type WorkspaceSidebarContext = {
currentDir: Accessor<string>
navList: Accessor<SessionInfo[]>
sidebarExpanded: Accessor<boolean>
sidebarHovering: Accessor<boolean>
clearHoverProjectSoon: () => void
prefetchSession: (session: SessionInfo, priority?: "high" | "low") => void
archiveSession: (session: SessionInfo) => Promise<void>
workspaceName: (directory: string, projectId?: string, branch?: string) => string | undefined
renameWorkspace: (directory: string, next: string, projectId?: string, branch?: string) => void
editorOpen: (id: string) => boolean
openEditor: (id: string, value: string) => void
closeEditor: () => void
setEditor: (key: "value", value: string) => void
InlineEditor: InlineEditorComponent
isBusy: (directory: string) => boolean
workspaceExpanded: (directory: string, local: boolean) => boolean
setWorkspaceExpanded: (directory: string, value: boolean) => void
showResetWorkspaceDialog: (root: string, directory: string) => void
showDeleteWorkspaceDialog: (root: string, directory: string) => void
setScrollContainerRef: (el: HTMLDivElement | undefined, mobile?: boolean) => void
}
export const WorkspaceDragOverlay = (props: {
sidebarProject: Accessor<LocalProject | undefined>
activeWorkspace: Accessor<string | undefined>
workspaceLabel: (directory: string, branch?: string, projectId?: string) => string
}): JSX.Element => {
const serverSync = useServerSync()
const language = useLanguage()
const label = createMemo(() => {
const project = props.sidebarProject()
if (!project) return
const directory = props.activeWorkspace()
if (!directory) return
const [workspaceStore] = serverSync().child(directory, { bootstrap: false })
const kind =
directory === project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox")
const name = props.workspaceLabel(directory, workspaceStore.vcs?.branch, project.id)
return `${kind} : ${name}`
})
return (
<Show when={label()}>
{(value) => <div class="bg-background-base rounded-md px-2 py-1 text-14-medium text-text-strong">{value()}</div>}
</Show>
)
}
const WorkspaceHeader = (props: {
local: Accessor<boolean>
busy: Accessor<boolean>
open: Accessor<boolean>
directory: string
language: ReturnType<typeof useLanguage>
branch: Accessor<string | undefined>
workspaceValue: Accessor<string>
workspaceEditActive: Accessor<boolean>
InlineEditor: WorkspaceSidebarContext["InlineEditor"]
renameWorkspace: WorkspaceSidebarContext["renameWorkspace"]
setEditor: WorkspaceSidebarContext["setEditor"]
projectId?: string
}): JSX.Element => (
<div class="flex items-center gap-1 min-w-0 flex-1">
<div class="flex items-center justify-center shrink-0 size-6">
<Show when={props.busy()} fallback={<Icon name="branch" size="small" />}>
<Spinner class="size-[15px]" />
</Show>
</div>
<span class="text-14-medium text-text-base shrink-0">
{props.local() ? props.language.t("workspace.type.local") : props.language.t("workspace.type.sandbox")} :
</span>
<Show
when={!props.local()}
fallback={
<span class="text-14-medium text-text-base min-w-0 truncate">
{props.branch() ?? getFilename(props.directory)}
</span>
}
>
<props.InlineEditor
id={`workspace:${props.directory}`}
value={props.workspaceValue}
onSave={(next) => {
const trimmed = next.trim()
if (!trimmed) return
props.renameWorkspace(props.directory, trimmed, props.projectId, props.branch())
props.setEditor("value", props.workspaceValue())
}}
class="text-14-medium text-text-base min-w-0 truncate"
displayClass="text-14-medium text-text-base min-w-0 truncate"
editing={props.workspaceEditActive()}
stopPropagation={false}
openOnDblClick={false}
/>
</Show>
<div class="flex items-center justify-center shrink-0 overflow-hidden w-0 opacity-0 transition-all duration-200 group-hover/workspace:w-3.5 group-hover/workspace:opacity-100 group-focus-within/workspace:w-3.5 group-focus-within/workspace:opacity-100">
<Icon name={props.open() ? "chevron-down" : "chevron-right"} size="small" class="text-icon-base" />
</div>
</div>
)
const WorkspaceActions = (props: {
directory: string
local: Accessor<boolean>
busy: Accessor<boolean>
menuOpen: Accessor<boolean>
pendingRename: Accessor<boolean>
setMenuOpen: (open: boolean) => void
setPendingRename: (value: boolean) => void
sidebarHovering: Accessor<boolean>
touch: Accessor<boolean>
language: ReturnType<typeof useLanguage>
workspaceValue: Accessor<string>
openEditor: WorkspaceSidebarContext["openEditor"]
showResetWorkspaceDialog: WorkspaceSidebarContext["showResetWorkspaceDialog"]
showDeleteWorkspaceDialog: WorkspaceSidebarContext["showDeleteWorkspaceDialog"]
root: string
clearHoverProjectSoon: WorkspaceSidebarContext["clearHoverProjectSoon"]
navigateToNewSession: () => void
}): JSX.Element => (
<div
class="absolute right-1 top-1/2 -translate-y-1/2 flex items-center gap-0.5 transition-opacity"
classList={{
"opacity-100 pointer-events-auto": props.menuOpen(),
"opacity-0 pointer-events-none": !props.menuOpen(),
"group-hover/workspace:opacity-100 group-hover/workspace:pointer-events-auto": true,
"group-focus-within/workspace:opacity-100 group-focus-within/workspace:pointer-events-auto": true,
}}
>
<DropdownMenu
modal={!props.sidebarHovering()}
open={props.menuOpen()}
onOpenChange={(open) => props.setMenuOpen(open)}
>
<Tooltip value={props.language.t("common.moreOptions")} placement="top">
<DropdownMenu.Trigger
as={IconButton}
icon="dot-grid"
variant="ghost"
class="size-6 rounded-md"
data-action="workspace-menu"
data-workspace={base64Encode(props.directory)}
aria-label={props.language.t("common.moreOptions")}
/>
</Tooltip>
<DropdownMenu.Portal>
<DropdownMenu.Content
onCloseAutoFocus={(event) => {
if (!props.pendingRename()) return
event.preventDefault()
props.setPendingRename(false)
props.openEditor(`workspace:${props.directory}`, props.workspaceValue())
}}
>
<DropdownMenu.Item
disabled={props.local()}
onSelect={() => {
props.setPendingRename(true)
props.setMenuOpen(false)
}}
>
<DropdownMenu.ItemLabel>{props.language.t("common.rename")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Item
// TODO: Restore reset when V2 exposes project-copy reset and instance disposal.
// onSelect={() => props.showResetWorkspaceDialog(props.root, props.directory)}
disabled
>
<DropdownMenu.ItemLabel>{props.language.t("common.reset")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Item
disabled={props.local() || props.busy()}
onSelect={() => props.showDeleteWorkspaceDialog(props.root, props.directory)}
>
<DropdownMenu.ItemLabel>{props.language.t("common.delete")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
<Show when={!props.touch()}>
<Tooltip value={props.language.t("command.session.new")} placement="top">
<IconButtonV2
icon={<IconV2 name="edit" size="small" />}
variant="ghost"
size="small"
class="size-6 rounded-md opacity-0 pointer-events-none group-hover/workspace:opacity-100 group-hover/workspace:pointer-events-auto group-focus-within/workspace:opacity-100 group-focus-within/workspace:pointer-events-auto"
data-action="workspace-new-session"
data-workspace={base64Encode(props.directory)}
aria-label={props.language.t("command.session.new")}
onClick={(event) => {
event.preventDefault()
event.stopPropagation()
props.clearHoverProjectSoon()
props.navigateToNewSession()
}}
/>
</Tooltip>
</Show>
</div>
)
const WorkspaceSessionList = (props: {
slug: Accessor<string>
mobile?: boolean
ctx: WorkspaceSidebarContext
showNew: Accessor<boolean>
loading: Accessor<boolean>
sessions: Accessor<SessionInfo[]>
hasMore: Accessor<boolean>
loadMore: () => Promise<void>
language: ReturnType<typeof useLanguage>
}): JSX.Element => (
<nav class="flex flex-col gap-1">
<Show when={props.showNew()}>
<NewSessionItem
slug={props.slug()}
mobile={props.mobile}
sidebarExpanded={props.ctx.sidebarExpanded}
clearHoverProjectSoon={props.ctx.clearHoverProjectSoon}
/>
</Show>
<Show when={props.loading()}>
<SessionSkeleton />
</Show>
<For each={props.sessions()}>
{(session) => (
<SessionItem
session={session}
list={props.sessions()}
navList={props.ctx.navList}
slug={props.slug()}
mobile={props.mobile}
showChild
sidebarExpanded={props.ctx.sidebarExpanded}
clearHoverProjectSoon={props.ctx.clearHoverProjectSoon}
prefetchSession={props.ctx.prefetchSession}
archiveSession={props.ctx.archiveSession}
/>
)}
</For>
<Show when={props.hasMore()}>
<div class="relative w-full py-1">
<Button
variant="ghost"
class="flex w-full text-left justify-start text-14-regular text-text-weak pl-2 pr-10"
size="large"
onClick={(e: MouseEvent) => {
void props.loadMore()
;(e.currentTarget as HTMLButtonElement).blur()
}}
>
{props.language.t("common.loadMore")}
</Button>
</div>
</Show>
</nav>
)
export const SortableWorkspace = (props: {
ctx: WorkspaceSidebarContext
directory: string
project: LocalProject
sortNow: Accessor<number>
mobile?: boolean
}): JSX.Element => {
const navigate = useNavigate()
const params = useParams()
const serverSync = useServerSync()
const queryOptions = useQueryOptions()
const language = useLanguage()
const sortable = createSortable(props.directory)
const [workspaceStore, setWorkspaceStore] = serverSync().child(props.directory, { bootstrap: false })
const [menu, setMenu] = createStore({
open: false,
pendingRename: false,
})
const slug = createMemo(() => base64Encode(props.directory))
const sessions = createMemo(() => sortedRootSessions(workspaceStore, props.sortNow()))
const local = createMemo(() => props.directory === props.project.worktree)
const active = createMemo(() => pathKey(props.ctx.currentDir()) === pathKey(props.directory))
const workspaceValue = createMemo(() => {
const branch = workspaceStore.vcs?.branch
const name = branch ?? getFilename(props.directory)
return props.ctx.workspaceName(props.directory, props.project.id, branch) ?? name
})
const open = createMemo(() => props.ctx.workspaceExpanded(props.directory, local()))
const boot = createMemo(() => open() || active())
const count = createMemo(() => sessions()?.length ?? 0)
const hasMore = createMemo(() => workspaceStore.sessionTotal > count())
const fetching = useIsFetching(() => queryOptions().sessions(pathKey(props.directory)))
const busy = createMemo(() => props.ctx.isBusy(props.directory))
const loading = () => fetching() > 0 && count() === 0
const touch = createMediaQuery("(hover: none)")
const showNew = createMemo(() => !loading() && (touch() || count() === 0 || (active() && !params.id)))
const loadMore = async () => {
setWorkspaceStore("limit", (limit) => (limit ?? 0) + 5)
await serverSync().project.loadSessions(props.directory)
}
const workspaceEditActive = createMemo(() => props.ctx.editorOpen(`workspace:${props.directory}`))
const header = () => (
<WorkspaceHeader
local={local}
busy={busy}
open={open}
directory={props.directory}
language={language}
branch={() => workspaceStore.vcs?.branch}
workspaceValue={workspaceValue}
workspaceEditActive={workspaceEditActive}
InlineEditor={props.ctx.InlineEditor}
renameWorkspace={props.ctx.renameWorkspace}
setEditor={props.ctx.setEditor}
projectId={props.project.id}
/>
)
const openWrapper = (value: boolean) => {
props.ctx.setWorkspaceExpanded(props.directory, value)
if (value) return
if (props.ctx.editorOpen(`workspace:${props.directory}`)) props.ctx.closeEditor()
}
createEffect(() => {
if (!boot()) return
serverSync().child(props.directory, { bootstrap: true })
})
return (
<div
// @ts-ignore
use:sortable
classList={{
"opacity-30": sortable.isActiveDraggable,
"opacity-50 pointer-events-none": busy(),
}}
>
<Collapsible variant="ghost" open={open()} class="shrink-0" onOpenChange={openWrapper}>
<div class="py-1">
<div
class="group/workspace relative"
data-component="workspace-item"
data-workspace={base64Encode(props.directory)}
>
<div class="flex items-center gap-1">
<Show
when={workspaceEditActive()}
fallback={
<Collapsible.Trigger
class={`flex items-center justify-between w-full pl-2 py-1.5 rounded-md hover:bg-surface-raised-base-hover transition-[padding] duration-200 ${
menu.open ? "pr-16" : "pr-2"
} group-hover/workspace:pr-16 group-focus-within/workspace:pr-16`}
data-action="workspace-toggle"
data-workspace={base64Encode(props.directory)}
>
{header()}
</Collapsible.Trigger>
}
>
<div
class={`flex items-center justify-between w-full pl-2 py-1.5 rounded-md transition-[padding] duration-200 ${
menu.open ? "pr-16" : "pr-2"
} group-hover/workspace:pr-16 group-focus-within/workspace:pr-16`}
>
{header()}
</div>
</Show>
<WorkspaceActions
directory={props.directory}
local={local}
busy={busy}
menuOpen={() => menu.open}
pendingRename={() => menu.pendingRename}
setMenuOpen={(open) => setMenu("open", open)}
setPendingRename={(value) => setMenu("pendingRename", value)}
sidebarHovering={props.ctx.sidebarHovering}
touch={touch}
language={language}
workspaceValue={workspaceValue}
openEditor={props.ctx.openEditor}
showResetWorkspaceDialog={props.ctx.showResetWorkspaceDialog}
showDeleteWorkspaceDialog={props.ctx.showDeleteWorkspaceDialog}
root={props.project.worktree}
clearHoverProjectSoon={props.ctx.clearHoverProjectSoon}
navigateToNewSession={() => navigate(`/${slug()}/session`)}
/>
</div>
</div>
</div>
<Collapsible.Content>
<WorkspaceSessionList
slug={slug}
mobile={props.mobile}
ctx={props.ctx}
showNew={showNew}
loading={loading}
sessions={sessions}
hasMore={hasMore}
loadMore={loadMore}
language={language}
/>
</Collapsible.Content>
</Collapsible>
</div>
)
}
export const LocalWorkspace = (props: {
ctx: WorkspaceSidebarContext
project: LocalProject
sortNow: Accessor<number>
mobile?: boolean
}): JSX.Element => {
const serverSync = useServerSync()
const queryOptions = useQueryOptions()
const language = useLanguage()
const workspace = createMemo(() => {
const [store, setStore] = serverSync().child(props.project.worktree)
return { store, setStore }
})
const slug = createMemo(() => base64Encode(props.project.worktree))
const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow()))
const count = createMemo(() => sessions()?.length ?? 0)
const fetching = useIsFetching(() => queryOptions().sessions(pathKey(props.project.worktree)))
const hasMore = createMemo(() => workspace().store.sessionTotal > count())
const loading = () => fetching() > 0 && count() === 0
const loadMore = async () => {
workspace().setStore("limit", (limit) => (limit ?? 0) + 5)
await serverSync().project.loadSessions(props.project.worktree)
}
return (
<div
ref={(el) => props.ctx.setScrollContainerRef(el, props.mobile)}
class="size-full flex flex-col py-2 overflow-y-auto no-scrollbar [overflow-anchor:none]"
>
<WorkspaceSessionList
slug={slug}
mobile={props.mobile}
ctx={props.ctx}
showNew={() => false}
loading={loading}
sessions={sessions}
hasMore={hasMore}
loadMore={loadMore}
language={language}
/>
</div>
)
}
+13 -12
View File
@@ -688,8 +688,6 @@ export default function Page() {
return { return {
queryKey: [...vcsKey(), mode] as const, queryKey: [...vcsKey(), mode] as const,
enabled, enabled,
refetchOnMount: "always" as const,
refetchOnWindowFocus: true,
queryFn: mode queryFn: mode
? () => ? () =>
sdk() sdk()
@@ -703,16 +701,6 @@ export default function Page() {
} }
}) })
const refreshVcs = debounce(() => void queryClient.invalidateQueries({ queryKey: vcsKey() }), 100) const refreshVcs = debounce(() => void queryClient.invalidateQueries({ queryKey: vcsKey() }), 100)
createEffect(
on(
() => desktopReviewOpen() || mobileChanges(),
(open, previous) => {
if (!open || previous || !desktopFileTreeOpen() || vcsQuery.isFetching) return
refreshVcs()
},
{ defer: true },
),
)
const reviewDiffs = () => { const reviewDiffs = () => {
if (reviewMode() === "git" || reviewMode() === "branch") if (reviewMode() === "git" || reviewMode() === "branch")
// avoids suspense // avoids suspense
@@ -959,6 +947,19 @@ export default function Page() {
), ),
) )
const stopVcs = sdk().event.listen((evt) => {
const details = evt.details as { type: string; properties?: unknown }
if (details.type !== "file.watcher.updated" && details.type !== "filesystem.changed") return
const props =
typeof details.properties === "object" && details.properties
? (details.properties as Record<string, unknown>)
: undefined
const file = typeof props?.file === "string" ? props.file : undefined
if (!file || file.startsWith(".git/")) return
refreshVcs()
})
onCleanup(stopVcs)
createEffect( createEffect(
on( on(
() => sdk().directory, () => sdk().directory,
@@ -2,7 +2,7 @@ import { For } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { DockShell } from "@opencode-ai/ui/dock-surface" import { DockShell } from "@opencode-ai/ui/dock-surface"
import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock" import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock"
import { SettingsProvider } from "@/context/settings" import { SettingsProvider, useSettings } from "@/context/settings"
export default { export default {
title: "Composer/Revert Dock", title: "Composer/Revert Dock",
@@ -21,6 +21,9 @@ Real \`SessionRevertDock\` from app code, rendered above a mock composer card.
The live composer overlaps the dock's bottom by 18px (\`session-composer-region-controller.ts\` \`lift()\`). The live composer overlaps the dock's bottom by 18px (\`session-composer-region-controller.ts\` \`lift()\`).
The card below reproduces that overlap so the collapsed/expanded cutoff behavior can be verified in isolation. The card below reproduces that overlap so the collapsed/expanded cutoff behavior can be verified in isolation.
### Layout split
Use the **Layout** button to toggle \`newLayoutDesigns\` and preview both the v2 dock and the legacy (v1) \`DockTray\` fallback.
### Notes ### Notes
- \`onRestore\` only mutates local story state, so nothing in the real session is affected. - \`onRestore\` only mutates local story state, so nothing in the real session is affected.
- Click the header to expand/collapse. Click "Restore message" to remove a row.`, - Click the header to expand/collapse. Click "Restore message" to remove a row.`,
@@ -50,9 +53,11 @@ const btn = (accent?: boolean) =>
}) as const }) as const
function Stage(props: { count: number }) { function Stage(props: { count: number }) {
const settings = useSettings()
const seed = () => messages.slice(0, props.count).map((text, index) => ({ id: `rolled-${index}`, text })) const seed = () => messages.slice(0, props.count).map((text, index) => ({ id: `rolled-${index}`, text }))
const [store, setStore] = createStore({ items: seed() }) const [store, setStore] = createStore({ items: seed() })
const v2 = () => settings.general.newLayoutDesigns()
const reset = () => setStore("items", seed()) const reset = () => setStore("items", seed())
const restore = (id: string) => const restore = (id: string) =>
setStore( setStore(
@@ -66,15 +71,22 @@ function Stage(props: { count: number }) {
<button style={btn()} onClick={reset}> <button style={btn()} onClick={reset}>
Reset ({props.count}) Reset ({props.count})
</button> </button>
<button style={btn(v2())} onClick={() => settings.general.setNewLayoutDesigns(!v2())}>
Layout: {v2() ? "v2" : "v1"}
</button>
</div> </div>
{/* Reproduce the real composer stack: dock + card overlapping the dock's bottom by lift() = 18px */} {/* Reproduce the real composer stack: dock + card overlapping the dock's bottom by lift() = 18px */}
<div style={{ display: "flex", "flex-direction": "column" }}> <div style={{ display: "flex", "flex-direction": "column" }}>
<SessionRevertDock items={store.items} onRestore={restore} /> <SessionRevertDock items={store.items} onRestore={restore} />
<DockShell <DockShell
data-dock-border-underlay="v2" data-dock-border-underlay={v2() ? "v2" : "legacy"}
style={{ position: "relative", "z-index": 70, "margin-top": "-18px" }} style={{ position: "relative", "z-index": 70, "margin-top": "-18px" }}
class="min-h-24 w-full rounded-[12px] bg-v2-background-bg-base px-4 py-3 text-[13px] text-v2-text-text-faint" classList={{
"min-h-24 w-full rounded-[12px] px-4 py-3 text-[13px]": true,
"bg-v2-background-bg-base text-v2-text-text-faint": v2(),
"text-text-weak": !v2(),
}}
> >
Ask anything... Ask anything...
</DockShell> </DockShell>
+1 -2
View File
@@ -22,6 +22,5 @@
} }
}, },
"include": ["src", "package.json"], "include": ["src", "package.json"],
"exclude": ["dist", "ts-dist"], "exclude": ["dist", "ts-dist"]
"references": [{ "path": "../core" }]
} }
@@ -26,7 +26,6 @@ export default Runtime.handler(
const location = await client.location.get({ location: { directory: process.cwd() } }) const location = await client.location.get({ location: { directory: process.cwd() } })
const page = await client.session.list({ const page = await client.session.list({
directory: location.directory, directory: location.directory,
workspace: location.workspaceID,
parentID: null, parentID: null,
order: "desc", order: "desc",
limit: 50, limit: 50,
+1 -1
View File
@@ -86,7 +86,7 @@ export async function runMini(input: MiniCommandInput) {
) => ) =>
resolveSessionTarget({ resolveSessionTarget({
client, client,
location: { directory: next.location.directory, workspace: next.location.workspaceID }, location: { directory: next.location.directory },
agent: next.agent, agent: next.agent,
model: next.model model: next.model
? { providerID: next.model.providerID, id: next.model.modelID, variant: next.variant } ? { providerID: next.model.providerID, id: next.model.modelID, variant: next.variant }
+1 -2
View File
@@ -703,7 +703,7 @@ export async function runNonInteractivePrompt(input: Input) {
? Promise.resolve(undefined) ? Promise.resolve(undefined)
: input.client.form.request : input.client.form.request
.list({ .list({
location: { directory: input.location.directory, workspace: input.location.workspaceID }, location: { directory: input.location.directory },
}) })
.catch(() => undefined), .catch(() => undefined),
]) ])
@@ -757,7 +757,6 @@ function formRequestOptions(location: LocationRef | undefined): [] | [{ headers:
{ {
headers: { headers: {
"x-opencode-directory": encodeURIComponent(location.directory), "x-opencode-directory": encodeURIComponent(location.directory),
...(location.workspaceID ? { "x-opencode-workspace": location.workspaceID } : {}),
}, },
}, },
] ]
+1 -1
View File
@@ -96,7 +96,7 @@ async function execute(input: RunCommandInput, prepared: Prepared, endpoint: End
next.model ?? next.model ??
(options.variant (options.variant
? await client.model ? await client.model
.default({ location: { directory: next.location.directory, workspace: next.location.workspaceID } }) .default({ location: { directory: next.location.directory } })
.then((result) => result.data) .then((result) => result.data)
: undefined) : undefined)
const model = selected const model = selected
-1
View File
@@ -136,7 +136,6 @@ async function latestSession(
const page = await client.session.list( const page = await client.session.list(
{ {
directory: location.directory, directory: location.directory,
workspace: location.workspaceID,
parentID: null, parentID: null,
limit: SESSION_PAGE_LIMIT, limit: SESSION_PAGE_LIMIT,
order: "desc", order: "desc",
+1 -2
View File
@@ -426,14 +426,13 @@ describe("runNonInteractivePrompt", () => {
const globalOptions = { const globalOptions = {
headers: { headers: {
"x-opencode-directory": "%2Fwork%20tree", "x-opencode-directory": "%2Fwork%20tree",
"x-opencode-workspace": "wrk_1",
}, },
} }
expect(sdk.form.cancel).toHaveBeenCalledWith({ sessionID: "global", formID: "frm_live" }, globalOptions) expect(sdk.form.cancel).toHaveBeenCalledWith({ sessionID: "global", formID: "frm_live" }, globalOptions)
expect(sdk.form.cancel).toHaveBeenCalledWith({ sessionID: "ses_1", formID: "frm_pending" }) expect(sdk.form.cancel).toHaveBeenCalledWith({ sessionID: "ses_1", formID: "frm_pending" })
expect(sdk.form.cancel).toHaveBeenCalledWith({ sessionID: "global", formID: "frm_pending_global" }, globalOptions) expect(sdk.form.cancel).toHaveBeenCalledWith({ sessionID: "global", formID: "frm_pending_global" }, globalOptions)
expect(sdk.form.request.list).toHaveBeenCalledWith({ expect(sdk.form.request.list).toHaveBeenCalledWith({
location: { directory: "/work tree", workspace: "wrk_1" }, location: { directory: "/work tree" },
}) })
expect(sdk.question.list).not.toHaveBeenCalled() expect(sdk.question.list).not.toHaveBeenCalled()
expect(sdk.question.reject).not.toHaveBeenCalled() expect(sdk.question.reject).not.toHaveBeenCalled()
+87 -180
View File
@@ -3,13 +3,13 @@ import type { Effect, Stream } from "effect"
import type { Location } from "@opencode-ai/schema/location" import type { Location } from "@opencode-ai/schema/location"
import type { Agent } from "@opencode-ai/schema/agent" import type { Agent } from "@opencode-ai/schema/agent"
import type { Plugin } from "@opencode-ai/schema/plugin" import type { Plugin } from "@opencode-ai/schema/plugin"
import type { Workspace } from "@opencode-ai/schema/workspace"
import type { Session } from "@opencode-ai/schema/session" import type { Session } from "@opencode-ai/schema/session"
import type { AbsolutePath } from "@opencode-ai/schema/schema" import type { AbsolutePath } from "@opencode-ai/schema/schema"
import type { Project } from "@opencode-ai/schema/project" import type { Project } from "@opencode-ai/schema/project"
import type { RelativePath } from "@opencode-ai/schema/schema" import type { RelativePath } from "@opencode-ai/schema/schema"
import type { Brand } from "effect" import type { Brand } from "effect"
import type { Model } from "@opencode-ai/schema/model" import type { Model } from "@opencode-ai/schema/model"
import type { Workspace } from "@opencode-ai/schema/workspace"
import type { SessionMessage } from "@opencode-ai/schema/session-message" import type { SessionMessage } from "@opencode-ai/schema/session-message"
import type { PromptInput } from "@opencode-ai/schema/prompt-input" import type { PromptInput } from "@opencode-ai/schema/prompt-input"
import type { AgentAttachment } from "@opencode-ai/schema/prompt" import type { AgentAttachment } from "@opencode-ai/schema/prompt"
@@ -59,9 +59,7 @@ export interface ServerApi<E = never> {
readonly get: ServerGetOperation<E> readonly get: ServerGetOperation<E>
} }
export type Endpoint2_0Input = { export type Endpoint2_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint2_0Output = Location.Info export type Endpoint2_0Output = Location.Info
export type LocationGetOperation<E = never> = (input?: Endpoint2_0Input) => Effect.Effect<Endpoint2_0Output, E> export type LocationGetOperation<E = never> = (input?: Endpoint2_0Input) => Effect.Effect<Endpoint2_0Output, E>
@@ -69,15 +67,13 @@ export interface LocationApi<E = never> {
readonly get: LocationGetOperation<E> readonly get: LocationGetOperation<E>
} }
export type Endpoint3_0Input = { export type Endpoint3_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint3_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Agent.Info> } export type Endpoint3_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Agent.Info> }
export type AgentListOperation<E = never> = (input?: Endpoint3_0Input) => Effect.Effect<Endpoint3_0Output, E> export type AgentListOperation<E = never> = (input?: Endpoint3_0Input) => Effect.Effect<Endpoint3_0Output, E>
export type Endpoint3_1Input = { export type Endpoint3_1Input = {
readonly agentID: Agent.ID readonly agentID: Agent.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint3_1Output = { readonly location: Location.Info; readonly data: Agent.Info } export type Endpoint3_1Output = { readonly location: Location.Info; readonly data: Agent.Info }
export type AgentGetOperation<E = never> = (input: Endpoint3_1Input) => Effect.Effect<Endpoint3_1Output, E> export type AgentGetOperation<E = never> = (input: Endpoint3_1Input) => Effect.Effect<Endpoint3_1Output, E>
@@ -87,9 +83,7 @@ export interface AgentApi<E = never> {
readonly get: AgentGetOperation<E> readonly get: AgentGetOperation<E>
} }
export type Endpoint4_0Input = { export type Endpoint4_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint4_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Plugin.Info> } export type Endpoint4_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Plugin.Info> }
export type PluginListOperation<E = never> = (input?: Endpoint4_0Input) => Effect.Effect<Endpoint4_0Output, E> export type PluginListOperation<E = never> = (input?: Endpoint4_0Input) => Effect.Effect<Endpoint4_0Output, E>
@@ -98,7 +92,6 @@ export interface PluginApi<E = never> {
} }
export type Endpoint5_0Input = { export type Endpoint5_0Input = {
readonly workspace?: Workspace.ID | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
readonly order?: "asc" | "desc" | undefined readonly order?: "asc" | "desc" | undefined
readonly search?: string | undefined readonly search?: string | undefined
@@ -263,52 +256,38 @@ export type Endpoint5_23Input = { readonly sessionID: Session.ID }
export type Endpoint5_23Output = ReadonlyArray<SessionPending.Info> export type Endpoint5_23Output = ReadonlyArray<SessionPending.Info>
export type SessionPendingListOperation<E = never> = (input: Endpoint5_23Input) => Effect.Effect<Endpoint5_23Output, E> export type SessionPendingListOperation<E = never> = (input: Endpoint5_23Input) => Effect.Effect<Endpoint5_23Output, E>
export type Endpoint5_24Input = { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID } export type Endpoint5_24Input = { readonly sessionID: Session.ID }
export type Endpoint5_24Output = void export type Endpoint5_24Output = ReadonlyArray<InstructionEntry.Info>
export type SessionPendingCancelOperation<E = never> = ( export type SessionInstructionsEntryListOperation<E = never> = (
input: Endpoint5_24Input, input: Endpoint5_24Input,
) => Effect.Effect<Endpoint5_24Output, E> ) => Effect.Effect<Endpoint5_24Output, E>
export type Endpoint5_25Input = { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID } export type Endpoint5_25Input = {
export type Endpoint5_25Output = void
export type SessionPendingSteerOperation<E = never> = (input: Endpoint5_25Input) => Effect.Effect<Endpoint5_25Output, E>
export type Endpoint5_26Input = { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
export type Endpoint5_26Output = void
export type SessionPendingQueueOperation<E = never> = (input: Endpoint5_26Input) => Effect.Effect<Endpoint5_26Output, E>
export type Endpoint5_27Input = { readonly sessionID: Session.ID }
export type Endpoint5_27Output = ReadonlyArray<InstructionEntry.Info>
export type SessionInstructionsEntryListOperation<E = never> = (
input: Endpoint5_27Input,
) => Effect.Effect<Endpoint5_27Output, E>
export type Endpoint5_28Input = {
readonly sessionID: Session.ID readonly sessionID: Session.ID
readonly key: InstructionEntry.Key readonly key: InstructionEntry.Key
readonly value: Schema.Json readonly value: Schema.Json
} }
export type Endpoint5_28Output = void export type Endpoint5_25Output = void
export type SessionInstructionsEntryPutOperation<E = never> = ( export type SessionInstructionsEntryPutOperation<E = never> = (
input: Endpoint5_28Input, input: Endpoint5_25Input,
) => Effect.Effect<Endpoint5_28Output, E> ) => Effect.Effect<Endpoint5_25Output, E>
export type Endpoint5_29Input = { readonly sessionID: Session.ID; readonly key: InstructionEntry.Key } export type Endpoint5_26Input = { readonly sessionID: Session.ID; readonly key: InstructionEntry.Key }
export type Endpoint5_29Output = void export type Endpoint5_26Output = void
export type SessionInstructionsEntryRemoveOperation<E = never> = ( export type SessionInstructionsEntryRemoveOperation<E = never> = (
input: Endpoint5_29Input, input: Endpoint5_26Input,
) => Effect.Effect<Endpoint5_29Output, E> ) => Effect.Effect<Endpoint5_26Output, E>
export type Endpoint5_30Input = { readonly sessionID: Session.ID; readonly prompt: string } export type Endpoint5_27Input = { readonly sessionID: Session.ID; readonly prompt: string }
export type Endpoint5_30Output = { readonly text: string } export type Endpoint5_27Output = { readonly text: string }
export type SessionGenerateOperation<E = never> = (input: Endpoint5_30Input) => Effect.Effect<Endpoint5_30Output, E> export type SessionGenerateOperation<E = never> = (input: Endpoint5_27Input) => Effect.Effect<Endpoint5_27Output, E>
export type Endpoint5_31Input = { export type Endpoint5_28Input = {
readonly sessionID: Session.ID readonly sessionID: Session.ID
readonly after?: Event.Seq | undefined readonly after?: Event.Seq | undefined
readonly follow?: boolean | undefined readonly follow?: boolean | undefined
} }
export type Endpoint5_31Output = export type Endpoint5_28Output =
| ( | (
| { | {
readonly id: Event.ID readonly id: Event.ID
@@ -418,33 +397,6 @@ export type Endpoint5_31Output =
readonly input: SessionPending.Message readonly input: SessionPending.Message
} }
} }
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.input.cancelled"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
readonly location?: Location.Ref | undefined
readonly data: { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.input.steered"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
readonly location?: Location.Ref | undefined
readonly data: { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.input.queued"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
readonly location?: Location.Ref | undefined
readonly data: { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
}
| { | {
readonly id: Event.ID readonly id: Event.ID
readonly created: DateTime.Utc readonly created: DateTime.Utc
@@ -903,19 +855,19 @@ export type Endpoint5_31Output =
} }
) )
| EventLog.Synced | EventLog.Synced
export type SessionLogOperation<E = never> = (input: Endpoint5_31Input) => Stream.Stream<Endpoint5_31Output, E> export type SessionLogOperation<E = never> = (input: Endpoint5_28Input) => Stream.Stream<Endpoint5_28Output, E>
export type Endpoint5_32Input = { readonly sessionID: Session.ID } export type Endpoint5_29Input = { readonly sessionID: Session.ID }
export type Endpoint5_32Output = void export type Endpoint5_29Output = void
export type SessionInterruptOperation<E = never> = (input: Endpoint5_32Input) => Effect.Effect<Endpoint5_32Output, E> export type SessionInterruptOperation<E = never> = (input: Endpoint5_29Input) => Effect.Effect<Endpoint5_29Output, E>
export type Endpoint5_33Input = { readonly sessionID: Session.ID } export type Endpoint5_30Input = { readonly sessionID: Session.ID }
export type Endpoint5_33Output = void export type Endpoint5_30Output = void
export type SessionBackgroundOperation<E = never> = (input: Endpoint5_33Input) => Effect.Effect<Endpoint5_33Output, E> export type SessionBackgroundOperation<E = never> = (input: Endpoint5_30Input) => Effect.Effect<Endpoint5_30Output, E>
export type Endpoint5_34Input = { readonly sessionID: Session.ID; readonly messageID: SessionMessage.ID } export type Endpoint5_31Input = { readonly sessionID: Session.ID; readonly messageID: SessionMessage.ID }
export type Endpoint5_34Output = SessionMessage.Info export type Endpoint5_31Output = SessionMessage.Info
export type SessionMessageOperation<E = never> = (input: Endpoint5_34Input) => Effect.Effect<Endpoint5_34Output, E> export type SessionMessageOperation<E = never> = (input: Endpoint5_31Input) => Effect.Effect<Endpoint5_31Output, E>
export interface SessionApi<E = never> { export interface SessionApi<E = never> {
readonly list: SessionListOperation<E> readonly list: SessionListOperation<E>
@@ -943,12 +895,7 @@ export interface SessionApi<E = never> {
readonly commit: SessionRevertCommitOperation<E> readonly commit: SessionRevertCommitOperation<E>
} }
readonly context: SessionContextOperation<E> readonly context: SessionContextOperation<E>
readonly pending: { readonly pending: { readonly list: SessionPendingListOperation<E> }
readonly list: SessionPendingListOperation<E>
readonly cancel: SessionPendingCancelOperation<E>
readonly steer: SessionPendingSteerOperation<E>
readonly queue: SessionPendingQueueOperation<E>
}
readonly instructions: { readonly instructions: {
readonly entry: { readonly entry: {
readonly list: SessionInstructionsEntryListOperation<E> readonly list: SessionInstructionsEntryListOperation<E>
@@ -979,15 +926,11 @@ export interface MessageApi<E = never> {
readonly list: MessageListOperation<E> readonly list: MessageListOperation<E>
} }
export type Endpoint7_0Input = { export type Endpoint7_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint7_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Model.Info> } export type Endpoint7_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Model.Info> }
export type ModelListOperation<E = never> = (input?: Endpoint7_0Input) => Effect.Effect<Endpoint7_0Output, E> export type ModelListOperation<E = never> = (input?: Endpoint7_0Input) => Effect.Effect<Endpoint7_0Output, E>
export type Endpoint7_1Input = { export type Endpoint7_1Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint7_1Output = { readonly location: Location.Info; readonly data: Model.Info | undefined } export type Endpoint7_1Output = { readonly location: Location.Info; readonly data: Model.Info | undefined }
export type ModelDefaultOperation<E = never> = (input?: Endpoint7_1Input) => Effect.Effect<Endpoint7_1Output, E> export type ModelDefaultOperation<E = never> = (input?: Endpoint7_1Input) => Effect.Effect<Endpoint7_1Output, E>
@@ -997,7 +940,7 @@ export interface ModelApi<E = never> {
} }
export type Endpoint8_0Input = { export type Endpoint8_0Input = {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly prompt: string readonly prompt: string
readonly model?: Model.Ref | undefined readonly model?: Model.Ref | undefined
} }
@@ -1008,15 +951,13 @@ export interface GenerateApi<E = never> {
readonly text: GenerateTextOperation<E> readonly text: GenerateTextOperation<E>
} }
export type Endpoint9_0Input = { export type Endpoint9_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint9_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Provider.Info> } export type Endpoint9_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Provider.Info> }
export type ProviderListOperation<E = never> = (input?: Endpoint9_0Input) => Effect.Effect<Endpoint9_0Output, E> export type ProviderListOperation<E = never> = (input?: Endpoint9_0Input) => Effect.Effect<Endpoint9_0Output, E>
export type Endpoint9_1Input = { export type Endpoint9_1Input = {
readonly providerID: Provider.ID readonly providerID: Provider.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint9_1Output = { readonly location: Location.Info; readonly data: Provider.Info } export type Endpoint9_1Output = { readonly location: Location.Info; readonly data: Provider.Info }
export type ProviderGetOperation<E = never> = (input: Endpoint9_1Input) => Effect.Effect<Endpoint9_1Output, E> export type ProviderGetOperation<E = never> = (input: Endpoint9_1Input) => Effect.Effect<Endpoint9_1Output, E>
@@ -1026,21 +967,19 @@ export interface ProviderApi<E = never> {
readonly get: ProviderGetOperation<E> readonly get: ProviderGetOperation<E>
} }
export type Endpoint10_0Input = { export type Endpoint10_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint10_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Integration.Info> } export type Endpoint10_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Integration.Info> }
export type IntegrationListOperation<E = never> = (input?: Endpoint10_0Input) => Effect.Effect<Endpoint10_0Output, E> export type IntegrationListOperation<E = never> = (input?: Endpoint10_0Input) => Effect.Effect<Endpoint10_0Output, E>
export type Endpoint10_1Input = { export type Endpoint10_1Input = {
readonly integrationID: Integration.ID readonly integrationID: Integration.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint10_1Output = { readonly location: Location.Info; readonly data: Integration.Info | undefined } export type Endpoint10_1Output = { readonly location: Location.Info; readonly data: Integration.Info | undefined }
export type IntegrationGetOperation<E = never> = (input: Endpoint10_1Input) => Effect.Effect<Endpoint10_1Output, E> export type IntegrationGetOperation<E = never> = (input: Endpoint10_1Input) => Effect.Effect<Endpoint10_1Output, E>
export type Endpoint10_2Input = { export type Endpoint10_2Input = {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly url: string readonly url: string
} }
export type Endpoint10_2Output = void export type Endpoint10_2Output = void
@@ -1050,7 +989,7 @@ export type IntegrationWellknownAddOperation<E = never> = (
export type Endpoint10_3Input = { export type Endpoint10_3Input = {
readonly integrationID: Integration.ID readonly integrationID: Integration.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly key: string readonly key: string
readonly label?: string | undefined readonly label?: string | undefined
} }
@@ -1061,7 +1000,7 @@ export type IntegrationConnectKeyOperation<E = never> = (
export type Endpoint10_4Input = { export type Endpoint10_4Input = {
readonly integrationID: Integration.ID readonly integrationID: Integration.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly methodID: Integration.MethodID readonly methodID: Integration.MethodID
readonly inputs: { readonly [x: string]: string } readonly inputs: { readonly [x: string]: string }
readonly label?: string | undefined readonly label?: string | undefined
@@ -1074,7 +1013,7 @@ export type IntegrationOauthConnectOperation<E = never> = (
export type Endpoint10_5Input = { export type Endpoint10_5Input = {
readonly integrationID: Integration.ID readonly integrationID: Integration.ID
readonly attemptID: Integration.AttemptID readonly attemptID: Integration.AttemptID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint10_5Output = { readonly location: Location.Info; readonly data: Integration.AttemptStatus } export type Endpoint10_5Output = { readonly location: Location.Info; readonly data: Integration.AttemptStatus }
export type IntegrationOauthStatusOperation<E = never> = ( export type IntegrationOauthStatusOperation<E = never> = (
@@ -1084,7 +1023,7 @@ export type IntegrationOauthStatusOperation<E = never> = (
export type Endpoint10_6Input = { export type Endpoint10_6Input = {
readonly integrationID: Integration.ID readonly integrationID: Integration.ID
readonly attemptID: Integration.AttemptID readonly attemptID: Integration.AttemptID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly code?: string | undefined readonly code?: string | undefined
} }
export type Endpoint10_6Output = void export type Endpoint10_6Output = void
@@ -1095,7 +1034,7 @@ export type IntegrationOauthCompleteOperation<E = never> = (
export type Endpoint10_7Input = { export type Endpoint10_7Input = {
readonly integrationID: Integration.ID readonly integrationID: Integration.ID
readonly attemptID: Integration.AttemptID readonly attemptID: Integration.AttemptID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint10_7Output = void export type Endpoint10_7Output = void
export type IntegrationOauthCancelOperation<E = never> = ( export type IntegrationOauthCancelOperation<E = never> = (
@@ -1104,7 +1043,7 @@ export type IntegrationOauthCancelOperation<E = never> = (
export type Endpoint10_8Input = { export type Endpoint10_8Input = {
readonly integrationID: Integration.ID readonly integrationID: Integration.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly methodID: Integration.MethodID readonly methodID: Integration.MethodID
readonly label?: string | undefined readonly label?: string | undefined
} }
@@ -1116,7 +1055,7 @@ export type IntegrationCommandConnectOperation<E = never> = (
export type Endpoint10_9Input = { export type Endpoint10_9Input = {
readonly integrationID: Integration.ID readonly integrationID: Integration.ID
readonly attemptID: Integration.AttemptID readonly attemptID: Integration.AttemptID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint10_9Output = { readonly location: Location.Info; readonly data: Integration.CommandAttemptStatus } export type Endpoint10_9Output = { readonly location: Location.Info; readonly data: Integration.CommandAttemptStatus }
export type IntegrationCommandStatusOperation<E = never> = ( export type IntegrationCommandStatusOperation<E = never> = (
@@ -1126,7 +1065,7 @@ export type IntegrationCommandStatusOperation<E = never> = (
export type Endpoint10_10Input = { export type Endpoint10_10Input = {
readonly integrationID: Integration.ID readonly integrationID: Integration.ID
readonly attemptID: Integration.AttemptID readonly attemptID: Integration.AttemptID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint10_10Output = void export type Endpoint10_10Output = void
export type IntegrationCommandCancelOperation<E = never> = ( export type IntegrationCommandCancelOperation<E = never> = (
@@ -1151,15 +1090,13 @@ export interface IntegrationApi<E = never> {
} }
} }
export type Endpoint11_0Input = { export type Endpoint11_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint11_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Mcp.Server> } export type Endpoint11_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Mcp.Server> }
export type McpListOperation<E = never> = (input?: Endpoint11_0Input) => Effect.Effect<Endpoint11_0Output, E> export type McpListOperation<E = never> = (input?: Endpoint11_0Input) => Effect.Effect<Endpoint11_0Output, E>
export type Endpoint11_1Input = { export type Endpoint11_1Input = {
readonly server: string readonly server: string
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly config: Mcp.LocalConfig | Mcp.RemoteConfig readonly config: Mcp.LocalConfig | Mcp.RemoteConfig
} }
export type Endpoint11_1Output = void export type Endpoint11_1Output = void
@@ -1167,28 +1104,26 @@ export type McpAddOperation<E = never> = (input: Endpoint11_1Input) => Effect.Ef
export type Endpoint11_2Input = { export type Endpoint11_2Input = {
readonly server: string readonly server: string
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint11_2Output = void export type Endpoint11_2Output = void
export type McpRemoveOperation<E = never> = (input: Endpoint11_2Input) => Effect.Effect<Endpoint11_2Output, E> export type McpRemoveOperation<E = never> = (input: Endpoint11_2Input) => Effect.Effect<Endpoint11_2Output, E>
export type Endpoint11_3Input = { export type Endpoint11_3Input = {
readonly server: string readonly server: string
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint11_3Output = void export type Endpoint11_3Output = void
export type McpConnectOperation<E = never> = (input: Endpoint11_3Input) => Effect.Effect<Endpoint11_3Output, E> export type McpConnectOperation<E = never> = (input: Endpoint11_3Input) => Effect.Effect<Endpoint11_3Output, E>
export type Endpoint11_4Input = { export type Endpoint11_4Input = {
readonly server: string readonly server: string
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint11_4Output = void export type Endpoint11_4Output = void
export type McpDisconnectOperation<E = never> = (input: Endpoint11_4Input) => Effect.Effect<Endpoint11_4Output, E> export type McpDisconnectOperation<E = never> = (input: Endpoint11_4Input) => Effect.Effect<Endpoint11_4Output, E>
export type Endpoint11_5Input = { export type Endpoint11_5Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint11_5Output = { readonly location: Location.Info; readonly data: Mcp.ResourceCatalog } export type Endpoint11_5Output = { readonly location: Location.Info; readonly data: Mcp.ResourceCatalog }
export type McpResourceCatalogOperation<E = never> = (input?: Endpoint11_5Input) => Effect.Effect<Endpoint11_5Output, E> export type McpResourceCatalogOperation<E = never> = (input?: Endpoint11_5Input) => Effect.Effect<Endpoint11_5Output, E>
@@ -1203,7 +1138,7 @@ export interface McpApi<E = never> {
export type Endpoint12_0Input = { export type Endpoint12_0Input = {
readonly credentialID: Credential.ID readonly credentialID: Credential.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly label: string readonly label: string
} }
export type Endpoint12_0Output = void export type Endpoint12_0Output = void
@@ -1211,7 +1146,7 @@ export type CredentialUpdateOperation<E = never> = (input: Endpoint12_0Input) =>
export type Endpoint12_1Input = { export type Endpoint12_1Input = {
readonly credentialID: Credential.ID readonly credentialID: Credential.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint12_1Output = void export type Endpoint12_1Output = void
export type CredentialRemoveOperation<E = never> = (input: Endpoint12_1Input) => Effect.Effect<Endpoint12_1Output, E> export type CredentialRemoveOperation<E = never> = (input: Endpoint12_1Input) => Effect.Effect<Endpoint12_1Output, E>
@@ -1224,15 +1159,13 @@ export interface CredentialApi<E = never> {
export type Endpoint13_0Output = ReadonlyArray<Project.Info> export type Endpoint13_0Output = ReadonlyArray<Project.Info>
export type ProjectListOperation<E = never> = () => Effect.Effect<Endpoint13_0Output, E> export type ProjectListOperation<E = never> = () => Effect.Effect<Endpoint13_0Output, E>
export type Endpoint13_1Input = { export type Endpoint13_1Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint13_1Output = Project.Current export type Endpoint13_1Output = Project.Current
export type ProjectCurrentOperation<E = never> = (input?: Endpoint13_1Input) => Effect.Effect<Endpoint13_1Output, E> export type ProjectCurrentOperation<E = never> = (input?: Endpoint13_1Input) => Effect.Effect<Endpoint13_1Output, E>
export type Endpoint13_2Input = { export type Endpoint13_2Input = {
readonly projectID: Project.ID readonly projectID: Project.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint13_2Output = Project.Directories export type Endpoint13_2Output = Project.Directories
export type ProjectDirectoriesOperation<E = never> = (input: Endpoint13_2Input) => Effect.Effect<Endpoint13_2Output, E> export type ProjectDirectoriesOperation<E = never> = (input: Endpoint13_2Input) => Effect.Effect<Endpoint13_2Output, E>
@@ -1243,9 +1176,7 @@ export interface ProjectApi<E = never> {
readonly directories: ProjectDirectoriesOperation<E> readonly directories: ProjectDirectoriesOperation<E>
} }
export type Endpoint14_0Input = { export type Endpoint14_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint14_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Form.Info> } export type Endpoint14_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Form.Info> }
export type FormRequestListOperation<E = never> = (input?: Endpoint14_0Input) => Effect.Effect<Endpoint14_0Output, E> export type FormRequestListOperation<E = never> = (input?: Endpoint14_0Input) => Effect.Effect<Endpoint14_0Output, E>
@@ -1289,9 +1220,7 @@ export interface FormApi<E = never> {
readonly cancel: FormCancelOperation<E> readonly cancel: FormCancelOperation<E>
} }
export type Endpoint15_0Input = { export type Endpoint15_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint15_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Permission.Request> } export type Endpoint15_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Permission.Request> }
export type PermissionRequestListOperation<E = never> = ( export type PermissionRequestListOperation<E = never> = (
input?: Endpoint15_0Input, input?: Endpoint15_0Input,
@@ -1349,14 +1278,14 @@ export interface PermissionApi<E = never> {
} }
export type Endpoint16_0Input = { export type Endpoint16_0Input = {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly path?: RelativePath | undefined readonly path?: RelativePath | undefined
} }
export type Endpoint16_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<FileSystem.Entry> } export type Endpoint16_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<FileSystem.Entry> }
export type FileListOperation<E = never> = (input?: Endpoint16_0Input) => Effect.Effect<Endpoint16_0Output, E> export type FileListOperation<E = never> = (input?: Endpoint16_0Input) => Effect.Effect<Endpoint16_0Output, E>
export type Endpoint16_1Input = { export type Endpoint16_1Input = {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly query: string readonly query: string
readonly type?: "file" | "directory" | undefined readonly type?: "file" | "directory" | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
@@ -1369,9 +1298,7 @@ export interface FileApi<E = never> {
readonly find: FileFindOperation<E> readonly find: FileFindOperation<E>
} }
export type Endpoint17_0Input = { export type Endpoint17_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint17_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Command.Info> } export type Endpoint17_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Command.Info> }
export type CommandListOperation<E = never> = (input?: Endpoint17_0Input) => Effect.Effect<Endpoint17_0Output, E> export type CommandListOperation<E = never> = (input?: Endpoint17_0Input) => Effect.Effect<Endpoint17_0Output, E>
@@ -1379,9 +1306,7 @@ export interface CommandApi<E = never> {
readonly list: CommandListOperation<E> readonly list: CommandListOperation<E>
} }
export type Endpoint18_0Input = { export type Endpoint18_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint18_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Skill.Info> } export type Endpoint18_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Skill.Info> }
export type SkillListOperation<E = never> = (input?: Endpoint18_0Input) => Effect.Effect<Endpoint18_0Output, E> export type SkillListOperation<E = never> = (input?: Endpoint18_0Input) => Effect.Effect<Endpoint18_0Output, E>
@@ -1396,14 +1321,12 @@ export interface EventApi<E = never> {
readonly subscribe: EventSubscribeOperation<E> readonly subscribe: EventSubscribeOperation<E>
} }
export type Endpoint20_0Input = { export type Endpoint20_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint20_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Pty.Info> } export type Endpoint20_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Pty.Info> }
export type PtyListOperation<E = never> = (input?: Endpoint20_0Input) => Effect.Effect<Endpoint20_0Output, E> export type PtyListOperation<E = never> = (input?: Endpoint20_0Input) => Effect.Effect<Endpoint20_0Output, E>
export type Endpoint20_1Input = { export type Endpoint20_1Input = {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly command?: string | undefined readonly command?: string | undefined
readonly args?: ReadonlyArray<string> | undefined readonly args?: ReadonlyArray<string> | undefined
readonly cwd?: string | undefined readonly cwd?: string | undefined
@@ -1415,14 +1338,14 @@ export type PtyCreateOperation<E = never> = (input?: Endpoint20_1Input) => Effec
export type Endpoint20_2Input = { export type Endpoint20_2Input = {
readonly ptyID: Pty.ID readonly ptyID: Pty.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint20_2Output = { readonly location: Location.Info; readonly data: Pty.Info } export type Endpoint20_2Output = { readonly location: Location.Info; readonly data: Pty.Info }
export type PtyGetOperation<E = never> = (input: Endpoint20_2Input) => Effect.Effect<Endpoint20_2Output, E> export type PtyGetOperation<E = never> = (input: Endpoint20_2Input) => Effect.Effect<Endpoint20_2Output, E>
export type Endpoint20_3Input = { export type Endpoint20_3Input = {
readonly ptyID: Pty.ID readonly ptyID: Pty.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly title?: string | undefined readonly title?: string | undefined
readonly size?: { readonly rows: number; readonly cols: number } | undefined readonly size?: { readonly rows: number; readonly cols: number } | undefined
} }
@@ -1431,7 +1354,7 @@ export type PtyUpdateOperation<E = never> = (input: Endpoint20_3Input) => Effect
export type Endpoint20_4Input = { export type Endpoint20_4Input = {
readonly ptyID: Pty.ID readonly ptyID: Pty.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint20_4Output = void export type Endpoint20_4Output = void
export type PtyRemoveOperation<E = never> = (input: Endpoint20_4Input) => Effect.Effect<Endpoint20_4Output, E> export type PtyRemoveOperation<E = never> = (input: Endpoint20_4Input) => Effect.Effect<Endpoint20_4Output, E>
@@ -1444,14 +1367,12 @@ export interface PtyApi<E = never> {
readonly remove: PtyRemoveOperation<E> readonly remove: PtyRemoveOperation<E>
} }
export type Endpoint21_0Input = { export type Endpoint21_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint21_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Shell.Info> } export type Endpoint21_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Shell.Info> }
export type ShellListOperation<E = never> = (input?: Endpoint21_0Input) => Effect.Effect<Endpoint21_0Output, E> export type ShellListOperation<E = never> = (input?: Endpoint21_0Input) => Effect.Effect<Endpoint21_0Output, E>
export type Endpoint21_1Input = { export type Endpoint21_1Input = {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly command: string readonly command: string
readonly cwd?: string | undefined readonly cwd?: string | undefined
readonly timeout: number readonly timeout: number
@@ -1462,14 +1383,14 @@ export type ShellCreateOperation<E = never> = (input: Endpoint21_1Input) => Effe
export type Endpoint21_2Input = { export type Endpoint21_2Input = {
readonly id: Shell.ID readonly id: Shell.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint21_2Output = { readonly location: Location.Info; readonly data: Shell.Info } export type Endpoint21_2Output = { readonly location: Location.Info; readonly data: Shell.Info }
export type ShellGetOperation<E = never> = (input: Endpoint21_2Input) => Effect.Effect<Endpoint21_2Output, E> export type ShellGetOperation<E = never> = (input: Endpoint21_2Input) => Effect.Effect<Endpoint21_2Output, E>
export type Endpoint21_3Input = { export type Endpoint21_3Input = {
readonly id: Shell.ID readonly id: Shell.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly timeout: number readonly timeout: number
} }
export type Endpoint21_3Output = { readonly location: Location.Info; readonly data: Shell.Info } export type Endpoint21_3Output = { readonly location: Location.Info; readonly data: Shell.Info }
@@ -1477,7 +1398,7 @@ export type ShellTimeoutOperation<E = never> = (input: Endpoint21_3Input) => Eff
export type Endpoint21_4Input = { export type Endpoint21_4Input = {
readonly id: Shell.ID readonly id: Shell.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly cursor?: number | undefined readonly cursor?: number | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
} }
@@ -1494,7 +1415,7 @@ export type ShellOutputOperation<E = never> = (input: Endpoint21_4Input) => Effe
export type Endpoint21_5Input = { export type Endpoint21_5Input = {
readonly id: Shell.ID readonly id: Shell.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint21_5Output = void export type Endpoint21_5Output = void
export type ShellRemoveOperation<E = never> = (input: Endpoint21_5Input) => Effect.Effect<Endpoint21_5Output, E> export type ShellRemoveOperation<E = never> = (input: Endpoint21_5Input) => Effect.Effect<Endpoint21_5Output, E>
@@ -1508,9 +1429,7 @@ export interface ShellApi<E = never> {
readonly remove: ShellRemoveOperation<E> readonly remove: ShellRemoveOperation<E>
} }
export type Endpoint22_0Input = { export type Endpoint22_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint22_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Question.Request> } export type Endpoint22_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Question.Request> }
export type QuestionRequestListOperation<E = never> = ( export type QuestionRequestListOperation<E = never> = (
input?: Endpoint22_0Input, input?: Endpoint22_0Input,
@@ -1539,9 +1458,7 @@ export interface QuestionApi<E = never> {
readonly reject: QuestionRejectOperation<E> readonly reject: QuestionRejectOperation<E>
} }
export type Endpoint23_0Input = { export type Endpoint23_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint23_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Reference.Info> } export type Endpoint23_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Reference.Info> }
export type ReferenceListOperation<E = never> = (input?: Endpoint23_0Input) => Effect.Effect<Endpoint23_0Output, E> export type ReferenceListOperation<E = never> = (input?: Endpoint23_0Input) => Effect.Effect<Endpoint23_0Output, E>
@@ -1551,7 +1468,7 @@ export interface ReferenceApi<E = never> {
export type Endpoint24_0Input = { export type Endpoint24_0Input = {
readonly projectID: Project.ID readonly projectID: Project.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly strategy: ProjectCopy.StrategyID readonly strategy: ProjectCopy.StrategyID
readonly directory: AbsolutePath readonly directory: AbsolutePath
readonly name?: string | undefined readonly name?: string | undefined
@@ -1561,7 +1478,7 @@ export type ProjectCopyCreateOperation<E = never> = (input: Endpoint24_0Input) =
export type Endpoint24_1Input = { export type Endpoint24_1Input = {
readonly projectID: Project.ID readonly projectID: Project.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly directory: AbsolutePath readonly directory: AbsolutePath
readonly force: boolean readonly force: boolean
} }
@@ -1570,7 +1487,7 @@ export type ProjectCopyRemoveOperation<E = never> = (input: Endpoint24_1Input) =
export type Endpoint24_2Input = { export type Endpoint24_2Input = {
readonly projectID: Project.ID readonly projectID: Project.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
} }
export type Endpoint24_2Output = void export type Endpoint24_2Output = void
export type ProjectCopyRefreshOperation<E = never> = (input: Endpoint24_2Input) => Effect.Effect<Endpoint24_2Output, E> export type ProjectCopyRefreshOperation<E = never> = (input: Endpoint24_2Input) => Effect.Effect<Endpoint24_2Output, E>
@@ -1581,20 +1498,16 @@ export interface ProjectCopyApi<E = never> {
readonly refresh: ProjectCopyRefreshOperation<E> readonly refresh: ProjectCopyRefreshOperation<E>
} }
export type Endpoint25_0Input = { export type Endpoint25_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint25_0Output = { readonly location: Location.Info; readonly data: Vcs.Info } export type Endpoint25_0Output = { readonly location: Location.Info; readonly data: Vcs.Info }
export type VcsGetOperation<E = never> = (input?: Endpoint25_0Input) => Effect.Effect<Endpoint25_0Output, E> export type VcsGetOperation<E = never> = (input?: Endpoint25_0Input) => Effect.Effect<Endpoint25_0Output, E>
export type Endpoint25_1Input = { export type Endpoint25_1Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint25_1Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Vcs.FileStatus> } export type Endpoint25_1Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Vcs.FileStatus> }
export type VcsStatusOperation<E = never> = (input?: Endpoint25_1Input) => Effect.Effect<Endpoint25_1Output, E> export type VcsStatusOperation<E = never> = (input?: Endpoint25_1Input) => Effect.Effect<Endpoint25_1Output, E>
export type Endpoint25_2Input = { export type Endpoint25_2Input = {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly mode: Vcs.Mode readonly mode: Vcs.Mode
readonly context?: number | undefined readonly context?: number | undefined
} }
@@ -1610,9 +1523,7 @@ export interface VcsApi<E = never> {
export type Endpoint26_0Output = ReadonlyArray<Location.Ref> export type Endpoint26_0Output = ReadonlyArray<Location.Ref>
export type DebugLocationListOperation<E = never> = () => Effect.Effect<Endpoint26_0Output, E> export type DebugLocationListOperation<E = never> = () => Effect.Effect<Endpoint26_0Output, E>
export type Endpoint26_1Input = { export type Endpoint26_1Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint26_1Output = void export type Endpoint26_1Output = void
export type DebugLocationEvictOperation<E = never> = (input?: Endpoint26_1Input) => Effect.Effect<Endpoint26_1Output, E> export type DebugLocationEvictOperation<E = never> = (input?: Endpoint26_1Input) => Effect.Effect<Endpoint26_1Output, E>
@@ -1637,14 +1548,12 @@ export interface MigrationApi<E = never> {
readonly v1: { readonly status: MigrationV1StatusOperation<E> } readonly v1: { readonly status: MigrationV1StatusOperation<E> }
} }
export type Endpoint28_0Input = { export type Endpoint28_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint28_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<WebSearch.Provider> } export type Endpoint28_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<WebSearch.Provider> }
export type WebsearchProvidersOperation<E = never> = (input?: Endpoint28_0Input) => Effect.Effect<Endpoint28_0Output, E> export type WebsearchProvidersOperation<E = never> = (input?: Endpoint28_0Input) => Effect.Effect<Endpoint28_0Output, E>
export type Endpoint28_1Input = { export type Endpoint28_1Input = {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly query: string readonly query: string
readonly providerID?: WebSearch.ID | undefined readonly providerID?: WebSearch.ID | undefined
} }
@@ -1656,9 +1565,7 @@ export interface WebsearchApi<E = never> {
readonly query: WebsearchQueryOperation<E> readonly query: WebsearchQueryOperation<E>
} }
export type Endpoint29_0Input = { export type Endpoint29_0Input = { readonly location?: { readonly directory?: string | undefined } | undefined }
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint29_0Output = ReadonlyArray<Config.Entry> export type Endpoint29_0Output = ReadonlyArray<Config.Entry>
export type ConfigGetOperation<E = never> = (input?: Endpoint29_0Input) => Effect.Effect<Endpoint29_0Output, E> export type ConfigGetOperation<E = never> = (input?: Endpoint29_0Input) => Effect.Effect<Endpoint29_0Output, E>
+21 -49
View File
@@ -80,12 +80,6 @@ import type {
Endpoint5_30Output, Endpoint5_30Output,
Endpoint5_31Input, Endpoint5_31Input,
Endpoint5_31Output, Endpoint5_31Output,
Endpoint5_32Input,
Endpoint5_32Output,
Endpoint5_33Input,
Endpoint5_33Output,
Endpoint5_34Input,
Endpoint5_34Output,
Endpoint6_0Input, Endpoint6_0Input,
Endpoint6_0Output, Endpoint6_0Output,
Endpoint7_0Input, Endpoint7_0Input,
@@ -298,7 +292,6 @@ const Endpoint5_0 = (raw: RawClient["server.session"]) => (input?: Endpoint5_0In
preserveEffect<Endpoint5_0Output>()( preserveEffect<Endpoint5_0Output>()(
raw["session.list"]({ raw["session.list"]({
query: { query: {
workspace: input?.["workspace"],
limit: input?.["limit"], limit: input?.["limit"],
order: input?.["order"], order: input?.["order"],
search: input?.["search"], search: input?.["search"],
@@ -529,58 +522,37 @@ const Endpoint5_23 = (raw: RawClient["server.session"]) => (input: Endpoint5_23I
const Endpoint5_24 = (raw: RawClient["server.session"]) => (input: Endpoint5_24Input) => const Endpoint5_24 = (raw: RawClient["server.session"]) => (input: Endpoint5_24Input) =>
preserveEffect<Endpoint5_24Output>()( preserveEffect<Endpoint5_24Output>()(
raw["session.pending.cancel"]({ params: { sessionID: input["sessionID"], inputID: input["inputID"] } }).pipe(
Effect.mapError(mapClientError),
),
)
const Endpoint5_25 = (raw: RawClient["server.session"]) => (input: Endpoint5_25Input) =>
preserveEffect<Endpoint5_25Output>()(
raw["session.pending.steer"]({ params: { sessionID: input["sessionID"], inputID: input["inputID"] } }).pipe(
Effect.mapError(mapClientError),
),
)
const Endpoint5_26 = (raw: RawClient["server.session"]) => (input: Endpoint5_26Input) =>
preserveEffect<Endpoint5_26Output>()(
raw["session.pending.queue"]({ params: { sessionID: input["sessionID"], inputID: input["inputID"] } }).pipe(
Effect.mapError(mapClientError),
),
)
const Endpoint5_27 = (raw: RawClient["server.session"]) => (input: Endpoint5_27Input) =>
preserveEffect<Endpoint5_27Output>()(
raw["session.instructions.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe( raw["session.instructions.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
Effect.mapError(mapClientError), Effect.mapError(mapClientError),
Effect.map((value) => value.data), Effect.map((value) => value.data),
), ),
) )
const Endpoint5_28 = (raw: RawClient["server.session"]) => (input: Endpoint5_28Input) => const Endpoint5_25 = (raw: RawClient["server.session"]) => (input: Endpoint5_25Input) =>
preserveEffect<Endpoint5_28Output>()( preserveEffect<Endpoint5_25Output>()(
raw["session.instructions.entry.put"]({ raw["session.instructions.entry.put"]({
params: { sessionID: input["sessionID"], key: input["key"] }, params: { sessionID: input["sessionID"], key: input["key"] },
payload: { value: input["value"] }, payload: { value: input["value"] },
}).pipe(Effect.mapError(mapClientError)), }).pipe(Effect.mapError(mapClientError)),
) )
const Endpoint5_29 = (raw: RawClient["server.session"]) => (input: Endpoint5_29Input) => const Endpoint5_26 = (raw: RawClient["server.session"]) => (input: Endpoint5_26Input) =>
preserveEffect<Endpoint5_29Output>()( preserveEffect<Endpoint5_26Output>()(
raw["session.instructions.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe( raw["session.instructions.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe(
Effect.mapError(mapClientError), Effect.mapError(mapClientError),
), ),
) )
const Endpoint5_30 = (raw: RawClient["server.session"]) => (input: Endpoint5_30Input) => const Endpoint5_27 = (raw: RawClient["server.session"]) => (input: Endpoint5_27Input) =>
preserveEffect<Endpoint5_30Output>()( preserveEffect<Endpoint5_27Output>()(
raw["session.generate"]({ params: { sessionID: input["sessionID"] }, payload: { prompt: input["prompt"] } }).pipe( raw["session.generate"]({ params: { sessionID: input["sessionID"] }, payload: { prompt: input["prompt"] } }).pipe(
Effect.mapError(mapClientError), Effect.mapError(mapClientError),
Effect.map((value) => value.data), Effect.map((value) => value.data),
), ),
) )
const Endpoint5_31 = (raw: RawClient["server.session"]) => (input: Endpoint5_31Input) => const Endpoint5_28 = (raw: RawClient["server.session"]) => (input: Endpoint5_28Input) =>
preserveStream<Endpoint5_31Output>()( preserveStream<Endpoint5_28Output>()(
Stream.unwrap( Stream.unwrap(
raw["session.log"]({ raw["session.log"]({
params: { sessionID: input["sessionID"] }, params: { sessionID: input["sessionID"] },
@@ -592,18 +564,18 @@ const Endpoint5_31 = (raw: RawClient["server.session"]) => (input: Endpoint5_31I
), ),
) )
const Endpoint5_32 = (raw: RawClient["server.session"]) => (input: Endpoint5_32Input) => const Endpoint5_29 = (raw: RawClient["server.session"]) => (input: Endpoint5_29Input) =>
preserveEffect<Endpoint5_32Output>()( preserveEffect<Endpoint5_29Output>()(
raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)), raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)),
) )
const Endpoint5_33 = (raw: RawClient["server.session"]) => (input: Endpoint5_33Input) => const Endpoint5_30 = (raw: RawClient["server.session"]) => (input: Endpoint5_30Input) =>
preserveEffect<Endpoint5_33Output>()( preserveEffect<Endpoint5_30Output>()(
raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)), raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)),
) )
const Endpoint5_34 = (raw: RawClient["server.session"]) => (input: Endpoint5_34Input) => const Endpoint5_31 = (raw: RawClient["server.session"]) => (input: Endpoint5_31Input) =>
preserveEffect<Endpoint5_34Output>()( preserveEffect<Endpoint5_31Output>()(
raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe( raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe(
Effect.mapError(mapClientError), Effect.mapError(mapClientError),
Effect.map((value) => value.data), Effect.map((value) => value.data),
@@ -632,13 +604,13 @@ const adaptGroup5 = (raw: RawClient["server.session"]) => ({
wait: Endpoint5_18(raw), wait: Endpoint5_18(raw),
revert: { stage: Endpoint5_19(raw), clear: Endpoint5_20(raw), commit: Endpoint5_21(raw) }, revert: { stage: Endpoint5_19(raw), clear: Endpoint5_20(raw), commit: Endpoint5_21(raw) },
context: Endpoint5_22(raw), context: Endpoint5_22(raw),
pending: { list: Endpoint5_23(raw), cancel: Endpoint5_24(raw), steer: Endpoint5_25(raw), queue: Endpoint5_26(raw) }, pending: { list: Endpoint5_23(raw) },
instructions: { entry: { list: Endpoint5_27(raw), put: Endpoint5_28(raw), remove: Endpoint5_29(raw) } }, instructions: { entry: { list: Endpoint5_24(raw), put: Endpoint5_25(raw), remove: Endpoint5_26(raw) } },
generate: Endpoint5_30(raw), generate: Endpoint5_27(raw),
log: Endpoint5_31(raw), log: Endpoint5_28(raw),
interrupt: Endpoint5_32(raw), interrupt: Endpoint5_29(raw),
background: Endpoint5_33(raw), background: Endpoint5_30(raw),
message: Endpoint5_34(raw), message: Endpoint5_31(raw),
}) })
const Endpoint6_0 = (raw: RawClient["server.message"]) => (input: Endpoint6_0Input) => const Endpoint6_0 = (raw: RawClient["server.message"]) => (input: Endpoint6_0Input) =>
@@ -58,12 +58,6 @@ import type {
SessionContextOutput, SessionContextOutput,
SessionPendingListInput, SessionPendingListInput,
SessionPendingListOutput, SessionPendingListOutput,
SessionPendingCancelInput,
SessionPendingCancelOutput,
SessionPendingSteerInput,
SessionPendingSteerOutput,
SessionPendingQueueInput,
SessionPendingQueueOutput,
SessionInstructionsEntryListInput, SessionInstructionsEntryListInput,
SessionInstructionsEntryListOutput, SessionInstructionsEntryListOutput,
SessionInstructionsEntryPutInput, SessionInstructionsEntryPutInput,
@@ -454,7 +448,6 @@ export function make(options: ClientOptions) {
method: "GET", method: "GET",
path: `/api/session`, path: `/api/session`,
query: { query: {
workspace: input?.["workspace"],
limit: input?.["limit"], limit: input?.["limit"],
order: input?.["order"], order: input?.["order"],
search: input?.["search"], search: input?.["search"],
@@ -483,7 +476,7 @@ export function make(options: ClientOptions) {
location: input?.["location"], location: input?.["location"],
}, },
successStatus: 200, successStatus: 200,
declaredStatuses: [401, 400], declaredStatuses: [400, 401],
empty: false, empty: false,
}, },
requestOptions, requestOptions,
@@ -772,39 +765,6 @@ export function make(options: ClientOptions) {
}, },
requestOptions, requestOptions,
).then((value) => value.data), ).then((value) => value.data),
cancel: (input: SessionPendingCancelInput, requestOptions?: RequestOptions) =>
request<SessionPendingCancelOutput>(
{
method: "DELETE",
path: `/api/session/${encodeURIComponent(input.sessionID)}/pending/${encodeURIComponent(input.inputID)}`,
successStatus: 204,
declaredStatuses: [409, 404, 401, 400],
empty: true,
},
requestOptions,
),
steer: (input: SessionPendingSteerInput, requestOptions?: RequestOptions) =>
request<SessionPendingSteerOutput>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/pending/${encodeURIComponent(input.inputID)}/steer`,
successStatus: 204,
declaredStatuses: [409, 404, 401, 400],
empty: true,
},
requestOptions,
),
queue: (input: SessionPendingQueueInput, requestOptions?: RequestOptions) =>
request<SessionPendingQueueOutput>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/pending/${encodeURIComponent(input.inputID)}/queue`,
successStatus: 204,
declaredStatuses: [409, 404, 401, 400],
empty: true,
},
requestOptions,
),
}, },
instructions: { instructions: {
entry: { entry: {
+68 -256
View File
@@ -502,36 +502,6 @@ export type SessionInputPromoted = {
data: { sessionID: string; inputID: string } data: { sessionID: string; inputID: string }
} }
export type SessionInputCancelled = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.input.cancelled"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; inputID: string }
}
export type SessionInputSteered = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.input.steered"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; inputID: string }
}
export type SessionInputQueued = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.input.queued"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; inputID: string }
}
export type SessionExecutionStarted = { export type SessionExecutionStarted = {
id: string id: string
created: number created: number
@@ -2000,9 +1970,6 @@ export type SessionEventDurable =
| SessionForked | SessionForked
| SessionInputPromoted | SessionInputPromoted
| SessionInputAdmitted | SessionInputAdmitted
| SessionInputCancelled
| SessionInputSteered
| SessionInputQueued
| SessionExecutionStarted | SessionExecutionStarted
| SessionExecutionSucceeded | SessionExecutionSucceeded
| SessionExecutionFailed | SessionExecutionFailed
@@ -2057,9 +2024,6 @@ export type V2Event =
| SessionForked | SessionForked
| SessionInputPromoted | SessionInputPromoted
| SessionInputAdmitted | SessionInputAdmitted
| SessionInputCancelled
| SessionInputSteered
| SessionInputQueued
| SessionExecutionStarted | SessionExecutionStarted
| SessionExecutionSucceeded | SessionExecutionSucceeded
| SessionExecutionFailed | SessionExecutionFailed
@@ -2319,9 +2283,7 @@ export type HealthStopOutput = ServiceStopResponse
export type ServerGetOutput = { urls: Array<string> } export type ServerGetOutput = { urls: Array<string> }
export type LocationGetInput = { export type LocationGetInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type LocationGetOutput = { export type LocationGetOutput = {
@@ -2331,9 +2293,7 @@ export type LocationGetOutput = {
} }
export type AgentListInput = { export type AgentListInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type AgentListOutput = { export type AgentListOutput = {
@@ -2343,9 +2303,7 @@ export type AgentListOutput = {
export type AgentGetInput = { export type AgentGetInput = {
readonly agentID: { readonly agentID: string }["agentID"] readonly agentID: { readonly agentID: string }["agentID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type AgentGetOutput = { export type AgentGetOutput = {
@@ -2354,9 +2312,7 @@ export type AgentGetOutput = {
} }
export type PluginListInput = { export type PluginListInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type PluginListOutput = { export type PluginListOutput = {
@@ -2365,19 +2321,7 @@ export type PluginListOutput = {
} }
export type SessionListInput = { export type SessionListInput = {
readonly workspace?: {
readonly workspace?: string | undefined
readonly limit?: number | undefined
readonly order?: "asc" | "desc" | undefined
readonly search?: string | undefined
readonly parentID?: string | null | undefined
readonly directory?: string | undefined
readonly project?: string | undefined
readonly subpath?: string | undefined
readonly cursor?: string | undefined
}["workspace"]
readonly limit?: { readonly limit?: {
readonly workspace?: string | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
readonly order?: "asc" | "desc" | undefined readonly order?: "asc" | "desc" | undefined
readonly search?: string | undefined readonly search?: string | undefined
@@ -2388,7 +2332,6 @@ export type SessionListInput = {
readonly cursor?: string | undefined readonly cursor?: string | undefined
}["limit"] }["limit"]
readonly order?: { readonly order?: {
readonly workspace?: string | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
readonly order?: "asc" | "desc" | undefined readonly order?: "asc" | "desc" | undefined
readonly search?: string | undefined readonly search?: string | undefined
@@ -2399,7 +2342,6 @@ export type SessionListInput = {
readonly cursor?: string | undefined readonly cursor?: string | undefined
}["order"] }["order"]
readonly search?: { readonly search?: {
readonly workspace?: string | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
readonly order?: "asc" | "desc" | undefined readonly order?: "asc" | "desc" | undefined
readonly search?: string | undefined readonly search?: string | undefined
@@ -2410,7 +2352,6 @@ export type SessionListInput = {
readonly cursor?: string | undefined readonly cursor?: string | undefined
}["search"] }["search"]
readonly parentID?: { readonly parentID?: {
readonly workspace?: string | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
readonly order?: "asc" | "desc" | undefined readonly order?: "asc" | "desc" | undefined
readonly search?: string | undefined readonly search?: string | undefined
@@ -2421,7 +2362,6 @@ export type SessionListInput = {
readonly cursor?: string | undefined readonly cursor?: string | undefined
}["parentID"] }["parentID"]
readonly directory?: { readonly directory?: {
readonly workspace?: string | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
readonly order?: "asc" | "desc" | undefined readonly order?: "asc" | "desc" | undefined
readonly search?: string | undefined readonly search?: string | undefined
@@ -2432,7 +2372,6 @@ export type SessionListInput = {
readonly cursor?: string | undefined readonly cursor?: string | undefined
}["directory"] }["directory"]
readonly project?: { readonly project?: {
readonly workspace?: string | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
readonly order?: "asc" | "desc" | undefined readonly order?: "asc" | "desc" | undefined
readonly search?: string | undefined readonly search?: string | undefined
@@ -2443,7 +2382,6 @@ export type SessionListInput = {
readonly cursor?: string | undefined readonly cursor?: string | undefined
}["project"] }["project"]
readonly subpath?: { readonly subpath?: {
readonly workspace?: string | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
readonly order?: "asc" | "desc" | undefined readonly order?: "asc" | "desc" | undefined
readonly search?: string | undefined readonly search?: string | undefined
@@ -2454,7 +2392,6 @@ export type SessionListInput = {
readonly cursor?: string | undefined readonly cursor?: string | undefined
}["subpath"] }["subpath"]
readonly cursor?: { readonly cursor?: {
readonly workspace?: string | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
readonly order?: "asc" | "desc" | undefined readonly order?: "asc" | "desc" | undefined
readonly search?: string | undefined readonly search?: string | undefined
@@ -3725,27 +3662,6 @@ export type SessionPendingListInput = { readonly sessionID: { readonly sessionID
export type SessionPendingListOutput = { data: Array<SessionPendingInfo> }["data"] export type SessionPendingListOutput = { data: Array<SessionPendingInfo> }["data"]
export type SessionPendingCancelInput = {
readonly sessionID: { readonly sessionID: string; readonly inputID: string }["sessionID"]
readonly inputID: { readonly sessionID: string; readonly inputID: string }["inputID"]
}
export type SessionPendingCancelOutput = void
export type SessionPendingSteerInput = {
readonly sessionID: { readonly sessionID: string; readonly inputID: string }["sessionID"]
readonly inputID: { readonly sessionID: string; readonly inputID: string }["inputID"]
}
export type SessionPendingSteerOutput = void
export type SessionPendingQueueInput = {
readonly sessionID: { readonly sessionID: string; readonly inputID: string }["sessionID"]
readonly inputID: { readonly sessionID: string; readonly inputID: string }["inputID"]
}
export type SessionPendingQueueOutput = void
export type SessionInstructionsEntryListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } export type SessionInstructionsEntryListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
export type SessionInstructionsEntryListOutput = { data: Array<InstructionEntryInfo> }["data"] export type SessionInstructionsEntryListOutput = { data: Array<InstructionEntryInfo> }["data"]
@@ -3817,9 +3733,7 @@ export type MessageListInput = {
export type MessageListOutput = SessionMessagesResponse export type MessageListOutput = SessionMessagesResponse
export type ModelListInput = { export type ModelListInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type ModelListOutput = { export type ModelListOutput = {
@@ -3828,9 +3742,7 @@ export type ModelListOutput = {
} }
export type ModelDefaultInput = { export type ModelDefaultInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type ModelDefaultOutput = { export type ModelDefaultOutput = {
@@ -3839,9 +3751,7 @@ export type ModelDefaultOutput = {
} }
export type GenerateTextInput = { export type GenerateTextInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly prompt: { readonly prompt: {
readonly prompt: string readonly prompt: string
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
@@ -3855,9 +3765,7 @@ export type GenerateTextInput = {
export type GenerateTextOutput = GenerateTextResponse["data"] export type GenerateTextOutput = GenerateTextResponse["data"]
export type ProviderListInput = { export type ProviderListInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type ProviderListOutput = { export type ProviderListOutput = {
@@ -3867,9 +3775,7 @@ export type ProviderListOutput = {
export type ProviderGetInput = { export type ProviderGetInput = {
readonly providerID: { readonly providerID: string }["providerID"] readonly providerID: { readonly providerID: string }["providerID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type ProviderGetOutput = { export type ProviderGetOutput = {
@@ -3878,9 +3784,7 @@ export type ProviderGetOutput = {
} }
export type IntegrationListInput = { export type IntegrationListInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type IntegrationListOutput = { export type IntegrationListOutput = {
@@ -3890,9 +3794,7 @@ export type IntegrationListOutput = {
export type IntegrationGetInput = { export type IntegrationGetInput = {
readonly integrationID: { readonly integrationID: string }["integrationID"] readonly integrationID: { readonly integrationID: string }["integrationID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type IntegrationGetOutput = { export type IntegrationGetOutput = {
@@ -3901,9 +3803,7 @@ export type IntegrationGetOutput = {
} }
export type IntegrationWellknownAddInput = { export type IntegrationWellknownAddInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly url: { readonly url: string }["url"] readonly url: { readonly url: string }["url"]
} }
@@ -3911,9 +3811,7 @@ export type IntegrationWellknownAddOutput = void
export type IntegrationConnectKeyInput = { export type IntegrationConnectKeyInput = {
readonly integrationID: { readonly integrationID: string }["integrationID"] readonly integrationID: { readonly integrationID: string }["integrationID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly key: { readonly key: string; readonly label?: string | undefined }["key"] readonly key: { readonly key: string; readonly label?: string | undefined }["key"]
readonly label?: { readonly key: string; readonly label?: string | undefined }["label"] readonly label?: { readonly key: string; readonly label?: string | undefined }["label"]
} }
@@ -3922,9 +3820,7 @@ export type IntegrationConnectKeyOutput = void
export type IntegrationOauthConnectInput = { export type IntegrationOauthConnectInput = {
readonly integrationID: { readonly integrationID: string }["integrationID"] readonly integrationID: { readonly integrationID: string }["integrationID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly methodID: { readonly methodID: {
readonly methodID: string readonly methodID: string
readonly inputs: { readonly [x: string]: string } readonly inputs: { readonly [x: string]: string }
@@ -3956,9 +3852,7 @@ export type IntegrationOauthConnectOutput = {
export type IntegrationOauthStatusInput = { export type IntegrationOauthStatusInput = {
readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"] readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"]
readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"] readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type IntegrationOauthStatusOutput = { export type IntegrationOauthStatusOutput = {
@@ -3969,9 +3863,7 @@ export type IntegrationOauthStatusOutput = {
export type IntegrationOauthCompleteInput = { export type IntegrationOauthCompleteInput = {
readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"] readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"]
readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"] readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly code?: { readonly code?: string | undefined }["code"] readonly code?: { readonly code?: string | undefined }["code"]
} }
@@ -3980,18 +3872,14 @@ export type IntegrationOauthCompleteOutput = void
export type IntegrationOauthCancelInput = { export type IntegrationOauthCancelInput = {
readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"] readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"]
readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"] readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type IntegrationOauthCancelOutput = void export type IntegrationOauthCancelOutput = void
export type IntegrationCommandConnectInput = { export type IntegrationCommandConnectInput = {
readonly integrationID: { readonly integrationID: string }["integrationID"] readonly integrationID: { readonly integrationID: string }["integrationID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly methodID: { readonly methodID: string; readonly label?: string | undefined }["methodID"] readonly methodID: { readonly methodID: string; readonly label?: string | undefined }["methodID"]
readonly label?: { readonly methodID: string; readonly label?: string | undefined }["label"] readonly label?: { readonly methodID: string; readonly label?: string | undefined }["label"]
} }
@@ -4004,9 +3892,7 @@ export type IntegrationCommandConnectOutput = {
export type IntegrationCommandStatusInput = { export type IntegrationCommandStatusInput = {
readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"] readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"]
readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"] readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type IntegrationCommandStatusOutput = { export type IntegrationCommandStatusOutput = {
@@ -4017,17 +3903,13 @@ export type IntegrationCommandStatusOutput = {
export type IntegrationCommandCancelInput = { export type IntegrationCommandCancelInput = {
readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"] readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"]
readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"] readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type IntegrationCommandCancelOutput = void export type IntegrationCommandCancelOutput = void
export type McpListInput = { export type McpListInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type McpListOutput = { export type McpListOutput = {
@@ -4037,9 +3919,7 @@ export type McpListOutput = {
export type McpAddInput = { export type McpAddInput = {
readonly server: { readonly server: string }["server"] readonly server: { readonly server: string }["server"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly config: { readonly config: {
readonly config: readonly config:
| { | {
@@ -4075,35 +3955,27 @@ export type McpAddOutput = void
export type McpRemoveInput = { export type McpRemoveInput = {
readonly server: { readonly server: string }["server"] readonly server: { readonly server: string }["server"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type McpRemoveOutput = void export type McpRemoveOutput = void
export type McpConnectInput = { export type McpConnectInput = {
readonly server: { readonly server: string }["server"] readonly server: { readonly server: string }["server"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type McpConnectOutput = void export type McpConnectOutput = void
export type McpDisconnectInput = { export type McpDisconnectInput = {
readonly server: { readonly server: string }["server"] readonly server: { readonly server: string }["server"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type McpDisconnectOutput = void export type McpDisconnectOutput = void
export type McpResourceCatalogInput = { export type McpResourceCatalogInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type McpResourceCatalogOutput = { export type McpResourceCatalogOutput = {
@@ -4113,9 +3985,7 @@ export type McpResourceCatalogOutput = {
export type CredentialUpdateInput = { export type CredentialUpdateInput = {
readonly credentialID: { readonly credentialID: string }["credentialID"] readonly credentialID: { readonly credentialID: string }["credentialID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly label: { readonly label: string }["label"] readonly label: { readonly label: string }["label"]
} }
@@ -4123,9 +3993,7 @@ export type CredentialUpdateOutput = void
export type CredentialRemoveInput = { export type CredentialRemoveInput = {
readonly credentialID: { readonly credentialID: string }["credentialID"] readonly credentialID: { readonly credentialID: string }["credentialID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type CredentialRemoveOutput = void export type CredentialRemoveOutput = void
@@ -4133,26 +4001,20 @@ export type CredentialRemoveOutput = void
export type ProjectListOutput = Array<Project> export type ProjectListOutput = Array<Project>
export type ProjectCurrentInput = { export type ProjectCurrentInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type ProjectCurrentOutput = ProjectCurrent export type ProjectCurrentOutput = ProjectCurrent
export type ProjectDirectoriesInput = { export type ProjectDirectoriesInput = {
readonly projectID: { readonly projectID: string }["projectID"] readonly projectID: { readonly projectID: string }["projectID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type ProjectDirectoriesOutput = ProjectDirectories export type ProjectDirectoriesOutput = ProjectDirectories
export type FormRequestListInput = { export type FormRequestListInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type FormRequestListOutput = { export type FormRequestListOutput = {
@@ -5006,9 +4868,7 @@ export type FormCancelInput = {
export type FormCancelOutput = void export type FormCancelOutput = void
export type PermissionRequestListInput = { export type PermissionRequestListInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type PermissionRequestListOutput = { export type PermissionRequestListOutput = {
@@ -5114,9 +4974,7 @@ export type PermissionReplyInput = {
export type PermissionReplyOutput = void export type PermissionReplyOutput = void
export type FileReadInput = { export type FileReadInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly path: string readonly path: string
} }
@@ -5124,11 +4982,11 @@ export type FileReadOutput = globalThis.Uint8Array
export type FileListInput = { export type FileListInput = {
readonly location?: { readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly path?: string | undefined readonly path?: string | undefined
}["location"] }["location"]
readonly path?: { readonly path?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly path?: string | undefined readonly path?: string | undefined
}["path"] }["path"]
} }
@@ -5140,25 +4998,25 @@ export type FileListOutput = {
export type FileFindInput = { export type FileFindInput = {
readonly location?: { readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly query: string readonly query: string
readonly type?: "file" | "directory" | undefined readonly type?: "file" | "directory" | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
}["location"] }["location"]
readonly query: { readonly query: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly query: string readonly query: string
readonly type?: "file" | "directory" | undefined readonly type?: "file" | "directory" | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
}["query"] }["query"]
readonly type?: { readonly type?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly query: string readonly query: string
readonly type?: "file" | "directory" | undefined readonly type?: "file" | "directory" | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
}["type"] }["type"]
readonly limit?: { readonly limit?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly query: string readonly query: string
readonly type?: "file" | "directory" | undefined readonly type?: "file" | "directory" | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
@@ -5171,9 +5029,7 @@ export type FileFindOutput = {
} }
export type CommandListInput = { export type CommandListInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type CommandListOutput = { export type CommandListOutput = {
@@ -5182,9 +5038,7 @@ export type CommandListOutput = {
} }
export type SkillListInput = { export type SkillListInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type SkillListOutput = { export type SkillListOutput = {
@@ -5195,9 +5049,7 @@ export type SkillListOutput = {
export type EventSubscribeOutput = V2Event export type EventSubscribeOutput = V2Event
export type PtyListInput = { export type PtyListInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type PtyListOutput = { export type PtyListOutput = {
@@ -5206,9 +5058,7 @@ export type PtyListOutput = {
} }
export type PtyCreateInput = { export type PtyCreateInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly command?: { readonly command?: {
readonly command?: string readonly command?: string
readonly args?: ReadonlyArray<string> readonly args?: ReadonlyArray<string>
@@ -5253,9 +5103,7 @@ export type PtyCreateOutput = {
export type PtyGetInput = { export type PtyGetInput = {
readonly ptyID: { readonly ptyID: string }["ptyID"] readonly ptyID: { readonly ptyID: string }["ptyID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type PtyGetOutput = { export type PtyGetOutput = {
@@ -5265,9 +5113,7 @@ export type PtyGetOutput = {
export type PtyUpdateInput = { export type PtyUpdateInput = {
readonly ptyID: { readonly ptyID: string }["ptyID"] readonly ptyID: { readonly ptyID: string }["ptyID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly title?: { readonly title?: {
readonly title?: string readonly title?: string
readonly size?: { readonly rows: number; readonly cols: number } readonly size?: { readonly rows: number; readonly cols: number }
@@ -5282,17 +5128,13 @@ export type PtyUpdateOutput = {
export type PtyRemoveInput = { export type PtyRemoveInput = {
readonly ptyID: { readonly ptyID: string }["ptyID"] readonly ptyID: { readonly ptyID: string }["ptyID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type PtyRemoveOutput = void export type PtyRemoveOutput = void
export type ShellListInput = { export type ShellListInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type ShellListOutput = { export type ShellListOutput = {
@@ -5301,9 +5143,7 @@ export type ShellListOutput = {
} }
export type ShellCreateInput = { export type ShellCreateInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly command: { readonly command: {
readonly command: string readonly command: string
readonly cwd?: string readonly cwd?: string
@@ -5337,9 +5177,7 @@ export type ShellCreateOutput = {
export type ShellGetInput = { export type ShellGetInput = {
readonly id: { readonly id: string }["id"] readonly id: { readonly id: string }["id"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type ShellGetOutput = { export type ShellGetOutput = {
@@ -5349,9 +5187,7 @@ export type ShellGetOutput = {
export type ShellTimeoutInput = { export type ShellTimeoutInput = {
readonly id: { readonly id: string }["id"] readonly id: { readonly id: string }["id"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly timeout: { readonly timeout: number }["timeout"] readonly timeout: { readonly timeout: number }["timeout"]
} }
@@ -5363,17 +5199,17 @@ export type ShellTimeoutOutput = {
export type ShellOutputInput = { export type ShellOutputInput = {
readonly id: { readonly id: string }["id"] readonly id: { readonly id: string }["id"]
readonly location?: { readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly cursor?: number | undefined readonly cursor?: number | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
}["location"] }["location"]
readonly cursor?: { readonly cursor?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly cursor?: number | undefined readonly cursor?: number | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
}["cursor"] }["cursor"]
readonly limit?: { readonly limit?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly cursor?: number | undefined readonly cursor?: number | undefined
readonly limit?: number | undefined readonly limit?: number | undefined
}["limit"] }["limit"]
@@ -5386,17 +5222,13 @@ export type ShellOutputOutput = {
export type ShellRemoveInput = { export type ShellRemoveInput = {
readonly id: { readonly id: string }["id"] readonly id: { readonly id: string }["id"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type ShellRemoveOutput = void export type ShellRemoveOutput = void
export type QuestionRequestListInput = { export type QuestionRequestListInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type QuestionRequestListOutput = { export type QuestionRequestListOutput = {
@@ -5424,9 +5256,7 @@ export type QuestionRejectInput = {
export type QuestionRejectOutput = void export type QuestionRejectOutput = void
export type ReferenceListInput = { export type ReferenceListInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type ReferenceListOutput = { export type ReferenceListOutput = {
@@ -5436,9 +5266,7 @@ export type ReferenceListOutput = {
export type ProjectCopyCreateInput = { export type ProjectCopyCreateInput = {
readonly projectID: { readonly projectID: string }["projectID"] readonly projectID: { readonly projectID: string }["projectID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly strategy: { readonly strategy: string; readonly directory: string; readonly name?: string }["strategy"] readonly strategy: { readonly strategy: string; readonly directory: string; readonly name?: string }["strategy"]
readonly directory: { readonly strategy: string; readonly directory: string; readonly name?: string }["directory"] readonly directory: { readonly strategy: string; readonly directory: string; readonly name?: string }["directory"]
readonly name?: { readonly strategy: string; readonly directory: string; readonly name?: string }["name"] readonly name?: { readonly strategy: string; readonly directory: string; readonly name?: string }["name"]
@@ -5448,9 +5276,7 @@ export type ProjectCopyCreateOutput = ProjectCopyCopy
export type ProjectCopyRemoveInput = { export type ProjectCopyRemoveInput = {
readonly projectID: { readonly projectID: string }["projectID"] readonly projectID: { readonly projectID: string }["projectID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly directory: { readonly directory: string; readonly force: boolean }["directory"] readonly directory: { readonly directory: string; readonly force: boolean }["directory"]
readonly force: { readonly directory: string; readonly force: boolean }["force"] readonly force: { readonly directory: string; readonly force: boolean }["force"]
} }
@@ -5459,17 +5285,13 @@ export type ProjectCopyRemoveOutput = void
export type ProjectCopyRefreshInput = { export type ProjectCopyRefreshInput = {
readonly projectID: { readonly projectID: string }["projectID"] readonly projectID: { readonly projectID: string }["projectID"]
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type ProjectCopyRefreshOutput = void export type ProjectCopyRefreshOutput = void
export type VcsGetInput = { export type VcsGetInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type VcsGetOutput = { export type VcsGetOutput = {
@@ -5478,9 +5300,7 @@ export type VcsGetOutput = {
} }
export type VcsStatusInput = { export type VcsStatusInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type VcsStatusOutput = { export type VcsStatusOutput = {
@@ -5490,17 +5310,17 @@ export type VcsStatusOutput = {
export type VcsDiffInput = { export type VcsDiffInput = {
readonly location?: { readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly mode: "working" | "branch" readonly mode: "working" | "branch"
readonly context?: number | undefined readonly context?: number | undefined
}["location"] }["location"]
readonly mode: { readonly mode: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly mode: "working" | "branch" readonly mode: "working" | "branch"
readonly context?: number | undefined readonly context?: number | undefined
}["mode"] }["mode"]
readonly context?: { readonly context?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined readonly location?: { readonly directory?: string | undefined } | undefined
readonly mode: "working" | "branch" readonly mode: "working" | "branch"
readonly context?: number | undefined readonly context?: number | undefined
}["context"] }["context"]
@@ -5514,9 +5334,7 @@ export type VcsDiffOutput = {
export type DebugLocationListOutput = Array<LocationRef> export type DebugLocationListOutput = Array<LocationRef>
export type DebugLocationEvictInput = { export type DebugLocationEvictInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type DebugLocationEvictOutput = void export type DebugLocationEvictOutput = void
@@ -5527,9 +5345,7 @@ export type MigrationV1StatusOutput =
| { status: "error"; error: string } | { status: "error"; error: string }
export type WebsearchProvidersInput = { export type WebsearchProvidersInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type WebsearchProvidersOutput = { export type WebsearchProvidersOutput = {
@@ -5538,9 +5354,7 @@ export type WebsearchProvidersOutput = {
} }
export type WebsearchQueryInput = { export type WebsearchQueryInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly query: { readonly query: string; readonly providerID?: string }["query"] readonly query: { readonly query: string; readonly providerID?: string }["query"]
readonly providerID?: { readonly query: string; readonly providerID?: string }["providerID"] readonly providerID?: { readonly query: string; readonly providerID?: string }["providerID"]
} }
@@ -5551,9 +5365,7 @@ export type WebsearchQueryOutput = {
} }
export type ConfigGetInput = { export type ConfigGetInput = {
readonly location?: { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
} }
export type ConfigGetOutput = Array<ConfigEntry> export type ConfigGetOutput = Array<ConfigEntry>
@@ -19,7 +19,7 @@ test("effect entrypoint exposes canonical Schema contracts", () => {
test("generated Effect API names canonical and composed outputs", async () => { test("generated Effect API names canonical and composed outputs", async () => {
const source = await Bun.file(new URL("../src/effect/api/api.ts", import.meta.url)).text() const source = await Bun.file(new URL("../src/effect/api/api.ts", import.meta.url)).text()
expect(source).toContain("export type Endpoint5_5Output = Session.Info") expect(source).toContain("export type Endpoint5_3Output = Session.Info")
expect(source).toContain("export type Endpoint19_0Output = OpenCodeEvent") expect(source).toContain("export type Endpoint19_0Output = OpenCodeEvent")
expect(source).not.toContain("HttpApiClient.ForApi") expect(source).not.toContain("HttpApiClient.ForApi")
}) })
-23
View File
@@ -32,7 +32,6 @@ test("exposes every standard HTTP API group", () => {
"projectCopy", "projectCopy",
"vcs", "vcs",
"debug", "debug",
"migration",
"websearch", "websearch",
"config", "config",
]) ])
@@ -357,28 +356,6 @@ test("session.pending.list uses the public HTTP contract", async () => {
expect(requests).toEqual([{ method: "GET", url: "http://localhost:3000/api/session/ses_test/pending" }]) expect(requests).toEqual([{ method: "GET", url: "http://localhost:3000/api/session/ses_test/pending" }])
}) })
test("session.pending mutations use the public HTTP contract", async () => {
const requests: Array<{ method: string; url: string }> = []
const client = OpenCode.make({
baseUrl: "http://localhost:3000",
fetch: async (input, init) => {
const request = input instanceof Request ? input : new Request(input, init)
requests.push({ method: request.method, url: request.url })
return new Response(null, { status: 204 })
},
})
await client.session.pending.cancel({ sessionID: "ses_test", inputID: "msg_cancel" })
await client.session.pending.steer({ sessionID: "ses_test", inputID: "msg_steer" })
await client.session.pending.queue({ sessionID: "ses_test", inputID: "msg_queue" })
expect(requests).toEqual([
{ method: "DELETE", url: "http://localhost:3000/api/session/ses_test/pending/msg_cancel" },
{ method: "POST", url: "http://localhost:3000/api/session/ses_test/pending/msg_steer/steer" },
{ method: "POST", url: "http://localhost:3000/api/session/ses_test/pending/msg_queue/queue" },
])
})
test("event.subscribe exposes the Promise event stream wire projection", async () => { test("event.subscribe exposes the Promise event stream wire projection", async () => {
const client = OpenCode.make({ const client = OpenCode.make({
baseUrl: "http://localhost:3000", baseUrl: "http://localhost:3000",
+1 -2
View File
@@ -1021,13 +1021,12 @@ describe("OpenAPI.fromSpec", () => {
await Effect.runPromise( await Effect.runPromise(
location location
.execute({ location: { directory: "/tmp", workspace: "workspace-1" } }) .execute({ location: { directory: "/tmp" } })
.pipe(Effect.provide(client.layer)), .pipe(Effect.provide(client.layer)),
) )
const url = new URL(client.requests[0]!.url) const url = new URL(client.requests[0]!.url)
expect(url.searchParams.get("location[directory]")).toBe("/tmp") expect(url.searchParams.get("location[directory]")).toBe("/tmp")
expect(url.searchParams.get("location[workspace]")).toBe("workspace-1")
}) })
test("serializes supported simple and form parameter shapes", async () => { test("serializes supported simple and form parameter shapes", async () => {
+1 -3
View File
@@ -11,13 +11,12 @@
"fix-node-pty": "bun run script/fix-node-pty.ts", "fix-node-pty": "bun run script/fix-node-pty.ts",
"benchmark:location": "bun run script/benchmark-location.ts", "benchmark:location": "bun run script/benchmark-location.ts",
"test": "bun test --only-failures", "test": "bun test --only-failures",
"typecheck": "tsgo -b tsconfig.json tsconfig.tests.json" "typecheck": "tsgo --noEmit"
}, },
"bin": { "bin": {
"opencode": "./bin/opencode" "opencode": "./bin/opencode"
}, },
"exports": { "exports": {
"./environment": "./src/environment/index.ts",
"./session/runner": "./src/session/runner/index.ts", "./session/runner": "./src/session/runner/index.ts",
"./instructions": "./src/instructions/index.ts", "./instructions": "./src/instructions/index.ts",
"./*": "./src/*.ts" "./*": "./src/*.ts"
@@ -118,7 +117,6 @@
"immer": "11.1.4", "immer": "11.1.4",
"ignore": "7.0.5", "ignore": "7.0.5",
"jsonc-parser": "3.3.1", "jsonc-parser": "3.3.1",
"mime-types": "3.0.2",
"turndown": "7.2.0", "turndown": "7.2.0",
"tree-sitter-bash": "0.25.0", "tree-sitter-bash": "0.25.0",
"tree-sitter-powershell": "0.25.10", "tree-sitter-powershell": "0.25.10",
+70 -105
View File
@@ -1,15 +1,11 @@
{ {
"version": "7", "version": "7",
"dialect": "sqlite", "dialect": "sqlite",
"id": "2d214a71-3b0a-48c1-a667-741952c4e188", "id": "2415fa65-3def-4f9f-be85-eb35f4f94ddd",
"prevIds": [ "prevIds": [
"f14a9b18-8207-487e-a3d3-227e629ba9ad" "4d283c23-533f-4b87-9aa4-218f7fd36bca"
], ],
"ddl": [ "ddl": [
{
"name": "workspace",
"entityType": "tables"
},
{ {
"name": "account_state", "name": "account_state",
"entityType": "tables" "entityType": "tables"
@@ -75,84 +71,8 @@
"entityType": "tables" "entityType": "tables"
}, },
{ {
"type": "text", "name": "workspace",
"notNull": false, "entityType": "tables"
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "workspace"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "type",
"entityType": "columns",
"table": "workspace"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": "''",
"generated": null,
"name": "name",
"entityType": "columns",
"table": "workspace"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "branch",
"entityType": "columns",
"table": "workspace"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "directory",
"entityType": "columns",
"table": "workspace"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "extra",
"entityType": "columns",
"table": "workspace"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "project_id",
"entityType": "columns",
"table": "workspace"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_used",
"entityType": "columns",
"table": "workspace"
}, },
{ {
"type": "integer", "type": "integer",
@@ -1385,18 +1305,63 @@
"table": "session_v2" "table": "session_v2"
}, },
{ {
"columns": [ "type": "text",
"project_id" "notNull": false,
], "autoincrement": false,
"tableTo": "project", "default": null,
"columnsTo": [ "generated": null,
"id" "name": "id",
], "entityType": "columns",
"onUpdate": "NO ACTION", "table": "workspace"
"onDelete": "CASCADE", },
"nameExplicit": false, {
"name": "fk_workspace_project_id_project_id_fk", "type": "text",
"entityType": "fks", "notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "provider",
"entityType": "columns",
"table": "workspace"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "binding",
"entityType": "columns",
"table": "workspace"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "root",
"entityType": "columns",
"table": "workspace"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "workspace"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "workspace" "table": "workspace"
}, },
{ {
@@ -1564,15 +1529,6 @@
"entityType": "pks", "entityType": "pks",
"table": "instruction_entry" "table": "instruction_entry"
}, },
{
"columns": [
"id"
],
"nameExplicit": false,
"name": "workspace_pk",
"table": "workspace",
"entityType": "pks"
},
{ {
"columns": [ "columns": [
"id" "id"
@@ -1690,6 +1646,15 @@
"table": "session_v2", "table": "session_v2",
"entityType": "pks" "entityType": "pks"
}, },
{
"columns": [
"id"
],
"nameExplicit": false,
"name": "workspace_pk",
"table": "workspace",
"entityType": "pks"
},
{ {
"columns": [ "columns": [
{ {
+6 -10
View File
@@ -132,16 +132,14 @@ function renderMigration(name: string, sql: string) {
return `import { Effect } from "effect" return `import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
const migration: DatabaseMigration.Migration = { export default {
id: ${JSON.stringify(name)}, id: ${JSON.stringify(name)},
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
${renderStatements(sql)} ${renderStatements(sql)}
}) })
}, },
} } satisfies DatabaseMigration.Migration
export default migration
` `
} }
@@ -149,15 +147,13 @@ function renderSchema(sql: string) {
return `import { Effect } from "effect" return `import { Effect } from "effect"
import type { DatabaseMigration } from "./migration" import type { DatabaseMigration } from "./migration"
const schema: Omit<DatabaseMigration.Migration, "id"> = { export default {
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
${renderStatements(sql)} ${renderStatements(sql)}
}) })
}, },
} } satisfies Omit<DatabaseMigration.Migration, "id">
export default schema
` `
} }
@@ -195,10 +191,10 @@ async function formatTypescript(input: string) {
function renderRegistry(names: string[]) { function renderRegistry(names: string[]) {
return `import type { DatabaseMigration } from "./migration" return `import type { DatabaseMigration } from "./migration"
export const migrations: DatabaseMigration.Migration[] = ( export const migrations = (
await Promise.all([ await Promise.all([
${names.map((name) => ` import("./migration/${name}"),`).join("\n")} ${names.map((name) => ` import("./migration/${name}"),`).join("\n")}
]) ])
).map((module) => module.default) ).map((module) => module.default) satisfies DatabaseMigration.Migration[]
` `
} }
+1 -1
View File
@@ -263,7 +263,6 @@ function mapOpenRouterOptions(settings: Readonly<Record<string, unknown>>) {
"extraBody", "extraBody",
"fetch", "fetch",
"headers", "headers",
"promptCacheKey",
"timeout", "timeout",
].includes(key), ].includes(key),
), ),
@@ -280,6 +279,7 @@ function mapXAIOptions(settings: Readonly<Record<string, unknown>>) {
const options = { const options = {
...(typeof settings.reasoningEffort === "string" ? { reasoningEffort: settings.reasoningEffort } : {}), ...(typeof settings.reasoningEffort === "string" ? { reasoningEffort: settings.reasoningEffort } : {}),
...(typeof settings.store === "boolean" ? { store: settings.store } : {}), ...(typeof settings.store === "boolean" ? { store: settings.store } : {}),
...(typeof settings.promptCacheKey === "string" ? { promptCacheKey: settings.promptCacheKey } : {}),
} }
if (Object.keys(options).length === 0) return {} if (Object.keys(options).length === 0) return {}
return { providerOptions: { xai: options } } return { providerOptions: { xai: options } }
+1 -1
View File
@@ -126,7 +126,7 @@ ${render(current)}`
const key = Instructions.Key.make("core/codemode") const key = Instructions.Key.make("core/codemode")
const codec = Schema.toCodecJson(CodeModeCatalog.Summary) const codec = Schema.toCodecJson(CodeModeCatalog.Summary)
export const make = (entries?: ReadonlyArray<CodeModeCatalog.Entry>): Instructions.List => { export const make = (entries?: ReadonlyArray<CodeModeCatalog.Entry>): Instructions.Instructions => {
const catalog = entries === undefined ? Instructions.removed : CodeModeCatalog.summarize(entries) const catalog = entries === undefined ? Instructions.removed : CodeModeCatalog.summarize(entries)
return Instructions.make({ return Instructions.make({
key, key,
+1 -18
View File
@@ -41,7 +41,7 @@ export type Result =
| { readonly type: "rejected"; readonly diagnostics: readonly Diagnostic[] } | { readonly type: "rejected"; readonly diagnostics: readonly Diagnostic[] }
const options = { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" } as const const options = { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" } as const
const unsupportedTopLevel = ["logLevel", "server", "subagent_depth", "layout"] as const const unsupportedTopLevel = ["logLevel", "server", "small_model", "subagent_depth", "layout"] as const
const unsupportedExperimental = [ const unsupportedExperimental = [
"disable_paste_summary", "disable_paste_summary",
"batch_tool", "batch_tool",
@@ -113,23 +113,6 @@ export function normalize(input: unknown): Result {
const legacyAgents = mapValues(decodeMap(input.agent, ConfigAgentV1.Info, ["agent"], diagnostics), (value) => const legacyAgents = mapValues(decodeMap(input.agent, ConfigAgentV1.Info, ["agent"], diagnostics), (value) =>
canonical(ConfigAgent.Info, ConfigMigrateV1.migrateAgent(value)), canonical(ConfigAgent.Info, ConfigMigrateV1.migrateAgent(value)),
) )
const legacySmallModel = own(input, "small_model")
? decodeValue(Schema.String, input.small_model, ["small_model"], diagnostics)
: undefined
const migratedSmallModel = legacySmallModel
? ConfigMigrateV1.migrate({ small_model: legacySmallModel }).agents?.title?.model
: undefined
if (legacySmallModel && !migratedSmallModel)
diagnostics.push({
kind: "unsupported",
path: ["small_model"],
message: "omitted unsupported legacy model reference",
})
if (migratedSmallModel)
legacyAgents.title = {
model: migratedSmallModel,
...legacyAgents.title,
}
const modeAgents = mapValues(decodeMap(input.mode, ConfigAgentV1.Info, ["mode"], diagnostics), (value) => const modeAgents = mapValues(decodeMap(input.mode, ConfigAgentV1.Info, ["mode"], diagnostics), (value) =>
canonical(ConfigAgent.Info, ConfigMigrateV1.migrateAgent({ ...value, mode: "primary" })), canonical(ConfigAgent.Info, ConfigMigrateV1.migrateAgent({ ...value, mode: "primary" })),
) )
+3 -6
View File
@@ -161,14 +161,11 @@ function isPathAction(action: string): action is PathAction {
} }
function expandHome(resource: string, home: string) { function expandHome(resource: string, home: string) {
if (resource.startsWith("~/")) return home + resource.slice(1)
if (resource === "~") return home if (resource === "~") return home
if (resource === "$HOME") return home if (resource === "$HOME") return home
const relative = resource.startsWith("~/") if (resource.startsWith("$HOME/")) return home + resource.slice(5)
? resource.slice(2) if (resource.startsWith("$HOME\\")) return home + resource.slice(5)
: resource.startsWith("$HOME/") || resource.startsWith("$HOME\\")
? resource.slice(6)
: undefined
if (relative !== undefined) return (path.posix.isAbsolute(home) ? path.posix : path.win32).join(home, relative)
return resource return resource
} }
+4 -6
View File
@@ -13,14 +13,12 @@ export const Plugin = define({
const config = yield* Config.Service const config = yield* Config.Service
const loaded = { entries: yield* config.entries() } const loaded = { entries: yield* config.entries() }
yield* ctx.integration.transform((integrations) => { yield* ctx.integration.transform((integrations) => {
const configuredIntegrations = new Set(
configuredProviders(loaded.entries).flatMap(([id, provider]) => (provider.env === undefined ? [] : [id])),
)
for (const [id, provider] of configuredProviders(loaded.entries)) { for (const [id, provider] of configuredProviders(loaded.entries)) {
const integrationID = id const integrationID = id
if (!integrations.get(integrationID)) { if (!configuredIntegrations.has(id) && !integrations.get(integrationID)) continue
integrations.method.update({
integrationID,
method: { type: "key", label: "Manually enter API Key" },
})
}
integrations.update(integrationID, (integration) => { integrations.update(integrationID, (integration) => {
integration.name = provider.name ?? integration.name integration.name = provider.name ?? integration.name
}) })
@@ -1,20 +0,0 @@
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"
import { ProjectTable } from "../project/sql"
import { Project } from "../project"
import { Workspace } from "../workspace"
export const WorkspaceTable = sqliteTable("workspace", {
id: text().$type<Workspace.ID>().primaryKey(),
type: text().notNull(),
name: text().notNull().default(""),
branch: text(),
directory: text(),
extra: text({ mode: "json" }),
project_id: text()
.$type<Project.ID>()
.notNull()
.references(() => ProjectTable.id, { onDelete: "cascade" }),
time_used: integer()
.notNull()
.$default(() => Date.now()),
})
+4 -2
View File
@@ -1,6 +1,6 @@
import type { DatabaseMigration } from "./migration" import type { DatabaseMigration } from "./migration"
export const migrations: DatabaseMigration.Migration[] = ( export const migrations = (
await Promise.all([ await Promise.all([
import("./migration/20260127222353_familiar_lady_ursula"), import("./migration/20260127222353_familiar_lady_ursula"),
import("./migration/20260211171708_add_project_commands"), import("./migration/20260211171708_add_project_commands"),
@@ -42,5 +42,7 @@ export const migrations: DatabaseMigration.Migration[] = (
import("./migration/20260622202450_simplify_session_input"), import("./migration/20260622202450_simplify_session_input"),
import("./migration/20260804233008_loose_psylocke"), import("./migration/20260804233008_loose_psylocke"),
import("./migration/20260805200742_import_legacy_credentials"), import("./migration/20260805200742_import_legacy_credentials"),
import("./migration/20260806150823_remove_workspace"),
import("./migration/20260806150824_add_workspace"),
]) ])
).map((module) => module.default) ).map((module) => module.default) satisfies DatabaseMigration.Migration[]
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
const migration: DatabaseMigration.Migration = { export default {
id: "20260127222353_familiar_lady_ursula", id: "20260127222353_familiar_lady_ursula",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -104,6 +104,4 @@ const migration: DatabaseMigration.Migration = {
yield* tx.run(`CREATE INDEX \`todo_session_idx\` ON \`todo\` (\`session_id\`);`) yield* tx.run(`CREATE INDEX \`todo_session_idx\` ON \`todo\` (\`session_id\`);`)
}) })
}, },
} } satisfies DatabaseMigration.Migration
export default migration
@@ -1,13 +1,11 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
const migration: DatabaseMigration.Migration = { export default {
id: "20260211171708_add_project_commands", id: "20260211171708_add_project_commands",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
yield* tx.run(`ALTER TABLE \`project\` ADD \`commands\` text;`) yield* tx.run(`ALTER TABLE \`project\` ADD \`commands\` text;`)
}) })
}, },
} } satisfies DatabaseMigration.Migration
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
const migration: DatabaseMigration.Migration = { export default {
id: "20260213144116_wakeful_the_professor", id: "20260213144116_wakeful_the_professor",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -20,6 +20,4 @@ const migration: DatabaseMigration.Migration = {
`) `)
}) })
}, },
} } satisfies DatabaseMigration.Migration
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
const migration: DatabaseMigration.Migration = { export default {
id: "20260225215848_workspace", id: "20260225215848_workspace",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -16,6 +16,4 @@ const migration: DatabaseMigration.Migration = {
`) `)
}) })
}, },
} } satisfies DatabaseMigration.Migration
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
const migration: DatabaseMigration.Migration = { export default {
id: "20260227213759_add_session_workspace_id", id: "20260227213759_add_session_workspace_id",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -9,6 +9,4 @@ const migration: DatabaseMigration.Migration = {
yield* tx.run(`CREATE INDEX \`session_workspace_idx\` ON \`session\` (\`workspace_id\`);`) yield* tx.run(`CREATE INDEX \`session_workspace_idx\` ON \`session\` (\`workspace_id\`);`)
}) })
}, },
} } satisfies DatabaseMigration.Migration
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
const migration: DatabaseMigration.Migration = { export default {
id: "20260228203230_blue_harpoon", id: "20260228203230_blue_harpoon",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -27,6 +27,4 @@ const migration: DatabaseMigration.Migration = {
`) `)
}) })
}, },
} } satisfies DatabaseMigration.Migration
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
const migration: DatabaseMigration.Migration = { export default {
id: "20260303231226_add_workspace_fields", id: "20260303231226_add_workspace_fields",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -12,6 +12,4 @@ const migration: DatabaseMigration.Migration = {
yield* tx.run(`ALTER TABLE \`workspace\` DROP COLUMN \`config\`;`) yield* tx.run(`ALTER TABLE \`workspace\` DROP COLUMN \`config\`;`)
}) })
}, },
} } satisfies DatabaseMigration.Migration
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
const migration: DatabaseMigration.Migration = { export default {
id: "20260309230000_move_org_to_state", id: "20260309230000_move_org_to_state",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -12,6 +12,4 @@ const migration: DatabaseMigration.Migration = {
yield* tx.run(`ALTER TABLE \`account\` DROP COLUMN \`selected_org_id\`;`) yield* tx.run(`ALTER TABLE \`account\` DROP COLUMN \`selected_org_id\`;`)
}) })
}, },
} } satisfies DatabaseMigration.Migration
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
const migration: DatabaseMigration.Migration = { export default {
id: "20260312043431_session_message_cursor", id: "20260312043431_session_message_cursor",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -13,6 +13,4 @@ const migration: DatabaseMigration.Migration = {
yield* tx.run(`CREATE INDEX \`part_message_id_id_idx\` ON \`part\` (\`message_id\`,\`id\`);`) yield* tx.run(`CREATE INDEX \`part_message_id_id_idx\` ON \`part\` (\`message_id\`,\`id\`);`)
}) })
}, },
} } satisfies DatabaseMigration.Migration
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
const migration: DatabaseMigration.Migration = { export default {
id: "20260323234822_events", id: "20260323234822_events",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -23,6 +23,4 @@ const migration: DatabaseMigration.Migration = {
`) `)
}) })
}, },
} } satisfies DatabaseMigration.Migration
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
const migration: DatabaseMigration.Migration = { export default {
id: "20260410174513_workspace-name", id: "20260410174513_workspace-name",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -26,6 +26,4 @@ const migration: DatabaseMigration.Migration = {
yield* tx.run(`PRAGMA foreign_keys=ON;`) yield* tx.run(`PRAGMA foreign_keys=ON;`)
}) })
}, },
} } satisfies DatabaseMigration.Migration
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
const migration: DatabaseMigration.Migration = { export default {
id: "20260413175956_chief_energizer", id: "20260413175956_chief_energizer",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -21,6 +21,4 @@ const migration: DatabaseMigration.Migration = {
yield* tx.run(`CREATE INDEX \`session_entry_time_created_idx\` ON \`session_entry\` (\`time_created\`);`) yield* tx.run(`CREATE INDEX \`session_entry_time_created_idx\` ON \`session_entry\` (\`time_created\`);`)
}) })
}, },
} } satisfies DatabaseMigration.Migration
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
const migration: DatabaseMigration.Migration = { export default {
id: "20260423070820_add_icon_url_override", id: "20260423070820_add_icon_url_override",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -11,6 +11,4 @@ const migration: DatabaseMigration.Migration = {
`) `)
}) })
}, },
} } satisfies DatabaseMigration.Migration
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
const migration: DatabaseMigration.Migration = { export default {
id: "20260427172553_slow_nightmare", id: "20260427172553_slow_nightmare",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -27,6 +27,4 @@ const migration: DatabaseMigration.Migration = {
yield* tx.run(`DROP TABLE \`session_entry\`;`) yield* tx.run(`DROP TABLE \`session_entry\`;`)
}) })
}, },
} } satisfies DatabaseMigration.Migration
export default migration
@@ -1,13 +1,11 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
const migration: DatabaseMigration.Migration = { export default {
id: "20260428004200_add_session_path", id: "20260428004200_add_session_path",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
yield* tx.run(`ALTER TABLE \`session\` ADD \`path\` text;`) yield* tx.run(`ALTER TABLE \`session\` ADD \`path\` text;`)
}) })
}, },
} } satisfies DatabaseMigration.Migration
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
const migration: DatabaseMigration.Migration = { export default {
id: "20260501142318_next_venus", id: "20260501142318_next_venus",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -9,6 +9,4 @@ const migration: DatabaseMigration.Migration = {
yield* tx.run(`ALTER TABLE \`session\` ADD \`model\` text;`) yield* tx.run(`ALTER TABLE \`session\` ADD \`model\` text;`)
}) })
}, },
} } satisfies DatabaseMigration.Migration
export default migration
@@ -1,13 +1,11 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
const migration: DatabaseMigration.Migration = { export default {
id: "20260504145000_add_sync_owner", id: "20260504145000_add_sync_owner",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
yield* tx.run(`ALTER TABLE \`event_sequence\` ADD \`owner_id\` text;`) yield* tx.run(`ALTER TABLE \`event_sequence\` ADD \`owner_id\` text;`)
}) })
}, },
} } satisfies DatabaseMigration.Migration
export default migration
@@ -1,13 +1,11 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
const migration: DatabaseMigration.Migration = { export default {
id: "20260507164347_add_workspace_time", id: "20260507164347_add_workspace_time",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
yield* tx.run(`ALTER TABLE \`workspace\` ADD \`time_used\` integer NOT NULL DEFAULT 0;`) yield* tx.run(`ALTER TABLE \`workspace\` ADD \`time_used\` integer NOT NULL DEFAULT 0;`)
}) })
}, },
} } satisfies DatabaseMigration.Migration
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
const migration: DatabaseMigration.Migration = { export default {
id: "20260510033149_session_usage", id: "20260510033149_session_usage",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -53,6 +53,4 @@ const migration: DatabaseMigration.Migration = {
`) `)
}) })
}, },
} } satisfies DatabaseMigration.Migration
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
const migration: DatabaseMigration.Migration = { export default {
id: "20260511000411_data_migration_state", id: "20260511000411_data_migration_state",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -13,6 +13,4 @@ const migration: DatabaseMigration.Migration = {
`) `)
}) })
}, },
} } satisfies DatabaseMigration.Migration
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
const migration: DatabaseMigration.Migration = { export default {
id: "20260511173437_session-metadata", id: "20260511173437_session-metadata",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -13,6 +13,4 @@ const migration: DatabaseMigration.Migration = {
yield* tx.run(`ALTER TABLE \`session\` ADD \`metadata\` text;`) yield* tx.run(`ALTER TABLE \`session\` ADD \`metadata\` text;`)
}) })
}, },
} } satisfies DatabaseMigration.Migration
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
const migration: DatabaseMigration.Migration = { export default {
id: "20260601010001_normalize_storage_paths", id: "20260601010001_normalize_storage_paths",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -19,6 +19,4 @@ const migration: DatabaseMigration.Migration = {
) )
}) })
}, },
} } satisfies DatabaseMigration.Migration
export default migration
@@ -1,13 +1,11 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
const migration: DatabaseMigration.Migration = { export default {
id: "20260601202201_amazing_prowler", id: "20260601202201_amazing_prowler",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
yield* tx.run(`DROP TABLE \`permission\`;`) yield* tx.run(`DROP TABLE \`permission\`;`)
}) })
}, },
} } satisfies DatabaseMigration.Migration
export default migration
@@ -1,7 +1,7 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { DatabaseMigration } from "../migration" import type { DatabaseMigration } from "../migration"
const migration: DatabaseMigration.Migration = { export default {
id: "20260602002951_lowly_union_jack", id: "20260602002951_lowly_union_jack",
up(tx) { up(tx) {
return Effect.gen(function* () { return Effect.gen(function* () {
@@ -21,6 +21,4 @@ const migration: DatabaseMigration.Migration = {
) )
}) })
}, },
} } satisfies DatabaseMigration.Migration
export default migration

Some files were not shown because too many files have changed in this diff Show More