wip: migrate session-timeline component

This commit is contained in:
Adam 2025-11-19 22:50:50 -06:00
parent bfab6a2e1a
commit d5eb2b0d68
No known key found for this signature in database
GPG key ID: 9CB48779AF150E75
29 changed files with 1094 additions and 885 deletions

View file

@ -410,8 +410,8 @@
"@openauthjs/openauth": "0.0.0-20250322224806",
"@pierre/precision-diffs": "0.4.4",
"@solidjs/meta": "0.29.4",
"@solidjs/router": "0.15.3",
"@solidjs/start": "1.1.0",
"@solidjs/router": "0.15.4",
"@solidjs/start": "1.2.0",
"@tailwindcss/vite": "4.1.11",
"@tsconfig/bun": "1.0.9",
"@tsconfig/node22": "22.0.2",
@ -425,14 +425,14 @@
"hono-openapi": "1.1.1",
"luxon": "3.6.1",
"remeda": "2.26.0",
"solid-js": "1.9.9",
"solid-js": "1.9.10",
"solid-list": "0.3.0",
"tailwindcss": "4.1.11",
"typescript": "5.8.2",
"ulid": "3.0.1",
"virtua": "0.42.3",
"vite": "7.1.4",
"vite-plugin-solid": "2.11.8",
"vite-plugin-solid": "2.11.10",
"zod": "4.1.8",
},
"packages": {
@ -1418,9 +1418,9 @@
"@solidjs/meta": ["@solidjs/meta@0.29.4", "", { "peerDependencies": { "solid-js": ">=1.8.4" } }, "sha512-zdIWBGpR9zGx1p1bzIPqF5Gs+Ks/BH8R6fWhmUa/dcK1L2rUC8BAcZJzNRYBQv74kScf1TSOs0EY//Vd/I0V8g=="],
"@solidjs/router": ["@solidjs/router@0.15.3", "", { "peerDependencies": { "solid-js": "^1.8.6" } }, "sha512-iEbW8UKok2Oio7o6Y4VTzLj+KFCmQPGEpm1fS3xixwFBdclFVBvaQVeibl1jys4cujfAK5Kn6+uG2uBm3lxOMw=="],
"@solidjs/router": ["@solidjs/router@0.15.4", "", { "peerDependencies": { "solid-js": "^1.8.6" } }, "sha512-WOpgg9a9T638cR+5FGbFi/IV4l2FpmBs1GpIMSPa0Ce9vyJN7Wts+X2PqMf9IYn0zUj2MlSJtm1gp7/HI/n5TQ=="],
"@solidjs/start": ["@solidjs/start@1.1.0", "", { "dependencies": { "@tanstack/server-functions-plugin": "^1.99.5", "@vinxi/plugin-directives": "^0.5.0", "@vinxi/server-components": "^0.5.0", "defu": "^6.1.2", "error-stack-parser": "^2.1.4", "html-to-image": "^1.11.11", "radix3": "^1.1.0", "seroval": "^1.0.2", "seroval-plugins": "^1.0.2", "shiki": "^1.26.1", "source-map-js": "^1.0.2", "terracotta": "^1.0.4", "tinyglobby": "^0.2.2", "vite-plugin-solid": "^2.11.1" }, "peerDependencies": { "vinxi": "^0.5.3" } }, "sha512-7MNhNVt8uF7tdvLkvJhj4357vg3Ha+yqJP8XhQ6IbSZbsyk/xMkYmfc1h6w4GWiWZ5tn1DvS1uqGXjLFbKRy6g=="],
"@solidjs/start": ["@solidjs/start@1.2.0", "", { "dependencies": { "@tanstack/server-functions-plugin": "1.121.21", "@vinxi/plugin-directives": "^0.5.0", "@vinxi/server-components": "^0.5.0", "cookie-es": "^2.0.0", "defu": "^6.1.2", "error-stack-parser": "^2.1.4", "html-to-image": "^1.11.11", "radix3": "^1.1.0", "seroval": "^1.0.2", "seroval-plugins": "^1.0.2", "shiki": "^1.26.1", "source-map-js": "^1.0.2", "terracotta": "^1.0.4", "tinyglobby": "^0.2.2", "vite-plugin-solid": "^2.11.1" }, "peerDependencies": { "vinxi": "^0.5.7" } }, "sha512-SRv1g3R+4sxZnxCBPK1IedtLKsPhPJ7W/Yv4xEHjM4jJGPWi3ed35/yd0D5zhRK0C7zJIkZKbhnR/S3g8JUD5w=="],
"@speed-highlight/core": ["@speed-highlight/core@1.2.12", "", {}, "sha512-uilwrK0Ygyri5dToHYdZSjcvpS2ZwX0w5aSt3GCEN9hrjxWCoeV4Z2DTXuxjwbntaLQIEEAlCeNQss5SoHvAEA=="],
@ -1462,11 +1462,11 @@
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.11", "", { "dependencies": { "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "tailwindcss": "4.1.11" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw=="],
"@tanstack/directive-functions-plugin": ["@tanstack/directive-functions-plugin@1.139.0", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.27.7", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@tanstack/router-utils": "1.139.0", "babel-dead-code-elimination": "^1.0.10", "pathe": "^2.0.3", "tiny-invariant": "^1.3.3" }, "peerDependencies": { "vite": ">=6.0.0 || >=7.0.0" } }, "sha512-qLGxldnWa0pp/siZEFEYDU+eB/j40bd1V3IuTzP0sFnrYi11Ldx1yVkOruDKUbO1WM0o+OlPhp22Q1h+LMdDMA=="],
"@tanstack/directive-functions-plugin": ["@tanstack/directive-functions-plugin@1.121.21", "", { "dependencies": { "@babel/code-frame": "7.26.2", "@babel/core": "^7.26.8", "@babel/traverse": "^7.26.8", "@babel/types": "^7.26.8", "@tanstack/router-utils": "^1.121.21", "babel-dead-code-elimination": "^1.0.10", "tiny-invariant": "^1.3.3" }, "peerDependencies": { "vite": ">=6.0.0" } }, "sha512-B9z/HbF7gJBaRHieyX7f2uQ4LpLLAVAEutBZipH6w+CYD6RHRJvSVPzECGHF7icFhNWTiJQL2QR6K07s59yzEw=="],
"@tanstack/router-utils": ["@tanstack/router-utils@1.139.0", "", { "dependencies": { "@babel/core": "^7.27.4", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.5", "@babel/preset-typescript": "^7.27.1", "ansis": "^4.1.0", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-jT7D6NimWqoFSkid4vCno8gvTyfL1+NHpgm3es0B2UNhKKRV3LngOGilm1m6v8Qvk/gy6Fh/tvB+s+hBl6GhOg=="],
"@tanstack/server-functions-plugin": ["@tanstack/server-functions-plugin@1.139.0", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.27.7", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@tanstack/directive-functions-plugin": "1.139.0", "babel-dead-code-elimination": "^1.0.9", "tiny-invariant": "^1.3.3" } }, "sha512-IpNFiCoy2YU6gY/4lCKIVlFyU67ltlcUMGcdnrevqOgq20AbMyeLbbBVo9tAA3TkHK9F+9Hd7DqGXsup2pmBLg=="],
"@tanstack/server-functions-plugin": ["@tanstack/server-functions-plugin@1.121.21", "", { "dependencies": { "@babel/code-frame": "7.26.2", "@babel/core": "^7.26.8", "@babel/plugin-syntax-jsx": "^7.25.9", "@babel/plugin-syntax-typescript": "^7.25.9", "@babel/template": "^7.26.8", "@babel/traverse": "^7.26.8", "@babel/types": "^7.26.8", "@tanstack/directive-functions-plugin": "1.121.21", "babel-dead-code-elimination": "^1.0.9", "tiny-invariant": "^1.3.3" } }, "sha512-a05fzK+jBGacsSAc1vE8an7lpBh4H0PyIEcivtEyHLomgSeElAJxm9E2It/0nYRZ5Lh23m0okbhzJNaYWZpAOg=="],
"@thisbeyond/solid-dnd": ["@thisbeyond/solid-dnd@0.7.5", "", { "peerDependencies": { "solid-js": "^1.5" } }, "sha512-DfI5ff+yYGpK9M21LhYwIPlbP2msKxN2ARwuu6GF8tT1GgNVDTI8VCQvH4TJFoVApP9d44izmAcTh/iTCH2UUw=="],
@ -1552,7 +1552,7 @@
"@types/scheduler": ["@types/scheduler@0.26.0", "", {}, "sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA=="],
"@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="],
"@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="],
"@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="],
@ -1898,7 +1898,7 @@
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
"cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="],
"cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="],
"cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="],
@ -3274,7 +3274,7 @@
"smol-toml": ["smol-toml@1.5.2", "", {}, "sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ=="],
"solid-js": ["solid-js@1.9.9", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", "seroval-plugins": "~1.3.0" } }, "sha512-A0ZBPJQldAeGCTW0YRYJmt7RCeh5rbFfPZ2aOttgYnctHE7HgKeHCBB/PVc2P7eOfmNXqMFFFoYYdm3S4dcbkA=="],
"solid-js": ["solid-js@1.9.10", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", "seroval-plugins": "~1.3.0" } }, "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew=="],
"solid-list": ["solid-list@0.3.0", "", { "dependencies": { "@corvu/utils": "~0.4.0" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-t4hx/F/l8Vmq+ib9HtZYl7Z9F1eKxq3eKJTXlvcm7P7yI4Z8O7QSOOEVHb/K6DD7M0RxzVRobK/BS5aSfLRwKg=="],
@ -3590,7 +3590,7 @@
"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-solid": ["vite-plugin-solid@2.11.8", "", { "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.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-hFrCxBfv3B1BmFqnJF4JOCYpjrmi/zwyeKjcomQ0khh8HFyQ8SbuBWQ7zGojfrz6HUOBFrJBNySDi/JgAHytWg=="],
"vite-plugin-solid": ["vite-plugin-solid@2.11.10", "", { "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.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-Yr1dQybmtDtDAHkii6hXuc1oVH9CPcS/Zb2jN/P36qqcrkNnVPsMTzQ06jyzFPFjj3U1IYKMVt/9ZqcwGCEbjw=="],
"vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
@ -3924,11 +3924,11 @@
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tanstack/directive-functions-plugin/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"@tanstack/directive-functions-plugin/@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="],
"@tanstack/router-utils/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"@types/serve-static/@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="],
"@tanstack/server-functions-plugin/@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="],
"@vercel/nft/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
@ -4048,6 +4048,8 @@
"gray-matter/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="],
"h3/cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="],
"handlebars/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"hast-util-to-parse5/property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="],
@ -4092,8 +4094,6 @@
"nitropack/c12": ["c12@3.3.2", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-QkikB2X5voO1okL3QsES0N690Sn/K9WokXqUsDQsWy5SnYb+psYQFGA10iy1bZHj3fjISKsI67Q90gruvWWM3A=="],
"nitropack/cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="],
"nitropack/globby": ["globby@15.0.0", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "fast-glob": "^3.3.3", "ignore": "^7.0.5", "path-type": "^6.0.0", "slash": "^5.1.0", "unicorn-magic": "^0.3.0" } }, "sha512-oB4vkQGqlMl682wL1IlWd02tXCbquGWM4voPEI85QmNKCaw8zGTm1f1rubFgkg3Eli2PtKlFgrnmUqasbQWlkw=="],
"nitropack/h3": ["h3@1.15.4", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.5", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.2", "radix3": "^1.1.2", "ufo": "^1.6.1", "uncrypto": "^0.1.3" } }, "sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ=="],
@ -4256,8 +4256,6 @@
"yargs/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
"youch/cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="],
"zod-to-json-schema/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"zod-to-ts/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
@ -4664,6 +4662,8 @@
"listhen/@parcel/watcher-wasm/napi-wasm": ["napi-wasm@1.1.3", "", { "bundled": true }, "sha512-h/4nMGsHjZDCYmQVNODIrYACVJ+I9KItbG+0si6W/jSjdA9JbWDoU4LLeMXVcEQGHjttI2tuXqDrbGF7qkUHHg=="],
"listhen/h3/cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="],
"miniflare/sharp/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
@ -4722,6 +4722,8 @@
"type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"unstorage/h3/cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="],
"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=="],

View file

@ -29,9 +29,6 @@
"@cloudflare/workers-types": "4.20251008.0",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@pierre/precision-diffs": "0.4.4",
"@solidjs/meta": "0.29.4",
"@solidjs/router": "0.15.3",
"@solidjs/start": "1.1.0",
"@tailwindcss/vite": "4.1.11",
"diff": "8.0.2",
"ai": "5.0.97",
@ -43,12 +40,15 @@
"@typescript/native-preview": "7.0.0-dev.20251014.1",
"zod": "4.1.8",
"remeda": "2.26.0",
"solid-js": "1.9.9",
"solid-list": "0.3.0",
"tailwindcss": "4.1.11",
"virtua": "0.42.3",
"vite": "7.1.4",
"vite-plugin-solid": "2.11.8"
"@solidjs/meta": "0.29.4",
"@solidjs/router": "0.15.4",
"@solidjs/start": "1.2.0",
"solid-js": "1.9.10",
"vite-plugin-solid": "2.11.10"
}
},
"devDependencies": {

View file

@ -1,6 +1,7 @@
import { useLocal, type LocalFile } from "@/context/local"
import { FileIcon, Tooltip } from "@opencode-ai/ui"
import { Collapsible } from "@/ui"
import { FileIcon } from "@opencode-ai/ui/file-icon"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { For, Match, Switch, Show, type ComponentProps, type ParentProps } from "solid-js"
import { Dynamic } from "solid-js/web"

View file

@ -1,4 +1,3 @@
import { Button, FileIcon, Icon, IconButton, Select, SelectDialog, Tooltip } from "@opencode-ai/ui"
import { useFilteredList } from "@opencode-ai/ui/hooks"
import { createEffect, on, Component, Show, For, onMount, onCleanup, Switch, Match } from "solid-js"
import { createStore } from "solid-js/store"
@ -10,6 +9,13 @@ import { ContentPart, DEFAULT_PROMPT, isPromptEqual, Prompt, useSession } from "
import { useSDK } from "@/context/sdk"
import { useNavigate } from "@solidjs/router"
import { useSync } from "@/context/sync"
import { FileIcon } from "@opencode-ai/ui/file-icon"
import { SelectDialog } from "@opencode-ai/ui/select-dialog"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Select } from "@opencode-ai/ui/select"
interface PromptInputProps {
class?: string
@ -183,8 +189,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const range = selection.getRangeAt(0)
if (atMatch) {
let node: Node | null = range.startContainer
let offset = range.startOffset
// let node: Node | null = range.startContainer
// let offset = range.startOffset
let runningLength = 0
const walker = document.createTreeWalker(editorRef, NodeFilter.SHOW_TEXT, null)

View file

@ -3,7 +3,8 @@ import "@/index.css"
import { render } from "solid-js/web"
import { Router, Route, Navigate } from "@solidjs/router"
import { MetaProvider } from "@solidjs/meta"
import { Fonts, MarkedProvider } from "@opencode-ai/ui"
import { Fonts } from "@opencode-ai/ui/fonts"
import { MarkedProvider } from "@opencode-ai/ui/context/marked"
import { GlobalSyncProvider, useGlobalSync } from "./context/global-sync"
import Layout from "@/pages/layout"
import DirectoryLayout from "@/pages/directory-layout"

View file

@ -2,7 +2,7 @@ import { useGlobalSync } from "@/context/global-sync"
import { base64Encode, getFilename } from "@/utils"
import { For } from "solid-js"
import { A } from "@solidjs/router"
import { Button } from "@opencode-ai/ui"
import { Button } from "@opencode-ai/ui/button"
export default function Home() {
const sync = useGlobalSync()

View file

@ -1,10 +1,16 @@
import { Button, Tooltip, DiffChanges, IconButton, Mark, Icon, Collapsible } from "@opencode-ai/ui"
import { createMemo, For, ParentProps, Show } from "solid-js"
import { DateTime } from "luxon"
import { A, useParams } from "@solidjs/router"
import { useLayout } from "@/context/layout"
import { useGlobalSync } from "@/context/global-sync"
import { base64Encode, getFilename } from "@/utils"
import { Mark } from "@opencode-ai/ui/logo"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { Collapsible } from "@opencode-ai/ui/collapsible"
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
export default function Layout(props: ParentProps) {
const params = useParams()

View file

@ -1,39 +1,20 @@
import {
SelectDialog,
IconButton,
Tabs,
Icon,
Accordion,
Diff,
Collapsible,
DiffChanges,
Message,
Typewriter,
Card,
Code,
Tooltip,
ProgressCircle,
FileIcon,
SessionReview,
MessageProgress,
} from "@opencode-ai/ui"
import {
For,
onCleanup,
onMount,
Show,
Match,
Switch,
createSignal,
createEffect,
createMemo,
createResource,
} from "solid-js"
import { For, onCleanup, onMount, Show, Match, Switch, createResource } from "solid-js"
import { useLocal, type LocalFile } from "@/context/local"
import { createStore } from "solid-js/store"
import { getDirectory, getFilename } from "@/utils"
import { PromptInput } from "@/components/prompt-input"
import { DateTime } from "luxon"
import { FileIcon } from "@opencode-ai/ui/file-icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Icon } from "@opencode-ai/ui/icon"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
import { Tabs } from "@opencode-ai/ui/tabs"
import { Code } from "@opencode-ai/ui/code"
import { SessionTimeline } from "@opencode-ai/ui/session-timeline"
import { SessionReview } from "@opencode-ai/ui/session-review"
import { SelectDialog } from "@opencode-ai/ui/select-dialog"
import {
DragDropProvider,
DragDropSensors,
@ -46,11 +27,8 @@ import {
import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd"
import type { JSX } from "solid-js"
import { useSync } from "@/context/sync"
import { type AssistantMessage as AssistantMessageType } from "@opencode-ai/sdk"
import { Markdown, Spinner, StickyAccordionHeader } from "@opencode-ai/ui"
import { useSession } from "@/context/session"
import { useLayout } from "@/context/layout"
import { createSessionSeen } from "@/hooks/create-session-seen"
export default function Page() {
const layout = useLayout()
@ -63,7 +41,6 @@ export default function Page() {
activeDraggable: undefined as string | undefined,
})
let inputRef!: HTMLDivElement
let messageScrollElement!: HTMLDivElement
const MOD = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform) ? "Meta" : "Control"
@ -356,284 +333,10 @@ export default function Page() {
<div class="relative shrink-0 px-6 py-3 flex flex-col gap-6 flex-1 min-h-0 w-full max-w-xl mx-auto">
<Switch>
<Match when={session.id}>
<div
classList={{
"flex-1 min-h-0 pb-20": true,
"flex items-start justify-start": layout.review.state() === "pane",
}}
>
<Show when={session.messages.user().length > 1}>
{(_) => {
const expanded = createMemo(() => layout.review.state() === "tab" || !session.diffs().length)
return (
<ul
role="list"
classList={{
"mr-8 shrink-0 flex flex-col items-start": true,
"absolute right-full w-60 mt-3 @7xl:gap-2 @7xl:mt-1": expanded(),
"mt-3": !expanded(),
}}
>
<For each={session.messages.user()}>
{(message) => {
const working = createMemo(
() => message.id === session.messages.last()?.id && session.working(),
)
const handleClick = () => session.messages.setActive(message.id)
return (
<li
classList={{
"group/li flex items-center self-stretch justify-end": true,
"@7xl:justify-start": expanded(),
}}
>
<Tooltip
placement="right"
gutter={8}
value={
<div class="flex items-center gap-2">
<DiffChanges changes={message.summary?.diffs ?? []} variant="bars" />
{message.summary?.title}
</div>
}
>
<button
data-active={session.messages.active()?.id === message.id}
onClick={handleClick}
classList={{
"group/tick flex items-center justify-start h-2 w-8 -mr-3": true,
"data-[active=true]:[&>div]:bg-icon-strong-base data-[active=true]:[&>div]:w-full": true,
"@7xl:hidden": expanded(),
}}
>
<div class="h-px w-5 bg-icon-base group-hover/tick:w-full group-hover/tick:bg-icon-strong-base" />
</button>
</Tooltip>
<button
classList={{
"hidden items-center self-stretch w-full gap-x-2 cursor-default": true,
"@7xl:flex": expanded(),
}}
onClick={handleClick}
>
<Switch>
<Match when={working()}>
<Spinner class="text-text-base shrink-0 w-[18px] aspect-square" />
</Match>
<Match when={true}>
<DiffChanges changes={message.summary?.diffs ?? []} variant="bars" />
</Match>
</Switch>
<div
data-active={session.messages.active()?.id === message.id}
classList={{
"text-14-regular text-text-weak whitespace-nowrap truncate min-w-0": true,
"text-text-weak data-[active=true]:text-text-strong group-hover/li:text-text-base": true,
}}
>
<Show when={message.summary?.title} fallback="New message">
{message.summary?.title}
</Show>
</div>
</button>
</li>
)
}}
</For>
</ul>
)
}}
</Show>
<div ref={messageScrollElement} class="grow size-full min-w-0 overflow-y-auto no-scrollbar">
<For each={session.messages.user()}>
{(message) => {
const isActive = createMemo(() => session.messages.active()?.id === message.id)
const titleSeen = createSessionSeen(`message-title-${message.id}`)
const contentSeen = createSessionSeen(`message-content-${message.id}`)
const [titled, setTitled] = createSignal(titleSeen())
const assistantMessages = createMemo(() => {
if (!session.id) return []
return sync.data.message[session.id]?.filter(
(m) => m.role === "assistant" && m.parentID == message.id,
) as AssistantMessageType[]
})
const error = createMemo(() => assistantMessages().find((m) => m?.error)?.error)
const [detailsExpanded, setDetailsExpanded] = createSignal(false)
const parts = createMemo(() => sync.data.part[message.id])
const hasToolPart = createMemo(() =>
assistantMessages()
?.flatMap((m) => sync.data.part[m.id])
.some((p) => p?.type === "tool"),
)
const working = createMemo(
() => message.id === session.messages.last()?.id && session.working(),
)
const initialCompleted = !(message.id === session.messages.last()?.id && session.working())
const [completed, setCompleted] = createSignal(initialCompleted)
// allowing time for the animations to finish
createEffect(() => {
if (titleSeen()) return
const title = message.summary?.title
if (title) setTimeout(() => setTitled(true), 10_000)
})
createEffect(() => {
const completed = !working()
setTimeout(() => setCompleted(completed), 1200)
})
return (
<Show when={isActive()}>
<div
data-message={message.id}
class="flex flex-col items-start self-stretch gap-8 pb-20"
>
{/* Title */}
<div class="flex items-center gap-2 self-stretch sticky top-0 bg-background-stronger z-20 h-8">
<div class="w-full text-14-medium text-text-strong">
<Show
when={titled()}
fallback={
<Typewriter
as="h1"
text={message.summary?.title}
class="overflow-hidden text-ellipsis min-w-0 text-nowrap"
/>
}
>
<h1 class="overflow-hidden text-ellipsis min-w-0 text-nowrap">
{message.summary?.title}
</h1>
</Show>
</div>
</div>
<Message message={message} parts={parts()} />
{/* Summary */}
<Show when={completed()}>
<div class="w-full flex flex-col gap-6 items-start self-stretch">
<div class="flex flex-col items-start gap-1 self-stretch">
<h2 class="text-12-medium text-text-weak">
<Switch>
<Match when={message.summary?.diffs?.length}>Summary</Match>
<Match when={true}>Response</Match>
</Switch>
</h2>
<Show when={message.summary?.body}>
{(summary) => (
<Markdown
classList={{
"text-14-regular": !!message.summary?.diffs?.length,
"[&>*]:fade-up-text": !message.summary?.diffs?.length && !contentSeen(),
}}
text={summary()}
/>
)}
</Show>
</div>
<Accordion class="w-full" multiple>
<For each={message.summary?.diffs ?? []}>
{(diff) => (
<Accordion.Item value={diff.file}>
<StickyAccordionHeader class="top-10 data-expanded:before:-top-10">
<Accordion.Trigger>
<div class="flex items-center justify-between w-full gap-5">
<div class="grow flex items-center gap-5 min-w-0">
<FileIcon
node={{ path: diff.file, type: "file" }}
class="shrink-0 size-4"
/>
<div class="flex grow min-w-0">
<Show when={diff.file.includes("/")}>
<span class="text-text-base truncate-start">
{getDirectory(diff.file)}&lrm;
</span>
</Show>
<span class="text-text-strong shrink-0">
{getFilename(diff.file)}
</span>
</div>
</div>
<div class="shrink-0 flex gap-4 items-center justify-end">
<DiffChanges changes={diff} />
<Icon name="chevron-grabber-vertical" size="small" />
</div>
</div>
</Accordion.Trigger>
</StickyAccordionHeader>
<Accordion.Content class="max-h-60 overflow-y-auto no-scrollbar">
<Diff
before={{
name: diff.file!,
contents: diff.before!,
}}
after={{
name: diff.file!,
contents: diff.after!,
}}
/>
</Accordion.Content>
</Accordion.Item>
)}
</For>
</Accordion>
</div>
</Show>
<Show when={error() && !detailsExpanded()}>
<Card variant="error" class="text-text-on-critical-base">
{error()?.data?.message as string}
</Card>
</Show>
{/* Response */}
<div class="w-full">
<Switch>
<Match when={!completed()}>
<MessageProgress assistantMessages={assistantMessages} done={!working()} />
</Match>
<Match when={completed() && hasToolPart()}>
<Collapsible
variant="ghost"
open={detailsExpanded()}
onOpenChange={setDetailsExpanded}
>
<Collapsible.Trigger class="text-text-weak hover:text-text-strong">
<div class="flex items-center gap-1 self-stretch">
<div class="text-12-medium">
<Switch>
<Match when={detailsExpanded()}>Hide details</Match>
<Match when={!detailsExpanded()}>Show details</Match>
</Switch>
</div>
<Collapsible.Arrow />
</div>
</Collapsible.Trigger>
<Collapsible.Content>
<div class="w-full flex flex-col items-start self-stretch gap-3">
<For each={assistantMessages()}>
{(assistantMessage) => {
const parts = createMemo(() => sync.data.part[assistantMessage.id])
return <Message message={assistantMessage} parts={parts()} />
}}
</For>
<Show when={error()}>
<Card variant="error" class="text-text-on-critical-base">
{error()?.data?.message as string}
</Card>
</Show>
</div>
</Collapsible.Content>
</Collapsible>
</Match>
</Switch>
</div>
</div>
</Show>
)
}}
</For>
</div>
</div>
<SessionTimeline
sessionID={session.id}
expanded={layout.review.state() === "tab" || !session.diffs().length}
/>
</Match>
<Match when={true}>
<div class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch">

View file

@ -1,7 +1,7 @@
import { Collapsible as KobalteCollapsible } from "@kobalte/core/collapsible"
import { Icon, IconProps } from "@opencode-ai/ui/icon"
import { splitProps } from "solid-js"
import type { ComponentProps, ParentProps } from "solid-js"
import { Icon, type IconProps } from "@opencode-ai/ui"
export interface CollapsibleProps extends ComponentProps<typeof KobalteCollapsible> {}
export interface CollapsibleTriggerProps extends ComponentProps<typeof KobalteCollapsible.Trigger> {}

View file

@ -12,6 +12,7 @@ export default defineConfig({
plugins: [tailwindcss(), solidPlugin()],
server: {
host: "0.0.0.0",
allowedHosts: true,
port: 3000,
},
build: {

View file

@ -4,5 +4,9 @@ import tailwindcss from "@tailwindcss/vite"
export default defineConfig({
vite: {
plugins: [tailwindcss() as any],
server: {
host: "0.0.0.0",
allowedHosts: true,
},
},
})

View file

@ -3,6 +3,7 @@ import { FileRoutes } from "@solidjs/start/router"
import { Suspense } from "solid-js"
import { Fonts } from "@opencode-ai/ui/fonts"
import { MetaProvider } from "@solidjs/meta"
import { MarkedProvider } from "@opencode-ai/ui/context/marked"
import "./app.css"
export default function App() {
@ -11,10 +12,12 @@ export default function App() {
root={(props) => (
<>
<Suspense>
<MetaProvider>
<Fonts />
{props.children}
</MetaProvider>
<MarkedProvider>
<MetaProvider>
<Fonts />
{props.children}
</MetaProvider>
</MarkedProvider>
</Suspense>
</>
)}

View file

@ -1,4 +1,4 @@
import { FileDiff, Message, Part, Session } from "@opencode-ai/sdk"
import { FileDiff, Message, Part, Session, SessionStatus } from "@opencode-ai/sdk"
import { fn } from "@opencode-ai/util/fn"
import { iife } from "@opencode-ai/util/iife"
import z from "zod"
@ -28,6 +28,10 @@ export namespace Share {
type: z.literal("session_diff"),
data: z.custom<FileDiff[]>(),
}),
z.object({
type: z.literal("session_status"),
data: z.custom<SessionStatus>(),
}),
])
export type Data = z.infer<typeof Data>
@ -92,6 +96,9 @@ export namespace Share {
case "session_diff":
await Storage.write(["share_data", input.share.id, "session_diff"], item.data)
break
case "session_status":
await Storage.write(["share_data", input.share.id, "session_status"], item.data)
break
}
}),
)

View file

@ -1,5 +1,9 @@
import { FileDiff, Message, Part, Session } from "@opencode-ai/sdk"
import { FileDiff, Message, Part, Session, SessionStatus } from "@opencode-ai/sdk"
import { SessionTimeline } from "@opencode-ai/ui/session-timeline"
import { SessionReview } from "@opencode-ai/ui/session-review"
import { DataProvider } from "@opencode-ai/ui/context"
import { createAsync, query, RouteDefinition, useParams } from "@solidjs/router"
import { Show } from "solid-js"
import { Share } from "~/core/share"
const getData = query(async (sessionID) => {
@ -9,6 +13,9 @@ const getData = query(async (sessionID) => {
session_diff: {
[sessionID: string]: FileDiff[]
}
session_status: {
[sessionID: string]: SessionStatus
}
message: {
[sessionID: string]: Message[]
}
@ -20,6 +27,11 @@ const getData = query(async (sessionID) => {
session_diff: {
[sessionID]: [],
},
session_status: {
[sessionID]: {
type: "idle",
},
},
message: {},
part: {},
}
@ -32,6 +44,9 @@ const getData = query(async (sessionID) => {
case "session_diff":
result.session_diff[sessionID] = item.data
break
case "session_status":
result.session_status[sessionID] = item.data
break
case "message":
result.message[item.data.sessionID] = result.message[item.data.sessionID] ?? []
result.message[item.data.sessionID].push(item.data)
@ -55,5 +70,26 @@ export default function () {
if (!params.sessionID) return
return getData(params.sessionID)
})
return <pre>{JSON.stringify(data(), null, 2)}</pre>
return (
<Show when={data()}>
{(data) => (
<DataProvider data={data()}>
<div class="relative bg-background-base size-full overflow-x-hidden flex flex-col">
<div class="@container select-text flex flex-col flex-1 min-h-0 overflow-y-hidden">
<div class="w-full flex-1 min-h-0 flex">
<div class="relative shrink-0 px-6 py-3 flex flex-col gap-6 flex-1 min-h-0 w-full max-w-xl mx-auto">
<SessionTimeline sessionID={params.sessionID!} expanded />
</div>
<Show when={data().session_diff[params.sessionID!]?.length}>
<div class="relative grow px-6 py-3 flex-1 min-h-0 border-l border-border-weak-base">
<SessionReview diffs={data().session_diff[params.sessionID!]} />
</div>
</Show>
</div>
</div>
</div>
</DataProvider>
)}
</Show>
)
}

View file

@ -3,11 +3,10 @@
"version": "1.0.85",
"type": "module",
"exports": {
".": "./src/components/index.ts",
"./*": "./src/components/*.tsx",
"./hooks": "./src/hooks/index.ts",
"./context": "./src/context/index.ts",
"./context/*": "./src/context/*",
"./context/*": "./src/context/*.tsx",
"./styles": "./src/styles/index.css",
"./styles/tailwind": "./src/styles/tailwind/index.css",
"./fonts/*": "./src/assets/fonts/*"

View file

@ -4,8 +4,6 @@ import {
type DiffLineAnnotation,
type HunkData,
FileDiffOptions,
registerCustomTheme,
ThemeRegistrationResolved,
} from "@pierre/precision-diffs"
import { ComponentProps, createEffect, splitProps } from "solid-js"
@ -173,373 +171,3 @@ export function Diff<T>(props: DiffProps<T>) {
/>
)
}
registerCustomTheme("OpenCode", () => {
return Promise.resolve({
name: "OpenCode",
colors: {
"editor.background": "transparent",
"editor.foreground": "var(--text-base)",
"gitDecoration.addedResourceForeground": "var(--syntax-diff-add)",
"gitDecoration.deletedResourceForeground": "var(--syntax-diff-delete)",
// "gitDecoration.conflictingResourceForeground": "#ffca00",
// "gitDecoration.modifiedResourceForeground": "#1a76d4",
// "gitDecoration.untrackedResourceForeground": "#00cab1",
// "gitDecoration.ignoredResourceForeground": "#84848A",
// "terminal.titleForeground": "#adadb1",
// "terminal.titleInactiveForeground": "#84848A",
// "terminal.background": "#141415",
// "terminal.foreground": "#adadb1",
// "terminal.ansiBlack": "#141415",
// "terminal.ansiRed": "#ff2e3f",
// "terminal.ansiGreen": "#0dbe4e",
// "terminal.ansiYellow": "#ffca00",
// "terminal.ansiBlue": "#008cff",
// "terminal.ansiMagenta": "#c635e4",
// "terminal.ansiCyan": "#08c0ef",
// "terminal.ansiWhite": "#c6c6c8",
// "terminal.ansiBrightBlack": "#141415",
// "terminal.ansiBrightRed": "#ff2e3f",
// "terminal.ansiBrightGreen": "#0dbe4e",
// "terminal.ansiBrightYellow": "#ffca00",
// "terminal.ansiBrightBlue": "#008cff",
// "terminal.ansiBrightMagenta": "#c635e4",
// "terminal.ansiBrightCyan": "#08c0ef",
// "terminal.ansiBrightWhite": "#c6c6c8",
},
tokenColors: [
{
scope: ["comment", "punctuation.definition.comment", "string.comment"],
settings: {
foreground: "var(--syntax-comment)",
},
},
{
scope: ["entity.other.attribute-name"],
settings: {
foreground: "var(--syntax-property)", // maybe attribute
},
},
{
scope: ["constant", "entity.name.constant", "variable.other.constant", "variable.language", "entity"],
settings: {
foreground: "var(--syntax-constant)",
},
},
{
scope: ["entity.name", "meta.export.default", "meta.definition.variable"],
settings: {
foreground: "var(--syntax-type)",
},
},
{
scope: ["meta.object.member"],
settings: {
foreground: "var(--syntax-primitive)",
},
},
{
scope: [
"variable.parameter.function",
"meta.jsx.children",
"meta.block",
"meta.tag.attributes",
"entity.name.constant",
"meta.embedded.expression",
"meta.template.expression",
"string.other.begin.yaml",
"string.other.end.yaml",
],
settings: {
foreground: "var(--syntax-punctuation)",
},
},
{
scope: ["entity.name.function", "support.type.primitive"],
settings: {
foreground: "var(--syntax-primitive)",
},
},
{
scope: ["support.class.component"],
settings: {
foreground: "var(--syntax-type)",
},
},
{
scope: "keyword",
settings: {
foreground: "var(--syntax-keyword)",
},
},
{
scope: [
"keyword.operator",
"storage.type.function.arrow",
"punctuation.separator.key-value.css",
"entity.name.tag.yaml",
"punctuation.separator.key-value.mapping.yaml",
],
settings: {
foreground: "var(--syntax-operator)",
},
},
{
scope: ["storage", "storage.type"],
settings: {
foreground: "var(--syntax-keyword)",
},
},
{
scope: ["storage.modifier.package", "storage.modifier.import", "storage.type.java"],
settings: {
foreground: "var(--syntax-primitive)",
},
},
{
scope: [
"string",
"punctuation.definition.string",
"string punctuation.section.embedded source",
"entity.name.tag",
],
settings: {
foreground: "var(--syntax-string)",
},
},
{
scope: "support",
settings: {
foreground: "var(--syntax-primitive)",
},
},
{
scope: ["support.type.object.module", "variable.other.object", "support.type.property-name.css"],
settings: {
foreground: "var(--syntax-object)",
},
},
{
scope: "meta.property-name",
settings: {
foreground: "var(--syntax-property)",
},
},
{
scope: "variable",
settings: {
foreground: "var(--syntax-variable)",
},
},
{
scope: "variable.other",
settings: {
foreground: "var(--syntax-variable)",
},
},
{
scope: [
"invalid.broken",
"invalid.illegal",
"invalid.unimplemented",
"invalid.deprecated",
"message.error",
"markup.deleted",
"meta.diff.header.from-file",
"punctuation.definition.deleted",
"brackethighlighter.unmatched",
"token.error-token",
],
settings: {
foreground: "var(--syntax-critical)",
},
},
{
scope: "carriage-return",
settings: {
foreground: "var(--syntax-keyword)",
},
},
{
scope: "string source",
settings: {
foreground: "var(--syntax-variable)",
},
},
{
scope: "string variable",
settings: {
foreground: "var(--syntax-constant)",
},
},
{
scope: [
"source.regexp",
"string.regexp",
"string.regexp.character-class",
"string.regexp constant.character.escape",
"string.regexp source.ruby.embedded",
"string.regexp string.regexp.arbitrary-repitition",
"string.regexp constant.character.escape",
],
settings: {
foreground: "var(--syntax-regexp)",
},
},
{
scope: "support.constant",
settings: {
foreground: "var(--syntax-primitive)",
},
},
{
scope: "support.variable",
settings: {
foreground: "var(--syntax-variable)",
},
},
{
scope: "meta.module-reference",
settings: {
foreground: "var(--syntax-info)",
},
},
{
scope: "punctuation.definition.list.begin.markdown",
settings: {
foreground: "var(--syntax-punctuation)",
},
},
{
scope: ["markup.heading", "markup.heading entity.name"],
settings: {
fontStyle: "bold",
foreground: "var(--syntax-info)",
},
},
{
scope: "markup.quote",
settings: {
foreground: "var(--syntax-info)",
},
},
{
scope: "markup.italic",
settings: {
fontStyle: "italic",
// foreground: "",
},
},
{
scope: "markup.bold",
settings: {
fontStyle: "bold",
foreground: "var(--text-strong)",
},
},
{
scope: [
"markup.raw",
"markup.inserted",
"meta.diff.header.to-file",
"punctuation.definition.inserted",
"markup.changed",
"punctuation.definition.changed",
"markup.ignored",
"markup.untracked",
],
settings: {
foreground: "var(--text-base)",
},
},
{
scope: "meta.diff.range",
settings: {
fontStyle: "bold",
foreground: "var(--syntax-unknown)",
},
},
{
scope: "meta.diff.header",
settings: {
foreground: "var(--syntax-unknown)",
},
},
{
scope: "meta.separator",
settings: {
fontStyle: "bold",
foreground: "var(--syntax-unknown)",
},
},
{
scope: "meta.output",
settings: {
foreground: "var(--syntax-unknown)",
},
},
{
scope: "meta.export.default",
settings: {
foreground: "var(--syntax-unknown)",
},
},
{
scope: [
"brackethighlighter.tag",
"brackethighlighter.curly",
"brackethighlighter.round",
"brackethighlighter.square",
"brackethighlighter.angle",
"brackethighlighter.quote",
],
settings: {
foreground: "var(--syntax-unknown)",
},
},
{
scope: ["constant.other.reference.link", "string.other.link"],
settings: {
fontStyle: "underline",
foreground: "var(--syntax-unknown)",
},
},
{
scope: "token.info-token",
settings: {
foreground: "var(--syntax-info)",
},
},
{
scope: "token.warn-token",
settings: {
foreground: "var(--syntax-warning)",
},
},
{
scope: "token.debug-token",
settings: {
foreground: "var(--syntax-info)",
},
},
],
semanticTokenColors: {
comment: "var(--syntax-comment)",
string: "var(--syntax-string)",
number: "var(--syntax-constant)",
regexp: "var(--syntax-regexp)",
keyword: "var(--syntax-keyword)",
variable: "var(--syntax-variable)",
parameter: "var(--syntax-variable)",
property: "var(--syntax-property)",
function: "var(--syntax-primitive)",
method: "var(--syntax-primitive)",
type: "var(--syntax-type)",
class: "var(--syntax-type)",
namespace: "var(--syntax-type)",
enumMember: "var(--syntax-primitive)",
"variable.constant": "var(--syntax-constant)",
"variable.defaultLibrary": "var(--syntax-unknown)",
},
} as unknown as ThemeRegistrationResolved)
})

View file

@ -1,33 +0,0 @@
export * from "./accordion"
export * from "./button"
export * from "./card"
export * from "./checkbox"
export * from "./code"
export * from "./collapsible"
export * from "./dialog"
export * from "./diff"
export * from "./diff-changes"
export * from "./icon"
export * from "./icon-button"
export * from "./input"
export * from "./favicon"
export * from "./fonts"
export * from "./list"
export * from "./logo"
export * from "./markdown"
export * from "./message-part"
export * from "./message-progress"
export * from "./progress-circle"
export * from "./select"
export * from "./select-dialog"
export * from "./spinner"
export * from "./sticky-accordion-header"
export * from "./tabs"
export * from "./basic-tool"
export * from "./file-icon"
export * from "./tooltip"
export * from "./typewriter"
export * from "./session-review"
export * from "../context/helper"
export * from "../context/marked"

View file

@ -1,30 +0,0 @@
[data-component="list"] {
/* overflow-y: auto; */
/* Hide scrollbar */
&::-webkit-scrollbar {
display: none;
}
-ms-overflow-style: none;
scrollbar-width: none;
[data-slot="item"] {
width: 100%;
margin-bottom: 6px;
padding: 4px 12px;
text-align: left;
border-radius: var(--radius-md);
transition: background-color 0.2s ease-in-out;
&[data-active="true"] {
background-color: var(--surface-raised-base-hover);
}
&:hover {
background-color: var(--surface-raised-base-hover);
}
&:focus {
outline: none;
}
}
}

View file

@ -1,83 +0,0 @@
import { ComponentProps, createEffect, createSignal, type JSX } from "solid-js"
import { VirtualizerHandle, VList } from "virtua/solid"
import { createList } from "solid-list"
import { createStore } from "solid-js/store"
export interface ListProps<T> {
data: T[]
children: (x: T) => JSX.Element
key: (x: T) => string
current?: T
onSelect?: (value: T | undefined) => void
onHover?: (value: T | undefined) => void
class?: ComponentProps<"div">["class"]
}
export function List<T>(props: ListProps<T>) {
const [virtualizer, setVirtualizer] = createSignal<VirtualizerHandle | undefined>(undefined)
const [store, setStore] = createStore({
mouseActive: false,
})
const list = createList({
items: () => props.data.map(props.key),
initialActive: props.current ? props.key(props.current) : undefined,
loop: true,
})
createEffect(() => {
if (props.current) list.setActive(props.key(props.current))
})
// const resetSelection = () => {
// if (props.data.length === 0) return
// list.setActive(props.key(props.data[0]))
// }
const handleSelect = (item: T) => {
props.onSelect?.(item)
list.setActive(props.key(item))
}
const handleKey = (e: KeyboardEvent) => {
setStore("mouseActive", false)
if (e.key === "Enter") {
e.preventDefault()
const selected = props.data.find((x) => props.key(x) === list.active())
if (selected) handleSelect(selected)
} else {
list.onKeyDown(e)
}
}
createEffect(() => {
if (store.mouseActive || props.data.length === 0) return
const index = props.data.findIndex((x) => props.key(x) === list.active())
props.onHover?.(props.data[index])
if (index === 0) {
virtualizer()?.scrollTo(0)
return
}
// virtualizer()?.scrollTo(list.active())
// const element = virtualizer()?.querySelector(`[data-key="${list.active()}"]`)
// element?.scrollIntoView({ block: "nearest", behavior: "smooth" })
})
return (
<VList data-component="list" ref={setVirtualizer} data={props.data} onKeyDown={handleKey} class={props.class}>
{(item) => (
<button
data-slot="item"
data-key={props.key(item)}
data-active={props.key(item) === list.active()}
onClick={() => handleSelect(item)}
onMouseMove={() => {
// e.currentTarget.focus()
setStore("mouseActive", true)
// list.setActive(props.key(item))
}}
>
{props.children(item)}
</button>
)}
</VList>
)
}

View file

@ -1,7 +1,10 @@
import { createEffect, Show, For, type JSX, splitProps } from "solid-js"
import { Dialog, DialogProps, Icon, IconButton, Input } from "@opencode-ai/ui"
import { createStore } from "solid-js/store"
import { FilteredListProps, useFilteredList } from "@opencode-ai/ui/hooks"
import { Dialog, DialogProps } from "./dialog"
import { Icon } from "./icon"
import { Input } from "./input"
import { IconButton } from "./icon-button"
interface SelectDialogProps<T>
extends FilteredListProps<T>,

View file

@ -1,7 +1,8 @@
import { Select as Kobalte } from "@kobalte/core/select"
import { createMemo, type ComponentProps } from "solid-js"
import { Icon, Button, type ButtonProps } from "@opencode-ai/ui"
import { pipe, groupBy, entries, map } from "remeda"
import { Button, ButtonProps } from "./button"
import { Icon } from "./icon"
export interface SelectProps<T> {
placeholder?: string

View file

@ -0,0 +1,325 @@
[data-component="session-timeline"] {
flex: 1;
min-height: 0;
padding-bottom: 80px;
display: flex;
align-items: flex-start;
justify-content: flex-start;
[data-slot="timeline-list"] {
margin-right: 32px;
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: flex-start;
margin-top: 12px;
&[data-expanded="true"] {
position: absolute;
right: 100%;
width: 240px;
margin-top: 12px;
@media (min-width: 80rem) {
gap: 8px;
margin-top: 4px;
}
}
}
[data-slot="timeline-item"] {
display: flex;
align-items: center;
align-self: stretch;
justify-content: flex-end;
&[data-expanded="true"] {
@media (min-width: 80rem) {
justify-content: flex-start;
}
}
}
[data-slot="tick-button"] {
display: flex;
align-items: center;
justify-content: flex-start;
height: 8px;
width: 32px;
margin-right: -12px;
cursor: pointer;
border: none;
background: none;
padding: 0;
&[data-active="true"] [data-slot="tick-line"] {
background-color: var(--icon-strong-base);
width: 100%;
}
&[data-expanded="true"] {
@media (min-width: 80rem) {
display: none;
}
}
}
[data-slot="tick-line"] {
height: 1px;
width: 20px;
background-color: var(--icon-base);
transition:
width 0.2s,
background-color 0.2s;
}
[data-slot="tick-button"]:hover [data-slot="tick-line"] {
width: 100%;
background-color: var(--icon-strong-base);
}
[data-slot="message-button"] {
display: none;
align-items: center;
align-self: stretch;
width: 100%;
column-gap: 8px;
cursor: default;
border: none;
background: none;
padding: 0;
&[data-expanded="true"] {
@media (min-width: 80rem) {
display: flex;
}
}
}
.spinner {
color: var(--text-base);
flex-shrink: 0;
width: 18px;
aspect-ratio: 1;
}
[data-slot="message-title-preview"] {
font-size: 14px; /* text-14-regular */
color: var(--text-weak);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
text-align: left;
&[data-active="true"] {
color: var(--text-strong);
}
}
[data-slot="timeline-item"]:hover [data-slot="message-title-preview"] {
color: var(--text-base);
}
[data-slot="content"] {
flex-grow: 1;
width: 100%;
height: 100%;
min-width: 0;
overflow-y: auto;
scrollbar-width: none;
}
[data-slot="content"]::-webkit-scrollbar {
display: none;
}
[data-slot="message-container"] {
display: flex;
flex-direction: column;
align-items: flex-start;
align-self: stretch;
gap: 32px;
padding-bottom: 80px;
}
[data-slot="message-header"] {
display: flex;
align-items: center;
gap: 8px;
align-self: stretch;
position: sticky;
top: 0;
background-color: var(--background-stronger);
z-index: 20;
height: 32px;
}
[data-slot="message-title"] {
width: 100%;
font-size: 14px; /* text-14-medium */
font-weight: 500;
color: var(--text-strong);
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
white-space: nowrap;
}
[data-slot="message-title"] h1 {
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
white-space: nowrap;
font-size: inherit;
font-weight: inherit;
}
[data-slot="summary-section"] {
width: 100%;
display: flex;
flex-direction: column;
gap: 24px;
align-items: flex-start;
align-self: stretch;
}
[data-slot="summary-header"] {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
align-self: stretch;
}
[data-slot="summary-title"] {
font-size: 12px; /* text-12-medium */
font-weight: 500;
color: var(--text-weak);
}
[data-slot="markdown"] {
&[data-diffs="true"] {
font-size: 14px; /* text-14-regular */
}
&[data-fade="true"] > * {
animation: fade-up-text 0.3s ease-out forwards;
}
}
[data-slot="accordion"] {
width: 100%;
}
[data-slot="sticky-header"] {
top: 40px; /* top-10 */
&::before {
top: -40px; /* -top-10 */
}
}
[data-slot="accordion-trigger-content"] {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
gap: 20px;
}
[data-slot="file-info"] {
flex-grow: 1;
display: flex;
align-items: center;
gap: 20px;
min-width: 0;
}
[data-slot="file-icon"] {
flex-shrink: 0;
width: 16px;
height: 16px;
}
[data-slot="file-path"] {
display: flex;
flex-grow: 1;
min-width: 0;
}
[data-slot="directory"] {
color: var(--text-base);
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
direction: rtl;
text-align: left;
}
[data-slot="filename"] {
color: var(--text-strong);
flex-shrink: 0;
}
[data-slot="accordion-actions"] {
flex-shrink: 0;
display: flex;
gap: 16px;
align-items: center;
justify-content: flex-end;
}
[data-slot="accordion-content"] {
max-height: 240px; /* max-h-60 */
overflow-y: auto;
scrollbar-width: none;
}
[data-slot="accordion-content"]::-webkit-scrollbar {
display: none;
}
[data-slot="response-section"] {
width: 100%;
}
[data-slot="collapsible-trigger"] {
color: var(--text-weak);
cursor: pointer;
background: none;
border: none;
padding: 0;
display: flex;
align-items: center;
&:hover {
color: var(--text-strong);
}
}
[data-slot="collapsible-trigger-content"] {
display: flex;
align-items: center;
gap: 4px;
align-self: stretch;
}
[data-slot="details-text"] {
font-size: 12px; /* text-12-medium */
font-weight: 500;
}
.error-card {
color: var(--text-on-critical-base);
}
[data-slot="collapsible-content-inner"] {
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
align-self: stretch;
gap: 12px;
}
}

View file

@ -0,0 +1,265 @@
import { createSeen } from "../hooks/create-seen"
import { AssistantMessage } from "@opencode-ai/sdk"
import { useData } from "../context"
import { Binary } from "@opencode-ai/util/binary"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { createEffect, createMemo, createSignal, For, Match, Show, Switch } from "solid-js"
import { createStore } from "solid-js/store"
import { DiffChanges } from "./diff-changes"
import { Spinner } from "./spinner"
import { Typewriter } from "./typewriter"
import { Message } from "./message-part"
import { Markdown } from "./markdown"
import { Accordion } from "./accordion"
import { StickyAccordionHeader } from "./sticky-accordion-header"
import { FileIcon } from "./file-icon"
import { Icon } from "./icon"
import { Diff } from "./diff"
import { Card } from "./card"
import { MessageProgress } from "./message-progress"
import { Collapsible } from "./collapsible"
export function SessionTimeline(props: { sessionID: string; expanded?: boolean }) {
const data = useData()
const [store, setStore] = createStore({
messageId: undefined as string | undefined,
})
const match = Binary.search(data.session, props.sessionID, (s) => s.id)
if (!match.found) throw new Error(`Session ${props.sessionID} not found`)
// const info = createMemo(() => data.session[match.index])
const messages = createMemo(() => (props.sessionID ? (data.message[props.sessionID] ?? []) : []))
const userMessages = createMemo(() =>
messages()
.filter((m) => m.role === "user")
.sort((a, b) => b.id.localeCompare(a.id)),
)
const lastUserMessage = createMemo(() => {
return userMessages()?.at(0)
})
const activeMessage = createMemo(() => {
if (!store.messageId) return lastUserMessage()
return userMessages()?.find((m) => m.id === store.messageId)
})
const status = createMemo(
() =>
data.session_status[props.sessionID] ?? {
type: "idle",
},
)
const working = createMemo(() => status()?.type !== "idle")
return (
<div data-component="session-timeline">
<Show when={userMessages().length > 1}>
<ul role="list" data-slot="timeline-list" data-expanded={props.expanded}>
<For each={userMessages()}>
{(message) => {
const messageWorking = createMemo(() => message.id === lastUserMessage()?.id && working())
const handleClick = () => setStore("messageId", message.id)
return (
<li data-slot="timeline-item" data-expanded={props.expanded}>
<button
data-slot="tick-button"
data-active={activeMessage()?.id === message.id}
data-expanded={props.expanded}
onClick={handleClick}
>
<div data-slot="tick-line" />
</button>
<button data-slot="message-button" data-expanded={props.expanded} onClick={handleClick}>
<Switch>
<Match when={messageWorking()}>
<Spinner class="spinner" />
</Match>
<Match when={true}>
<DiffChanges changes={message.summary?.diffs ?? []} variant="bars" />
</Match>
</Switch>
<div data-slot="message-title-preview" data-active={activeMessage()?.id === message.id}>
<Show when={message.summary?.title} fallback="New message">
{message.summary?.title}
</Show>
</div>
</button>
</li>
)
}}
</For>
</ul>
</Show>
<div data-slot="content">
<For each={userMessages()}>
{(message) => {
const isActive = createMemo(() => activeMessage()?.id === message.id)
const titleSeen = createMemo(() => true)
const contentSeen = createMemo(() => true)
{
/* const titleSeen = createSeen(`message-title-${message.id}`) */
}
{
/* const contentSeen = createSeen(`message-content-${message.id}`) */
}
const [titled, setTitled] = createSignal(titleSeen())
const assistantMessages = createMemo(() => {
return messages()?.filter((m) => m.role === "assistant" && m.parentID == message.id) as AssistantMessage[]
})
const error = createMemo(() => assistantMessages().find((m) => m?.error)?.error)
const [detailsExpanded, setDetailsExpanded] = createSignal(false)
const parts = createMemo(() => data.part[message.id])
const hasToolPart = createMemo(() =>
assistantMessages()
?.flatMap((m) => data.part[m.id])
.some((p) => p?.type === "tool"),
)
const messageWorking = createMemo(() => message.id === lastUserMessage()?.id && working())
const initialCompleted = !(message.id === lastUserMessage()?.id && working())
const [completed, setCompleted] = createSignal(initialCompleted)
// allowing time for the animations to finish
createEffect(() => {
if (titleSeen()) return
const title = message.summary?.title
if (title) setTimeout(() => setTitled(true), 10_000)
})
createEffect(() => {
const completed = !messageWorking()
setTimeout(() => setCompleted(completed), 1200)
})
return (
<Show when={isActive()}>
<div data-message={message.id} data-slot="message-container">
{/* Title */}
<div data-slot="message-header">
<div data-slot="message-title">
<Show
when={titled()}
fallback={
<Typewriter
as="h1"
text={message.summary?.title}
class="overflow-hidden text-ellipsis min-w-0 text-nowrap"
/>
}
>
<h1>{message.summary?.title}</h1>
</Show>
</div>
</div>
<Message message={message} parts={parts()} />
{/* Summary */}
<Show when={completed()}>
<div data-slot="summary-section">
<div data-slot="summary-header">
<h2 data-slot="summary-title">
<Switch>
<Match when={message.summary?.diffs?.length}>Summary</Match>
<Match when={true}>Response</Match>
</Switch>
</h2>
<Show when={message.summary?.body}>
{(summary) => (
<Markdown
data-slot="markdown"
data-diffs={!!message.summary?.diffs?.length}
data-fade={!message.summary?.diffs?.length && !contentSeen()}
text={summary()}
/>
)}
</Show>
</div>
<Accordion data-slot="accordion" multiple>
<For each={message.summary?.diffs ?? []}>
{(diff) => (
<Accordion.Item value={diff.file}>
<StickyAccordionHeader data-slot="sticky-header">
<Accordion.Trigger>
<div data-slot="accordion-trigger-content">
<div data-slot="file-info">
<FileIcon node={{ path: diff.file, type: "file" }} data-slot="file-icon" />
<div data-slot="file-path">
<Show when={diff.file.includes("/")}>
<span data-slot="directory">{getDirectory(diff.file)}&lrm;</span>
</Show>
<span data-slot="filename">{getFilename(diff.file)}</span>
</div>
</div>
<div data-slot="accordion-actions">
<DiffChanges changes={diff} />
<Icon name="chevron-grabber-vertical" size="small" />
</div>
</div>
</Accordion.Trigger>
</StickyAccordionHeader>
<Accordion.Content data-slot="accordion-content">
<Diff
before={{
name: diff.file!,
contents: diff.before!,
}}
after={{
name: diff.file!,
contents: diff.after!,
}}
/>
</Accordion.Content>
</Accordion.Item>
)}
</For>
</Accordion>
</div>
</Show>
<Show when={error() && !detailsExpanded()}>
<Card variant="error" class="error-card">
{error()?.data?.message as string}
</Card>
</Show>
{/* Response */}
<div data-slot="response-section">
<Switch>
<Match when={!completed()}>
<MessageProgress assistantMessages={assistantMessages} done={!messageWorking()} />
</Match>
<Match when={completed() && hasToolPart()}>
<Collapsible variant="ghost" open={detailsExpanded()} onOpenChange={setDetailsExpanded}>
<Collapsible.Trigger data-slot="collapsible-trigger">
<div data-slot="collapsible-trigger-content">
<div data-slot="details-text">
<Switch>
<Match when={detailsExpanded()}>Hide details</Match>
<Match when={!detailsExpanded()}>Show details</Match>
</Switch>
</div>
<Collapsible.Arrow />
</div>
</Collapsible.Trigger>
<Collapsible.Content>
<div data-slot="collapsible-content-inner">
<For each={assistantMessages()}>
{(assistantMessage) => {
const parts = createMemo(() => data.part[assistantMessage.id])
return <Message message={assistantMessage} parts={parts()} />
}}
</For>
<Show when={error()}>
<Card variant="error" class="error-card">
{error()?.data?.message as string}
</Card>
</Show>
</div>
</Collapsible.Content>
</Collapsible>
</Match>
</Switch>
</div>
</div>
</Show>
)
}}
</For>
</div>
</div>
)
}

View file

@ -1,5 +1,5 @@
import { Tooltip as KobalteTooltip } from "@kobalte/core/tooltip"
import { children, createEffect, createSignal, Match, splitProps, Switch, type JSX } from "solid-js"
import { children, createSignal, Match, onMount, splitProps, Switch, type JSX } from "solid-js"
import type { ComponentProps } from "solid-js"
export interface TooltipProps extends ComponentProps<typeof KobalteTooltip> {
@ -14,7 +14,7 @@ export function Tooltip(props: TooltipProps) {
const c = children(() => local.children)
createEffect(() => {
onMount(() => {
const childElements = c()
if (childElements instanceof HTMLElement) {
childElements.addEventListener("focus", () => setOpen(true))

View file

@ -2,9 +2,377 @@ import { marked } from "marked"
import markedShiki from "marked-shiki"
import { bundledLanguages, type BundledLanguage } from "shiki"
import { createSimpleContext } from "./helper"
import { getSharedHighlighter } from "@pierre/precision-diffs"
import { getSharedHighlighter, registerCustomTheme, ThemeRegistrationResolved } from "@pierre/precision-diffs"
const highlighter = await getSharedHighlighter({ themes: ["OpenCode"], langs: [] })
registerCustomTheme("OpenCode", () => {
return Promise.resolve({
name: "OpenCode",
colors: {
"editor.background": "transparent",
"editor.foreground": "var(--text-base)",
"gitDecoration.addedResourceForeground": "var(--syntax-diff-add)",
"gitDecoration.deletedResourceForeground": "var(--syntax-diff-delete)",
// "gitDecoration.conflictingResourceForeground": "#ffca00",
// "gitDecoration.modifiedResourceForeground": "#1a76d4",
// "gitDecoration.untrackedResourceForeground": "#00cab1",
// "gitDecoration.ignoredResourceForeground": "#84848A",
// "terminal.titleForeground": "#adadb1",
// "terminal.titleInactiveForeground": "#84848A",
// "terminal.background": "#141415",
// "terminal.foreground": "#adadb1",
// "terminal.ansiBlack": "#141415",
// "terminal.ansiRed": "#ff2e3f",
// "terminal.ansiGreen": "#0dbe4e",
// "terminal.ansiYellow": "#ffca00",
// "terminal.ansiBlue": "#008cff",
// "terminal.ansiMagenta": "#c635e4",
// "terminal.ansiCyan": "#08c0ef",
// "terminal.ansiWhite": "#c6c6c8",
// "terminal.ansiBrightBlack": "#141415",
// "terminal.ansiBrightRed": "#ff2e3f",
// "terminal.ansiBrightGreen": "#0dbe4e",
// "terminal.ansiBrightYellow": "#ffca00",
// "terminal.ansiBrightBlue": "#008cff",
// "terminal.ansiBrightMagenta": "#c635e4",
// "terminal.ansiBrightCyan": "#08c0ef",
// "terminal.ansiBrightWhite": "#c6c6c8",
},
tokenColors: [
{
scope: ["comment", "punctuation.definition.comment", "string.comment"],
settings: {
foreground: "var(--syntax-comment)",
},
},
{
scope: ["entity.other.attribute-name"],
settings: {
foreground: "var(--syntax-property)", // maybe attribute
},
},
{
scope: ["constant", "entity.name.constant", "variable.other.constant", "variable.language", "entity"],
settings: {
foreground: "var(--syntax-constant)",
},
},
{
scope: ["entity.name", "meta.export.default", "meta.definition.variable"],
settings: {
foreground: "var(--syntax-type)",
},
},
{
scope: ["meta.object.member"],
settings: {
foreground: "var(--syntax-primitive)",
},
},
{
scope: [
"variable.parameter.function",
"meta.jsx.children",
"meta.block",
"meta.tag.attributes",
"entity.name.constant",
"meta.embedded.expression",
"meta.template.expression",
"string.other.begin.yaml",
"string.other.end.yaml",
],
settings: {
foreground: "var(--syntax-punctuation)",
},
},
{
scope: ["entity.name.function", "support.type.primitive"],
settings: {
foreground: "var(--syntax-primitive)",
},
},
{
scope: ["support.class.component"],
settings: {
foreground: "var(--syntax-type)",
},
},
{
scope: "keyword",
settings: {
foreground: "var(--syntax-keyword)",
},
},
{
scope: [
"keyword.operator",
"storage.type.function.arrow",
"punctuation.separator.key-value.css",
"entity.name.tag.yaml",
"punctuation.separator.key-value.mapping.yaml",
],
settings: {
foreground: "var(--syntax-operator)",
},
},
{
scope: ["storage", "storage.type"],
settings: {
foreground: "var(--syntax-keyword)",
},
},
{
scope: ["storage.modifier.package", "storage.modifier.import", "storage.type.java"],
settings: {
foreground: "var(--syntax-primitive)",
},
},
{
scope: [
"string",
"punctuation.definition.string",
"string punctuation.section.embedded source",
"entity.name.tag",
],
settings: {
foreground: "var(--syntax-string)",
},
},
{
scope: "support",
settings: {
foreground: "var(--syntax-primitive)",
},
},
{
scope: ["support.type.object.module", "variable.other.object", "support.type.property-name.css"],
settings: {
foreground: "var(--syntax-object)",
},
},
{
scope: "meta.property-name",
settings: {
foreground: "var(--syntax-property)",
},
},
{
scope: "variable",
settings: {
foreground: "var(--syntax-variable)",
},
},
{
scope: "variable.other",
settings: {
foreground: "var(--syntax-variable)",
},
},
{
scope: [
"invalid.broken",
"invalid.illegal",
"invalid.unimplemented",
"invalid.deprecated",
"message.error",
"markup.deleted",
"meta.diff.header.from-file",
"punctuation.definition.deleted",
"brackethighlighter.unmatched",
"token.error-token",
],
settings: {
foreground: "var(--syntax-critical)",
},
},
{
scope: "carriage-return",
settings: {
foreground: "var(--syntax-keyword)",
},
},
{
scope: "string source",
settings: {
foreground: "var(--syntax-variable)",
},
},
{
scope: "string variable",
settings: {
foreground: "var(--syntax-constant)",
},
},
{
scope: [
"source.regexp",
"string.regexp",
"string.regexp.character-class",
"string.regexp constant.character.escape",
"string.regexp source.ruby.embedded",
"string.regexp string.regexp.arbitrary-repitition",
"string.regexp constant.character.escape",
],
settings: {
foreground: "var(--syntax-regexp)",
},
},
{
scope: "support.constant",
settings: {
foreground: "var(--syntax-primitive)",
},
},
{
scope: "support.variable",
settings: {
foreground: "var(--syntax-variable)",
},
},
{
scope: "meta.module-reference",
settings: {
foreground: "var(--syntax-info)",
},
},
{
scope: "punctuation.definition.list.begin.markdown",
settings: {
foreground: "var(--syntax-punctuation)",
},
},
{
scope: ["markup.heading", "markup.heading entity.name"],
settings: {
fontStyle: "bold",
foreground: "var(--syntax-info)",
},
},
{
scope: "markup.quote",
settings: {
foreground: "var(--syntax-info)",
},
},
{
scope: "markup.italic",
settings: {
fontStyle: "italic",
// foreground: "",
},
},
{
scope: "markup.bold",
settings: {
fontStyle: "bold",
foreground: "var(--text-strong)",
},
},
{
scope: [
"markup.raw",
"markup.inserted",
"meta.diff.header.to-file",
"punctuation.definition.inserted",
"markup.changed",
"punctuation.definition.changed",
"markup.ignored",
"markup.untracked",
],
settings: {
foreground: "var(--text-base)",
},
},
{
scope: "meta.diff.range",
settings: {
fontStyle: "bold",
foreground: "var(--syntax-unknown)",
},
},
{
scope: "meta.diff.header",
settings: {
foreground: "var(--syntax-unknown)",
},
},
{
scope: "meta.separator",
settings: {
fontStyle: "bold",
foreground: "var(--syntax-unknown)",
},
},
{
scope: "meta.output",
settings: {
foreground: "var(--syntax-unknown)",
},
},
{
scope: "meta.export.default",
settings: {
foreground: "var(--syntax-unknown)",
},
},
{
scope: [
"brackethighlighter.tag",
"brackethighlighter.curly",
"brackethighlighter.round",
"brackethighlighter.square",
"brackethighlighter.angle",
"brackethighlighter.quote",
],
settings: {
foreground: "var(--syntax-unknown)",
},
},
{
scope: ["constant.other.reference.link", "string.other.link"],
settings: {
fontStyle: "underline",
foreground: "var(--syntax-unknown)",
},
},
{
scope: "token.info-token",
settings: {
foreground: "var(--syntax-info)",
},
},
{
scope: "token.warn-token",
settings: {
foreground: "var(--syntax-warning)",
},
},
{
scope: "token.debug-token",
settings: {
foreground: "var(--syntax-info)",
},
},
],
semanticTokenColors: {
comment: "var(--syntax-comment)",
string: "var(--syntax-string)",
number: "var(--syntax-constant)",
regexp: "var(--syntax-regexp)",
keyword: "var(--syntax-keyword)",
variable: "var(--syntax-variable)",
parameter: "var(--syntax-variable)",
property: "var(--syntax-property)",
function: "var(--syntax-primitive)",
method: "var(--syntax-primitive)",
type: "var(--syntax-type)",
class: "var(--syntax-type)",
namespace: "var(--syntax-type)",
enumMember: "var(--syntax-primitive)",
"variable.constant": "var(--syntax-constant)",
"variable.defaultLibrary": "var(--syntax-unknown)",
},
} as unknown as ThemeRegistrationResolved)
})
export const { use: useMarked, provider: MarkedProvider } = createSimpleContext({
name: "Marked",
@ -12,6 +380,7 @@ export const { use: useMarked, provider: MarkedProvider } = createSimpleContext(
return marked.use(
markedShiki({
async highlight(code, lang) {
const highlighter = await getSharedHighlighter({ themes: ["OpenCode"], langs: [] })
if (!(lang in bundledLanguages)) {
lang = "text"
}

View file

@ -8,7 +8,6 @@ import {
Tabs,
Tooltip,
Fonts,
List,
Dialog,
Icon,
IconButton,
@ -131,11 +130,6 @@ const Demo: Component = () => {
</Tooltip>
</section>
<h3>List</h3>
<section style={{ height: "300px" }}>
<List data={["Item 1", "Item 2", "Item 3"]} key={(x) => x}>
{(x) => <div>{x}</div>}
</List>
</section>
<h3>Input</h3>
<section>
<Input

View file

@ -1,7 +1,7 @@
import { createSignal, onCleanup, onMount } from "solid-js"
import { isServer } from "solid-js/web"
export function createSessionSeen(key: string, delay = 1000) {
export function createSeen(key: string, delay = 1000) {
// 1. Initialize state based on storage (default to true on server to avoid flash)
const storageKey = `app:seen:${key}`
const [hasSeen] = createSignal(!isServer && sessionStorage.getItem(storageKey) === "true")

View file

@ -1 +1,2 @@
export * from "./use-filtered-list"
export * from "./create-seen"

View file

@ -18,7 +18,6 @@
@import "../components/icon.css" layer(components);
@import "../components/icon-button.css" layer(components);
@import "../components/input.css" layer(components);
@import "../components/list.css" layer(components);
@import "../components/logo.css" layer(components);
@import "../components/markdown.css" layer(components);
@import "../components/message-part.css" layer(components);
@ -27,6 +26,7 @@
@import "../components/select-dialog.css" layer(components);
@import "../components/spinner.css" layer(components);
@import "../components/session-review.css" layer(components);
@import "../components/session-timeline.css" layer(components);
@import "../components/sticky-accordion-header.css" layer(components);
@import "../components/tabs.css" layer(components);
@import "../components/tooltip.css" layer(components);