mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-21 17:54:23 +01:00
wip: desktop work
This commit is contained in:
19
bun.lock
19
bun.lock
@@ -114,7 +114,6 @@
|
|||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@pierre/precision-diffs": "catalog:",
|
|
||||||
"@shikijs/transformers": "3.9.2",
|
"@shikijs/transformers": "3.9.2",
|
||||||
"@solid-primitives/active-element": "2.1.3",
|
"@solid-primitives/active-element": "2.1.3",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
@@ -141,7 +140,6 @@
|
|||||||
"@types/luxon": "3.7.1",
|
"@types/luxon": "3.7.1",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
"opencode": "workspace:*",
|
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
"vite": "catalog:",
|
"vite": "catalog:",
|
||||||
"vite-plugin-icons-spritesheet": "3.0.1",
|
"vite-plugin-icons-spritesheet": "3.0.1",
|
||||||
@@ -281,6 +279,7 @@
|
|||||||
"version": "0.15.29",
|
"version": "0.15.29",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@pierre/precision-diffs": "catalog:",
|
"@pierre/precision-diffs": "catalog:",
|
||||||
"@solidjs/meta": "catalog:",
|
"@solidjs/meta": "catalog:",
|
||||||
"fuzzysort": "catalog:",
|
"fuzzysort": "catalog:",
|
||||||
@@ -1080,7 +1079,7 @@
|
|||||||
|
|
||||||
"@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ=="],
|
"@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ=="],
|
||||||
|
|
||||||
"@shikijs/core": ["@shikijs/core@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA=="],
|
"@shikijs/core": ["@shikijs/core@3.9.2", "", { "dependencies": { "@shikijs/types": "3.9.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3q/mzmw09B2B6PgFNeiaN8pkNOixWS726IHmJEpjDAcneDPMQmUg2cweT9cWXY4XcyQS3i6mOOUgQz9RRUP6HA=="],
|
||||||
|
|
||||||
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.9.2", "", { "dependencies": { "@shikijs/types": "3.9.2", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-kUTRVKPsB/28H5Ko6qEsyudBiWEDLst+Sfi+hwr59E0GLHV0h8RfgbQU7fdN5Lt9A8R1ulRiZyTvAizkROjwDA=="],
|
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.9.2", "", { "dependencies": { "@shikijs/types": "3.9.2", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-kUTRVKPsB/28H5Ko6qEsyudBiWEDLst+Sfi+hwr59E0GLHV0h8RfgbQU7fdN5Lt9A8R1ulRiZyTvAizkROjwDA=="],
|
||||||
|
|
||||||
@@ -3518,6 +3517,8 @@
|
|||||||
|
|
||||||
"@parcel/watcher-wasm/napi-wasm": ["napi-wasm@1.1.3", "", { "bundled": true }, "sha512-h/4nMGsHjZDCYmQVNODIrYACVJ+I9KItbG+0si6W/jSjdA9JbWDoU4LLeMXVcEQGHjttI2tuXqDrbGF7qkUHHg=="],
|
"@parcel/watcher-wasm/napi-wasm": ["napi-wasm@1.1.3", "", { "bundled": true }, "sha512-h/4nMGsHjZDCYmQVNODIrYACVJ+I9KItbG+0si6W/jSjdA9JbWDoU4LLeMXVcEQGHjttI2tuXqDrbGF7qkUHHg=="],
|
||||||
|
|
||||||
|
"@pierre/precision-diffs/@shikijs/core": ["@shikijs/core@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA=="],
|
||||||
|
|
||||||
"@pierre/precision-diffs/@shikijs/transformers": ["@shikijs/transformers@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/types": "3.13.0" } }, "sha512-833lcuVzcRiG+fXvgslWsM2f4gHpjEgui1ipIknSizRuTgMkNZupiXE5/TVJ6eSYfhNBFhBZKkReKWO2GgYmqA=="],
|
"@pierre/precision-diffs/@shikijs/transformers": ["@shikijs/transformers@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/types": "3.13.0" } }, "sha512-833lcuVzcRiG+fXvgslWsM2f4gHpjEgui1ipIknSizRuTgMkNZupiXE5/TVJ6eSYfhNBFhBZKkReKWO2GgYmqA=="],
|
||||||
|
|
||||||
"@pierre/precision-diffs/shiki": ["shiki@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/engine-javascript": "3.13.0", "@shikijs/engine-oniguruma": "3.13.0", "@shikijs/langs": "3.13.0", "@shikijs/themes": "3.13.0", "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-aZW4l8Og16CokuCLf8CF8kq+KK2yOygapU5m3+hoGw0Mdosc6fPitjM+ujYarppj5ZIKGyPDPP1vqmQhr+5/0g=="],
|
"@pierre/precision-diffs/shiki": ["shiki@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/engine-javascript": "3.13.0", "@shikijs/engine-oniguruma": "3.13.0", "@shikijs/langs": "3.13.0", "@shikijs/themes": "3.13.0", "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-aZW4l8Og16CokuCLf8CF8kq+KK2yOygapU5m3+hoGw0Mdosc6fPitjM+ujYarppj5ZIKGyPDPP1vqmQhr+5/0g=="],
|
||||||
@@ -3530,10 +3531,6 @@
|
|||||||
|
|
||||||
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||||
|
|
||||||
"@shikijs/core/@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="],
|
|
||||||
|
|
||||||
"@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.9.2", "", { "dependencies": { "@shikijs/types": "3.9.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3q/mzmw09B2B6PgFNeiaN8pkNOixWS726IHmJEpjDAcneDPMQmUg2cweT9cWXY4XcyQS3i6mOOUgQz9RRUP6HA=="],
|
|
||||||
|
|
||||||
"@slack/bolt/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
|
"@slack/bolt/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
|
||||||
|
|
||||||
"@slack/oauth/@slack/logger": ["@slack/logger@3.0.0", "", { "dependencies": { "@types/node": ">=12.0.0" } }, "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA=="],
|
"@slack/oauth/@slack/logger": ["@slack/logger@3.0.0", "", { "dependencies": { "@types/node": ">=12.0.0" } }, "sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA=="],
|
||||||
@@ -3798,8 +3795,6 @@
|
|||||||
|
|
||||||
"send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
|
"send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
|
||||||
|
|
||||||
"shiki/@shikijs/core": ["@shikijs/core@3.9.2", "", { "dependencies": { "@shikijs/types": "3.9.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3q/mzmw09B2B6PgFNeiaN8pkNOixWS726IHmJEpjDAcneDPMQmUg2cweT9cWXY4XcyQS3i6mOOUgQz9RRUP6HA=="],
|
|
||||||
|
|
||||||
"sitemap/sax": ["sax@1.4.1", "", {}, "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="],
|
"sitemap/sax": ["sax@1.4.1", "", {}, "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="],
|
||||||
|
|
||||||
"source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
"source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
@@ -3954,6 +3949,8 @@
|
|||||||
|
|
||||||
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
|
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
|
||||||
|
|
||||||
|
"@expressive-code/plugin-shiki/shiki/@shikijs/core": ["@shikijs/core@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA=="],
|
||||||
|
|
||||||
"@expressive-code/plugin-shiki/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-Ty7xv32XCp8u0eQt8rItpMs6rU9Ki6LJ1dQOW3V/56PKDcpvfHPnYFbsx5FFUP2Yim34m/UkazidamMNVR4vKg=="],
|
"@expressive-code/plugin-shiki/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-Ty7xv32XCp8u0eQt8rItpMs6rU9Ki6LJ1dQOW3V/56PKDcpvfHPnYFbsx5FFUP2Yim34m/UkazidamMNVR4vKg=="],
|
||||||
|
|
||||||
"@expressive-code/plugin-shiki/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg=="],
|
"@expressive-code/plugin-shiki/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg=="],
|
||||||
@@ -4088,6 +4085,8 @@
|
|||||||
|
|
||||||
"@opencode-ai/web/shiki/@shikijs/types": ["@shikijs/types@3.4.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zHC1l7L+eQlDXLnxvM9R91Efh2V4+rN3oMVS2swCBssbj2U/FBwybD1eeLaq8yl/iwT+zih8iUbTBCgGZOYlVg=="],
|
"@opencode-ai/web/shiki/@shikijs/types": ["@shikijs/types@3.4.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zHC1l7L+eQlDXLnxvM9R91Efh2V4+rN3oMVS2swCBssbj2U/FBwybD1eeLaq8yl/iwT+zih8iUbTBCgGZOYlVg=="],
|
||||||
|
|
||||||
|
"@pierre/precision-diffs/@shikijs/core/@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="],
|
||||||
|
|
||||||
"@pierre/precision-diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="],
|
"@pierre/precision-diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="],
|
||||||
|
|
||||||
"@pierre/precision-diffs/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-Ty7xv32XCp8u0eQt8rItpMs6rU9Ki6LJ1dQOW3V/56PKDcpvfHPnYFbsx5FFUP2Yim34m/UkazidamMNVR4vKg=="],
|
"@pierre/precision-diffs/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-Ty7xv32XCp8u0eQt8rItpMs6rU9Ki6LJ1dQOW3V/56PKDcpvfHPnYFbsx5FFUP2Yim34m/UkazidamMNVR4vKg=="],
|
||||||
@@ -4350,6 +4349,8 @@
|
|||||||
|
|
||||||
"@actions/github/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
|
"@actions/github/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
|
||||||
|
|
||||||
|
"@astrojs/mdx/@astrojs/markdown-remark/shiki/@shikijs/core": ["@shikijs/core@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA=="],
|
||||||
|
|
||||||
"@astrojs/mdx/@astrojs/markdown-remark/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-Ty7xv32XCp8u0eQt8rItpMs6rU9Ki6LJ1dQOW3V/56PKDcpvfHPnYFbsx5FFUP2Yim34m/UkazidamMNVR4vKg=="],
|
"@astrojs/mdx/@astrojs/markdown-remark/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-Ty7xv32XCp8u0eQt8rItpMs6rU9Ki6LJ1dQOW3V/56PKDcpvfHPnYFbsx5FFUP2Yim34m/UkazidamMNVR4vKg=="],
|
||||||
|
|
||||||
"@astrojs/mdx/@astrojs/markdown-remark/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg=="],
|
"@astrojs/mdx/@astrojs/markdown-remark/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg=="],
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"typecheck": "tsgo --noEmit",
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
@@ -11,7 +12,6 @@
|
|||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"opencode": "workspace:*",
|
|
||||||
"@tailwindcss/vite": "catalog:",
|
"@tailwindcss/vite": "catalog:",
|
||||||
"@tsconfig/bun": "1.0.9",
|
"@tsconfig/bun": "1.0.9",
|
||||||
"@types/luxon": "3.7.1",
|
"@types/luxon": "3.7.1",
|
||||||
@@ -26,7 +26,6 @@
|
|||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@pierre/precision-diffs": "catalog:",
|
|
||||||
"@shikijs/transformers": "3.9.2",
|
"@shikijs/transformers": "3.9.2",
|
||||||
"@solid-primitives/active-element": "2.1.3",
|
"@solid-primitives/active-element": "2.1.3",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import { FileDiff } from "@opencode-ai/sdk"
|
|
||||||
import { createMemo, Show } from "solid-js"
|
|
||||||
|
|
||||||
export function DiffChanges(props: { diff: FileDiff | FileDiff[] }) {
|
|
||||||
const additions = createMemo(() =>
|
|
||||||
Array.isArray(props.diff) ? props.diff.reduce((acc, diff) => acc + (diff.additions ?? 0), 0) : props.diff.additions,
|
|
||||||
)
|
|
||||||
const deletions = createMemo(() =>
|
|
||||||
Array.isArray(props.diff) ? props.diff.reduce((acc, diff) => acc + (diff.deletions ?? 0), 0) : props.diff.deletions,
|
|
||||||
)
|
|
||||||
const total = createMemo(() => additions() + deletions())
|
|
||||||
return (
|
|
||||||
<Show when={total() > 0}>
|
|
||||||
<div class="flex gap-2 justify-end items-center">
|
|
||||||
<span class="text-12-mono text-right text-text-diff-add-base">{`+${additions()}`}</span>
|
|
||||||
<span class="text-12-mono text-right text-text-diff-delete-base">{`-${deletions()}`}</span>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,238 +1,57 @@
|
|||||||
import type { Part, ReasoningPart, TextPart, ToolPart, Message, AssistantMessage, UserMessage } from "@opencode-ai/sdk"
|
import type { Part, TextPart, ToolPart, Message } from "@opencode-ai/sdk"
|
||||||
import { children, Component, createMemo, For, Match, Show, Switch, type JSX } from "solid-js"
|
import { createMemo, For, Show } from "solid-js"
|
||||||
import { Dynamic } from "solid-js/web"
|
import { Dynamic } from "solid-js/web"
|
||||||
import { Markdown } from "./markdown"
|
import { Markdown } from "./markdown"
|
||||||
import { Checkbox, Collapsible, Diff, Icon, IconProps } from "@opencode-ai/ui"
|
import { Checkbox, Diff, Icon } from "@opencode-ai/ui"
|
||||||
|
import { Message as MessageDisplay, registerPartComponent } from "@opencode-ai/ui"
|
||||||
|
import { BasicTool, GenericTool, ToolRegistry, DiffChanges } from "@opencode-ai/ui"
|
||||||
import { getDirectory, getFilename } from "@/utils"
|
import { getDirectory, getFilename } from "@/utils"
|
||||||
import type { Tool } from "opencode/tool/tool"
|
|
||||||
import type { ReadTool } from "opencode/tool/read"
|
|
||||||
import type { ListTool } from "opencode/tool/ls"
|
|
||||||
import type { GlobTool } from "opencode/tool/glob"
|
|
||||||
import type { GrepTool } from "opencode/tool/grep"
|
|
||||||
import type { WebFetchTool } from "opencode/tool/webfetch"
|
|
||||||
import type { TaskTool } from "opencode/tool/task"
|
|
||||||
import type { BashTool } from "opencode/tool/bash"
|
|
||||||
import type { EditTool } from "opencode/tool/edit"
|
|
||||||
import type { WriteTool } from "opencode/tool/write"
|
|
||||||
import type { TodoWriteTool } from "opencode/tool/todo"
|
|
||||||
import { DiffChanges } from "./diff-changes"
|
|
||||||
|
|
||||||
export function Message(props: { message: Message; parts: Part[] }) {
|
export function Message(props: { message: Message; parts: Part[] }) {
|
||||||
return (
|
return <MessageDisplay message={props.message} parts={props.parts} />
|
||||||
<Switch>
|
|
||||||
<Match when={props.message.role === "user" && props.message}>
|
|
||||||
{(userMessage) => <UserMessage message={userMessage()} parts={props.parts} />}
|
|
||||||
</Match>
|
|
||||||
<Match when={props.message.role === "assistant" && props.message}>
|
|
||||||
{(assistantMessage) => <AssistantMessage message={assistantMessage()} parts={props.parts} />}
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function AssistantMessage(props: { message: AssistantMessage; parts: Part[] }) {
|
registerPartComponent("text", function TextPartDisplay(props) {
|
||||||
const filteredParts = createMemo(() => {
|
const part = props.part as TextPart
|
||||||
return props.parts?.filter((x) => {
|
return (
|
||||||
if (x.type === "reasoning") return false
|
<Show when={part.text.trim()}>
|
||||||
return x.type !== "tool" || x.tool !== "todoread"
|
<Markdown text={part.text.trim()} />
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
registerPartComponent("reasoning", function ReasoningPartDisplay(props) {
|
||||||
|
const part = props.part as any
|
||||||
|
return (
|
||||||
|
<Show when={part.text.trim()}>
|
||||||
|
<Markdown text={part.text.trim()} />
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
})
|
})
|
||||||
return (
|
|
||||||
<div class="w-full flex flex-col items-start gap-4">
|
|
||||||
<For each={filteredParts()}>{(part) => <Part part={part} message={props.message} />}</For>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function UserMessage(props: { message: UserMessage; parts: Part[] }) {
|
registerPartComponent("tool", function ToolPartDisplay(props) {
|
||||||
const text = createMemo(() =>
|
const part = props.part as ToolPart
|
||||||
props.parts
|
|
||||||
?.filter((p) => p.type === "text" && !p.synthetic)
|
|
||||||
?.map((p) => (p as TextPart).text)
|
|
||||||
?.join(""),
|
|
||||||
)
|
|
||||||
return <div class="text-12-regular text-text-base line-clamp-3">{text()}</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Part(props: { part: Part; message: Message; hideDetails?: boolean }) {
|
|
||||||
const component = createMemo(() => PART_MAPPING[props.part.type as keyof typeof PART_MAPPING])
|
|
||||||
return (
|
|
||||||
<Show when={component()}>
|
|
||||||
<Dynamic
|
|
||||||
component={component()}
|
|
||||||
part={props.part as any}
|
|
||||||
message={props.message}
|
|
||||||
hideDetails={props.hideDetails}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const PART_MAPPING = {
|
|
||||||
text: TextPart,
|
|
||||||
tool: ToolPart,
|
|
||||||
reasoning: ReasoningPart,
|
|
||||||
}
|
|
||||||
|
|
||||||
function ReasoningPart(props: { part: ReasoningPart; message: Message }) {
|
|
||||||
return (
|
|
||||||
<Show when={props.part.text.trim()}>
|
|
||||||
<Markdown text={props.part.text.trim()} />
|
|
||||||
</Show>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TextPart(props: { part: TextPart; message: Message }) {
|
|
||||||
return (
|
|
||||||
<Show when={props.part.text.trim()}>
|
|
||||||
<Markdown text={props.part.text.trim()} />
|
|
||||||
</Show>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ToolPart(props: { part: ToolPart; message: Message; hideDetails?: boolean }) {
|
|
||||||
const component = createMemo(() => {
|
const component = createMemo(() => {
|
||||||
const render = ToolRegistry.render(props.part.tool) ?? GenericTool
|
const render = ToolRegistry.render(part.tool) ?? GenericTool
|
||||||
const metadata = props.part.state.status === "pending" ? {} : (props.part.state.metadata ?? {})
|
const metadata = part.state.status === "pending" ? {} : (part.state.metadata ?? {})
|
||||||
const input = props.part.state.status === "completed" ? props.part.state.input : {}
|
const input = part.state.status === "completed" ? part.state.input : {}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dynamic
|
<Dynamic
|
||||||
component={render}
|
component={render}
|
||||||
input={input}
|
input={input}
|
||||||
tool={props.part.tool}
|
tool={part.tool}
|
||||||
metadata={metadata}
|
metadata={metadata}
|
||||||
output={props.part.state.status === "completed" ? props.part.state.output : undefined}
|
output={part.state.status === "completed" ? part.state.output : undefined}
|
||||||
hideDetails={props.hideDetails}
|
hideDetails={props.hideDetails}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
return <Show when={component()}>{component()}</Show>
|
return <Show when={component()}>{component()}</Show>
|
||||||
}
|
})
|
||||||
|
|
||||||
type TriggerTitle = {
|
ToolRegistry.register({
|
||||||
title: string
|
|
||||||
titleClass?: string
|
|
||||||
subtitle?: string
|
|
||||||
subtitleClass?: string
|
|
||||||
args?: string[]
|
|
||||||
argsClass?: string
|
|
||||||
action?: JSX.Element
|
|
||||||
}
|
|
||||||
|
|
||||||
const isTriggerTitle = (val: any): val is TriggerTitle => {
|
|
||||||
return typeof val === "object" && val !== null && "title" in val && !(val instanceof Node)
|
|
||||||
}
|
|
||||||
|
|
||||||
function BasicTool(props: {
|
|
||||||
icon: IconProps["name"]
|
|
||||||
trigger: TriggerTitle | JSX.Element
|
|
||||||
children?: JSX.Element
|
|
||||||
hideDetails?: boolean
|
|
||||||
}) {
|
|
||||||
const resolved = children(() => props.children)
|
|
||||||
return (
|
|
||||||
<Collapsible>
|
|
||||||
<Collapsible.Trigger>
|
|
||||||
<div class="w-full flex items-center self-stretch gap-5 justify-between">
|
|
||||||
<div class="w-full flex items-center self-stretch gap-5">
|
|
||||||
<Icon name={props.icon} size="small" class="shrink-0" />
|
|
||||||
<div class="grow min-w-0">
|
|
||||||
<Switch>
|
|
||||||
<Match when={isTriggerTitle(props.trigger) && props.trigger}>
|
|
||||||
{(trigger) => (
|
|
||||||
<div class="w-full flex items-center gap-2 justify-between">
|
|
||||||
<div class="flex items-center gap-2 whitespace-nowrap truncate">
|
|
||||||
<span
|
|
||||||
classList={{
|
|
||||||
"text-12-medium text-text-base": true,
|
|
||||||
[trigger().titleClass ?? ""]: !!trigger().titleClass,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{trigger().title}
|
|
||||||
</span>
|
|
||||||
<Show when={trigger().subtitle}>
|
|
||||||
<span
|
|
||||||
classList={{
|
|
||||||
"text-12-medium text-text-weak": true,
|
|
||||||
[trigger().subtitleClass ?? ""]: !!trigger().subtitleClass,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{trigger().subtitle}
|
|
||||||
</span>
|
|
||||||
</Show>
|
|
||||||
<Show when={trigger().args?.length}>
|
|
||||||
<For each={trigger().args}>
|
|
||||||
{(arg) => (
|
|
||||||
<span
|
|
||||||
classList={{
|
|
||||||
"text-12-regular text-text-weak": true,
|
|
||||||
[trigger().argsClass ?? ""]: !!trigger().argsClass,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{arg}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
<Show when={trigger().action}>{trigger().action}</Show>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Match>
|
|
||||||
<Match when={true}>{props.trigger as JSX.Element}</Match>
|
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Show when={resolved() && !props.hideDetails}>
|
|
||||||
<Collapsible.Arrow />
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Collapsible.Trigger>
|
|
||||||
<Show when={resolved() && !props.hideDetails}>
|
|
||||||
<Collapsible.Content>{resolved()}</Collapsible.Content>
|
|
||||||
</Show>
|
|
||||||
</Collapsible>
|
|
||||||
// <>
|
|
||||||
// <Show when={props.part.state.status === "error"}>{props.part.state.error.replace("Error: ", "")}</Show>
|
|
||||||
// </>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function GenericTool(props: ToolProps<any>) {
|
|
||||||
return <BasicTool icon="mcp" trigger={{ title: props.tool }} hideDetails={props.hideDetails} />
|
|
||||||
}
|
|
||||||
|
|
||||||
type ToolProps<T extends Tool.Info> = {
|
|
||||||
input: Partial<Tool.InferParameters<T>>
|
|
||||||
metadata: Partial<Tool.InferMetadata<T>>
|
|
||||||
tool: string
|
|
||||||
output?: string
|
|
||||||
hideDetails?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const ToolRegistry = (() => {
|
|
||||||
const state: Record<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
name: string
|
|
||||||
render?: Component<ToolProps<any>>
|
|
||||||
}
|
|
||||||
> = {}
|
|
||||||
function register<T extends Tool.Info>(input: { name: string; render?: Component<ToolProps<T>> }) {
|
|
||||||
state[input.name] = input
|
|
||||||
return input
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
register,
|
|
||||||
render(name: string) {
|
|
||||||
return state[name]?.render
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
|
|
||||||
ToolRegistry.register<typeof ReadTool>({
|
|
||||||
name: "read",
|
name: "read",
|
||||||
render(props) {
|
render(props) {
|
||||||
return (
|
return (
|
||||||
@@ -244,7 +63,7 @@ ToolRegistry.register<typeof ReadTool>({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
ToolRegistry.register<typeof ListTool>({
|
ToolRegistry.register({
|
||||||
name: "list",
|
name: "list",
|
||||||
render(props) {
|
render(props) {
|
||||||
return (
|
return (
|
||||||
@@ -257,7 +76,7 @@ ToolRegistry.register<typeof ListTool>({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
ToolRegistry.register<typeof GlobTool>({
|
ToolRegistry.register({
|
||||||
name: "glob",
|
name: "glob",
|
||||||
render(props) {
|
render(props) {
|
||||||
return (
|
return (
|
||||||
@@ -277,7 +96,7 @@ ToolRegistry.register<typeof GlobTool>({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
ToolRegistry.register<typeof GrepTool>({
|
ToolRegistry.register({
|
||||||
name: "grep",
|
name: "grep",
|
||||||
render(props) {
|
render(props) {
|
||||||
const args = []
|
const args = []
|
||||||
@@ -300,7 +119,7 @@ ToolRegistry.register<typeof GrepTool>({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
ToolRegistry.register<typeof WebFetchTool>({
|
ToolRegistry.register({
|
||||||
name: "webfetch",
|
name: "webfetch",
|
||||||
render(props) {
|
render(props) {
|
||||||
return (
|
return (
|
||||||
@@ -325,7 +144,7 @@ ToolRegistry.register<typeof WebFetchTool>({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
ToolRegistry.register<typeof TaskTool>({
|
ToolRegistry.register({
|
||||||
name: "task",
|
name: "task",
|
||||||
render(props) {
|
render(props) {
|
||||||
return (
|
return (
|
||||||
@@ -345,7 +164,7 @@ ToolRegistry.register<typeof TaskTool>({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
ToolRegistry.register<typeof BashTool>({
|
ToolRegistry.register({
|
||||||
name: "bash",
|
name: "bash",
|
||||||
render(props) {
|
render(props) {
|
||||||
return (
|
return (
|
||||||
@@ -364,7 +183,7 @@ ToolRegistry.register<typeof BashTool>({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
ToolRegistry.register<typeof EditTool>({
|
ToolRegistry.register({
|
||||||
name: "edit",
|
name: "edit",
|
||||||
render(props) {
|
render(props) {
|
||||||
return (
|
return (
|
||||||
@@ -402,7 +221,7 @@ ToolRegistry.register<typeof EditTool>({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
ToolRegistry.register<typeof WriteTool>({
|
ToolRegistry.register({
|
||||||
name: "write",
|
name: "write",
|
||||||
render(props) {
|
render(props) {
|
||||||
return (
|
return (
|
||||||
@@ -431,7 +250,7 @@ ToolRegistry.register<typeof WriteTool>({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
ToolRegistry.register<typeof TodoWriteTool>({
|
ToolRegistry.register({
|
||||||
name: "todowrite",
|
name: "todowrite",
|
||||||
render(props) {
|
render(props) {
|
||||||
return (
|
return (
|
||||||
@@ -439,13 +258,13 @@ ToolRegistry.register<typeof TodoWriteTool>({
|
|||||||
icon="checklist"
|
icon="checklist"
|
||||||
trigger={{
|
trigger={{
|
||||||
title: "To-dos",
|
title: "To-dos",
|
||||||
subtitle: `${props.input.todos?.filter((t) => t.status === "completed").length}/${props.input.todos?.length}`,
|
subtitle: `${props.input.todos?.filter((t: any) => t.status === "completed").length}/${props.input.todos?.length}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Show when={props.input.todos?.length}>
|
<Show when={props.input.todos?.length}>
|
||||||
<div class="px-12 pt-2.5 pb-6 flex flex-col gap-2">
|
<div class="px-12 pt-2.5 pb-6 flex flex-col gap-2">
|
||||||
<For each={props.input.todos}>
|
<For each={props.input.todos}>
|
||||||
{(todo) => (
|
{(todo: any) => (
|
||||||
<Checkbox readOnly checked={todo.status === "completed"}>
|
<Checkbox readOnly checked={todo.status === "completed"}>
|
||||||
<div classList={{ "line-through text-text-weaker": todo.status === "completed" }}>{todo.content}</div>
|
<div classList={{ "line-through text-text-weaker": todo.status === "completed" }}>{todo.content}</div>
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
|
|||||||
@@ -1,536 +0,0 @@
|
|||||||
import { Icon, Tooltip } from "@opencode-ai/ui"
|
|
||||||
import { Collapsible } from "@/ui"
|
|
||||||
import type { AssistantMessage, Message, Part, ToolPart } from "@opencode-ai/sdk"
|
|
||||||
import { DateTime } from "luxon"
|
|
||||||
import {
|
|
||||||
createSignal,
|
|
||||||
For,
|
|
||||||
Match,
|
|
||||||
splitProps,
|
|
||||||
Switch,
|
|
||||||
type ComponentProps,
|
|
||||||
type ParentProps,
|
|
||||||
createEffect,
|
|
||||||
createMemo,
|
|
||||||
Show,
|
|
||||||
} from "solid-js"
|
|
||||||
import { getFilename } from "@/utils"
|
|
||||||
import { Markdown } from "./markdown"
|
|
||||||
import { Code } from "./code"
|
|
||||||
import { createElementSize } from "@solid-primitives/resize-observer"
|
|
||||||
import { createScrollPosition } from "@solid-primitives/scroll"
|
|
||||||
import { ProgressCircle } from "./progress-circle"
|
|
||||||
import { pipe, sumBy } from "remeda"
|
|
||||||
import { useSync } from "@/context/sync"
|
|
||||||
import { useLocal } from "@/context/local"
|
|
||||||
|
|
||||||
function Part(props: ParentProps & ComponentProps<"div">) {
|
|
||||||
const [local, others] = splitProps(props, ["class", "classList", "children"])
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
classList={{
|
|
||||||
...(local.classList ?? {}),
|
|
||||||
"h-6 flex items-center": true,
|
|
||||||
[local.class ?? ""]: !!local.class,
|
|
||||||
}}
|
|
||||||
{...others}
|
|
||||||
>
|
|
||||||
<p class="text-12-medium text-left">{local.children}</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CollapsiblePart(props: { title: ParentProps["children"] } & ParentProps & ComponentProps<typeof Collapsible>) {
|
|
||||||
return (
|
|
||||||
<Collapsible {...props}>
|
|
||||||
<Collapsible.Trigger class="peer/collapsible">
|
|
||||||
<Part>{props.title}</Part>
|
|
||||||
</Collapsible.Trigger>
|
|
||||||
<Collapsible.Content>
|
|
||||||
<p class="flex-auto min-w-0 text-pretty">
|
|
||||||
<span class="text-12-medium text-text-weak break-words">{props.children}</span>
|
|
||||||
</p>
|
|
||||||
</Collapsible.Content>
|
|
||||||
</Collapsible>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ReadToolPart(props: { part: ToolPart }) {
|
|
||||||
const sync = useSync()
|
|
||||||
const local = useLocal()
|
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
<Match when={props.part.state.status === "pending"}>
|
|
||||||
<Part>Reading file...</Part>
|
|
||||||
</Match>
|
|
||||||
<Match when={props.part.state.status === "completed" && props.part.state}>
|
|
||||||
{(state) => {
|
|
||||||
const path = state().input["filePath"] as string
|
|
||||||
return (
|
|
||||||
<Part onClick={() => local.file.open(path)}>
|
|
||||||
<span class="">Read</span> {getFilename(path)}
|
|
||||||
</Part>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Match>
|
|
||||||
<Match when={props.part.state.status === "error" && props.part.state}>
|
|
||||||
{(state) => (
|
|
||||||
<div>
|
|
||||||
<Part>
|
|
||||||
<span class="">Read</span> {getFilename(state().input["filePath"] as string)}
|
|
||||||
</Part>
|
|
||||||
<div class="text-icon-critical-active">{sync.sanitize(state().error)}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function EditToolPart(props: { part: ToolPart }) {
|
|
||||||
const sync = useSync()
|
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
<Match when={props.part.state.status === "pending"}>
|
|
||||||
<Part>Preparing edit...</Part>
|
|
||||||
</Match>
|
|
||||||
<Match when={props.part.state.status === "completed" && props.part.state}>
|
|
||||||
{(state) => (
|
|
||||||
<CollapsiblePart
|
|
||||||
title={
|
|
||||||
<>
|
|
||||||
<span class="">Edit</span> {getFilename(state().input["filePath"] as string)}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Code path={state().input["filePath"] as string} code={state().metadata["diff"] as string} />
|
|
||||||
</CollapsiblePart>
|
|
||||||
)}
|
|
||||||
</Match>
|
|
||||||
<Match when={props.part.state.status === "error" && props.part.state}>
|
|
||||||
{(state) => (
|
|
||||||
<CollapsiblePart
|
|
||||||
title={
|
|
||||||
<>
|
|
||||||
<span class="">Edit</span> {getFilename(state().input["filePath"] as string)}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class="text-icon-critical-active">{sync.sanitize(state().error)}</div>
|
|
||||||
</CollapsiblePart>
|
|
||||||
)}
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function WriteToolPart(props: { part: ToolPart }) {
|
|
||||||
const sync = useSync()
|
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
<Match when={props.part.state.status === "pending"}>
|
|
||||||
<Part>Preparing write...</Part>
|
|
||||||
</Match>
|
|
||||||
<Match when={props.part.state.status === "completed" && props.part.state}>
|
|
||||||
{(state) => (
|
|
||||||
<CollapsiblePart
|
|
||||||
title={
|
|
||||||
<>
|
|
||||||
<span class="">Write</span> {getFilename(state().input["filePath"] as string)}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class="p-2 bg-background-panel rounded-md border border-border-subtle"></div>
|
|
||||||
</CollapsiblePart>
|
|
||||||
)}
|
|
||||||
</Match>
|
|
||||||
<Match when={props.part.state.status === "error" && props.part.state}>
|
|
||||||
{(state) => (
|
|
||||||
<div>
|
|
||||||
<Part>
|
|
||||||
<span class="">Write</span> {getFilename(state().input["filePath"] as string)}
|
|
||||||
</Part>
|
|
||||||
<div class="text-icon-critical-active">{sync.sanitize(state().error)}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function BashToolPart(props: { part: ToolPart }) {
|
|
||||||
const sync = useSync()
|
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
<Match when={props.part.state.status === "pending"}>
|
|
||||||
<Part>Writing shell command...</Part>
|
|
||||||
</Match>
|
|
||||||
<Match when={props.part.state.status === "completed" && props.part.state}>
|
|
||||||
{(state) => (
|
|
||||||
<CollapsiblePart
|
|
||||||
defaultOpen
|
|
||||||
title={
|
|
||||||
<>
|
|
||||||
<span class="">Run command:</span> {state().input["command"]}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Markdown text={`\`\`\`command\n${state().input["command"]}\n${state().output}\`\`\``} />
|
|
||||||
</CollapsiblePart>
|
|
||||||
)}
|
|
||||||
</Match>
|
|
||||||
<Match when={props.part.state.status === "error" && props.part.state}>
|
|
||||||
{(state) => (
|
|
||||||
<CollapsiblePart
|
|
||||||
title={
|
|
||||||
<>
|
|
||||||
<span class="">Shell</span> {state().input["command"]}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class="text-icon-critical-active">{sync.sanitize(state().error)}</div>
|
|
||||||
</CollapsiblePart>
|
|
||||||
)}
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ToolPart(props: { part: ToolPart }) {
|
|
||||||
// read
|
|
||||||
// edit
|
|
||||||
// write
|
|
||||||
// bash
|
|
||||||
// ls
|
|
||||||
// glob
|
|
||||||
// grep
|
|
||||||
// todowrite
|
|
||||||
// todoread
|
|
||||||
// webfetch
|
|
||||||
// websearch
|
|
||||||
// patch
|
|
||||||
// task
|
|
||||||
return (
|
|
||||||
<div class="min-w-0 flex-auto text-12-medium">
|
|
||||||
<Switch
|
|
||||||
fallback={
|
|
||||||
<span>
|
|
||||||
{props.part.type}:{props.part.tool}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Match when={props.part.tool === "read"}>
|
|
||||||
<ReadToolPart part={props.part} />
|
|
||||||
</Match>
|
|
||||||
<Match when={props.part.tool === "edit"}>
|
|
||||||
<EditToolPart part={props.part} />
|
|
||||||
</Match>
|
|
||||||
<Match when={props.part.tool === "write"}>
|
|
||||||
<WriteToolPart part={props.part} />
|
|
||||||
</Match>
|
|
||||||
<Match when={props.part.tool === "bash"}>
|
|
||||||
<BashToolPart part={props.part} />
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SessionTimeline(props: { session: string; class?: string }) {
|
|
||||||
const sync = useSync()
|
|
||||||
const [scrollElement, setScrollElement] = createSignal<HTMLElement | undefined>(undefined)
|
|
||||||
const [root, setRoot] = createSignal<HTMLDivElement | undefined>(undefined)
|
|
||||||
const [tail, setTail] = createSignal(true)
|
|
||||||
const size = createElementSize(root)
|
|
||||||
const scroll = createScrollPosition(scrollElement)
|
|
||||||
|
|
||||||
const valid = (part: Part) => {
|
|
||||||
if (!part) return false
|
|
||||||
switch (part.type) {
|
|
||||||
case "step-start":
|
|
||||||
case "step-finish":
|
|
||||||
case "file":
|
|
||||||
case "patch":
|
|
||||||
return false
|
|
||||||
case "text":
|
|
||||||
return !part.synthetic && part.text.trim()
|
|
||||||
case "reasoning":
|
|
||||||
return part.text.trim()
|
|
||||||
case "tool":
|
|
||||||
switch (part.tool) {
|
|
||||||
case "todoread":
|
|
||||||
case "todowrite":
|
|
||||||
case "list":
|
|
||||||
case "grep":
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasValidParts = (message: Message) => {
|
|
||||||
return sync.data.part[message.id]?.filter(valid).length > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasTextPart = (message: Message) => {
|
|
||||||
return !!sync.data.part[message.id]?.filter(valid).find((p) => p.type === "text")
|
|
||||||
}
|
|
||||||
|
|
||||||
const session = createMemo(() => sync.session.get(props.session))
|
|
||||||
const messages = createMemo(() => sync.data.message[props.session] ?? [])
|
|
||||||
const messagesWithValidParts = createMemo(() => sync.data.message[props.session]?.filter(hasValidParts) ?? [])
|
|
||||||
const working = createMemo(() => {
|
|
||||||
const last = messages()[messages().length - 1]
|
|
||||||
if (!last) return false
|
|
||||||
if (last.role === "user") return true
|
|
||||||
return !last.time.completed
|
|
||||||
})
|
|
||||||
|
|
||||||
const cost = createMemo(() => {
|
|
||||||
const total = pipe(
|
|
||||||
messages(),
|
|
||||||
sumBy((x) => (x.role === "assistant" ? x.cost : 0)),
|
|
||||||
)
|
|
||||||
return new Intl.NumberFormat("en-US", {
|
|
||||||
style: "currency",
|
|
||||||
currency: "USD",
|
|
||||||
}).format(total)
|
|
||||||
})
|
|
||||||
|
|
||||||
const last = createMemo(() => {
|
|
||||||
return messages().findLast((x) => x.role === "assistant") as AssistantMessage
|
|
||||||
})
|
|
||||||
|
|
||||||
const model = createMemo(() => {
|
|
||||||
if (!last()) return
|
|
||||||
const model = sync.data.provider.find((x) => x.id === last().providerID)?.models[last().modelID]
|
|
||||||
return model
|
|
||||||
})
|
|
||||||
|
|
||||||
const tokens = createMemo(() => {
|
|
||||||
if (!last()) return
|
|
||||||
const tokens = last().tokens
|
|
||||||
const total = tokens.input + tokens.output + tokens.reasoning + tokens.cache.read + tokens.cache.write
|
|
||||||
return new Intl.NumberFormat("en-US", {
|
|
||||||
notation: "compact",
|
|
||||||
compactDisplay: "short",
|
|
||||||
}).format(total)
|
|
||||||
})
|
|
||||||
|
|
||||||
const context = createMemo(() => {
|
|
||||||
if (!last()) return
|
|
||||||
if (!model()?.limit.context) return 0
|
|
||||||
const tokens = last().tokens
|
|
||||||
const total = tokens.input + tokens.output + tokens.reasoning + tokens.cache.read + tokens.cache.write
|
|
||||||
return Math.round((total / model()!.limit.context) * 100)
|
|
||||||
})
|
|
||||||
|
|
||||||
const getScrollParent = (el: HTMLElement | null): HTMLElement | undefined => {
|
|
||||||
let p = el?.parentElement
|
|
||||||
while (p && p !== document.body) {
|
|
||||||
const s = getComputedStyle(p)
|
|
||||||
if (s.overflowY === "auto" || s.overflowY === "scroll") return p
|
|
||||||
p = p.parentElement
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (!root()) return
|
|
||||||
setScrollElement(getScrollParent(root()!))
|
|
||||||
})
|
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
|
||||||
const element = scrollElement()
|
|
||||||
if (!element) return
|
|
||||||
element.scrollTop = element.scrollHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
size.height
|
|
||||||
if (tail()) scrollToBottom()
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (working()) {
|
|
||||||
setTail(true)
|
|
||||||
scrollToBottom()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
let lastScrollY = 0
|
|
||||||
createEffect(() => {
|
|
||||||
if (scroll.y < lastScrollY) {
|
|
||||||
setTail(false)
|
|
||||||
}
|
|
||||||
lastScrollY = scroll.y
|
|
||||||
})
|
|
||||||
|
|
||||||
const duration = (part: Part) => {
|
|
||||||
switch (part.type) {
|
|
||||||
default:
|
|
||||||
if (
|
|
||||||
"time" in part &&
|
|
||||||
part.time &&
|
|
||||||
"start" in part.time &&
|
|
||||||
part.time.start &&
|
|
||||||
"end" in part.time &&
|
|
||||||
part.time.end
|
|
||||||
) {
|
|
||||||
const start = DateTime.fromMillis(part.time.start)
|
|
||||||
const end = DateTime.fromMillis(part.time.end)
|
|
||||||
return end.diff(start).toFormat("s")
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
console.log("WHAT")
|
|
||||||
console.log(JSON.stringify(messagesWithValidParts()))
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={setRoot}
|
|
||||||
classList={{
|
|
||||||
"select-text flex flex-col text-text-weak": true,
|
|
||||||
[props.class ?? ""]: !!props.class,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="flex justify-end items-center self-stretch">
|
|
||||||
<div class="flex items-center gap-6">
|
|
||||||
<Tooltip value={`${tokens()} Tokens`} class="flex items-center gap-1.5">
|
|
||||||
<Show when={context()}>
|
|
||||||
<ProgressCircle percentage={context()!} />
|
|
||||||
</Show>
|
|
||||||
<div class="text-14-regular text-text-weak text-right">{context()}%</div>
|
|
||||||
</Tooltip>
|
|
||||||
<div class="text-14-regular text-text-strong text-right">{cost()}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ul role="list" class="flex flex-col items-start self-stretch">
|
|
||||||
<For each={messagesWithValidParts()}>
|
|
||||||
{(message) => (
|
|
||||||
<div
|
|
||||||
classList={{
|
|
||||||
"flex flex-col gap-1 justify-center items-start self-stretch": true,
|
|
||||||
"mt-6": hasTextPart(message),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<For each={sync.data.part[message.id]?.filter(valid) ?? []}>
|
|
||||||
{(part) => (
|
|
||||||
<li class="group/li">
|
|
||||||
<Switch fallback={<div class="">{part.type}</div>}>
|
|
||||||
<Match when={part.type === "text" && part}>
|
|
||||||
{(part) => (
|
|
||||||
<Switch>
|
|
||||||
<Match when={message.role === "user"}>
|
|
||||||
<div class="w-fit flex items-center px-3 py-1 rounded-md bg-surface-weak">
|
|
||||||
<span class="text-14-regular text-text-strong whitespace-pre-wrap break-words">
|
|
||||||
{part().text}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Match>
|
|
||||||
<Match when={message.role === "assistant"}>
|
|
||||||
<Markdown text={sync.sanitize(part().text)} />
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
)}
|
|
||||||
</Match>
|
|
||||||
<Match when={part.type === "reasoning" && part}>
|
|
||||||
{(part) => (
|
|
||||||
<CollapsiblePart
|
|
||||||
title={
|
|
||||||
<Switch fallback={<span class="text-text-weak">Thinking</span>}>
|
|
||||||
<Match when={part().time.end}>
|
|
||||||
<span class="text-12-medium text-text-weak">Thought</span> for {duration(part())}s
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Markdown text={part().text} />
|
|
||||||
</CollapsiblePart>
|
|
||||||
)}
|
|
||||||
</Match>
|
|
||||||
<Match when={part.type === "tool" && part}>{(part) => <ToolPart part={part()} />}</Match>
|
|
||||||
</Switch>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</ul>
|
|
||||||
<Show when={false}>
|
|
||||||
<Collapsible defaultOpen={false}>
|
|
||||||
<Collapsible.Trigger>
|
|
||||||
<div class="mt-12 ml-1 flex items-center gap-x-2 text-xs text-text-muted">
|
|
||||||
<Icon name="file-code" />
|
|
||||||
<span>Raw Session Data</span>
|
|
||||||
<Collapsible.Arrow class="text-text-muted" />
|
|
||||||
</div>
|
|
||||||
</Collapsible.Trigger>
|
|
||||||
<Collapsible.Content class="mt-5">
|
|
||||||
<ul role="list" class="space-y-2">
|
|
||||||
<li>
|
|
||||||
<Collapsible>
|
|
||||||
<Collapsible.Trigger>
|
|
||||||
<div class="flex items-center gap-x-2 text-xs text-text-muted ml-1">
|
|
||||||
<Icon name="file-code" />
|
|
||||||
<span>session</span>
|
|
||||||
<Collapsible.Arrow class="text-text-muted" />
|
|
||||||
</div>
|
|
||||||
</Collapsible.Trigger>
|
|
||||||
<Collapsible.Content>
|
|
||||||
<Code path="session.json" code={JSON.stringify(session(), null, 2)} />
|
|
||||||
</Collapsible.Content>
|
|
||||||
</Collapsible>
|
|
||||||
</li>
|
|
||||||
<For each={messages()}>
|
|
||||||
{(message) => (
|
|
||||||
<>
|
|
||||||
<li>
|
|
||||||
<Collapsible>
|
|
||||||
<Collapsible.Trigger>
|
|
||||||
<div class="flex items-center gap-x-2 text-xs text-text-muted ml-1">
|
|
||||||
<Icon name="file-code" />
|
|
||||||
<span>{message.role === "user" ? "user" : "assistant"}</span>
|
|
||||||
<Collapsible.Arrow class="text-text-muted" />
|
|
||||||
</div>
|
|
||||||
</Collapsible.Trigger>
|
|
||||||
<Collapsible.Content>
|
|
||||||
<Code path={message.id + ".json"} code={JSON.stringify(message, null, 2)} />
|
|
||||||
</Collapsible.Content>
|
|
||||||
</Collapsible>
|
|
||||||
</li>
|
|
||||||
<For each={sync.data.part[message.id]}>
|
|
||||||
{(part) => (
|
|
||||||
<li>
|
|
||||||
<Collapsible>
|
|
||||||
<Collapsible.Trigger>
|
|
||||||
<div class="flex items-center gap-x-2 text-xs text-text-muted ml-1">
|
|
||||||
<Icon name="file-code" />
|
|
||||||
<span>{part.type}</span>
|
|
||||||
<Collapsible.Arrow class="text-text-muted" />
|
|
||||||
</div>
|
|
||||||
</Collapsible.Trigger>
|
|
||||||
<Collapsible.Content>
|
|
||||||
<Code path={message.id + "." + part.id + ".json"} code={JSON.stringify(part, null, 2)} />
|
|
||||||
</Collapsible.Content>
|
|
||||||
</Collapsible>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</ul>
|
|
||||||
</Collapsible.Content>
|
|
||||||
</Collapsible>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
Accordion,
|
Accordion,
|
||||||
Diff,
|
Diff,
|
||||||
Collapsible,
|
Collapsible,
|
||||||
|
Part,
|
||||||
} from "@opencode-ai/ui"
|
} from "@opencode-ai/ui"
|
||||||
import { FileIcon } from "@/ui"
|
import { FileIcon } from "@/ui"
|
||||||
import FileTree from "@/components/file-tree"
|
import FileTree from "@/components/file-tree"
|
||||||
@@ -33,9 +34,9 @@ import { Code } from "@/components/code"
|
|||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { ProgressCircle } from "@/components/progress-circle"
|
import { ProgressCircle } from "@/components/progress-circle"
|
||||||
import { Message, Part } from "@/components/message"
|
import { Message } from "@/components/message"
|
||||||
import { type AssistantMessage as AssistantMessageType } from "@opencode-ai/sdk"
|
import { type AssistantMessage as AssistantMessageType } from "@opencode-ai/sdk"
|
||||||
import { DiffChanges } from "@/components/diff-changes"
|
import { DiffChanges } from "@opencode-ai/ui"
|
||||||
import { Markdown } from "@/components/markdown"
|
import { Markdown } from "@/components/markdown"
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
@@ -497,7 +498,7 @@ export default function Page() {
|
|||||||
<Show
|
<Show
|
||||||
when={local.session.active()}
|
when={local.session.active()}
|
||||||
fallback={
|
fallback={
|
||||||
<div class="flex flex-col pb-36 justify-end items-start gap-4 flex-[1_0_0] self-stretch">
|
<div class="flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch">
|
||||||
<div class="text-20-medium text-text-weaker">New session</div>
|
<div class="text-20-medium text-text-weaker">New session</div>
|
||||||
<div class="flex justify-center items-center gap-3">
|
<div class="flex justify-center items-center gap-3">
|
||||||
<Icon name="folder" size="small" />
|
<Icon name="folder" size="small" />
|
||||||
@@ -660,7 +661,7 @@ export default function Page() {
|
|||||||
class="flex flex-col items-start self-stretch gap-8 min-h-screen"
|
class="flex flex-col items-start self-stretch gap-8 min-h-screen"
|
||||||
>
|
>
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<div class="py-2 flex flex-col items-start gap-2 self-stretch sticky top-0 bg-background-stronger">
|
<div class="py-2 flex flex-col items-start gap-2 self-stretch sticky top-0 bg-background-stronger z-10">
|
||||||
<h1 class="text-14-medium text-text-strong overflow-hidden text-ellipsis min-w-0">
|
<h1 class="text-14-medium text-text-strong overflow-hidden text-ellipsis min-w-0">
|
||||||
{title() ?? prompt()}
|
{title() ?? prompt()}
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"./fonts/*": "./src/assets/fonts/*"
|
"./fonts/*": "./src/assets/fonts/*"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"typecheck": "tsgo --noEmit",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"generate:tailwind": "bun run script/tailwind.ts"
|
"generate:tailwind": "bun run script/tailwind.ts"
|
||||||
},
|
},
|
||||||
@@ -24,6 +25,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@pierre/precision-diffs": "catalog:",
|
"@pierre/precision-diffs": "catalog:",
|
||||||
"@solidjs/meta": "catalog:",
|
"@solidjs/meta": "catalog:",
|
||||||
"fuzzysort": "catalog:",
|
"fuzzysort": "catalog:",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
[data-slot="collapsible-trigger"] {
|
[data-slot="collapsible-trigger"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 40px;
|
height: 32px;
|
||||||
padding: 6px 8px 6px 12px;
|
padding: 6px 8px 6px 12px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
|
|||||||
28
packages/ui/src/components/diff-changes.css
Normal file
28
packages/ui/src/components/diff-changes.css
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
[data-component="diff-changes"] {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
[data-slot="additions"] {
|
||||||
|
font-family: var(--font-family-mono);
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: var(--font-weight-regular);
|
||||||
|
line-height: var(--line-height-large);
|
||||||
|
letter-spacing: var(--letter-spacing-normal);
|
||||||
|
text-align: right;
|
||||||
|
color: var(--text-diff-add-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="deletions"] {
|
||||||
|
font-family: var(--font-family-mono);
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: var(--font-weight-regular);
|
||||||
|
line-height: var(--line-height-large);
|
||||||
|
letter-spacing: var(--letter-spacing-normal);
|
||||||
|
text-align: right;
|
||||||
|
color: var(--text-diff-delete-base);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
packages/ui/src/components/diff-changes.tsx
Normal file
24
packages/ui/src/components/diff-changes.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import type { FileDiff } from "@opencode-ai/sdk"
|
||||||
|
import { createMemo, Show } from "solid-js"
|
||||||
|
|
||||||
|
export function DiffChanges(props: { diff: FileDiff | FileDiff[] }) {
|
||||||
|
const additions = createMemo(() =>
|
||||||
|
Array.isArray(props.diff)
|
||||||
|
? props.diff.reduce((acc, diff) => acc + (diff.additions ?? 0), 0)
|
||||||
|
: props.diff.additions,
|
||||||
|
)
|
||||||
|
const deletions = createMemo(() =>
|
||||||
|
Array.isArray(props.diff)
|
||||||
|
? props.diff.reduce((acc, diff) => acc + (diff.deletions ?? 0), 0)
|
||||||
|
: props.diff.deletions,
|
||||||
|
)
|
||||||
|
const total = createMemo(() => (additions() ?? 0) + (deletions() ?? 0))
|
||||||
|
return (
|
||||||
|
<Show when={total() > 0}>
|
||||||
|
<div data-component="diff-changes">
|
||||||
|
<span data-slot="additions">{`+${additions()}`}</span>
|
||||||
|
<span data-slot="deletions">{`-${deletions()}`}</span>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -4,12 +4,16 @@ export * from "./checkbox"
|
|||||||
export * from "./collapsible"
|
export * from "./collapsible"
|
||||||
export * from "./dialog"
|
export * from "./dialog"
|
||||||
export * from "./diff"
|
export * from "./diff"
|
||||||
|
export * from "./diff-changes"
|
||||||
export * from "./icon"
|
export * from "./icon"
|
||||||
export * from "./icon-button"
|
export * from "./icon-button"
|
||||||
export * from "./input"
|
export * from "./input"
|
||||||
export * from "./fonts"
|
export * from "./fonts"
|
||||||
export * from "./list"
|
export * from "./list"
|
||||||
|
export * from "./message-part"
|
||||||
export * from "./select"
|
export * from "./select"
|
||||||
export * from "./select-dialog"
|
export * from "./select-dialog"
|
||||||
export * from "./tabs"
|
export * from "./tabs"
|
||||||
|
export * from "./tool-display"
|
||||||
|
export * from "./tool-registry"
|
||||||
export * from "./tooltip"
|
export * from "./tooltip"
|
||||||
|
|||||||
22
packages/ui/src/components/message-part.css
Normal file
22
packages/ui/src/components/message-part.css
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
[data-component="assistant-message"] {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="user-message"] {
|
||||||
|
font-family: var(--font-family-sans);
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: var(--font-weight-regular);
|
||||||
|
line-height: var(--line-height-large);
|
||||||
|
letter-spacing: var(--letter-spacing-normal);
|
||||||
|
color: var(--text-base);
|
||||||
|
display: -webkit-box;
|
||||||
|
line-clamp: 3;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
87
packages/ui/src/components/message-part.tsx
Normal file
87
packages/ui/src/components/message-part.tsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { Component, createMemo, For, Match, Show, Switch } from "solid-js"
|
||||||
|
import { Dynamic } from "solid-js/web"
|
||||||
|
import {
|
||||||
|
AssistantMessage,
|
||||||
|
Message as MessageType,
|
||||||
|
Part as PartType,
|
||||||
|
TextPart,
|
||||||
|
ToolPart,
|
||||||
|
UserMessage,
|
||||||
|
} from "@opencode-ai/sdk"
|
||||||
|
|
||||||
|
export interface MessageProps {
|
||||||
|
message: MessageType
|
||||||
|
parts: PartType[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessagePartProps {
|
||||||
|
part: PartType
|
||||||
|
message: MessageType
|
||||||
|
hideDetails?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PartComponent = Component<MessagePartProps>
|
||||||
|
|
||||||
|
const PART_MAPPING: Record<string, PartComponent | undefined> = {}
|
||||||
|
|
||||||
|
export function registerPartComponent(type: string, component: PartComponent) {
|
||||||
|
PART_MAPPING[type] = component
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Message(props: MessageProps) {
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
<Match when={props.message.role === "user" && props.message}>
|
||||||
|
{(userMessage) => (
|
||||||
|
<UserMessageDisplay message={userMessage() as UserMessage} parts={props.parts} />
|
||||||
|
)}
|
||||||
|
</Match>
|
||||||
|
<Match when={props.message.role === "assistant" && props.message}>
|
||||||
|
{(assistantMessage) => (
|
||||||
|
<AssistantMessageDisplay
|
||||||
|
message={assistantMessage() as AssistantMessage}
|
||||||
|
parts={props.parts}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AssistantMessageDisplay(props: { message: AssistantMessage; parts: PartType[] }) {
|
||||||
|
const filteredParts = createMemo(() => {
|
||||||
|
return props.parts?.filter((x) => {
|
||||||
|
if (x.type === "reasoning") return false
|
||||||
|
return x.type !== "tool" || (x as ToolPart).tool !== "todoread"
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<div data-component="assistant-message">
|
||||||
|
<For each={filteredParts()}>{(part) => <Part part={part} message={props.message} />}</For>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UserMessageDisplay(props: { message: UserMessage; parts: PartType[] }) {
|
||||||
|
const text = createMemo(() =>
|
||||||
|
props.parts
|
||||||
|
?.filter((p) => p.type === "text" && !(p as TextPart).synthetic)
|
||||||
|
?.map((p) => (p as TextPart).text)
|
||||||
|
?.join(""),
|
||||||
|
)
|
||||||
|
return <div data-component="user-message">{text()}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Part(props: MessagePartProps) {
|
||||||
|
const component = createMemo(() => PART_MAPPING[props.part.type])
|
||||||
|
return (
|
||||||
|
<Show when={component()}>
|
||||||
|
<Dynamic
|
||||||
|
component={component()}
|
||||||
|
part={props.part}
|
||||||
|
message={props.message}
|
||||||
|
hideDetails={props.hideDetails}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
||||||
76
packages/ui/src/components/tool-display.css
Normal file
76
packages/ui/src/components/tool-display.css
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
[data-component="tool-trigger"] {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
align-self: stretch;
|
||||||
|
gap: 20px;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
[data-slot="tool-trigger-content"] {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
align-self: stretch;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="tool-icon"] {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="tool-info"] {
|
||||||
|
flex-grow: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="tool-info-structured"] {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="tool-info-main"] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="tool-title"] {
|
||||||
|
font-family: var(--font-family-sans);
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
line-height: var(--line-height-large);
|
||||||
|
letter-spacing: var(--letter-spacing-normal);
|
||||||
|
color: var(--text-base);
|
||||||
|
|
||||||
|
&.capitalize {
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="tool-subtitle"] {
|
||||||
|
font-family: var(--font-family-sans);
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
line-height: var(--line-height-large);
|
||||||
|
letter-spacing: var(--letter-spacing-normal);
|
||||||
|
color: var(--text-weak);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="tool-arg"] {
|
||||||
|
font-family: var(--font-family-sans);
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: var(--font-weight-regular);
|
||||||
|
line-height: var(--line-height-large);
|
||||||
|
letter-spacing: var(--letter-spacing-normal);
|
||||||
|
color: var(--text-weak);
|
||||||
|
}
|
||||||
|
}
|
||||||
95
packages/ui/src/components/tool-display.tsx
Normal file
95
packages/ui/src/components/tool-display.tsx
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { children, For, Match, Show, Switch, type JSX } from "solid-js"
|
||||||
|
import { Collapsible } from "./collapsible"
|
||||||
|
import { Icon, IconProps } from "./icon"
|
||||||
|
|
||||||
|
export type TriggerTitle = {
|
||||||
|
title: string
|
||||||
|
titleClass?: string
|
||||||
|
subtitle?: string
|
||||||
|
subtitleClass?: string
|
||||||
|
args?: string[]
|
||||||
|
argsClass?: string
|
||||||
|
action?: JSX.Element
|
||||||
|
}
|
||||||
|
|
||||||
|
const isTriggerTitle = (val: any): val is TriggerTitle => {
|
||||||
|
return typeof val === "object" && val !== null && "title" in val && !(val instanceof Node)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BasicToolProps {
|
||||||
|
icon: IconProps["name"]
|
||||||
|
trigger: TriggerTitle | JSX.Element
|
||||||
|
children?: JSX.Element
|
||||||
|
hideDetails?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BasicTool(props: BasicToolProps) {
|
||||||
|
const resolved = children(() => props.children)
|
||||||
|
return (
|
||||||
|
<Collapsible>
|
||||||
|
<Collapsible.Trigger>
|
||||||
|
<div data-component="tool-trigger">
|
||||||
|
<div data-slot="tool-trigger-content">
|
||||||
|
<Icon name={props.icon} size="small" data-slot="tool-icon" />
|
||||||
|
<div data-slot="tool-info">
|
||||||
|
<Switch>
|
||||||
|
<Match when={isTriggerTitle(props.trigger) && props.trigger}>
|
||||||
|
{(trigger) => (
|
||||||
|
<div data-slot="tool-info-structured">
|
||||||
|
<div data-slot="tool-info-main">
|
||||||
|
<span
|
||||||
|
data-slot="tool-title"
|
||||||
|
classList={{
|
||||||
|
[trigger().titleClass ?? ""]: !!trigger().titleClass,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{trigger().title}
|
||||||
|
</span>
|
||||||
|
<Show when={trigger().subtitle}>
|
||||||
|
<span
|
||||||
|
data-slot="tool-subtitle"
|
||||||
|
classList={{
|
||||||
|
[trigger().subtitleClass ?? ""]: !!trigger().subtitleClass,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{trigger().subtitle}
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
<Show when={trigger().args?.length}>
|
||||||
|
<For each={trigger().args}>
|
||||||
|
{(arg) => (
|
||||||
|
<span
|
||||||
|
data-slot="tool-arg"
|
||||||
|
classList={{
|
||||||
|
[trigger().argsClass ?? ""]: !!trigger().argsClass,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{arg}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<Show when={trigger().action}>{trigger().action}</Show>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Match>
|
||||||
|
<Match when={true}>{props.trigger as JSX.Element}</Match>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Show when={resolved() && !props.hideDetails}>
|
||||||
|
<Collapsible.Arrow />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
<Show when={resolved() && !props.hideDetails}>
|
||||||
|
<Collapsible.Content>{resolved()}</Collapsible.Content>
|
||||||
|
</Show>
|
||||||
|
</Collapsible>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GenericTool(props: { tool: string; hideDetails?: boolean }) {
|
||||||
|
return <BasicTool icon="mcp" trigger={{ title: props.tool }} hideDetails={props.hideDetails} />
|
||||||
|
}
|
||||||
33
packages/ui/src/components/tool-registry.tsx
Normal file
33
packages/ui/src/components/tool-registry.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Component } from "solid-js"
|
||||||
|
|
||||||
|
export interface ToolProps {
|
||||||
|
input: Record<string, any>
|
||||||
|
metadata: Record<string, any>
|
||||||
|
tool: string
|
||||||
|
output?: string
|
||||||
|
hideDetails?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ToolComponent = Component<ToolProps>
|
||||||
|
|
||||||
|
const state: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
name: string
|
||||||
|
render?: ToolComponent
|
||||||
|
}
|
||||||
|
> = {}
|
||||||
|
|
||||||
|
export function registerTool(input: { name: string; render?: ToolComponent }) {
|
||||||
|
state[input.name] = input
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTool(name: string) {
|
||||||
|
return state[name]?.render
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ToolRegistry = {
|
||||||
|
register: registerTool,
|
||||||
|
render: getTool,
|
||||||
|
}
|
||||||
@@ -9,15 +9,18 @@
|
|||||||
@import "../components/button.css" layer(components);
|
@import "../components/button.css" layer(components);
|
||||||
@import "../components/checkbox.css" layer(components);
|
@import "../components/checkbox.css" layer(components);
|
||||||
@import "../components/diff.css" layer(components);
|
@import "../components/diff.css" layer(components);
|
||||||
|
@import "../components/diff-changes.css" layer(components);
|
||||||
@import "../components/collapsible.css" layer(components);
|
@import "../components/collapsible.css" layer(components);
|
||||||
@import "../components/dialog.css" layer(components);
|
@import "../components/dialog.css" layer(components);
|
||||||
@import "../components/icon.css" layer(components);
|
@import "../components/icon.css" layer(components);
|
||||||
@import "../components/icon-button.css" layer(components);
|
@import "../components/icon-button.css" layer(components);
|
||||||
@import "../components/input.css" layer(components);
|
@import "../components/input.css" layer(components);
|
||||||
@import "../components/list.css" layer(components);
|
@import "../components/list.css" layer(components);
|
||||||
|
@import "../components/message-part.css" layer(components);
|
||||||
@import "../components/select.css" layer(components);
|
@import "../components/select.css" layer(components);
|
||||||
@import "../components/select-dialog.css" layer(components);
|
@import "../components/select-dialog.css" layer(components);
|
||||||
@import "../components/tabs.css" layer(components);
|
@import "../components/tabs.css" layer(components);
|
||||||
|
@import "../components/tool-display.css" layer(components);
|
||||||
@import "../components/tooltip.css" layer(components);
|
@import "../components/tooltip.css" layer(components);
|
||||||
|
|
||||||
@import "./utilities.css" layer(utilities);
|
@import "./utilities.css" layer(utilities);
|
||||||
|
|||||||
Reference in New Issue
Block a user