Compare commits

..
Author SHA1 Message Date
Hona b530418425 fix(session-ui): share timeline tool headers 2026-08-30 01:48:49 +00:00
27 changed files with 509 additions and 1276 deletions
+33 -56
View File
@@ -430,7 +430,6 @@
"@types/bun": "catalog:",
"@types/node": "catalog:",
"@typescript/native-preview": "catalog:",
"app-builder-lib": "26.15.7",
"drizzle-orm": "catalog:",
"effect": "catalog:",
"electron": "42.10.1",
@@ -1679,7 +1678,7 @@
"@electron/fuses": ["@electron/fuses@1.8.0", "", { "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", "minimist": "^1.2.5" }, "bin": { "electron-fuses": "dist/bin.js" } }, "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw=="],
"@electron/get": ["@electron/get@3.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ=="],
"@electron/get": ["@electron/get@5.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^3.0.0", "graceful-fs": "^4.2.11", "progress": "^2.0.3", "semver": "^7.6.3", "sumchecker": "^3.0.1" }, "optionalDependencies": { "undici": "^7.24.4" } }, "sha512-3kSBtG8ObcTVfXanm5vVJ6UnBLEVmVsRk1M+vGqCuMBV+XLCbJYuWQful+yIy0GQDsSlK0kHEriEHn7SPk4EnA=="],
"@electron/notarize": ["@electron/notarize@2.5.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.1", "promise-retry": "^2.0.1" } }, "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A=="],
@@ -2775,7 +2774,7 @@
"@silvia-odwyer/photon-node": ["@silvia-odwyer/photon-node@0.3.4", "", {}, "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA=="],
"@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="],
"@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="],
"@smithy/config-resolver": ["@smithy/config-resolver@4.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "tslib": "^2.6.2" } }, "sha512-Y1XfSefHIOub9762qm3ShafdlEE/Va8h3kLUeMq765fNeWeNLcOP2YUPr86H1SlyGwZTOqQ67RlBZPZ3k9Djgg=="],
@@ -3533,7 +3532,7 @@
"chromium-pickle-js": ["chromium-pickle-js@0.2.0", "", {}, "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw=="],
"ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="],
"ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
"classnames": ["classnames@2.3.2", "", {}, "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="],
@@ -3813,7 +3812,7 @@
"dot-prop": ["dot-prop@8.0.2", "", { "dependencies": { "type-fest": "^3.8.0" } }, "sha512-xaBe6ZT4DHPkg0k4Ytbvn5xoxgpG0jOS1dYxSOwAHPuNLjP3/OzN0gH55SrLqpx8cBfSaVt91lXYkApjb+nYdQ=="],
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
"dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="],
@@ -3885,7 +3884,7 @@
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
"env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="],
"err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="],
@@ -4207,7 +4206,7 @@
"hono-openapi": ["hono-openapi@1.1.2", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-toUcO60MftRBxqcVyxsHNYs2m4vf4xkQaiARAucQx3TiBPDtMNNkoh+C4I1vAretQZiGyaLOZNWn1YxfSyUA5g=="],
"hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="],
"hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="],
"html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="],
@@ -5813,7 +5812,7 @@
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
"yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
@@ -5961,8 +5960,6 @@
"@astrojs/starlight/js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="],
"@astrojs/telemetry/ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
"@astrojs/telemetry/is-docker": ["is-docker@4.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA=="],
"@aws-crypto/crc32/@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="],
@@ -6117,9 +6114,7 @@
"@electron/fuses/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
"@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
"@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@electron/get/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="],
"@electron/notarize/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
@@ -6155,12 +6150,6 @@
"@modelcontextprotocol/sdk/jose": ["jose@6.2.9", "", {}, "sha512-XrchZOFZUl/T3vTwRe8XK+cJrGtMF4th1ARnDfwbBXFKThGhlsxEE4Zu03AD/bjJSt/9jT/mxrOCkJWOg77aPA=="],
"@npmcli/arborist/hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="],
"@npmcli/config/ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
"@npmcli/package-json/hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="],
"@npmcli/query/postcss-selector-parser": ["postcss-selector-parser@7.1.5", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw=="],
"@octokit/auth-app/@octokit/request": ["@octokit/request@10.0.14", "", { "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.1.1", "@octokit/types": "^17.0.0", "content-type": "^2.0.0", "json-with-bigint": "^3.5.12", "universal-user-agent": "^7.0.2" } }, "sha512-bgWgiSfFS689/AxQDarU+b3Qu1FYewfVr5vI/jhV84s7GQ3C2OsdRxukGGYKB37MCgwcS50UrfBLlHzhiG13sw=="],
@@ -6301,16 +6290,12 @@
"@pierre/trees/react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="],
"@poppinss/dumper/@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="],
"@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
"@protobuf-ts/plugin/typescript": ["typescript@3.9.10", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q=="],
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"@sentry/bundler-plugins/dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
"@sentry/bundler-plugins/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
"@sentry/cli/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
@@ -6383,6 +6368,14 @@
"anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
"app-builder-lib/@electron/get": ["@electron/get@3.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ=="],
"app-builder-lib/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="],
"app-builder-lib/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"app-builder-lib/hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="],
"app-builder-lib/js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="],
"app-builder-lib/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
@@ -6393,8 +6386,6 @@
"astro/@clack/prompts": ["@clack/prompts@1.7.0", "", { "dependencies": { "@clack/core": "1.4.3", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A=="],
"astro/ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
"astro/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
"astro/es-module-lexer": ["es-module-lexer@2.3.2", "", {}, "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw=="],
@@ -6439,8 +6430,6 @@
"conf/dot-prop": ["dot-prop@10.2.0", "", { "dependencies": { "type-fest": "^5.0.0" } }, "sha512-BTJ9aZYL3vCfZlZOBLy9v8TUqWGQ0pzFnygKwFZt5udj6viBoFIBviKPUoZLDCPn1FoXffv6McQFDenrm5Krfw=="],
"conf/env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="],
"config-chain/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
@@ -6467,16 +6456,14 @@
"dot-prop/type-fest": ["type-fest@3.13.1", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="],
"dotenv-expand/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"duplexer2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"editorconfig/commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="],
"editorconfig/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
"electron/@electron/get": ["@electron/get@5.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^3.0.0", "graceful-fs": "^4.2.11", "progress": "^2.0.3", "semver": "^7.6.3", "sumchecker": "^3.0.1" }, "optionalDependencies": { "undici": "^7.24.4" } }, "sha512-3kSBtG8ObcTVfXanm5vVJ6UnBLEVmVsRk1M+vGqCuMBV+XLCbJYuWQful+yIy0GQDsSlK0kHEriEHn7SPk4EnA=="],
"electron-builder/ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
"electron-publish/mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="],
"electron-store/type-fest": ["type-fest@5.8.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA=="],
@@ -6523,7 +6510,7 @@
"globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
"got/@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="],
"html-minifier-terser/commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="],
@@ -6569,9 +6556,9 @@
"nitro/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="],
"node-gyp/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="],
"node-gyp/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
"npm-package-arg/hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="],
"node-gyp/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="],
"openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="],
@@ -6655,6 +6642,8 @@
"svgo/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
"tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
"temp/rimraf": ["rimraf@2.6.3", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA=="],
"tempy/type-fest": ["type-fest@0.16.0", "", {}, "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg=="],
@@ -6797,8 +6786,6 @@
"@astrojs/starlight/astro/acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
"@astrojs/starlight/astro/ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
"@astrojs/starlight/astro/common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="],
"@astrojs/starlight/astro/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
@@ -6921,8 +6908,6 @@
"@electron/fuses/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
"@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
"@electron/notarize/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
"@electron/universal/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
@@ -7145,8 +7130,6 @@
"@opencode-ai/web/astro/acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
"@opencode-ai/web/astro/ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
"@opencode-ai/web/astro/common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="],
"@opencode-ai/web/astro/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
@@ -7251,6 +7234,14 @@
"ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"app-builder-lib/@electron/get/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
"app-builder-lib/@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
"app-builder-lib/@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"app-builder-lib/hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
"app-builder-lib/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"app-builder-lib/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
@@ -7269,8 +7260,6 @@
"astro-expressive-code/astro/acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
"astro-expressive-code/astro/ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
"astro-expressive-code/astro/common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="],
"astro-expressive-code/astro/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
@@ -7405,10 +7394,6 @@
"electron-winstaller/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
"electron/@electron/get/env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="],
"electron/@electron/get/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="],
"esbuild-plugin-copy/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"filelist/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="],
@@ -7417,8 +7402,6 @@
"gcp-metadata/gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
"hosted-git-info/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"js-beautify/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"js-beautify/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
@@ -7467,12 +7450,6 @@
"miniflare/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="],
"minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"openid-client/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
@@ -7499,8 +7476,6 @@
"toolbeam-docs-theme/astro/acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
"toolbeam-docs-theme/astro/ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
"toolbeam-docs-theme/astro/common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="],
"toolbeam-docs-theme/astro/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
@@ -8179,6 +8154,8 @@
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
"archiver-utils/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
"archiver-utils/glob/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="],
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-YAKhbMPKeXAZLYgJzLRQi/fwIMjPOkVIvOWkG3mCgTE=",
"aarch64-linux": "sha256-UnIhoAleUQXKP8E0pVG0OyEpugmKEhfh6fEvpkpN2QI=",
"aarch64-darwin": "sha256-m68nJKpcVrX9R6EZqVvPKgRmZEXilJGcISMaQpN/k+Q=",
"x86_64-darwin": "sha256-DEjeoEn10K5YdJJCWtLw7LtthXChUXc0Vniccl/6KIc="
"x86_64-linux": "sha256-No3mCuG2tGQauX1HUpO+rebiWh+rrpSHUCC7rtXFu1s=",
"aarch64-linux": "sha256-8joWv1iDkc6TejEukGBEX0wW8DPs55wJEPY7+9+HDdM=",
"aarch64-darwin": "sha256-4MWGFQUIP1Ae4dujztTb4G8/uDJx2wiOoypcPSUWDbw=",
"x86_64-darwin": "sha256-BtvnraCJmVagtA3Iv+EbWodjFG74sTd2Purqgo7Wkr4="
}
}
@@ -1375,17 +1375,7 @@ const onMessageStop = Effect.fn("AnthropicMessages.onMessageStop")(function* (st
const events: LLMEvent[] = []
const lifecycle = result.events.length ? Lifecycle.stepStart(state.lifecycle, events) : state.lifecycle
events.push(...result.events)
const closed = Object.entries(state.reasoningSignatures).reduce(
(current, [index, signature]) =>
Lifecycle.reasoningEnd(
current,
events,
`reasoning-${index}`,
providerMetadata(state.providerMetadataKey, { signature }),
),
lifecycle,
)
const finished = Lifecycle.finish(closed, events, {
const finished = Lifecycle.finish(lifecycle, events, {
reason: state.pendingFinish?.reason ?? {
normalized: "unknown",
raw: undefined,
@@ -469,6 +469,7 @@ const mapFinishReason = (reason: string): FinishReason => {
if (reason === "max_tokens" || reason === "model_context_window_exceeded") return "length"
if (reason === "tool_use") return "tool-calls"
if (reason === "content_filtered" || reason === "guardrail_intervened") return "content-filter"
if (reason === "malformed_model_output" || reason === "malformed_tool_use") return "error"
return "unknown"
}
@@ -623,15 +624,6 @@ const step = (state: ParserState, event: BedrockEvent) =>
}
if (event.messageStop) {
if (
event.messageStop.stopReason === "malformed_model_output" ||
event.messageStop.stopReason === "malformed_tool_use"
)
return yield* ProviderShared.eventError(
ADAPTER,
`Bedrock Converse stopped with ${event.messageStop.stopReason}`,
ProviderShared.encodeJson(event),
)
return [
{
...state,
@@ -903,54 +903,6 @@ describe("Anthropic Messages route", () => {
}),
)
it.effect("preserves a reasoning signature when message_stop closes the block", () =>
Effect.gen(function* () {
const compatible = Route.make({
id: "custom-anthropic-messages",
provider: "custom-anthropic",
protocol: AnthropicMessages.protocol,
endpoint: Endpoint.path("/messages", { baseURL: "https://compatible.test/v1" }),
auth: Auth.header("x-api-key", "test"),
framing: AnthropicMessages.framing,
}).model({ id: "custom-model" })
const body = sseEvents(
{ type: "message_start", message: { usage: { input_tokens: 5 } } },
{
type: "content_block_start",
index: 0,
content_block: { type: "thinking", thinking: "", signature: "" },
},
{ type: "content_block_delta", index: 0, delta: { type: "thinking_delta", thinking: "Reasoning." } },
{ type: "content_block_delta", index: 0, delta: { type: "signature_delta", signature: "sig_1" } },
{ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 2 } },
{ type: "message_stop" },
)
const response = yield* LLMClient.generate(LLM.request({ model: compatible, prompt: "Think." })).pipe(
Effect.provide(fixedResponse(body)),
)
const reasoningEnds = response.events.filter((event) => event.type === "reasoning-end")
expect(reasoningEnds).toHaveLength(1)
expect(reasoningEnds[0]).toMatchObject({
providerMetadata: { "custom-anthropic": { signature: "sig_1" } },
})
expect(response.message.content).toEqual([
{
type: "reasoning",
text: "Reasoning.",
providerMetadata: { "custom-anthropic": { signature: "sig_1" } },
},
])
const prepared = yield* compileRequest(
LLM.request({ model: compatible, messages: [response.message], cache: "none" }),
)
expect(prepared.body.messages).toEqual([
{ role: "assistant", content: [{ type: "thinking", thinking: "Reasoning.", signature: "sig_1" }] },
])
}),
)
it.effect("parses text, reasoning, and usage stream fixtures", () =>
Effect.gen(function* () {
const body = sseEvents(
@@ -984,7 +936,6 @@ describe("Anthropic Messages route", () => {
expect(response.events.find((event) => event.type === "reasoning-end")).toMatchObject({
providerMetadata: { anthropic: { signature: "sig_1" } },
})
expect(response.events.filter((event) => event.type === "reasoning-end")).toHaveLength(1)
expect(response.events.find((event) => event.type === "reasoning-delta" && event.text === "")).toBeUndefined()
expect(response.message.content).toEqual([
{ type: "text", text: "Hello!" },
@@ -1,12 +1,11 @@
import { EventStreamCodec } from "@smithy/eventstream-codec"
import { fromUtf8, toUtf8 } from "@smithy/util-utf8"
import { describe, expect } from "bun:test"
import { Effect, Ref, Stream } from "effect"
import { Effect } from "effect"
import {
CacheHint,
GenerationOptions,
LLM,
LLMEvent,
LLMRequest,
Message,
ToolCallPart,
@@ -386,39 +385,19 @@ describe("Bedrock Converse route", () => {
}),
)
it.effect("maps model context window exhaustion to length", () =>
it.effect("maps truncation and malformed output stop reasons", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(baseRequest).pipe(
Effect.provide(fixedBytes(eventStreamBody(["messageStop", { stopReason: "model_context_window_exceeded" }]))),
)
const reasons = [
["model_context_window_exceeded", "length"],
["malformed_model_output", "error"],
["malformed_tool_use", "error"],
] as const
expect(response.finishReason).toEqual({
normalized: "length",
raw: "model_context_window_exceeded",
})
}),
)
it.effect("fails malformed output stop reasons", () =>
Effect.gen(function* () {
for (const reason of ["malformed_model_output", "malformed_tool_use"] as const) {
const events = yield* Ref.make<ReadonlyArray<LLMEvent>>([])
const error = yield* LLMClient.stream(baseRequest).pipe(
Stream.tap((event) => Ref.update(events, (current) => [...current, event])),
Stream.runDrain,
Effect.provide(fixedBytes(eventStreamBody(["messageStop", { stopReason: reason }]))),
Effect.flip,
for (const [raw, normalized] of reasons) {
const response = yield* LLMClient.generate(baseRequest).pipe(
Effect.provide(fixedBytes(eventStreamBody(["messageStop", { stopReason: raw }]))),
)
expect(error).toMatchObject({
reason: { _tag: "InvalidProviderOutput" },
message: `Bedrock Converse stopped with ${reason}`,
})
expect(JSON.parse(error.reason.body ?? "")).toMatchObject({
headers: { ":event-type": { value: "messageStop" } },
body: JSON.stringify({ stopReason: reason }),
})
expect((yield* Ref.get(events)).some((event) => event.type === "finish")).toBeFalse()
expect(response.finishReason).toEqual({ normalized, raw })
}
}),
)
@@ -1,187 +0,0 @@
import { expect, test } from "@playwright/test"
import { base64Encode } from "@opencode-ai/util/encode"
import type { OpenCodeEvent } from "@opencode-ai/client/promise"
import { fixture } from "../smoke/session-timeline.fixture"
import { mockOpenCodeServer } from "../utils/mock-server"
test("keeps history visible and recovers the composer by choosing another directory", async ({ page }) => {
const directory = "/projects/deleted"
const destination = "/projects/restored"
const sessionID = "ses_missing_location"
const session = { id: sessionID, projectID: fixture.project.id, directory, title: "Missing location" }
const events: OpenCodeEvent[] = []
const moves: unknown[] = []
await mockOpenCodeServer(page, {
directory: destination,
project: { ...fixture.project, worktree: destination },
provider: fixture.provider,
sessions: [session],
events: () => events.splice(0),
fileList: () => [],
pageMessages: () => ({
items: [{ id: "msg_saved", type: "user", text: "Keep this session history", time: { created: 1 } }],
}),
})
await page.route("**/api/**", (route) => {
if (new URL(route.request().url()).searchParams.get("location[directory]") !== directory) return route.fallback()
return route.fulfill({ status: 500, body: "", headers: { "access-control-allow-origin": "*" } })
})
await page.goto(`/server/${base64Encode(fixture.serverKey)}/session/${sessionID}`)
await expect(page.getByText("Keep this session history", { exact: true })).toBeVisible()
await expect(page.getByRole("status")).toContainText("Session location unavailable")
await expect(page.getByRole("status")).toContainText(directory)
await expect(page.getByRole("textbox", { name: "Prompt", exact: true })).toHaveCount(0)
const choose = page.getByRole("button", { name: "Choose directory", exact: true })
await expect(choose).toBeEnabled()
await choose.click()
const dialog = page.getByRole("dialog", { name: "Choose directory", exact: true })
await expect(dialog.getByRole("combobox")).toBeFocused()
await dialog.getByRole("combobox").press("Escape")
await expect(dialog).toHaveCount(0)
await expect(choose).toBeFocused()
expect(moves).toEqual([])
// The server can still reject a destination after it was selected in the picker.
await page.route(`**/api/session/${sessionID}/move`, (route) => {
if (route.request().method() !== "POST") return route.fallback()
moves.push(route.request().postDataJSON())
return route.fulfill({
status: 400,
json: { _tag: "InvalidRequestError", message: "Destination is unavailable" },
headers: { "access-control-allow-origin": "*" },
})
})
await choose.press("Enter")
await expect(dialog.getByRole("combobox")).toBeFocused()
await dialog.getByRole("combobox").fill(destination)
await dialog.getByRole("combobox").press("Enter")
await expect(dialog.locator(".directory-picker-selection")).toHaveText(destination)
await dialog.getByRole("button", { name: "Select folder", exact: true }).click()
await expect(page.getByText("Failed to move session", { exact: true })).toBeVisible()
await expect(choose).toBeEnabled()
await expect(page.getByRole("textbox", { name: "Prompt", exact: true })).toHaveCount(0)
await expect(choose).toBeFocused()
expect(moves).toEqual([{ directory: destination }])
await page.route(`**/api/session/${sessionID}/move`, (route) => {
if (route.request().method() !== "POST") return route.fallback()
moves.push(route.request().postDataJSON())
session.directory = destination
events.push({
id: "evt_location_recovered",
type: "session.moved",
created: 2,
durable: { aggregateID: sessionID, seq: 1, version: 1 },
data: { sessionID, location: { directory: destination }, projectID: fixture.project.id },
})
return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } })
})
await choose.click()
await dialog.getByRole("combobox").fill(destination)
await dialog.getByRole("combobox").press("Enter")
await expect(dialog.locator(".directory-picker-selection")).toHaveText(destination)
await dialog.getByRole("button", { name: "Select folder", exact: true }).click()
await expect(page.getByText("Session location unavailable", { exact: true })).toHaveCount(0)
await expect(page.getByRole("textbox", { name: "Prompt", exact: true })).toBeEditable()
await expect(page.locator('[data-action="composer-model"]')).toBeVisible()
await expect(page.getByText("Keep this session history", { exact: true })).toBeVisible()
await expect(page).toHaveURL(new RegExp(`/session/${sessionID}$`))
expect(moves).toEqual([{ directory: destination }, { directory: destination }])
})
for (const create of [false, true]) {
test(`recovers into ${create ? "a new" : "an existing"} worktree without resolving the deleted location`, async ({
page,
}) => {
const canonical = "/projects/repository"
const directory = "/projects/deleted-worktree"
const destination = create ? "/projects/new-worktree" : "/projects/existing-worktree"
const sessionID = "ses_worktree_recovery"
const events: OpenCodeEvent[] = []
const requests: { operation: string; body: unknown }[] = []
let listing = Promise.withResolvers<void>()
let listingRequested = Promise.withResolvers<void>()
const moving = Promise.withResolvers<void>()
const moveRequested = Promise.withResolvers<void>()
await mockOpenCodeServer(page, {
directory: canonical,
project: { ...fixture.project, worktree: canonical },
provider: fixture.provider,
sessions: [{ id: sessionID, projectID: fixture.project.id, directory }],
pageMessages: () => ({
items: [{ id: "msg_worktree", type: "user", text: "Recover my worktree", time: { created: 1 } }],
}),
events: () => events.splice(0),
})
await page.route("**/api/**", async (route) => {
const url = new URL(route.request().url())
const headers = { "access-control-allow-origin": "*" }
if (url.searchParams.get("location[directory]") === directory)
return route.fulfill({ status: 500, body: "", headers })
if (url.pathname === `/api/worktree/${fixture.project.id}`) {
if (route.request().method() === "GET") {
listingRequested.resolve()
await listing.promise
return route.fulfill({
json: [
{ directory: canonical, strategy: null },
{ directory: "/projects/existing-worktree", strategy: "git" },
],
headers,
})
}
if (route.request().method() === "POST") {
requests.push({ operation: "create", body: route.request().postDataJSON() })
return route.fulfill({
json: { directory: destination, name: "new-worktree", branch: "new-worktree" },
headers,
})
}
}
if (url.pathname === `/api/session/${sessionID}/move` && route.request().method() === "POST") {
requests.push({ operation: "move", body: route.request().postDataJSON() })
moveRequested.resolve()
await moving.promise
events.push({
id: "evt_worktree_recovered",
type: "session.moved",
created: 2,
durable: { aggregateID: sessionID, seq: 1, version: 1 },
data: { sessionID, location: { directory: destination }, projectID: fixture.project.id },
})
return route.fulfill({ status: 204, headers })
}
return route.fallback()
})
await page.goto(`/server/${base64Encode(fixture.serverKey)}/session/${sessionID}`)
await page.getByRole("button", { name: "Choose worktree", exact: true }).click()
await listingRequested.promise
await expect(page.getByRole("menuitem", { name: "Loading", exact: true })).toBeVisible()
await expect(page.getByRole("menuitem", { name: "New workspace", exact: true })).toBeVisible()
await expect(page.getByText("Recover my worktree", { exact: true })).toBeVisible()
await expect(page.getByText("Session location unavailable", { exact: true })).toBeVisible()
listing.resolve()
await expect(page.getByRole("menuitem", { name: "existing-worktree", exact: true })).toBeVisible()
await page.keyboard.press("Escape")
await expect(page.getByRole("menu")).toHaveCount(0)
listing = Promise.withResolvers<void>()
listingRequested = Promise.withResolvers<void>()
await page.getByRole("button", { name: "Choose worktree", exact: true }).click()
await listingRequested.promise
await expect(page.getByRole("menuitem", { name: "existing-worktree", exact: true })).toBeVisible()
await expect(page.getByText("Recover my worktree", { exact: true })).toBeVisible()
listing.resolve()
await page.getByRole("menuitem", { name: create ? "New workspace" : "existing-worktree", exact: true }).click()
await moveRequested.promise
await expect(page.getByRole("button", { name: "Moving session…", exact: true })).toBeDisabled()
await expect(page.getByRole("button", { name: "Choose worktree", exact: true })).toBeDisabled()
moving.resolve()
await expect(page.getByText("Session location unavailable", { exact: true })).toHaveCount(0)
await expect(page.getByRole("textbox", { name: "Prompt", exact: true })).toBeEditable()
await expect(page.getByText("Recover my worktree", { exact: true })).toBeVisible()
expect(requests).toEqual([
...(create ? [{ operation: "create", body: { strategy: "git", from: canonical, directory: "/projects/" } }] : []),
{ operation: "move", body: { directory: destination } },
])
})
}
-6
View File
@@ -2,12 +2,6 @@ import { DESKTOP_NATIVE_ENGLISH } from "./desktop-native"
export const dict = {
...DESKTOP_NATIVE_ENGLISH,
"session.location.unavailable": "Session location unavailable",
"session.location.description": "Choose another directory to continue this session.",
"session.location.choose": "Choose directory",
"session.location.worktree": "Choose worktree",
"session.location.worktreesFailed": "Failed to load worktrees",
"session.location.moving": "Moving session…",
"command.category.suggested": "Suggested",
"command.category.view": "View",
"command.category.project": "Project",
@@ -1,136 +0,0 @@
import { Button } from "@opencode-ai/ui/button"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { DockShell, DockTray } from "@opencode-ai/ui/dock-surface"
import { Icon } from "@opencode-ai/ui/icon"
import { Menu } from "@opencode-ai/ui/menu"
import { getFilename } from "@opencode-ai/util/path"
import { createMutation } from "@tanstack/solid-query"
import { createEffect, createResource, For, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { useLanguage } from "@/runtime/i18n/language"
import { useServerSDK } from "@/runtime/server/client"
import { useData } from "@/runtime/server/current"
import { showToast } from "@/shell/notifications/toast"
import { useDirectoryPicker } from "@/workspaces/selection/picker"
import { createWorktree } from "@/workspaces/create"
export function SessionLocationMissing(props: { sessionID: string; projectID: string; directory: string }) {
const language = useLanguage()
const sdk = useServerSDK()
const data = useData()
const dialog = useDialog()
const pickDirectory = useDirectoryPicker()
const [state, setState] = createStore({ restoreFocus: false, worktreesOpen: false })
const project = () => data.project.get(props.projectID)
const [worktrees] = createResource(
() => (state.worktreesOpen ? props.projectID : undefined),
async (projectID) => {
try {
await sdk.api.worktree.refresh({ projectID })
return await sdk.api.worktree.list({ projectID })
} catch {
showToast({ variant: "error", title: language.t("session.location.worktreesFailed") })
return []
}
},
// Seed latest so even the first fetch does not enter Suspense.
{ initialValue: [] },
)
let button: HTMLButtonElement | undefined
const move = createMutation(() => ({
mutationFn: async (input: { sessionID: string; directory?: string }) => {
// A deleted worktree cannot resolve its own location. Create from the
// project's saved canonical checkout instead.
const current = project()
const destination =
input.directory ??
(current &&
(await createWorktree({
api: sdk.api,
directory: current.canonical,
project: { id: current.id, canonical: current.canonical, directory: current.canonical },
})))
if (!destination) return
await sdk.api.session.move({ sessionID: input.sessionID, directory: destination })
},
onError: (error) => {
setState("restoreFocus", true)
showToast({
variant: "error",
title: language.t("workspace.move.failed"),
description: error instanceof Error ? error.message : language.t("common.requestFailed"),
})
},
}))
createEffect(() => {
if (!state.restoreFocus || move.isPending || dialog.active) return
setState("restoreFocus", false)
button?.focus()
})
function choose() {
if (move.isPending) return
const sessionID = props.sessionID
pickDirectory({
server: sdk.server,
title: language.t("session.location.choose"),
onSelect: (result) => {
const directory = Array.isArray(result) ? result[0] : result
if (directory) move.mutate({ sessionID, directory })
if (!directory) setState("restoreFocus", true)
},
})
}
return (
<div data-component="session-location-missing">
<DockShell class="flex flex-col gap-2 p-3">
<div role="status" class="flex items-start gap-2 text-13-regular leading-[var(--line-height-base)]">
<Icon name="warning" class="shrink-0 text-icon-warning-base" />
<div class="min-w-0 flex flex-col gap-1">
<div class="font-medium text-text-strong">{language.t("session.location.unavailable")}</div>
<div class="break-all font-mono text-12-regular text-text-weak">{props.directory}</div>
<div class="text-text-base">{language.t("session.location.description")}</div>
</div>
</div>
</DockShell>
<DockTray class="flex flex-wrap justify-end gap-2 p-2">
<Show when={project()?.vcs === "git"}>
<Menu placement="top-end" onOpenChange={(open) => setState("worktreesOpen", open)}>
<Menu.Trigger as={Button} variant="neutral" disabled={move.isPending}>
{language.t("session.location.worktree")}
</Menu.Trigger>
<Menu.Portal>
<Menu.Content class="max-h-80 max-w-[calc(100vw-32px)] overflow-y-auto">
<Menu.Item onSelect={() => move.mutate({ sessionID: props.sessionID })} disabled={move.isPending}>
<Icon name="workspace-new" />
{language.t("workspace.new")}
</Menu.Item>
<Show when={worktrees.loading}>
<Menu.Item disabled>{language.t("common.loading")}</Menu.Item>
</Show>
<For each={worktrees.latest.filter((item) => item.strategy && item.directory !== props.directory)}>
{(worktree) => (
<Menu.Item
title={worktree.directory}
onSelect={() => move.mutate({ sessionID: props.sessionID, directory: worktree.directory })}
disabled={move.isPending}
>
<Icon name="workspace-isolated" />
<span class="truncate">{getFilename(worktree.directory)}</span>
</Menu.Item>
)}
</For>
</Menu.Content>
</Menu.Portal>
</Menu>
</Show>
<Button ref={button} variant="contrast" onClick={choose} disabled={move.isPending}>
{language.t(move.isPending ? "session.location.moving" : "session.location.choose")}
</Button>
</DockTray>
</div>
)
}
+7 -30
View File
@@ -4,7 +4,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
import { isScrollKeyTarget, scrollKey, scrollKeyOwner } from "@opencode-ai/ui/scroll-view"
import { makeEventListener } from "@solid-primitives/event-listener"
import { useNavigate } from "@solidjs/router"
import { createEffect, createMemo, on, onMount, Show } from "solid-js"
import { createEffect, on, onMount } from "solid-js"
import { Composer } from "@/composer/composer"
import { createComposerModel, type ComposerModel } from "@/composer/model"
import { useComposerState } from "@/composer/persistence"
@@ -32,7 +32,6 @@ import { SessionQueuePanel } from "./queue-panel"
import { resolveSessionComposerSelection } from "./selection"
import { createSessionRequestModel } from "../requests/model"
import { useSettings } from "@/settings/model"
import { SessionLocationMissing } from "./location-missing"
export function createActiveSessionRegion(input: {
session: SessionModel
@@ -220,14 +219,6 @@ export function ActiveSessionComposerRegion(props: {
onResponseSubmit: () => void
}) {
const settings = useSettings()
const location = useWorkspaceLocation()
const missing = createMemo(() => {
const error = location().error
const current = props.session.data.info()?.location
if (error && current?.directory === error.location.directory && current.workspaceID === error.location.workspaceID)
return current.directory
return undefined
})
const region = createSessionComposerRegionController({
state: props.model.region.state,
parentID: props.session.data.parentID,
@@ -256,26 +247,12 @@ export function ActiveSessionComposerRegion(props: {
<SessionComposerRegion
controller={region}
composer={
<Show
when={missing()}
keyed
fallback={
<div class="relative">
<SessionQueuePanel queue={queue} />
<div class="relative z-10">
<Composer model={composer} borderUnderlay />
</div>
</div>
}
>
{(directory) => (
<SessionLocationMissing
sessionID={requireSessionID(props.session)}
projectID={props.session.data.info()!.projectID}
directory={directory}
/>
)}
</Show>
<div class="relative">
<SessionQueuePanel queue={queue} />
<div class="relative z-10">
<Composer model={composer} borderUnderlay />
</div>
</div>
}
/>
)
@@ -1,32 +0,0 @@
import type { APIEvent } from "@solidjs/start/server"
import { Workspace } from "@opencode-ai/console-core/workspace.js"
import { safeEqual } from "@opencode-ai/console-core/util/crypto.js"
import { Resource } from "@opencode-ai/console-resource"
import z from "zod"
const Body = z.object({
workspaceID: z.string().startsWith("wrk_"),
requesterEmail: z.email(),
})
export async function DELETE(event: APIEvent) {
if (!safeEqual(event.request.headers.get("authorization") ?? "", `Bearer ${Resource.SUPPORT_API_KEY.value}`)) {
return Response.json({ error: "Unauthorized" }, { status: 401 })
}
const body = Body.safeParse(await event.request.json().catch(() => undefined))
if (!body.success) {
return Response.json({ error: "Invalid request", issues: body.error.issues }, { status: 400 })
}
return Workspace.removeExact({
workspaceID: body.data.workspaceID,
expectedRequesterEmail: body.data.requesterEmail,
})
.then(() => Response.json({ success: true, message: "Workspace deleted" }))
.catch((error) => {
if (error instanceof Workspace.RemovalRejected) {
return Response.json({ error: error.message }, { status: 400 })
}
return Response.json({ error: "Workspace deletion outcome is unknown" }, { status: 500 })
})
}
+4 -13
View File
@@ -6,7 +6,6 @@ import { Identifier } from "./identifier"
import { KeyTable } from "./schema/key.sql"
import { UserTable } from "./schema/user.sql"
import { AuthTable } from "./schema/auth.sql"
import { WorkspaceTable } from "./schema/workspace.sql"
export namespace Key {
export const list = fn(z.void(), async () => {
@@ -58,24 +57,16 @@ export namespace Key {
}
const keyID = Identifier.create("key")
await Database.transaction(async (tx) => {
const workspace = await tx
.select({ id: WorkspaceTable.id })
.from(WorkspaceTable)
.where(and(eq(WorkspaceTable.id, Actor.workspace()), isNull(WorkspaceTable.timeDeleted)))
.for("update")
.then((rows) => rows[0])
if (!workspace) throw new Error("Workspace is not active")
await tx.insert(KeyTable).values({
await Database.use((tx) =>
tx.insert(KeyTable).values({
id: keyID,
workspaceID: Actor.workspace(),
userID: input.userID,
name,
key: secretKey,
timeUsed: null,
})
})
}),
)
return keyID
},
+1 -118
View File
@@ -4,19 +4,13 @@ import { Actor } from "./actor"
import { Database } from "./drizzle"
import { Identifier } from "./identifier"
import { UserTable } from "./schema/user.sql"
import { BillingTable, LiteTable, SubscriptionTable } from "./schema/billing.sql"
import { BillingTable } from "./schema/billing.sql"
import { WorkspaceTable } from "./schema/workspace.sql"
import { AccountTable } from "./schema/account.sql"
import { Key } from "./key"
import { and, eq, isNull, sql } from "drizzle-orm"
import { AuthTable } from "./schema/auth.sql"
import { KeyTable } from "./schema/key.sql"
import { ProviderTable } from "./schema/provider.sql"
import { ModelTable } from "./schema/model.sql"
export namespace Workspace {
export class RemovalRejected extends Error {}
export const Region = z.enum(["us", "eu", "sg", "cn"])
export type Region = z.infer<typeof Region>
@@ -110,117 +104,6 @@ export namespace Workspace {
)
})
export const removeExact = fn(
z.object({
workspaceID: z.string().startsWith("wrk_"),
expectedRequesterEmail: z.email(),
}),
async (input) => {
await Database.transaction(async (tx) => {
const workspace = await tx
.select({ id: WorkspaceTable.id, timeDeleted: WorkspaceTable.timeDeleted })
.from(WorkspaceTable)
.where(eq(WorkspaceTable.id, input.workspaceID))
.for("update")
.then((rows) => rows[0])
if (!workspace) throw new RemovalRejected("Workspace not found")
const requester = await tx
.select({
accountID: AccountTable.id,
role: UserTable.role,
invitationEmail: UserTable.email,
membershipDeleted: UserTable.timeDeleted,
})
.from(AuthTable)
.innerJoin(AccountTable, and(eq(AccountTable.id, AuthTable.accountID), isNull(AccountTable.timeDeleted)))
.innerJoin(
UserTable,
and(eq(UserTable.accountID, AccountTable.id), eq(UserTable.workspaceID, input.workspaceID)),
)
.where(
and(
eq(AuthTable.provider, "email"),
eq(AuthTable.subject, input.expectedRequesterEmail),
isNull(AuthTable.timeDeleted),
),
)
.for("update")
if (requester.length !== 1 || requester[0].role !== "admin" || requester[0].invitationEmail) {
throw new RemovalRejected("Expected requester is not an administrator of this workspace")
}
if (!workspace.timeDeleted && requester[0].membershipDeleted) {
throw new RemovalRejected("Expected requester does not have an active workspace membership")
}
if (workspace.timeDeleted) {
if (!requester[0].membershipDeleted) throw new RemovalRejected("Deleted workspace has an active membership")
return
}
const billing = await tx
.select({
timeDeleted: BillingTable.timeDeleted,
balance: BillingTable.balance,
reload: BillingTable.reload,
subscription: BillingTable.subscription,
subscriptionID: BillingTable.subscriptionID,
subscriptionPlan: BillingTable.subscriptionPlan,
timeSubscriptionBooked: BillingTable.timeSubscriptionBooked,
timeSubscriptionSelected: BillingTable.timeSubscriptionSelected,
liteSubscriptionID: BillingTable.liteSubscriptionID,
lite: BillingTable.lite,
})
.from(BillingTable)
.where(eq(BillingTable.workspaceID, input.workspaceID))
.for("update")
if (billing.length !== 1 || billing[0].timeDeleted) throw new RemovalRejected("Workspace billing state is inconsistent")
if (billing[0].balance > 0) throw new RemovalRejected("Workspace has a positive Zen balance")
if (billing[0].reload) throw new RemovalRejected("Workspace has Zen reload enabled")
if (
billing[0].subscription ||
billing[0].subscriptionID ||
billing[0].subscriptionPlan ||
billing[0].timeSubscriptionBooked ||
billing[0].timeSubscriptionSelected
) {
throw new RemovalRejected("Workspace has active or inconsistent Black billing state")
}
if (billing[0].liteSubscriptionID || billing[0].lite) {
throw new RemovalRejected("Workspace has active or inconsistent Go billing state")
}
const black = await tx
.select({ id: SubscriptionTable.id })
.from(SubscriptionTable)
.where(and(eq(SubscriptionTable.workspaceID, input.workspaceID), isNull(SubscriptionTable.timeDeleted)))
.for("update")
if (black.length > 0) throw new RemovalRejected("Workspace has active or inconsistent Black entitlement state")
const go = await tx
.select({ id: LiteTable.id })
.from(LiteTable)
.where(and(eq(LiteTable.workspaceID, input.workspaceID), isNull(LiteTable.timeDeleted)))
.for("update")
if (go.length > 0) throw new RemovalRejected("Workspace has active or inconsistent Go entitlement state")
const timeDeleted = new Date()
await tx
.update(WorkspaceTable)
.set({ timeDeleted })
.where(and(eq(WorkspaceTable.id, input.workspaceID), isNull(WorkspaceTable.timeDeleted)))
await tx
.update(UserTable)
.set({ timeDeleted })
.where(and(eq(UserTable.workspaceID, input.workspaceID), isNull(UserTable.timeDeleted)))
await tx
.update(KeyTable)
.set({ timeDeleted })
.where(and(eq(KeyTable.workspaceID, input.workspaceID), isNull(KeyTable.timeDeleted)))
await tx.delete(ProviderTable).where(eq(ProviderTable.workspaceID, input.workspaceID))
await tx.delete(ModelTable).where(eq(ModelTable.workspaceID, input.workspaceID))
})
},
)
export const unblock = fn(z.string().startsWith("wrk_"), async (workspaceID) => {
await Database.transaction(async (tx) => {
const workspace = await tx
@@ -1,195 +0,0 @@
import { beforeEach, describe, expect, mock, test } from "bun:test"
import { and, eq, getTableName, isNull, sql } from "drizzle-orm"
import type { SQLWrapper, Table } from "drizzle-orm"
import { MySqlDialect } from "drizzle-orm/mysql-core"
type Row = Record<string, unknown>
class Query {
constructor(private rows: Row[]) {}
innerJoin() {
return this
}
where() {
return this
}
for() {
return this
}
then(resolve: (rows: Row[]) => unknown) {
return Promise.resolve(this.rows).then(resolve)
}
}
class Update {
private values: Row = {}
constructor(
private database: TestDatabase,
private table: Table,
) {}
set(values: Row) {
this.values = values
return this
}
where(condition: SQLWrapper) {
const table = getTableName(this.table)
const query = new MySqlDialect().sqlToQuery(condition.getSQL())
const workspaceID = query.params.find((param) => typeof param === "string" && param.startsWith("wrk_"))
const rows = table === "workspace" ? this.database.workspaces : this.database.rows[table]
rows?.forEach((row) => {
const matches = table === "workspace" ? row.id === workspaceID : row.workspaceID === workspaceID
if (matches && !row.timeDeleted) Object.assign(row, this.values)
})
if (table === "user") Object.assign(this.database.requester[0], { membershipDeleted: this.values.timeDeleted })
this.database.updated.push(table)
return Promise.resolve()
}
}
class Delete {
constructor(
private database: TestDatabase,
private table: Table,
) {}
where(condition: SQLWrapper) {
const table = getTableName(this.table)
const query = new MySqlDialect().sqlToQuery(condition.getSQL())
const workspaceID = query.params.find((param) => typeof param === "string" && param.startsWith("wrk_"))
this.database.rows[table] = this.database.rows[table]?.filter((row) => row.workspaceID !== workspaceID) ?? []
this.database.updated.push(`delete:${table}`)
return Promise.resolve()
}
}
class TestDatabase {
workspaces: Row[] = [
{ id: target, timeDeleted: null },
{ id: other, timeDeleted: null },
]
rows: Record<string, Row[]> = {
user: [
{ id: "usr_target", workspaceID: target, timeDeleted: null },
{ id: "usr_other", workspaceID: other, timeDeleted: null },
],
key: [
{ id: "key_target", workspaceID: target, timeDeleted: null },
{ id: "key_other", workspaceID: other, timeDeleted: null },
],
provider: [
{ id: "provider_target", workspaceID: target, timeDeleted: null },
{ id: "provider_other", workspaceID: other, timeDeleted: null },
],
model: [
{ id: "model_target", workspaceID: target, timeDeleted: null },
{ id: "model_other", workspaceID: other, timeDeleted: null },
],
}
billing: Row = {
timeDeleted: null,
balance: 0,
reload: false,
subscription: null,
subscriptionID: null,
subscriptionPlan: null,
timeSubscriptionBooked: null,
timeSubscriptionSelected: null,
liteSubscriptionID: null,
lite: null,
}
requester: Row[] = [{ accountID: "acc_target", role: "admin", invitationEmail: null, membershipDeleted: null }]
black: Row[] = []
go: Row[] = []
updated: string[] = []
select() {
return {
from: (table: Table) => {
const name = getTableName(table)
if (name === "workspace") return new Query(this.workspaces.filter((row) => row.id === target))
if (name === "auth") return new Query(this.requester)
if (name === "billing") return new Query([this.billing])
if (name === "subscription") return new Query(this.black)
if (name === "lite") return new Query(this.go)
throw new Error(`Unexpected select from ${name}`)
},
}
}
update(table: Table) {
return new Update(this, table)
}
delete(table: Table) {
return new Delete(this, table)
}
}
const target = "wrk_target"
const other = "wrk_other"
let database = new TestDatabase()
mock.module("../src/drizzle", () => ({
and,
Database: {
transaction: (callback: (tx: TestDatabase) => Promise<unknown>) => callback(database),
},
eq,
isNull,
sql,
}))
const { Workspace } = await import("../src/workspace")
beforeEach(() => {
database = new TestDatabase()
})
describe("Workspace.removeExact", () => {
test("deletes only the exact workspace and revokes its memberships and keys", async () => {
await Workspace.removeExact({ workspaceID: target, expectedRequesterEmail: "owner@example.com" })
expect(database.workspaces.find((row) => row.id === target)?.timeDeleted).toBeInstanceOf(Date)
expect(database.rows.user.find((row) => row.workspaceID === target)?.timeDeleted).toBeInstanceOf(Date)
expect(database.rows.key.find((row) => row.workspaceID === target)?.timeDeleted).toBeInstanceOf(Date)
expect(database.workspaces.find((row) => row.id === other)?.timeDeleted).toBeNull()
expect(database.rows.user.find((row) => row.workspaceID === other)?.timeDeleted).toBeNull()
expect(database.rows.key.find((row) => row.workspaceID === other)?.timeDeleted).toBeNull()
expect(database.rows.provider.some((row) => row.workspaceID === target)).toBe(false)
expect(database.rows.model.some((row) => row.workspaceID === target)).toBe(false)
expect(database.rows.provider.some((row) => row.workspaceID === other)).toBe(true)
expect(database.rows.model.some((row) => row.workspaceID === other)).toBe(true)
expect(database.updated).toEqual(["workspace", "user", "key", "delete:provider", "delete:model"])
})
test("allows an exact retry only when the deleted admin membership still proves ownership", async () => {
await Workspace.removeExact({ workspaceID: target, expectedRequesterEmail: "owner@example.com" })
database.updated = []
await Workspace.removeExact({ workspaceID: target, expectedRequesterEmail: "owner@example.com" })
expect(database.updated).toEqual([])
})
test.each([
["non-admin requester", () => (database.requester[0].role = "member")],
["invitation-only requester", () => (database.requester = [])],
["inconsistent accepted invitation", () => (database.requester[0].invitationEmail = "owner@example.com")],
["positive Zen balance", () => (database.billing.balance = 1)],
["Zen reload", () => (database.billing.reload = true)],
["Black billing", () => (database.billing.subscriptionID = "sub_active")],
["Go billing", () => (database.billing.liteSubscriptionID = "sub_active")],
["Black entitlement", () => (database.black = [{ id: "sub_black" }])],
["Go entitlement", () => (database.go = [{ id: "sub_go" }])],
])("rejects %s without deleting data", async (_name, arrange) => {
arrange()
expect(
Workspace.removeExact({ workspaceID: target, expectedRequesterEmail: "owner@example.com" }),
).rejects.toBeTruthy()
expect(database.updated).toEqual([])
})
})
@@ -3,7 +3,6 @@ import path from "node:path"
import { fileURLToPath } from "node:url"
import { promisify } from "node:util"
import type { CustomMacSignOptions } from "app-builder-lib"
import type { Configuration } from "electron-builder"
const execFileAsync = promisify(execFile)
@@ -30,18 +29,6 @@ async function signWindows(configuration: { path: string }) {
)
}
export function macSignOptions(options: CustomMacSignOptions): CustomMacSignOptions {
return {
...options,
optionsForFile: (file) => {
const defaults = options.optionsForFile?.(file)
if (file !== path.join(options.app, "Contents/Resources/opencode-cli")) return defaults ?? {}
// The Bun CLI loads bun-pty's native library; Electron and its helpers do not need this exception.
return { ...defaults, entitlements: path.join(packageDir, "resources/entitlements.cli.plist") }
},
}
}
const channel = (() => {
const raw = process.env.OPENCODE_CHANNEL
if (raw === "dev" || raw === "beta" || raw === "prod") return raw
@@ -101,10 +88,6 @@ const getBase = (appId: string): Configuration => ({
gatekeeperAssess: false,
entitlements: "resources/entitlements.plist",
entitlementsInherit: "resources/entitlements.plist",
sign: async (options) => {
const { sign } = await import("app-builder-lib/out/codeSign/macCodeSign")
await sign(macSignOptions(options))
},
notarize: true,
target: ["dmg", "zip"],
},
-1
View File
@@ -44,7 +44,6 @@
"@types/bun": "catalog:",
"@types/node": "catalog:",
"@typescript/native-preview": "catalog:",
"app-builder-lib": "26.15.7",
"drizzle-orm": "catalog:",
"effect": "catalog:",
"electron": "42.10.1",
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,126 @@
import { expect, story } from "../../storybook/playwright/story"
const id = "current-session-tool-headers--shared-headers"
story("shares compact title and detail metrics across tool families", async ({ mount }, info) => {
const root = await mount(id)
await root.getByRole("button", { name: /^Used 7 / }).click()
const headers = root.locator('[data-component="context-tool-group-list"] [data-component="tool-header"]')
await expect(headers).toHaveCount(7)
const titles = headers.locator('[data-slot="basic-tool-tool-title"]')
await expect(headers.locator('[data-component="text-shimmer"][aria-label="Write"]')).toBeVisible()
await expect(headers.locator('[data-component="text-shimmer"][aria-label="Edit"]')).toBeVisible()
for (const title of await titles.all()) {
await expect(title).toBeVisible()
await expect(title).toHaveCSS("font-family", /^Inter,/)
await expect(title).toHaveCSS("font-size", "13px")
await expect(title).toHaveCSS("line-height", "16px")
await expect(title).toHaveCSS("font-weight", "530")
await expect(title.locator('[data-component="text-shimmer"]')).toHaveAttribute("data-active", "false")
}
const details = headers.locator(
'[data-slot="basic-tool-tool-subtitle"], [data-slot="basic-tool-tool-arg"], [data-slot="tool-header-directory"]',
)
expect(await details.count()).toBeGreaterThan(7)
for (const detail of await details.all()) {
await expect(detail).toHaveCSS("font-size", "13px")
await expect(detail).toHaveCSS("line-height", "16px")
await expect(detail).toHaveCSS("font-weight", "440")
}
await expect(headers.locator('[data-slot="basic-tool-tool-arg"]')).toHaveText([
"offset=12",
"limit=40",
"pattern=header",
"include=*.tsx",
])
await root.locator('[data-component="session-timeline"]').screenshot({ path: info.outputPath("tool-headers.png") })
})
story("keeps pending file titles active without showing unfinished paths", async ({ mount }) => {
const root = await mount(id, { args: { phase: "streaming", pathKnown: false } })
await root
.locator(
'[data-component="collapsed-tool-group"] > [data-component="collapsible"] > [data-slot="collapsible-trigger"]',
)
.click()
for (const action of [undefined, "Provide paths", "Run tools"]) {
if (action) await root.getByRole("button", { name: action, exact: true }).click()
for (const name of ["edit", "write"]) {
const header = root.locator(`[data-timeline-part-id="tool_header_${name}"] [data-component="tool-header"]`)
await expect(header).toBeVisible()
await expect(header.locator('[data-component="text-shimmer"]')).toHaveAttribute("data-active", "true")
await expect(header.locator('[data-slot="basic-tool-tool-title"]')).toHaveCSS("line-height", "16px")
await expect(
header.locator('[data-slot="basic-tool-tool-subtitle"], [data-slot="tool-header-directory"]'),
).toHaveCount(0)
}
}
await root.getByRole("button", { name: "Complete tools", exact: true }).click()
const group = root.getByRole("button", { name: /^Used 7 / })
if ((await group.getAttribute("aria-expanded")) === "false") await group.click()
for (const name of ["edit", "write"]) {
const header = root.locator(`[data-timeline-part-id="tool_header_${name}"] [data-component="tool-header"]`)
await expect(header.locator('[data-slot="basic-tool-tool-subtitle"]')).toHaveText(`${name}.ts`)
await expect(header.locator('[data-slot="tool-header-directory"]')).toContainText("src/components")
await expect(header.locator('[data-component="text-shimmer"]')).toHaveAttribute("data-active", "false")
}
})
for (const theme of ["light", "dark"]) {
for (const width of [390, 1000]) {
story(`truncates long file headers at ${width}px in ${theme}`, async ({ mount, page }, info) => {
await page.setViewportSize({ width, height: 850 })
const root = await mount(id, { args: { longPath: true }, globals: { theme } })
await root.getByRole("button", { name: /^Used 7 / }).click()
for (const name of ["edit", "write"]) {
const header = root.locator(`[data-timeline-part-id="tool_header_${name}"] [data-component="tool-header"]`)
const filename = header.locator('[data-slot="basic-tool-tool-subtitle"]')
const directory = header.locator('[data-slot="tool-header-directory"] > span')
await expect(filename).toContainText(`${name}.ts`)
for (const text of [filename, directory]) {
await expect(text).toHaveCSS("text-overflow", "ellipsis")
await expect(text).toHaveCSS("white-space", "nowrap")
await expect(text).toHaveCSS("line-height", "16px")
}
expect(await filename.evaluate((node) => node.scrollWidth > node.clientWidth)).toBe(true)
const bounds = await header.boundingBox()
expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(width)
}
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true)
await root
.locator('[data-component="session-timeline"]')
.screenshot({ path: info.outputPath("long-headers.png") })
})
}
}
story("preserves keyboard disclosures and the webfetch link", async ({ mount }) => {
const root = await mount(id)
await root.getByRole("button", { name: /^Used 7 / }).click()
for (const name of ["shell", "execute", "edit", "write"]) {
const row = root.locator(`[data-timeline-part-id="tool_header_${name}"]`)
const trigger = row.locator('[data-slot="collapsible-trigger"]').first()
const content = row.locator('[data-slot="collapsible-content"]').first()
await expect(trigger).toHaveAttribute("aria-expanded", "false")
await trigger.focus()
await trigger.press("Enter")
await expect(trigger).toHaveAttribute("aria-expanded", "true")
await expect(content).toBeVisible()
await expect(trigger).toBeFocused()
await trigger.press("Space")
await expect(trigger).toHaveAttribute("aria-expanded", "false")
await expect(content).toBeHidden()
await expect(trigger).toBeFocused()
}
const link = root.getByRole("link", { name: "https://example.com/docs" })
await expect(link).toBeVisible()
await expect(link).toHaveAttribute("href", "https://example.com/docs")
await expect(link).toHaveAttribute("target", "_blank")
await expect(link).toHaveAttribute("rel", /noopener/)
await expect(link).toHaveCSS("font-size", "13px")
await expect(link).toHaveCSS("font-weight", "440")
await expect(link).toHaveCSS("line-height", "16px")
await expect(link).toHaveCSS("letter-spacing", "-0.04px")
await link.focus()
await expect(link).toBeFocused()
})
@@ -83,22 +83,11 @@
[data-slot="basic-tool-tool-title"] {
flex-shrink: 0;
font-family: var(--font-family-sans);
font-size: 14px;
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: var(--line-height-large);
letter-spacing: var(--letter-spacing-normal);
color: var(--v2-text-text-base);
&.capitalize {
text-transform: capitalize;
}
&.agent-title {
color: var(--v2-text-text-base);
font-weight: var(--font-weight-medium);
}
}
[data-slot="basic-tool-tool-subtitle"] {
@@ -107,13 +96,6 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: var(--font-family-sans);
font-variant-numeric: tabular-nums;
font-size: 14px;
font-style: normal;
font-weight: var(--font-weight-regular);
line-height: var(--line-height-large);
letter-spacing: var(--letter-spacing-normal);
color: var(--v2-text-text-muted);
&.clickable:not(.webfetch-link) {
@@ -153,13 +135,6 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: var(--font-family-sans);
font-variant-numeric: tabular-nums;
font-size: 14px;
font-style: normal;
font-weight: var(--font-weight-regular);
line-height: var(--line-height-large);
letter-spacing: var(--letter-spacing-normal);
color: var(--v2-text-text-muted);
}
@@ -177,23 +152,6 @@
[data-slot="basic-tool-tool-info-main"] {
gap: 6px;
}
[data-slot="basic-tool-tool-title"] {
font-family: var(--v2-font-family-sans);
font-size: 13px;
font-weight: 530;
/* Keep compact text on the shared metric; solid 13px line boxes clip Inter descenders. */
line-height: var(--line-height-compact);
letter-spacing: -0.04px;
}
[data-slot="basic-tool-tool-subtitle"] {
font-family: var(--v2-font-family-sans);
font-size: 13px;
font-weight: 440;
line-height: var(--line-height-compact);
letter-spacing: -0.04px;
}
}
[data-component="task-tool-card"] {
@@ -1,7 +1,6 @@
import {
createEffect,
createMemo,
For,
Match,
on,
onCleanup,
@@ -16,17 +15,9 @@ import { useI18n } from "@opencode-ai/ui/context/i18n"
import { createStore } from "solid-js/store"
import { Collapsible } from "@opencode-ai/ui/collapsible"
import type { IconProps } from "@opencode-ai/ui/icon"
import { TextShimmer } from "@opencode-ai/ui/text-shimmer"
import { ToolHeader, type ToolHeaderProps } from "./tool-header"
export type TriggerTitle = {
title: string
titleClass?: string
subtitle?: string
subtitleClass?: string
args?: string[]
argsClass?: string
action?: JSX.Element
}
export type TriggerTitle = Omit<ToolHeaderProps, "active" | "onSubtitleClick">
const isTriggerTitle = (val: unknown): val is TriggerTitle => {
if (typeof val !== "object" || val === null) return false
@@ -216,54 +207,12 @@ export function BasicTool(props: BasicToolProps) {
<Switch>
<Match when={triggerTitle()}>
{(title) => (
<div data-slot="basic-tool-tool-info-structured">
<div data-slot="basic-tool-tool-info-main">
<span
data-slot="basic-tool-tool-title"
classList={{
[title().titleClass ?? ""]: !!title().titleClass,
}}
>
<TextShimmer text={title().title} active={pending()} />
</span>
<Show when={!pending() || title().subtitle || title().args?.length}>
<Show when={title().subtitle}>
<span
data-slot="basic-tool-tool-subtitle"
classList={{
[title().subtitleClass ?? ""]: !!title().subtitleClass,
clickable: !!props.onSubtitleClick,
}}
onClick={(e) => {
if (props.onSubtitleClick) {
e.stopPropagation()
props.onSubtitleClick()
}
}}
>
{title().subtitle}
</span>
</Show>
<Show when={title().args?.length}>
<For each={title().args}>
{(arg) => (
<span
data-slot="basic-tool-tool-arg"
classList={{
[title().argsClass ?? ""]: !!title().argsClass,
}}
>
{arg}
</span>
)}
</For>
</Show>
</Show>
</div>
<Show when={!pending() && title().action}>
<span data-slot="basic-tool-tool-action">{title().action}</span>
</Show>
</div>
<ToolHeader
{...title()}
active={pending()}
onSubtitleClick={props.onSubtitleClick}
action={!pending() ? title().action : undefined}
/>
)}
</Match>
<Match when={true}>{triggerContent() as JSX.Element}</Match>
@@ -446,102 +446,6 @@
--tool-content-gap: 6px;
}
[data-component="edit-trigger"],
[data-component="write-trigger"] {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
[data-slot="message-part-title-area"] {
flex-grow: 1;
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
[data-slot="message-part-title"] {
flex: 1 1 auto;
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
font-family: var(--font-family-sans);
font-size: 14px;
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: var(--line-height-large);
letter-spacing: var(--letter-spacing-normal);
color: var(--v2-text-text-muted);
}
[data-slot="message-part-title-spinner"] {
margin-left: 4px;
width: 16px;
height: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: var(--v2-text-text-muted);
[data-component="spinner"] {
width: 16px;
height: 16px;
}
}
[data-slot="message-part-title-text"] {
flex-shrink: 0;
text-transform: capitalize;
color: var(--v2-text-text-base);
}
[data-slot="message-part-title-filename"] {
/* No text-transform - preserve original filename casing */
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: var(--font-weight-regular);
direction: ltr;
unicode-bidi: isolate;
}
[data-slot="message-part-path"] {
display: flex;
flex-grow: 1;
min-width: 0;
font-weight: var(--font-weight-regular);
direction: ltr;
unicode-bidi: isolate;
}
[data-slot="message-part-directory"] {
color: var(--v2-text-text-muted);
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
direction: rtl;
text-align: left;
}
[data-slot="message-part-filename"] {
color: var(--v2-text-text-base);
flex-shrink: 0;
}
[data-slot="message-part-actions"] {
display: flex;
gap: 16px;
align-items: center;
justify-content: flex-end;
flex-shrink: 0;
}
}
[data-component="edit-content"] {
border-radius: inherit;
border-top: 0.5px solid var(--v2-border-border-muted);
@@ -678,24 +582,6 @@
gap: 0px;
cursor: default;
[data-slot="context-tool-group-title"] {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 1;
min-width: 0;
}
[data-slot="context-tool-group-prefix"] {
flex-shrink: 0;
color: var(--v2-text-text-muted);
font-family: var(--v2-font-family-sans);
font-size: 13px;
font-weight: 440;
line-height: var(--line-height-compact);
letter-spacing: -0.04px;
}
[data-slot="basic-tool-tool-title"] {
overflow: hidden;
text-overflow: ellipsis;
@@ -740,17 +626,6 @@
gap: 6px;
}
[data-slot="basic-tool-tool-title"],
[data-slot="basic-tool-tool-subtitle"],
[data-slot="basic-tool-tool-arg"],
[data-slot="context-tool-group-matches"] {
font-family: var(--v2-font-family-sans);
font-size: 13px;
font-weight: 440;
line-height: var(--line-height-compact);
letter-spacing: -0.04px;
}
[data-slot="basic-tool-tool-subtitle"] {
color: var(--v2-text-text-faint);
}
@@ -0,0 +1,87 @@
[data-component="tool-header"] {
font-family: var(--v2-font-family-sans);
font-size: 13px;
font-style: normal;
font-weight: 440;
/* Truncated text still needs room for Inter descenders. */
line-height: var(--line-height-compact);
letter-spacing: -0.04px;
color: var(--v2-text-text-muted);
&[data-slot="basic-tool-tool-info-structured"] {
display: inline-flex;
align-items: center;
gap: 6px;
min-width: 0;
max-width: 100%;
}
[data-slot="basic-tool-tool-info-main"] {
display: flex;
align-items: baseline;
gap: 6px;
min-width: 0;
overflow: hidden;
}
[data-slot="basic-tool-tool-title"] {
flex-shrink: 0;
font: inherit;
font-weight: 530;
color: var(--v2-text-text-base);
&.capitalize {
text-transform: capitalize;
}
}
[data-slot="tool-header-affix"] {
flex-shrink: 0;
}
[data-slot="basic-tool-tool-subtitle"],
[data-slot="basic-tool-tool-arg"] {
flex-shrink: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font: inherit;
font-variant-numeric: tabular-nums;
&[dir] {
unicode-bidi: isolate;
}
&.clickable {
cursor: pointer;
text-decoration: underline;
}
}
[data-slot="tool-header-directory"] {
display: flex;
min-width: 0;
unicode-bidi: isolate;
> span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
direction: rtl;
text-align: left;
}
}
[data-slot="basic-tool-tool-action"] {
display: inline-flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.webfetch-link {
font: inherit;
letter-spacing: inherit;
}
}
@@ -0,0 +1,73 @@
import { children, For, Show, type JSX } from "solid-js"
import { TextShimmer } from "@opencode-ai/ui/text-shimmer"
export type ToolHeaderProps = {
title: string
active?: boolean
titleClass?: string
prefix?: string
suffix?: string
subtitle?: JSX.Element
subtitleClass?: string
subtitleDir?: "ltr" | "rtl"
directory?: string
args?: string[]
argsClass?: string
action?: JSX.Element
onSubtitleClick?: () => void
}
/** Shared presentation for tool rows; callers own values, status, and disclosure. */
export function ToolHeader(props: ToolHeaderProps) {
const subtitle = children(() => props.subtitle)
const action = children(() => props.action)
return (
<div data-component="tool-header" data-slot="basic-tool-tool-info-structured">
<div data-slot="basic-tool-tool-info-main">
<Show when={props.prefix}>
<span data-slot="tool-header-affix">{props.prefix}</span>
</Show>
<span data-slot="basic-tool-tool-title" class={props.titleClass}>
<Show when={props.active !== undefined} fallback={props.title}>
<TextShimmer text={props.title} active={props.active} />
</Show>
</span>
<Show when={props.suffix}>
<span data-slot="tool-header-affix">{props.suffix}</span>
</Show>
<Show when={subtitle()}>
<span
data-slot="basic-tool-tool-subtitle"
dir={props.subtitleDir}
classList={{
[props.subtitleClass ?? ""]: !!props.subtitleClass,
clickable: !!props.onSubtitleClick,
}}
onClick={(event) => {
if (!props.onSubtitleClick) return
event.stopPropagation()
props.onSubtitleClick()
}}
>
{subtitle()}
</span>
</Show>
<For each={props.args}>
{(arg) => (
<span data-slot="basic-tool-tool-arg" class={props.argsClass}>
{arg}
</span>
)}
</For>
</div>
<Show when={props.directory}>
<span data-slot="tool-header-directory" dir="ltr">
<span>{props.directory}</span>
</span>
</Show>
<Show when={action()}>
<span data-slot="basic-tool-tool-action">{action()}</span>
</Show>
</div>
)
}
@@ -14,7 +14,6 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
import { Icon } from "@opencode-ai/ui/icon"
import { Button } from "@opencode-ai/ui/button"
import { TextReveal } from "@opencode-ai/ui/text-reveal"
import { TextShimmer } from "@opencode-ai/ui/text-shimmer"
import { BasicTool } from "../components/basic-tool"
import { reasoningHeading } from "../timeline/projection"
import { Card } from "@opencode-ai/ui/card"
@@ -534,30 +533,14 @@ export function AssistantReasoningContent(props: {
props.onOpenChange?.(value)
props.onContentRendered?.()
}}
trigger={
<div data-slot="basic-tool-tool-info-structured">
<div data-slot="basic-tool-tool-info-main">
<span data-slot="basic-tool-tool-title">
<TextShimmer
text={i18n.t(props.streaming ? "ui.sessionTurn.status.thinking" : "ui.message.thought")}
active={props.streaming}
/>
</span>
<Show
when={props.streaming && !open()}
fallback={
<Show when={!props.streaming && duration()}>
{(value) => <span data-slot="basic-tool-tool-subtitle">{value()}</span>}
</Show>
}
>
<span data-slot="basic-tool-tool-subtitle">
<TextReveal text={heading()} />
</span>
</Show>
</div>
</div>
}
trigger={{
title: i18n.t(props.streaming ? "ui.sessionTurn.status.thinking" : "ui.message.thought"),
subtitle: (
<Show when={props.streaming && !open()} fallback={!props.streaming ? duration() : undefined}>
<TextReveal text={heading()} />
</Show>
),
}}
>
<PacedMarkdown text={props.content.text} cacheKey={props.id} streaming={props.streaming} />
</BasicTool>
+1
View File
@@ -1,6 +1,7 @@
@layer theme, base, components, utilities;
@import "../components/basic-tool.css" layer(components);
@import "../components/tool-header.css" layer(components);
@import "../components/file.css" layer(components);
@import "../components/markdown.css" layer(components);
@import "../components/message-part.css" layer(components);
@@ -0,0 +1,84 @@
import { createMemo, createSignal } from "solid-js"
import { CurrentSessionProviders } from "../storybook/current-session-story"
import { storyDocument, storyTool } from "../storybook/current-session-scenarios"
import { SessionTimeline } from "./session-timeline"
export default {
title: "OpenCode/Work/Tool headers",
id: "current-session-tool-headers",
component: SessionTimeline,
parameters: { layout: "fullscreen" },
}
export const SharedHeaders = {
args: { phase: "completed", pathKnown: true, longPath: false },
argTypes: { phase: { control: "select", options: ["streaming", "running", "completed"] } },
render: (args: { phase: "streaming" | "running" | "completed"; pathKnown: boolean; longPath: boolean }) => {
const [phase, setPhase] = createSignal(args.phase)
const [known, setKnown] = createSignal(args.pathKnown)
const path = (name: string) =>
args.longPath
? `src/components/session/timeline/tools/deeply/nested/directory/with/a/long/path/${"long-filename-".repeat(8)}${name}.ts`
: `src/components/${name}.ts`
const document = createMemo(() =>
storyDocument(
[
storyTool("tool_header_read", "read", phase(), { path: path("read"), offset: 12, limit: 40 }),
storyTool(
"tool_header_grep",
"grep",
phase(),
{ path: "src/components", pattern: "header", include: "*.tsx" },
{ metadata: { matches: 3 } },
),
storyTool("tool_header_shell", "shell", phase(), { command: "printf checked" }, { output: "checked" }),
storyTool(
"tool_header_execute",
"execute",
phase(),
{ code: 'console.log("checked")' },
{ output: "checked" },
),
storyTool("tool_header_webfetch", "webfetch", phase(), { url: "https://example.com/docs" }),
storyTool("tool_header_edit", "edit", phase(), {
...(known() ? { path: path("edit") } : {}),
oldString: "export const before = true\n",
newString: "export const after = true\n",
}),
storyTool("tool_header_write", "write", phase(), {
...(known() ? { path: path("write") } : {}),
content: "export const written = true\n",
}),
],
phase() !== "completed",
),
)
return (
<section class="mx-auto flex w-full max-w-[840px] flex-col gap-4 p-6">
<div class="flex flex-wrap gap-3">
<button type="button" onClick={() => setKnown(true)}>
Provide paths
</button>
<button type="button" onClick={() => setPhase("running")}>
Run tools
</button>
<button type="button" onClick={() => setPhase("completed")}>
Complete tools
</button>
<button
type="button"
onClick={() => {
setPhase(args.phase)
setKnown(args.pathKnown)
}}
>
Reset
</button>
</div>
<CurrentSessionProviders document={document()}>
<SessionTimeline document={document()} />
</CurrentSessionProviders>
</section>
)
},
}
+60 -119
View File
@@ -19,6 +19,7 @@ import { type SessionSummary, useData } from "../context"
import { useFileComponent } from "@opencode-ai/ui/context/file"
import { type UiI18n, useI18n } from "@opencode-ai/ui/context/i18n"
import { BasicTool, GenericTool } from "../components/basic-tool"
import { ToolHeader } from "../components/tool-header"
import { Accordion } from "@opencode-ai/ui/accordion"
import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
import { Collapsible } from "@opencode-ai/ui/collapsible"
@@ -560,15 +561,7 @@ export function CurrentContextToolGroup(props: {
onOpenChange={change}
trigger={
<div data-component="context-tool-group-trigger" aria-label={label().text}>
<span data-slot="context-tool-group-title">
<Show when={label().before}>
{(before) => <span data-slot="context-tool-group-prefix">{before()}</span>}
</Show>
<span data-slot="basic-tool-tool-title">{label().title}</span>
<Show when={label().after}>
{(after) => <span data-slot="context-tool-group-prefix">{after()}</span>}
</Show>
</span>
<ToolHeader title={label().title} prefix={label().before} suffix={label().after} />
</div>
}
>
@@ -689,32 +682,22 @@ export function CurrentContextToolGroup(props: {
<div data-component="tool-trigger">
<div data-slot="basic-tool-tool-trigger-content">
<div data-slot="basic-tool-tool-info">
<div data-slot="basic-tool-tool-info-structured">
<div data-slot="basic-tool-tool-info-main">
<span data-slot="basic-tool-tool-title">
<TextShimmer
text={trigger().title}
active={
tool().state.status === "streaming" || tool().state.status === "running"
}
/>
</span>
<Show when={trigger().subtitle}>
{(subtitle) => <span data-slot="basic-tool-tool-subtitle">{subtitle()}</span>}
<ToolHeader
title={trigger().title}
subtitle={trigger().subtitle}
args={trigger().args}
active={tool().state.status === "streaming" || tool().state.status === "running"}
action={
<Show when={trigger().matches}>
{(matches) => (
<>
<span data-slot="context-tool-group-dot" />
<span data-slot="context-tool-group-matches">{matches()}</span>
</>
)}
</Show>
<For each={trigger().args}>
{(arg) => <span data-slot="basic-tool-tool-arg">{arg}</span>}
</For>
</div>
<Show when={trigger().matches}>
{(matches) => (
<>
<span data-slot="context-tool-group-dot" />
<span data-slot="context-tool-group-matches">{matches()}</span>
</>
)}
</Show>
</div>
}
/>
</div>
</div>
</div>
@@ -1209,28 +1192,23 @@ ToolRegistry.register({
{...props}
hideDetails
icon="window-cursor"
trigger={
<div data-slot="basic-tool-tool-info-structured">
<div data-slot="basic-tool-tool-info-main">
<span data-slot="basic-tool-tool-title">
<TextShimmer text={i18n.t("ui.tool.webfetch")} active={pending()} />
</span>
<Show when={!pending() && url()}>
<a
data-slot="basic-tool-tool-subtitle"
class="webfetch-link"
href={url()}
target="_blank"
rel="noopener noreferrer"
onClick={(event) => event.stopPropagation()}
>
<span data-slot="webfetch-link-text">{url()}</span>
<Icon name="outline-square-arrow" class="webfetch-link-icon" />
</a>
</Show>
</div>
</div>
}
trigger={{
title: i18n.t("ui.tool.webfetch"),
subtitle: (
<Show when={!pending() && url()}>
<a
class="webfetch-link"
href={url()}
target="_blank"
rel="noopener noreferrer"
onClick={(event) => event.stopPropagation()}
>
<span data-slot="webfetch-link-text">{url()}</span>
<Icon name="outline-square-arrow" class="webfetch-link-icon" />
</a>
</Show>
),
}}
/>
)
},
@@ -1443,16 +1421,15 @@ ToolRegistry.register({
compact
allowOpenWhilePending
trigger={(open) => (
<div data-slot="basic-tool-tool-info-structured">
<div data-slot="basic-tool-tool-info-main">
<span data-slot="basic-tool-tool-title">
<TextShimmer text={i18n.t("ui.tool.execute")} active={pending()} />
</span>
<ToolHeader
title={i18n.t("ui.tool.execute")}
active={pending()}
subtitle={
<Show when={!open() && code()}>
<ShellSubmessage text={code().split("\n")[0]} animate={sawPending} />
</Show>
</div>
</div>
}
/>
)}
>
<ConsoleOutput copy={code()} variant="shell">
@@ -1532,25 +1509,20 @@ ToolRegistry.register({
compact
allowOpenWhilePending
trigger={(open) => (
<div data-slot="basic-tool-tool-info-structured">
<div data-slot="basic-tool-tool-info-main">
<span data-slot="basic-tool-tool-title">
<TextShimmer text={i18n.t("ui.tool.shell")} active={pending()} />
</span>
<ToolHeader
title={i18n.t("ui.tool.shell")}
active={pending()}
subtitle={
<Show when={!open()}>
<Show
when={command()}
fallback={
<Show when={streaming()}>
<span data-slot="basic-tool-tool-subtitle">{i18n.t("ui.tool.shell.writingCommand")}</span>
</Show>
}
fallback={<Show when={streaming()}>{i18n.t("ui.tool.shell.writingCommand")}</Show>}
>
{(command) => <ShellSubmessage text={command()} animate={sawStreaming} />}
</Show>
</Show>
</div>
</div>
}
/>
)}
>
<ConsoleOutput copy={command()} variant="shell">
@@ -1663,30 +1635,13 @@ ToolRegistry.register({
icon="code-lines"
rail={false}
defer={props.deferContent !== false}
trigger={
<div data-component="edit-trigger">
<div data-slot="message-part-title-area">
<div data-slot="message-part-title">
<span data-slot="message-part-title-text">
<TextShimmer text={i18n.t("ui.messagePart.title.edit")} active={pending()} />
</span>
<Show when={!pending()}>
<span data-slot="message-part-title-filename">{filename()}</span>
</Show>
</div>
<Show when={!pending() && inputPath().includes("/")}>
<div data-slot="message-part-path">
<span data-slot="message-part-directory">{displayDirectory(inputPath())}</span>
</div>
</Show>
</div>
<div data-slot="message-part-actions">
<Show when={!pending() ? diff() : undefined}>
{(diff) => <DiffChanges appearance="standard" changes={diff()} />}
</Show>
</div>
</div>
}
trigger={{
title: i18n.t("ui.messagePart.title.edit"),
subtitle: !pending() ? filename() : undefined,
subtitleDir: "ltr",
directory: !pending() && inputPath().includes("/") ? displayDirectory(inputPath()) : undefined,
action: <Show when={diff()}>{(diff) => <DiffChanges appearance="standard" changes={diff()} />}</Show>,
}}
>
<Show when={path()}>
<ToolFileAccordion
@@ -1732,26 +1687,12 @@ ToolRegistry.register({
icon="code-lines"
rail={false}
defer={props.deferContent !== false}
trigger={
<div data-component="write-trigger">
<div data-slot="message-part-title-area">
<div data-slot="message-part-title">
<span data-slot="message-part-title-text">
<TextShimmer text={i18n.t("ui.messagePart.title.write")} active={pending()} />
</span>
<Show when={!pending()}>
<span data-slot="message-part-title-filename">{filename()}</span>
</Show>
</div>
<Show when={!pending() && path().includes("/")}>
<div data-slot="message-part-path">
<span data-slot="message-part-directory">{displayDirectory(path())}</span>
</div>
</Show>
</div>
<div data-slot="message-part-actions">{/* <DiffChanges diff={diff} /> */}</div>
</div>
}
trigger={{
title: i18n.t("ui.messagePart.title.write"),
subtitle: !pending() ? filename() : undefined,
subtitleDir: "ltr",
directory: !pending() && path().includes("/") ? displayDirectory(path()) : undefined,
}}
>
<Show when={content() && path()}>
<ToolFileAccordion path={path()}>