Compare commits

..
51 Commits
Author SHA1 Message Date
Brendan Allan bdec7a4f87 fix(app): lighten background move hint (#44217) 2026-08-23 01:25:02 +08:00
opencode-agent[bot] d35f81ef92 chore: update nix node_modules hashes 2026-08-22 17:03:24 +00:00
Brendan Allan d0d0ada4a9 feat(app): cache mobile web startup assets (#44205) 2026-08-22 16:46:20 +00:00
opencode-agent[bot]andBrendonovich beb6216343 fix(ui): constrain focus trap to top dialog (#44188)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
2026-08-22 23:37:37 +08:00
Brendan Allan d9c9c50809 fix(app): pin background hint to timeline spacer (#44187) 2026-08-22 23:31:30 +08:00
opencode-agent[bot]andBrendonovich a7106ae023 fix(app): clear terminal line with command delete (#44183)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
2026-08-22 14:56:14 +00:00
Brendan Allan 2ae3b82c24 feat(app): add bottom terminal panel (#44175) 2026-08-22 22:52:26 +08:00
opencode-agent[bot] 784042202e chore: update nix node_modules hashes 2026-08-22 14:25:38 +00:00
opencode-agent[bot]andBrendonovich 320da4256d fix(app): preserve home during server switch (#44178)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
2026-08-22 22:22:55 +08:00
Brendan Allan 2bbaeb0d4b fix(app): simplify timeline anchoring (#44147) 2026-08-22 14:09:00 +00:00
Brendan Allan ada4f1bee6 feat(app): merge repeated file edits (#44107) 2026-08-22 21:49:09 +08:00
opencode-agent[bot]andjlongster 75efc9d833 fix(tui): seed moved worktree location (#44143)
Co-authored-by: jlongster <17031+jlongster@users.noreply.github.com>
2026-08-22 09:07:27 -04:00
Brendan Allan 93e1f383dd fix(app): allow selecting diff text (#44140) 2026-08-22 20:34:47 +08:00
opencode-agent[bot]andBrendonovich e4924ebf64 feat(app): add copy project ID command (#44126)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
2026-08-22 19:57:47 +08:00
opencode-agent[bot]andBrendonovich 992446e85d feat(app): add copy session ID command (#44125)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
2026-08-22 19:52:04 +08:00
Brendan Allan b94ba27f00 feat(app): fade the background move hint (#44109) 2026-08-22 19:50:27 +08:00
Brendan Allan 8a2966fdbe fix(app): avoid duplicate retry errors (#44124) 2026-08-22 19:49:22 +08:00
Brendan Allan 4aca1be945 fix(app): prevent duplicate web mounts (#44119) 2026-08-22 19:38:25 +08:00
Luke Parker 100f23ca99 feat(app): show writing command state (#44095) 2026-08-22 20:21:33 +10:00
Brendan Allan 8f54597ef2 fix(app): collapse failed patch cards (#44105) 2026-08-22 18:05:23 +08:00
OpeOginniandBrendan Allan 85f32fa0da fix(app): load workspace sessions by directory (#44027)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
2026-08-22 07:50:08 +00:00
Luke Parker 8062b5455a feat(app): align moved location notice (#44085) 2026-08-22 17:30:42 +10:00
Luke Parker 4566395d42 feat(app): show subagent delegation state (#44078) 2026-08-22 17:03:32 +10:00
opencode-agent[bot]andHona 3c70f6df28 fix(app): enforce single titlebar action owner (#44075)
Co-authored-by: Hona <10430890+Hona@users.noreply.github.com>
2026-08-22 17:03:24 +10:00
Brendan Allan 97536add75 fix(app): preserve bottom scroll anchoring (#44077) 2026-08-22 14:53:01 +08:00
Brendan Allan 6258c1943a feat(app): group adjacent patch calls (#43901) 2026-08-22 06:51:53 +00:00
Luke Parker 0d2c865c93 feat(app): move background work to session summary (#44068) 2026-08-22 16:31:53 +10:00
Luke Parker 7032a096bf fix(desktop): configure app before instance lock (#44067) 2026-08-22 16:09:13 +10:00
Luke Parker 80ef4f454f feat(desktop): align loaded resource calls with figma (#44060) 2026-08-22 15:17:11 +10:00
Luke Parker aa2dd5040f fix(desktop): omit shared rail from framed disclosures (#44061) 2026-08-22 15:17:09 +10:00
Luke Parker 9b17449e88 fix(desktop): match instructions notice typography to figma (#44059) 2026-08-22 15:06:13 +10:00
Luke Parker 346689fb43 fix(storybook): use production styles and remove synthetic tool stories (#44058) 2026-08-22 15:05:51 +10:00
Luke Parker 87d2488c78 feat(desktop): apply left border rail styling to all tool disclosures (#44052) 2026-08-22 14:23:18 +10:00
Aiden ClineandMajor Hayden a1d0f43531 fix(ai): preserve unique Gemini function call IDs (#44051)
Signed-off-by: Major Hayden <major@mhtx.net>
Co-authored-by: Major Hayden <major@mhtx.net>
2026-08-21 23:12:04 -05:00
Luke Parker 030b2d9543 feat(desktop): align instructions updated notice presentation with figma (#44050) 2026-08-22 13:49:24 +10:00
Luke Parker e7177a8764 fix(desktop): align web search results presentation with figma (#44039) 2026-08-22 13:22:22 +10:00
opencode-agent[bot]andHona a84b1c15ce fix(app): wait for session route id (#44048)
Co-authored-by: Hona <10430890+Hona@users.noreply.github.com>
2026-08-22 13:22:10 +10:00
opencode-agent[bot]andHona d3eee25ee2 fix(session-ui): scope context row keys (#44043)
Co-authored-by: Hona <10430890+Hona@users.noreply.github.com>
2026-08-22 02:57:53 +00:00
Luke Parker b4fabf5984 fix(desktop): align webfetch link styling with figma (#44037) 2026-08-22 02:29:03 +00:00
Kit Langton e33d688428 fix(codegen): stabilize generated contract names (#44000) 2026-08-22 00:16:27 +00:00
Kit Langton f69f78ec6b refactor(core): inline portable shell scanner (#44026) 2026-08-22 00:06:03 +00:00
opencode-agent[bot] e2758b9945 chore: update nix node_modules hashes 2026-08-21 23:53:08 +00:00
Kit Langton 7c6ecaaca8 fix(core): inherit fork instruction entries (#44004) 2026-08-21 19:46:49 -04:00
Kit Langton c29a7c152d feat(session): add viewed state (#42811) 2026-08-21 19:34:35 -04:00
Kit Langton fa4c5b26dc fix(core): stabilize forked message IDs (#44011) 2026-08-21 19:28:51 -04:00
Kit Langton fb703ede73 fix(core): transfer only settled history (#44008) 2026-08-21 19:28:04 -04:00
Kit Langton 2937f0e635 fix(core): omit running shells from forks (#44001) 2026-08-21 19:27:13 -04:00
Kit Langton 3694149135 fix(core): resolve device verification URL (#44021) 2026-08-21 23:26:40 +00:00
Kit Langton c4eeefe0f1 refactor: reuse undefined effect (#44010) 2026-08-21 19:17:29 -04:00
Kit Langton 667c274c7f fix(tui): contain MCP sidebar errors (#44003) 2026-08-21 23:00:06 +00:00
Kit Langton 2c8e2a2b28 fix(core): retry recreated locations (#44005) 2026-08-21 22:50:38 +00:00
198 changed files with 21367 additions and 17341 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@opencode-ai/core": patch
---
Fix OpenCode Console device authorization URLs when the server returns an origin-rooted verification path.
+301 -45
View File
@@ -107,6 +107,7 @@
"typescript": "catalog:",
"vite": "8.2.1",
"vite-plugin-icons-spritesheet": "3.0.1",
"vite-plugin-pwa": "1.3.0",
"vite-plugin-solid": "2.11.14",
},
},
@@ -365,7 +366,6 @@
"@opencode-ai/codemode": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/schema": "workspace:*",
"@opencode-ai/shell-scan": "workspace:*",
"@opencode-ai/util": "workspace:*",
"@parcel/watcher": "2.5.1",
"@silvia-odwyer/photon-node": "0.3.4",
@@ -630,6 +630,7 @@
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"@typescript/native-preview": "catalog:",
"prettier": "3.6.2",
"typescript": "catalog:",
},
},
@@ -734,15 +735,6 @@
"vite": "catalog:",
},
},
"packages/shell-scan": {
"name": "@opencode-ai/shell-scan",
"version": "0.0.0",
"devDependencies": {
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"@typescript/native-preview": "catalog:",
},
},
"packages/simulation": {
"name": "@opencode-ai/simulation",
"version": "1.17.13",
@@ -1059,7 +1051,7 @@
],
"patchedDependencies": {
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
"@tanstack/virtual-core@3.17.8": "patches/@tanstack%2Fvirtual-core@3.17.8.patch",
"drizzle-orm@1.0.0-rc.2": "patches/drizzle-orm@1.0.0-rc.2.patch",
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
@@ -1109,7 +1101,7 @@
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
"@standard-schema/spec": "1.1.0",
"@tailwindcss/vite": "4.1.11",
"@tanstack/solid-virtual": "3.13.32",
"@tanstack/solid-virtual": "3.13.37",
"@tsconfig/bun": "1.0.9",
"@tsconfig/node22": "22.0.2",
"@types/bun": "1.3.13",
@@ -1225,6 +1217,8 @@
"@anycable/core": ["@anycable/core@0.9.2", "", { "dependencies": { "nanoevents": "^7.0.1" } }, "sha512-x5ZXDcW/N4cxWl93CnbHs/u7qq4793jS2kNPWm+duPrXlrva+ml2ZGT7X9tuOBKzyIHf60zWCdIK7TUgMPAwXA=="],
"@apideck/better-ajv-errors": ["@apideck/better-ajv-errors@0.3.7", "", { "dependencies": { "jsonpointer": "^5.0.1", "leven": "^3.1.0" }, "peerDependencies": { "ajv": ">=8" } }, "sha512-TajUJwGWbDwkCx/CZi7tRE8PVB7simCvKJfHUsSdvps+aTM/PDPP4gkLmKnc+x3CE//y9i/nj74GqdL/hwk7Iw=="],
"@ast-grep/cli": ["@ast-grep/cli@0.44.0", "", { "dependencies": { "detect-libc": "2.1.2" }, "optionalDependencies": { "@ast-grep/cli-darwin-arm64": "0.44.0", "@ast-grep/cli-darwin-x64": "0.44.0", "@ast-grep/cli-linux-arm64-gnu": "0.44.0", "@ast-grep/cli-linux-x64-gnu": "0.44.0", "@ast-grep/cli-win32-arm64-msvc": "0.44.0", "@ast-grep/cli-win32-ia32-msvc": "0.44.0", "@ast-grep/cli-win32-x64-msvc": "0.44.0" }, "bin": { "sg": "sg", "ast-grep": "ast-grep" } }, "sha512-Jf4PuP7XjzsMa3m9gYxmzV8KyWZc4w1ZzKe/t0+90wWxmSasQJe6AtMkJxHEi98MGgfAF1nWziqjDd0/6EsBjA=="],
"@ast-grep/cli-darwin-arm64": ["@ast-grep/cli-darwin-arm64@0.44.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bF7euu/hF/cYg4510z8110vh60rrqfrBdsfRqVGd6xqNSPENu7CJnTVN/Z4Nk5U1NM8YKzUD+dYx1ySUJ0CUNQ=="],
@@ -1279,11 +1273,11 @@
"@astrojs/mdx": ["@astrojs/mdx@7.0.5", "", { "dependencies": { "@astrojs/internal-helpers": "0.10.2", "@astrojs/markdown-remark": "7.2.2", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.16.0", "es-module-lexer": "^2.0.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "piccolore": "^0.1.3", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.6", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3" }, "peerDependencies": { "@astrojs/markdown-satteri": "^0.3.1", "astro": "^7.0.0" }, "optionalPeers": ["@astrojs/markdown-satteri"] }, "sha512-wEM/HH1RiEntyPVagdiF+yArzfcYLKBB0C1RZspVidKZ97rRMbaqP1Nbl/GR0sJs8zwaceqxRymw8aOKKJRdYw=="],
"@astrojs/node": ["@astrojs/node@11.1.2", "", { "dependencies": { "@astrojs/internal-helpers": "0.10.2", "send": "^1.2.1", "server-destroy": "^1.0.1" }, "peerDependencies": { "astro": "^7.0.0" } }, "sha512-lbOjylG18s8wURjT9teqa/SPXVtVpBgm9vccEg/BXlrW3ktqqgFv23vtOswEfluDmx2Z74ngyGE6hrGC14qWow=="],
"@astrojs/node": ["@astrojs/node@11.1.4", "", { "dependencies": { "@astrojs/internal-helpers": "0.10.4", "send": "^1.2.1", "server-destroy": "^1.0.1" }, "peerDependencies": { "astro": "^7.2.1" } }, "sha512-R5tNIHm5ihyEYa0w1mLlfwLpwF1ArRmzHD/is5PUHYRY43G1Xm0k3D2HRJXgjey8/7+1DvDMJLUeBxXwgAyNDg=="],
"@astrojs/prism": ["@astrojs/prism@3.2.0", "", { "dependencies": { "prismjs": "^1.29.0" } }, "sha512-GilTHKGCW6HMq7y3BUv9Ac7GMe/MO9gi9GW62GzKtth0SwukCu/qp2wLiGpEujhY+VVhaG9v7kv/5vFzvf4NYw=="],
"@astrojs/react": ["@astrojs/react@6.0.3", "", { "dependencies": { "@astrojs/internal-helpers": "0.10.3", "@vitejs/plugin-react": "^5.2.0", "devalue": "^5.8.1", "ultrahtml": "^1.6.0", "vite": "^8.0.13" }, "peerDependencies": { "@types/react": "^17.0.50 || ^18.0.21 || ^19.0.0", "@types/react-dom": "^17.0.17 || ^18.0.6 || ^19.0.0", "react": "^17.0.2 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0" } }, "sha512-u4jhlM3D6YyeUgL0EJbyRHPlP0JZ4bN+97vOO2+QmCwf/IYvQJwnSVwn1T3k82c4JkYJU4Snd2v4aBMlGsWirg=="],
"@astrojs/react": ["@astrojs/react@6.0.4", "", { "dependencies": { "@astrojs/internal-helpers": "0.10.4", "@vitejs/plugin-react": "^5.2.0", "devalue": "^5.8.1", "ultrahtml": "^1.6.0", "vite": "^8.0.13" }, "peerDependencies": { "@types/react": "^17.0.50 || ^18.0.21 || ^19.0.0", "@types/react-dom": "^17.0.17 || ^18.0.6 || ^19.0.0", "react": "^17.0.2 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0" } }, "sha512-0PDs0gM00vbUnw84Dh+YaxYaZzlppyzJub0J8wOO8/8AG4gV8L8X1ttiRp8cbSBGiWcu3UuspCaqIDO31ta8Uw=="],
"@astrojs/sitemap": ["@astrojs/sitemap@3.7.3", "", { "dependencies": { "sitemap": "^9.0.0", "stream-replace-string": "^2.0.0", "zod": "^4.3.6" } }, "sha512-f8euLVsyeAmAkSm/1M2Kb8sL8byQmfgbvBNaHFItCheTj/IpiJYSEWVcqDHZ/yEHxiS7+w87mQkzwZaPHmk5GA=="],
@@ -1295,7 +1289,7 @@
"@astrojs/underscore-redirects": ["@astrojs/underscore-redirects@1.0.3", "", {}, "sha512-cxnGSw+sJigBLdX4TMSZKkzV6C3gMLJMucDk2W+n281Xhie68T2/9f1+1NMNDCZsc5i0FED7Qt5I10g2O9wtZg=="],
"@astrojs/vercel": ["@astrojs/vercel@11.0.6", "", { "dependencies": { "@astrojs/internal-helpers": "0.10.3", "@vercel/analytics": "^1.6.1", "@vercel/functions": "^3.4.3", "@vercel/nft": "^1.3.2", "@vercel/routing-utils": "^5.3.3", "esbuild": "^0.28.0", "tinyglobby": "^0.2.15" }, "peerDependencies": { "astro": "^7.0.0" } }, "sha512-GW+2Sw0qWnpuAa2dFqid8nmdSPWw3ZhF+GXxLZkyAl//H+bzNsG0N5kYXO9UU88jtDH91igYqQE1oUq1AJrOZA=="],
"@astrojs/vercel": ["@astrojs/vercel@11.0.7", "", { "dependencies": { "@astrojs/internal-helpers": "0.10.4", "@vercel/analytics": "^1.6.1", "@vercel/functions": "^3.4.3", "@vercel/nft": "^1.3.2", "@vercel/routing-utils": "^5.3.3", "esbuild": "^0.28.0", "tinyglobby": "^0.2.15" }, "peerDependencies": { "astro": "^7.0.0" } }, "sha512-Kq9X4sEffNgmmqlFAaLzuWdMJBAz/aGfHRUpx6Q4ljX9roJBXvUnifbABho4V/raBg8fr765Yy9HvxbCtUvcpQ=="],
"@astrojs/yaml2ts": ["@astrojs/yaml2ts@0.2.4", "", { "dependencies": { "yaml": "^2.8.3" } }, "sha512-8oddpOae35pJsXPQXhTkM0ypfKPskVsh2bCxRtbf7e+/Epw2nReakFYpLKjZMEr75CsoF203PMnCocpfz0s69A=="],
@@ -1443,6 +1437,10 @@
"@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg=="],
"@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg=="],
"@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.8", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "debug": "^4.4.3", "lodash.debounce": "^4.0.8", "resolve": "^1.22.11" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA=="],
"@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
"@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg=="],
@@ -1455,6 +1453,8 @@
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="],
"@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-wrap-function": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og=="],
"@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.29.7", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ=="],
"@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ=="],
@@ -1465,24 +1465,148 @@
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="],
"@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw=="],
"@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="],
"@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="],
"@babel/plugin-bugfix-firefox-class-in-computed-class-key": ["@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w=="],
"@babel/plugin-bugfix-safari-class-field-initializer-scope": ["@babel/plugin-bugfix-safari-class-field-initializer-scope@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ=="],
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ["@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ=="],
"@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": ["@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA=="],
"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ["@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/plugin-transform-optional-chaining": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.13.0" } }, "sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw=="],
"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": ["@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw=="],
"@babel/plugin-proposal-private-property-in-object": ["@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w=="],
"@babel/plugin-syntax-import-assertions": ["@babel/plugin-syntax-import-assertions@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw=="],
"@babel/plugin-syntax-import-attributes": ["@babel/plugin-syntax-import-attributes@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg=="],
"@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A=="],
"@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA=="],
"@babel/plugin-syntax-unicode-sets-regex": ["@babel/plugin-syntax-unicode-sets-regex@7.18.6", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg=="],
"@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ=="],
"@babel/plugin-transform-async-generator-functions": ["@babel/plugin-transform-async-generator-functions@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-remap-async-to-generator": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA=="],
"@babel/plugin-transform-async-to-generator": ["@babel/plugin-transform-async-to-generator@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-remap-async-to-generator": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w=="],
"@babel/plugin-transform-block-scoped-functions": ["@babel/plugin-transform-block-scoped-functions@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA=="],
"@babel/plugin-transform-block-scoping": ["@babel/plugin-transform-block-scoping@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ=="],
"@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.29.7", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA=="],
"@babel/plugin-transform-class-static-block": ["@babel/plugin-transform-class-static-block@7.29.7", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.12.0" } }, "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A=="],
"@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g=="],
"@babel/plugin-transform-computed-properties": ["@babel/plugin-transform-computed-properties@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/template": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA=="],
"@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg=="],
"@babel/plugin-transform-dotall-regex": ["@babel/plugin-transform-dotall-regex@7.29.7", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ=="],
"@babel/plugin-transform-duplicate-keys": ["@babel/plugin-transform-duplicate-keys@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ=="],
"@babel/plugin-transform-duplicate-named-capturing-groups-regex": ["@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.7", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA=="],
"@babel/plugin-transform-dynamic-import": ["@babel/plugin-transform-dynamic-import@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg=="],
"@babel/plugin-transform-explicit-resource-management": ["@babel/plugin-transform-explicit-resource-management@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/plugin-transform-destructuring": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw=="],
"@babel/plugin-transform-exponentiation-operator": ["@babel/plugin-transform-exponentiation-operator@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ=="],
"@babel/plugin-transform-export-namespace-from": ["@babel/plugin-transform-export-namespace-from@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA=="],
"@babel/plugin-transform-for-of": ["@babel/plugin-transform-for-of@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ=="],
"@babel/plugin-transform-function-name": ["@babel/plugin-transform-function-name@7.29.7", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg=="],
"@babel/plugin-transform-json-strings": ["@babel/plugin-transform-json-strings@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg=="],
"@babel/plugin-transform-literals": ["@babel/plugin-transform-literals@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw=="],
"@babel/plugin-transform-logical-assignment-operators": ["@babel/plugin-transform-logical-assignment-operators@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q=="],
"@babel/plugin-transform-member-expression-literals": ["@babel/plugin-transform-member-expression-literals@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg=="],
"@babel/plugin-transform-modules-amd": ["@babel/plugin-transform-modules-amd@7.29.7", "", { "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew=="],
"@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.29.7", "", { "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ=="],
"@babel/plugin-transform-modules-systemjs": ["@babel/plugin-transform-modules-systemjs@7.29.8", "", { "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.8" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6iSnEK0zlkLKU4heofK/AdmRD4e2SHVpJMtrwnTCzhnaM98ria4rTrOXBBi45BTTYnJtO8txnPsX4fChYXkmeA=="],
"@babel/plugin-transform-modules-umd": ["@babel/plugin-transform-modules-umd@7.29.7", "", { "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA=="],
"@babel/plugin-transform-named-capturing-groups-regex": ["@babel/plugin-transform-named-capturing-groups-regex@7.29.7", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ=="],
"@babel/plugin-transform-new-target": ["@babel/plugin-transform-new-target@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A=="],
"@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg=="],
"@babel/plugin-transform-numeric-separator": ["@babel/plugin-transform-numeric-separator@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw=="],
"@babel/plugin-transform-object-rest-spread": ["@babel/plugin-transform-object-rest-spread@7.29.7", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/plugin-transform-destructuring": "^7.29.7", "@babel/plugin-transform-parameters": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A=="],
"@babel/plugin-transform-object-super": ["@babel/plugin-transform-object-super@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA=="],
"@babel/plugin-transform-optional-catch-binding": ["@babel/plugin-transform-optional-catch-binding@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng=="],
"@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ=="],
"@babel/plugin-transform-parameters": ["@babel/plugin-transform-parameters@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g=="],
"@babel/plugin-transform-private-methods": ["@babel/plugin-transform-private-methods@7.29.7", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug=="],
"@babel/plugin-transform-private-property-in-object": ["@babel/plugin-transform-private-property-in-object@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA=="],
"@babel/plugin-transform-property-literals": ["@babel/plugin-transform-property-literals@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA=="],
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw=="],
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q=="],
"@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.29.8", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0UpIXPtdDtMXfnV2OJAVMLpj3H/92vmkA6lpSRakmycJvj3VUy6Xs1dM8tXRugupykr5WB+LpiVl0J8LMVg2mg=="],
"@babel/plugin-transform-regexp-modifiers": ["@babel/plugin-transform-regexp-modifiers@7.29.7", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ=="],
"@babel/plugin-transform-reserved-words": ["@babel/plugin-transform-reserved-words@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA=="],
"@babel/plugin-transform-shorthand-properties": ["@babel/plugin-transform-shorthand-properties@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg=="],
"@babel/plugin-transform-spread": ["@babel/plugin-transform-spread@7.29.8", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-4S9ksMGVWUshvgK0mKfvZky7leuG5/uoFVwMpAomJ8bMoDJiNHRVmc1EglwW/CmGVSqqWpEbXm9FmbRit22qoA=="],
"@babel/plugin-transform-sticky-regex": ["@babel/plugin-transform-sticky-regex@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA=="],
"@babel/plugin-transform-template-literals": ["@babel/plugin-transform-template-literals@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA=="],
"@babel/plugin-transform-typeof-symbol": ["@babel/plugin-transform-typeof-symbol@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A=="],
"@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/plugin-syntax-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw=="],
"@babel/plugin-transform-unicode-escapes": ["@babel/plugin-transform-unicode-escapes@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA=="],
"@babel/plugin-transform-unicode-property-regex": ["@babel/plugin-transform-unicode-property-regex@7.29.7", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw=="],
"@babel/plugin-transform-unicode-regex": ["@babel/plugin-transform-unicode-regex@7.29.7", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA=="],
"@babel/plugin-transform-unicode-sets-regex": ["@babel/plugin-transform-unicode-sets-regex@7.29.7", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg=="],
"@babel/preset-env": ["@babel/preset-env@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.29.7", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.29.7", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.29.7", "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.7", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.29.7", "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.29.7", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-import-assertions": "^7.29.7", "@babel/plugin-syntax-import-attributes": "^7.29.7", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.29.7", "@babel/plugin-transform-async-generator-functions": "^7.29.7", "@babel/plugin-transform-async-to-generator": "^7.29.7", "@babel/plugin-transform-block-scoped-functions": "^7.29.7", "@babel/plugin-transform-block-scoping": "^7.29.7", "@babel/plugin-transform-class-properties": "^7.29.7", "@babel/plugin-transform-class-static-block": "^7.29.7", "@babel/plugin-transform-classes": "^7.29.7", "@babel/plugin-transform-computed-properties": "^7.29.7", "@babel/plugin-transform-destructuring": "^7.29.7", "@babel/plugin-transform-dotall-regex": "^7.29.7", "@babel/plugin-transform-duplicate-keys": "^7.29.7", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.7", "@babel/plugin-transform-dynamic-import": "^7.29.7", "@babel/plugin-transform-explicit-resource-management": "^7.29.7", "@babel/plugin-transform-exponentiation-operator": "^7.29.7", "@babel/plugin-transform-export-namespace-from": "^7.29.7", "@babel/plugin-transform-for-of": "^7.29.7", "@babel/plugin-transform-function-name": "^7.29.7", "@babel/plugin-transform-json-strings": "^7.29.7", "@babel/plugin-transform-literals": "^7.29.7", "@babel/plugin-transform-logical-assignment-operators": "^7.29.7", "@babel/plugin-transform-member-expression-literals": "^7.29.7", "@babel/plugin-transform-modules-amd": "^7.29.7", "@babel/plugin-transform-modules-commonjs": "^7.29.7", "@babel/plugin-transform-modules-systemjs": "^7.29.7", "@babel/plugin-transform-modules-umd": "^7.29.7", "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.7", "@babel/plugin-transform-new-target": "^7.29.7", "@babel/plugin-transform-nullish-coalescing-operator": "^7.29.7", "@babel/plugin-transform-numeric-separator": "^7.29.7", "@babel/plugin-transform-object-rest-spread": "^7.29.7", "@babel/plugin-transform-object-super": "^7.29.7", "@babel/plugin-transform-optional-catch-binding": "^7.29.7", "@babel/plugin-transform-optional-chaining": "^7.29.7", "@babel/plugin-transform-parameters": "^7.29.7", "@babel/plugin-transform-private-methods": "^7.29.7", "@babel/plugin-transform-private-property-in-object": "^7.29.7", "@babel/plugin-transform-property-literals": "^7.29.7", "@babel/plugin-transform-regenerator": "^7.29.7", "@babel/plugin-transform-regexp-modifiers": "^7.29.7", "@babel/plugin-transform-reserved-words": "^7.29.7", "@babel/plugin-transform-shorthand-properties": "^7.29.7", "@babel/plugin-transform-spread": "^7.29.7", "@babel/plugin-transform-sticky-regex": "^7.29.7", "@babel/plugin-transform-template-literals": "^7.29.7", "@babel/plugin-transform-typeof-symbol": "^7.29.7", "@babel/plugin-transform-unicode-escapes": "^7.29.7", "@babel/plugin-transform-unicode-property-regex": "^7.29.7", "@babel/plugin-transform-unicode-regex": "^7.29.7", "@babel/plugin-transform-unicode-sets-regex": "^7.29.7", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.15", "babel-plugin-polyfill-corejs3": "^0.14.0", "babel-plugin-polyfill-regenerator": "^0.6.6", "core-js-compat": "^3.48.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA=="],
"@babel/preset-modules": ["@babel/preset-modules@0.1.6-no-external-plugins", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/types": "^7.4.4", "esutils": "^2.0.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA=="],
"@babel/preset-typescript": ["@babel/preset-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ=="],
"@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="],
@@ -1893,7 +2017,7 @@
"@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="],
"@mermaid-js/parser": ["@mermaid-js/parser@1.2.0", "", { "dependencies": { "@chevrotain/types": "~11.1.2" } }, "sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA=="],
"@mermaid-js/parser": ["@mermaid-js/parser@1.2.1", "", { "dependencies": { "@chevrotain/types": "~11.1.2" } }, "sha512-n12NohV3mrUyUL2o93IgG/ifeW9FTyeJn3zDxkhwa8MJ9Fxg3HQMlA3RiGmD/3UnJvheztkjjQAjA2T4LmUcpw=="],
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
@@ -2063,8 +2187,6 @@
"@opencode-ai/session-ui": ["@opencode-ai/session-ui@workspace:packages/session-ui"],
"@opencode-ai/shell-scan": ["@opencode-ai/shell-scan@workspace:packages/shell-scan"],
"@opencode-ai/simulation": ["@opencode-ai/simulation@workspace:packages/simulation"],
"@opencode-ai/stats-app": ["@opencode-ai/stats-app@workspace:packages/stats/app"],
@@ -2221,7 +2343,7 @@
"@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.127.0", "", { "os": "win32", "cpu": "x64" }, "sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w=="],
"@oxc-project/types": ["@oxc-project/types@0.144.0", "", {}, "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg=="],
"@oxc-project/types": ["@oxc-project/types@0.146.0", "", {}, "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA=="],
"@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.24.2", "", { "os": "android", "cpu": "arm" }, "sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA=="],
@@ -2515,36 +2637,46 @@
"@remix-run/router": ["@remix-run/router@1.9.0", "", {}, "sha512-bV63itrKBC0zdT27qYm6SDZHlkXwFL1xMBuhkn+X7l0+IIhNaH5wuuvZKp6eKhCD4KFhujhfhCT1YxXW6esUIA=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.4", "", { "os": "android", "cpu": "arm64" }, "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA=="],
"@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.5", "", { "os": "android", "cpu": "arm" }, "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.5", "", { "os": "android", "cpu": "arm64" }, "sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.5", "", { "os": "linux", "cpu": "arm" }, "sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.4", "", { "os": "none", "cpu": "arm64" }, "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.5", "", { "os": "linux", "cpu": "x64" }, "sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.5", "", { "os": "none", "cpu": "arm64" }, "sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.5", "", { "os": "win32", "cpu": "x64" }, "sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
"@rollup/plugin-babel": ["@rollup/plugin-babel@6.1.0", "", { "dependencies": { "@babel/helper-module-imports": "^7.18.6", "@rollup/pluginutils": "^5.0.1" }, "peerDependencies": { "@babel/core": "^7.0.0", "@types/babel__core": "^7.1.9", "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["@types/babel__core", "rollup"] }, "sha512-dFZNuFD2YRcoomP4oYf+DvQNSUA9ih+A3vUqopQx5EdtPGo3WBnQcI/S8pwpz91UsGfL0HsMSOlaMld8HrbubA=="],
"@rollup/plugin-node-resolve": ["@rollup/plugin-node-resolve@16.0.3", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", "is-module": "^1.0.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.78.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg=="],
"@rollup/plugin-replace": ["@rollup/plugin-replace@6.0.3", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "magic-string": "^0.30.3" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA=="],
"@rollup/plugin-terser": ["@rollup/plugin-terser@1.0.0", "", { "dependencies": { "serialize-javascript": "^7.0.3", "smob": "^1.0.0", "terser": "^5.17.4" }, "peerDependencies": { "rollup": "^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ=="],
"@rollup/pluginutils": ["@rollup/pluginutils@5.4.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.4", "", { "os": "android", "cpu": "arm" }, "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg=="],
@@ -2949,9 +3081,9 @@
"@tanstack/solid-query": ["@tanstack/solid-query@5.91.4", "", { "dependencies": { "@tanstack/query-core": "5.91.2" }, "peerDependencies": { "solid-js": "^1.6.0" } }, "sha512-oCEgn8iT7WnF/7ISd7usBpUK1C9EdvQfg8ZUpKNKZ4edVClICZrCX6f3/Bp8ZlwQnL21KLc2rp+CejEuehlRxg=="],
"@tanstack/solid-virtual": ["@tanstack/solid-virtual@3.13.32", "", { "dependencies": { "@tanstack/virtual-core": "3.17.3" }, "peerDependencies": { "solid-js": "^1.3.0" } }, "sha512-yhX4A4Kgn+wyTg6Mmu8+zwoMTwjz4K1ucvLfRJ8f0rPGDDAIqSaf0v6oU0yT9+SvrjmUaZQ0VX7g4byexbhNng=="],
"@tanstack/solid-virtual": ["@tanstack/solid-virtual@3.13.37", "", { "dependencies": { "@tanstack/virtual-core": "3.17.8" }, "peerDependencies": { "solid-js": "^1.3.0" } }, "sha512-RxJC8n/6nnFHcPvDGMg7ZkqSzCVUco4JNw3ctIChj1HjoKs5I9wt5nsU+DUVtMVEiMfizUxOAx4TPIN8SHjxUQ=="],
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.3", "", {}, "sha512-8Np/TFELpI0ySuJoVmjvOrQYXH/8sTX0Biv9szhFhY39xOdAAY+smrMxjxOum/ux3eM8MUJQsEJ0/R0UpvC8dw=="],
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.8", "", {}, "sha512-BfEvehNpOT75r5Ksc5xW6NZuXujTfb7nlSEyVu4XHG3gdxNg1KqXruWbDewXOUaUYIo4oRbSfkjIajz4MAT8tA=="],
"@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
@@ -2959,6 +3091,8 @@
"@testing-library/user-event": ["@testing-library/user-event@14.6.5", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-FhqjldLTpteueBaKflhNFlMT3+PM0O5fiBUivht6b9CZ1eesJyy7+g3Jr7XwJzt/Hip3ZG5hWwK1MX1FuDiE4w=="],
"@trickfilm400/rollup-plugin-off-main-thread": ["@trickfilm400/rollup-plugin-off-main-thread@3.0.0-pre1", "", { "dependencies": { "ejs": "^3.1.10", "json5": "^2.2.3", "magic-string": "^0.30.21", "string.prototype.matchall": "^4.0.12" } }, "sha512-/67zpWDBLV+oYAEL682s1ktXL0HgqX76f6gaVGkGnVZlBbm1zd0v4Bz8MFF2GGhoX9rvfq3KSQHubFHwa6w6/Q=="],
"@tsconfig/bun": ["@tsconfig/bun@1.0.9", "", {}, "sha512-4M0/Ivfwcpz325z6CwSifOBZYji3DFOEpY6zEUt0+Xi2qRhzwvmqQN9XAHJh3OVvRJuAqVTLU2abdCplvp6mwQ=="],
"@tsconfig/node22": ["@tsconfig/node22@22.0.2", "", {}, "sha512-Kmwj4u8sDRDrMYRoN9FDEcXD8UpBSaPQQ24Gz+Gamqfm7xxn+GBR7ge/Z7pK8OXNGyUzbSwJj+TH6B+DS/epyA=="],
@@ -3129,6 +3263,8 @@
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="],
"@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="],
"@types/sax": ["@types/sax@1.2.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A=="],
@@ -3387,6 +3523,12 @@
"babel-plugin-module-resolver": ["babel-plugin-module-resolver@5.0.2", "", { "dependencies": { "find-babel-config": "^2.1.1", "glob": "^9.3.3", "pkg-up": "^3.1.0", "reselect": "^4.1.7", "resolve": "^1.22.8" } }, "sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg=="],
"babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.17", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-define-polyfill-provider": "^0.6.8", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w=="],
"babel-plugin-polyfill-corejs3": ["babel-plugin-polyfill-corejs3@0.14.2", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.8", "core-js-compat": "^3.48.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g=="],
"babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.8", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.8" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg=="],
"babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="],
"babel-preset-solid": ["babel-preset-solid@1.9.15", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.10" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.15" }, "optionalPeers": ["solid-js"] }, "sha512-GBmg1OiPb+OwcH51XbDAKPtvrPfQW7rCJTJxcp8+yhtWwN+kqnbEJk2SgVybd+uhTxTKAvjaFyiQSr/eUZBwzg=="],
@@ -3583,6 +3725,8 @@
"common-ancestor-path": ["common-ancestor-path@2.0.0", "", {}, "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng=="],
"common-tags": ["common-tags@1.8.2", "", {}, "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA=="],
"compare-version": ["compare-version@0.1.2", "", {}, "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A=="],
"compress-commons": ["compress-commons@6.0.2", "", { "dependencies": { "crc-32": "^1.2.0", "crc32-stream": "^6.0.0", "is-stream": "^2.0.1", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0" } }, "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg=="],
@@ -3609,6 +3753,8 @@
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
"core-js-compat": ["core-js-compat@3.50.0", "", { "dependencies": { "browserslist": "^4.28.7" } }, "sha512-XGpFGbMLHwSt74YLTKho7Ib242qi6O8MSX+sRokV4oz7iKXvQWGYZthjIhjRGMxjzVkAubBO512dKGYcefmX3Q=="],
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
@@ -3627,6 +3773,8 @@
"crossws": ["crossws@0.4.10", "", { "peerDependencies": { "srvx": ">=0.11.5" }, "optionalPeers": ["srvx"] }, "sha512-pz3oubH/dt12KjqsUB0IuXW4nwRDQ583iDsP4555Cpdqx0NoU7pGlWBcayyFI8f/l/idRpgjMEfwuOxSWJYlIA=="],
"crypto-random-string": ["crypto-random-string@2.0.0", "", {}, "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA=="],
"css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="],
"css-selector-parser": ["css-selector-parser@3.3.0", "", {}, "sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g=="],
@@ -3859,7 +4007,7 @@
"electron-store": ["electron-store@11.0.2", "", { "dependencies": { "conf": "^15.0.2", "type-fest": "^5.0.1" } }, "sha512-4VkNRdN+BImL2KcCi41WvAYbh6zLX5AUTi4so68yPqiItjbgTjqpEnGAqasgnG+lB6GuAyUltKwVopp6Uv+gwQ=="],
"electron-to-chromium": ["electron-to-chromium@1.5.409", "", {}, "sha512-ChI4N44d0B4A6C8prnNjMOaGgE59fUyEVYcRYm2XEXIjMbbvF5i9UL1cblDbpGqiU0uS8FE8UcKxqZqTXdmzbQ=="],
"electron-to-chromium": ["electron-to-chromium@1.5.411", "", {}, "sha512-gglkxzokjHfawpGxq75XdBV2/l3BAPzrsMs70qgaZdTW5rpV1tC4MdgJVP9fN126bODA4ZJQkn1wryEzJyQXIg=="],
"electron-updater": ["electron-updater@6.8.9", "", { "dependencies": { "builder-util-runtime": "9.7.0", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", "lodash.escaperegexp": "^4.1.2", "lodash.isequal": "^4.5.0", "semver": "~7.7.3", "tiny-typed-emitter": "^2.1.0" } }, "sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig=="],
@@ -3951,6 +4099,10 @@
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"eta": ["eta@4.6.0", "", {}, "sha512-lW6is4T1NFOYnmqGZIfvixqj7A7sSvScF+DN8EK6K58xI5MZ5UvYe0GjopxOXQtZvUn4eDdVuZ8XSoYWTMEKwA=="],
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
"event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
@@ -4101,6 +4253,8 @@
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
"get-own-enumerable-property-symbols": ["get-own-enumerable-property-symbols@3.0.2", "", {}, "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g=="],
"get-port": ["get-port@7.2.0", "", {}, "sha512-afP4W205ONCuMoPBqcR6PSXnzX35KTcJygfJfcp+QY+uwm3p20p1YczWXhlICIzGMCxYBQcySEcOgsJcrkyobg=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
@@ -4259,6 +4413,8 @@
"iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="],
"idb": ["idb@7.1.1", "", {}, "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
@@ -4345,6 +4501,8 @@
"is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="],
"is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="],
"is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="],
"is-network-error": ["is-network-error@1.3.2", "", {}, "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA=="],
@@ -4353,7 +4511,7 @@
"is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="],
"is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="],
"is-obj": ["is-obj@1.0.1", "", {}, "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg=="],
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
@@ -4363,6 +4521,8 @@
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
"is-regexp": ["is-regexp@1.0.0", "", {}, "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA=="],
"is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="],
"is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="],
@@ -4375,7 +4535,7 @@
"is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="],
"is-unsafe": ["is-unsafe@2.0.0", "", {}, "sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA=="],
"is-unsafe": ["is-unsafe@2.0.2", "", {}, "sha512-HgbIHPBH0KHHCcjLfGsCvhtPTVxjaAZlXjwdz7/GQC40SjSe4sfQsar8J5VFo8JOSbarkpV0OLG95bbaNd9aAQ=="],
"is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="],
@@ -4529,6 +4689,8 @@
"lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="],
"lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="],
"lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="],
"lodash.isequal": ["lodash.isequal@4.5.0", "", {}, "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="],
@@ -5037,6 +5199,8 @@
"pretty": ["pretty@2.0.0", "", { "dependencies": { "condense-newlines": "^0.2.1", "extend-shallow": "^2.0.1", "js-beautify": "^1.6.12" } }, "sha512-G9xUchgTEiNpormdYBl+Pha50gOUovT18IvAe7EYMZ1/f9W/WWMPRn+xI68yXNMUk3QXHDwo/1wV/4NejVNe1w=="],
"pretty-bytes": ["pretty-bytes@6.1.1", "", {}, "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ=="],
"pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
"prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
@@ -5119,7 +5283,7 @@
"read-binary-file-arch": ["read-binary-file-arch@1.0.6", "", { "dependencies": { "debug": "^4.3.4" }, "bin": { "read-binary-file-arch": "cli.js" } }, "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg=="],
"read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="],
"read-cache": ["read-cache@1.0.1", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-vmdQ4JEAaAINu1pc2H9HdZIeL2+IQehMpelB7BMNiNIA+mFxA5/02ObFUhEHkNAtNZOBS7kFCZxziUr4ho1j7A=="],
"read-cmd-shim": ["read-cmd-shim@6.0.0", "", {}, "sha512-1zM5HuOfagXCBWMN83fuFI/x+T/UhZ7k+KIzhrHXcQoeX5+7gmaDYjELQHmmzIodumBHeByBJT4QYS7ufAgs7A=="],
@@ -5145,6 +5309,10 @@
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
"regenerate": ["regenerate@1.4.2", "", {}, "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A=="],
"regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="],
"regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
"regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
@@ -5153,6 +5321,12 @@
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
"regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="],
"regjsgen": ["regjsgen@0.8.0", "", {}, "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q=="],
"regjsparser": ["regjsparser@0.13.2", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ=="],
"rehype": ["rehype@13.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "rehype-parse": "^9.0.0", "rehype-stringify": "^10.0.0", "unified": "^11.0.0" } }, "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A=="],
"rehype-autolink-headings": ["rehype-autolink-headings@7.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-heading-rank": "^3.0.0", "hast-util-is-element": "^3.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw=="],
@@ -5235,7 +5409,7 @@
"robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="],
"rolldown": ["rolldown@1.2.4", "", { "dependencies": { "@oxc-project/types": "=0.144.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.2.4", "@rolldown/binding-darwin-arm64": "1.2.4", "@rolldown/binding-darwin-x64": "1.2.4", "@rolldown/binding-freebsd-x64": "1.2.4", "@rolldown/binding-linux-arm-gnueabihf": "1.2.4", "@rolldown/binding-linux-arm64-gnu": "1.2.4", "@rolldown/binding-linux-arm64-musl": "1.2.4", "@rolldown/binding-linux-ppc64-gnu": "1.2.4", "@rolldown/binding-linux-s390x-gnu": "1.2.4", "@rolldown/binding-linux-x64-gnu": "1.2.4", "@rolldown/binding-linux-x64-musl": "1.2.4", "@rolldown/binding-openharmony-arm64": "1.2.4", "@rolldown/binding-win32-arm64-msvc": "1.2.4", "@rolldown/binding-win32-x64-msvc": "1.2.4" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w=="],
"rolldown": ["rolldown@1.2.5", "", { "dependencies": { "@oxc-project/types": "=0.146.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.5", "@rolldown/binding-android-arm64": "1.2.5", "@rolldown/binding-darwin-arm64": "1.2.5", "@rolldown/binding-darwin-x64": "1.2.5", "@rolldown/binding-freebsd-x64": "1.2.5", "@rolldown/binding-linux-arm-gnueabihf": "1.2.5", "@rolldown/binding-linux-arm64-gnu": "1.2.5", "@rolldown/binding-linux-arm64-musl": "1.2.5", "@rolldown/binding-linux-ppc64-gnu": "1.2.5", "@rolldown/binding-linux-s390x-gnu": "1.2.5", "@rolldown/binding-linux-x64-gnu": "1.2.5", "@rolldown/binding-linux-x64-musl": "1.2.5", "@rolldown/binding-openharmony-arm64": "1.2.5", "@rolldown/binding-win32-arm64-msvc": "1.2.5", "@rolldown/binding-win32-x64-msvc": "1.2.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA=="],
"rollup": ["rollup@4.62.4", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.62.4", "@rollup/rollup-android-arm64": "4.62.4", "@rollup/rollup-darwin-arm64": "4.62.4", "@rollup/rollup-darwin-x64": "4.62.4", "@rollup/rollup-freebsd-arm64": "4.62.4", "@rollup/rollup-freebsd-x64": "4.62.4", "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", "@rollup/rollup-linux-arm-musleabihf": "4.62.4", "@rollup/rollup-linux-arm64-gnu": "4.62.4", "@rollup/rollup-linux-arm64-musl": "4.62.4", "@rollup/rollup-linux-loong64-gnu": "4.62.4", "@rollup/rollup-linux-loong64-musl": "4.62.4", "@rollup/rollup-linux-ppc64-gnu": "4.62.4", "@rollup/rollup-linux-ppc64-musl": "4.62.4", "@rollup/rollup-linux-riscv64-gnu": "4.62.4", "@rollup/rollup-linux-riscv64-musl": "4.62.4", "@rollup/rollup-linux-s390x-gnu": "4.62.4", "@rollup/rollup-linux-x64-gnu": "4.62.4", "@rollup/rollup-linux-x64-musl": "4.62.4", "@rollup/rollup-openbsd-x64": "4.62.4", "@rollup/rollup-openharmony-arm64": "4.62.4", "@rollup/rollup-win32-arm64-msvc": "4.62.4", "@rollup/rollup-win32-ia32-msvc": "4.62.4", "@rollup/rollup-win32-x64-gnu": "4.62.4", "@rollup/rollup-win32-x64-msvc": "4.62.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg=="],
@@ -5287,6 +5461,8 @@
"serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="],
"serialize-javascript": ["serialize-javascript@7.1.0", "", {}, "sha512-RNEqWOyhhUQYN9V1GfHwu9AR/g+NTciH6Z5u3/no6X3/w+04J2lVDL+svFQVXgXrEGBMG2puMVN3gq2SNGuTGw=="],
"seroval": ["seroval@1.3.2", "", {}, "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ=="],
"seroval-plugins": ["seroval-plugins@1.3.3", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w=="],
@@ -5347,6 +5523,8 @@
"smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
"smob": ["smob@1.6.2", "", {}, "sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw=="],
"smol-toml": ["smol-toml@1.8.0", "", {}, "sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ=="],
"socket.io-client": ["socket.io-client@4.8.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g=="],
@@ -5449,6 +5627,8 @@
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="],
"string.prototype.trim": ["string.prototype.trim@1.2.11", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.2", "es-object-atoms": "^1.1.2", "has-property-descriptors": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w=="],
"string.prototype.trimend": ["string.prototype.trimend@1.0.10", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.2" } }, "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw=="],
@@ -5459,12 +5639,16 @@
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
"stringify-object": ["stringify-object@3.3.0", "", { "dependencies": { "get-own-enumerable-property-symbols": "^3.0.0", "is-obj": "^1.0.1", "is-regexp": "^1.0.0" } }, "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw=="],
"strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="],
"strip-comments": ["strip-comments@2.0.1", "", {}, "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw=="],
"strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="],
"strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="],
@@ -5513,8 +5697,12 @@
"temp": ["temp@0.9.4", "", { "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" } }, "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA=="],
"temp-dir": ["temp-dir@2.0.0", "", {}, "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg=="],
"temp-file": ["temp-file@3.4.0", "", { "dependencies": { "async-exit-hook": "^2.0.1", "fs-extra": "^10.0.0" } }, "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg=="],
"tempy": ["tempy@0.6.0", "", { "dependencies": { "is-stream": "^2.0.0", "temp-dir": "^2.0.0", "type-fest": "^0.16.0", "unique-string": "^2.0.0" } }, "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw=="],
"terracotta": ["terracotta@1.1.1", "", { "dependencies": { "solid-use": "^0.9.1" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-Sa6wnvkGMFmPq8N/wQb2aKkIW2eXH0LJvUOEk6QZLBEjqy+LsbzAOpDuqEfOmfA/hexssE6eQve2i1IIOeOh4g=="],
"terser": ["terser@5.50.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-CN9BVxWhgS/hRxtUMjtC2uRWSTcSfQFHMDWma6sKKfIivCD91sM+FOPfvwoaRMqCSrUpe1nv3jDamd9eEQ4y+w=="],
@@ -5637,14 +5825,24 @@
"unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="],
"unicode-canonical-property-names-ecmascript": ["unicode-canonical-property-names-ecmascript@2.0.1", "", {}, "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg=="],
"unicode-match-property-ecmascript": ["unicode-match-property-ecmascript@2.0.0", "", { "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", "unicode-property-aliases-ecmascript": "^2.0.0" } }, "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q=="],
"unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.1", "", {}, "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg=="],
"unicode-properties": ["unicode-properties@1.4.1", "", { "dependencies": { "base64-js": "^1.3.0", "unicode-trie": "^2.0.0" } }, "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg=="],
"unicode-property-aliases-ecmascript": ["unicode-property-aliases-ecmascript@2.2.0", "", {}, "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ=="],
"unicode-trie": ["unicode-trie@2.0.0", "", { "dependencies": { "pako": "^0.2.5", "tiny-inflate": "^1.0.0" } }, "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ=="],
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
"unifont": ["unifont@0.7.5", "", { "dependencies": { "css-tree": "^3.1.0", "ohash": "^2.0.11", "undici": "^8.0.0" } }, "sha512-ULe/Cs+ZIsq+dcFofNkhqielCrUJnb5mr+Yc4EBM2VlL+6OZR6+cjtI2mT1bJvRBrVncqHAbLURxmPLcCXzWMg=="],
"unique-string": ["unique-string@2.0.0", "", { "dependencies": { "crypto-random-string": "^2.0.0" } }, "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg=="],
"unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="],
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
@@ -5683,6 +5881,8 @@
"unzipper": ["unzipper@0.12.5", "", { "dependencies": { "bluebird": "~3.7.2", "duplexer2": "~0.1.4", "fs-extra": "11.3.1", "graceful-fs": "^4.2.2", "node-int64": "^0.4.0" } }, "sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A=="],
"upath": ["upath@1.2.0", "", {}, "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg=="],
"update-browserslist-db": ["update-browserslist-db@1.3.1", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ=="],
"uqr": ["uqr@0.1.3", "", {}, "sha512-0rjE8iEJe4YmT9TOhwsZtqCMRLc5DXZUI2UEYUUg63ikBkqqE5EYWaI0etFe/5KUcmcYwLih2RND1kq+hrUJXA=="],
@@ -5729,6 +5929,8 @@
"vite-plugin-icons-spritesheet": ["vite-plugin-icons-spritesheet@3.0.1", "", { "dependencies": { "chalk": "^5.4.1", "glob": "^11.0.1", "node-html-parser": "^7.0.1", "tinyexec": "^0.3.2" }, "peerDependencies": { "vite": ">=5.2.0" } }, "sha512-Cr0+Z6wRMwSwKisWW9PHeTjqmQFv0jwRQQMc3YgAhAgZEe03j21el0P/CA31KN/L5eiL1LhR14VTXl96LetonA=="],
"vite-plugin-pwa": ["vite-plugin-pwa@1.3.0", "", { "dependencies": { "debug": "^4.3.6", "pretty-bytes": "^6.1.1", "tinyglobby": "^0.2.10", "workbox-build": "^7.4.1", "workbox-window": "^7.4.1" }, "peerDependencies": { "@vite-pwa/assets-generator": "^1.0.0", "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@vite-pwa/assets-generator"] }, "sha512-c5kMgN+ITrOtHXp8PAtk2uOIEea6XjP/unCGxOWWBzQ6qa65qj/awHg0wf+QF9E/2u9vh86LqxPwzEPNbM2r5A=="],
"vite-plugin-solid": ["vite-plugin-solid@2.11.14", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.0.0 || ^7.0.0", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-7ZVBt8rpoyqmlwin2kRIUveaHoF6/kulY7gsnD+qFh4nS29V4OPAnw+ojoAspXIjObiL9o1xh9a/nTuYHm02Rw=="],
"vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
@@ -5807,6 +6009,38 @@
"widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="],
"workbox-background-sync": ["workbox-background-sync@7.4.1", "", { "dependencies": { "idb": "^7.0.1", "workbox-core": "7.4.1" } }, "sha512-HhT7KE8tOWDm02wRNshXUnUPofMlhenF2DBdUnDPOubhizzPeItkYTmAB6td1Z2cjYPa98vzEiPLEuzn5hN66g=="],
"workbox-broadcast-update": ["workbox-broadcast-update@7.4.1", "", { "dependencies": { "workbox-core": "7.4.1" } }, "sha512-uAlgslKLvbQY+suirIdnBCSYrcgBhjp81Nj4l1lj/Jmj0MJO2CJERnCJjT0GFVwmReV0N+zs78K6gqd5gr9/+A=="],
"workbox-build": ["workbox-build@7.4.1", "", { "dependencies": { "@apideck/better-ajv-errors": "^0.3.1", "@babel/core": "^7.24.4", "@babel/preset-env": "^7.11.0", "@babel/runtime": "^7.11.2", "@rollup/plugin-babel": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-replace": "^6.0.3", "@rollup/plugin-terser": "^1.0.0", "@trickfilm400/rollup-plugin-off-main-thread": "^3.0.0-pre1", "ajv": "^8.6.0", "common-tags": "^1.8.0", "eta": "^4.5.1", "fast-json-stable-stringify": "^2.1.0", "fs-extra": "^9.0.1", "glob": "^11.0.1", "pretty-bytes": "^5.3.0", "rollup": "^4.53.3", "source-map": "^0.8.0-beta.0", "stringify-object": "^3.3.0", "strip-comments": "^2.0.1", "tempy": "^0.6.0", "upath": "^1.2.0", "workbox-background-sync": "7.4.1", "workbox-broadcast-update": "7.4.1", "workbox-cacheable-response": "7.4.1", "workbox-core": "7.4.1", "workbox-expiration": "7.4.1", "workbox-google-analytics": "7.4.1", "workbox-navigation-preload": "7.4.1", "workbox-precaching": "7.4.1", "workbox-range-requests": "7.4.1", "workbox-recipes": "7.4.1", "workbox-routing": "7.4.1", "workbox-strategies": "7.4.1", "workbox-streams": "7.4.1", "workbox-sw": "7.4.1", "workbox-window": "7.4.1" } }, "sha512-SDhxIvEAde9Gy/5w4Yo1Jh/M49Z0qE3q0oteyE8zGq0DScxFqVBcCtIXFuLtmtxRQZCMbf0prco4VyEu3KBQuw=="],
"workbox-cacheable-response": ["workbox-cacheable-response@7.4.1", "", { "dependencies": { "workbox-core": "7.4.1" } }, "sha512-8xaFoJdDc2OjrlbbL3gEeBO1WKcMwRqwLRupgqahYXu75yXajPLuwrbXMrIGZuWYXrQwk0xDjOxZ/ujCy/oJYw=="],
"workbox-core": ["workbox-core@7.4.1", "", {}, "sha512-DT+vu46eh/2vRsSHTY4Xmc32Z1rr9PRlQUXr1Dx30ZuXRWwOsvZgGgcwxcasubQLQmbTNYZjv44LkBAQ4tT5tQ=="],
"workbox-expiration": ["workbox-expiration@7.4.1", "", { "dependencies": { "idb": "^7.0.1", "workbox-core": "7.4.1" } }, "sha512-lRKUF7b+OGbeXkQk1s6MHXOa3d7Xxf7Of31W6c6hCfipfIyrtdWZ89stq21AHZMaoG7VNFoHply4Ox+rU31TWg=="],
"workbox-google-analytics": ["workbox-google-analytics@7.4.1", "", { "dependencies": { "workbox-background-sync": "7.4.1", "workbox-core": "7.4.1", "workbox-routing": "7.4.1", "workbox-strategies": "7.4.1" } }, "sha512-Mks1JwLEt++ZAkF6sS1OpSh9RtAMIsiDgRpK+codiHGIPXeaUOgi4cPc3GFadUl8V5QPeypEk8Oxgl3HlwVzHw=="],
"workbox-navigation-preload": ["workbox-navigation-preload@7.4.1", "", { "dependencies": { "workbox-core": "7.4.1" } }, "sha512-C4KVsjPcYKJOhr631AxR9XoG2rLF3QiTk5aMv36MXOjtWvm8axwNFAtKUPGsWUwLXXAMgYM1En7fsvndaXeXRQ=="],
"workbox-precaching": ["workbox-precaching@7.4.1", "", { "dependencies": { "workbox-core": "7.4.1", "workbox-routing": "7.4.1", "workbox-strategies": "7.4.1" } }, "sha512-cdr/9qByww7yzEp7zg/qI4ukUrrNjQLgN+ONQRpjy/VqGQXwkgHwr00KksGJK8v0VifwDXBb8a4cWNZH71jn3Q=="],
"workbox-range-requests": ["workbox-range-requests@7.4.1", "", { "dependencies": { "workbox-core": "7.4.1" } }, "sha512-7i2oxAUE82gHdAJBCAQ04JzNOdRPqzuOzGfoUyJpFSmeqBNYGPrAH8GPoPjUQTfp+NycwrD2H68VtuF8qxv0vQ=="],
"workbox-recipes": ["workbox-recipes@7.4.1", "", { "dependencies": { "workbox-cacheable-response": "7.4.1", "workbox-core": "7.4.1", "workbox-expiration": "7.4.1", "workbox-precaching": "7.4.1", "workbox-routing": "7.4.1", "workbox-strategies": "7.4.1" } }, "sha512-gnbVfmV4/TtmQaM4x9AtuXhcdstJsep3XMVeztOrQVPT+R6+6DeBjGTCQ7fFCXm+4GEHUA5VEBTyi5+4gWGeog=="],
"workbox-routing": ["workbox-routing@7.4.1", "", { "dependencies": { "workbox-core": "7.4.1" } }, "sha512-yubJGErZOusuidAenaL5ypfhQOa7urxP/f8E0ws7FPb4039RiWXUWBAyUkmUoOL/BcQGen3h0J8872d51IYxtA=="],
"workbox-strategies": ["workbox-strategies@7.4.1", "", { "dependencies": { "workbox-core": "7.4.1" } }, "sha512-GZxpaw9NbmOelj7667uZ2kpk5BFpOGbO4X0qjwh5ls8XQ8C+Lha5LQchTiUzsTFSS+NlUpftYAyOVXvQUrcqOQ=="],
"workbox-streams": ["workbox-streams@7.4.1", "", { "dependencies": { "workbox-core": "7.4.1", "workbox-routing": "7.4.1" } }, "sha512-HWWtraKUbJknd9kgqGcpQ3G114HOPYvqs8HaJMDs2ebLNAimDkVDaWfAXE6Ybl+m8U6KsCE6pWyLYuigWmnAXw=="],
"workbox-sw": ["workbox-sw@7.4.1", "", {}, "sha512-fez5f2DUlDJWTFYkCWQpY10N8gtztd849NswCbVFk0QlcSM4HT5A8x4g4ii650yem4I8tHY0R7JZahwp3ltIPw=="],
"workbox-window": ["workbox-window@7.4.1", "", { "dependencies": { "@types/trusted-types": "^2.0.2", "workbox-core": "7.4.1" } }, "sha512-notZDH2u8VXaqyuD7xaqIfEFi6SRM4SUSd7ewe9PDsVqADuepxX2ZMY3uvuZGxzY5ZOsGC/vD3A/3smFtJt4/A=="],
"workerd": ["workerd@1.20251118.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20251118.0", "@cloudflare/workerd-darwin-arm64": "1.20251118.0", "@cloudflare/workerd-linux-64": "1.20251118.0", "@cloudflare/workerd-linux-arm64": "1.20251118.0", "@cloudflare/workerd-windows-64": "1.20251118.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-Om5ns0Lyx/LKtYI04IV0bjIrkBgoFNg0p6urzr2asekJlfP18RqFzyqMFZKf0i9Gnjtz/JfAS/Ol6tjCe5JJsQ=="],
"world-atlas": ["world-atlas@2.0.2", "", {}, "sha512-IXfV0qwlKXpckz1FhwXVwKRjiIhOnWttOskm5CtxMsjgE/MXAYRHWJqgXOpM8IkcPBoXnyTU5lFHcYa5ChG0LQ=="],
@@ -5969,6 +6203,8 @@
"@antfu/install-pkg/tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="],
"@apideck/better-ajv-errors/leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="],
"@astrojs/cloudflare/@cloudflare/vite-plugin": ["@cloudflare/vite-plugin@1.52.1", "", { "dependencies": { "@cloudflare/unenv-preset": "2.16.1", "miniflare": "5.20260811.1-alpha", "unenv": "2.0.0-rc.24", "workerd": "1.20260811.1", "wrangler": "4.123.0", "ws": "8.21.0" }, "peerDependencies": { "vite": "^6.1.0 || ^7.0.0 || ^8.0.0" }, "bin": { "cf-vite": "bin/cf-vite" } }, "sha512-O2IkD8E03qsUxNBdDRfDI9JPl25XIPt+76WJf8tgm206c34jYk8nn12ylBiOI/6NA8XUFhLjRTF1BGY+kVylXw=="],
"@astrojs/cloudflare/wrangler": ["wrangler@4.123.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "5.20260811.1-alpha", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260811.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^5.20260811.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js", "cf-wrangler": "bin/cf-wrangler.js" } }, "sha512-VXo2I1oa0x9aGAKIFPRSQPqTh0RBY5Ktl44YOhNmsJQFUdJKDA2vVTU6Xj+FC2koll6orJqWZN8jbXVIk9O67Q=="],
@@ -5991,9 +6227,9 @@
"@astrojs/mdx/es-module-lexer": ["es-module-lexer@2.3.2", "", {}, "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw=="],
"@astrojs/node/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.10.2", "", { "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "js-yaml": "^4.3.0", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "unified": "^11.0.5" } }, "sha512-yt7fMgPYqSM4Tmr+taTW6Per+hjJ8Pk6lA1PAcDyqzOt8HzJ6Kje5WzCxA2Sd+9wsUW7uhkLeoTMK0cXPwH9rQ=="],
"@astrojs/node/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.10.4", "", { "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "js-yaml": "^4.3.0", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "unified": "^11.0.5" } }, "sha512-nozZSy/mKYLqe4YrqbKtdOszedAfXYCtw3wZ0d+CAjz4GqQ4L9rl1ltIL5BlgwmYVinJg/RZ0MgGuWOdlyRZlA=="],
"@astrojs/react/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.10.3", "", { "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "js-yaml": "^4.3.0", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "unified": "^11.0.5" } }, "sha512-HIx/t1NywcqSTFaVwZh15n8JsC3YQdCaJZDaTS/aurYTEvNiWDXUGS3Z9uQX3bFB+B1pCgN/nljckkzYTpHZ2w=="],
"@astrojs/react/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.10.4", "", { "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "js-yaml": "^4.3.0", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "unified": "^11.0.5" } }, "sha512-nozZSy/mKYLqe4YrqbKtdOszedAfXYCtw3wZ0d+CAjz4GqQ4L9rl1ltIL5BlgwmYVinJg/RZ0MgGuWOdlyRZlA=="],
"@astrojs/react/@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="],
@@ -6009,7 +6245,7 @@
"@astrojs/telemetry/is-docker": ["is-docker@4.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA=="],
"@astrojs/vercel/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.10.3", "", { "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "js-yaml": "^4.3.0", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "unified": "^11.0.5" } }, "sha512-HIx/t1NywcqSTFaVwZh15n8JsC3YQdCaJZDaTS/aurYTEvNiWDXUGS3Z9uQX3bFB+B1pCgN/nljckkzYTpHZ2w=="],
"@astrojs/vercel/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.10.4", "", { "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "js-yaml": "^4.3.0", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "unified": "^11.0.5" } }, "sha512-nozZSy/mKYLqe4YrqbKtdOszedAfXYCtw3wZ0d+CAjz4GqQ4L9rl1ltIL5BlgwmYVinJg/RZ0MgGuWOdlyRZlA=="],
"@astrojs/vercel/@vercel/analytics": ["@vercel/analytics@1.6.1", "", { "peerDependencies": { "@remix-run/react": "^2", "@sveltejs/kit": "^1 || ^2", "next": ">= 13", "react": "^18 || ^19 || ^19.0.0-rc", "svelte": ">= 4", "vue": "^3", "vue-router": "^4" }, "optionalPeers": ["@remix-run/react", "@sveltejs/kit", "next", "react", "svelte", "vue", "vue-router"] }, "sha512-oH9He/bEM+6oKlv3chWuOOcp8Y6fo6/PSro8hEkgCW3pu9/OiCXiUpRUogDh3Fs3LH2sosDrx8CxeOLBEE+afg=="],
@@ -6147,6 +6383,10 @@
"@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@babel/helper-create-regexp-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@babel/preset-env/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@bruits/satteri-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
"@bruits/satteri-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
@@ -6527,6 +6767,8 @@
"babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="],
"babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"bin-links/write-file-atomic": ["write-file-atomic@7.0.1", "", { "dependencies": { "signal-exit": "^4.0.1" } }, "sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg=="],
"blume/@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=="],
@@ -6537,7 +6779,7 @@
"blume/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
"blume/dompurify": ["dompurify@3.4.13", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ=="],
"blume/dompurify": ["dompurify@3.4.14", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-dVoH9z+MY+C9IilgGCk3YfFqjLi3fChm2OiKJMzh6axrJ5qwxqWaZamgmHrpv22CN/KdbZJuGEGgfQoL00LTdg=="],
"blume/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=="],
@@ -6695,7 +6937,7 @@
"md-to-react-email/marked": ["marked@7.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ=="],
"mermaid/dompurify": ["dompurify@3.4.13", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ=="],
"mermaid/dompurify": ["dompurify@3.4.14", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-dVoH9z+MY+C9IilgGCk3YfFqjLi3fChm2OiKJMzh6axrJ5qwxqWaZamgmHrpv22CN/KdbZJuGEGgfQoL00LTdg=="],
"mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
@@ -6819,6 +7061,8 @@
"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=="],
"terser/acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
"tiny-async-pool/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="],
@@ -6871,6 +7115,14 @@
"which-builtin-type/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
"workbox-build/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=="],
"workbox-build/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
"workbox-build/pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="],
"workbox-build/source-map": ["source-map@0.8.0", "", {}, "sha512-d8EqvL+k/SOXCreS/SUzg2ciyHqBBLcN/yuRjFsbvVhHTE2pgei7oAhmPM7kWFbkX6OSMQfUq4KbkF3au9lhYQ=="],
"wrangler/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="],
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
@@ -7759,6 +8011,8 @@
"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=="],
"ow/dot-prop/is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="],
"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=="],
@@ -7837,6 +8091,8 @@
"vscode-languageserver/vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="],
"workbox-build/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
"wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
"wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="],
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-PatsUdaitHvSUpS5gkC5J2rsUNB5vwJKqHdlOFaKk70=",
"aarch64-linux": "sha256-gTRQMAADH/SpQ8yh+YS2IcnmQxnJcU4FUah2YfrKeP8=",
"aarch64-darwin": "sha256-QTqlwmugYh+iu5Sh/Hxv01NXH/OhzcQ8ObVUbA9A8AM=",
"x86_64-darwin": "sha256-0DPAbNCVw2nUMWkIGEhB6saMdxRRwAJi7wAoWCQc7xQ="
"x86_64-linux": "sha256-V1G3RuuwtlVx166oDVCqQ9v7ELWRr5JDYitcCMitbNA=",
"aarch64-linux": "sha256-nm/N60X7kdkNIa2oCgqyNp9ewrBrWvr6ES0qhxzypQc=",
"aarch64-darwin": "sha256-7UQtp4KrtxuT6MMrz7YYcJcF9/b5ECobkSwzI+yLe0Q=",
"x86_64-darwin": "sha256-jrn0GOa7qDskowio97bvhl7rsideXqHVbygSi3M1cf0="
}
}
+2 -2
View File
@@ -52,7 +52,7 @@
"@opentui/core": "0.5.6",
"@opentui/keymap": "0.5.6",
"@opentui/solid": "0.5.6",
"@tanstack/solid-virtual": "3.13.32",
"@tanstack/solid-virtual": "3.13.37",
"@shikijs/stream": "4.2.0",
"@standard-schema/spec": "1.1.0",
"ulid": "3.0.1",
@@ -174,7 +174,7 @@
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
"@tanstack/virtual-core@3.17.8": "patches/@tanstack%2Fvirtual-core@3.17.8.patch",
"@ff-labs/fff-bun@0.10.5": "patches/@ff-labs%2Ffff-bun@0.10.5.patch"
}
}
+4 -5
View File
@@ -212,7 +212,6 @@ type GeminiEvent = Schema.Schema.Type<typeof GeminiEvent>
interface ParserState {
readonly finishReason?: string
readonly hasToolCalls: boolean
readonly nextToolCallId: number
readonly promptFeedback?: GeminiPromptFeedback
readonly usage?: Usage
readonly lifecycle: Lifecycle.State
@@ -580,7 +579,6 @@ const step = (state: ParserState, event: GeminiEvent) => {
const events: LLMEvent[] = []
let hasToolCalls = nextState.hasToolCalls
let lifecycle = nextState.lifecycle
let nextToolCallId = nextState.nextToolCallId
let reasoningSignature = nextState.reasoningSignature
let textSignature = nextState.textSignature
@@ -620,7 +618,9 @@ const step = (state: ParserState, event: GeminiEvent) => {
if ("functionCall" in part) {
const input = part.functionCall.args === undefined ? {} : part.functionCall.args
const id = `tool_${nextToolCallId++}`
// Gemini 2.0+ and Vertex supply a unique function call ID on the part; when omitted (e.g. Gemini 1.5),
// generate a globally unique ID rather than a per-request counter to prevent cross-request collisions in downstream registries.
const id = part.functionCall.id ?? `tool_${crypto.randomUUID().replaceAll("-", "")}`
const metadata = {
...(part.functionCall.id === undefined ? {} : { functionCallId: part.functionCall.id }),
...(part.thoughtSignature === undefined ? {} : { thoughtSignature: part.thoughtSignature }),
@@ -649,7 +649,6 @@ const step = (state: ParserState, event: GeminiEvent) => {
...nextState,
hasToolCalls,
lifecycle,
nextToolCallId,
reasoningSignature,
textSignature,
finishReason: candidate.finishReason ?? nextState.finishReason,
@@ -673,7 +672,7 @@ export const protocol = Protocol.make({
},
stream: {
event: Protocol.jsonEvent(GeminiEvent),
initial: () => ({ hasToolCalls: false, nextToolCallId: 0, lifecycle: Lifecycle.initial() }),
initial: () => ({ hasToolCalls: false, lifecycle: Lifecycle.initial() }),
step,
onHalt: finish,
},
+1 -1
View File
@@ -110,7 +110,7 @@ export const model = (input: ModelInput) => {
const multipartImages = yield* Effect.forEach(sourceImages, (image) => {
if (image.type === "bytes") return Effect.succeed({ data: image.data, mediaType: image.mediaType })
if (image.type === "url") return ImageInputs.decodeDataUrl(image.url, ADAPTER)
return Effect.succeed(undefined)
return Effect.undefined
})
const multipartMask =
mask === undefined
@@ -16,7 +16,7 @@ export const decodeDataUrl = (
url: string,
module: string,
): Effect.Effect<{ readonly mediaType: string; readonly data: Uint8Array } | undefined, AIError> => {
if (!url.startsWith("data:")) return Effect.succeed(undefined)
if (!url.startsWith("data:")) return Effect.undefined
const match = /^data:([^;,]+);base64,(.*)$/s.exec(url)
if (!match) return Effect.fail(invalid(module, "Image data URLs must contain a MIME type and base64 data"))
return Effect.fromResult(Encoding.decodeBase64(match[2])).pipe(
+52 -26
View File
@@ -848,7 +848,7 @@ describe("Gemini route", () => {
providerMetadata: { google: { thoughtSignature: "thought_sig" } },
})
expect(toolCall).toMatchObject({
id: "tool_0",
id: "provider_call",
providerMetadata: { google: { functionCallId: "provider_call", thoughtSignature: "tool_sig" } },
})
expect(response.events.findIndex((event) => event.type === "reasoning-end")).toBeLessThan(
@@ -862,14 +862,14 @@ describe("Gemini route", () => {
Message.assistant([
{ type: "reasoning", text: "thinking", providerMetadata: reasoningEnd?.providerMetadata },
ToolCallPart.make({
id: "tool_0",
id: "provider_call",
name: "lookup",
input: { query: "weather" },
providerMetadata: toolCall?.providerMetadata,
}),
]),
Message.tool({
id: "tool_0",
id: "provider_call",
name: "lookup",
result: "done",
resultType: "text",
@@ -1101,21 +1101,17 @@ describe("Gemini route", () => {
providerMetadata: { google: { promptTokenCount: 5, candidatesTokenCount: 1 } },
})
expect(response.toolCalls).toEqual([
{
type: "tool-call",
id: "tool_0",
name: "lookup",
input: { query: "weather" },
providerExecuted: undefined,
providerMetadata: undefined,
},
])
expect(response.toolCalls[0].id).toMatch(/^tool_[0-9a-zA-Z]+$/)
expect(response.toolCalls[0]).toMatchObject({
type: "tool-call",
name: "lookup",
input: { query: "weather" },
})
expect(response.events).toEqual([
{ type: "step-start", index: 0 },
{
type: "tool-call",
id: "tool_0",
id: response.toolCalls[0].id,
name: "lookup",
input: { query: "weather" },
providerExecuted: undefined,
@@ -1158,7 +1154,8 @@ describe("Gemini route", () => {
),
)
expect(response.toolCalls).toEqual([{ type: "tool-call", id: "tool_0", name: "ping", input: {} }])
expect(response.toolCalls[0].id).toMatch(/^tool_[0-9a-zA-Z]+$/)
expect(response.toolCalls).toMatchObject([{ type: "tool-call", name: "ping", input: {} }])
}),
)
@@ -1198,7 +1195,7 @@ describe("Gemini route", () => {
content: {
role: "model",
parts: [
{ functionCall: { id: "tool_0", name: "lookup", args: { query: "weather" } } },
{ functionCall: { id: "call_0", name: "lookup", args: { query: "weather" } } },
{ functionCall: { name: "lookup", args: { query: "news" } } },
],
},
@@ -1212,16 +1209,20 @@ describe("Gemini route", () => {
}),
).pipe(Effect.provide(fixedResponse(body)))
expect(response.toolCalls).toEqual([
{
type: "tool-call",
id: "tool_0",
name: "lookup",
input: { query: "weather" },
providerMetadata: { google: { functionCallId: "tool_0" } },
},
{ type: "tool-call", id: "tool_1", name: "lookup", input: { query: "news" } },
])
expect(response.toolCalls[0]).toMatchObject({
type: "tool-call",
id: "call_0",
name: "lookup",
input: { query: "weather" },
providerMetadata: { google: { functionCallId: "call_0" } },
})
expect(response.toolCalls[1]).toMatchObject({
type: "tool-call",
name: "lookup",
input: { query: "news" },
})
expect(response.toolCalls[1].id).toMatch(/^tool_[0-9a-zA-Z]+$/)
expect(response.toolCalls[0].id).not.toBe(response.toolCalls[1].id)
expect(response.events.at(-1)).toMatchObject({
type: "finish",
reason: { normalized: "tool-calls", raw: "STOP" },
@@ -1229,6 +1230,31 @@ describe("Gemini route", () => {
}),
)
it.effect("assigns distinct unique fallback ids across separate requests", () =>
Effect.gen(function* () {
const body = sseEvents({
candidates: [
{
content: {
role: "model",
parts: [{ functionCall: { name: "lookup", args: { query: "weather" } } }],
},
finishReason: "STOP",
},
],
})
const req = LLMRequest.update(request, {
tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })],
})
const first = yield* LLMClient.generate(req).pipe(Effect.provide(fixedResponse(body)))
const second = yield* LLMClient.generate(req).pipe(Effect.provide(fixedResponse(body)))
expect(first.toolCalls[0].id).toMatch(/^tool_[0-9a-zA-Z]+$/)
expect(second.toolCalls[0].id).toMatch(/^tool_[0-9a-zA-Z]+$/)
expect(first.toolCalls[0].id).not.toBe(second.toolCalls[0].id)
}),
)
it.effect("maps length and content-filter finish reasons", () =>
Effect.gen(function* () {
const length = yield* LLMClient.generate(request).pipe(
@@ -259,6 +259,18 @@ export function event(
return makeEvent(type, data)
}
export function toolInputStarted(data: Extract<OpenCodeEvent, { type: "session.tool.input.started" }>["data"]) {
return makeEvent("session.tool.input.started", data)
}
export function toolInputEnded(data: Extract<OpenCodeEvent, { type: "session.tool.input.ended" }>["data"]) {
return makeEvent("session.tool.input.ended", data)
}
export function toolCalled(data: Extract<OpenCodeEvent, { type: "session.tool.called" }>["data"]) {
return makeEvent("session.tool.called", data)
}
export function validateTimelineEvent(input: unknown): OpenCodeEvent {
if (!input || typeof input !== "object") throw new Error("Timeline event must be an object")
if (!("type" in input) || typeof input.type !== "string") throw new Error("Timeline event requires a type")
@@ -1,4 +1,5 @@
import { expect, test } from "@playwright/test"
import { createTwoFilesPatch } from "diff"
import {
defineVisualRegions,
reportVisualStability,
@@ -13,10 +14,63 @@ import {
setupTimeline,
shell,
textPart,
toolPart,
userMessage,
type TimelineMessage,
} from "./fixture"
test("follows an expanded patch that arrives as the user reaches the bottom", async ({ page }) => {
const toolID = "prt_bottom_follow_patch"
const input = { patchText: "Update src/edit.ts" }
const timeline = await setupTimeline(page, {
messages: [
...history(20),
userMessage(),
assistantMessage([textPart("prt_bottom_follow_text", "Working")], { completed: false }),
],
settings: { editToolPartsExpanded: true },
reducedMotion: true,
})
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
await scroller.evaluate((element) => {
element.scrollTop = Math.max(0, element.scrollHeight - element.clientHeight - 300)
element.dispatchEvent(new WheelEvent("wheel", { bubbles: true, cancelable: true, deltaY: 300 }))
element.scrollTop = element.scrollHeight
})
await expect
.poll(() => scroller.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop))
.toBeLessThanOrEqual(1)
await timeline.send(partUpdated(toolPart(toolID, "patch", "running", input)))
await timeline.send(
partUpdated(
toolPart(toolID, "patch", "completed", input, {
metadata: {
files: [
{
file: "src/edit.ts",
status: "modified",
patch: createTwoFilesPatch(
"a/src/edit.ts",
"b/src/edit.ts",
Array.from({ length: 40 }, (_, index) => `export const value${index} = ${index}\n`).join(""),
Array.from({ length: 40 }, (_, index) => `export const value${index} = ${index + 1}\n`).join(""),
),
additions: 40,
deletions: 40,
},
],
},
}),
),
)
await timeline.waitForPart(toolID)
await expect
.poll(() => scroller.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop))
.toBeLessThanOrEqual(1)
})
test("does not reverse visible rows when the user wheels during shell remeasurement", async ({ page }, testInfo) => {
const shellID = "prt_wheel_01_shell"
const followingID = "prt_wheel_02_following"
@@ -18,7 +18,7 @@ const branchDiffs = [
),
]
test("keeps the review tree and terminal sized when both panels are open", async ({ page }) => {
test("uses side placement by default and supports the terminal across the bottom", async ({ page }) => {
test.setTimeout(120_000)
await page.setViewportSize({ width: 1400, height: 900 })
await mockOpenCodeServer(page, {
@@ -27,7 +27,7 @@ test("keeps the review tree and terminal sized when both panels are open", async
id: projectID,
worktree: directory,
vcs: "git",
name: "review-terminal-stacked",
name: "review-terminal-bottom",
time: { created: 1700000000000, updated: 1700000000000 },
sandboxes: [],
},
@@ -45,7 +45,7 @@ test("keeps the review tree and terminal sized when both panels are open", async
sessions: [
{
id: sessionID,
slug: "review-terminal-stacked",
slug: "review-terminal-bottom",
projectID,
directory,
title,
@@ -138,7 +138,22 @@ test("keeps the review tree and terminal sized when both panels are open", async
await page.keyboard.press("Control+Backquote")
await expect(page.locator("#terminal-panel")).toBeVisible()
await expectTree(page, 2_773, "action.yml")
await expectStackGeometry(page)
await expectSideGeometry(page)
await page.evaluate(() => {
const settings = JSON.parse(localStorage.getItem("settings.v3") ?? "{}")
localStorage.setItem(
"settings.v3",
JSON.stringify({ ...settings, general: { ...settings.general, terminalPlacement: "bottom" } }),
)
})
await page.reload()
await expectSessionReady(page, { server, sessionID, title })
await expect(page.locator("#review-panel")).toBeVisible()
await page.keyboard.press("Control+Backquote")
await expect(page.locator("#terminal-panel")).toBeVisible()
await expectTree(page, 2_773, "action.yml")
await expectBottomGeometry(page)
})
async function expectTree(page: Page, total: number, file: string) {
@@ -163,23 +178,51 @@ async function expectMountedTree(page: Page, total: number) {
expect(state.rows).toBeLessThanOrEqual(60)
}
async function expectStackGeometry(page: Page) {
async function expectSideGeometry(page: Page) {
const geometry = await page.evaluate(() => {
const review = document.querySelector<HTMLElement>("#review-panel")!.getBoundingClientRect()
const terminal = document.querySelector<HTMLElement>("#terminal-panel")!.getBoundingClientRect()
return {
reviewLeft: review.left,
reviewRight: review.right,
terminalLeft: terminal.left,
terminalRight: terminal.right,
terminalTop: terminal.top,
reviewTop: review.top,
}
})
expect(Math.abs(geometry.terminalLeft - geometry.reviewLeft)).toBeLessThanOrEqual(1)
expect(Math.abs(geometry.terminalRight - geometry.reviewRight)).toBeLessThanOrEqual(1)
expect(geometry.terminalTop).toBeGreaterThan(geometry.reviewTop)
}
async function expectBottomGeometry(page: Page) {
const geometry = await page.evaluate(() => {
const review = document.querySelector<HTMLElement>("#review-panel")!
const terminal = document.querySelector<HTMLElement>("#terminal-panel")!
const terminalRect = terminal.getBoundingClientRect()
const reviewParent = review.parentElement!.getBoundingClientRect()
const terminalParent = terminal.parentElement!.getBoundingClientRect()
const sidebar = review.querySelector<HTMLElement>('[data-slot="session-review-v2-sidebar"]')!
return {
review: review.getBoundingClientRect().height,
reviewBottom: review.getBoundingClientRect().bottom,
reviewParent: reviewParent.height,
terminal: terminal.getBoundingClientRect().height,
terminal: terminalRect.height,
terminalLeft: terminalRect.left,
terminalRight: terminalRect.right,
terminalTop: terminalRect.top,
terminalParent: terminalParent.height,
sidebar: sidebar.getBoundingClientRect().width,
viewport: window.innerWidth,
}
})
expect(Math.abs(geometry.review - geometry.reviewParent)).toBeLessThanOrEqual(1)
expect(Math.abs(geometry.terminal - geometry.terminalParent)).toBeLessThanOrEqual(1)
expect(geometry.terminalTop - geometry.reviewBottom).toBeGreaterThanOrEqual(7)
expect(geometry.terminalTop - geometry.reviewBottom).toBeLessThanOrEqual(9)
expect(geometry.terminalLeft).toBeLessThanOrEqual(9)
expect(geometry.terminalRight).toBeGreaterThanOrEqual(geometry.viewport - 9)
expect(geometry.sidebar).toBeGreaterThanOrEqual(240)
}
@@ -0,0 +1,48 @@
import { expect, test, type Route } from "@playwright/test"
const server = "http://127.0.0.1:4097"
test("nested server dialog keeps focus inside the top layer", async ({ page }) => {
await page.addInitScript((server) => {
localStorage.setItem("opencode.global.dat:server", JSON.stringify({ list: [server] }))
}, server)
await page.route("**/*", async (route) => {
const url = new URL(route.request().url())
if (url.origin !== server) return route.fallback()
if (url.pathname === "/api/event") {
return route.fulfill({
status: 200,
contentType: "text/event-stream",
body: 'data: {"id":"evt_connected","type":"server.connected","data":{}}\n\n',
})
}
if (url.pathname === "/api/global/health" || url.pathname === "/api/health") {
return json(route, { healthy: true, version: "2.0.0" })
}
return json(route, {})
})
await page.goto("/")
await page.keyboard.press("Control+,")
const settings = page.locator(".settings-dialog")
await expect(settings).toBeVisible()
await settings.getByRole("tab", { name: "Servers" }).click()
await settings.getByRole("button", { name: "Add server" }).click()
const editor = page.getByRole("dialog", { name: "Add server" })
await expect(editor.getByPlaceholder("http://localhost:4096")).toBeFocused()
const username = editor.getByPlaceholder("username")
const password = editor.getByPlaceholder("password")
await username.click()
await expect(username).toBeFocused()
await username.fill("kit")
await expect(username).toHaveValue("kit")
await page.keyboard.press("Tab")
await expect(password).toBeFocused()
await password.fill("secret")
await expect(password).toHaveValue("secret")
})
function json(route: Route, body: unknown, status = 200) {
return route.fulfill({ status, contentType: "application/json", body: JSON.stringify(body) })
}
@@ -99,7 +99,7 @@ test.describe("regression: session timeline local row state", () => {
await wrapper.evaluate((element) => {
;(element as HTMLElement).dataset.regressionMarker = "before-stream"
})
await wrapper.locator('[data-slot="collapsible-trigger"]').first().click()
await wrapper.locator('[data-scope="apply-patch"] button').click()
await expectExpanded(wrapper, false)
events.push(...textEvents())
@@ -179,8 +179,8 @@ test.describe("regression: session timeline local row state", () => {
await expectSessionTitle(page, title)
const wrapper = page.locator(`[data-timeline-part-id="${editPartID}"]`).first()
const trigger = wrapper.locator('[data-slot="collapsible-trigger"]').first()
const diff = wrapper.locator('[data-component="edit-content"]').first()
const trigger = wrapper.locator('[data-component="sticky-accordion-header"]')
const diff = wrapper.locator('[data-component="apply-patch-file-diff"]').first()
await expectAppVisible(diff)
await expect.poll(() => wrapper.evaluate((element) => element.getBoundingClientRect().height)).toBeGreaterThan(500)
const samples = await wrapper.evaluate(async (element) => {
@@ -190,8 +190,8 @@ test.describe("regression: session timeline local row state", () => {
for (const offset of [0, 120, 240, 360, 480]) {
root.scrollBy(0, offset - (result.at(-1)?.offset ?? 0))
await new Promise(requestAnimationFrame)
const trigger = element.querySelector<HTMLElement>('[data-slot="collapsible-trigger"]')!
const diff = element.querySelector<HTMLElement>('[data-component="edit-content"]')!
const trigger = element.querySelector<HTMLElement>('[data-component="sticky-accordion-header"]')!
const diff = element.querySelector<HTMLElement>('[data-component="apply-patch-file-diff"]')!
result.push({
offset,
trigger: trigger.getBoundingClientRect().y,
@@ -202,7 +202,7 @@ test.describe("regression: session timeline local row state", () => {
return result
})
expect(samples[0]!.trigger).toBeLessThan(samples[0]!.diff)
expect(samples[0]!.trigger).toBeGreaterThanOrEqual(samples[0]!.diff)
expect(samples.every((sample) => Math.abs(sample.trigger - samples[0]!.trigger) <= 1)).toBe(true)
expect(samples.every((sample) => sample.trigger < sample.bottom)).toBe(true)
})
@@ -234,7 +234,9 @@ async function readToolState(page: Page) {
.evaluate(
(element, textPartID) => ({
expanded: (() => {
const trigger = element.querySelector('[data-slot="collapsible-trigger"]')
const trigger =
element.querySelector('[data-scope="apply-patch"] button') ??
element.querySelector('[data-slot="collapsible-trigger"]')
const aria = trigger?.getAttribute("aria-expanded")
if (aria === "true") return true
if (aria === "false") return false
@@ -409,7 +411,9 @@ function eventValue<Type extends OpenCodeEvent["type"]>(
}
function readExpanded(element: Element) {
const trigger = element.querySelector('[data-slot="collapsible-trigger"]')
const trigger =
element.querySelector('[data-scope="apply-patch"] button') ??
element.querySelector('[data-slot="collapsible-trigger"]')
const aria = trigger?.getAttribute("aria-expanded")
if (aria === "true") return true
if (aria === "false") return false
@@ -1,5 +1,12 @@
import { expect, test } from "@playwright/test"
import { assistantMessage, setupTimeline, toolPart, userMessage } from "../performance/timeline-stability/fixture"
import { createTwoFilesPatch } from "diff"
import {
assistantMessage,
setupTimeline,
textPart,
toolPart,
userMessage,
} from "../performance/timeline-stability/fixture"
test("renders completed write content", async ({ page }) => {
const id = "prt_file_projection_write"
@@ -47,5 +54,103 @@ test("renders a completed single-file patch", async ({ page }) => {
settings: { editToolPartsExpanded: true },
})
await expect(page.locator(`[data-timeline-part-id="${id}"] [data-component="apply-patch-file-diff"]`)).toBeVisible()
const wrapper = page.locator(`[data-timeline-part-id="${id}"]`)
const file = wrapper.locator('[data-scope="apply-patch"]')
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
await expect(file.getByRole("button")).toHaveAttribute("aria-expanded", "false")
await expect(wrapper.locator('[data-component="apply-patch-file-diff"]')).toHaveCount(0)
await file.getByRole("button").click()
await expect(wrapper.locator('[data-component="apply-patch-file-diff"]')).toBeVisible()
await expect
.poll(() => scroller.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop))
.toBeLessThanOrEqual(1)
await file.getByRole("button").click()
await expect(wrapper.locator('[data-component="apply-patch-file-diff"]')).toHaveCount(0)
await expect
.poll(() => scroller.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop))
.toBeLessThanOrEqual(1)
await file.getByRole("button").click()
await expect(wrapper.locator('[data-component="apply-patch-file-diff"]')).toBeVisible()
await expect
.poll(() => scroller.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop))
.toBeLessThanOrEqual(1)
})
test("keeps an expanded file diff header at the same viewport position", async ({ page }) => {
const id = "prt_file_projection_anchored_patch"
const before = Array.from({ length: 80 }, (_, index) => `export const value${index} = ${index}\n`).join("")
const after = before.replaceAll(" = ", " = compute(").replaceAll("\n", ")\n")
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([
toolPart(
id,
"patch",
"completed",
{ patchText: "Update src/anchored.ts" },
{
metadata: {
files: [
{
file: "src/anchored.ts",
status: "modified",
patch: createTwoFilesPatch("a/src/anchored.ts", "b/src/anchored.ts", before, after),
additions: 80,
deletions: 80,
},
],
},
},
),
textPart("prt_after_anchored_patch", "The diff is ready.\n\n".repeat(4)),
]),
],
viewport: { width: 1200, height: 600 },
})
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
const wrapper = page.locator(`[data-timeline-part-id="${id}"]`)
const trigger = wrapper.getByRole("button")
await scroller.evaluate((element) => {
element.scrollTop = element.scrollHeight - element.clientHeight - 0.25
})
await expect(trigger).toBeInViewport()
await expect
.poll(() => scroller.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop))
.toBeLessThanOrEqual(0.5)
await trigger.dispatchEvent("wheel", { deltaY: -1, deltaMode: 0 })
await trigger.dispatchEvent("pointerdown")
const y = await trigger.evaluate((element) => element.getBoundingClientRect().y)
await trigger.dispatchEvent("click")
await expect(wrapper.locator('[data-component="apply-patch-file-diff"]')).toBeVisible()
await expect
.poll(() => trigger.evaluate((element, initialY) => Math.abs(element.getBoundingClientRect().y - initialY), y))
.toBeLessThanOrEqual(5)
const scrollTop = await scroller.evaluate((element) => element.scrollTop)
await scroller.hover()
await page.mouse.wheel(0, 200)
await timeline.settle(40)
const scrolled = await scroller.evaluate((element, initial) => element.scrollTop - initial, scrollTop)
expect(scrolled).toBeGreaterThan(50)
expect(scrolled).toBeLessThan(400)
const expandedY = await trigger.evaluate((element) => element.getBoundingClientRect().y)
await trigger.click()
await expect(wrapper.locator('[data-component="apply-patch-file-diff"]')).toHaveCount(0)
await expect
.poll(() =>
scroller.evaluate((element) => Math.abs(element.scrollHeight - element.clientHeight - element.scrollTop)),
)
.toBeLessThanOrEqual(1)
await expect.poll(() => trigger.evaluate((element) => element.getBoundingClientRect().y)).toBeGreaterThan(expandedY)
await trigger.click()
await expect(wrapper.locator('[data-component="apply-patch-file-diff"]')).toBeVisible()
await expect
.poll(() => scroller.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop))
.toBeLessThanOrEqual(1)
})
@@ -2,7 +2,7 @@ import { expect, test } from "@playwright/test"
import { assistantMessage, setupTimeline, toolPart, userMessage } from "../performance/timeline-stability/fixture"
import { createTwoFilesPatch } from "diff"
test("preserves nested patch file state through outer collapse and reopen", async ({ page }) => {
test("keeps patch file disclosures independent", async ({ page }) => {
const patchID = "prt_nested_patch"
const files = [patchFile("src/a.ts", "modified"), patchFile("src/b.ts", "added"), patchFile("src/old.ts", "deleted")]
await setupTimeline(page, {
@@ -21,15 +21,17 @@ test("preserves nested patch file state through outer collapse and reopen", asyn
settings: { editToolPartsExpanded: true },
})
const wrapper = page.locator(`[data-timeline-part-id="${patchID}"]`)
const outer = wrapper.locator('[data-slot="collapsible-trigger"]').first()
const modified = wrapper.locator('[data-scope="apply-patch"] [data-type="update"]')
const deleted = wrapper.locator('[data-scope="apply-patch"] [data-type="delete"]')
await expect(wrapper.locator('[data-scope="apply-patch"] [aria-expanded="false"]')).toHaveCount(3)
await deleted.getByRole("button").click()
await expect(deleted.getByRole("button")).toHaveAttribute("aria-expanded", "true")
await outer.click()
await expect(outer).toHaveAttribute("aria-expanded", "false")
await outer.click()
await expect(outer).toHaveAttribute("aria-expanded", "true")
await expect(deleted.getByRole("button")).toHaveAttribute("aria-expanded", "true")
await expect(modified.getByRole("button")).toHaveAttribute("aria-expanded", "false")
await modified.getByRole("button").click()
await expect(modified.getByRole("button")).toHaveAttribute("aria-expanded", "true")
await deleted.getByRole("button").click()
await expect(deleted.getByRole("button")).toHaveAttribute("aria-expanded", "false")
await expect(modified.getByRole("button")).toHaveAttribute("aria-expanded", "true")
})
function patchFile(file: string, status: "added" | "modified" | "deleted") {
@@ -1,5 +1,6 @@
import { expect, test } from "@playwright/test"
import {
assistantID,
assistantMessage,
completedAssistantInfo,
messageUpdated,
@@ -8,9 +9,13 @@ import {
renderedPartID,
setupTimeline,
shell,
sessionID,
status,
stepStarted,
textPart,
toolCalled,
toolInputEnded,
toolInputStarted,
userMessage,
} from "../performance/timeline-stability/fixture"
@@ -34,7 +39,56 @@ for (const expanded of [false, true]) {
})
}
test("shows and expands a running shell command without shimmering it", async ({ page }) => {
test("transitions a streaming shell from writing through command execution", async ({ page }) => {
const id = "prt_shell_streaming_input"
const command = "printf ready"
const timeline = await setupTimeline(page, {
messages: [userMessage(), assistantMessage([], { completed: false })],
})
await timeline.send(toolInputStarted({ sessionID, assistantMessageID: assistantID, id, name: "shell" }))
const tool = page.locator(`[data-timeline-part-id="${id}"]`)
const title = tool.locator('[data-slot="basic-tool-tool-title"]')
const titleShimmer = title.locator('[data-component="text-shimmer"]')
const subtitle = tool.locator('[data-slot="basic-tool-tool-subtitle"]')
await expect(titleShimmer).toHaveAttribute("aria-label", "Shell")
await expect(titleShimmer).toHaveAttribute("data-active", "true")
await expect(subtitle).toHaveText("Writing command...")
await expect(subtitle.locator('[data-component="text-shimmer"]')).toHaveCount(0)
await expect(tool.locator('[data-component="shell-submessage"]')).toHaveCount(0)
await expect(tool.locator('[data-slot="collapsible-trigger"]')).toHaveCSS("height", "28px")
await expect(tool.locator('[data-component="tool-trigger"]')).toHaveCSS("gap", "6px")
await expect(title).toHaveCSS("font-size", "13px")
await expect(title).toHaveCSS("font-family", "Inter, sans-serif")
await expect(title).toHaveCSS("font-weight", "530")
await expect(title).toHaveCSS("line-height", "16px")
await expect(title).toHaveCSS("color", "rgb(22, 22, 22)")
await expect(subtitle).toHaveCSS("font-size", "13px")
await expect(subtitle).toHaveCSS("font-family", "Inter, sans-serif")
await expect(subtitle).toHaveCSS("font-weight", "440")
await expect(subtitle).toHaveCSS("line-height", "16px")
await expect(subtitle).toHaveCSS("color", "rgb(92, 92, 92)")
const input = JSON.stringify({ command })
await timeline.send(toolInputEnded({ sessionID, assistantMessageID: assistantID, id, text: input }))
await expect(titleShimmer).toHaveAttribute("data-active", "true")
await expect(subtitle).toHaveText(command)
await expect(tool).not.toContainText("Writing command...")
await timeline.send(
toolCalled({
sessionID,
assistantMessageID: assistantID,
id,
input: { command },
executed: true,
}),
)
await expect(titleShimmer).toHaveAttribute("data-active", "true")
await expect(subtitle).toHaveText(command)
})
test("shimmers and expands a running shell command", async ({ page }) => {
const id = "prt_shell_running_command"
const command = "sleep 10 && echo done"
await setupTimeline(page, {
@@ -44,8 +98,10 @@ test("shows and expands a running shell command without shimmering it", async ({
const tool = page.locator(`[data-timeline-part-id="${id}"]`)
await expect(tool.locator('[data-component="text-shimmer"]')).toHaveAttribute("data-active", "true")
await expect(tool).not.toContainText("Writing command...")
await expect(tool.locator('[data-component="shell-submessage"]')).toHaveText(command)
await expect(tool.locator('[data-component="shell-submessage"] [data-component="text-shimmer"]')).toHaveCount(0)
await expect(tool.locator('[data-slot="collapsible-trigger"]')).toHaveCSS("height", "28px")
await tool.locator('[data-slot="collapsible-trigger"]').click()
await expect(tool.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "true")
await expect(tool.locator('[data-slot="bash-pre"]')).toContainText("still running")
@@ -1,6 +1,6 @@
import { expect, test } from "@playwright/test"
import type { SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/client/promise"
import { event, session, sessionID, setupTimeline } from "../performance/timeline-stability/fixture"
import { event, session, sessionID, setupTimeline, toolPart } from "../performance/timeline-stability/fixture"
const user = { id: "msg_user", type: "user", text: "Run it", time: { created: 1 } } satisfies SessionMessageInfo
@@ -73,6 +73,81 @@ test("renders current protocol notices in CLI order", async ({ page }) => {
expect(ownerWarnings).toEqual([])
})
test("shows a delegating row while subagent input streams", async ({ page }) => {
await setupTimeline(page, {
sessionMessages: [
user,
{
...assistant(false),
content: [toolPart("call_subagent", "subagent", "streaming", {})],
},
],
})
const delegating = page.locator('[data-component="task-tool-delegating"]')
await expect(delegating).toBeVisible()
await expect(delegating.locator('[data-component="text-shimmer"]')).toHaveAttribute(
"aria-label",
"Delegating agent...",
)
const icon = delegating.locator('[data-slot="icon-svg"]')
await expect(icon.locator('use[href="#opencode-v2-icon-subagent"]')).toBeVisible()
await expect(icon).toHaveCSS("color", "rgb(174, 174, 174)")
await expect(page.locator('[data-component="task-tool-card"]')).toHaveCount(0)
await expect(page.locator('[data-timeline-row="Thinking"]')).toHaveCount(0)
})
test("renders the moved location notice in its compact timeline style", async ({ page }) => {
const directory = `/Users/usrnk1/Developer/opencode/${"nested-directory/".repeat(24)}session`
await page.setViewportSize({ width: 480, height: 720 })
await setupTimeline(page, {
sessionMessages: [
user,
{
id: "msg_location",
type: "location-switched",
location: { directory },
time: { created: 2 },
},
],
})
const notice = page.locator('[data-slot="session-timeline-notice"][data-type="location-switched"]')
const label = notice.locator('[data-slot="session-timeline-notice-label"]')
const value = notice.locator('[data-slot="session-timeline-notice-value"]')
const tooltipTrigger = notice.locator('[data-component="tooltip-v2-trigger"]')
await expect(label).toHaveText("Moved to")
await expect(value).toHaveText(directory)
await expect(notice).not.toContainText("·")
await expect(notice.locator("svg")).toHaveCount(0)
await expect(notice).toHaveCSS("height", "28px")
await expect(notice).toHaveCSS("gap", "8px")
await expect(notice).toHaveCSS("padding-top", "4px")
await expect(notice).toHaveCSS("padding-bottom", "4px")
await expect(label).toHaveCSS("font-size", "13px")
await expect(label).toHaveCSS("font-weight", "530")
await expect(label).toHaveCSS("line-height", "13px")
await expect(label).toHaveCSS("color", "rgb(128, 128, 128)")
await expect(value).toHaveCSS("font-size", "13px")
await expect(value).toHaveCSS("font-weight", "440")
await expect(value).toHaveCSS("line-height", "13px")
await expect(value).toHaveCSS("color", "rgb(128, 128, 128)")
await expect(value).toHaveCSS("text-overflow", "ellipsis")
await expect(value).toHaveCSS("white-space", "nowrap")
await expect(value).toHaveAttribute("dir", "ltr")
await expect.poll(() => value.evaluate((element) => element.scrollWidth > element.clientWidth)).toBe(true)
const tooltip = page.getByText("Session working directory changed", { exact: true })
await label.hover()
await expect(tooltip).toBeVisible()
await page.mouse.move(0, 0)
await expect(tooltip).toBeHidden()
await tooltipTrigger.focus()
await expect(tooltipTrigger).toBeFocused()
await expect(tooltip).toBeVisible()
})
test("moves blocking work to the background with Ctrl+B", async ({ page }) => {
await setupTimeline(page, { sessionMessages: [user, assistant(false, true)] })
const card = page.locator('[data-component="task-tool-card"]')
@@ -81,7 +156,24 @@ test("moves blocking work to the background with Ctrl+B", async ({ page }) => {
await expect(card).not.toContainText("(background)")
await expect(page.getByText("Called `subagent`", { exact: false })).toHaveCount(0)
await expect(page.locator('[data-component="background-tool-control"]')).toHaveCount(0)
await expect(page.locator('[data-action="session-background-toggle"]')).toContainText("Move 1 subagent to background")
const hint = page.locator('[data-component="session-background-hint"]')
const hintPrefix = hint.locator('[data-slot="session-background-hint-prefix"]')
await expect(hint).toBeVisible()
await expect(page.locator('[data-timeline-row="Thinking"]')).toHaveCount(0)
await expect
.poll(async () => {
const [cardBox, hintBox, prefixBox] = await Promise.all([
card.boundingBox(),
hint.boundingBox(),
hintPrefix.boundingBox(),
])
if (!cardBox || !hintBox || !prefixBox) return undefined
return {
aligned: Math.abs(cardBox.x - prefixBox.x) < 2,
ordered: cardBox.y < hintBox.y,
}
})
.toEqual({ aligned: true, ordered: true })
const request = page.waitForRequest(
(request) =>
@@ -104,10 +196,10 @@ test("navigates from a running subagent card and hides background controls in th
sessionStatus: { [sessionID]: { type: "busy" }, [childID]: { type: "busy" } },
})
await expect(page.locator('[data-action="session-background-toggle"]')).toContainText("Move 1 subagent to background")
await expect(page.getByText(/move running work to the background/i)).toBeVisible()
await page.locator('[data-component="task-tool-card"]').click()
await expect(page).toHaveURL(new RegExp(`/session/${childID}$`))
await expect(page.locator('[data-component="session-background-dock"]')).toHaveCount(0)
await expect(page.getByText(/move running work to the background/i)).toHaveCount(0)
})
test("shows a badge for active background work", async ({ page }) => {
@@ -118,7 +210,14 @@ test("shows a badge for active background work", async ({ page }) => {
sessionStatus: { [childID]: { type: "busy" } },
})
await expect(page.locator('[data-component="session-background-dock"]')).toContainText("1 subagent in background")
await page.getByRole("button", { name: "Session details" }).click()
const summary = page.getByRole("button", { name: "1 item running in background" })
await expect(summary).toContainText("1")
await expect(summary).toContainText("Running work in background")
await summary.click()
await expect(
page.locator('[data-component="session-background-list"]').getByText("Agent", { exact: true }),
).toBeVisible()
})
test("separates blocking and already-backgrounded work into two rows", async ({ page }) => {
@@ -193,10 +292,15 @@ test("separates blocking and already-backgrounded work into two rows", async ({
},
})
const dock = page.locator('[data-component="session-background-dock"]')
const backgroundCard = page.locator('[data-timeline-part-id="call_backgrounded"]')
await expect(dock).toContainText("Move 1 subagent to background")
await expect(dock.getByText("Running 1 shell and 1 subagent in background", { exact: true })).toBeVisible()
await expect(page.getByText(/move running work to the background/i)).toBeVisible()
await page.getByRole("button", { name: "Session details" }).click()
const summary = page.getByRole("button", { name: "2 items running in background" })
await expect(summary).toContainText("2")
await summary.click()
const list = page.locator('[data-component="session-background-list"]')
await expect(list).toContainText("Background task")
await expect(list).toContainText("sleep 120")
await expect(backgroundCard).toContainText("Background task (background)")
await expect(backgroundCard.locator('[data-component="session-progress-indicator-v2"]')).toBeVisible()
await expect(
@@ -1,6 +1,7 @@
import { expect, test } from "@playwright/test"
import {
assistantMessage,
partUpdated,
setupTimeline,
status,
toolPart,
@@ -69,9 +70,126 @@ test.describe("session timeline projection", () => {
]) {
await expect(page.locator(`[data-timeline-part-id="${id}"]`).first(), id).toBeVisible()
}
const patch = page.locator('[data-timeline-part-id="prt_patch"]')
await expect(patch.getByText("1 file", { exact: true })).toBeVisible()
await expect(patch.getByRole("button", { name: "Patch 1 file", exact: true })).toHaveCount(0)
await expect(patch.getByRole("button")).toHaveCount(1)
await expect(patch.locator('[data-scope="apply-patch"] button[aria-expanded="false"]')).toHaveCount(1)
await expect(patch.locator('[data-slot="message-part-title-filename"]')).toHaveCount(0)
await expect(patch.locator('[data-slot="message-part-actions"]')).toHaveCount(0)
const edit = page.locator('[data-timeline-part-id="prt_edit"]')
await expect(edit.locator('[data-component="apply-patch-tool"]')).toBeVisible()
await expect(edit.locator('[data-slot="basic-tool-tool-title"]')).toContainText("Edit")
await expect(page.locator('[data-timeline-part-id="prt_todo"]')).toHaveCount(0)
})
test("combines adjacent patch calls and repeated files into one group", async ({ page }) => {
const first = "prt_patch_first"
const second = "prt_patch_second"
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([
toolPart(
first,
"patch",
"completed",
{ patchText: "Update src/first.ts" },
{
metadata: { files: [patchFile("src/first.ts", "modified")] },
},
),
]),
],
})
const initial = page.locator(`[data-timeline-part-id="${first}"]`)
const initialFile = initial.locator('[data-scope="apply-patch"] [data-type="update"]')
await expect(initialFile).toBeVisible()
await initialFile.getByRole("button").click()
await expect(initialFile.getByRole("button")).toHaveAttribute("aria-expanded", "true")
await initial.evaluate((element) => {
const row = element.closest<HTMLElement>("[data-timeline-key]")
if (row) row.dataset.patchRow = "stable"
})
await timeline.send(
partUpdated(toolPart(second, "patch", "running", { patchText: "Update more files" }, { metadata: {} })),
)
const group = page.locator(`[data-timeline-part-ids="${first},${second}"]`)
await expect(group.locator("xpath=ancestor::*[@data-timeline-key]")).toHaveAttribute("data-patch-row", "stable")
await expect(group.locator('[data-slot="apply-patch-filename"]')).toHaveText(["first.ts"])
await expect(group.locator('[data-scope="apply-patch"] [data-type="update"] button')).toHaveAttribute(
"aria-expanded",
"true",
)
await timeline.send(
partUpdated(
toolPart(
second,
"patch",
"completed",
{ patchText: "Update more files" },
{
metadata: {
files: [patchFile("src/first.ts", "modified"), patchFile("src/second.ts", "added")],
},
},
),
),
)
await expect(group.locator('[data-slot="apply-patch-filename"]')).toHaveText(["first.ts", "second.ts"])
await expect(group.locator('[data-scope="apply-patch"] [data-type="update"] button')).toHaveAttribute(
"aria-expanded",
"true",
)
await expect(group.locator('[data-scope="apply-patch"] [data-type="add"] button')).toHaveAttribute(
"aria-expanded",
"false",
)
await expect(page.locator(`[data-timeline-part-id="${first}"], [data-timeline-part-id="${second}"]`)).toHaveCount(0)
})
test("combines adjacent edit calls and repeated files into one group", async ({ page }) => {
const first = "prt_edit_first"
const second = "prt_edit_second"
await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([
toolPart(
first,
"edit",
"completed",
{ path: "src/first.ts", oldString: "one", newString: "two" },
{
metadata: { files: [patchFile("src/first.ts", "modified")] },
},
),
toolPart(
second,
"edit",
"completed",
{ path: "src/first.ts", oldString: "two", newString: "three" },
{
metadata: { files: [patchFile("src/first.ts", "modified")] },
},
),
]),
],
settings: { editToolPartsExpanded: true },
})
const group = page.locator(`[data-timeline-part-ids="${first},${second}"]`)
await expect(group.locator('[data-slot="basic-tool-tool-title"]')).toContainText("Edit")
await expect(group.getByText("1 file", { exact: true })).toBeVisible()
await expect(group.locator('[data-slot="apply-patch-filename"]')).toHaveText(["first.ts"])
await expect(group.locator('[data-scope="apply-patch"] button')).toHaveAttribute("aria-expanded", "true")
})
test("projects gaps, dividers, assistant parts, and errors together", async ({ page }) => {
const firstUser = userMessage(
[
@@ -196,11 +314,7 @@ function patchPart(id: string) {
{ patchText: "Update the projected files" },
{
metadata: {
files: [
patchFile("src/a.ts", "modified"),
patchFile("src/b.ts", "added"),
patchFile("src/old.ts", "deleted"),
],
files: [patchFile("src/a.ts", "modified")],
},
},
)
@@ -127,19 +127,16 @@ test("labels skill tools from IDs and result metadata", async ({ page }) => {
],
})
await expect(page.locator(`[data-timeline-part-id="${pending}"] [data-component="text-shimmer"]`)).toHaveAttribute(
"aria-label",
"sample-skill",
)
await expect(page.locator(`[data-timeline-part-id="${completed}"] [data-component="text-shimmer"]`)).toHaveAttribute(
"aria-label",
"OpenCode",
)
for (const id of [pending, completed]) {
for (const [id, name] of [
[pending, "sample-skill"],
[completed, "OpenCode"],
] as const) {
const skill = page.locator(`[data-timeline-part-id="${id}"]`)
await expect(skill.locator('[data-slot="skill-tool-label"]')).toHaveText("Skill")
await expect(skill.locator('[data-slot="skill-tool-separator"]')).toHaveText("·")
await expect(skill.locator('use[href="#opencode-v2-icon-post-skill"]')).toBeVisible()
const loaded = skill.locator('[data-component="tool-loaded-item"]')
await expect(loaded).toHaveAttribute("aria-label", `Loaded ${name} skill`)
await expect(loaded.locator('[data-slot="tool-loaded-label"]')).toHaveText("Loaded")
await expect(loaded.locator('[data-slot="tool-loaded-kind"]')).toHaveText("skill")
await expect(loaded.locator('[data-component="text-shimmer"]')).toHaveAttribute("aria-label", name)
}
})
@@ -9,10 +9,12 @@ const sessionID = "ses_terminal_composer_focus"
const ptyID = "pty_terminal_composer_focus"
const newPtyID = "pty_terminal_composer_focus_new"
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
const ptyInput: string[] = []
test.use({ viewport: { width: 1440, height: 900 } })
test.beforeEach(async ({ page }) => {
ptyInput.length = 0
await mockOpenCodeServer(page, {
directory,
project: {
@@ -70,7 +72,22 @@ test.beforeEach(async ({ page }) => {
body: JSON.stringify({ location: ptyLocation(), data: { ticket: "e2e-ticket", expires_in: 60 } }),
}),
)
await page.routeWebSocket(new RegExp(`/api/pty/${ptyID}/connect`), () => undefined)
await page.routeWebSocket(new RegExp(`/api/pty/${ptyID}/connect`), (ws) => {
ws.onMessage((message) => ptyInput.push(message.toString()))
})
})
test("clears the terminal line with Command+Delete", async ({ page }) => {
await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`)
await expectSessionTitle(page, "Terminal composer focus")
const terminal = page.locator('[data-component="terminal"]')
await page.keyboard.press("Control+Backquote")
await expect(terminal.locator("textarea")).toHaveCount(1)
await page.keyboard.press("Meta+Backspace")
await expect.poll(() => ptyInput.join("")).toBe("\x15")
})
test("routes typing to the composer unless the open terminal is focused", async ({ page }) => {
+1
View File
@@ -48,6 +48,7 @@
"typescript": "catalog:",
"vite": "8.2.1",
"vite-plugin-icons-spritesheet": "3.0.1",
"vite-plugin-pwa": "1.3.0",
"vite-plugin-solid": "2.11.14"
},
"dependencies": {
+11
View File
@@ -15,3 +15,14 @@
/*.css
Content-Type: text/css
/site.webmanifest
Content-Type: application/manifest+json
/sw.js
Content-Type: application/javascript
Cache-Control: no-cache
/registerSW.js
Content-Type: application/javascript
Cache-Control: no-cache
+3 -1
View File
@@ -61,7 +61,9 @@ if (import.meta.env.VITE_SENTRY_DSN) {
})
}
if (root instanceof HTMLElement) {
if (root instanceof HTMLElement && root.dataset.opencodeMounted === undefined) {
// Lazy chunks can import the entry chunk back under a distinct URL, so claim the root before async startup.
root.dataset.opencodeMounted = ""
void loadInitialLocale().then((locale) => {
const auth = authFromToken(new URLSearchParams(location.search).get("auth_token"))
clearAuthToken()
+3 -2
View File
@@ -3,7 +3,7 @@ import { type HomeProjectSelection, useLayout } from "@/shell/state/layout"
import { ServerConnection } from "@/runtime/server/registry"
import { useTabs } from "@/shell/tabs/tabs"
import { toggleHomeProjectSelection } from "@/shell/layout/helpers"
import { createEffect, createMemo } from "solid-js"
import { createEffect, createMemo, startTransition } from "solid-js"
export function createHomeController() {
const layout = useLayout()
@@ -50,7 +50,8 @@ export function createHomeController() {
selection: {
value: selection,
set: setSelection,
focusServer: (conn: ServerConnection.Any) => setSelection({ server: ServerConnection.key(conn) }),
focusServer: (conn: ServerConnection.Any) =>
void startTransition(() => setSelection({ server: ServerConnection.key(conn) })),
},
server: {
list: global.servers.list,
+1 -3
View File
@@ -1,6 +1,5 @@
import { createPromptProjectController } from "@/new-session/project/selector"
import { useSettingsDialog } from "@/settings/command"
import { useTitlebarRightMount } from "@/shell/titlebar/titlebar"
import { useSettings } from "@/settings/model"
import { useTabs, type DraftTab } from "@/shell/tabs/tabs"
import { useSearchParams } from "@solidjs/router"
@@ -15,7 +14,6 @@ import { useNewSessionCommands } from "./commands"
/** The draft-only Session page. Submitting promotes the draft into a real Session. */
export default function NewSessionPage(props: { draftId: string }) {
const settings = useSettings()
const rightMount = useTitlebarRightMount()
const [search, setSearch] = useSearchParams<{ draftId?: string; prompt?: string }>()
const tabs = useTabs()
const openWorkspaces = useSettingsDialog("workspaces")
@@ -69,7 +67,7 @@ export default function NewSessionPage(props: { draftId: string }) {
return (
<div class="relative size-full overflow-hidden flex flex-col">
{suspendUntilPromptReady()}
<NewSessionStatus mount={rightMount()} visible={settings.visibility.status()} />
<NewSessionStatus visible={settings.visibility.status()} />
<div class="flex-1 min-h-0 flex flex-col gap-2 p-2">
<NewSessionView composer={model} project={project} workspace={workspace} />
</div>
+9 -14
View File
@@ -4,7 +4,6 @@ import { Icon } from "@opencode-ai/ui/icon"
import { Wordmark } from "@opencode-ai/ui/wordmark"
import { Show, createMemo, createSignal } from "solid-js"
import { createStore } from "solid-js/store"
import { Portal } from "solid-js/web"
import createPresence from "solid-presence"
import { Composer } from "@/composer/composer"
import type { ComposerModel } from "@/composer/model"
@@ -15,6 +14,7 @@ import {
type PromptProjectController,
} from "@/new-session/project/selector"
import { StatusPopover } from "@/shell/status/status-popover"
import { TitlebarRight } from "@/shell/titlebar/right-slot"
import { useLanguage } from "@/runtime/i18n/language"
import { useWorkspaceLocation } from "@/workspaces/location"
import { useProviders } from "@/providers/catalog/providers"
@@ -87,21 +87,16 @@ export function NewSessionView(props: {
)
}
export function NewSessionStatus(props: { mount: HTMLElement | null; visible: boolean }) {
export function NewSessionStatus(props: { visible: boolean }) {
const language = useLanguage()
return (
<Show when={props.mount} keyed>
{(mount) => (
<Portal mount={mount}>
<Show when={props.visible}>
<Tooltip appearance="standard" placement="bottom" value={language.t("status.popover.trigger")}>
<StatusPopover />
</Tooltip>
</Show>
</Portal>
)}
</Show>
<TitlebarRight>
<Show when={props.visible}>
<Tooltip appearance="standard" placement="bottom" value={language.t("status.popover.trigger")}>
<StatusPopover />
</Tooltip>
</Show>
</TitlebarRight>
)
}
+14 -1
View File
@@ -30,6 +30,7 @@ export const dict = {
"command.project.previous": "Previous project",
"command.project.next": "Next project",
"command.project.index": "Switch to project {{index}}",
"command.project.copyID": "Copy Project ID",
"command.provider.connect": "Connect provider",
"command.server.switch": "Switch server",
"command.settings.open": "Open settings",
@@ -93,6 +94,7 @@ export const dict = {
"command.session.fork.description": "Create a new session from a previous message",
"command.session.export": "Export session",
"command.session.export.description": "Export the full session transcript as JSON",
"command.session.copyID": "Copy Session ID",
"palette.search.placeholder": "Search files, commands, and sessions",
"palette.search.placeholder.home": "Search commands and sessions",
@@ -544,7 +546,6 @@ export const dict = {
"toast.context.noLineSelection.title": "No line selection",
"toast.context.noLineSelection.description": "Select a line range in a file tab first.",
"toast.session.unshare.success.title": "Session unshared",
"toast.session.unshare.success.description": "Session unshared successfully!",
"toast.session.unshare.failed.title": "Failed to unshare session",
@@ -554,6 +555,10 @@ export const dict = {
"toast.session.export.success.description": "Saved session to {{filename}}",
"toast.session.export.failed.title": "Failed to export session",
"toast.session.export.failed.description": "An error occurred while exporting the session",
"toast.session.copyID.failed.title": "Failed to copy session ID",
"toast.session.copyID.failed.description": "An error occurred while copying the session ID",
"toast.project.copyID.failed.title": "Failed to copy project ID",
"toast.project.copyID.failed.description": "An error occurred while copying the project ID",
"toast.session.listFailed.title": "Failed to load sessions for {{project}}",
"toast.project.reloadFailed.title": "Failed to reload {{project}}",
@@ -657,6 +662,10 @@ export const dict = {
"{{server}} is running OpenCode {{version}}, which isn't compatible with this app. Upgrade the server to OpenCode V2 to continue.",
"session.background.moveTasks": "Move {{tasks}} to background",
"session.background.inBackground": "Running {{tasks}} in background",
"session.background.moveInline": "Press {{keybind}} to move running work to the background",
"session.background.running": "Running work in background",
"session.background.runningCount.one": "{{count}} item running in background",
"session.background.runningCount.other": "{{count}} items running in background",
"session.background.combine": "{{first}} and {{second}}",
"session.background.shell.one": "{{count}} shell",
"session.background.shell.other": "{{count}} shells",
@@ -913,6 +922,10 @@ export const dict = {
"settings.general.row.shell.description": "Shell used by the terminal and agent tools",
"settings.general.row.shell.autoDefault": "Auto (Default)",
"settings.general.row.shell.terminalOnly": "terminal only",
"settings.general.row.terminalPlacement.title": "Terminal placement",
"settings.general.row.terminalPlacement.description": "Choose where the terminal opens in sessions",
"settings.general.row.terminalPlacement.side": "Side",
"settings.general.row.terminalPlacement.bottom": "Bottom",
"settings.general.row.appearance.title": "Appearance",
"settings.general.row.appearance.description": "Customise how OpenCode looks on your device",
"settings.general.row.colorScheme.title": "Color scheme",
@@ -89,6 +89,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
const focusInput = actions.focusInput
const sessionCommand = withCategory(language.t("command.category.session"))
const projectCommand = withCategory(language.t("command.category.project"))
const fileCommand = withCategory(language.t("command.category.file"))
const contextCommand = withCategory(language.t("command.category.context"))
const viewCommand = withCategory(language.t("command.category.view"))
@@ -126,6 +127,46 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
}
}
const copySessionID = async () => {
const sessionID = actions.session.identity.params.id
if (!sessionID) return
try {
await navigator.clipboard.writeText(sessionID)
showToast({
variant: "success",
icon: "circle-check",
title: language.t("common.copied"),
description: sessionID,
})
} catch (err) {
showToast({
variant: "error",
title: language.t("toast.session.copyID.failed.title"),
description: err instanceof Error ? err.message : language.t("toast.session.copyID.failed.description"),
})
}
}
const copyProjectID = async () => {
const projectID = actions.session.data.info()?.projectID
if (!projectID) return
try {
await navigator.clipboard.writeText(projectID)
showToast({
variant: "success",
icon: "circle-check",
title: language.t("common.copied"),
description: projectID,
})
} catch (err) {
showToast({
variant: "error",
title: language.t("toast.project.copyID.failed.title"),
description: err instanceof Error ? err.message : language.t("toast.project.copyID.failed.description"),
})
}
}
const openFile = () => {
void openDialog(
() => import("@/shell/commands/dialog"),
@@ -271,6 +312,12 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
disabled: !actions.session.identity.params.id,
onSelect: exportSession,
}),
sessionCommand({
id: "session.copyID",
title: language.t("command.session.copyID"),
disabled: !actions.session.identity.params.id,
onSelect: copySessionID,
}),
]
const fileCmds = () => {
@@ -294,6 +341,15 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
].filter((v) => !!v)
}
const projectCmds = () => [
projectCommand({
id: "project.copyID",
title: language.t("command.project.copyID"),
disabled: !actions.session.data.info()?.projectID,
onSelect: copyProjectID,
}),
]
const contextCmds = () => [
contextCommand({
id: "context.addSelection",
@@ -407,6 +463,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
command.register("session", () => [
...sessionCmds(),
...projectCmds(),
...fileCmds(),
...contextCmds(),
...viewCmds(),
+1 -1
View File
@@ -133,7 +133,7 @@ export function createActiveSessionRegion(input: {
const scroller = input.timeline.scroller()
if (!scroller || !isScrollKeyTarget(target ?? null, key)) return
if (scrollKeyOwner(scroller, target ?? null, key) !== scroller) return
input.timeline.view.markGesture(scroller)
input.timeline.view.markUserScroll(scroller)
return
}
if (event.key.length !== 1 || event.key === "Unidentified" || event.ctrlKey || event.metaKey) return
@@ -2,15 +2,12 @@ import { Show, type JSX } from "solid-js"
import { useLanguage } from "@/runtime/i18n/language"
import { SessionPermissionDock } from "@/session/requests/session-permission-dock"
import { SessionQuestionDock } from "@/session/requests/session-question-dock"
import { SessionBackgroundDock } from "@/session/requests/session-background-dock"
import type { SessionComposerRegionController } from "./session-composer-region-controller"
type SessionComposerRegionState = Pick<
SessionComposerRegionController["state"],
"questionRequest" | "permissionRequest" | "permissionResponding" | "decide" | "blocked"
> & {
background: Pick<SessionComposerRegionController["state"]["background"], "blocking" | "tasks" | "move">
}
>
export type SessionComposerRegionViewController = Pick<
SessionComposerRegionController,
@@ -32,9 +29,6 @@ export function SessionComposerRegion(props: {
}) {
const language = useLanguage()
const controller = props.controller
const background = () =>
controller.state.background.blocking().length > 0 || controller.state.background.tasks().length > 0
return (
<div
ref={controller.setDockRef}
@@ -81,22 +75,10 @@ export function SessionComposerRegion(props: {
</>
}
>
<Show when={background()}>
<div>
<SessionBackgroundDock
blocking={controller.state.background.blocking()}
tasks={controller.state.background.tasks()}
onBackground={() => void controller.state.background.move()}
/>
</div>
</Show>
<div
classList={{
"relative z-[70]": true,
}}
style={{
"margin-top": `${background() ? -36 : 0}px`,
}}
>
<Show
when={controller.child()}
@@ -1,13 +1,12 @@
import { createMemo, Show } from "solid-js"
import { createMemo } from "solid-js"
import { createMediaQuery } from "@solid-primitives/media"
import { Portal } from "solid-js/web"
import { useCommand } from "@/shell/commands/command"
import { useLanguage } from "@/runtime/i18n/language"
import { useSettings } from "@/settings/model"
import { useSessionLayout } from "@/session/session-layout"
import { reviewTooltipKeybind } from "@/shell/commands/tooltip-keybind"
import { StatusPopover } from "@/shell/status/status-popover"
import { useTitlebarRightMount } from "@/shell/titlebar/titlebar"
import { TitlebarRight } from "@/shell/titlebar/right-slot"
import { SessionHeaderActions, type SessionHeaderActionsState } from "./session-header-actions"
export function SessionHeader() {
@@ -28,15 +27,9 @@ export function SessionHeader() {
onReviewToggle: () => view().reviewPanel.toggle(),
}))
const rightMount = useTitlebarRightMount()
return (
<Show when={rightMount()} keyed>
{(mount) => (
<Portal mount={mount}>
<SessionHeaderActions state={actions()} />
</Portal>
)}
</Show>
<TitlebarRight>
<SessionHeaderActions state={actions()} />
</TitlebarRight>
)
}
@@ -63,6 +63,7 @@ export function createSessionRequestModel() {
return [
{
type: part.name as "shell" | "subagent",
partID: part.id,
id: typeof value === "string" ? value : undefined,
label: typeof label === "string" ? label : undefined,
},
@@ -93,11 +94,13 @@ export function createSessionRequestModel() {
const sessionID = part.state.metadata.sessionID
if (typeof sessionID !== "string" || completed.has(sessionID)) return []
const description = part.state.input.description
const agent = part.state.input.agent
return [
{
id: sessionID,
type: "subagent" as const,
label: typeof description === "string" ? description : sessionID,
agent: typeof agent === "string" ? agent : undefined,
},
]
})
@@ -1,83 +0,0 @@
import { useLanguage } from "@/runtime/i18n/language"
import { useCommand } from "@/shell/commands/command"
import { Keybind } from "@opencode-ai/ui/keybind"
import { For, createMemo } from "solid-js"
import { createStore } from "solid-js/store"
import { SessionBackgroundPullout } from "./session-background-pullout"
export function SessionBackgroundDock(props: {
blocking: { type: "shell" | "subagent"; id?: string; label?: string }[]
tasks: { id: string; type: "shell" | "subagent"; label: string }[]
onBackground: () => void
}) {
const language = useLanguage()
const command = useCommand()
const [store, setStore] = createStore({ collapsed: true })
const describe = (shells: number, subagents: number) => {
const shell = shells ? language.plural("session.background.shell", shells, { count: shells }) : undefined
const subagent = subagents
? language.plural("session.background.subagent", subagents, { count: subagents })
: undefined
if (shell && subagent) return language.t("session.background.combine", { first: shell, second: subagent })
return shell ?? subagent ?? ""
}
const summary = createMemo(() => {
const shells = props.tasks.filter((task) => task.type === "shell").length
return describe(shells, props.tasks.length - shells)
})
const moving = createMemo(() => {
const shells = props.blocking.filter((task) => task.type === "shell").length
const subagents = props.blocking.length - shells
const tasks = describe(shells, subagents)
return tasks ? language.t("session.background.moveTasks", { tasks }) : ""
})
const background = createMemo(() =>
summary() ? language.t("session.background.inBackground", { tasks: summary() }) : "",
)
const blocking = () => props.blocking.length > 0
const toggle = () => {
if (blocking()) {
props.onBackground()
return
}
setStore("collapsed", (value) => !value)
}
return (
<SessionBackgroundPullout
label={
<span class="flex flex-col items-start">
{blocking() && (
<span>
<span class="text-v2-text-text-muted">{moving()}</span>
<span class="pl-2">
<Keybind keys={command.keybindParts("session.background")} variant="neutral" />
</span>
</span>
)}
{!!props.tasks.length && <span class="text-v2-text-text-faint">{background()}</span>}
</span>
}
ariaLabel={[moving(), background()].filter(Boolean).join(". ")}
multiline={blocking() && props.tasks.length > 0}
collapsed={blocking() || store.collapsed}
collapsible={!blocking()}
onToggle={toggle}
collapseLabel={language.t("session.todo.collapse")}
expandLabel={language.t("session.todo.expand")}
>
<div class="px-4 pb-11 flex flex-col gap-1.5">
<For each={props.tasks}>
{(task) => (
<div class="flex min-w-0 items-baseline gap-2 text-13-regular">
<span class="shrink-0 text-13-medium text-text-strong">
{language.t(task.type === "shell" ? "ui.tool.shell" : "ui.tool.agent.default")}
</span>
<span class="truncate text-text-weak">{task.label}</span>
</div>
)}
</For>
</div>
</SessionBackgroundPullout>
)
}
@@ -1,114 +0,0 @@
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Icon } from "@opencode-ai/ui/icon"
import { useSpring } from "@opencode-ai/ui/motion-spring"
import { createResizeObserver } from "@solid-primitives/resize-observer"
import { createEffect, createMemo, type JSX } from "solid-js"
import { createStore } from "solid-js/store"
export function SessionBackgroundPullout(props: {
label: JSX.Element
ariaLabel: string
multiline?: boolean
collapsed: boolean
collapsible?: boolean
onToggle: () => void
collapseLabel: string
expandLabel: string
children: JSX.Element
}) {
const [store, setStore] = createStore({ height: 78, header: 42 })
const collapse = useSpring(() => (props.collapsed ? 1 : 0), { visualDuration: 0.3, bounce: 0 })
const value = createMemo(() => Math.max(0, Math.min(1, collapse())))
const off = createMemo(() => value() > 0.98)
const base = createMemo(() => Math.max(78, store.header + 36))
const full = createMemo(() => Math.max(base(), store.height))
let contentRef: HTMLDivElement | undefined
let headerRef: HTMLDivElement | undefined
createEffect(() => {
const element = contentRef
const header = headerRef
if (!element || !header) return
const update = () => {
setStore("height", (height) => Math.max(height, element.scrollHeight))
setStore("header", header.getBoundingClientRect().height)
}
update()
createResizeObserver([element, header], update)
})
return (
<div
data-component="session-background-dock"
class="w-full overflow-hidden rounded-xl border-[0.5px] border-v2-border-border-base bg-v2-background-bg-layer-01"
style={{
"overflow-x": "visible",
"overflow-y": "hidden",
"max-height": `${Math.max(base(), full() - value() * (full() - base()))}px`,
}}
>
<div ref={contentRef}>
<div
ref={headerRef}
data-action="session-background-toggle"
class="flex items-center gap-2 overflow-visible pl-4 pr-2"
classList={{
"h-[42px]": !props.multiline,
"min-h-[42px] py-2": props.multiline,
}}
role="button"
tabIndex={0}
onClick={props.onToggle}
onKeyDown={(event) => {
if (event.key !== "Enter" && event.key !== " ") return
event.preventDefault()
props.onToggle()
}}
>
<span
class="cursor-default inline-flex items-baseline shrink-0 overflow-visible font-[440] text-[13px] leading-5 tracking-[-0.04px] text-v2-text-text-muted"
aria-label={props.ariaLabel}
style={{
"--tool-motion-odometer-ms": "600ms",
"--tool-motion-mask": "18%",
"--tool-motion-mask-height": "0px",
"--tool-motion-spring-ms": "560ms",
"white-space": "pre",
}}
>
{props.label}
</span>
{props.collapsible !== false && (
<div class="ml-auto">
<IconButton
data-action="session-background-toggle-button"
data-collapsed={props.collapsed ? "true" : "false"}
icon={<Icon name="chevron-down" />}
size="normal"
variant="ghost"
style={{ transform: `rotate(${value() * 180}deg)` }}
onMouseDown={(event) => {
event.preventDefault()
event.stopPropagation()
}}
onClick={(event) => {
event.stopPropagation()
props.onToggle()
}}
aria-label={props.collapsed ? props.expandLabel : props.collapseLabel}
/>
</div>
)}
</div>
<div
data-slot="session-background-list"
aria-hidden={props.collapsed || off()}
classList={{ "pointer-events-none": value() > 0.1 }}
style={{ visibility: off() ? "hidden" : "visible", opacity: `${Math.max(0, 1 - value())}` }}
>
{props.children}
</div>
</div>
</div>
)
}
+9 -5
View File
@@ -15,6 +15,10 @@ export function createSessionScreenLayout(session: SessionModel, serverScope: st
const reviewPanelOpen = createMemo(() => reviewOpen() && !!session.identity.params.id)
const terminalOpen = createMemo(() => session.layout.view().terminal.opened())
const desktopTerminalOpen = createMemo(() => session.isDesktop() && terminalOpen())
const sideTerminalOpen = createMemo(() => desktopTerminalOpen() && settings.general.terminalPlacement() === "side")
const bottomTerminalOpen = createMemo(
() => desktopTerminalOpen() && settings.general.terminalPlacement() === "bottom",
)
const fileTreeOpen = createMemo(
() =>
session.isDesktop() &&
@@ -23,7 +27,7 @@ export function createSessionScreenLayout(session: SessionModel, serverScope: st
opened: layout.fileTree.opened(),
}),
)
const resizable = createMemo(() => reviewPanelOpen() || desktopTerminalOpen())
const resizable = createMemo(() => reviewPanelOpen() || sideTerminalOpen())
const sidePanelOpen = createMemo(() => resizable() || fileTreeOpen())
const [rowWidth, setRowWidth] = createSignal<number>()
let row: HTMLDivElement | undefined
@@ -57,7 +61,7 @@ export function createSessionScreenLayout(session: SessionModel, serverScope: st
const panelLayout = createMemo(() =>
sessionPanelLayout({
review: reviewPanelOpen(),
terminal: desktopTerminalOpen(),
terminal: sideTerminalOpen(),
files: fileTreeOpen(),
}),
)
@@ -97,11 +101,11 @@ export function createSessionScreenLayout(session: SessionModel, serverScope: st
panelOpen: reviewPanelOpen,
snap: reviewSnap,
},
side: { layout: panelLayout, open: sidePanelOpen },
side: { layout: panelLayout },
size,
terminal: {
desktopOpen: desktopTerminalOpen,
inlineOnlyOpen: createMemo(() => desktopTerminalOpen() && !reviewPanelOpen()),
bottomOpen: bottomTerminalOpen,
inlineOnlyOpen: createMemo(() => sideTerminalOpen() && !reviewPanelOpen()),
open: terminalOpen,
},
}
+83 -56
View File
@@ -72,25 +72,24 @@ export function SessionScreen(props: { session: SessionModel }) {
{(_id) => (
<MessageTimeline
session={session}
background={composer.region.state.background}
actions={composer.actions.timeline}
scroll={timeline.scroll}
onResumeScroll={timeline.actions.resume}
setScrollRef={timeline.view.setScrollRef}
onScheduleScrollState={timeline.view.scheduleScrollState}
onAutoScrollHandleScroll={timeline.autoScroll.handleScroll}
onMarkScrollGesture={timeline.view.markGesture}
hasScrollGesture={timeline.view.hasGesture()}
onPin={timeline.view.pin}
onUnpin={timeline.view.unpin}
onUserScroll={timeline.view.markUserScroll}
onHistoryScroll={timeline.view.onHistoryScroll}
onAutoScrollInteraction={timeline.autoScroll.handleInteraction}
shouldAnchorBottom={timeline.view.shouldAnchorBottom()}
onSelectionInteraction={timeline.view.selectionInteraction}
pinned={timeline.view.pinned()}
centered={screen.centered()}
setContentRef={timeline.view.setContentRef}
diffs={review.details.diffs}
onReview={review.open}
workspaceMoveEligible={composer.workspaceMoveEligible()}
onSummaryOpenChange={review.details.setOpen}
setHistoryAnchor={timeline.view.setHistoryAnchor}
anchor={timeline.view.anchor}
setRevealMessage={timeline.view.setRevealMessage}
setScrollToEnd={timeline.view.setScrollToEnd}
@@ -120,51 +119,88 @@ export function SessionScreen(props: { session: SessionModel }) {
return (
<SessionRouteFrame>
<SessionHeader />
<div ref={screen.panel.ref} class="flex-1 min-h-0 flex flex-col md:flex-row gap-2 p-2">
<div
classList={{
"@container relative shrink-0 flex flex-col min-h-0 h-full flex-1 md:flex-none transition-[width]": true,
"duration-[240ms] ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
!screen.size.active() && !screen.review.snap() && !screen.terminal.inlineOnlyOpen(),
}}
style={{
width: screen.panel.width(),
}}
>
<Show when={screen.panel.key()} keyed>
{(_) => (
<SessionPanelFrame raised={!!session.identity.params.id}>
<ErrorBoundary fallback={sessionErrorFallback}>{sessionPanelContent()}</ErrorBoundary>
</SessionPanelFrame>
)}
</Show>
<div class="flex-1 min-h-0 flex flex-col gap-2 p-2">
<div ref={screen.panel.ref} class="flex-1 min-h-0 flex flex-col md:flex-row gap-2">
<div
classList={{
"@container relative shrink-0 flex flex-col min-h-0 h-full flex-1 md:flex-none transition-[width]": true,
"duration-[240ms] ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
!screen.size.active() && !screen.review.snap() && !screen.terminal.inlineOnlyOpen(),
}}
style={{
width: screen.panel.width(),
}}
>
<Show when={screen.panel.key()} keyed>
{(_) => (
<SessionPanelFrame raised={!!session.identity.params.id}>
<ErrorBoundary fallback={sessionErrorFallback}>{sessionPanelContent()}</ErrorBoundary>
</SessionPanelFrame>
)}
</Show>
<Show when={screen.panel.resizable()}>
<div onPointerDown={() => screen.size.start()}>
<ResizeHandle
class="-end-1"
direction="horizontal"
size={screen.panel.resizedWidth()}
min={SESSION_PANEL_WIDTH_MIN}
max={screen.panel.max()}
onResize={(width) => {
screen.size.touch()
layout.session.resize(width)
}}
/>
<Show when={screen.panel.resizable()}>
<div onPointerDown={() => screen.size.start()}>
<ResizeHandle
class="-end-1"
direction="horizontal"
size={screen.panel.resizedWidth()}
min={SESSION_PANEL_WIDTH_MIN}
max={screen.panel.max()}
onResize={(width) => {
screen.size.touch()
layout.session.resize(width)
}}
/>
</div>
</Show>
</div>
<Show when={isDesktop() && screen.side.layout().visible}>
<div class="min-w-0 h-full flex flex-1 flex-col">
<Show when={screen.review.panelOpen() || screen.files.open()}>
<div class="min-h-0 flex-1">
<SessionDesktopReview review={review} />
</div>
</Show>
<Show when={screen.side.layout().stacked}>
<div class="relative h-2 shrink-0" onPointerDown={() => screen.size.start()}>
<ResizeHandle
class="!relative !inset-auto !h-full !w-full !transform-none"
direction="vertical"
size={layout.terminal.height()}
min={100}
max={typeof window === "undefined" ? 600 : window.innerHeight * 0.6}
collapseThreshold={50}
onResize={(height) => {
screen.size.touch()
layout.terminal.resize(height)
}}
onCollapse={() => session.layout.view().terminal.close()}
/>
</div>
</Show>
<Show when={screen.terminal.open() && !screen.terminal.bottomOpen()}>
<div
classList={{
"min-h-0 shrink-0": screen.side.layout().stacked,
"min-h-0 flex-1": !screen.side.layout().stacked,
}}
>
<TerminalPanel stacked={screen.side.layout().stacked} />
</div>
</Show>
</div>
</Show>
</div>
<Show when={isDesktop() ? screen.side.layout().visible : screen.terminal.open()}>
<div class="min-w-0 h-full flex flex-1 flex-col">
<Show when={isDesktop() && (screen.review.panelOpen() || screen.files.open())}>
<div class="min-h-0 flex-1">
<SessionDesktopReview review={review} />
</div>
</Show>
<Show when={screen.side.layout().stacked}>
<div class="relative h-2 shrink-0" onPointerDown={() => screen.size.start()}>
<Show when={screen.terminal.open() && (!isDesktop() || screen.terminal.bottomOpen())}>
<div classList={{ "relative min-h-0 shrink-0": isDesktop() }}>
<Show when={isDesktop()}>
<div
class="absolute z-10 -top-1 left-0 right-0 h-2"
onPointerDown={() => screen.size.start()}
>
<ResizeHandle
class="!relative !inset-auto !h-full !w-full !transform-none"
direction="vertical"
@@ -180,16 +216,7 @@ export function SessionScreen(props: { session: SessionModel }) {
/>
</div>
</Show>
<Show when={screen.terminal.open()}>
<div
classList={{
"min-h-0 shrink-0": screen.side.layout().stacked,
"min-h-0 flex-1": !screen.side.layout().stacked,
}}
>
<TerminalPanel stacked={screen.side.layout().stacked} />
</div>
</Show>
<TerminalPanel stacked={isDesktop()} />
</div>
</Show>
</div>
@@ -0,0 +1,23 @@
import { describe, expect, test } from "bun:test"
import { createRoot } from "solid-js"
import { createSessionResolution } from "./session-resolution"
describe("session resolution", () => {
test("waits for a route session ID", () => {
createRoot((dispose) => {
let syncs = 0
const sessions = {
get: () => undefined,
sync: () => {
syncs++
return Promise.resolve()
},
}
const session = createSessionResolution(() => undefined, () => sessions)
expect(session()).toBeUndefined()
expect(syncs).toBe(0)
dispose()
})
})
})
@@ -29,15 +29,20 @@ type Resolution<T> = { id: string; store: SessionStore<T> } & (
// session that simply has not resolved yet. Resolve failures rethrow on read so
// the enclosing SessionRouteErrorBoundary renders the scoped session error.
export function createSessionResolution<T>(
sessionID: () => string,
sessionID: () => string | undefined,
sessions: () => SessionStore<T>,
options?: { children?: boolean },
) {
const cached = createMemo(() => sessions().get(sessionID()))
const cached = createMemo(() => {
const id = sessionID()
if (!id) return
return sessions().get(id)
})
const [status, setStatus] = createSignal<Resolution<T>>()
createEffect(
on([sessionID, sessions] as const, ([id, store]) => {
if (!id) return
let stale = false
onCleanup(() => {
stale = true
@@ -60,10 +65,11 @@ export function createSessionResolution<T>(
return createMemo(() => {
const id = sessionID()
if (!id) return
const value = cached()
if (value) return value
const state = status()
if (state?.id !== id || state.store !== sessions()) return undefined
if (!state || state.id !== id || state.store !== sessions()) return undefined
if (state.state === "failed") throw state.failure
// A session missing after settlement was deleted, possibly by another client.
// Match the resolve error so the boundary shows the
-8
View File
@@ -84,7 +84,6 @@ export type SessionPreviewProps = {
draft?: string
request?: { type: "permission"; value: PermissionRequest } | { type: "question"; value: FormInfo }
reviewOpened?: boolean
backgroundTasks?: { id: string; type: "shell" | "subagent"; label: string }[]
child?: { parentID: string }
terminal?: { title: string; lines: string[] }
}
@@ -196,13 +195,6 @@ function SessionSurfaceState(props: SessionPreviewProps & { onReset: () => void
setState("request", undefined)
setState("activity", `Permission response: ${response}`)
},
background: {
blocking: () => [],
tasks: () => props.backgroundTasks ?? [],
move: async () => {
setState("activity", "Requested background execution")
},
},
blocked: () => state.request !== undefined,
},
centered: () => true,
@@ -0,0 +1,17 @@
import { describe, expect, test } from "bun:test"
import { terminalKeyInput } from "./terminal-key-event"
describe("terminalKeyInput", () => {
test("maps Command+Delete to the terminal line-clear control code", () => {
const event = new KeyboardEvent("keydown", { key: "Backspace", metaKey: true })
expect(terminalKeyInput(event)).toBe("\x15")
})
test("leaves other Backspace shortcuts to the terminal", () => {
expect(terminalKeyInput(new KeyboardEvent("keydown", { key: "Backspace" }))).toBeUndefined()
expect(
terminalKeyInput(new KeyboardEvent("keydown", { key: "Backspace", metaKey: true, shiftKey: true })),
).toBeUndefined()
})
})
@@ -0,0 +1,5 @@
export function terminalKeyInput(event: KeyboardEvent) {
if (!event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return
if (event.key.toLowerCase() !== "backspace") return
return "\x15"
}
@@ -15,6 +15,7 @@ import { useServerSDK } from "@/runtime/server/client"
import { terminalFontFamily, useSettings } from "@/settings/model"
import type { LocalPTY } from "@/session/terminal/context"
import { disposeIfDisposable, getHoveredLinkText, setOptionIfSupported } from "@/session/terminal/runtime-adapters"
import { terminalKeyInput } from "@/session/terminal/terminal-key-event"
import { terminalWriter } from "@/session/terminal/writer"
const TOGGLE_TERMINAL_ID = "terminal.toggle"
@@ -408,6 +409,12 @@ export const Terminal = (props: TerminalProps) => {
t.attachCustomKeyEventHandler((event) => {
const key = event.key.toLowerCase()
const input = terminalKeyInput(event)
if (input) {
t.input(input, true)
return true
}
if (event.ctrlKey && event.shiftKey && !event.metaKey && key === "c") {
document.execCommand("copy")
return true
@@ -0,0 +1,36 @@
import { BackgroundMoveHint, BackgroundWorkSummary } from "./message-timeline"
const tasks = [
{ id: "task_explore", type: "subagent" as const, agent: "explore", label: "Reviewing component implementation" },
{ id: "task_status", type: "shell" as const, label: "opencode2 service status" },
{ id: "task_openapi", type: "shell" as const, label: "opencode2 api get /openapi.json" },
{ id: "task_tests", type: "shell" as const, label: "bun test packages/app" },
]
export default {
title: "OpenCode/Session/Background work",
id: "session-background-work",
parameters: {
docs: {
description: {
component: "Production controls for moving blocking work and inspecting active background tasks.",
},
},
},
}
export const InlineMoveHint = {
render: () => (
<div class="flex w-[696px] max-w-full flex-col items-start gap-4">
<BackgroundMoveHint keybind={["Ctrl", "B"]} />
</div>
),
}
export const SummaryPanelEntry = {
render: () => (
<div class="w-[280px] rounded-[6px] bg-v2-background-bg-base px-0.5 py-1.5 shadow-[var(--v2-elevation-raised)]">
<BackgroundWorkSummary tasks={tasks} />
</div>
),
}
@@ -1,8 +1,7 @@
import type { SessionMessageUser } from "@opencode-ai/client/promise"
import { createAutoScroll } from "@opencode-ai/ui/hooks"
import { createResizeObserver } from "@solid-primitives/resize-observer"
import { useLocation } from "@solidjs/router"
import { createEffect, on, onCleanup } from "solid-js"
import { createEffect, createSignal, on, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { useLayout } from "@/shell/state/layout"
import type { SessionModel } from "../model"
@@ -16,7 +15,6 @@ export function createSessionTimelineInteraction(session: SessionModel) {
const [state, setState] = createStore({
messageID: undefined as string | undefined,
pendingMessage: undefined as string | undefined,
gestureAt: 0,
scroll: {
overflow: false,
jump: false,
@@ -26,13 +24,18 @@ export function createSessionTimelineInteraction(session: SessionModel) {
dock: undefined as HTMLDivElement | undefined,
},
})
const autoScroll = createAutoScroll({ working: () => true, overflowAnchor: "none" })
// The single source of truth for "follow the newest content". The virtualizer pins and unpins
// it from scroll geometry; everything else only expresses explicit intent.
const [pinned, setPinned] = createSignal(true)
const pin = () => setPinned(true)
const unpin = () => {
if (!scroller || scroller.scrollHeight - scroller.clientHeight <= 1) return
setPinned(false)
}
let scroller: HTMLDivElement | undefined
let dockHeight = 0
let revealMessage = (_id: string) => {}
let scrollToEnd = () => {}
let captureHistoryAnchor = () => {}
let restoreHistoryAnchor = (_done: boolean) => {}
let scrollMark = 0
let messageMark = 0
let scrollStateFrame: number | undefined
@@ -102,10 +105,10 @@ export function createSessionTimelineInteraction(session: SessionModel) {
pendingMessage: () => state.pendingMessage,
setPendingMessage: (value) => setState("pendingMessage", value),
setActiveMessage,
autoScroll: {
pause: autoScroll.pause,
forceScrollToBottom: () => {
autoScroll.resume()
follow: {
unpin,
toBottom: () => {
pin()
scrollToEnd()
},
},
@@ -117,7 +120,7 @@ export function createSessionTimelineInteraction(session: SessionModel) {
})
const resume = () => {
setState("messageID", undefined)
autoScroll.resume()
pin()
scrollToEnd()
clearMessageHash()
if (scroller) scheduleScrollState(scroller)
@@ -133,21 +136,23 @@ export function createSessionTimelineInteraction(session: SessionModel) {
resume()
return
}
autoScroll.pause()
unpin()
scrollToMessage(messages[target], "auto")
}
const shouldAnchorBottom = () =>
!location.hash && !state.messageID && !state.pendingMessage && !autoScroll.userScrolled()
const markGesture = (target?: EventTarget | null) => {
// A gesture inside a nested scrollable region scrolls that region, not the timeline.
const markUserScroll = (target?: EventTarget | null) => {
if (!scroller) return
const element = target instanceof Element ? target : undefined
const nested = element?.closest("[data-scrollable]")
if (nested && nested !== scroller) return
setState("gestureAt", Date.now())
scrollMark += 1
}
const selectionInteraction = () => {
const selection = window.getSelection()
if (selection && selection.toString().length > 0) unpin()
}
const setScrollRef = (element: HTMLDivElement | undefined) => {
scroller = element
autoScroll.scrollRef(element)
if (!element) return
scheduleScrollState(element)
fill()
@@ -158,15 +163,12 @@ export function createSessionTimelineInteraction(session: SessionModel) {
historyRequests.add(owner.key)
const before = timeline.messages().length
try {
await timeline.history.loadOlder({
before: () => owner.run(captureHistoryAnchor),
after: (done) => owner.run(() => restoreHistoryAnchor(done)),
})
await timeline.history.loadOlder()
} finally {
historyRequests.delete(owner.key)
}
if (!owner.current() || timeline.messages().length <= before) return
if (!autoScroll.userScrolled() || !scroller || scroller.scrollTop >= 200 || !timeline.history.more()) return
if (pinned() || !scroller || scroller.scrollTop >= 200 || !timeline.history.more()) return
if (historyContinuationFrame !== undefined) cancelAnimationFrame(historyContinuationFrame)
historyContinuationFrame = requestAnimationFrame(() => {
historyContinuationFrame = undefined
@@ -177,7 +179,7 @@ export function createSessionTimelineInteraction(session: SessionModel) {
if (
historyRequests.has(session.ownership.key()) ||
timeline.history.loading() ||
!autoScroll.userScrolled() ||
pinned() ||
!scroller ||
scroller.scrollTop >= 200
)
@@ -189,7 +191,7 @@ export function createSessionTimelineInteraction(session: SessionModel) {
fillFrame = requestAnimationFrame(() => {
fillFrame = undefined
if (!session.identity.params.id || !timeline.ready()) return
if (autoScroll.userScrolled() || timeline.history.loading() || !scroller) return
if (!pinned() || timeline.history.loading() || !scroller) return
if (scroller.scrollHeight > scroller.clientHeight + 1 || !timeline.history.more()) return
void loadOlder()
})
@@ -218,15 +220,16 @@ export function createSessionTimelineInteraction(session: SessionModel) {
() => session.identity.params.id,
(id, previous) => {
if (!id || !previous || id === previous || state.messageID || state.pendingMessage || location.hash) return
autoScroll.resume()
pin()
scrollToEnd()
},
),
)
createEffect(
on(
autoScroll.userScrolled,
(scrolled) => {
if (scrolled) return
pinned,
(value) => {
if (!value) return
setState("messageID", undefined)
clearMessageHash()
},
@@ -241,11 +244,11 @@ export function createSessionTimelineInteraction(session: SessionModel) {
timeline.ready(),
timeline.history.more(),
timeline.history.loading(),
autoScroll.userScrolled(),
pinned(),
visibleUserMessages().length,
] as const,
([id, ready, more, loading, scrolled]) => {
if (id && ready && more && !loading && !scrolled) fill()
([id, ready, more, loading, following]) => {
if (id && ready && more && !loading && following) fill()
},
{ defer: true },
),
@@ -264,8 +267,7 @@ export function createSessionTimelineInteraction(session: SessionModel) {
if (next === dockHeight) return
const delta = next - dockHeight
const stick = scroller
? !autoScroll.userScrolled() ||
scroller.scrollHeight - scroller.clientHeight - scroller.scrollTop < 10 + Math.max(0, delta)
? pinned() || scroller.scrollHeight - scroller.clientHeight - scroller.scrollTop < 10 + Math.max(0, delta)
: false
dockHeight = next
if (stick) scrollToEnd()
@@ -285,7 +287,6 @@ export function createSessionTimelineInteraction(session: SessionModel) {
resume,
setActiveMessage,
},
autoScroll,
lastUserMessage: timeline.lastUserMessage,
resource: timeline.resource,
ready: timeline.ready,
@@ -293,25 +294,19 @@ export function createSessionTimelineInteraction(session: SessionModel) {
scroller: () => scroller,
view: {
anchor,
hasGesture: () => Date.now() - state.gestureAt < 250,
markGesture,
markUserScroll: () => {
scrollMark += 1
},
markUserScroll,
onHistoryScroll,
pin,
pinned,
selectionInteraction,
scheduleScrollState,
setContentRef: (element: HTMLDivElement | undefined) => {
setState("refs", "content", element)
autoScroll.contentRef(element)
if (scroller) scheduleScrollState(scroller)
},
setDockRef: (element: HTMLDivElement | undefined) => {
setState("refs", "dock", element)
},
setHistoryAnchor: (handlers: { capture: () => void; restore: (done: boolean) => void }) => {
captureHistoryAnchor = handlers.capture
restoreHistoryAnchor = handlers.restore
},
setRevealMessage: (reveal: (id: string) => void) => {
revealMessage = reveal
},
@@ -319,7 +314,7 @@ export function createSessionTimelineInteraction(session: SessionModel) {
setScrollToEnd: (scroll: () => void) => {
scrollToEnd = scroll
},
shouldAnchorBottom,
unpin,
},
}
}
@@ -1,30 +0,0 @@
import { expect, test } from "bun:test"
import { scheduleConnectedMeasure } from "./measure"
test("does not measure an element detached before the frame", async () => {
const element = document.createElement("div")
document.body.append(element)
let calls = 0
scheduleConnectedMeasure(element, () => {
calls += 1
})
element.remove()
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
expect(calls).toBe(0)
})
test("measures a connected element on the next frame", async () => {
const element = document.createElement("div")
document.body.append(element)
let calls = 0
scheduleConnectedMeasure(element, () => {
calls += 1
})
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
expect(calls).toBe(1)
element.remove()
})
@@ -1,5 +0,0 @@
export function scheduleConnectedMeasure<T extends HTMLElement>(element: T, measure: (element: T) => void) {
return requestAnimationFrame(() => {
if (element.isConnected) measure(element)
})
}
@@ -1,11 +1,15 @@
import { createEffect, createMemo, createSignal, on, Show, type Accessor } from "solid-js"
import { createEffect, createMemo, createSignal, For, on, Show, type Accessor } from "solid-js"
import createPresence from "solid-presence"
import { createStore } from "solid-js/store"
import type { SessionUserActions } from "@opencode-ai/session-ui/actions"
import { Badge } from "@opencode-ai/ui/badge"
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { InlineInput } from "@opencode-ai/ui/inline-input"
import { Keybind } from "@opencode-ai/ui/keybind"
import { Menu } from "@opencode-ai/ui/menu"
import { TextShimmer } from "@opencode-ai/ui/text-shimmer"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { ProjectAvatar } from "@opencode-ai/ui/project-avatar"
import type { Project } from "@/runtime/server/types"
@@ -15,7 +19,7 @@ import { SessionContextUsage } from "@/session/timeline/session-context-usage"
import { useLanguage } from "@/runtime/i18n/language"
import { useData } from "@/runtime/server/current"
import { useWorkspaceLocation } from "@/workspaces/location"
import { Timeline } from "@opencode-ai/session-ui/timeline/projection"
import { Timeline, TimelineRow } from "@opencode-ai/session-ui/timeline/projection"
import { createSessionTimelineRowRenderer } from "@opencode-ai/session-ui/timeline/row"
import { createTimelineController, type TimelineController, type TimelineSessionSource } from "./controller"
import { createTimelineVirtualizer } from "./virtualizer"
@@ -24,6 +28,98 @@ import { SessionWorkspaceMenu } from "@/session/timeline/session-workspace-menu"
import { getProjectAvatarVariant } from "@/shell/state/layout"
import { displayName, getProjectAvatarSource } from "@/shell/layout/helpers"
import { parseCommentNote, readPromptPresentation } from "@/composer/comment-note"
import { useCommand } from "@/shell/commands/command"
type BackgroundTask = {
id: string
type: "shell" | "subagent"
label: string
agent?: string
}
type SessionBackground = {
blocking: Accessor<{ type: "shell" | "subagent"; partID: string; id?: string; label?: string }[]>
tasks: Accessor<BackgroundTask[]>
move: () => Promise<void>
}
export function BackgroundMoveHint(props: { keybind?: string[] }) {
const language = useLanguage()
const command = useCommand()
const marker = "__OPENCODE_BACKGROUND_KEYBIND__"
const parts = createMemo(() => language.t("session.background.moveInline", { keybind: marker }).split(marker))
const keys = () => props.keybind ?? command.keybindParts("session.background")
const keybind = () => props.keybind?.join("+") ?? command.keybind("session.background")
return (
<div
data-component="session-background-hint"
class="flex h-6 max-w-full items-center justify-center gap-[3px] overflow-hidden text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-muted"
aria-label={language.t("session.background.moveInline", { keybind: keybind() })}
>
<span data-slot="session-background-hint-prefix" class="shrink-0">
{parts()[0].trim()}
</span>
<Keybind keys={keys()} variant="neutral" />
<span class="min-w-0 truncate">{parts()[1].trim()}</span>
</div>
)
}
export function BackgroundWorkSummary(props: { tasks: BackgroundTask[] }) {
const language = useLanguage()
const [open, setOpen] = createSignal(false)
const taskType = (task: BackgroundTask) => {
if (task.type === "shell") return language.t("ui.tool.shell")
if (!task.agent) return language.t("ui.tool.agent.default")
return task.agent.slice(0, 1).toUpperCase() + task.agent.slice(1)
}
return (
<Popover
open={open()}
placement={language.direction() === "rtl" ? "right-end" : "left-end"}
gutter={4}
onOpenChange={setOpen}
>
<Popover.Trigger
as="button"
type="button"
data-component="session-background-summary"
class="flex h-7 w-full items-center gap-2 rounded-[4px] px-3 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-base hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none data-[expanded]:bg-v2-overlay-simple-overlay-pressed"
aria-label={language.plural("session.background.runningCount", props.tasks.length)}
>
<Badge class="!w-4 !px-0 !border-v2-border-border-strong !bg-v2-background-bg-layer-03">
{props.tasks.length}
</Badge>
<TextShimmer
as="span"
text={language.t("session.background.running")}
active
class="min-w-0 flex-1 truncate text-start"
/>
</Popover.Trigger>
<Popover.Portal>
<Popover.Content
data-component="session-background-list"
class="z-[60] w-[200px] overflow-hidden rounded-[6px] bg-v2-background-bg-layer-01 p-0.5 shadow-[var(--v2-elevation-floating)] outline-none"
>
<For each={props.tasks.slice(0, 10)}>
{(task) => (
<div
data-component="session-background-list-item"
class="flex h-7 min-w-0 items-center gap-2 rounded-[4px] px-3 text-[13px] font-[440] leading-none tracking-[-0.04px]"
>
<span class="shrink-0 text-v2-text-text-base">{taskType(task)}</span>
<span class="min-w-0 flex-1 truncate text-v2-text-text-faint">{task.label}</span>
</div>
)}
</For>
</Popover.Content>
</Popover.Portal>
</Popover>
)
}
function WorkspaceMoveAction(props: {
variant: "inline" | "panel"
@@ -94,6 +190,7 @@ function SessionSummaryPanel(props: {
moveDismissed: boolean
onMoveDismiss: () => void
onReview: () => void
backgroundTasks: BackgroundTask[]
}) {
const language = useLanguage()
const location = () => {
@@ -168,6 +265,9 @@ function SessionSummaryPanel(props: {
)}
</Show>
</button>
<Show when={props.backgroundTasks.length > 0}>
<BackgroundWorkSummary tasks={props.backgroundTasks} />
</Show>
</div>
<Show when={props.local && props.diffs && props.diffs.length > 0 && props.moveEligible}>
<WorkspaceMoveAction
@@ -186,18 +286,18 @@ function SessionSummaryPanel(props: {
type MessageTimelineProps = {
session: TimelineSessionSource
background: SessionBackground
actions?: SessionUserActions
scroll: { overflow: boolean; jump: boolean }
onResumeScroll: () => void
setScrollRef: (el: HTMLDivElement | undefined) => void
onScheduleScrollState: (el: HTMLDivElement) => void
onAutoScrollHandleScroll: () => void
onMarkScrollGesture: (target?: EventTarget | null) => void
hasScrollGesture: boolean
onUserScroll: () => void
onPin: () => void
onUnpin: () => void
onUserScroll: (target?: EventTarget | null) => void
onHistoryScroll: () => void
onAutoScrollInteraction: (event: MouseEvent) => void
shouldAnchorBottom: boolean
onSelectionInteraction: (event: MouseEvent) => void
pinned: boolean
centered: boolean
setContentRef: (el: HTMLDivElement) => void
diffs: Accessor<{ additions: number; deletions: number }[] | undefined>
@@ -207,7 +307,6 @@ type MessageTimelineProps = {
anchor: (id: string) => string
setRevealMessage?: (fn: (id: string) => void) => void
setScrollToEnd?: (fn: () => void) => void
setHistoryAnchor?: (handlers: { capture: () => void; restore: (done: boolean) => void }) => void
}
export function MessageTimeline(props: MessageTimelineProps) {
@@ -264,28 +363,25 @@ function MessageTimelineView(
)
const turnPadding = () => "px-4 md:px-5"
const showHeader = createMemo(() => props.data.showHeader() || workspaceSession())
const shouldAnchorBottom = createMemo(() => props.shouldAnchorBottom)
const hasScrollGesture = createMemo(() => props.hasScrollGesture)
const pinned = createMemo(() => props.pinned)
const messageByID = projection.messageByID
const virtualized = createTimelineVirtualizer({
sessionKey: props.data.sessionKey,
projection,
showHeader,
shouldAnchorBottom,
hasScrollGesture,
pinned,
scroll: () => props.scroll,
onResumeScroll: props.onResumeScroll,
setScrollRef: props.setScrollRef,
setContentRef: props.setContentRef,
onScheduleScrollState: props.onScheduleScrollState,
onAutoScrollHandleScroll: props.onAutoScrollHandleScroll,
onAutoScrollInteraction: props.onAutoScrollInteraction,
onMarkScrollGesture: props.onMarkScrollGesture,
onPin: props.onPin,
onUnpin: props.onUnpin,
onSelectionInteraction: props.onSelectionInteraction,
onUserScroll: props.onUserScroll,
onHistoryScroll: props.onHistoryScroll,
setRevealMessage: props.setRevealMessage,
setScrollToEnd: props.setScrollToEnd,
setHistoryAnchor: props.setHistoryAnchor,
})
const VirtualizedTimeline = virtualized.View
const [title, setTitle] = createStore({
@@ -350,14 +446,59 @@ function MessageTimelineView(
padding: turnPadding,
anchor: props.anchor,
})
const backgroundHintPartID = createMemo(() => {
const blocking = new Set(props.background.blocking().map((task) => task.partID))
const row = projection
.rows()
.findLast(
(row) => row._tag === "AssistantPart" && row.group.type === "part" && blocking.has(row.group.ref.partID),
)
if (row?._tag !== "AssistantPart" || row.group.type !== "part") return
return row.group.ref.partID
})
const [backgroundHintRef, setBackgroundHintRef] = createSignal<HTMLDivElement>()
const backgroundHintVisibility = createMemo<{ show: boolean; animate: boolean }>(
(previous) => {
const show = backgroundHintPartID() !== undefined
return { show, animate: previous.animate || previous.show !== show }
},
{ show: backgroundHintPartID() !== undefined, animate: false },
)
const backgroundHintPresence = createPresence({
show: () => backgroundHintVisibility().show,
element: () => backgroundHintRef() ?? null,
})
return (
<VirtualizedTimeline
workspaceSession={workspaceSession}
bottomSpacer={
<Show when={backgroundHintPresence.present()}>
<div
data-component="session-background-hint-row"
classList={{
"min-w-0 w-full max-w-full": true,
"md:max-w-200 2xl:max-w-[1000px] md:mx-auto": props.centered,
}}
>
<div
ref={setBackgroundHintRef}
class="duration-150 motion-reduce:animate-none"
classList={{
[`flex h-8 items-start pt-2 ${turnPadding()}`]: true,
"animate-in fade-in": backgroundHintVisibility().animate && backgroundHintVisibility().show,
"animate-out fade-out fill-mode-forwards":
backgroundHintVisibility().animate && !backgroundHintVisibility().show,
}}
>
<BackgroundMoveHint />
</div>
</div>
</Show>
}
deferred={(row) => {
if (row._tag !== "AssistantPart" || row.group.type !== "part") return false
const content = Timeline.resolveContent(messageByID().get(row.group.ref.messageID), row.group.ref.partID)
return content?.type === "tool" && ["edit", "write", "patch"].includes(content.name)
return content?.type === "tool" && ["edit", "write"].includes(content.name)
}}
renderRow={(row, onSizeChange) => <rowRenderer.Row row={row} onSizeChange={onSizeChange} />}
header={
@@ -485,6 +626,7 @@ function MessageTimelineView(
setSummary(false)
props.onReview()
}}
backgroundTasks={props.background.tasks()}
/>
</Popover.Content>
</Popover.Portal>
@@ -3,6 +3,7 @@ import { observeElementOffset, type Virtualizer } from "@tanstack/solid-virtual"
export function observeElementOffsetReconnectAware<TScrollElement extends Element, TItemElement extends Element>(
instance: Virtualizer<TScrollElement, TItemElement>,
callback: (offset: number, isScrolling: boolean) => void,
onReconnect?: () => void,
) {
let active = true
const deliver = (offset: number, isScrolling: boolean) => {
@@ -54,6 +55,7 @@ export function observeElementOffsetReconnectAware<TScrollElement extends Elemen
}
if (!removed || !element.isConnected || !mutationNodesContainElement(record.addedNodes, element)) return
removed = false
onReconnect?.()
startCheck()
})
})
+104 -206
View File
@@ -1,7 +1,6 @@
import { createVirtualizer, defaultRangeExtractor, elementScroll, type VirtualItem } from "@tanstack/solid-virtual"
import { isScrollKeyTarget, scrollKey, scrollKeyOwner, ScrollView } from "@opencode-ai/ui/scroll-view"
import { TimelineRow } from "@opencode-ai/session-ui/timeline/projection"
import { normalizeWheelDelta, shouldMarkBoundaryGesture } from "@/session/message-gesture"
import { useLanguage } from "@/runtime/i18n/language"
import {
createEffect,
@@ -17,11 +16,14 @@ import {
} from "solid-js"
import { createStore } from "solid-js/store"
import type { createTimelineProjection } from "./projection"
import { scheduleConnectedMeasure } from "./measure"
import { observeElementOffsetReconnectAware } from "./observe-element-offset"
import { filterVirtualIndexes } from "./virtual-items"
const fallbackItemSize = 60
// Distance from the bottom that counts as "at the end". Deliberately tight: a collapse clamps
// exactly to the end, while a one-pixel nudge upward is a deliberate move away from it.
const endEpsilon = 0.5
const upwardKeys = new Set(["up", "page-up", "home"])
const cache = new Map<string, { measurements: VirtualItem[]; toolOpen: Record<string, boolean | undefined> }>()
type Projection = Pick<
@@ -33,25 +35,25 @@ type Input = {
sessionKey: Accessor<string>
projection: Projection
showHeader: Accessor<boolean>
shouldAnchorBottom: Accessor<boolean>
hasScrollGesture: Accessor<boolean>
/** True while the timeline follows the newest content. Drives every anchoring decision. */
pinned: Accessor<boolean>
scroll: Accessor<{ overflow: boolean; jump: boolean }>
onResumeScroll: () => void
setScrollRef: (element: HTMLDivElement | undefined) => void
setContentRef: (element: HTMLDivElement) => void
onScheduleScrollState: (element: HTMLDivElement) => void
onAutoScrollHandleScroll: () => void
onAutoScrollInteraction: (event: MouseEvent) => void
onMarkScrollGesture: (target?: EventTarget | null) => void
onUserScroll: () => void
onPin: () => void
onUnpin: () => void
onSelectionInteraction: (event: MouseEvent) => void
onUserScroll: (target?: EventTarget | null) => void
onHistoryScroll: () => void
setRevealMessage?: (fn: (id: string) => void) => void
setScrollToEnd?: (fn: () => void) => void
setHistoryAnchor?: (handlers: { capture: () => void; restore: (done: boolean) => void }) => void
}
type ViewProps = {
header: JSX.Element
bottomSpacer?: JSX.Element
workspaceSession: Accessor<boolean>
deferred: (row: TimelineRow.TimelineRow) => boolean
renderRow: (row: Accessor<TimelineRow.TimelineRow>, onSizeChange?: () => void) => JSX.Element
@@ -62,88 +64,31 @@ export function createTimelineVirtualizer(input: Input) {
const ownerSessionKey = input.sessionKey()
const cached = cache.get(ownerSessionKey)
const initialMeasurements = cached?.measurements
const coldBottomMount = !initialMeasurements?.length && input.shouldAnchorBottom()
const coldBottomMount = !initialMeasurements?.length && input.pinned()
const [listRoot, setListRoot] = createSignal<HTMLDivElement>()
const [toolOpen, setToolOpen] = createStore<Record<string, boolean | undefined>>(cached?.toolOpen ?? {})
const [renderOverscan, setRenderOverscan] = createSignal(initialMeasurements?.length || coldBottomMount ? 6 : 20)
const rows = input.projection.rows
const rowByKey = input.projection.rowByKey
let touchGesture: number | undefined
let prependAnchor: { key: string; offset: number } | undefined
let prependAnchorFrame: number | undefined
let prependLoading = false
let resizePinnedIndexes: number[] = []
let resizePinFrame: number | undefined
const knownKeys = new Set(rows().map(TimelineRow.key))
const addedKeys = new Set<string>()
let touchStart: number | undefined
let pointerHeld = false
let maxScroll = 0
let virtualContent: HTMLDivElement | undefined
const clearPrependAnchor = () => {
prependLoading = false
prependAnchor = undefined
if (prependAnchorFrame === undefined) return
cancelAnimationFrame(prependAnchorFrame)
prependAnchorFrame = undefined
}
const capturePrependAnchor = () => {
prependLoading = true
updatePrependAnchor()
}
const updatePrependAnchor = () => {
const root = listRoot()
if (!root) return
const view = root.getBoundingClientRect()
const anchor = [...root.querySelectorAll<HTMLElement>("[data-timeline-key]")]
.map((element) => ({ element, rect: element.getBoundingClientRect() }))
.filter((item) => item.rect.bottom > view.top && item.rect.top < view.bottom)
.sort((a, b) => a.rect.top - b.rect.top)[0]
if (!anchor) return
if (!anchor.element.dataset.timelineKey) return
prependAnchor = { key: anchor.element.dataset.timelineKey, offset: anchor.rect.top - view.top }
}
const restorePrependAnchor = (done: boolean) => {
if (done) prependLoading = false
applyPrependAnchor()
}
const applyPrependAnchor = () => {
const root = listRoot()
if (!root || !prependAnchor) return
if (prependAnchorFrame !== undefined) cancelAnimationFrame(prependAnchorFrame)
let frames = 0
let stable = 0
const apply = () => {
prependAnchorFrame = undefined
const anchor = prependAnchor
if (!anchor) return
const element = root.querySelector<HTMLElement>(`[data-timeline-key="${CSS.escape(anchor.key)}"]`)
const delta = element
? element.getBoundingClientRect().top - root.getBoundingClientRect().top - anchor.offset
: undefined
if (delta !== undefined && Math.abs(delta) > 0.5) {
root.scrollTop += delta
stable = 0
} else {
stable += 1
}
frames += 1
if (stable >= 30 || frames >= 180) {
if (!prependLoading) prependAnchor = undefined
return
}
prependAnchorFrame = requestAnimationFrame(apply)
}
prependAnchorFrame = requestAnimationFrame(apply)
}
let scrollTop = 0
const virtualizer = createVirtualizer<HTMLDivElement, HTMLDivElement>({
get count() {
return rows().length
},
getScrollElement: () => listRoot() ?? null,
observeElementOffset: observeElementOffsetReconnectAware,
initialOffset: () => (input.shouldAnchorBottom() ? Number.MAX_SAFE_INTEGER : 0),
// Route navigation detaches and reattaches the scroll element, which drops its offset.
observeElementOffset: (instance, callback) =>
observeElementOffsetReconnectAware(instance, callback, () => {
if (input.pinned()) virtualizer.scrollToEnd()
}),
initialOffset: () => (input.pinned() ? Number.MAX_SAFE_INTEGER : 0),
initialMeasurementsCache: initialMeasurements,
estimateSize: () => fallbackItemSize,
scrollToFn: (offset, options, instance) => {
@@ -152,67 +97,62 @@ export function createTimelineVirtualizer(input: Input) {
},
get getItemKey() {
const items = rows()
items
.map(TimelineRow.key)
.filter((key) => !knownKeys.has(key))
.forEach((key) => {
knownKeys.add(key)
addedKeys.add(key)
})
return (index: number) => {
const row = items[index]
if (!row) return `removed:${index}`
return TimelineRow.key(row)
}
},
anchorTo: "end",
followOnAppend: true,
get anchorTo() {
return input.pinned() ? "end" : "start"
},
get followOnAppend() {
return input.pinned()
},
scrollEndThreshold: 80,
get scrollMargin() {
return input.showHeader() ? 64 : 0
},
overscan: 50,
paddingEnd: 64,
rangeExtractor: (range) => {
const id = input.projection.activeMessageID()
const active = id ? (input.projection.messageLastRowIndex().get(id) ?? -1) : -1
const indexes = defaultRangeExtractor({ ...range, overscan: renderOverscan() })
return filterVirtualIndexes(
[...new Set([...resizePinnedIndexes, ...indexes, ...(active < 0 ? [] : [active])])].sort((a, b) => a - b),
[...new Set([...indexes, ...(active < 0 ? [] : [active])])].sort((a, b) => a - b),
range.count,
)
},
})
const resizeItem = virtualizer.resizeItem
let resizeAnchorScheduled = false
// Rows measure asynchronously, so the last row can still hold its estimate when TanStack
// reconciles the end. Coalesce one correction per measurement batch, before paint.
const anchorResizedBottom = () => {
if (resizeAnchorScheduled || input.hasScrollGesture()) return
if (resizeAnchorScheduled) return
resizeAnchorScheduled = true
queueMicrotask(() => {
resizeAnchorScheduled = false
if (!input.shouldAnchorBottom() || input.hasScrollGesture()) return
if (!input.pinned()) return
virtualizer.scrollToEnd()
})
}
virtualizer.resizeItem = (index, size) => {
const item = virtualizer.measurementsCache[index]
const previous = item ? (virtualizer.itemSizeCache.get(item.key) ?? item.size) : undefined
const root = listRoot()
if (root && previous !== undefined && Math.abs(size - previous) > root.clientHeight) {
const view = root.getBoundingClientRect()
resizePinnedIndexes = [...root.querySelectorAll<HTMLElement>("[data-index]")]
.filter((element) => {
const rect = element.getBoundingClientRect()
return rect.bottom > view.top && rect.top < view.bottom
})
.map((element) => Number(element.dataset.index))
if (resizePinFrame !== undefined) cancelAnimationFrame(resizePinFrame)
resizePinFrame = requestAnimationFrame(() => {
resizePinFrame = requestAnimationFrame(() => {
resizePinFrame = undefined
resizePinnedIndexes = []
})
})
}
resizeItem(index, size)
if (root && input.shouldAnchorBottom()) anchorResizedBottom()
if (listRoot() && input.pinned()) anchorResizedBottom()
}
virtualizer.shouldAdjustScrollPositionOnItemSizeChange = (item) => {
if (input.shouldAnchorBottom()) return false
const first = virtualizer.range?.startIndex
virtualizer.shouldAdjustScrollPositionOnItemSizeChange = (item, _delta, instance) => {
if (!instance.itemSizeCache.has(item.key) && addedKeys.delete(String(item.key))) {
return item.start < (instance.scrollOffset ?? 0) + instance.scrollAdjustments
}
const first = instance.range?.startIndex
return first !== undefined && item.index < first
}
const virtualItemByKey = createMemo(
@@ -226,92 +166,72 @@ export function createTimelineVirtualizer(input: Input) {
if (index === undefined) return
virtualizer.scrollToIndex(index, { align: "center" })
})
input.setScrollToEnd?.(() => virtualizer.scrollToEnd())
input.setHistoryAnchor?.({ capture: capturePrependAnchor, restore: restorePrependAnchor })
input.setScrollToEnd?.(() => {
input.onPin()
virtualizer.scrollToEnd()
})
})
let overscanFrame: number | undefined
onMount(() => {
overscanFrame = requestAnimationFrame(() => {
if (input.shouldAnchorBottom()) virtualizer.scrollToEnd()
overscanFrame = requestAnimationFrame(() => {
overscanFrame = undefined
if (renderOverscan() < 20) setRenderOverscan(20)
if (input.shouldAnchorBottom()) virtualizer.scrollToEnd()
})
overscanFrame = undefined
if (renderOverscan() < 20) setRenderOverscan(20)
})
})
const maybeAnchorBottom = () => {
if (rows().length === 0) return
if (!input.shouldAnchorBottom() || input.hasScrollGesture()) return
if (resizePinFrame !== undefined) cancelAnimationFrame(resizePinFrame)
clearPrependAnchor()
if (prependAnchorFrame !== undefined) cancelAnimationFrame(prependAnchorFrame)
virtualizer.scrollToEnd()
}
let measuredSessionKey = input.sessionKey()
createEffect(() => {
const key = input.sessionKey()
rows().length
if (measuredSessionKey !== key) {
measuredSessionKey = key
virtualizer.measure()
}
maybeAnchorBottom()
})
const bindListRoot = (root: HTMLDivElement) => {
if (root === listRoot()) return
setListRoot(root)
// TanStack owns anchoring; browser scroll anchoring would fight its adjustments.
root.style.overflowAnchor = "none"
scrollTop = root.scrollTop
maxScroll = root.scrollHeight - root.clientHeight
input.setScrollRef(root)
}
// Upward input is the one intent geometry cannot recover: nudging up while still a pixel from
// the end must stop following, even though the resulting position still looks like the end.
const handleListWheel = (event: WheelEvent & { currentTarget: HTMLDivElement }) => {
if (!prependLoading) clearPrependAnchor()
const root = event.currentTarget
const delta = normalizeWheelDelta({
deltaY: event.deltaY,
deltaMode: event.deltaMode,
rootHeight: root.clientHeight,
})
if (!delta) return
markBoundaryGesture({ root, target: event.target, delta, onMarkScrollGesture: input.onMarkScrollGesture })
input.onUserScroll(event.target)
if (event.deltaY < 0) input.onUnpin()
}
const handleListTouchStart = (event: TouchEvent) => {
if (!prependLoading) clearPrependAnchor()
touchGesture = event.touches[0]?.clientY
input.onUserScroll(event.target)
touchStart = event.touches[0]?.clientY
}
const handleListTouchMove = (event: TouchEvent & { currentTarget: HTMLDivElement }) => {
const next = event.touches[0]?.clientY
const previous = touchGesture
touchGesture = next
if (next === undefined || previous === undefined) return
const delta = previous - next
if (!delta) return
markBoundaryGesture({
root: event.currentTarget,
target: event.target,
delta,
onMarkScrollGesture: input.onMarkScrollGesture,
})
}
const handleListTouchEnd = () => {
touchGesture = undefined
const current = event.touches[0]?.clientY
if (current === undefined || touchStart === undefined) return
// Dragging the content downward reveals earlier messages.
if (current <= touchStart) return
touchStart = current
input.onUnpin()
}
// Drag-selecting past the edge and dragging the scrollbar both scroll without a wheel or key,
// so a held pointer is what separates those from the virtualizer's own measurement adjustments.
const handleListPointerDown = (event: PointerEvent & { currentTarget: HTMLDivElement }) => {
if (!prependLoading) clearPrependAnchor()
input.onMarkScrollGesture(event.target)
}
const handleListPointerMove = (event: PointerEvent) => {
if (event.buttons !== 1) return
input.onMarkScrollGesture(event.target)
input.onUserScroll(event.target)
pointerHeld = true
const release = () => {
pointerHeld = false
window.removeEventListener("pointerup", release)
window.removeEventListener("pointercancel", release)
}
window.addEventListener("pointerup", release)
window.addEventListener("pointercancel", release)
}
const handleListKeyDown = (event: KeyboardEvent & { currentTarget: HTMLDivElement }) => {
@@ -319,18 +239,25 @@ export function createTimelineVirtualizer(input: Input) {
if (!key) return
if (!isScrollKeyTarget(event.target, key)) return
if (scrollKeyOwner(event.currentTarget, event.target, key) !== event.currentTarget) return
if (!prependLoading) clearPrependAnchor()
input.onMarkScrollGesture(event.currentTarget)
input.onUserScroll(event.currentTarget)
if (upwardKeys.has(key)) input.onUnpin()
}
// Following resumes by arriving at the end, either by scrolling there or by content shrinking
// under a viewport that was already there. Merely resting near the end is not enough, otherwise
// a later scroll would overwrite an upward intent expressed a pixel short of the bottom.
const handleListScroll = (event: Event & { currentTarget: HTMLDivElement }) => {
if (prependLoading) updatePrependAnchor()
input.onScheduleScrollState(event.currentTarget)
const root = event.currentTarget
const previousTop = scrollTop
const previousMaxScroll = maxScroll
scrollTop = root.scrollTop
maxScroll = root.scrollHeight - root.clientHeight
const atEnd = maxScroll - scrollTop <= endEpsilon
const arrived = scrollTop > previousTop + endEpsilon || maxScroll < previousMaxScroll
if (maxScroll <= 1 || (atEnd && arrived)) input.onPin()
else if (pointerHeld && scrollTop < previousTop - endEpsilon) input.onUnpin()
input.onScheduleScrollState(root)
input.onHistoryScroll()
if (!input.hasScrollGesture()) return
input.onUserScroll()
input.onAutoScrollHandleScroll()
input.onMarkScrollGesture(event.currentTarget)
}
function View(props: ViewProps) {
@@ -381,7 +308,10 @@ export function createTimelineVirtualizer(input: Input) {
{props.renderRow(row, () => {
setReady(true)
if (contentMeasureFrame !== undefined) cancelAnimationFrame(contentMeasureFrame)
contentMeasureFrame = scheduleConnectedMeasure(element, virtualizer.measureElement)
contentMeasureFrame = requestAnimationFrame(() => {
contentMeasureFrame = undefined
if (element.isConnected) virtualizer.measureElement(element)
})
})}
</div>
</div>
@@ -423,13 +353,10 @@ export function createTimelineVirtualizer(input: Input) {
onWheel={handleListWheel}
onTouchStart={handleListTouchStart}
onTouchMove={handleListTouchMove}
onTouchEnd={handleListTouchEnd}
onTouchCancel={handleListTouchEnd}
onPointerDown={handleListPointerDown}
onPointerMove={handleListPointerMove}
onKeyDown={handleListKeyDown}
onScroll={handleListScroll}
onClick={input.onAutoScrollInteraction}
onClick={input.onSelectionInteraction}
class="relative min-w-0 w-full h-full"
style={{ "--sticky-accordion-top": input.showHeader() ? "48px" : "0px" }}
>
@@ -446,10 +373,11 @@ export function createTimelineVirtualizer(input: Input) {
<Show when={rows().length > 0}>
<div
data-timeline-row="bottom-spacer"
aria-hidden="true"
class="h-16 absolute top-0 left-0 w-full"
style={{ transform: `translateY(${virtualizer.getTotalSize() - 64}px)` }}
/>
>
{props.bottomSpacer}
</div>
</Show>
</div>
</ScrollView>
@@ -458,16 +386,13 @@ export function createTimelineVirtualizer(input: Input) {
}
onCleanup(() => {
clearPrependAnchor()
cache.delete(ownerSessionKey)
cache.set(ownerSessionKey, { measurements: virtualizer.takeSnapshot(), toolOpen: { ...toolOpen } })
while (cache.size > 16) cache.delete(cache.keys().next().value!)
if (resizePinFrame !== undefined) cancelAnimationFrame(resizePinFrame)
if (overscanFrame !== undefined) cancelAnimationFrame(overscanFrame)
input.setScrollRef(undefined)
input.setRevealMessage?.(() => {})
input.setScrollToEnd?.(() => {})
input.setHistoryAnchor?.({ capture: () => {}, restore: () => {} })
})
return {
@@ -478,30 +403,3 @@ export function createTimelineVirtualizer(input: Input) {
View,
}
}
function boundaryTarget(root: HTMLElement, target: EventTarget | null) {
const current = target instanceof Element ? target : undefined
const nested = current?.closest("[data-scrollable]")
if (!(nested instanceof HTMLElement) || nested === root) return undefined
return nested
}
function markBoundaryGesture(input: {
root: HTMLElement
target: EventTarget | null
delta: number
onMarkScrollGesture: (target?: EventTarget | null) => void
}) {
const target = boundaryTarget(input.root, input.target)
if (
target &&
!shouldMarkBoundaryGesture({
delta: input.delta,
scrollTop: target.scrollTop,
scrollHeight: target.scrollHeight,
clientHeight: target.clientHeight,
})
)
return
input.onMarkScrollGesture(input.root)
}
@@ -15,7 +15,7 @@ export const useSessionHashScroll = (input: {
pendingMessage: () => string | undefined
setPendingMessage: (value: string | undefined) => void
setActiveMessage: (message: SessionMessageUser | undefined) => void
autoScroll: { pause: () => void; forceScrollToBottom: () => void }
follow: { unpin: () => void; toBottom: () => void }
scroller: () => HTMLDivElement | undefined
anchor: (id: string) => string
revealMessage?: (id: string) => void
@@ -101,7 +101,7 @@ export const useSessionHashScroll = (input: {
const applyHash = (behavior: ScrollBehavior) => {
const hash = location.hash.slice(1)
if (!hash) {
input.autoScroll.forceScrollToBottom()
input.follow.toBottom()
const el = input.scroller()
if (el) input.scheduleScrollState(el)
return
@@ -109,7 +109,7 @@ export const useSessionHashScroll = (input: {
const messageId = messageIdFromHash(hash)
if (messageId) {
input.autoScroll.pause()
input.follow.unpin()
const msg = messageById().get(messageId)
if (msg) {
scrollToMessage(msg, behavior)
@@ -120,12 +120,12 @@ export const useSessionHashScroll = (input: {
const target = document.getElementById(hash)
if (target) {
input.autoScroll.pause()
input.follow.unpin()
scrollToElement(target, behavior)
return
}
input.autoScroll.forceScrollToBottom()
input.follow.toBottom()
const el = input.scroller()
if (el) input.scheduleScrollState(el)
}
@@ -166,7 +166,7 @@ export const useSessionHashScroll = (input: {
if (pending) input.setPendingMessage(undefined)
if (input.currentMessageId() === targetId && !pending) return
input.autoScroll.pause()
input.follow.unpin()
cancel()
queue(() => scrollToMessage(msg, "auto"))
})
+29 -1
View File
@@ -7,7 +7,7 @@ import { TextInput } from "@opencode-ai/ui/text-input"
import { useLanguage } from "@/runtime/i18n/language"
import { usePlatform } from "@/runtime/platform/platform"
import { useUpdaterAction } from "@/shell/updates/action"
import { type WorkspaceDefaultDestination, useSettings } from "@/settings/model"
import { type TerminalPlacement, type WorkspaceDefaultDestination, useSettings } from "@/settings/model"
import { ExternalLink } from "@/runtime/platform/external-link"
import { SettingsList } from "@/settings/list"
import { SettingsRow } from "@/settings/row"
@@ -123,6 +123,33 @@ const ShellSetting: Component<{ controller: ShellSettingsController }> = (props)
)
}
const TerminalPlacementSetting: Component = () => {
const language = useLanguage()
const settings = useSettings()
const options = createMemo((): { value: TerminalPlacement; label: string }[] => [
{ value: "side", label: language.t("settings.general.row.terminalPlacement.side") },
{ value: "bottom", label: language.t("settings.general.row.terminalPlacement.bottom") },
])
return (
<SettingsRow
title={language.t("settings.general.row.terminalPlacement.title")}
description={language.t("settings.general.row.terminalPlacement.description")}
>
<Select
data-action="settings-terminal-placement"
options={options()}
current={options().find((option) => option.value === settings.general.terminalPlacement())}
value={(option) => option.value}
label={(option) => option.label}
placement="bottom-end"
gutter={6}
onSelect={(option) => option && settings.general.setTerminalPlacement(option.value)}
/>
</SettingsRow>
)
}
const AppearanceSection: Component<{ controller: AppearanceSettingsController }> = (props) => {
const language = useLanguage()
return (
@@ -273,6 +300,7 @@ export const SettingsGeneral: Component<{
<PermissionScopeSetting controller={permissionScope} />
<ShellSetting controller={shell} />
<TerminalPlacementSetting />
<SettingsRow
title={language.t("settings.general.row.reasoningSummaries.title")}
+10
View File
@@ -6,6 +6,7 @@ import { ScopedKey, type ServerScope } from "@/runtime/server/scope"
export type WorkspaceDefaultDestination = "last-used" | "local" | "new"
export type WorkspaceLastUsed = "local" | "workspace"
export type TerminalPlacement = "side" | "bottom"
export interface NotificationSettings {
agent: boolean
@@ -36,6 +37,7 @@ export interface Settings {
editToolPartsExpanded: boolean
showCustomAgents: boolean
mobileTitlebarPosition: "top" | "bottom"
terminalPlacement: TerminalPlacement
}
appearance: {
fontSize: number
@@ -121,6 +123,7 @@ const defaultSettings: Settings = {
editToolPartsExpanded: false,
showCustomAgents: false,
mobileTitlebarPosition: "top",
terminalPlacement: "side",
},
appearance: {
fontSize: 14,
@@ -240,6 +243,13 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
setMobileTitlebarPosition(value: "top" | "bottom") {
setStore("general", "mobileTitlebarPosition", value)
},
terminalPlacement: withFallback(
() => store.general?.terminalPlacement,
defaultSettings.general.terminalPlacement,
),
setTerminalPlacement(value: TerminalPlacement) {
setStore("general", "terminalPlacement", value)
},
},
visibility: {
fileTree: showFileTree,
@@ -93,14 +93,24 @@ export const SettingsWorkspaces: Component<{ activeDirectory?: string }> = (prop
activeDirectory: props.activeDirectory,
}
}
const loadSessions = async (context = captureDeleteContext()) => {
const fetched = await listAllSessions(context.sdk.api.session, { order: "desc" })
fetched.forEach(context.data.session.remember)
return mergeWorkspaceSessionInventory(fetched, context.data.session.list())
// Fetch sessions per workspace directory instead of paging through every session on the server.
const loadSessions = async (directories: readonly string[], context = captureDeleteContext()) => {
const fetched = await Promise.all(
directories.map((directory) => listAllSessions(context.sdk.api.session, { order: "desc", directory })),
)
const sessions = fetched.flat()
return mergeWorkspaceSessionInventory(sessions, context.data.session.list())
}
const workspaceDirectories = createMemo(() => workspaces().map((workspace) => workspace.directory))
const sessionQuery = useQuery(() => ({
queryKey: [serverSDK.scope, null, "settings-workspace-sessions"] as const,
queryFn: () => loadSessions().then(() => Date.now()),
queryKey: [
serverSDK.scope,
null,
"settings-workspace-sessions",
workspaceDirectories().map((directory) => String(pathKey(directory))),
] as const,
queryFn: () => loadSessions(workspaceDirectories()),
enabled: workspaceDirectories().length > 0,
refetchOnMount: "always",
}))
const sessionsByWorkspace = createMemo(
@@ -108,7 +118,7 @@ export const SettingsWorkspaces: Component<{ activeDirectory?: string }> = (prop
new Map(
workspaces().map((workspace) => [
pathKey(workspace.directory),
sessionQuery.isSuccess ? sessionsForWorkspace(data.session.list(), workspace.directory) : [],
sessionQuery.data ? sessionsForWorkspace(sessionQuery.data, workspace.directory) : [],
]),
),
)
@@ -136,7 +146,7 @@ export const SettingsWorkspaces: Component<{ activeDirectory?: string }> = (prop
const [working, branch, sessions] = await Promise.all([
context.sdk.api.vcs.status({ location: { directory: workspace.directory } }),
context.sdk.api.vcs.diff({ location: { directory: workspace.directory }, mode: "branch" }),
loadSessions(context),
loadSessions([workspace.directory], context),
])
const result = inspectWorkspaceDeletion({
workspace: workspace.directory,
+28 -25
View File
@@ -3,6 +3,7 @@ import { createStore } from "solid-js/store"
import { Titlebar, type TitlebarUpdate } from "@/shell/titlebar/titlebar"
import { usePlatform } from "@/runtime/platform/platform"
import { ToastRegion } from "@/shell/notifications/toast"
import { TitlebarRightProvider } from "@/shell/titlebar/right-slot"
const DebugBar = lazy(() => import("@/shell/debug/debug-bar").then((module) => ({ default: module.DebugBar })))
@@ -23,30 +24,32 @@ export default function Layout(props: ParentProps) {
}
return (
<div
class="relative bg-v2-background-bg-deep flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text"
style={{
"padding-top": "env(safe-area-inset-top, 0px)",
"padding-bottom": "env(safe-area-inset-bottom, 0px)",
}}
>
<Titlebar
update={update}
debugTools={
import.meta.env.DEV
? { visible: state.debugTools, toggle: () => setState("debugTools", (value) => !value) }
: undefined
}
/>
<main class="flex-1 min-h-0 min-w-0 overflow-x-hidden flex flex-col items-start contain-strict">
<Suspense>{props.children}</Suspense>
</main>
<Show when={import.meta.env.DEV && state.debugTools}>
<Suspense>
<DebugBar inline />
</Suspense>
</Show>
<ToastRegion />
</div>
<TitlebarRightProvider>
<div
class="relative bg-v2-background-bg-deep flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text"
style={{
"padding-top": "env(safe-area-inset-top, 0px)",
"padding-bottom": "env(safe-area-inset-bottom, 0px)",
}}
>
<Titlebar
update={update}
debugTools={
import.meta.env.DEV
? { visible: state.debugTools, toggle: () => setState("debugTools", (value) => !value) }
: undefined
}
/>
<main class="flex-1 min-h-0 min-w-0 overflow-x-hidden flex flex-col items-start contain-strict">
<Suspense>{props.children}</Suspense>
</main>
<Show when={import.meta.env.DEV && state.debugTools}>
<Suspense>
<DebugBar inline />
</Suspense>
</Show>
<ToastRegion />
</div>
</TitlebarRightProvider>
)
}
@@ -0,0 +1,27 @@
import { describe, expect, test } from "bun:test"
import { createRoot } from "solid-js"
import { createTitlebarRightSlot } from "./right-slot"
describe("titlebar right slot", () => {
test("selects the latest owner and restores the previous owner after overlap", () => {
createRoot((dispose) => {
const slot = createTitlebarRightSlot()
const committed = slot.createRegistration()
committed.register()
expect(committed.active()).toBe(true)
const shadow = slot.createRegistration()
shadow.register()
expect(committed.active()).toBe(false)
expect(shadow.active()).toBe(true)
shadow.unregister()
expect(committed.active()).toBe(true)
expect(shadow.active()).toBe(false)
committed.unregister()
expect(committed.active()).toBe(false)
dispose()
})
})
})
@@ -0,0 +1,65 @@
import { createContext, onCleanup, onMount, Show, useContext, type ParentProps } from "solid-js"
import { createStore } from "solid-js/store"
import { Portal } from "solid-js/web"
type Registration = {
active: () => boolean
register: () => void
unregister: () => void
}
type TitlebarRightSlot = {
createRegistration: () => Registration
mount: () => HTMLElement | undefined
setMount: (mount: HTMLElement) => void
}
const TitlebarRightContext = createContext<TitlebarRightSlot>()
export function TitlebarRightProvider(props: ParentProps) {
return (
<TitlebarRightContext.Provider value={createTitlebarRightSlot()}>{props.children}</TitlebarRightContext.Provider>
)
}
export function createTitlebarRightSlot(): TitlebarRightSlot {
const [store, setStore] = createStore<{ mount?: HTMLElement; registrations: symbol[] }>({ registrations: [] })
return {
mount: () => store.mount,
setMount: (mount) => setStore("mount", mount),
createRegistration() {
const id = Symbol()
return {
active: () => store.registrations.at(-1) === id,
register: () => setStore("registrations", (items) => [...items, id]),
unregister: () => setStore("registrations", (items) => items.filter((item) => item !== id)),
}
},
}
}
export function TitlebarRightMount() {
const slot = useTitlebarRightSlot()
return <div ref={slot.setMount} id="opencode-titlebar-right" class="flex shrink-0 items-center justify-end gap-0" />
}
export function TitlebarRight(props: ParentProps) {
const slot = useTitlebarRightSlot()
const registration = slot.createRegistration()
onMount(() => {
registration.register()
onCleanup(registration.unregister)
})
return (
<Show when={registration.active() && slot.mount()} keyed>
{(mount) => <Portal mount={mount}>{props.children}</Portal>}
</Show>
)
}
function useTitlebarRightSlot() {
const slot = useContext(TitlebarRightContext)
if (!slot) throw new Error("TitlebarRight must be used within TitlebarRightProvider")
return slot
}
+3 -22
View File
@@ -1,15 +1,4 @@
import {
createEffect,
createMemo,
createResource,
createSignal,
Match,
on,
onMount,
Show,
Switch,
untrack,
} from "solid-js"
import { createEffect, createMemo, createResource, Match, createSignal, Show, Switch, untrack } from "solid-js"
import { createStore } from "solid-js/store"
import { useLocation, useNavigate } from "@solidjs/router"
import { IconButton } from "@opencode-ai/ui/icon-button"
@@ -34,6 +23,7 @@ import { tabKey, useTabs } from "@/shell/tabs/tabs"
import type { ComposerState } from "@/composer/persistence"
import "./titlebar.css"
import { newTabTooltipKeybind } from "@/shell/commands/tooltip-keybind"
import { TitlebarRightMount } from "@/shell/titlebar/right-slot"
const titlebarHeight = 36
const minTitlebarZoom = 0.25
@@ -46,15 +36,6 @@ export type TitlebarUpdate = {
install: () => void
}
export function useTitlebarRightMount() {
const language = useLanguage()
const [mount, setMount] = createSignal<HTMLElement | null>(null)
const sync = () => setMount(document.getElementById("opencode-titlebar-right"))
onMount(sync)
createEffect(on(language.direction, sync, { defer: true }))
return mount
}
export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visible: boolean; toggle: () => void } }) {
const platform = usePlatform()
const command = useCommand()
@@ -421,7 +402,7 @@ function TitlebarRight(props: { state: TitlebarRightState }) {
<Show when={props.state.update.visible}>
<TitlebarUpdateIconButton state={props.state.update} />
</Show>
<div id="opencode-titlebar-right" class="flex shrink-0 items-center justify-end gap-0" />
<TitlebarRightMount />
</div>
)
}
@@ -27,6 +27,34 @@ test("end anchoring survives consecutive resizes when the first scroll write is
expect(writes).toEqual([{ offset: 120, adjustments: 80 }])
})
test("start anchoring preserves a stable visible item across prepends", () => {
const root = document.createElement("div")
const writes: number[] = []
const options = (keys: string[]) => ({
count: keys.length,
estimateSize: () => 50,
initialOffset: 50,
initialRect: { width: 400, height: 100 },
anchorTo: "start" as const,
getItemKey: (index: number) => keys[index]!,
getScrollElement: () => root,
scrollToFn: (offset: number) => writes.push(offset),
observeElementRect: () => {},
observeElementOffset: (_element: HTMLDivElement, callback: (offset: number, isScrolling: boolean) => void) => {
callback(50, false)
},
})
const virtualizer = new Virtualizer<HTMLDivElement, HTMLDivElement>(options(["c", "d", "e"]))
virtualizer._willUpdate()
virtualizer.getVirtualItems()
virtualizer.setOptions(options(["a", "b", "c", "d", "e"]))
virtualizer._willUpdate()
expect(virtualizer.getScrollOffset()).toBe(150)
expect(writes.at(-1)).toBe(150)
})
test("reactive count updates preserve measured row sizes", () => {
createRoot((dispose) => {
const [count, setCount] = createSignal(2)
+42 -1
View File
@@ -1,5 +1,6 @@
import { sentryVitePlugin } from "@sentry/vite-plugin"
import { defineConfig } from "vite"
import { VitePWA } from "vite-plugin-pwa"
import desktopPlugin from "./vite.js"
const sentry =
@@ -20,7 +21,47 @@ const sentry =
: false
export default defineConfig({
plugins: [desktopPlugin, sentry] as any,
plugins: [
desktopPlugin,
VitePWA({
strategies: "generateSW",
manifest: false,
workbox: {
cleanupOutdatedCaches: true,
clientsClaim: true,
inlineWorkboxRuntime: true,
navigateFallback: "/index.html",
navigateFallbackDenylist: [/^\/api(?:\/|$)/],
globPatterns: [
"index.html",
"site.webmanifest",
"favicon*",
"apple-touch-icon*",
"web-app-manifest*",
"assets/index-*.{js,css}",
"assets/session-*.js",
"assets/Inter.ttf",
"assets/JetBrainsMonoNerdFontMono-Regular.woff2",
],
runtimeCaching: [
{
urlPattern: ({ url }) => url.origin === self.location.origin && url.pathname.startsWith("/assets/"),
handler: "CacheFirst",
options: {
cacheName: "opencode-assets",
cacheableResponse: {
statuses: [200],
},
expiration: {
maxEntries: 1000,
},
},
},
],
},
}),
sentry,
] as any,
server: {
host: "0.0.0.0",
allowedHosts: true,
+2 -1
View File
@@ -32,9 +32,10 @@ function serveUI(request: HttpServerRequest.HttpServerRequest, url: URL, assets:
if (request.method !== "GET" && request.method !== "HEAD")
return Effect.succeed(HttpServerResponse.empty({ status: 405 }))
const html = name === "index.html"
const revalidate = html || name === "sw.js" || name === "registerSW.js"
const headers = {
"content-type": FSUtil.mimeType(name),
"cache-control": html ? "no-cache" : "public, max-age=31536000, immutable",
"cache-control": revalidate ? "no-cache" : "public, max-age=31536000, immutable",
"content-security-policy": html
? cspForHtml(typeof file === "string" ? file : Buffer.from(file).toString())
: csp(),
+9
View File
@@ -20,6 +20,8 @@ describe("web UI", () => {
const assets = {
"index.html": await Bun.file(index).text(),
"app.js": await Bun.file(asset).text(),
"sw.js": "service worker",
"registerSW.js": "registration",
"font.woff2": new Uint8Array([0, 1, 2, 255]),
}
@@ -53,6 +55,13 @@ describe("web UI", () => {
const script = yield* Effect.promise(() => fetch(`${origin}/app.js`))
expect(yield* Effect.promise(() => script.text())).toBe("console.log('embedded')")
expect(script.headers.get("cache-control")).toBe("public, max-age=31536000, immutable")
const worker = yield* Effect.promise(() => fetch(`${origin}/sw.js`))
expect(worker.headers.get("cache-control")).toBe("no-cache")
const registration = yield* Effect.promise(() => fetch(`${origin}/registerSW.js`))
expect(registration.headers.get("cache-control")).toBe("no-cache")
const font = yield* Effect.promise(() => fetch(`${origin}/font.woff2`))
expect(new Uint8Array(yield* Effect.promise(() => font.arrayBuffer()))).toEqual(
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -80,6 +80,8 @@ import type {
SessionMessageOutput,
SessionEnvironmentInput,
SessionEnvironmentOutput,
SessionViewInput,
SessionViewOutput,
MessageListInput,
MessageListOutput,
ModelListInput,
@@ -898,6 +900,18 @@ export function make(options: ClientOptions) {
},
requestOptions,
),
view: (input: SessionViewInput, requestOptions?: RequestOptions) =>
request<SessionViewOutput>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/view`,
body: { idle: input["idle"] },
successStatus: 204,
declaredStatuses: [404, 401, 400],
empty: true,
},
requestOptions,
),
},
message: {
list: (input: MessageListInput, requestOptions?: RequestOptions) =>
+65 -16
View File
@@ -263,6 +263,8 @@ export type SessionInboxCompaction = {
export type InstructionEntryInfo = { key: InstructionEntryKey; value: JsonValue }
export type InstructionEntrySnapshot = Array<{ key: InstructionEntryKey; value: JsonValue; removed: boolean }>
export type SessionCreated = {
id: string
created: number
@@ -324,6 +326,16 @@ export type SessionRenamed = {
data: { sessionID: string; title: string }
}
export type SessionViewed = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.viewed"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; idle: number }
}
export type SessionDeleted = {
id: string
created: number
@@ -334,16 +346,6 @@ export type SessionDeleted = {
data: { sessionID: string }
}
export type SessionForked = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.forked"
durable: { aggregateID: string; seq: number; version: 2 }
location?: LocationRef
data: { sessionID: string; parentID: string; boundary: SessionForkBoundary; instructions?: { [x: string]: string } }
}
export type SessionInboxDelivered = {
id: string
created: number
@@ -1087,7 +1089,7 @@ export type PtyUpdated = {
data: { info: Pty }
}
export type SessionStatus2 = {
export type SessionStatusUpdated = {
id: string
created: number
metadata?: { [x: string]: any }
@@ -1302,6 +1304,22 @@ export type FormWhen = {
export type ToolContent = ToolTextContent | ToolFileContent
export type SessionForked = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.forked"
durable: { aggregateID: string; seq: number; version: 2 }
location?: LocationRef
data: {
sessionID: string
parentID: string
boundary: SessionForkBoundary
instructions?: { [x: string]: string }
instructionEntries?: InstructionEntrySnapshot
}
}
export type SessionShellStarted = {
id: string
created: number
@@ -1664,7 +1682,8 @@ export type SessionInfo = {
model?: ModelRef
cost: MoneyUSD
tokens: TokenUsageInfo
time: { created: number; updated: number; archived?: number }
outcome?: "succeeded" | "failed" | "interrupted"
time: { created: number; updated: number; idle?: number; viewed?: number; archived?: number }
title?: string
location: LocationRef
subpath?: string
@@ -1949,6 +1968,7 @@ export type SessionEventDurable =
| SessionModelSelected
| SessionMoved
| SessionRenamed
| SessionViewed
| SessionDeleted
| SessionForked
| SessionInboxDelivered
@@ -2034,6 +2054,7 @@ export type V2Event =
| SessionModelSelected
| SessionMoved
| SessionRenamed
| SessionViewed
| SessionUsageUpdated
| SessionDeleted
| SessionForked
@@ -2096,7 +2117,7 @@ export type V2Event =
| FormReplied
| FormCancelled
| WebsearchUpdated
| SessionStatus2
| SessionStatusUpdated
| SessionIdle
| TuiPromptAppend
| TuiCommandExecute
@@ -2510,7 +2531,14 @@ export type SessionImportInput = {
readonly reasoning: number
readonly cache: { readonly read: number; readonly write: number }
}
readonly time: { readonly created: number; readonly updated: number; readonly archived?: number }
readonly outcome?: "succeeded" | "failed" | "interrupted"
readonly time: {
readonly created: number
readonly updated: number
readonly idle?: number
readonly viewed?: number
readonly archived?: number
}
readonly title?: string
readonly location: { readonly directory: string; readonly workspaceID?: string }
readonly subpath?: string
@@ -2778,7 +2806,14 @@ export type SessionImportInput = {
readonly reasoning: number
readonly cache: { readonly read: number; readonly write: number }
}
readonly time: { readonly created: number; readonly updated: number; readonly archived?: number }
readonly outcome?: "succeeded" | "failed" | "interrupted"
readonly time: {
readonly created: number
readonly updated: number
readonly idle?: number
readonly viewed?: number
readonly archived?: number
}
readonly title?: string
readonly location: { readonly directory: string; readonly workspaceID?: string }
readonly subpath?: string
@@ -3046,7 +3081,14 @@ export type SessionImportInput = {
readonly reasoning: number
readonly cache: { readonly read: number; readonly write: number }
}
readonly time: { readonly created: number; readonly updated: number; readonly archived?: number }
readonly outcome?: "succeeded" | "failed" | "interrupted"
readonly time: {
readonly created: number
readonly updated: number
readonly idle?: number
readonly viewed?: number
readonly archived?: number
}
readonly title?: string
readonly location: { readonly directory: string; readonly workspaceID?: string }
readonly subpath?: string
@@ -3980,6 +4022,13 @@ export type SessionEnvironmentInput = {
export type SessionEnvironmentOutput = void
export type SessionViewInput = {
readonly sessionID: { readonly sessionID: string }["sessionID"]
readonly idle: { readonly idle: number }["idle"]
}
export type SessionViewOutput = void
export type MessageListInput = {
readonly sessionID: { readonly sessionID: string }["sessionID"]
readonly limit?: {
+40 -16
View File
@@ -1,7 +1,7 @@
// Client data layer: apply server events and cache API reads into a Solid store.
// Prefer straightforward projection. Do not add generation counters, stale-response
// merges, live/history overlays, or other race machinery here—last write wins.
// Reconnect invalidates cached reads; active UI owners decide what to sync again.
// Prefer straightforward projection. Invalidated reads revalidate serially so an older
// response cannot commit after its replacement. Reconnect invalidates cached reads;
// active UI owners decide what to sync again.
import type {
AgentInfo,
@@ -120,32 +120,46 @@ function locationQuery(ref?: LocationRef) {
}
function createSync() {
const state = new Map<string, true | Promise<void>>()
type Pending = { promise: Promise<void>; invalidated: boolean }
const state = new Map<string, true | Pending>()
const start = (key: string, load: () => Promise<void>, wait?: Promise<void>) => {
const entry: Pending = { promise: Promise.resolve(), invalidated: false }
state.set(key, entry)
entry.promise = (wait ? wait.catch(() => undefined).then(load) : load())
.then(() => {
if (state.get(key) === entry) state.set(key, true)
})
.finally(() => {
if (state.get(key) === entry) state.delete(key)
})
return entry.promise
}
return {
run(key: string, load: () => Promise<void>) {
const active = state.get(key)
if (active === true) return Promise.resolve()
if (active) return active
const pending = load()
.then(() => {
if (state.get(key) === pending) state.set(key, true)
})
.finally(() => {
if (state.get(key) === pending) state.delete(key)
})
state.set(key, pending)
return pending
if (!active) return start(key, load)
if (!active.invalidated) return active.promise
return start(key, load, active.promise)
},
complete(key: string) {
if (state.has(key)) return
state.set(key, true)
},
has(key: string) {
return state.has(key)
},
invalidate(key?: string) {
if (key) {
state.delete(key)
const active = state.get(key)
if (active === true) state.delete(key)
if (active !== undefined && active !== true) active.invalidated = true
return
}
state.clear()
state.forEach((active, current) => {
if (active === true) state.delete(current)
if (active !== true) active.invalidated = true
})
},
}
}
@@ -882,6 +896,16 @@ export function createData(config: CreateDataInput) {
const currentAssistant = message.activeAssistant(draft)
if (currentAssistant) currentAssistant.retry = undefined
})
if (event.type === "session.execution.interrupted" && event.data.reason === "shutdown") return
// An event can overtake the first read; queue a revalidation when that read is still active.
if (!store.session.info[event.data.sessionID] && !sync.has(`session:${event.data.sessionID}`)) return
result.session.invalidate(event.data.sessionID)
void result.session.sync(event.data.sessionID)
return
case "session.viewed":
if (!store.session.info[event.data.sessionID] && !sync.has(`session:${event.data.sessionID}`)) return
result.session.invalidate(event.data.sessionID)
void result.session.sync(event.data.sessionID)
return
case "session.revert.staged":
if (store.session.info[event.data.sessionID])
@@ -19,8 +19,8 @@ test("effect entrypoint exposes canonical Schema contracts", () => {
test("generated Effect API names canonical and composed outputs", async () => {
const source = await Bun.file(new URL("../src/effect/api/api.ts", import.meta.url)).text()
expect(source).toContain("export type Endpoint5_5Output = Session.Info")
expect(source).toContain("export type Endpoint19_0Output = OpenCodeEvent")
expect(source).toContain("export type SessionGetOutput = Session.Info")
expect(source).toContain("export type EventSubscribeOutput = OpenCodeEvent")
expect(source).not.toContain("HttpApiClient.ForApi")
})
+11 -1
View File
@@ -136,8 +136,10 @@ test("event.subscribe terminates on Effect protocol decode failures", async () =
test("session methods retain decoded Effect inputs and outputs", async () => {
const logQueries: Array<Record<string, string>> = []
const requests: Array<{ method: string; url: string }> = []
const httpClient = HttpClient.make((request) => {
const url = request.url
requests.push({ method: request.method, url })
if (url.includes("/log")) {
logQueries.push(Object.fromEntries(request.urlParams.params))
return Effect.succeed(
@@ -183,6 +185,7 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
const created = yield* client.session.create({
location: Location.Ref.make({ directory: AbsolutePath.make("/tmp/project") }),
})
yield* client.session.view({ sessionID: Session.ID.make("ses_test"), idle: session.data.time.idle })
yield* client.session.switchAgent({ sessionID: Session.ID.make("ses_test"), agent: Agent.ID.make("build") })
yield* client.session.switchModel({
sessionID: Session.ID.make("ses_test"),
@@ -207,7 +210,11 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
return { page, active, created, admitted, context, log, message }
}).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
expect(DateTime.toEpochMillis(result.page.data[0].time.created)).toBe(1_717_171_717_000)
const listed = result.page.data[0]
if (!listed?.time.idle || !listed.time.viewed) throw new Error("Expected attention times")
expect(DateTime.toEpochMillis(listed.time.created)).toBe(1_717_171_717_000)
expect(DateTime.toEpochMillis(listed.time.idle)).toBe(1_717_171_717_002)
expect(DateTime.toEpochMillis(listed.time.viewed)).toBe(1_717_171_717_001)
expect(result.active).toEqual({ ses_test: { type: "running" } })
expect(Object.getPrototypeOf(result.page.data[0])).toBe(Object.prototype)
expect(Object.getPrototypeOf(result.created)).toBe(Object.prototype)
@@ -217,6 +224,7 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
expect(DateTime.toEpochMillis(result.admitted.timeCreated)).toBe(1_717_171_717_000)
expect(result.context).toEqual([])
expect(logQueries[0]).toEqual({ after: "0" })
expect(requests).toContainEqual({ method: "POST", url: "http://localhost:3000/api/session/ses_test/view" })
const logged = Array.from(result.log)
expect(logged.map((item) => item.type)).toEqual(["session.model.selected", "log.synced"])
expect(logged[0]?.type === "session.model.selected" && logged[0].created).toBe(1_717_171_717_000)
@@ -258,6 +266,8 @@ const session = {
time: {
created: 1_717_171_717_000,
updated: 1_717_171_717_000,
idle: 1_717_171_717_002,
viewed: 1_717_171_717_001,
},
title: "Test",
location: { directory: "/tmp/project" },
+8
View File
@@ -524,6 +524,7 @@ test("session methods use the public HTTP contract", async () => {
const page = await client.session.list({ limit: 10, order: "desc", parentID: null })
const active = await client.session.active()
const created = await client.session.create({ location: { directory: "/tmp/project" } })
await client.session.view({ sessionID: "ses_test", idle: session.data.time.idle })
await client.session.switchAgent({ sessionID: "ses_test", agent: "build" })
await client.session.switchModel({
sessionID: "ses_test",
@@ -550,6 +551,7 @@ test("session methods use the public HTTP contract", async () => {
const message = await client.session.message({ sessionID: "ses_test", messageID: "msg_model" })
expect(page.cursor.next).toBe("next")
expect(page.data[0].time).toMatchObject({ idle: 1_717_171_717_002, viewed: 1_717_171_717_001 })
expect(active).toEqual({ ses_test: { type: "running" } })
expect(created.id).toBe("ses_test")
expect(admitted.id).toBe("msg_test")
@@ -562,6 +564,7 @@ test("session methods use the public HTTP contract", async () => {
["GET", "http://localhost:3000/api/session?limit=10&order=desc&parentID=null"],
["GET", "http://localhost:3000/api/session/active"],
["POST", "http://localhost:3000/api/session"],
["POST", "http://localhost:3000/api/session/ses_test/view"],
["POST", "http://localhost:3000/api/session/ses_test/agent"],
["POST", "http://localhost:3000/api/session/ses_test/model"],
["POST", "http://localhost:3000/api/session/ses_test/prompt"],
@@ -574,6 +577,9 @@ test("session methods use the public HTTP contract", async () => {
["POST", "http://localhost:3000/api/session/ses_test/interrupt?continue=true"],
["GET", "http://localhost:3000/api/session/ses_test/message/msg_model"],
])
const viewBody = requests.find((request) => request.url.endsWith("/api/session/ses_test/view"))?.init?.body
if (typeof viewBody !== "string") throw new Error("Expected JSON view request body")
expect(JSON.parse(viewBody)).toEqual({ idle: session.data.time.idle })
const body = requests.find((request) => request.url.endsWith("/api/session/ses_test/prompt"))?.init?.body
if (typeof body !== "string") throw new Error("Expected JSON request body")
expect(JSON.parse(body)).toEqual({
@@ -636,6 +642,8 @@ const session = {
time: {
created: 1_717_171_717_000,
updated: 1_717_171_717_000,
idle: 1_717_171_717_002,
viewed: 1_717_171_717_001,
},
title: "Test",
location: { directory: "/tmp/project" },
+81
View File
@@ -0,0 +1,81 @@
import { test } from "bun:test"
import { createRoot } from "solid-js"
import { createData, type CreateDataInput } from "../src/solid"
import { OpenCode, type OpenCodeEvent, type SessionInfo } from "../src/promise"
const session = (viewed: number): SessionInfo => ({
id: "ses_refresh",
projectID: "project",
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
outcome: "succeeded",
time: { created: 0, updated: 0, idle: 2, viewed },
location: { directory: "/project" },
})
test("revalidates after an event overtakes an active session read", async () => {
let release!: () => void
const gate = new Promise<void>((resolve) => (release = resolve))
const listeners = new Set<Parameters<CreateDataInput["event"]["listen"]>[0]>()
let requests = 0
const api = OpenCode.make({
baseUrl: "http://opencode.local",
fetch: async (input, init) => {
const request = input instanceof Request ? input : new Request(input, init)
if (!request.url.endsWith("/api/session/ses_refresh")) throw new Error(`Unexpected request: ${request.url}`)
requests++
if (requests === 1) {
await gate
return Response.json({ data: session(1) })
}
return Response.json({ data: session(2) })
},
})
const event: CreateDataInput["event"] = {
on:
<Type extends OpenCodeEvent["type"]>(
_type: Type,
_handler: (event: Extract<OpenCodeEvent, { type: Type }>) => void,
) =>
() => {},
listen(handler) {
listeners.add(handler)
return () => listeners.delete(handler)
},
}
const setup = createRoot((dispose) => ({
data: createData({ api: () => api, directory: "/project", event, connection: { status: () => "connected" } }),
dispose,
}))
try {
setup.data.session.remember(session(1))
setup.data.session.invalidate("ses_refresh")
const initial = setup.data.session.sync("ses_refresh")
await wait(() => requests === 1)
const viewed: OpenCodeEvent = {
id: "evt_viewed",
created: 2,
type: "session.viewed",
durable: { aggregateID: "ses_refresh", seq: 1, version: 1 },
data: { sessionID: "ses_refresh", idle: 2 },
}
listeners.forEach((listener) => listener({ name: viewed.type, details: viewed }))
await Bun.sleep(20)
release()
await initial
await wait(() => requests === 2 && setup.data.session.get("ses_refresh")?.time.viewed === 2)
} finally {
setup.dispose()
}
})
async function wait(check: () => boolean) {
const started = Date.now()
while (!check()) {
if (Date.now() - started > 2_000) throw new Error("Timed out waiting for condition")
await Bun.sleep(10)
}
}
+3 -3
View File
@@ -841,17 +841,17 @@ export class Interpreter<R> {
private customIterator(value: unknown, node: AstNode, allowAsync = true) {
if (value instanceof CodeModeGenerator) {
if (value.asynchronous && !allowAsync) return Effect.succeed(undefined)
if (value.asynchronous && !allowAsync) return Effect.undefined
return Effect.succeed({
iterator: value,
next: new GeneratorMethodReference(value, "next"),
asynchronous: value.asynchronous,
})
}
if (!isRecord(value) || isRuntimeReference(value)) return Effect.succeed(undefined)
if (!isRecord(value) || isRuntimeReference(value)) return Effect.undefined
const asyncMethod = allowAsync ? Reflect.get(value, AsyncIteratorSymbol) : undefined
const method = asyncMethod ?? Reflect.get(value, IteratorSymbol)
if (method === undefined || method === null) return Effect.succeed(undefined)
if (method === undefined || method === null) return Effect.undefined
const self = this
return Effect.map(
this.invokeCallable(this.requireIteratorMethod(method, "Iterator method", node), [], node),
-1
View File
@@ -118,7 +118,6 @@
"@opencode-ai/ai": "workspace:*",
"@opencode-ai/schema": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/shell-scan": "workspace:*",
"@opencode-ai/util": "workspace:*",
"@standard-schema/spec": "catalog:",
"@parcel/watcher": "2.5.1",
+32 -2
View File
@@ -1,8 +1,8 @@
{
"version": "7",
"dialect": "sqlite",
"id": "dcde8e6b-4bf4-4f6b-b2be-4030c2c3e936",
"prevIds": ["5c1aa56b-c3ee-4283-9a84-c0bf626dc604"],
"id": "3fb67508-0196-4bae-b2bd-c08ece7583fd",
"prevIds": ["dcde8e6b-4bf4-4f6b-b2be-4030c2c3e936"],
"ddl": [
{
"name": "account_state",
@@ -1350,6 +1350,36 @@
"entityType": "columns",
"table": "session_v2"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_idle",
"entityType": "columns",
"table": "session_v2"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_viewed",
"entityType": "columns",
"table": "session_v2"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "idle_outcome",
"entityType": "columns",
"table": "session_v2"
},
{
"type": "integer",
"notNull": false,
+1 -1
View File
@@ -46,7 +46,7 @@ export const Plugin = define({
function firstMissing(target: string): Effect.Effect<string | undefined> {
const parent = path.dirname(target)
if (parent === target) return Effect.succeed(undefined)
if (parent === target) return Effect.undefined
return fs.isDir(parent).pipe(Effect.flatMap((exists) => (exists ? Effect.succeed(target) : firstMissing(parent))))
}
+2
View File
@@ -43,6 +43,7 @@ import m40 from "./migration/20260808023530_workspace_domain.js"
import m41 from "./migration/20260811161259_execution_claim_attempts.js"
import m42 from "./migration/20260812181746_session_inbox.js"
import m43 from "./migration/20260812213948_worktree.js"
import m44 from "./migration/20260819222447_session_viewed_state.js"
export const migrations = [
m00,
@@ -89,4 +90,5 @@ export const migrations = [
m41,
m42,
m43,
m44,
] satisfies DatabaseMigration.Migration[]
@@ -0,0 +1,15 @@
import { Effect } from "effect"
import type { DatabaseMigration } from "../migration.js"
const migration: DatabaseMigration.Migration = {
id: "20260819222447_session_viewed_state",
up(tx) {
return Effect.gen(function* () {
yield* tx.run(`ALTER TABLE \`session_v2\` ADD \`time_idle\` integer;`)
yield* tx.run(`ALTER TABLE \`session_v2\` ADD \`time_viewed\` integer;`)
yield* tx.run(`ALTER TABLE \`session_v2\` ADD \`idle_outcome\` text;`)
})
},
}
export default migration
+3
View File
@@ -209,6 +209,9 @@ const schema: Omit<DatabaseMigration.Migration, "id"> = {
\`model\` text,
\`time_created\` integer NOT NULL,
\`time_updated\` integer NOT NULL,
\`time_idle\` integer,
\`time_viewed\` integer,
\`idle_outcome\` text,
\`time_compacting\` integer,
\`time_archived\` integer,
\`time_suspended\` integer,
+1 -1
View File
@@ -109,7 +109,7 @@ const layer = Layer.effect(
const next = Bom.split(input.content)
const current = yield* environment.files.read(input.target.absolute, { offset: 0, length: 3 }).pipe(
Effect.map((result) => result.bytes),
Effect.catchTag("Environment.NotFound", () => Effect.succeed(undefined)),
Effect.catchTag("Environment.NotFound", () => Effect.undefined),
)
yield* environment.files.write(
input.target.absolute,
+7 -12
View File
@@ -9,7 +9,6 @@ import { AppProcess } from "@opencode-ai/util/process"
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
import { File } from "./file.js"
import { KeyedMutex } from "./effect/keyed-mutex.js"
import { gitExecutable } from "./util/git-executable.js"
export class Repository extends Schema.Class<Repository>("Git.Repository")({
worktree: AbsolutePath,
@@ -315,7 +314,7 @@ const layer = Layer.effect(
) {
const result = yield* proc
.run(
ChildProcess.make(gitExecutable, repositoryArgs(repository, args), {
ChildProcess.make("git", repositoryArgs(repository, args), {
cwd: repository.worktree,
env: options?.env,
extendEnv: true,
@@ -450,14 +449,10 @@ const layer = Layer.effect(
if (!input.paths.length) return new Set<RelativePath>()
const result = yield* proc
.run(
ChildProcess.make(
gitExecutable,
repositoryArgs(input.repository, ["check-ignore", "--no-index", "--stdin", "-z"]),
{
cwd: input.repository.worktree,
extendEnv: true,
},
),
ChildProcess.make("git", repositoryArgs(input.repository, ["check-ignore", "--no-index", "--stdin", "-z"]), {
cwd: input.repository.worktree,
extendEnv: true,
}),
{ stdin: input.paths.join("\0") + "\0" },
)
.pipe(
@@ -630,7 +625,7 @@ const layer = Layer.effect(
cwd = repository.worktree,
) {
const result = yield* proc
.run(ChildProcess.make(gitExecutable, args, { cwd, extendEnv: true, stdin: "ignore" }))
.run(ChildProcess.make("git", args, { cwd, extendEnv: true, stdin: "ignore" }))
.pipe(
Effect.mapError(
(cause) => new WorktreeError({ operation, directory: worktreeDirectory, message: cause.message, cause }),
@@ -727,7 +722,7 @@ function execute(cwd: string, proc: AppProcess.Interface) {
return (args: string[]) =>
proc
.run(
ChildProcess.make(gitExecutable, args, {
ChildProcess.make("git", args, {
cwd,
extendEnv: true,
stdin: "ignore",
+2 -3
View File
@@ -78,9 +78,8 @@ const layer = Layer.effect(
? "Directory"
: input.kind === "file"
? "File"
: (yield* fs
.stat(absolute)
.pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(undefined))))?.type
: (yield* fs.stat(absolute).pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.undefined)))
?.type
const externalDirectory = type === "Directory" ? absolute : path.dirname(absolute)
const externalResource = slash(path.join(externalDirectory, "*"))
return {
+2 -1
View File
@@ -1,4 +1,5 @@
import { Effect, Layer, LayerMap } from "effect"
import { existsSync } from "fs"
import path from "path"
import { Agent } from "./agent.js"
import { AISDK } from "./aisdk.js"
@@ -146,7 +147,7 @@ export function buildLocationServiceMap(
Layer.provide(LayerNode.compile(location.hoisted)),
)
},
{ idleTimeToLive: "60 minutes" },
{ idleTimeToLive: (ref) => (existsSync(ref.directory) ? "60 minutes" : 0) },
),
(inner) => ({
...inner,
+2 -2
View File
@@ -624,11 +624,11 @@ export const layer = (options?: Options) =>
Effect.map((input) => input as Record<string, SourceProvider>),
Effect.orElseSucceed(() => undefined),
)
: Effect.succeed(undefined)
: Effect.undefined
// The bundled snapshot is the boot-time floor for the catalog; the
// periodic fetch below still refreshes on top.
const loadSnapshot = options?.snapshot === false ? Effect.succeed(undefined) : bundledSnapshot
const loadSnapshot = options?.snapshot === false ? Effect.undefined : bundledSnapshot
const fetchAndWrite = Effect.fn("ModelsDev.fetchAndWrite")(function* () {
const text = yield* fetchApi()
+1 -1
View File
@@ -302,7 +302,7 @@ const layer = Layer.effect(
const rememberedRules = yield* savedRules()
for (const [id, item] of pending) {
const rules = yield* configured(item.request.sessionID, item.agent).pipe(
Effect.catchTag("Session.NotFoundError", () => Effect.succeed(undefined)),
Effect.catchTag("Session.NotFoundError", () => Effect.undefined),
)
if (!rules) continue
if (denied(item.request, rules)) continue
+11 -8
View File
@@ -46,15 +46,18 @@ function oauth(http: HttpClient.HttpClient) {
Effect.gen(function* () {
const server = yield* normalizeServer(answer.server ?? defaultServer)
const device = yield* post(http, `${server}/auth/device/code`, { client_id: clientID }, Device)
const verification = URL.canParse(device.verification_uri_complete)
? new URL(device.verification_uri_complete)
: undefined
if (verification && verification.protocol !== "http:" && verification.protocol !== "https:") {
return yield* Effect.fail(new Error("Invalid device verification URL: expected HTTP(S)"))
}
const verification = yield* Effect.try({
try: () => {
const url = new URL(device.verification_uri_complete, `${server}/`)
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("expected HTTP(S)")
return url
},
catch: (cause) =>
new Error(`Invalid device verification URL: ${cause instanceof Error ? cause.message : String(cause)}`),
})
return {
mode: "auto" as const,
url: verification?.href ?? `${server}/${device.verification_uri_complete.replace(/^\/+/, "")}`,
url: verification.href,
instructions: `Enter code: ${device.user_code}`,
callback: poll(http, server, device.device_code, Duration.seconds(device.interval)),
}
@@ -213,7 +216,7 @@ function fetchProviders(http: HttpClient.HttpClient, value: Credential.Value) {
)
.pipe(
Effect.flatMap((response) => {
if (response.status === 404) return Effect.succeed(undefined)
if (response.status === 404) return Effect.undefined
return HttpClientResponse.filterStatusOk(response).pipe(
Effect.flatMap(HttpClientResponse.schemaBodyJson(RemoteResponse)),
Effect.map((remote) => remote.config.provider),
+1 -1
View File
@@ -10,7 +10,7 @@ export const parseResponse = <F extends Schema.Struct.Fields>(body: string, resu
const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Struct({ result })))
const parse = (payload: string) => {
const trimmed = payload.trim()
if (!trimmed.startsWith("{")) return Effect.succeed(undefined)
if (!trimmed.startsWith("{")) return Effect.undefined
return decode(trimmed).pipe(Effect.map((response) => response.result))
}
return Effect.gen(function* () {
+1 -1
View File
@@ -235,7 +235,7 @@ const layer = Layer.effect(
Effect.mapError((cause) => failure("Invalid ripgrep JSON output", cause)),
Effect.flatMap((json) => {
if (!json || typeof json !== "object" || !("type" in json) || json.type !== "match")
return Effect.succeed(undefined)
return Effect.undefined
return Schema.decodeUnknownEffect(RawMatch)(json).pipe(
Effect.map((match) => ({
...match.data,
+44 -4
View File
@@ -1,7 +1,7 @@
export * as Session from "./session.js"
export * from "./session/schema.js"
import { Effect, Layer, Schema, Context, RcMap, Stream, Scope } from "effect"
import { Cause, Effect, Layer, Schema, Context, RcMap, Stream, Scope } from "effect"
import { ListAnchor } from "@opencode-ai/schema/session"
import { and, asc, desc, eq, gt, isNull, like, lt, or, type SQL } from "drizzle-orm"
import { Project } from "./project.js"
@@ -54,6 +54,7 @@ import { KeyedMutex } from "./effect/keyed-mutex.js"
import { fileURLToPath } from "url"
import { SessionEnvironment } from "./session/environment.js"
import { SessionHistory } from "./session/history.js"
import { InstructionEntry } from "./session/instruction-entry.js"
// get project -> project.locations
//
@@ -156,6 +157,11 @@ export class DestinationNotDirectoryError extends Schema.TaggedError<Destination
"Session.DestinationNotDirectoryError",
{ directory: AbsolutePath },
) {}
export class DestinationUnavailableError extends Schema.TaggedError<DestinationUnavailableError>()(
"Session.DestinationUnavailableError",
{ directory: AbsolutePath },
) {}
export const MessageNotFoundError = SessionRevert.MessageNotFoundError
export type MessageNotFoundError = SessionRevert.MessageNotFoundError
@@ -172,6 +178,7 @@ export interface Interface {
readonly sessionID: SessionSchema.ID
readonly variables?: SessionEnvironment.Variables
}) => Effect.Effect<SessionEnvironment.Variables | undefined, NotFoundError>
readonly view: (input: { sessionID: SessionSchema.ID; idle: number }) => Effect.Effect<void, NotFoundError>
readonly remove: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError>
readonly messages: (input: {
sessionID: SessionSchema.ID
@@ -218,7 +225,10 @@ export interface Interface {
directory: AbsolutePath
workspaceID?: Location.Ref["workspaceID"]
delivery?: SessionInbox.Delivery
}) => Effect.Effect<void, NotFoundError | DestinationNotFoundError | DestinationNotDirectoryError>
}) => Effect.Effect<
void,
NotFoundError | DestinationNotFoundError | DestinationNotDirectoryError | DestinationUnavailableError
>
readonly prompt: (input: {
id?: SessionMessage.ID
sessionID: SessionSchema.ID
@@ -428,6 +438,14 @@ const layer = Layer.effect(
})
if (!boundary) return yield* new ForkEmptyError({ sessionID: input.sessionID })
const sessionID = SessionSchema.ID.create()
const inherited = yield* db
.transaction(() =>
Effect.all({
instructions: InstructionState.current(db, parent.id),
instructionEntries: InstructionEntry.snapshot(db, parent.id),
}),
)
.pipe(Effect.orDie)
// The fork adopts the parent's newest instruction values rather than the
// values in effect at the boundary; copied history may contain frozen
// instruction-update text the initial baseline already reflects.
@@ -435,7 +453,7 @@ const layer = Layer.effect(
sessionID,
parentID: parent.id,
boundary: { ...input.boundary, messageID: boundary.id },
instructions: yield* InstructionState.current(db, parent.id),
...inherited,
})
return yield* result.get(sessionID).pipe(Effect.orDie)
}),
@@ -449,6 +467,18 @@ const layer = Layer.effect(
if (input.variables !== undefined) yield* environments.set(input.sessionID, input.variables)
return yield* environments.get(input.sessionID)
}),
view: Effect.fn("Session.view")(function* (input) {
const row = yield* db
.select({ idle: SessionTable.time_idle, viewed: SessionTable.time_viewed })
.from(SessionTable)
.where(eq(SessionTable.id, input.sessionID))
.get()
.pipe(Effect.orDie)
if (!row) return yield* new NotFoundError({ sessionID: input.sessionID })
if (row.idle === null || input.idle > row.idle || (row.viewed !== null && row.viewed >= input.idle))
return yield* Effect.void
yield* bus.publish(SessionEvent.Viewed, { sessionID: input.sessionID, idle: input.idle })
}),
remove: Effect.fn("Session.remove")(function* (sessionID) {
const session = yield* result.get(sessionID)
yield* execution.interrupt(sessionID)
@@ -772,12 +802,22 @@ const layer = Layer.effect(
if (!info) return yield* new DestinationNotFoundError({ directory })
if (info.type !== "Directory") return yield* new DestinationNotDirectoryError({ directory })
const project = yield* projects.resolve(directory)
yield* persistProject(project)
const payload: SessionInbox.MovePayload = {
location: Location.Ref.make({ directory, workspaceID: input.workspaceID }),
projectID: project.id,
subpath: RelativePath.make(path.relative(project.directory, directory).replaceAll("\\", "/")),
}
yield* Location.Service.pipe(
Effect.provide(locations.get(payload.location)),
Effect.scoped,
Effect.catchCause((cause) => {
if (Cause.hasInterruptsOnly(cause)) return Effect.failCause(cause)
return Effect.logWarning("session move destination unavailable", { directory, cause }).pipe(
Effect.andThen(Effect.fail(new DestinationUnavailableError({ directory }))),
)
}),
)
yield* persistProject(project)
const item = SessionInbox.Item.make({
type: "move",
payload,
+3
View File
@@ -50,9 +50,12 @@ export function fromRow(row: typeof SessionTable.$inferSelect): SessionSchema.In
}),
subpath: row.path ? RelativePath.make(row.path) : undefined,
revert: row.revert ? decodeRevert(row.revert) : undefined,
outcome: row.idle_outcome ?? undefined,
time: {
created: DateTime.makeUnsafe(row.time_created),
updated: DateTime.makeUnsafe(row.time_updated),
idle: row.time_idle === null ? undefined : DateTime.makeUnsafe(row.time_idle),
viewed: row.time_viewed === null ? undefined : DateTime.makeUnsafe(row.time_viewed),
archived: row.time_archived ? DateTime.makeUnsafe(row.time_archived) : undefined,
},
})
@@ -13,9 +13,59 @@ export const Key = InstructionEntry.Key
export type Key = typeof Key.Type
export const Info = InstructionEntry.Info
export type Info = typeof Info.Type
export const Snapshot = InstructionEntry.Snapshot
export type Snapshot = typeof Snapshot.Type
export const MaxValueBytes = InstructionEntry.MaxValueBytes
export const ValueTooLargeError = InstructionEntry.ValueTooLargeError
type DatabaseService = Database.Interface["db"]
const InsertBatchSize = 10
export const snapshot = Effect.fn("InstructionEntry.snapshot")(function* (
db: DatabaseService,
sessionID: SessionSchema.ID,
) {
return yield* db
.select({
key: InstructionEntryTable.key,
value: InstructionEntryTable.value,
removed: InstructionEntryTable.removed,
})
.from(InstructionEntryTable)
.where(eq(InstructionEntryTable.session_id, sessionID))
.orderBy(asc(InstructionEntryTable.key))
.all()
.pipe(Effect.orDie)
})
export const initialize = Effect.fn("InstructionEntry.initialize")(function* (
db: DatabaseService,
sessionID: SessionSchema.ID,
entries: Snapshot,
created: number,
) {
const batches = Array.from({ length: Math.ceil(entries.length / InsertBatchSize) }, (_, index) =>
entries.slice(index * InsertBatchSize, (index + 1) * InsertBatchSize),
)
yield* Effect.forEach(
batches,
(batch) =>
db
.insert(InstructionEntryTable)
.values(
batch.map((entry) => ({
...entry,
session_id: sessionID,
time_created: created,
time_updated: created,
})),
)
.run()
.pipe(Effect.orDie),
{ discard: true },
)
})
export interface Interface {
readonly list: (sessionID: SessionSchema.ID) => Effect.Effect<ReadonlyArray<Info>>
readonly put: (input: {
@@ -70,6 +70,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
Match.type<SessionEvent.DurableEvent>(),
Match.discriminatorsExhaustive("type")({
"session.created": () => Effect.void,
"session.viewed": () => Effect.void,
"session.usage.recorded": () => Effect.void,
"session.agent.selected": (event) =>
Effect.gen(function* () {
+54 -4
View File
@@ -15,6 +15,7 @@ import { SessionInbox } from "./inbox.js"
import { Workspace } from "../workspace.js"
import { InstructionState } from "./instruction-state.js"
import { SessionInboxTable, SessionMessageTable, SessionTable } from "./sql.js"
import { InstructionEntry } from "./instruction-entry.js"
import { Slug } from "../util/slug.js"
import { FSUtil } from "@opencode-ai/util/fs-util"
import { Money } from "@opencode-ai/schema/money"
@@ -171,6 +172,9 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
.pipe(Effect.orDie)
if (!stored) return yield* Effect.die(new SessionAlreadyProjected())
if (event.data.instructionEntries)
yield* InstructionEntry.initialize(db, event.data.sessionID, event.data.instructionEntries, event.created)
let cursor = -1
while (copiedSeq !== undefined) {
const rows = yield* db
@@ -183,6 +187,7 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
lt(SessionMessageTable.seq, copiedSeq + 1),
// Terminal events for active projections stay on the parent, so forks copy only settled history.
sql`${SessionMessageTable.type} != 'assistant' or json_extract(${SessionMessageTable.data}, '$.time.completed') is not null`,
sql`${SessionMessageTable.type} != 'shell' or json_extract(${SessionMessageTable.data}, '$.status') != 'running'`,
sql`${SessionMessageTable.type} != 'compaction' or json_extract(${SessionMessageTable.data}, '$.status') != 'running'`,
),
)
@@ -196,7 +201,7 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
.insert(SessionMessageTable)
.values(
rows.map((row) => ({
id: SessionMessage.ID.create(),
id: SessionMessage.ID.make(`${SessionMessage.ID.fromEvent(event.id)}_${row.seq}`),
session_id: event.data.sessionID,
type: row.type,
seq: row.seq,
@@ -390,6 +395,37 @@ function insertMessage(db: DatabaseService, event: SessionEvent.DurableEvent, me
.pipe(Effect.orDie)
}
function projectIdle(
db: DatabaseService,
event:
| typeof SessionEvent.Execution.Succeeded.Type
| typeof SessionEvent.Execution.Failed.Type
| typeof SessionEvent.Execution.Interrupted.Type,
) {
return Effect.gen(function* () {
yield* run(db, event)
if (event.type === SessionEvent.Execution.Interrupted.type && event.data.reason === "shutdown") return
const time = event.created
const outcome =
event.type === SessionEvent.Execution.Succeeded.type
? "succeeded"
: event.type === SessionEvent.Execution.Failed.type
? "failed"
: "interrupted"
yield* db
.update(SessionTable)
.set({
// Unread uses a strict timestamp comparison, so every terminal must advance even within one millisecond.
time_idle: sql`max(${time}, coalesce(${SessionTable.time_idle} + 1, ${time}))`,
idle_outcome: outcome,
time_updated: sql`${SessionTable.time_updated}`,
})
.where(eq(SessionTable.id, event.data.sessionID))
.run()
.pipe(Effect.orDie)
})
}
const layer = Layer.effectDiscard(
Effect.gen(function* () {
const bus = yield* Bus.Service
@@ -511,6 +547,20 @@ const layer = Layer.effectDiscard(
.run()
.pipe(Effect.orDie),
)
yield* bus.project(SessionEvent.Viewed, (event) => {
const idle = event.data.idle
return db
.update(SessionTable)
.set({
// Monotone watermark: a duplicate or stale view never regresses, and a terminal event
// committing after the viewer's observation keeps the newer idle transition unread.
time_viewed: sql`max(${idle}, coalesce(${SessionTable.time_viewed}, ${idle}))`,
time_updated: sql`${SessionTable.time_updated}`,
})
.where(eq(SessionTable.id, event.data.sessionID))
.run()
.pipe(Effect.orDie)
})
yield* bus.project(SessionEvent.UsageRecorded, (event) => applyUsage(db, event.data.sessionID, event.data))
yield* bus.project(SessionEvent.Forked, (event) => projectFork(db, event))
yield* bus.project(SessionEvent.InboxDelivered, (event) =>
@@ -575,9 +625,9 @@ const layer = Layer.effectDiscard(
delivery: event.data.delivery,
}),
)
yield* bus.project(SessionEvent.Execution.Succeeded, (event) => run(db, event))
yield* bus.project(SessionEvent.Execution.Failed, (event) => run(db, event))
yield* bus.project(SessionEvent.Execution.Interrupted, (event) => run(db, event))
yield* bus.project(SessionEvent.Execution.Succeeded, (event) => projectIdle(db, event))
yield* bus.project(SessionEvent.Execution.Failed, (event) => projectIdle(db, event))
yield* bus.project(SessionEvent.Execution.Interrupted, (event) => projectIdle(db, event))
yield* bus.project(SessionEvent.InstructionsUpdated, (event) =>
Effect.gen(function* () {
yield* run(db, event)
+3
View File
@@ -57,6 +57,9 @@ export const SessionTable = sqliteTable(
variant?: string
}>(),
...Timestamps,
time_idle: integer(),
time_viewed: integer(),
idle_outcome: text().$type<NonNullable<Session.Info["outcome"]>>(),
time_compacting: integer(),
time_archived: integer(),
/** The execution claim timestamp (historical column name; see SessionStore.claim). */
+17 -2
View File
@@ -53,7 +53,7 @@ const layer = Layer.effect(
export: Effect.fn("SessionTransfer.export")(function* (input) {
const data = {
info: yield* sessions.get(input.sessionID),
messages: yield* sessions.messages({ sessionID: input.sessionID, order: "asc" }),
messages: (yield* sessions.messages({ sessionID: input.sessionID, order: "asc" })).filter(isSettled),
}
return input.sanitize ? sanitize(data) : data
}),
@@ -68,7 +68,7 @@ const layer = Layer.effect(
if (recorded) return yield* new ImportConflictError({ sessionID })
const project = yield* projects.resolve(input.location.directory)
yield* upsertProject(db, project).pipe(Effect.orDie)
const messages = input.data.messages.map((message, index) => {
const messages = input.data.messages.filter(isSettled).map((message, index) => {
const encoded = encodeMessage(message)
const { id: _, type, ...data } = encoded
return {
@@ -115,6 +115,15 @@ const layer = Layer.effect(
tokens_cache_write: input.data.info.tokens.cache.write,
time_created: DateTime.toEpochMillis(input.data.info.time.created),
time_updated: DateTime.toEpochMillis(input.data.info.time.updated),
time_idle: input.data.info.time.idle ? DateTime.toEpochMillis(input.data.info.time.idle) : null,
time_viewed:
input.data.info.time.idle && input.data.info.time.viewed
? Math.min(
DateTime.toEpochMillis(input.data.info.time.idle),
DateTime.toEpochMillis(input.data.info.time.viewed),
)
: null,
idle_outcome: input.data.info.time.idle ? (input.data.info.outcome ?? null) : null,
time_archived: input.data.info.time.archived
? DateTime.toEpochMillis(input.data.info.time.archived)
: null,
@@ -144,6 +153,12 @@ export const node = makeGlobalNode({
deps: [App.node, Bus.node, Database.node, Project.node, Session.node],
})
function isSettled(message: SessionMessage.Info) {
if (message.type === "assistant") return message.time.completed !== undefined
if (message.type === "shell" || message.type === "compaction") return message.status !== "running"
return true
}
function redact(kind: string, id: string, value: string) {
return value.trim() ? `[redacted:${kind}:${id}]` : value
}
+1 -1
View File
@@ -198,7 +198,7 @@ const scanLegacy = Effect.fnUntraced(function* (command: string, shell: string,
})
async function scanPortable(command: string, shell: string, cwd: string) {
const { ShellScan } = await import("@opencode-ai/shell-scan")
const { ShellScan } = await import("./scan.js")
const powershell = ShellSelect.ps(shell)
const result = powershell ? ShellScan.scanPowerShell(command) : ShellScan.scan(command)
if (result.kind === "opaque") return { commands: [{ resource: command, save: command }], directories: [] }
@@ -1,4 +1,4 @@
export * as ShellScan from "./index.js"
export * as ShellScan from "./scan.js"
export type OpaqueReason =
| "command-substitution"
+1 -1
View File
@@ -201,7 +201,7 @@ export const noopLayer = Layer.succeed(
Service.of({
transform: () => Effect.succeed({ dispose: Effect.void }),
reload: () => Effect.void,
capture: () => Effect.succeed(undefined),
capture: () => Effect.undefined,
files: () => Effect.succeed([]),
diff: () => Effect.succeed([]),
restore: () => Effect.void,
+1 -1
View File
@@ -78,7 +78,7 @@ export const Plugin = {
source,
})
const current = yield* FileMutation.readText(environment.files, target.absolute).pipe(
Effect.catchTag("Environment.NotFound", () => Effect.succeed(undefined)),
Effect.catchTag("Environment.NotFound", () => Effect.undefined),
)
const next = Bom.split(input.content)
const preview = fileDiff(target.resource, current?.text ?? "", next.text, current ? "modified" : "added")
-6
View File
@@ -1,6 +0,0 @@
import path from "path"
import { which } from "./which.js"
const resolved = process.platform === "win32" ? which("git") : undefined
export const gitExecutable = resolved ? path.resolve(resolved) : "git"
+1 -2
View File
@@ -8,7 +8,6 @@ import { AppProcess } from "@opencode-ai/util/process"
import type { DiffOptions, Interface } from "../vcs.js"
import { chunksByFile, emptyPatch, MAX_PATCH_BYTES, MAX_TOTAL_PATCH_BYTES, PATCH_CONTEXT_LINES } from "./patch.js"
import type { Patch } from "./patch.js"
import { gitExecutable } from "../util/git-executable.js"
/**
* Git adapter for the Vcs service. Ported from the V1 pipeline: patches are
@@ -129,7 +128,7 @@ function makeGit(proc: AppProcess.Interface) {
const run = Effect.fnUntraced(
function* (args: string[], opts: { cwd: string; maxOutputBytes?: number }) {
const result = yield* proc.run(
ChildProcess.make(gitExecutable, [...cfg, ...args], {
ChildProcess.make("git", [...cfg, ...args], {
cwd: opts.cwd,
extendEnv: true,
stdin: "ignore",
+1 -1
View File
@@ -31,7 +31,7 @@ export const make = Effect.gen(function* () {
return yield* Effect.forEach(entries, (entry) =>
canonical(fs, entry.directory).pipe(
Effect.map((directory) => ({ directory, type: entry.kind === "main" ? "root" : "worktree" }) as const),
Effect.catchTag("Worktree.DirectoryUnavailableError", () => Effect.succeed(undefined)),
Effect.catchTag("Worktree.DirectoryUnavailableError", () => Effect.undefined),
),
).pipe(Effect.map((items) => items.filter((item): item is ListEntry => item !== undefined)))
}),

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