feat: improve chat streaming UX and add Mermaid diagram rendering (#438)
* feat: show current branch in empty chat state * fix: display current git branch for worktrees and update them with branch change * refactor: improve read tool output parsing with structured data * feat: add support for message part delta events * fix(chat): improve streaming rendering, scroll behavior, and assistant action visibility * feat: Add mermaid diagram support to chat markdown rendering * fix: update table download functionality to include success notification and remove unused MarkdownRenderer import * refactor: streamline Streamdown component props for improved readability * fix: preserve Streamdown code-block markers and use native Tauri cache clearing * feat: add context overview panel to view conversation details
This commit is contained in:
committed by
GitHub
parent
138772e66e
commit
4d71bb27eb
@@ -30,7 +30,7 @@
|
|||||||
"@ibm/plex": "^6.4.1",
|
"@ibm/plex": "^6.4.1",
|
||||||
"@lezer/highlight": "^1.2.3",
|
"@lezer/highlight": "^1.2.3",
|
||||||
"@octokit/rest": "^22.0.1",
|
"@octokit/rest": "^22.0.1",
|
||||||
"@opencode-ai/sdk": "^1.1.65",
|
"@opencode-ai/sdk": "^1.2.5",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@openchamber/desktop",
|
"name": "@openchamber/desktop",
|
||||||
"version": "1.6.8",
|
"version": "1.6.9",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2",
|
"@tauri-apps/cli": "^2",
|
||||||
"@types/node": "^24.3.1",
|
"@types/node": "^24.3.1",
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@openchamber/ui",
|
"name": "@openchamber/ui",
|
||||||
"version": "1.6.8",
|
"version": "1.6.9",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.20.0",
|
"@codemirror/autocomplete": "^6.20.0",
|
||||||
"@codemirror/commands": "^6.10.1",
|
"@codemirror/commands": "^6.10.1",
|
||||||
@@ -132,7 +132,7 @@
|
|||||||
"@fontsource/ibm-plex-sans": "^5.1.1",
|
"@fontsource/ibm-plex-sans": "^5.1.1",
|
||||||
"@ibm/plex": "^6.4.1",
|
"@ibm/plex": "^6.4.1",
|
||||||
"@lezer/highlight": "^1.2.3",
|
"@lezer/highlight": "^1.2.3",
|
||||||
"@opencode-ai/sdk": "^1.1.65",
|
"@opencode-ai/sdk": "^1.2.5",
|
||||||
"@pierre/diffs": "1.1.0-beta.13",
|
"@pierre/diffs": "1.1.0-beta.13",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
@@ -145,6 +145,8 @@
|
|||||||
"@radix-ui/react-toggle": "^1.1.10",
|
"@radix-ui/react-toggle": "^1.1.10",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@remixicon/react": "^4.7.0",
|
"@remixicon/react": "^4.7.0",
|
||||||
|
"@streamdown/code": "^1.0.2",
|
||||||
|
"@streamdown/mermaid": "^1.0.2",
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -162,7 +164,7 @@
|
|||||||
"react-syntax-highlighter": "^15.6.6",
|
"react-syntax-highlighter": "^15.6.6",
|
||||||
"simple-git": "^3.28.0",
|
"simple-git": "^3.28.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"streamdown": "^1.6.10",
|
"streamdown": "^2.2.0",
|
||||||
"strip-json-comments": "^5.0.3",
|
"strip-json-comments": "^5.0.3",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"yaml": "^2.8.1",
|
"yaml": "^2.8.1",
|
||||||
@@ -198,10 +200,10 @@
|
|||||||
},
|
},
|
||||||
"packages/vscode": {
|
"packages/vscode": {
|
||||||
"name": "openchamber",
|
"name": "openchamber",
|
||||||
"version": "1.6.8",
|
"version": "1.6.9",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@openchamber/ui": "workspace:*",
|
"@openchamber/ui": "workspace:*",
|
||||||
"@opencode-ai/sdk": "^1.1.65",
|
"@opencode-ai/sdk": "^1.2.5",
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
"jsonc-parser": "^3.3.1",
|
"jsonc-parser": "^3.3.1",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
@@ -221,7 +223,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@openchamber/web",
|
"name": "@openchamber/web",
|
||||||
"version": "1.6.8",
|
"version": "1.6.9",
|
||||||
"bin": {
|
"bin": {
|
||||||
"openchamber": "./bin/cli.js",
|
"openchamber": "./bin/cli.js",
|
||||||
},
|
},
|
||||||
@@ -230,7 +232,7 @@
|
|||||||
"@fontsource/ibm-plex-sans": "^5.1.1",
|
"@fontsource/ibm-plex-sans": "^5.1.1",
|
||||||
"@ibm/plex": "^6.4.1",
|
"@ibm/plex": "^6.4.1",
|
||||||
"@octokit/rest": "^22.0.1",
|
"@octokit/rest": "^22.0.1",
|
||||||
"@opencode-ai/sdk": "^1.1.65",
|
"@opencode-ai/sdk": "^1.2.5",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
@@ -843,7 +845,7 @@
|
|||||||
|
|
||||||
"@openchamber/web": ["@openchamber/web@workspace:packages/web"],
|
"@openchamber/web": ["@openchamber/web@workspace:packages/web"],
|
||||||
|
|
||||||
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.1.65", "", {}, "sha512-XlpgQJQ5WwO4tYgyyHoTT0NAB5/1StXonabVUAYVpW0JdtbwWFSdFEaLkWx6CU7MNW6ELP+SMC4n6wWO6zRW8Q=="],
|
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.2.5", "", {}, "sha512-oFw08QNupBWuszpLa4HMRf/T+fDVqu8lbYSiqevdFTSH8Zih2cw/vPqLhjHgE6rPOSXyhvg46WivfSO7eNIpPw=="],
|
||||||
|
|
||||||
"@pierre/diffs": ["@pierre/diffs@1.1.0-beta.13", "", { "dependencies": { "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-D35rxDu5V7XHX5aVGU6PF12GhscL+I+9QYgxK/i3h0d2XSirAxDdVNm49aYwlOhgmdvL0NbS1IHxPswVB5yJvw=="],
|
"@pierre/diffs": ["@pierre/diffs@1.1.0-beta.13", "", { "dependencies": { "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-D35rxDu5V7XHX5aVGU6PF12GhscL+I+9QYgxK/i3h0d2XSirAxDdVNm49aYwlOhgmdvL0NbS1IHxPswVB5yJvw=="],
|
||||||
|
|
||||||
@@ -1057,6 +1059,10 @@
|
|||||||
|
|
||||||
"@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@2.3.0", "", {}, "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg=="],
|
"@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@2.3.0", "", {}, "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg=="],
|
||||||
|
|
||||||
|
"@streamdown/code": ["@streamdown/code@1.0.2", "", { "dependencies": { "shiki": "^3.19.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-QKLS3sC8no5y0YvhGLA+ZjtNhznWU09IvFcjRKgSA35ulckMLw3b5T1ha+o1DaW8BS8l0zceLPFZa3/X9+agWQ=="],
|
||||||
|
|
||||||
|
"@streamdown/mermaid": ["@streamdown/mermaid@1.0.2", "", { "dependencies": { "mermaid": "^11.12.2" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-Fr/4sBWnAeSnxM3PcrV/+DiZe5oPMq9gOkUIAH7ZauJeuwrZ/DVzD4g0zlav6AH0axh2m/sOfrfLtY5aLT7niw=="],
|
||||||
|
|
||||||
"@surma/rollup-plugin-off-main-thread": ["@surma/rollup-plugin-off-main-thread@2.2.3", "", { "dependencies": { "ejs": "^3.1.6", "json5": "^2.2.0", "magic-string": "^0.25.0", "string.prototype.matchall": "^4.0.6" } }, "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ=="],
|
"@surma/rollup-plugin-off-main-thread": ["@surma/rollup-plugin-off-main-thread@2.2.3", "", { "dependencies": { "ejs": "^3.1.6", "json5": "^2.2.0", "magic-string": "^0.25.0", "string.prototype.matchall": "^4.0.6" } }, "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ=="],
|
||||||
|
|
||||||
"@swc/helpers": ["@swc/helpers@0.5.18", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ=="],
|
"@swc/helpers": ["@swc/helpers@0.5.18", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ=="],
|
||||||
@@ -1225,8 +1231,6 @@
|
|||||||
|
|
||||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||||
|
|
||||||
"@types/katex": ["@types/katex@0.16.8", "", {}, "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg=="],
|
|
||||||
|
|
||||||
"@types/keyv": ["@types/keyv@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg=="],
|
"@types/keyv": ["@types/keyv@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg=="],
|
||||||
|
|
||||||
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
||||||
@@ -1973,18 +1977,8 @@
|
|||||||
|
|
||||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||||
|
|
||||||
"hast": ["hast@1.0.0", "", {}, "sha512-vFUqlRV5C+xqP76Wwq2SrM0kipnmpxJm7OfvVXpB35Fp+Fn4MV+ozr+JZr5qFvyR1q/U+Foim2x+3P+x9S1PLA=="],
|
|
||||||
|
|
||||||
"hast-util-from-dom": ["hast-util-from-dom@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hastscript": "^9.0.0", "web-namespaces": "^2.0.0" } }, "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q=="],
|
|
||||||
|
|
||||||
"hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="],
|
|
||||||
|
|
||||||
"hast-util-from-html-isomorphic": ["hast-util-from-html-isomorphic@2.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-dom": "^5.0.0", "hast-util-from-html": "^2.0.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw=="],
|
|
||||||
|
|
||||||
"hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="],
|
"hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="],
|
||||||
|
|
||||||
"hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="],
|
|
||||||
|
|
||||||
"hast-util-parse-selector": ["hast-util-parse-selector@2.2.5", "", {}, "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ=="],
|
"hast-util-parse-selector": ["hast-util-parse-selector@2.2.5", "", {}, "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ=="],
|
||||||
|
|
||||||
"hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="],
|
"hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="],
|
||||||
@@ -1997,8 +1991,6 @@
|
|||||||
|
|
||||||
"hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="],
|
"hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="],
|
||||||
|
|
||||||
"hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="],
|
|
||||||
|
|
||||||
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
|
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
|
||||||
|
|
||||||
"hastscript": ["hastscript@6.0.0", "", { "dependencies": { "@types/hast": "^2.0.0", "comma-separated-tokens": "^1.0.0", "hast-util-parse-selector": "^2.0.0", "property-information": "^5.0.0", "space-separated-tokens": "^1.0.0" } }, "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w=="],
|
"hastscript": ["hastscript@6.0.0", "", { "dependencies": { "@types/hast": "^2.0.0", "comma-separated-tokens": "^1.0.0", "hast-util-parse-selector": "^2.0.0", "property-information": "^5.0.0", "space-separated-tokens": "^1.0.0" } }, "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w=="],
|
||||||
@@ -2063,7 +2055,7 @@
|
|||||||
|
|
||||||
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
|
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
|
||||||
|
|
||||||
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
"internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="],
|
||||||
|
|
||||||
"intl-messageformat": ["intl-messageformat@10.7.18", "", { "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", "@formatjs/fast-memoize": "2.2.7", "@formatjs/icu-messageformat-parser": "2.11.4", "tslib": "^2.8.0" } }, "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g=="],
|
"intl-messageformat": ["intl-messageformat@10.7.18", "", { "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", "@formatjs/fast-memoize": "2.2.7", "@formatjs/icu-messageformat-parser": "2.11.4", "tslib": "^2.8.0" } }, "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g=="],
|
||||||
|
|
||||||
@@ -2301,15 +2293,13 @@
|
|||||||
|
|
||||||
"lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="],
|
"lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="],
|
||||||
|
|
||||||
"lucide-react": ["lucide-react@0.542.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw=="],
|
|
||||||
|
|
||||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||||
|
|
||||||
"markdown-it": ["markdown-it@14.1.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg=="],
|
"markdown-it": ["markdown-it@14.1.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg=="],
|
||||||
|
|
||||||
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
|
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
|
||||||
|
|
||||||
"marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
|
"marked": ["marked@17.0.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-s5HZGFQea7Huv5zZcAGhJLT3qLpAfnY7v7GWkICUr0+Wd5TFEtdlRR2XUL5Gg+RH7u2Df595ifrxR03mBaw7gA=="],
|
||||||
|
|
||||||
"matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="],
|
"matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="],
|
||||||
|
|
||||||
@@ -2331,8 +2321,6 @@
|
|||||||
|
|
||||||
"mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
|
"mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
|
||||||
|
|
||||||
"mdast-util-math": ["mdast-util-math@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "longest-streak": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.1.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w=="],
|
|
||||||
|
|
||||||
"mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="],
|
"mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="],
|
||||||
|
|
||||||
"mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="],
|
"mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="],
|
||||||
@@ -2361,12 +2349,6 @@
|
|||||||
|
|
||||||
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
|
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
|
||||||
|
|
||||||
"micromark-extension-cjk-friendly": ["micromark-extension-cjk-friendly@1.2.3", "", { "dependencies": { "devlop": "^1.1.0", "micromark-extension-cjk-friendly-util": "2.1.1", "micromark-util-chunked": "^2.0.1", "micromark-util-resolve-all": "^2.0.1", "micromark-util-symbol": "^2.0.1" }, "peerDependencies": { "micromark": "^4.0.0", "micromark-util-types": "^2.0.0" }, "optionalPeers": ["micromark-util-types"] }, "sha512-gRzVLUdjXBLX6zNPSnHGDoo+ZTp5zy+MZm0g3sv+3chPXY7l9gW+DnrcHcZh/jiPR6MjPKO4AEJNp4Aw6V9z5Q=="],
|
|
||||||
|
|
||||||
"micromark-extension-cjk-friendly-gfm-strikethrough": ["micromark-extension-cjk-friendly-gfm-strikethrough@1.2.3", "", { "dependencies": { "devlop": "^1.1.0", "get-east-asian-width": "^1.3.0", "micromark-extension-cjk-friendly-util": "2.1.1", "micromark-util-character": "^2.1.1", "micromark-util-chunked": "^2.0.1", "micromark-util-resolve-all": "^2.0.1", "micromark-util-symbol": "^2.0.1" }, "peerDependencies": { "micromark": "^4.0.0", "micromark-util-types": "^2.0.0" }, "optionalPeers": ["micromark-util-types"] }, "sha512-gSPnxgHDDqXYOBvQRq6lerrq9mjDhdtKn+7XETuXjxWcL62yZEfUdA28Ml1I2vDIPfAOIKLa0h2XDSGkInGHFQ=="],
|
|
||||||
|
|
||||||
"micromark-extension-cjk-friendly-util": ["micromark-extension-cjk-friendly-util@2.1.1", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "micromark-util-character": "^2.1.1", "micromark-util-symbol": "^2.0.1" } }, "sha512-egs6+12JU2yutskHY55FyR48ZiEcFOJFyk9rsiyIhcJ6IvWB6ABBqVrBw8IobqJTDZ/wdSr9eoXDPb5S2nW1bg=="],
|
|
||||||
|
|
||||||
"micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
|
"micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
|
||||||
|
|
||||||
"micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
|
"micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
|
||||||
@@ -2381,8 +2363,6 @@
|
|||||||
|
|
||||||
"micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="],
|
"micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="],
|
||||||
|
|
||||||
"micromark-extension-math": ["micromark-extension-math@3.1.0", "", { "dependencies": { "@types/katex": "^0.16.0", "devlop": "^1.0.0", "katex": "^0.16.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg=="],
|
|
||||||
|
|
||||||
"micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
|
"micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
|
||||||
|
|
||||||
"micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
|
"micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
|
||||||
@@ -2687,27 +2667,19 @@
|
|||||||
|
|
||||||
"rehype-harden": ["rehype-harden@1.1.7", "", { "dependencies": { "unist-util-visit": "^5.0.0" } }, "sha512-j5DY0YSK2YavvNGV+qBHma15J9m0WZmRe8posT5AtKDS6TNWtMVTo6RiqF8SidfcASYz8f3k2J/1RWmq5zTXUw=="],
|
"rehype-harden": ["rehype-harden@1.1.7", "", { "dependencies": { "unist-util-visit": "^5.0.0" } }, "sha512-j5DY0YSK2YavvNGV+qBHma15J9m0WZmRe8posT5AtKDS6TNWtMVTo6RiqF8SidfcASYz8f3k2J/1RWmq5zTXUw=="],
|
||||||
|
|
||||||
"rehype-katex": ["rehype-katex@7.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/katex": "^0.16.0", "hast-util-from-html-isomorphic": "^2.0.0", "hast-util-to-text": "^4.0.0", "katex": "^0.16.0", "unist-util-visit-parents": "^6.0.0", "vfile": "^6.0.0" } }, "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA=="],
|
|
||||||
|
|
||||||
"rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="],
|
"rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="],
|
||||||
|
|
||||||
"rehype-sanitize": ["rehype-sanitize@6.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-sanitize": "^5.0.0" } }, "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg=="],
|
"rehype-sanitize": ["rehype-sanitize@6.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-sanitize": "^5.0.0" } }, "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg=="],
|
||||||
|
|
||||||
"remark-cjk-friendly": ["remark-cjk-friendly@1.2.3", "", { "dependencies": { "micromark-extension-cjk-friendly": "1.2.3" }, "peerDependencies": { "@types/mdast": "^4.0.0", "unified": "^11.0.0" }, "optionalPeers": ["@types/mdast"] }, "sha512-UvAgxwlNk+l9Oqgl/9MWK2eWRS7zgBW/nXX9AthV7nd/3lNejF138E7Xbmk9Zs4WjTJGs721r7fAEc7tNFoH7g=="],
|
|
||||||
|
|
||||||
"remark-cjk-friendly-gfm-strikethrough": ["remark-cjk-friendly-gfm-strikethrough@1.2.3", "", { "dependencies": { "micromark-extension-cjk-friendly-gfm-strikethrough": "1.2.3" }, "peerDependencies": { "@types/mdast": "^4.0.0", "unified": "^11.0.0" }, "optionalPeers": ["@types/mdast"] }, "sha512-bXfMZtsaomK6ysNN/UGRIcasQAYkC10NtPmP0oOHOV8YOhA2TXmwRXCku4qOzjIFxAPfish5+XS0eIug2PzNZA=="],
|
|
||||||
|
|
||||||
"remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
|
"remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
|
||||||
|
|
||||||
"remark-math": ["remark-math@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-math": "^3.0.0", "micromark-extension-math": "^3.0.0", "unified": "^11.0.0" } }, "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA=="],
|
|
||||||
|
|
||||||
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
|
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
|
||||||
|
|
||||||
"remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
|
"remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
|
||||||
|
|
||||||
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
|
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
|
||||||
|
|
||||||
"remend": ["remend@1.0.1", "", {}, "sha512-152puVH0qMoRJQFnaMG+rVDdf01Jq/CaED+MBuXExurJgdbkLp0c3TIe4R12o28Klx8uyGsjvFNG05aFG69G9w=="],
|
"remend": ["remend@1.2.0", "", {}, "sha512-NbKrdWweTRuByPYErzQCNpNtsR9M1QQ0hK2UzmnmlSaEqHnkQ5Korlyi8KpdbOJ0rImJfRy4EAY0uDxYnL9Plw=="],
|
||||||
|
|
||||||
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||||
|
|
||||||
@@ -2853,7 +2825,7 @@
|
|||||||
|
|
||||||
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
|
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
|
||||||
|
|
||||||
"streamdown": ["streamdown@1.6.11", "", { "dependencies": { "clsx": "^2.1.1", "hast": "^1.0.0", "hast-util-to-jsx-runtime": "^2.3.6", "html-url-attributes": "^3.0.1", "katex": "^0.16.22", "lucide-react": "^0.542.0", "marked": "^16.2.1", "mermaid": "^11.11.0", "rehype-harden": "^1.1.6", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-cjk-friendly": "^1.2.3", "remark-cjk-friendly-gfm-strikethrough": "^1.2.3", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remend": "1.0.1", "shiki": "^3.12.2", "tailwind-merge": "^3.3.1", "unified": "^11.0.5", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-Y38fwRx5kCKTluwM+Gf27jbbi9q6Qy+WC9YrC1YbCpMkktT3PsRBJHMWiqYeF8y/JzLpB1IzDoeaB6qkQEDnAA=="],
|
"streamdown": ["streamdown@2.2.0", "", { "dependencies": { "clsx": "^2.1.1", "hast-util-to-jsx-runtime": "^2.3.6", "html-url-attributes": "^3.0.1", "marked": "^17.0.1", "rehype-harden": "^1.1.7", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remend": "1.2.0", "tailwind-merge": "^3.4.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-Y51o1I/sjpAy4Yn7j7R4TbUl9gcUZ7BTrHS+68IhrUBoYpNQZ28z06vww1MBFu4mSwvgF8xQIxIH2b9S9IHDyQ=="],
|
||||||
|
|
||||||
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
@@ -3023,14 +2995,10 @@
|
|||||||
|
|
||||||
"unique-string": ["unique-string@2.0.0", "", { "dependencies": { "crypto-random-string": "^2.0.0" } }, "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg=="],
|
"unique-string": ["unique-string@2.0.0", "", { "dependencies": { "crypto-random-string": "^2.0.0" } }, "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg=="],
|
||||||
|
|
||||||
"unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="],
|
|
||||||
|
|
||||||
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
|
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
|
||||||
|
|
||||||
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
|
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
|
||||||
|
|
||||||
"unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="],
|
|
||||||
|
|
||||||
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
|
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
|
||||||
|
|
||||||
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
|
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
|
||||||
@@ -3383,8 +3351,6 @@
|
|||||||
|
|
||||||
"globby/slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="],
|
"globby/slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="],
|
||||||
|
|
||||||
"hast-util-from-dom/hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="],
|
|
||||||
|
|
||||||
"hast-util-from-parse5/hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="],
|
"hast-util-from-parse5/hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="],
|
||||||
|
|
||||||
"hastscript/@types/hast": ["@types/hast@2.3.10", "", { "dependencies": { "@types/unist": "^2" } }, "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw=="],
|
"hastscript/@types/hast": ["@types/hast@2.3.10", "", { "dependencies": { "@types/unist": "^2" } }, "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw=="],
|
||||||
@@ -3415,6 +3381,8 @@
|
|||||||
|
|
||||||
"mdast-util-mdx-jsx/parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
|
"mdast-util-mdx-jsx/parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
|
||||||
|
|
||||||
|
"mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
|
||||||
|
|
||||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
|
||||||
"minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
"minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||||
@@ -3541,8 +3509,6 @@
|
|||||||
|
|
||||||
"cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="],
|
"cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="],
|
||||||
|
|
||||||
"d3-sankey/d3-array/internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="],
|
|
||||||
|
|
||||||
"d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="],
|
"d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="],
|
||||||
|
|
||||||
"electron/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
"electron/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
@@ -3551,8 +3517,6 @@
|
|||||||
|
|
||||||
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
|
|
||||||
"hast-util-from-dom/hastscript/hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="],
|
|
||||||
|
|
||||||
"hast-util-from-parse5/hastscript/hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="],
|
"hast-util-from-parse5/hastscript/hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="],
|
||||||
|
|
||||||
"hastscript/@types/hast/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
"hastscript/@types/hast/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||||
|
|||||||
+1
-1
@@ -83,7 +83,7 @@
|
|||||||
"@ibm/plex": "^6.4.1",
|
"@ibm/plex": "^6.4.1",
|
||||||
"@lezer/highlight": "^1.2.3",
|
"@lezer/highlight": "^1.2.3",
|
||||||
"@octokit/rest": "^22.0.1",
|
"@octokit/rest": "^22.0.1",
|
||||||
"@opencode-ai/sdk": "^1.1.65",
|
"@opencode-ai/sdk": "^1.2.5",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
|
|||||||
@@ -117,6 +117,8 @@ const MENU_ITEM_REPORT_BUG_ID: &str = "menu_report_bug";
|
|||||||
const MENU_ITEM_REQUEST_FEATURE_ID: &str = "menu_request_feature";
|
const MENU_ITEM_REQUEST_FEATURE_ID: &str = "menu_request_feature";
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
const MENU_ITEM_JOIN_DISCORD_ID: &str = "menu_join_discord";
|
const MENU_ITEM_JOIN_DISCORD_ID: &str = "menu_join_discord";
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
const MENU_ITEM_CLEAR_CACHE_ID: &str = "menu_clear_cache";
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
const GITHUB_BUG_REPORT_URL: &str =
|
const GITHUB_BUG_REPORT_URL: &str =
|
||||||
@@ -268,6 +270,9 @@ fn build_macos_menu<R: tauri::Runtime>(
|
|||||||
let join_discord =
|
let join_discord =
|
||||||
MenuItem::with_id(app, MENU_ITEM_JOIN_DISCORD_ID, "Join Discord", true, None::<&str>)?;
|
MenuItem::with_id(app, MENU_ITEM_JOIN_DISCORD_ID, "Join Discord", true, None::<&str>)?;
|
||||||
|
|
||||||
|
let clear_cache =
|
||||||
|
MenuItem::with_id(app, MENU_ITEM_CLEAR_CACHE_ID, "Clear Cache", true, None::<&str>)?;
|
||||||
|
|
||||||
let theme_submenu =
|
let theme_submenu =
|
||||||
Submenu::with_items(app, "Theme", true, &[&theme_light, &theme_dark, &theme_system])?;
|
Submenu::with_items(app, "Theme", true, &[&theme_light, &theme_dark, &theme_system])?;
|
||||||
|
|
||||||
@@ -293,6 +298,8 @@ fn build_macos_menu<R: tauri::Runtime>(
|
|||||||
&help_dialog,
|
&help_dialog,
|
||||||
&download_logs,
|
&download_logs,
|
||||||
&PredefinedMenuItem::separator(app)?,
|
&PredefinedMenuItem::separator(app)?,
|
||||||
|
&clear_cache,
|
||||||
|
&PredefinedMenuItem::separator(app)?,
|
||||||
&report_bug,
|
&report_bug,
|
||||||
&request_feature,
|
&request_feature,
|
||||||
&PredefinedMenuItem::separator(app)?,
|
&PredefinedMenuItem::separator(app)?,
|
||||||
@@ -412,6 +419,35 @@ fn desktop_set_auto_worktree_menu(app: tauri::AppHandle, enabled: bool) -> Resul
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn desktop_clear_cache(app: tauri::AppHandle) -> Result<(), String> {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
let mut failures: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
for (label, window) in app.webview_windows() {
|
||||||
|
if let Err(err) = window.clear_all_browsing_data() {
|
||||||
|
failures.push(format!("{label}: {err}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !failures.is_empty() {
|
||||||
|
return Err(format!("Failed to clear browsing data for some windows: {}", failures.join("; ")));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload all windows after clearing persisted browsing data so in-memory state is reset too.
|
||||||
|
eval_in_all_windows(&app, "window.location.reload();");
|
||||||
|
|
||||||
|
log::info!("[desktop] Cleared all webview browsing data and reloaded windows");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
{
|
||||||
|
Err("desktop_clear_cache is only supported on macOS".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn desktop_open_path(path: String, app: Option<String>) -> Result<(), String> {
|
fn desktop_open_path(path: String, app: Option<String>) -> Result<(), String> {
|
||||||
let trimmed = path.trim();
|
let trimmed = path.trim();
|
||||||
@@ -2455,6 +2491,14 @@ fn main() {
|
|||||||
}
|
}
|
||||||
if id == MENU_ITEM_DOWNLOAD_LOGS_ID {
|
if id == MENU_ITEM_DOWNLOAD_LOGS_ID {
|
||||||
dispatch_menu_action(app, "download-logs");
|
dispatch_menu_action(app, "download-logs");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if id == MENU_ITEM_CLEAR_CACHE_ID {
|
||||||
|
let app = app.clone();
|
||||||
|
tauri::async_runtime::spawn_blocking(move || {
|
||||||
|
let _ = crate::desktop_clear_cache(app);
|
||||||
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -2498,6 +2542,7 @@ fn main() {
|
|||||||
desktop_new_window,
|
desktop_new_window,
|
||||||
desktop_new_window_at_url,
|
desktop_new_window_at_url,
|
||||||
desktop_set_auto_worktree_menu,
|
desktop_set_auto_worktree_menu,
|
||||||
|
desktop_clear_cache,
|
||||||
desktop_open_path,
|
desktop_open_path,
|
||||||
desktop_filter_installed_apps,
|
desktop_filter_installed_apps,
|
||||||
desktop_get_installed_apps,
|
desktop_get_installed_apps,
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
"@fontsource/ibm-plex-sans": "^5.1.1",
|
"@fontsource/ibm-plex-sans": "^5.1.1",
|
||||||
"@ibm/plex": "^6.4.1",
|
"@ibm/plex": "^6.4.1",
|
||||||
"@lezer/highlight": "^1.2.3",
|
"@lezer/highlight": "^1.2.3",
|
||||||
"@opencode-ai/sdk": "^1.1.65",
|
"@opencode-ai/sdk": "^1.2.5",
|
||||||
"@pierre/diffs": "1.1.0-beta.13",
|
"@pierre/diffs": "1.1.0-beta.13",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
@@ -50,6 +50,8 @@
|
|||||||
"@radix-ui/react-toggle": "^1.1.10",
|
"@radix-ui/react-toggle": "^1.1.10",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@remixicon/react": "^4.7.0",
|
"@remixicon/react": "^4.7.0",
|
||||||
|
"@streamdown/code": "^1.0.2",
|
||||||
|
"@streamdown/mermaid": "^1.0.2",
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -67,7 +69,7 @@
|
|||||||
"react-syntax-highlighter": "^15.6.6",
|
"react-syntax-highlighter": "^15.6.6",
|
||||||
"simple-git": "^3.28.0",
|
"simple-git": "^3.28.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"streamdown": "^1.6.10",
|
"streamdown": "^2.2.0",
|
||||||
"strip-json-comments": "^5.0.3",
|
"strip-json-comments": "^5.0.3",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"yaml": "^2.8.1",
|
"yaml": "^2.8.1",
|
||||||
|
|||||||
@@ -341,10 +341,10 @@ export const ChatContainer: React.FC = () => {
|
|||||||
|
|
||||||
if (!shouldSkipScroll) {
|
if (!shouldSkipScroll) {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
scrollToBottom();
|
scrollToBottom({ instant: true });
|
||||||
} else {
|
} else {
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
scrollToBottom();
|
scrollToBottom({ instant: true });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -372,7 +372,7 @@ export const ChatContainer: React.FC = () => {
|
|||||||
style={isMobile ? { paddingBottom: 'var(--oc-keyboard-inset, 0px)' } : undefined}
|
style={isMobile ? { paddingBottom: 'var(--oc-keyboard-inset, 0px)' } : undefined}
|
||||||
>
|
>
|
||||||
<div className="flex-1 flex items-center justify-center">
|
<div className="flex-1 flex items-center justify-center">
|
||||||
<ChatEmptyState />
|
<ChatEmptyState showDraftContext />
|
||||||
</div>
|
</div>
|
||||||
<div className="relative bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 z-10">
|
<div className="relative bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 z-10">
|
||||||
<ChatInput scrollToBottom={scrollToBottom} />
|
<ChatInput scrollToBottom={scrollToBottom} />
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { RiGitBranchLine } from '@remixicon/react';
|
||||||
|
|
||||||
import { OpenChamberLogo } from '@/components/ui/OpenChamberLogo';
|
import { OpenChamberLogo } from '@/components/ui/OpenChamberLogo';
|
||||||
import { TextLoop } from '@/components/ui/TextLoop';
|
import { TextLoop } from '@/components/ui/TextLoop';
|
||||||
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
||||||
|
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||||
|
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
||||||
|
import { useGitStatus, useGitStore } from '@/stores/useGitStore';
|
||||||
|
|
||||||
const phrases = [
|
const phrases = [
|
||||||
"Fix the failing tests",
|
"Fix the failing tests",
|
||||||
@@ -23,15 +27,51 @@ const phrases = [
|
|||||||
"Add type definitions",
|
"Add type definitions",
|
||||||
];
|
];
|
||||||
|
|
||||||
const ChatEmptyState: React.FC = () => {
|
interface ChatEmptyStateProps {
|
||||||
|
showDraftContext?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChatEmptyState: React.FC<ChatEmptyStateProps> = ({
|
||||||
|
showDraftContext = false,
|
||||||
|
}) => {
|
||||||
const { currentTheme } = useThemeSystem();
|
const { currentTheme } = useThemeSystem();
|
||||||
|
const { git } = useRuntimeAPIs();
|
||||||
|
const effectiveDirectory = useEffectiveDirectory();
|
||||||
|
const { setActiveDirectory, fetchStatus } = useGitStore();
|
||||||
|
const gitStatus = useGitStatus(effectiveDirectory ?? null);
|
||||||
|
|
||||||
// Use theme's muted foreground for secondary text
|
// Use theme's muted foreground for secondary text
|
||||||
const textColor = currentTheme?.colors?.surface?.mutedForeground || 'var(--muted-foreground)';
|
const textColor = currentTheme?.colors?.surface?.mutedForeground || 'var(--muted-foreground)';
|
||||||
|
const branchName = typeof gitStatus?.current === 'string' && gitStatus.current.trim().length > 0
|
||||||
|
? gitStatus.current.trim()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!showDraftContext || !effectiveDirectory) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveDirectory(effectiveDirectory);
|
||||||
|
|
||||||
|
const state = useGitStore.getState().directories.get(effectiveDirectory);
|
||||||
|
if (!state?.status && state?.isGitRepo !== false) {
|
||||||
|
void fetchStatus(effectiveDirectory, git, { silent: true });
|
||||||
|
}
|
||||||
|
}, [effectiveDirectory, fetchStatus, git, setActiveDirectory, showDraftContext]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center min-h-full w-full gap-6">
|
<div className="flex flex-col items-center justify-center min-h-full w-full gap-6">
|
||||||
<OpenChamberLogo width={140} height={140} className="opacity-20" isAnimated />
|
<OpenChamberLogo width={140} height={140} className="opacity-20" isAnimated />
|
||||||
|
{showDraftContext && (
|
||||||
|
<div className="max-w-[calc(100%-2rem)] flex flex-col items-center gap-1">
|
||||||
|
{branchName && (
|
||||||
|
<div className="inline-flex items-center gap-1 text-body-md" style={{ color: textColor }}>
|
||||||
|
<RiGitBranchLine className="h-4 w-4 shrink-0" />
|
||||||
|
<span className="overflow-hidden whitespace-nowrap" title={branchName}>{branchName}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<TextLoop
|
<TextLoop
|
||||||
className="text-body-md"
|
className="text-body-md"
|
||||||
interval={4}
|
interval={4}
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Streamdown } from 'streamdown';
|
import { Streamdown } from 'streamdown';
|
||||||
|
import { code } from '@streamdown/code';
|
||||||
|
import { mermaid } from '@streamdown/mermaid';
|
||||||
|
import 'streamdown/styles.css';
|
||||||
import { FadeInOnReveal } from './message/FadeInOnReveal';
|
import { FadeInOnReveal } from './message/FadeInOnReveal';
|
||||||
import type { Part } from '@opencode-ai/sdk/v2';
|
import type { Part } from '@opencode-ai/sdk/v2';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { RiFileCopyLine, RiCheckLine, RiDownloadLine } from '@remixicon/react';
|
import { RiFileCopyLine, RiCheckLine, RiDownloadLine } from '@remixicon/react';
|
||||||
|
import { toast } from '@/components/ui';
|
||||||
|
|
||||||
import { isVSCodeRuntime } from '@/lib/desktop';
|
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||||
import { useOptionalThemeSystem } from '@/contexts/useThemeSystem';
|
import { useOptionalThemeSystem } from '@/contexts/useThemeSystem';
|
||||||
@@ -116,6 +120,45 @@ const useMarkdownShikiThemes = (): readonly [string | object, string | object] =
|
|||||||
return isVSCode ? themes : fallbackThemes;
|
return isVSCode ? themes : fallbackThemes;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const useStreamdownMermaidOptions = () => {
|
||||||
|
const themeSystem = useOptionalThemeSystem();
|
||||||
|
const fallbackLight = getDefaultTheme(false);
|
||||||
|
const fallbackDark = getDefaultTheme(true);
|
||||||
|
|
||||||
|
const currentTheme = themeSystem?.currentTheme
|
||||||
|
?? (typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
? fallbackDark
|
||||||
|
: fallbackLight);
|
||||||
|
|
||||||
|
const mermaidRenderKey = `${currentTheme.metadata.id}:${currentTheme.metadata.variant}:${themeSystem?.themeMode ?? 'fallback'}`;
|
||||||
|
|
||||||
|
const options = React.useMemo(() => {
|
||||||
|
const isDark = currentTheme.metadata.variant === 'dark';
|
||||||
|
return {
|
||||||
|
config: {
|
||||||
|
theme: isDark ? 'dark' : 'base',
|
||||||
|
themeVariables: {
|
||||||
|
primaryColor: currentTheme.colors.surface.elevated,
|
||||||
|
primaryTextColor: currentTheme.colors.surface.foreground,
|
||||||
|
primaryBorderColor: currentTheme.colors.interactive.border,
|
||||||
|
lineColor: currentTheme.colors.interactive.border,
|
||||||
|
secondaryColor: currentTheme.colors.surface.muted,
|
||||||
|
tertiaryColor: currentTheme.colors.surface.subtle,
|
||||||
|
background: currentTheme.colors.surface.background,
|
||||||
|
mainBkg: currentTheme.colors.surface.elevated,
|
||||||
|
nodeTextColor: currentTheme.colors.surface.foreground,
|
||||||
|
edgeLabelBackground: currentTheme.colors.surface.background,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, [currentTheme]);
|
||||||
|
|
||||||
|
return React.useMemo(
|
||||||
|
() => ({ options, mermaidRenderKey }),
|
||||||
|
[mermaidRenderKey, options],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Table utility functions
|
// Table utility functions
|
||||||
const extractTableData = (tableEl: HTMLTableElement): { headers: string[]; rows: string[][] } => {
|
const extractTableData = (tableEl: HTMLTableElement): { headers: string[]; rows: string[][] } => {
|
||||||
const headers: string[] = [];
|
const headers: string[] = [];
|
||||||
@@ -280,21 +323,22 @@ const TableDownloadButton: React.FC<{ tableRef: React.RefObject<HTMLDivElement |
|
|||||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleDownload = (format: 'csv' | 'markdown') => {
|
const handleDownload = (format: 'csv' | 'markdown') => {
|
||||||
const tableEl = tableRef.current?.querySelector('table');
|
const tableEl = tableRef.current?.querySelector('table');
|
||||||
if (!tableEl) return;
|
if (!tableEl) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = extractTableData(tableEl);
|
const data = extractTableData(tableEl);
|
||||||
const content = format === 'csv' ? tableToCSV(data) : tableToMarkdown(data);
|
const content = format === 'csv' ? tableToCSV(data) : tableToMarkdown(data);
|
||||||
const filename = format === 'csv' ? 'table.csv' : 'table.md';
|
const filename = format === 'csv' ? 'table.csv' : 'table.md';
|
||||||
const mimeType = format === 'csv' ? 'text/csv' : 'text/markdown';
|
const mimeType = format === 'csv' ? 'text/csv' : 'text/markdown';
|
||||||
downloadFile(filename, content, mimeType);
|
downloadFile(filename, content, mimeType);
|
||||||
setShowMenu(false);
|
setShowMenu(false);
|
||||||
} catch (err) {
|
toast.success(`Table downloaded as ${format.toUpperCase()}`);
|
||||||
console.error('Failed to download table:', err);
|
} catch (err) {
|
||||||
}
|
console.error('Failed to download table:', err);
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative" ref={menuRef}>
|
<div className="relative" ref={menuRef}>
|
||||||
@@ -348,9 +392,35 @@ type CodeBlockWrapperProps = React.HTMLAttributes<HTMLPreElement> & {
|
|||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getMermaidInfo = (children: React.ReactNode): { isMermaid: boolean; source: string } => {
|
||||||
|
if (!React.isValidElement(children)) return { isMermaid: false, source: '' };
|
||||||
|
const props = children.props as Record<string, unknown> | undefined;
|
||||||
|
const className = typeof props?.className === 'string' ? props.className : '';
|
||||||
|
if (!className.includes('language-mermaid')) return { isMermaid: false, source: '' };
|
||||||
|
// Extract raw mermaid source from the code element's children
|
||||||
|
const codeChildren = props?.children;
|
||||||
|
let source = '';
|
||||||
|
if (typeof codeChildren === 'string') {
|
||||||
|
source = codeChildren;
|
||||||
|
} else if (React.isValidElement(codeChildren)) {
|
||||||
|
const innerProps = codeChildren.props as Record<string, unknown> | undefined;
|
||||||
|
if (typeof innerProps?.children === 'string') source = innerProps.children;
|
||||||
|
}
|
||||||
|
return { isMermaid: true, source };
|
||||||
|
};
|
||||||
|
|
||||||
const CodeBlockWrapper: React.FC<CodeBlockWrapperProps> = ({ children, className, style, ...props }) => {
|
const CodeBlockWrapper: React.FC<CodeBlockWrapperProps> = ({ children, className, style, ...props }) => {
|
||||||
const [copied, setCopied] = React.useState(false);
|
const [copied, setCopied] = React.useState(false);
|
||||||
const codeRef = React.useRef<HTMLDivElement>(null);
|
const codeRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
const mermaidInfo = getMermaidInfo(children);
|
||||||
|
const codeChild = React.useMemo(
|
||||||
|
() => (
|
||||||
|
React.isValidElement(children)
|
||||||
|
? React.cloneElement(children as React.ReactElement<Record<string, unknown>>, { 'data-block': true })
|
||||||
|
: children
|
||||||
|
),
|
||||||
|
[children],
|
||||||
|
);
|
||||||
|
|
||||||
const normalizedStyle = React.useMemo<React.CSSProperties | undefined>(() => {
|
const normalizedStyle = React.useMemo<React.CSSProperties | undefined>(() => {
|
||||||
if (!style) return style;
|
if (!style) return style;
|
||||||
@@ -396,6 +466,11 @@ const CodeBlockWrapper: React.FC<CodeBlockWrapperProps> = ({ children, className
|
|||||||
return next;
|
return next;
|
||||||
}, [style]);
|
}, [style]);
|
||||||
|
|
||||||
|
// Mermaid blocks get their own controls via MermaidWrapper — skip the code copy button.
|
||||||
|
if (mermaidInfo.isMermaid) {
|
||||||
|
return <MermaidWrapper source={mermaidInfo.source}>{codeChild}</MermaidWrapper>;
|
||||||
|
}
|
||||||
|
|
||||||
const getCodeContent = (): string => {
|
const getCodeContent = (): string => {
|
||||||
if (!codeRef.current) return '';
|
if (!codeRef.current) return '';
|
||||||
const codeEl = codeRef.current.querySelector('code');
|
const codeEl = codeRef.current.querySelector('code');
|
||||||
@@ -422,7 +497,7 @@ const CodeBlockWrapper: React.FC<CodeBlockWrapperProps> = ({ children, className
|
|||||||
className={cn(className)}
|
className={cn(className)}
|
||||||
style={normalizedStyle}
|
style={normalizedStyle}
|
||||||
>
|
>
|
||||||
{children}
|
{codeChild}
|
||||||
</pre>
|
</pre>
|
||||||
<div className="absolute top-1 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
<div className="absolute top-1 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
<button
|
<button
|
||||||
@@ -442,6 +517,88 @@ const streamdownComponents = {
|
|||||||
table: TableWrapper,
|
table: TableWrapper,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const streamdownPlugins = {
|
||||||
|
code,
|
||||||
|
mermaid,
|
||||||
|
};
|
||||||
|
|
||||||
|
const streamdownControls = {
|
||||||
|
code: false,
|
||||||
|
table: false,
|
||||||
|
mermaid: {
|
||||||
|
download: false,
|
||||||
|
copy: false,
|
||||||
|
fullscreen: false,
|
||||||
|
panZoom: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mermaid copy button — copies raw mermaid source
|
||||||
|
const MermaidCopyButton: React.FC<{ source: string }> = ({ source }) => {
|
||||||
|
const [copied, setCopied] = React.useState(false);
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
if (!source) return;
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(source);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy diagram:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={handleCopy}
|
||||||
|
className="p-1 rounded hover:bg-interactive-hover/60 text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
title="Copy diagram source"
|
||||||
|
>
|
||||||
|
{copied ? <RiCheckLine className="size-3.5" /> : <RiFileCopyLine className="size-3.5" />}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mermaid download button — downloads rendered SVG
|
||||||
|
const MermaidDownloadButton: React.FC<{ containerRef: React.RefObject<HTMLDivElement | null> }> = ({ containerRef }) => {
|
||||||
|
const handleDownload = () => {
|
||||||
|
const svgEl = containerRef.current?.querySelector('svg');
|
||||||
|
if (!(svgEl instanceof SVGElement)) return;
|
||||||
|
const serializer = new XMLSerializer();
|
||||||
|
let markup = serializer.serializeToString(svgEl);
|
||||||
|
if (!markup.includes('xmlns=')) {
|
||||||
|
markup = markup.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"');
|
||||||
|
}
|
||||||
|
downloadFile('diagram.svg', markup, 'image/svg+xml');
|
||||||
|
toast.success('Diagram downloaded');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={handleDownload}
|
||||||
|
className="p-1 rounded hover:bg-interactive-hover/60 text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
title="Download diagram"
|
||||||
|
>
|
||||||
|
<RiDownloadLine className="size-3.5" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mermaid wrapper with custom controls (same pattern as TableWrapper)
|
||||||
|
const MermaidWrapper: React.FC<{ children: React.ReactNode; source: string }> = ({ children, source }) => {
|
||||||
|
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="group relative" ref={containerRef}>
|
||||||
|
{children}
|
||||||
|
<div className="absolute top-1 right-2 z-20 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
<MermaidDownloadButton containerRef={containerRef} />
|
||||||
|
<MermaidCopyButton source={source} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export type MarkdownVariant = 'assistant' | 'tool';
|
export type MarkdownVariant = 'assistant' | 'tool';
|
||||||
|
|
||||||
interface MarkdownRendererProps {
|
interface MarkdownRendererProps {
|
||||||
@@ -464,6 +621,7 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
|||||||
variant = 'assistant',
|
variant = 'assistant',
|
||||||
}) => {
|
}) => {
|
||||||
const shikiThemes = useMarkdownShikiThemes();
|
const shikiThemes = useMarkdownShikiThemes();
|
||||||
|
const { options: mermaidOptions, mermaidRenderKey } = useStreamdownMermaidOptions();
|
||||||
const componentKey = React.useMemo(() => {
|
const componentKey = React.useMemo(() => {
|
||||||
const signature = part?.id ? `part-${part.id}` : `message-${messageId}`;
|
const signature = part?.id ? `part-${part.id}` : `message-${messageId}`;
|
||||||
return `markdown-${signature}`;
|
return `markdown-${signature}`;
|
||||||
@@ -476,12 +634,15 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
|||||||
const markdownContent = (
|
const markdownContent = (
|
||||||
<div className={cn('break-words', className)}>
|
<div className={cn('break-words', className)}>
|
||||||
<Streamdown
|
<Streamdown
|
||||||
mode={isStreaming ? 'streaming' : 'static'}
|
key={`streamdown-${componentKey}-${mermaidRenderKey}`}
|
||||||
shikiTheme={shikiThemes}
|
mode={isStreaming ? 'streaming' : 'static'}
|
||||||
className={streamdownClassName}
|
shikiTheme={shikiThemes}
|
||||||
controls={{ code: false, table: false }}
|
className={streamdownClassName}
|
||||||
components={streamdownComponents}
|
controls={streamdownControls}
|
||||||
>
|
plugins={streamdownPlugins}
|
||||||
|
mermaid={mermaidOptions}
|
||||||
|
components={streamdownComponents}
|
||||||
|
>
|
||||||
{content}
|
{content}
|
||||||
</Streamdown>
|
</Streamdown>
|
||||||
</div>
|
</div>
|
||||||
@@ -504,6 +665,7 @@ export const SimpleMarkdownRenderer: React.FC<{
|
|||||||
variant?: MarkdownVariant;
|
variant?: MarkdownVariant;
|
||||||
}> = ({ content, className, variant = 'assistant' }) => {
|
}> = ({ content, className, variant = 'assistant' }) => {
|
||||||
const shikiThemes = useMarkdownShikiThemes();
|
const shikiThemes = useMarkdownShikiThemes();
|
||||||
|
const { options: mermaidOptions, mermaidRenderKey } = useStreamdownMermaidOptions();
|
||||||
|
|
||||||
const streamdownClassName = variant === 'tool'
|
const streamdownClassName = variant === 'tool'
|
||||||
? 'streamdown-content streamdown-tool'
|
? 'streamdown-content streamdown-tool'
|
||||||
@@ -512,10 +674,13 @@ export const SimpleMarkdownRenderer: React.FC<{
|
|||||||
return (
|
return (
|
||||||
<div className={cn('break-words', className)}>
|
<div className={cn('break-words', className)}>
|
||||||
<Streamdown
|
<Streamdown
|
||||||
|
key={`streamdown-simple-${mermaidRenderKey}`}
|
||||||
mode="static"
|
mode="static"
|
||||||
shikiTheme={shikiThemes}
|
shikiTheme={shikiThemes}
|
||||||
className={streamdownClassName}
|
className={streamdownClassName}
|
||||||
controls={{ code: false, table: false }}
|
controls={streamdownControls}
|
||||||
|
plugins={streamdownPlugins}
|
||||||
|
mermaid={mermaidOptions}
|
||||||
components={streamdownComponents}
|
components={streamdownComponents}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
|
|||||||
@@ -243,19 +243,25 @@ const isActivityStandaloneTool = (toolName: unknown): boolean => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const extractFinalAssistantText = (turn: Turn): string | undefined => {
|
const extractFinalAssistantText = (turn: Turn): string | undefined => {
|
||||||
for (const assistantMsg of turn.assistantMessages) {
|
for (let messageIndex = turn.assistantMessages.length - 1; messageIndex >= 0; messageIndex -= 1) {
|
||||||
|
const assistantMsg = turn.assistantMessages[messageIndex];
|
||||||
|
if (!assistantMsg) continue;
|
||||||
|
|
||||||
const infoFinish = (assistantMsg.info as { finish?: string | null | undefined }).finish;
|
const infoFinish = (assistantMsg.info as { finish?: string | null | undefined }).finish;
|
||||||
if (infoFinish === 'stop') {
|
if (infoFinish !== 'stop') continue;
|
||||||
const textPart = assistantMsg.parts.find(p => p.type === 'text');
|
|
||||||
if (textPart) {
|
for (let partIndex = assistantMsg.parts.length - 1; partIndex >= 0; partIndex -= 1) {
|
||||||
const textContent = (textPart as { text?: string | null | undefined }).text ??
|
const part = assistantMsg.parts[partIndex];
|
||||||
(textPart as { content?: string | null | undefined }).content;
|
if (!part || part.type !== 'text') continue;
|
||||||
if (typeof textContent === 'string' && textContent.trim().length > 0) {
|
|
||||||
return textContent;
|
const textContent = (part as { text?: string | null | undefined }).text ??
|
||||||
}
|
(part as { content?: string | null | undefined }).content;
|
||||||
|
if (typeof textContent === 'string' && textContent.trim().length > 0) {
|
||||||
|
return textContent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -102,21 +102,25 @@ export const detectTurns = (messages: ChatMessageEntry[]): Turn[] => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const extractFinalAssistantText = (turn: Turn): string | undefined => {
|
const extractFinalAssistantText = (turn: Turn): string | undefined => {
|
||||||
|
for (let messageIndex = turn.assistantMessages.length - 1; messageIndex >= 0; messageIndex -= 1) {
|
||||||
|
const assistantMsg = turn.assistantMessages[messageIndex];
|
||||||
|
if (!assistantMsg) continue;
|
||||||
|
|
||||||
for (const assistantMsg of turn.assistantMessages) {
|
|
||||||
const infoFinish = (assistantMsg.info as { finish?: string | null | undefined }).finish;
|
const infoFinish = (assistantMsg.info as { finish?: string | null | undefined }).finish;
|
||||||
|
if (infoFinish !== 'stop') continue;
|
||||||
|
|
||||||
if (infoFinish === 'stop') {
|
for (let partIndex = assistantMsg.parts.length - 1; partIndex >= 0; partIndex -= 1) {
|
||||||
const textPart = assistantMsg.parts.find(p => p.type === 'text');
|
const part = assistantMsg.parts[partIndex];
|
||||||
if (textPart) {
|
if (!part || part.type !== 'text') continue;
|
||||||
const textContent = (textPart as { text?: string | null | undefined }).text ??
|
|
||||||
(textPart as { content?: string | null | undefined }).content;
|
const textContent = (part as { text?: string | null | undefined }).text ??
|
||||||
if (typeof textContent === 'string' && textContent.trim().length > 0) {
|
(part as { content?: string | null | undefined }).content;
|
||||||
return textContent;
|
if (typeof textContent === 'string' && textContent.trim().length > 0) {
|
||||||
}
|
return textContent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
renderWebSearchOutput,
|
renderWebSearchOutput,
|
||||||
formatInputForDisplay,
|
formatInputForDisplay,
|
||||||
parseDiffToUnified,
|
parseDiffToUnified,
|
||||||
|
parseReadToolOutput,
|
||||||
type UnifiedDiffHunk,
|
type UnifiedDiffHunk,
|
||||||
type SideBySideDiffHunk,
|
type SideBySideDiffHunk,
|
||||||
type SideBySideDiffLine,
|
type SideBySideDiffLine,
|
||||||
@@ -740,33 +741,41 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tool === 'read') {
|
if (tool === 'read') {
|
||||||
const lines = popup.content.split('\n');
|
const parsedReadOutput = parseReadToolOutput(popup.content);
|
||||||
|
|
||||||
const inputMeta = popup.metadata?.input;
|
const inputMeta = popup.metadata?.input;
|
||||||
const inputObj = typeof inputMeta === 'object' && inputMeta !== null ? (inputMeta as Record<string, unknown>) : {};
|
const inputObj = typeof inputMeta === 'object' && inputMeta !== null ? (inputMeta as Record<string, unknown>) : {};
|
||||||
const offset = typeof inputObj.offset === 'number' ? inputObj.offset : 0;
|
const offset = typeof inputObj.offset === 'number' ? inputObj.offset : 0;
|
||||||
const limit = typeof inputObj.limit === 'number' ? inputObj.limit : undefined;
|
let fallbackLineCursor = offset;
|
||||||
|
const hasExplicitLineNumbers = parsedReadOutput.lines.some((line) => line.lineNumber !== null);
|
||||||
const isInfoMessage = (line: string) => line.trim().startsWith('(');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{lines.map((line: string, idx: number) => {
|
{parsedReadOutput.lines.map((line, idx: number) => {
|
||||||
|
if (line.lineNumber !== null) {
|
||||||
|
fallbackLineCursor = line.lineNumber;
|
||||||
|
}
|
||||||
|
|
||||||
const isInfo = isInfoMessage(line);
|
const shouldAssignFallbackLineNumber =
|
||||||
|
parsedReadOutput.type === 'file'
|
||||||
|
&& !hasExplicitLineNumbers
|
||||||
|
&& line.lineNumber === null
|
||||||
|
&& !line.isInfo;
|
||||||
|
|
||||||
const lineNumber = offset + idx + 1;
|
const effectiveLineNumber = line.lineNumber ?? (shouldAssignFallbackLineNumber
|
||||||
|
? (fallbackLineCursor += 1)
|
||||||
|
: null);
|
||||||
|
|
||||||
const shouldShowLineNumber = !isInfo && (limit === undefined || idx < limit);
|
const shouldShowLineNumber = !line.isInfo && effectiveLineNumber !== null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={idx} className={`typography-code font-mono flex ${isInfo ? 'text-muted-foreground/70 italic' : ''}`}>
|
<div key={idx} className={`typography-code font-mono flex ${line.isInfo ? 'text-muted-foreground/70 italic' : ''}`}>
|
||||||
<span className="w-12 flex-shrink-0 text-right pr-3 select-none border-r mr-3 -my-0.5 py-0.5" style={{ color: 'var(--tools-edit-line-number)', borderColor: 'var(--tools-border)' }}>
|
<span className="w-12 flex-shrink-0 text-right pr-3 select-none border-r mr-3 -my-0.5 py-0.5" style={{ color: 'var(--tools-edit-line-number)', borderColor: 'var(--tools-border)' }}>
|
||||||
{shouldShowLineNumber ? lineNumber : ''}
|
{shouldShowLineNumber ? effectiveLineNumber : ''}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
{isInfo ? (
|
{line.isInfo ? (
|
||||||
<div className="whitespace-pre-wrap break-words">{line}</div>
|
<div className="whitespace-pre-wrap break-words">{line.text}</div>
|
||||||
) : (
|
) : (
|
||||||
<SyntaxHighlighter
|
<SyntaxHighlighter
|
||||||
style={syntaxTheme}
|
style={syntaxTheme}
|
||||||
@@ -794,7 +803,7 @@ const ToolOutputDialog: React.FC<ToolOutputDialogProps> = ({ popup, onOpenChange
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{line}
|
{line.text}
|
||||||
</SyntaxHighlighter>
|
</SyntaxHighlighter>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
formatEditOutput,
|
formatEditOutput,
|
||||||
detectLanguageFromOutput,
|
detectLanguageFromOutput,
|
||||||
formatInputForDisplay,
|
formatInputForDisplay,
|
||||||
|
parseReadToolOutput,
|
||||||
} from '../toolRenderers';
|
} from '../toolRenderers';
|
||||||
|
|
||||||
type ToolStateWithMetadata = ToolStateUnion & { metadata?: Record<string, unknown>; input?: Record<string, unknown>; output?: string; error?: string; time?: { start: number; end?: number } };
|
type ToolStateWithMetadata = ToolStateUnion & { metadata?: Record<string, unknown>; input?: Record<string, unknown>; output?: string; error?: string; time?: { start: number; end?: number } };
|
||||||
@@ -808,31 +809,40 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = React.memo(({
|
|||||||
|
|
||||||
if (hasStringOutput && outputString.trim()) {
|
if (hasStringOutput && outputString.trim()) {
|
||||||
if (part.tool === 'read') {
|
if (part.tool === 'read') {
|
||||||
const formattedOutput = formatEditOutput(outputString, part.tool, metadata);
|
const parsedReadOutput = parseReadToolOutput(outputString);
|
||||||
const lines = formattedOutput.split('\n');
|
|
||||||
const offset = typeof input?.offset === 'number' ? input.offset : 0;
|
const offset = typeof input?.offset === 'number' ? input.offset : 0;
|
||||||
const limit = typeof input?.limit === 'number' ? input.limit : undefined;
|
const contentForLanguage = parsedReadOutput.lines.map((line) => line.text).join('\n');
|
||||||
const isInfoMessage = (line: string) => line.trim().startsWith('(');
|
let fallbackLineCursor = offset;
|
||||||
|
const hasExplicitLineNumbers = parsedReadOutput.lines.some((line) => line.lineNumber !== null);
|
||||||
|
|
||||||
return renderScrollableBlock(
|
return renderScrollableBlock(
|
||||||
<div className="typography-code w-full min-w-0 space-y-1">
|
<div className="typography-code w-full min-w-0 space-y-1">
|
||||||
{lines.map((line: string, idx: number) => {
|
{parsedReadOutput.lines.map((line, idx) => {
|
||||||
const isInfo = isInfoMessage(line);
|
if (line.lineNumber !== null) {
|
||||||
const lineNumber = offset + idx + 1;
|
fallbackLineCursor = line.lineNumber;
|
||||||
const shouldShowLineNumber = !isInfo && (limit === undefined || idx < limit);
|
}
|
||||||
|
const shouldAssignFallbackLineNumber =
|
||||||
|
parsedReadOutput.type === 'file'
|
||||||
|
&& !hasExplicitLineNumbers
|
||||||
|
&& line.lineNumber === null
|
||||||
|
&& !line.isInfo;
|
||||||
|
const effectiveLineNumber = line.lineNumber ?? (shouldAssignFallbackLineNumber
|
||||||
|
? (fallbackLineCursor += 1)
|
||||||
|
: null);
|
||||||
|
const shouldShowLineNumber = !line.isInfo && effectiveLineNumber !== null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={idx} className={cn('typography-code font-mono flex w-full min-w-0', isInfo && 'text-muted-foreground/70 italic')}>
|
<div key={idx} className={cn('typography-code font-mono flex w-full min-w-0', line.isInfo && 'text-muted-foreground/70 italic')}>
|
||||||
<span className="w-10 flex-shrink-0 text-right pr-3 select-none border-r mr-3 -my-0.5 py-0.5" style={{ color: 'var(--tools-edit-line-number)', borderColor: 'var(--tools-border)' }}>
|
<span className="w-10 flex-shrink-0 text-right pr-3 select-none border-r mr-3 -my-0.5 py-0.5" style={{ color: 'var(--tools-edit-line-number)', borderColor: 'var(--tools-border)' }}>
|
||||||
{shouldShowLineNumber ? lineNumber : ''}
|
{shouldShowLineNumber ? effectiveLineNumber : ''}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
{isInfo ? (
|
{line.isInfo ? (
|
||||||
<div className="whitespace-pre-wrap break-words">{line}</div>
|
<div className="whitespace-pre-wrap break-words">{line.text}</div>
|
||||||
) : (
|
) : (
|
||||||
<SyntaxHighlighter
|
<SyntaxHighlighter
|
||||||
style={syntaxTheme}
|
style={syntaxTheme}
|
||||||
language={detectLanguageFromOutput(formattedOutput, part.tool, input as Record<string, unknown>)}
|
language={detectLanguageFromOutput(contentForLanguage, part.tool, input as Record<string, unknown>)}
|
||||||
PreTag="div"
|
PreTag="div"
|
||||||
wrapLines
|
wrapLines
|
||||||
wrapLongLines
|
wrapLongLines
|
||||||
@@ -855,7 +865,7 @@ const ToolExpandedContent: React.FC<ToolExpandedContentProps> = React.memo(({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{line}
|
{line.text}
|
||||||
</SyntaxHighlighter>
|
</SyntaxHighlighter>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1073,6 +1083,7 @@ const ToolPart: React.FC<ToolPartProps> = ({ part, isExpanded, onToggle, syntaxT
|
|||||||
const showTextJustificationActivity = useUIStore((state) => state.showTextJustificationActivity);
|
const showTextJustificationActivity = useUIStore((state) => state.showTextJustificationActivity);
|
||||||
const justificationText = React.useMemo(() => {
|
const justificationText = React.useMemo(() => {
|
||||||
if (!showTextJustificationActivity) return null;
|
if (!showTextJustificationActivity) return null;
|
||||||
|
if (part.tool === 'apply_patch') return null;
|
||||||
// Get title or description from state - this is the "yapping" text like "Shows system information"
|
// Get title or description from state - this is the "yapping" text like "Shows system information"
|
||||||
const title = (stateWithData as { title?: string }).title;
|
const title = (stateWithData as { title?: string }).title;
|
||||||
if (typeof title === 'string' && title.trim().length > 0) {
|
if (typeof title === 'string' && title.trim().length > 0) {
|
||||||
@@ -1083,7 +1094,7 @@ const ToolPart: React.FC<ToolPartProps> = ({ part, isExpanded, onToggle, syntaxT
|
|||||||
return inputDesc;
|
return inputDesc;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [showTextJustificationActivity, stateWithData, input]);
|
}, [showTextJustificationActivity, part.tool, stateWithData, input]);
|
||||||
|
|
||||||
const runtime = React.useContext(RuntimeAPIContext);
|
const runtime = React.useContext(RuntimeAPIContext);
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,81 @@ export const formatEditOutput = (output: string, toolName: string, metadata?: Re
|
|||||||
return cleaned;
|
return cleaned;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface ParsedReadOutputLine {
|
||||||
|
text: string;
|
||||||
|
lineNumber: number | null;
|
||||||
|
isInfo: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ParsedReadToolOutput {
|
||||||
|
type: 'file' | 'directory' | 'unknown';
|
||||||
|
lines: ParsedReadOutputLine[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const parseReadToolOutput = (output: string): ParsedReadToolOutput => {
|
||||||
|
const typeMatch = output.match(/<type>(file|directory)<\/type>/i);
|
||||||
|
const detectedType = (typeMatch?.[1]?.toLowerCase() ?? 'unknown') as ParsedReadToolOutput['type'];
|
||||||
|
|
||||||
|
const contentMatch = output.match(/<content>([\s\S]*?)<\/content>/i);
|
||||||
|
const rawContent = contentMatch?.[1] ?? output;
|
||||||
|
const normalizedContent = rawContent.replace(/\r\n/g, '\n');
|
||||||
|
const rawLines = normalizedContent.split('\n');
|
||||||
|
|
||||||
|
const isTruncationInfoLine = (text: string): boolean => {
|
||||||
|
return /\(\s*File has more lines\..*offset.*\)/i.test(text.trim());
|
||||||
|
};
|
||||||
|
|
||||||
|
const parsedLines = rawLines.map((line): ParsedReadOutputLine => {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
const isInfo = (trimmed.startsWith('(') && trimmed.endsWith(')')) || isTruncationInfoLine(trimmed);
|
||||||
|
|
||||||
|
if (detectedType !== 'directory') {
|
||||||
|
const numberedMatch = line.match(/^(\d+):\s?(.*)$/);
|
||||||
|
if (numberedMatch) {
|
||||||
|
const numberedText = numberedMatch[2];
|
||||||
|
const numberedTrimmed = numberedText.trim();
|
||||||
|
const numberedIsInfo =
|
||||||
|
(numberedTrimmed.startsWith('(') && numberedTrimmed.endsWith(')'))
|
||||||
|
|| isTruncationInfoLine(numberedTrimmed);
|
||||||
|
return {
|
||||||
|
lineNumber: numberedIsInfo ? null : Number(numberedMatch[1]),
|
||||||
|
text: numberedText,
|
||||||
|
isInfo: numberedIsInfo,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
lineNumber: null,
|
||||||
|
text: line,
|
||||||
|
isInfo,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const lines = parsedLines.filter((line, index, arr) => {
|
||||||
|
if (line.text.trim().length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prev = arr[index - 1];
|
||||||
|
const next = arr[index + 1];
|
||||||
|
const adjacentToInfo = Boolean(prev?.isInfo || next?.isInfo);
|
||||||
|
const hasNumber = line.lineNumber !== null;
|
||||||
|
|
||||||
|
// Drop numbered blank lines wrapped around helper/info rows.
|
||||||
|
if (adjacentToInfo && hasNumber) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: detectedType,
|
||||||
|
lines,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const renderListOutput = (output: string, options?: { unstyled?: boolean }) => {
|
export const renderListOutput = (output: string, options?: { unstyled?: boolean }) => {
|
||||||
try {
|
try {
|
||||||
const lines = output.trim().split('\n').filter(Boolean);
|
const lines = output.trim().split('\n').filter(Boolean);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useFilesViewTabsStore } from '@/stores/useFilesViewTabsStore';
|
import { useFilesViewTabsStore } from '@/stores/useFilesViewTabsStore';
|
||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
|
import { ContextPanelContent } from './ContextSidebarTab';
|
||||||
|
|
||||||
const CONTEXT_PANEL_MIN_WIDTH = 360;
|
const CONTEXT_PANEL_MIN_WIDTH = 360;
|
||||||
const CONTEXT_PANEL_MAX_WIDTH = 1400;
|
const CONTEXT_PANEL_MAX_WIDTH = 1400;
|
||||||
@@ -144,14 +145,16 @@ export const ContextPanel: React.FC = () => {
|
|||||||
|
|
||||||
const activeFilePath = useFilesViewTabsStore((state) => (directoryKey ? (state.byRoot[directoryKey]?.selectedPath ?? null) : null));
|
const activeFilePath = useFilesViewTabsStore((state) => (directoryKey ? (state.byRoot[directoryKey]?.selectedPath ?? null) : null));
|
||||||
|
|
||||||
const panelTitle = panelState?.mode === 'diff' ? 'Diff' : panelState?.mode === 'file' ? 'File' : 'Panel';
|
const panelTitle = panelState?.mode === 'diff' ? 'Diff' : panelState?.mode === 'file' ? 'File' : panelState?.mode === 'context' ? 'Context' : 'Panel';
|
||||||
const effectivePath = panelState?.mode === 'file' ? (activeFilePath ?? panelState?.targetPath ?? null) : (panelState?.targetPath ?? null);
|
const effectivePath = panelState?.mode === 'file' ? (activeFilePath ?? panelState?.targetPath ?? null) : panelState?.mode === 'context' ? null : (panelState?.targetPath ?? null);
|
||||||
const pathLabel = getRelativePathLabel(effectivePath, effectiveDirectory);
|
const pathLabel = getRelativePathLabel(effectivePath, effectiveDirectory);
|
||||||
|
|
||||||
const content = panelState?.mode === 'diff'
|
const content = panelState?.mode === 'diff'
|
||||||
? <DiffView hideStackedFileSidebar stackedDefaultCollapsedAll hideFileSelector pinSelectedFileHeaderToTopOnNavigate />
|
? <DiffView hideStackedFileSidebar stackedDefaultCollapsedAll hideFileSelector pinSelectedFileHeaderToTopOnNavigate />
|
||||||
: panelState?.mode === 'file'
|
: panelState?.mode === 'file'
|
||||||
? <FilesView mode="editor-only" />
|
? <FilesView mode="editor-only" />
|
||||||
|
: panelState?.mode === 'context'
|
||||||
|
? <ContextPanelContent />
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const header = (
|
const header = (
|
||||||
|
|||||||
@@ -0,0 +1,606 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import type { Message, Part } from '@opencode-ai/sdk/v2';
|
||||||
|
import { RiCheckLine, RiFileCopyLine } from '@remixicon/react';
|
||||||
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||||
|
|
||||||
|
import { deriveMessageRole } from '@/components/chat/message/messageRole';
|
||||||
|
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
||||||
|
import { generateSyntaxTheme } from '@/lib/theme/syntaxThemeGenerator';
|
||||||
|
import { useConfigStore } from '@/stores/useConfigStore';
|
||||||
|
import { useSessionStore } from '@/stores/useSessionStore';
|
||||||
|
|
||||||
|
type SessionMessage = { info: Message; parts: Part[] };
|
||||||
|
|
||||||
|
const EMPTY_SESSION_MESSAGES: SessionMessage[] = [];
|
||||||
|
|
||||||
|
type ProviderModelLike = {
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
limit?: { context?: number };
|
||||||
|
};
|
||||||
|
|
||||||
|
type ProviderLike = {
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
models?: ProviderModelLike[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type TokenBreakdown = {
|
||||||
|
input: number;
|
||||||
|
output: number;
|
||||||
|
reasoning: number;
|
||||||
|
cacheRead: number;
|
||||||
|
cacheWrite: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ContextBuckets = {
|
||||||
|
user: number;
|
||||||
|
assistant: number;
|
||||||
|
tool: number;
|
||||||
|
other: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EMPTY_BREAKDOWN: TokenBreakdown = {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
reasoning: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const EMPTY_BUCKETS: ContextBuckets = {
|
||||||
|
user: 0,
|
||||||
|
assistant: 0,
|
||||||
|
tool: 0,
|
||||||
|
other: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const toNonNegativeNumber = (value: unknown): number => {
|
||||||
|
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractTokenBreakdown = (message: SessionMessage): TokenBreakdown => {
|
||||||
|
const tokenCandidate = (message.info as { tokens?: unknown }).tokens;
|
||||||
|
const source =
|
||||||
|
tokenCandidate !== undefined
|
||||||
|
? tokenCandidate
|
||||||
|
: (message.parts.find((part) => (part as { tokens?: unknown }).tokens !== undefined) as { tokens?: unknown } | undefined)?.tokens;
|
||||||
|
|
||||||
|
if (typeof source === 'number') {
|
||||||
|
return {
|
||||||
|
...EMPTY_BREAKDOWN,
|
||||||
|
total: toNonNegativeNumber(source),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!source || typeof source !== 'object') {
|
||||||
|
return EMPTY_BREAKDOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
const breakdown = source as {
|
||||||
|
input?: unknown;
|
||||||
|
output?: unknown;
|
||||||
|
reasoning?: unknown;
|
||||||
|
cache?: { read?: unknown; write?: unknown };
|
||||||
|
};
|
||||||
|
|
||||||
|
const input = toNonNegativeNumber(breakdown.input);
|
||||||
|
const output = toNonNegativeNumber(breakdown.output);
|
||||||
|
const reasoning = toNonNegativeNumber(breakdown.reasoning);
|
||||||
|
const cacheRead = toNonNegativeNumber(breakdown.cache?.read);
|
||||||
|
const cacheWrite = toNonNegativeNumber(breakdown.cache?.write);
|
||||||
|
|
||||||
|
return {
|
||||||
|
input,
|
||||||
|
output,
|
||||||
|
reasoning,
|
||||||
|
cacheRead,
|
||||||
|
cacheWrite,
|
||||||
|
total: input + output + reasoning + cacheRead + cacheWrite,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const pickString = (...values: unknown[]): string => {
|
||||||
|
for (const value of values) {
|
||||||
|
if (typeof value === 'string' && value.trim().length > 0) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const estimateTextLength = (value: unknown): number => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value.length;
|
||||||
|
}
|
||||||
|
if (typeof value === 'number' || typeof value === 'boolean') {
|
||||||
|
return String(value).length;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.reduce((sum, item) => sum + estimateTextLength(item), 0);
|
||||||
|
}
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
return Object.values(value as Record<string, unknown>).reduce<number>((sum, item) => sum + estimateTextLength(item), 0);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const estimatePartChars = (part: Part, role: 'user' | 'assistant' | 'tool' | 'other'): ContextBuckets => {
|
||||||
|
const partRecord = part as Record<string, unknown>;
|
||||||
|
const type = typeof partRecord.type === 'string' ? partRecord.type : '';
|
||||||
|
|
||||||
|
if (type === 'reasoning') {
|
||||||
|
return {
|
||||||
|
...EMPTY_BUCKETS,
|
||||||
|
assistant: estimateTextLength(partRecord.text) + estimateTextLength(partRecord.content),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const directText = pickString(
|
||||||
|
partRecord.text,
|
||||||
|
partRecord.content,
|
||||||
|
partRecord.value,
|
||||||
|
(partRecord.source as { value?: unknown; text?: { value?: unknown } } | undefined)?.value,
|
||||||
|
(partRecord.source as { value?: unknown; text?: { value?: unknown } } | undefined)?.text?.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (type === 'tool' || role === 'tool') {
|
||||||
|
const toolInputOutputLength =
|
||||||
|
estimateTextLength(partRecord.input)
|
||||||
|
+ estimateTextLength(partRecord.output)
|
||||||
|
+ estimateTextLength(partRecord.error)
|
||||||
|
+ estimateTextLength((partRecord.call as { input?: unknown; output?: unknown; error?: unknown } | undefined)?.input)
|
||||||
|
+ estimateTextLength((partRecord.call as { input?: unknown; output?: unknown; error?: unknown } | undefined)?.output)
|
||||||
|
+ estimateTextLength((partRecord.call as { input?: unknown; output?: unknown; error?: unknown } | undefined)?.error);
|
||||||
|
|
||||||
|
const toolPayloadLength =
|
||||||
|
toolInputOutputLength
|
||||||
|
+ estimateTextLength(partRecord.raw)
|
||||||
|
+ Math.round(estimateTextLength(partRecord.metadata) * 0.25)
|
||||||
|
+ Math.round(estimateTextLength(partRecord.state) * 0.1);
|
||||||
|
|
||||||
|
return { user: 0, assistant: 0, tool: toolPayloadLength, other: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role === 'user') {
|
||||||
|
return { user: directText.length, assistant: 0, tool: 0, other: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role === 'assistant') {
|
||||||
|
return { user: 0, assistant: directText.length, tool: 0, other: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { user: 0, assistant: 0, tool: 0, other: directText.length };
|
||||||
|
};
|
||||||
|
|
||||||
|
const addBuckets = (target: ContextBuckets, value: ContextBuckets): ContextBuckets => ({
|
||||||
|
user: target.user + value.user,
|
||||||
|
assistant: target.assistant + value.assistant,
|
||||||
|
tool: target.tool + value.tool,
|
||||||
|
other: target.other + value.other,
|
||||||
|
});
|
||||||
|
|
||||||
|
const deriveRoleBucket = (message: SessionMessage): 'user' | 'assistant' | 'tool' | 'other' => {
|
||||||
|
const roleInfo = deriveMessageRole(message.info);
|
||||||
|
if (roleInfo.isUser) return 'user';
|
||||||
|
if (roleInfo.role === 'assistant') return 'assistant';
|
||||||
|
if (roleInfo.role === 'tool') return 'tool';
|
||||||
|
return 'other';
|
||||||
|
};
|
||||||
|
|
||||||
|
const computeContextBreakdown = (
|
||||||
|
sessionMessages: SessionMessage[],
|
||||||
|
systemPrompt: string,
|
||||||
|
): ContextBuckets => {
|
||||||
|
if (sessionMessages.length === 0) {
|
||||||
|
return { ...EMPTY_BUCKETS };
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalChars = sessionMessages.reduce<ContextBuckets>((acc, message) => {
|
||||||
|
const role = deriveRoleBucket(message);
|
||||||
|
let bucket = { ...EMPTY_BUCKETS };
|
||||||
|
for (const part of message.parts) {
|
||||||
|
bucket = addBuckets(bucket, estimatePartChars(part, role));
|
||||||
|
}
|
||||||
|
return addBuckets(acc, bucket);
|
||||||
|
}, { ...EMPTY_BUCKETS });
|
||||||
|
|
||||||
|
totalChars.user += systemPrompt.length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
user: Math.ceil(totalChars.user / 4),
|
||||||
|
assistant: Math.ceil(totalChars.assistant / 4),
|
||||||
|
tool: Math.ceil(totalChars.tool / 4),
|
||||||
|
other: Math.ceil(totalChars.other / 4),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatNumber = (value: number): string => value.toLocaleString();
|
||||||
|
|
||||||
|
const formatMoney = (value: number): string => {
|
||||||
|
if (!Number.isFinite(value) || value <= 0) return '$0.00';
|
||||||
|
if (value < 0.01) return `$${value.toFixed(4)}`;
|
||||||
|
return `$${value.toFixed(2)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDateTime = (timestamp: number | null): string => {
|
||||||
|
if (!timestamp || !Number.isFinite(timestamp)) return '-';
|
||||||
|
const value = new Date(timestamp).toLocaleString(undefined, {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
return value.replace(/, (\d{1,2}:\d{2} [AP]M)$/, ' at $1');
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatMessageDateMeta = (timestamp: number | null): string => {
|
||||||
|
if (!timestamp || !Number.isFinite(timestamp)) return '-';
|
||||||
|
return new Date(timestamp).toLocaleString(undefined, {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const capitalizeRole = (role: string): string => {
|
||||||
|
if (!role) return role;
|
||||||
|
return `${role[0].toUpperCase()}${role.slice(1)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveProviderAndModel = (
|
||||||
|
providers: ProviderLike[],
|
||||||
|
providerID: string,
|
||||||
|
modelID: string,
|
||||||
|
): { providerName: string; modelName: string; contextLimit: number | null } => {
|
||||||
|
const provider = providers.find((entry) => entry.id === providerID);
|
||||||
|
const model = provider?.models?.find((entry) => entry.id === modelID);
|
||||||
|
|
||||||
|
return {
|
||||||
|
providerName: provider?.name || providerID || '-',
|
||||||
|
modelName: model?.name || modelID || '-',
|
||||||
|
contextLimit: typeof model?.limit?.context === 'number' ? model.limit.context : null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ContextPanelContent: React.FC = () => {
|
||||||
|
const { currentTheme } = useThemeSystem();
|
||||||
|
const syntaxTheme = React.useMemo(() => generateSyntaxTheme(currentTheme), [currentTheme]);
|
||||||
|
const [expandedRawMessages, setExpandedRawMessages] = React.useState<Record<string, boolean>>({});
|
||||||
|
const [copiedRawMessageId, setCopiedRawMessageId] = React.useState<string | null>(null);
|
||||||
|
const copyResetTimeoutRef = React.useRef<number | null>(null);
|
||||||
|
const currentSessionId = useSessionStore((state) => state.currentSessionId);
|
||||||
|
const sessions = useSessionStore((state) => state.sessions);
|
||||||
|
const sessionMessages = useSessionStore((state) => {
|
||||||
|
if (!state.currentSessionId) return EMPTY_SESSION_MESSAGES;
|
||||||
|
return state.messages.get(state.currentSessionId) ?? EMPTY_SESSION_MESSAGES;
|
||||||
|
});
|
||||||
|
const providers = useConfigStore((state) => state.providers);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (copyResetTimeoutRef.current !== null) {
|
||||||
|
window.clearTimeout(copyResetTimeoutRef.current);
|
||||||
|
copyResetTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
setExpandedRawMessages((prev) => (Object.keys(prev).length > 0 ? {} : prev));
|
||||||
|
setCopiedRawMessageId(null);
|
||||||
|
}, [currentSessionId]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (copyResetTimeoutRef.current !== null) {
|
||||||
|
window.clearTimeout(copyResetTimeoutRef.current);
|
||||||
|
copyResetTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCopyRawMessage = React.useCallback(async (messageId: string, value: string) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(value);
|
||||||
|
setCopiedRawMessageId(messageId);
|
||||||
|
if (copyResetTimeoutRef.current !== null) {
|
||||||
|
window.clearTimeout(copyResetTimeoutRef.current);
|
||||||
|
}
|
||||||
|
copyResetTimeoutRef.current = window.setTimeout(() => {
|
||||||
|
setCopiedRawMessageId((prev) => (prev === messageId ? null : prev));
|
||||||
|
copyResetTimeoutRef.current = null;
|
||||||
|
}, 2000);
|
||||||
|
} catch {
|
||||||
|
setCopiedRawMessageId(null);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const viewModel = React.useMemo(() => {
|
||||||
|
const currentSession = currentSessionId ? sessions.find((session) => session.id === currentSessionId) ?? null : null;
|
||||||
|
|
||||||
|
const assistantMessages = sessionMessages.filter((entry) => deriveMessageRole(entry.info).role === 'assistant');
|
||||||
|
const userMessages = sessionMessages.filter((entry) => deriveMessageRole(entry.info).isUser);
|
||||||
|
|
||||||
|
let contextMessage: SessionMessage | null = null;
|
||||||
|
for (let i = assistantMessages.length - 1; i >= 0; i -= 1) {
|
||||||
|
const message = assistantMessages[i];
|
||||||
|
if (extractTokenBreakdown(message).total > 0) {
|
||||||
|
contextMessage = message;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenBreakdown = contextMessage ? extractTokenBreakdown(contextMessage) : EMPTY_BREAKDOWN;
|
||||||
|
|
||||||
|
const totalAssistantCost = assistantMessages.reduce((sum, message) => {
|
||||||
|
const cost = toNonNegativeNumber((message.info as { cost?: unknown }).cost);
|
||||||
|
return sum + cost;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
const latestAssistantInfo = (contextMessage?.info ?? null) as (Message & { providerID?: string; modelID?: string }) | null;
|
||||||
|
const providerModel = resolveProviderAndModel(
|
||||||
|
providers as ProviderLike[],
|
||||||
|
latestAssistantInfo?.providerID || '',
|
||||||
|
latestAssistantInfo?.modelID || '',
|
||||||
|
);
|
||||||
|
|
||||||
|
const contextLimit = providerModel.contextLimit;
|
||||||
|
const usagePercent = contextLimit && contextLimit > 0
|
||||||
|
? Math.min(999, (tokenBreakdown.total / contextLimit) * 100)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const systemPrompt = ([...sessionMessages].reverse().find(
|
||||||
|
(entry) => deriveMessageRole(entry.info).isUser && typeof (entry.info as { system?: unknown }).system === 'string',
|
||||||
|
)?.info as { system?: string } | undefined)?.system || '';
|
||||||
|
|
||||||
|
const computedBreakdown = computeContextBreakdown(sessionMessages, systemPrompt);
|
||||||
|
|
||||||
|
const userTokens = computedBreakdown.user;
|
||||||
|
const assistantTokens = computedBreakdown.assistant;
|
||||||
|
const toolTokens = computedBreakdown.tool;
|
||||||
|
const otherTokens = Math.max(0, tokenBreakdown.input - userTokens - assistantTokens - toolTokens);
|
||||||
|
const breakdownTotal = userTokens + assistantTokens + toolTokens + otherTokens;
|
||||||
|
|
||||||
|
const firstMessageTs = sessionMessages[0]?.info?.time?.created;
|
||||||
|
const lastMessageTs = sessionMessages.length > 0
|
||||||
|
? sessionMessages[sessionMessages.length - 1]?.info?.time?.created
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
sessionTitle: currentSession?.title || 'Untitled Session',
|
||||||
|
messagesCount: sessionMessages.length,
|
||||||
|
userMessagesCount: userMessages.length,
|
||||||
|
assistantMessagesCount: assistantMessages.length,
|
||||||
|
createdAt: (currentSession?.time?.created ?? firstMessageTs ?? null) as number | null,
|
||||||
|
lastActivityAt: (lastMessageTs ?? currentSession?.time?.created ?? null) as number | null,
|
||||||
|
providerModel,
|
||||||
|
tokenBreakdown,
|
||||||
|
usagePercent,
|
||||||
|
totalAssistantCost,
|
||||||
|
contextLimit,
|
||||||
|
breakdown: {
|
||||||
|
user: userTokens,
|
||||||
|
assistant: assistantTokens,
|
||||||
|
tool: toolTokens,
|
||||||
|
other: otherTokens,
|
||||||
|
},
|
||||||
|
breakdownTotal,
|
||||||
|
};
|
||||||
|
}, [currentSessionId, providers, sessionMessages, sessions]);
|
||||||
|
|
||||||
|
if (!currentSessionId) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full items-center justify-center p-6 text-center typography-ui-label text-muted-foreground">
|
||||||
|
Open a session to inspect context.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const segments: Array<{ key: string; label: string; value: number; color: string }> = [
|
||||||
|
{ key: 'user', label: 'User', value: viewModel.breakdown.user, color: 'var(--status-success)' },
|
||||||
|
{ key: 'assistant', label: 'Assistant', value: viewModel.breakdown.assistant, color: 'var(--primary-base)' },
|
||||||
|
{ key: 'tool', label: 'Tool Calls', value: viewModel.breakdown.tool, color: 'var(--status-warning)' },
|
||||||
|
{ key: 'other', label: 'Other', value: viewModel.breakdown.other, color: 'var(--surface-muted-foreground)' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full overflow-y-auto bg-background">
|
||||||
|
<div className="mx-auto w-full max-w-[52rem] px-5 py-6">
|
||||||
|
|
||||||
|
{/* ── Session header ── */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h2 className="typography-ui-header font-semibold text-foreground truncate">{viewModel.sessionTitle}</h2>
|
||||||
|
<div className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-0.5 typography-micro text-muted-foreground/70">
|
||||||
|
<span>{viewModel.providerModel.providerName} / {viewModel.providerModel.modelName}</span>
|
||||||
|
{viewModel.createdAt && (
|
||||||
|
<>
|
||||||
|
<span>·</span>
|
||||||
|
<span>{formatDateTime(viewModel.createdAt)}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── Context usage ── */}
|
||||||
|
<div className="mb-5 rounded-lg bg-[var(--surface-elevated)]/70 px-4 py-3.5">
|
||||||
|
<div className="flex items-baseline justify-between">
|
||||||
|
<span className="typography-micro text-muted-foreground">Context</span>
|
||||||
|
<span className="typography-micro tabular-nums text-muted-foreground/70">
|
||||||
|
{formatNumber(viewModel.tokenBreakdown.total)}
|
||||||
|
{viewModel.contextLimit ? ` / ${formatNumber(viewModel.contextLimit)}` : ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2.5 flex h-1 w-full overflow-hidden rounded-full bg-[var(--surface-subtle)]">
|
||||||
|
{viewModel.usagePercent > 0 && (
|
||||||
|
<div
|
||||||
|
className="rounded-full transition-all duration-300"
|
||||||
|
style={{
|
||||||
|
width: `${Math.max(0.5, viewModel.usagePercent)}%`,
|
||||||
|
backgroundColor: viewModel.usagePercent > 80 ? 'var(--status-warning)' : 'var(--primary-base)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mt-1.5 typography-micro font-medium tabular-nums text-foreground/80">
|
||||||
|
{viewModel.usagePercent.toFixed(1)}% used
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── Stat grid ── */}
|
||||||
|
<div className="mb-5 grid grid-cols-2 gap-2">
|
||||||
|
{([
|
||||||
|
{ label: 'Messages', value: formatNumber(viewModel.messagesCount) },
|
||||||
|
{ label: 'User', value: formatNumber(viewModel.userMessagesCount) },
|
||||||
|
{ label: 'Assistant', value: formatNumber(viewModel.assistantMessagesCount) },
|
||||||
|
{ label: 'Cost', value: formatMoney(viewModel.totalAssistantCost) },
|
||||||
|
] as const).map((item) => (
|
||||||
|
<div key={item.label} className="rounded-lg bg-[var(--surface-elevated)]/70 px-3 py-2.5">
|
||||||
|
<div className="typography-micro text-muted-foreground/70">{item.label}</div>
|
||||||
|
<div className="mt-0.5 typography-ui-label tabular-nums text-foreground">{item.value}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── Last turn tokens ── */}
|
||||||
|
<div className="mb-5 rounded-lg bg-[var(--surface-elevated)]/70 px-4 py-3.5">
|
||||||
|
<div className="typography-micro text-muted-foreground">Last Assistant Message</div>
|
||||||
|
<div className="mt-2.5 grid grid-cols-3 gap-x-4 gap-y-2.5">
|
||||||
|
{([
|
||||||
|
{ label: 'Input', value: viewModel.tokenBreakdown.input },
|
||||||
|
{ label: 'Output', value: viewModel.tokenBreakdown.output },
|
||||||
|
{ label: 'Reasoning', value: viewModel.tokenBreakdown.reasoning },
|
||||||
|
{ label: 'Cache Read', value: viewModel.tokenBreakdown.cacheRead },
|
||||||
|
{ label: 'Cache Write', value: viewModel.tokenBreakdown.cacheWrite },
|
||||||
|
] as const).map((item) => (
|
||||||
|
<div key={item.label}>
|
||||||
|
<div className="typography-micro text-muted-foreground/70">{item.label}</div>
|
||||||
|
<div className="mt-0.5 typography-ui-label tabular-nums text-foreground">{formatNumber(item.value)}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── Context breakdown ── */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="flex h-1 w-full overflow-hidden rounded-full bg-[var(--surface-subtle)]">
|
||||||
|
{segments.map((segment) => {
|
||||||
|
if (segment.value <= 0 || viewModel.breakdownTotal <= 0) return null;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={segment.key}
|
||||||
|
style={{
|
||||||
|
width: `${(segment.value / viewModel.breakdownTotal) * 100}%`,
|
||||||
|
backgroundColor: segment.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 flex flex-wrap items-center gap-x-4 gap-y-1">
|
||||||
|
{segments.map((segment) => {
|
||||||
|
const pct = viewModel.breakdownTotal > 0 ? (segment.value / viewModel.breakdownTotal) * 100 : 0;
|
||||||
|
return (
|
||||||
|
<div key={segment.key} className="inline-flex items-center gap-1.5">
|
||||||
|
<span className="h-1.5 w-1.5 rounded-full" style={{ backgroundColor: segment.color }} />
|
||||||
|
<span className="typography-micro text-muted-foreground/70">
|
||||||
|
{segment.label} <span className="tabular-nums">{pct.toFixed(0)}%</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── Raw messages ── */}
|
||||||
|
<div>
|
||||||
|
<div className="typography-micro text-muted-foreground">Raw Messages</div>
|
||||||
|
<div className="mt-2.5 space-y-1">
|
||||||
|
{[...sessionMessages].reverse().map((message) => {
|
||||||
|
const role = deriveMessageRole(message.info).role;
|
||||||
|
const isExpanded = expandedRawMessages[message.info.id] === true;
|
||||||
|
const isCopied = copiedRawMessageId === message.info.id;
|
||||||
|
const messageCreatedAt = (message.info.time?.created ?? null) as number | null;
|
||||||
|
|
||||||
|
const jsonValue = isExpanded
|
||||||
|
? JSON.stringify({ info: message.info, parts: message.parts }, null, 2)
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={message.info.id}
|
||||||
|
className="overflow-hidden rounded-lg bg-[var(--surface-elevated)]/70"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="w-full cursor-pointer px-3 py-1.5 text-left hover:bg-[var(--interactive-hover)]"
|
||||||
|
aria-expanded={isExpanded}
|
||||||
|
onClick={() => {
|
||||||
|
setExpandedRawMessages((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[message.info.id]: !(prev[message.info.id] === true),
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between gap-2 whitespace-nowrap overflow-hidden">
|
||||||
|
<span className="min-w-0 inline-flex items-center gap-1.5">
|
||||||
|
<span className="typography-ui-label text-foreground shrink-0">{capitalizeRole(role)}</span>
|
||||||
|
<span className="min-w-0 truncate typography-micro text-muted-foreground">{message.info.id}</span>
|
||||||
|
</span>
|
||||||
|
<span className="typography-micro text-muted-foreground shrink-0">{formatMessageDateMeta(messageCreatedAt)}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="border-t border-[var(--surface-subtle)] p-0">
|
||||||
|
<div className="group relative max-h-[26rem] w-full overflow-auto bg-[var(--surface-background)]">
|
||||||
|
<div className="absolute top-1 right-2 z-10 opacity-0 transition-opacity group-hover:opacity-100">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="rounded p-1 text-muted-foreground transition-colors hover:bg-interactive-hover/60 hover:text-foreground"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
void handleCopyRawMessage(message.info.id, jsonValue);
|
||||||
|
}}
|
||||||
|
aria-label={isCopied ? 'Copied' : 'Copy JSON'}
|
||||||
|
title={isCopied ? 'Copied' : 'Copy'}
|
||||||
|
>
|
||||||
|
{isCopied ? <RiCheckLine className="size-3.5" /> : <RiFileCopyLine className="size-3.5" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<SyntaxHighlighter
|
||||||
|
language="json"
|
||||||
|
style={syntaxTheme}
|
||||||
|
PreTag="div"
|
||||||
|
customStyle={{
|
||||||
|
margin: 0,
|
||||||
|
padding: '0.75rem',
|
||||||
|
background: 'transparent',
|
||||||
|
fontSize: 'var(--text-micro)',
|
||||||
|
lineHeight: '1.35',
|
||||||
|
}}
|
||||||
|
codeTagProps={{
|
||||||
|
style: {
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
overflowWrap: 'break-word',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
wrapLongLines
|
||||||
|
>
|
||||||
|
{jsonValue}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -141,6 +141,9 @@ export const Header: React.FC = () => {
|
|||||||
const toggleSidebar = useUIStore((state) => state.toggleSidebar);
|
const toggleSidebar = useUIStore((state) => state.toggleSidebar);
|
||||||
const toggleBottomTerminal = useUIStore((state) => state.toggleBottomTerminal);
|
const toggleBottomTerminal = useUIStore((state) => state.toggleBottomTerminal);
|
||||||
const toggleRightSidebar = useUIStore((state) => state.toggleRightSidebar);
|
const toggleRightSidebar = useUIStore((state) => state.toggleRightSidebar);
|
||||||
|
const openContextOverview = useUIStore((state) => state.openContextOverview);
|
||||||
|
const closeContextPanel = useUIStore((state) => state.closeContextPanel);
|
||||||
|
const contextPanelByDirectory = useUIStore((state) => state.contextPanelByDirectory);
|
||||||
const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen);
|
const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen);
|
||||||
const activeMainTab = useUIStore((state) => state.activeMainTab);
|
const activeMainTab = useUIStore((state) => state.activeMainTab);
|
||||||
const setActiveMainTab = useUIStore((state) => state.setActiveMainTab);
|
const setActiveMainTab = useUIStore((state) => state.setActiveMainTab);
|
||||||
@@ -338,9 +341,15 @@ export const Header: React.FC = () => {
|
|||||||
const updateProjectTabsOverflow = React.useCallback(() => {
|
const updateProjectTabsOverflow = React.useCallback(() => {
|
||||||
const el = projectTabsScrollRef.current;
|
const el = projectTabsScrollRef.current;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
setProjectTabsOverflow({
|
const next = {
|
||||||
left: el.scrollLeft > 2,
|
left: el.scrollLeft > 2,
|
||||||
right: el.scrollLeft + el.clientWidth < el.scrollWidth - 2,
|
right: el.scrollLeft + el.clientWidth < el.scrollWidth - 2,
|
||||||
|
};
|
||||||
|
setProjectTabsOverflow((prev) => {
|
||||||
|
if (prev.left === next.left && prev.right === next.right) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return next;
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -884,6 +893,30 @@ export const Header: React.FC = () => {
|
|||||||
setSettingsDialogOpen(true);
|
setSettingsDialogOpen(true);
|
||||||
}, [blurActiveElement, isMobile, setSessionSwitcherOpen, setSettingsDialogOpen]);
|
}, [blurActiveElement, isMobile, setSessionSwitcherOpen, setSettingsDialogOpen]);
|
||||||
|
|
||||||
|
const handleOpenContextPanel = React.useCallback(() => {
|
||||||
|
const directory = normalize(openDirectory || '');
|
||||||
|
if (!directory) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const panelState = contextPanelByDirectory[directory];
|
||||||
|
if (panelState?.isOpen && panelState.mode === 'context') {
|
||||||
|
closeContextPanel(directory);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
openContextOverview(directory);
|
||||||
|
}, [closeContextPanel, contextPanelByDirectory, openContextOverview, openDirectory]);
|
||||||
|
|
||||||
|
const isContextPanelActive = React.useMemo(() => {
|
||||||
|
const directory = normalize(openDirectory || '');
|
||||||
|
if (!directory) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const panelState = contextPanelByDirectory[directory];
|
||||||
|
return Boolean(panelState?.isOpen && panelState.mode === 'context');
|
||||||
|
}, [contextPanelByDirectory, openDirectory]);
|
||||||
|
|
||||||
const headerIconButtonClass = 'app-region-no-drag inline-flex h-9 w-9 items-center justify-center gap-2 p-2 rounded-md typography-ui-label font-medium text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50 hover:text-foreground hover:bg-interactive-hover transition-colors';
|
const headerIconButtonClass = 'app-region-no-drag inline-flex h-9 w-9 items-center justify-center gap-2 p-2 rounded-md typography-ui-label font-medium text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50 hover:text-foreground hover:bg-interactive-hover transition-colors';
|
||||||
|
|
||||||
const desktopPaddingClass = React.useMemo(() => {
|
const desktopPaddingClass = React.useMemo(() => {
|
||||||
@@ -1604,9 +1637,11 @@ export const Header: React.FC = () => {
|
|||||||
size="compact"
|
size="compact"
|
||||||
hideIcon
|
hideIcon
|
||||||
showPercentIcon
|
showPercentIcon
|
||||||
|
onClick={handleOpenContextPanel}
|
||||||
|
pressed={isContextPanelActive}
|
||||||
className="mr-3.5"
|
className="mr-3.5"
|
||||||
valueClassName="typography-ui-label font-medium leading-none text-foreground"
|
valueClassName="typography-ui-label font-medium leading-none text-foreground"
|
||||||
percentIconClassName="h-5 w-5 text-muted-foreground"
|
percentIconClassName="h-5 w-5"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<OpenInAppButton directory={openDirectory} className="mr-1" />
|
<OpenInAppButton directory={openDirectory} className="mr-1" />
|
||||||
|
|||||||
@@ -1290,11 +1290,21 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
|
|
||||||
sortedWorktrees.forEach((meta) => {
|
sortedWorktrees.forEach((meta) => {
|
||||||
const directory = normalizePath(meta.path) ?? meta.path;
|
const directory = normalizePath(meta.path) ?? meta.path;
|
||||||
const label = meta.label || meta.name || formatDirectoryName(directory, homeDirectory) || directory;
|
const currentBranch = gitDirectories.get(directory)?.status?.current?.trim() || null;
|
||||||
|
const metadataBranch = meta.branch?.trim() || null;
|
||||||
|
const shouldSyncLabelWithBranch = Boolean(
|
||||||
|
currentBranch
|
||||||
|
&& metadataBranch
|
||||||
|
&& meta.label
|
||||||
|
&& normalizeForBranchComparison(meta.label) === normalizeForBranchComparison(metadataBranch),
|
||||||
|
);
|
||||||
|
const label = shouldSyncLabelWithBranch
|
||||||
|
? currentBranch!
|
||||||
|
: (meta.label || meta.name || formatDirectoryName(directory, homeDirectory) || directory);
|
||||||
groups.push({
|
groups.push({
|
||||||
id: `worktree:${directory}`,
|
id: `worktree:${directory}`,
|
||||||
label,
|
label,
|
||||||
branch: meta.branch || null,
|
branch: currentBranch || metadataBranch,
|
||||||
description: formatPathForDisplay(directory, homeDirectory),
|
description: formatPathForDisplay(directory, homeDirectory),
|
||||||
isMain: false,
|
isMain: false,
|
||||||
worktree: meta,
|
worktree: meta,
|
||||||
@@ -1309,10 +1319,11 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
.sort((a, b) => (groupOrder.get(a) ?? 0) - (groupOrder.get(b) ?? 0));
|
.sort((a, b) => (groupOrder.get(a) ?? 0) - (groupOrder.get(b) ?? 0));
|
||||||
|
|
||||||
orphanKeys.forEach((directory) => {
|
orphanKeys.forEach((directory) => {
|
||||||
|
const currentBranch = gitDirectories.get(directory)?.status?.current?.trim() || null;
|
||||||
groups.push({
|
groups.push({
|
||||||
id: `worktree:orphan:${directory}`,
|
id: `worktree:orphan:${directory}`,
|
||||||
label: formatDirectoryName(directory, homeDirectory) || directory,
|
label: formatDirectoryName(directory, homeDirectory) || directory,
|
||||||
branch: null,
|
branch: currentBranch,
|
||||||
description: formatPathForDisplay(directory, homeDirectory),
|
description: formatPathForDisplay(directory, homeDirectory),
|
||||||
isMain: false,
|
isMain: false,
|
||||||
worktree: null,
|
worktree: null,
|
||||||
@@ -1323,7 +1334,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
|
|
||||||
return groups;
|
return groups;
|
||||||
},
|
},
|
||||||
[homeDirectory, worktreeMetadata, pinnedSessionIds]
|
[homeDirectory, worktreeMetadata, pinnedSessionIds, gitDirectories]
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggleGroupSessionLimit = React.useCallback((groupId: string) => {
|
const toggleGroupSessionLimit = React.useCallback((groupId: string) => {
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ interface ContextUsageDisplayProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
valueClassName?: string;
|
valueClassName?: string;
|
||||||
percentIconClassName?: string;
|
percentIconClassName?: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
pressed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ContextUsageDisplay: React.FC<ContextUsageDisplayProps> = ({
|
export const ContextUsageDisplay: React.FC<ContextUsageDisplayProps> = ({
|
||||||
@@ -30,6 +32,8 @@ export const ContextUsageDisplay: React.FC<ContextUsageDisplayProps> = ({
|
|||||||
className,
|
className,
|
||||||
valueClassName,
|
valueClassName,
|
||||||
percentIconClassName,
|
percentIconClassName,
|
||||||
|
onClick,
|
||||||
|
pressed = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [mobileTooltipOpen, setMobileTooltipOpen] = React.useState(false);
|
const [mobileTooltipOpen, setMobileTooltipOpen] = React.useState(false);
|
||||||
|
|
||||||
@@ -56,16 +60,10 @@ export const ContextUsageDisplay: React.FC<ContextUsageDisplayProps> = ({
|
|||||||
`Output limit: ${formatTokens(safeOutputLimit)}`,
|
`Output limit: ${formatTokens(safeOutputLimit)}`,
|
||||||
];
|
];
|
||||||
|
|
||||||
const contextElement = (
|
const isInteractive = !isMobile && typeof onClick === 'function';
|
||||||
<div
|
|
||||||
className={cn(
|
const contextContent = (
|
||||||
'app-region-no-drag flex items-center gap-1.5 text-muted-foreground/60 select-none',
|
<>
|
||||||
size === 'compact' ? 'typography-micro' : 'typography-meta',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
aria-label="Context usage"
|
|
||||||
onClick={isMobile ? () => setMobileTooltipOpen(true) : undefined}
|
|
||||||
>
|
|
||||||
{!isMobile && !hideIcon && <RiDonutChartLine className="h-4 w-4 flex-shrink-0" />}
|
{!isMobile && !hideIcon && <RiDonutChartLine className="h-4 w-4 flex-shrink-0" />}
|
||||||
<span className={cn('font-medium inline-flex items-center gap-1.5', valueClassName)}>
|
<span className={cn('font-medium inline-flex items-center gap-1.5', valueClassName)}>
|
||||||
{showPercentIcon ? (
|
{showPercentIcon ? (
|
||||||
@@ -82,6 +80,39 @@ export const ContextUsageDisplay: React.FC<ContextUsageDisplayProps> = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const sharedClassName = cn(
|
||||||
|
'app-region-no-drag flex items-center gap-1.5 select-none',
|
||||||
|
size === 'compact' ? 'typography-micro' : 'typography-meta',
|
||||||
|
isInteractive
|
||||||
|
? cn(
|
||||||
|
'rounded-md px-2 py-1.5 text-foreground transition-colors',
|
||||||
|
'hover:bg-interactive-hover',
|
||||||
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary'
|
||||||
|
)
|
||||||
|
: 'text-muted-foreground/60',
|
||||||
|
className,
|
||||||
|
);
|
||||||
|
|
||||||
|
const contextElement = isInteractive ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={sharedClassName}
|
||||||
|
aria-label="Context usage"
|
||||||
|
aria-pressed={pressed}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{contextContent}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={sharedClassName}
|
||||||
|
aria-label="Context usage"
|
||||||
|
onClick={isMobile ? () => setMobileTooltipOpen(true) : undefined}
|
||||||
|
>
|
||||||
|
{contextContent}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ interface ErrorBoundaryState {
|
|||||||
hasError: boolean;
|
hasError: boolean;
|
||||||
error?: Error;
|
error?: Error;
|
||||||
errorInfo?: React.ErrorInfo;
|
errorInfo?: React.ErrorInfo;
|
||||||
|
copied?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ErrorBoundaryProps {
|
interface ErrorBoundaryProps {
|
||||||
@@ -25,17 +26,32 @@ export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoun
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||||
this.setState({ error, errorInfo });
|
this.setState({ error, errorInfo, copied: false });
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
console.error('Error caught by boundary:', error, errorInfo);
|
||||||
console.error('Error caught by boundary:', error, errorInfo);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleReset = () => {
|
handleReset = () => {
|
||||||
this.setState({ hasError: false, error: undefined, errorInfo: undefined });
|
this.setState({ hasError: false, error: undefined, errorInfo: undefined });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleCopy = async () => {
|
||||||
|
const errorText = this.state.error ? String(this.state.error) : 'Unknown error';
|
||||||
|
const stack = this.state.error?.stack ? `\n\nStack:\n${this.state.error.stack}` : '';
|
||||||
|
const componentStack = this.state.errorInfo?.componentStack ? `\n\nComponent stack:${this.state.errorInfo.componentStack}` : '';
|
||||||
|
const payload = `${errorText}${stack}${componentStack}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(payload);
|
||||||
|
this.setState({ copied: true });
|
||||||
|
window.setTimeout(() => {
|
||||||
|
this.setState((prev) => (prev.copied ? { copied: false } : null));
|
||||||
|
}, 1500);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.state.hasError) {
|
if (this.state.hasError) {
|
||||||
if (this.props.fallback) {
|
if (this.props.fallback) {
|
||||||
@@ -61,6 +77,7 @@ export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoun
|
|||||||
<summary className="cursor-pointer hover:bg-interactive-hover/80">Error details</summary>
|
<summary className="cursor-pointer hover:bg-interactive-hover/80">Error details</summary>
|
||||||
<pre className="mt-2 overflow-x-auto">
|
<pre className="mt-2 overflow-x-auto">
|
||||||
{this.state.error.toString()}
|
{this.state.error.toString()}
|
||||||
|
{this.state.errorInfo?.componentStack ? `\n\nComponent stack:${this.state.errorInfo.componentStack}` : ''}
|
||||||
</pre>
|
</pre>
|
||||||
</details>
|
</details>
|
||||||
)}
|
)}
|
||||||
@@ -70,6 +87,9 @@ export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoun
|
|||||||
<RiRestartLine className="h-4 w-4 mr-2" />
|
<RiRestartLine className="h-4 w-4 mr-2" />
|
||||||
Try again
|
Try again
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button onClick={this.handleCopy} variant="outline" className="flex-1">
|
||||||
|
{this.state.copied ? 'Copied' : 'Copy'}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ const PIN_THRESHOLD_RATIO = 0.10;
|
|||||||
export const useChatScrollManager = ({
|
export const useChatScrollManager = ({
|
||||||
currentSessionId,
|
currentSessionId,
|
||||||
sessionMessages,
|
sessionMessages,
|
||||||
|
streamingMessageId,
|
||||||
updateViewportAnchor,
|
updateViewportAnchor,
|
||||||
isSyncing,
|
isSyncing,
|
||||||
isMobile,
|
isMobile,
|
||||||
@@ -117,6 +118,15 @@ export const useChatScrollManager = ({
|
|||||||
scrollEngine.scrollToPosition(Math.max(0, bottom), options);
|
scrollEngine.scrollToPosition(Math.max(0, bottom), options);
|
||||||
}, [markProgrammaticScroll, scrollEngine]);
|
}, [markProgrammaticScroll, scrollEngine]);
|
||||||
|
|
||||||
|
const scrollPinnedToBottom = React.useCallback(() => {
|
||||||
|
if (streamingMessageId) {
|
||||||
|
scrollToBottomInternal({ followBottom: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToBottomInternal({ instant: true });
|
||||||
|
}, [scrollToBottomInternal, streamingMessageId]);
|
||||||
|
|
||||||
const updateScrollButtonVisibility = React.useCallback(() => {
|
const updateScrollButtonVisibility = React.useCallback(() => {
|
||||||
const container = scrollRef.current;
|
const container = scrollRef.current;
|
||||||
if (!container) {
|
if (!container) {
|
||||||
@@ -250,13 +260,12 @@ export const useChatScrollManager = ({
|
|||||||
const container = scrollRef.current;
|
const container = scrollRef.current;
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
// When pinned and content grows, scroll to bottom instantly
|
// When pinned and content grows, follow bottom with fast smooth scroll
|
||||||
const distanceFromBottom = getDistanceFromBottom();
|
const distanceFromBottom = getDistanceFromBottom();
|
||||||
if (distanceFromBottom > getPinThreshold()) {
|
if (distanceFromBottom > getPinThreshold()) {
|
||||||
markProgrammaticScroll();
|
scrollPinnedToBottom();
|
||||||
scrollToBottomInternal({ instant: true });
|
|
||||||
}
|
}
|
||||||
}, [getDistanceFromBottom, getPinThreshold, isSyncing, markProgrammaticScroll, scrollToBottomInternal, sessionMessages]);
|
}, [getDistanceFromBottom, getPinThreshold, isSyncing, scrollPinnedToBottom, sessionMessages]);
|
||||||
|
|
||||||
// Use ResizeObserver to detect content changes and maintain pin
|
// Use ResizeObserver to detect content changes and maintain pin
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -266,11 +275,11 @@ export const useChatScrollManager = ({
|
|||||||
const observer = new ResizeObserver(() => {
|
const observer = new ResizeObserver(() => {
|
||||||
updateScrollButtonVisibility();
|
updateScrollButtonVisibility();
|
||||||
|
|
||||||
// Maintain pin when content grows - always instant for smooth experience
|
// Maintain pin when content grows - fast smooth follow
|
||||||
if (isPinnedRef.current) {
|
if (isPinnedRef.current) {
|
||||||
const distanceFromBottom = getDistanceFromBottom();
|
const distanceFromBottom = getDistanceFromBottom();
|
||||||
if (distanceFromBottom > getPinThreshold()) {
|
if (distanceFromBottom > getPinThreshold()) {
|
||||||
scrollToBottomInternal({ instant: true });
|
scrollPinnedToBottom();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -282,7 +291,7 @@ export const useChatScrollManager = ({
|
|||||||
if (isPinnedRef.current) {
|
if (isPinnedRef.current) {
|
||||||
const distanceFromBottom = getDistanceFromBottom();
|
const distanceFromBottom = getDistanceFromBottom();
|
||||||
if (distanceFromBottom > getPinThreshold()) {
|
if (distanceFromBottom > getPinThreshold()) {
|
||||||
scrollToBottomInternal({ instant: true });
|
scrollPinnedToBottom();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -293,7 +302,7 @@ export const useChatScrollManager = ({
|
|||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
childObserver.disconnect();
|
childObserver.disconnect();
|
||||||
};
|
};
|
||||||
}, [getDistanceFromBottom, getPinThreshold, scrollToBottomInternal, updateScrollButtonVisibility]);
|
}, [getDistanceFromBottom, getPinThreshold, scrollPinnedToBottom, updateScrollButtonVisibility]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
@@ -315,14 +324,14 @@ export const useChatScrollManager = ({
|
|||||||
const handleMessageContentChange = React.useCallback(() => {
|
const handleMessageContentChange = React.useCallback(() => {
|
||||||
updateScrollButtonVisibility();
|
updateScrollButtonVisibility();
|
||||||
|
|
||||||
// Maintain pin when content changes - always instant
|
// Maintain pin when content changes - fast smooth follow
|
||||||
if (isPinnedRef.current) {
|
if (isPinnedRef.current) {
|
||||||
const distanceFromBottom = getDistanceFromBottom();
|
const distanceFromBottom = getDistanceFromBottom();
|
||||||
if (distanceFromBottom > getPinThreshold()) {
|
if (distanceFromBottom > getPinThreshold()) {
|
||||||
scrollToBottomInternal({ instant: true });
|
scrollPinnedToBottom();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [getDistanceFromBottom, getPinThreshold, scrollToBottomInternal, updateScrollButtonVisibility]);
|
}, [getDistanceFromBottom, getPinThreshold, scrollPinnedToBottom, updateScrollButtonVisibility]);
|
||||||
|
|
||||||
const getAnimationHandlers = React.useCallback((messageId: string): AnimationHandlers => {
|
const getAnimationHandlers = React.useCallback((messageId: string): AnimationHandlers => {
|
||||||
const existing = animationHandlersRef.current.get(messageId);
|
const existing = animationHandlersRef.current.get(messageId);
|
||||||
@@ -336,7 +345,7 @@ export const useChatScrollManager = ({
|
|||||||
if (isPinnedRef.current) {
|
if (isPinnedRef.current) {
|
||||||
const distanceFromBottom = getDistanceFromBottom();
|
const distanceFromBottom = getDistanceFromBottom();
|
||||||
if (distanceFromBottom > getPinThreshold()) {
|
if (distanceFromBottom > getPinThreshold()) {
|
||||||
scrollToBottomInternal({ instant: true });
|
scrollPinnedToBottom();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -350,7 +359,7 @@ export const useChatScrollManager = ({
|
|||||||
if (isPinnedRef.current) {
|
if (isPinnedRef.current) {
|
||||||
const distanceFromBottom = getDistanceFromBottom();
|
const distanceFromBottom = getDistanceFromBottom();
|
||||||
if (distanceFromBottom > getPinThreshold()) {
|
if (distanceFromBottom > getPinThreshold()) {
|
||||||
scrollToBottomInternal({ instant: true });
|
scrollPinnedToBottom();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -360,7 +369,7 @@ export const useChatScrollManager = ({
|
|||||||
|
|
||||||
animationHandlersRef.current.set(messageId, handlers);
|
animationHandlersRef.current.set(messageId, handlers);
|
||||||
return handlers;
|
return handlers;
|
||||||
}, [getDistanceFromBottom, getPinThreshold, scrollToBottomInternal, updateScrollButtonVisibility]);
|
}, [getDistanceFromBottom, getPinThreshold, scrollPinnedToBottom, updateScrollButtonVisibility]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
scrollRef,
|
scrollRef,
|
||||||
|
|||||||
@@ -818,7 +818,6 @@ export const useEventStream = () => {
|
|||||||
type: part.type || 'text',
|
type: part.type || 'text',
|
||||||
} as Part;
|
} as Part;
|
||||||
|
|
||||||
// Fallback: if we see assistant parts but session.status hasn't arrived yet, mark busy.
|
|
||||||
if (roleInfo === 'assistant') {
|
if (roleInfo === 'assistant') {
|
||||||
const partType = (messagePart as { type?: unknown }).type;
|
const partType = (messagePart as { type?: unknown }).type;
|
||||||
const partTime = (messagePart as { time?: { end?: unknown } }).time;
|
const partTime = (messagePart as { time?: { end?: unknown } }).time;
|
||||||
@@ -863,6 +862,81 @@ export const useEventStream = () => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'message.part.delta': {
|
||||||
|
const sessionId = readStringProp(props, ['sessionID', 'sessionId']);
|
||||||
|
const messageId = readStringProp(props, ['messageID', 'messageId']);
|
||||||
|
const partId = readStringProp(props, ['partID', 'partId']);
|
||||||
|
const field = readStringProp(props, ['field']);
|
||||||
|
const delta = typeof props.delta === 'string' ? props.delta : null;
|
||||||
|
|
||||||
|
if (!sessionId || !messageId || !partId || !field || delta === null) {
|
||||||
|
if (streamDebugEnabled()) {
|
||||||
|
console.debug('[useEventStream] Skipping message.part.delta with missing payload', {
|
||||||
|
sessionID: props.sessionID,
|
||||||
|
messageID: props.messageID,
|
||||||
|
partID: props.partID,
|
||||||
|
field: props.field,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastMessageEventBySessionRef.current.set(sessionId, Date.now());
|
||||||
|
const pendingTimer = pendingMessageStallTimersRef.current.get(sessionId);
|
||||||
|
if (pendingTimer) {
|
||||||
|
clearTimeout(pendingTimer);
|
||||||
|
pendingMessageStallTimersRef.current.delete(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmedHeadMaxId = useSessionStore.getState().sessionMemoryState.get(sessionId)?.trimmedHeadMaxId;
|
||||||
|
if (trimmedHeadMaxId && !isIdNewer(messageId, trimmedHeadMaxId)) {
|
||||||
|
if (streamDebugEnabled()) {
|
||||||
|
console.debug('[useEventStream] Skipping message.part.delta for trimmed message', {
|
||||||
|
sessionId,
|
||||||
|
messageId,
|
||||||
|
trimmedHeadMaxId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingMessage = getMessageFromStore(sessionId, messageId);
|
||||||
|
const existingPart = existingMessage?.parts?.find((item) => item?.id === partId);
|
||||||
|
if (!existingPart) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingPartRecord = existingPart as Record<string, unknown>;
|
||||||
|
const existingFieldValue = existingPartRecord[field];
|
||||||
|
const updatedPart: Part = {
|
||||||
|
...existingPart,
|
||||||
|
[field]: `${typeof existingFieldValue === 'string' ? existingFieldValue : ''}${delta}`,
|
||||||
|
} as Part;
|
||||||
|
|
||||||
|
let roleInfo = 'assistant';
|
||||||
|
const existingRole = (existingMessage?.info as Record<string, unknown> | undefined)?.role;
|
||||||
|
if (typeof existingRole === 'string') {
|
||||||
|
roleInfo = existingRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roleInfo === 'assistant' && delta.length > 0) {
|
||||||
|
const currentStatus = useSessionStore.getState().sessionStatus?.get(sessionId);
|
||||||
|
const recentlyConfirmedIdle =
|
||||||
|
currentStatus?.type === 'idle' &&
|
||||||
|
typeof currentStatus.confirmedAt === 'number' &&
|
||||||
|
Date.now() - currentStatus.confirmedAt < 1200;
|
||||||
|
if (!currentStatus || currentStatus.type === 'idle') {
|
||||||
|
if (!recentlyConfirmedIdle) {
|
||||||
|
updateSessionStatus(sessionId, { type: 'busy' }, 'sse:message.part.delta');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trackMessage(messageId, 'part_delta_received', { role: roleInfo, field });
|
||||||
|
addStreamingPart(sessionId, messageId, updatedPart, roleInfo);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'message.updated': {
|
case 'message.updated': {
|
||||||
const message = (typeof props.info === 'object' && props.info !== null) ? (props.info as Record<string, unknown>) : props;
|
const message = (typeof props.info === 'object' && props.info !== null) ? (props.info as Record<string, unknown>) : props;
|
||||||
const messageExt = message as Record<string, unknown>;
|
const messageExt = message as Record<string, unknown>;
|
||||||
|
|||||||
@@ -864,6 +864,11 @@ html:not(.dark) .chat-scroll {
|
|||||||
contain-intrinsic-size: none !important;
|
contain-intrinsic-size: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mermaid block: position context for controls wrapper. */
|
||||||
|
[data-streamdown="mermaid-block"] {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
/* Hide system caret in terminal - targets both Ghostty internal input and our custom touch input */
|
/* Hide system caret in terminal - targets both Ghostty internal input and our custom touch input */
|
||||||
.terminal-viewport-container textarea,
|
.terminal-viewport-container textarea,
|
||||||
.terminal-viewport-container input {
|
.terminal-viewport-container input {
|
||||||
|
|||||||
@@ -487,3 +487,18 @@ export const fetchDesktopInstalledApps = async (
|
|||||||
return { apps: [], success: false, hasCache: false, isCacheStale: false };
|
return { apps: [], success: false, hasCache: false, isCacheStale: false };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const clearDesktopCache = async (): Promise<boolean> => {
|
||||||
|
if (!isTauriShell() || !isDesktopLocalOriginActive()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
|
||||||
|
await tauri?.core?.invoke?.('desktop_clear_cache');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to clear cache', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { SEMANTIC_TYPOGRAPHY, getTypographyVariable, type SemanticTypographyKey
|
|||||||
|
|
||||||
export type MainTab = 'chat' | 'plan' | 'git' | 'diff' | 'terminal' | 'files';
|
export type MainTab = 'chat' | 'plan' | 'git' | 'diff' | 'terminal' | 'files';
|
||||||
export type RightSidebarTab = 'git' | 'files';
|
export type RightSidebarTab = 'git' | 'files';
|
||||||
export type ContextPanelMode = 'diff' | 'file';
|
export type ContextPanelMode = 'diff' | 'file' | 'context';
|
||||||
|
|
||||||
type ContextPanelDirectoryState = {
|
type ContextPanelDirectoryState = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -224,6 +224,7 @@ interface UIStore {
|
|||||||
setRightSidebarTab: (tab: RightSidebarTab) => void;
|
setRightSidebarTab: (tab: RightSidebarTab) => void;
|
||||||
openContextDiff: (directory: string, filePath: string) => void;
|
openContextDiff: (directory: string, filePath: string) => void;
|
||||||
openContextFile: (directory: string, filePath: string) => void;
|
openContextFile: (directory: string, filePath: string) => void;
|
||||||
|
openContextOverview: (directory: string) => void;
|
||||||
closeContextPanel: (directory: string) => void;
|
closeContextPanel: (directory: string) => void;
|
||||||
toggleContextPanelExpanded: (directory: string) => void;
|
toggleContextPanelExpanded: (directory: string) => void;
|
||||||
setContextPanelWidth: (directory: string, width: number) => void;
|
setContextPanelWidth: (directory: string, width: number) => void;
|
||||||
@@ -404,6 +405,18 @@ export const useUIStore = create<UIStore>()(
|
|||||||
|
|
||||||
setSidebarOpen: (open) => {
|
setSidebarOpen: (open) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
|
if (state.isSidebarOpen === open) {
|
||||||
|
if (!open) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
if (!state.hasManuallyResizedLeftSidebar && state.sidebarWidth !== LEFT_SIDEBAR_MIN_WIDTH) {
|
||||||
|
return {
|
||||||
|
isSidebarOpen: open,
|
||||||
|
sidebarWidth: LEFT_SIDEBAR_MIN_WIDTH,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
if (open && !state.hasManuallyResizedLeftSidebar) {
|
if (open && !state.hasManuallyResizedLeftSidebar) {
|
||||||
return {
|
return {
|
||||||
isSidebarOpen: open,
|
isSidebarOpen: open,
|
||||||
@@ -434,6 +447,18 @@ export const useUIStore = create<UIStore>()(
|
|||||||
|
|
||||||
setRightSidebarOpen: (open) => {
|
setRightSidebarOpen: (open) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
|
if (state.isRightSidebarOpen === open) {
|
||||||
|
if (!open) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
if (!state.hasManuallyResizedRightSidebar && state.rightSidebarWidth !== RIGHT_SIDEBAR_MIN_WIDTH) {
|
||||||
|
return {
|
||||||
|
isRightSidebarOpen: open,
|
||||||
|
rightSidebarWidth: RIGHT_SIDEBAR_MIN_WIDTH,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
if (open && !state.hasManuallyResizedRightSidebar) {
|
if (open && !state.hasManuallyResizedRightSidebar) {
|
||||||
return {
|
return {
|
||||||
isRightSidebarOpen: open,
|
isRightSidebarOpen: open,
|
||||||
@@ -501,6 +526,29 @@ export const useUIStore = create<UIStore>()(
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
openContextOverview: (directory) => {
|
||||||
|
const normalizedDirectory = normalizeDirectoryPath((directory || '').trim());
|
||||||
|
if (!normalizedDirectory) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
set((state) => {
|
||||||
|
const prev = state.contextPanelByDirectory[normalizedDirectory];
|
||||||
|
const current = touchContextPanelState(prev);
|
||||||
|
const byDirectory = {
|
||||||
|
...state.contextPanelByDirectory,
|
||||||
|
[normalizedDirectory]: {
|
||||||
|
...current,
|
||||||
|
isOpen: true,
|
||||||
|
mode: 'context' as const,
|
||||||
|
targetPath: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return { contextPanelByDirectory: clampContextPanelRoots(byDirectory, 20) };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
closeContextPanel: (directory) => {
|
closeContextPanel: (directory) => {
|
||||||
const normalizedDirectory = normalizeDirectoryPath((directory || '').trim());
|
const normalizedDirectory = normalizeDirectoryPath((directory || '').trim());
|
||||||
if (!normalizedDirectory) {
|
if (!normalizedDirectory) {
|
||||||
@@ -585,7 +633,25 @@ export const useUIStore = create<UIStore>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
setBottomTerminalOpen: (open) => {
|
setBottomTerminalOpen: (open) => {
|
||||||
set(() => {
|
set((state) => {
|
||||||
|
if (state.isBottomTerminalOpen === open) {
|
||||||
|
if (!open) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
if (!state.hasManuallyResizedBottomTerminal && typeof window !== 'undefined') {
|
||||||
|
const proportionalHeight = Math.floor(window.innerHeight * 0.32);
|
||||||
|
if (state.bottomTerminalHeight === proportionalHeight && state.hasManuallyResizedBottomTerminal === false) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
isBottomTerminalOpen: open,
|
||||||
|
bottomTerminalHeight: proportionalHeight,
|
||||||
|
hasManuallyResizedBottomTerminal: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
if (open && typeof window !== 'undefined') {
|
if (open && typeof window !== 'undefined') {
|
||||||
const proportionalHeight = Math.floor(window.innerHeight * 0.32);
|
const proportionalHeight = Math.floor(window.innerHeight * 0.32);
|
||||||
return {
|
return {
|
||||||
|
|||||||
+15
-1
@@ -4,6 +4,13 @@ declare module 'streamdown' {
|
|||||||
export interface StreamdownProps {
|
export interface StreamdownProps {
|
||||||
children: string;
|
children: string;
|
||||||
mode?: 'streaming' | 'static';
|
mode?: 'streaming' | 'static';
|
||||||
|
isAnimating?: boolean;
|
||||||
|
animated?: {
|
||||||
|
animation?: string;
|
||||||
|
duration?: number;
|
||||||
|
easing?: string;
|
||||||
|
sep?: 'word' | 'char';
|
||||||
|
};
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|
||||||
shikiTheme?: readonly [string | object, string | object];
|
shikiTheme?: readonly [string | object, string | object];
|
||||||
@@ -11,8 +18,15 @@ declare module 'streamdown' {
|
|||||||
controls?: boolean | {
|
controls?: boolean | {
|
||||||
code?: boolean;
|
code?: boolean;
|
||||||
table?: boolean;
|
table?: boolean;
|
||||||
mermaid?: boolean;
|
mermaid?: boolean | {
|
||||||
|
download?: boolean;
|
||||||
|
copy?: boolean;
|
||||||
|
fullscreen?: boolean;
|
||||||
|
panZoom?: boolean;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
plugins?: Record<string, unknown>;
|
||||||
|
mermaid?: Record<string, unknown>;
|
||||||
components?: {
|
components?: {
|
||||||
[key: string]: ComponentType<{ children?: ReactNode; className?: string; [key: string]: unknown }>;
|
[key: string]: ComponentType<{ children?: ReactNode; className?: string; [key: string]: unknown }>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -229,7 +229,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@openchamber/ui": "workspace:*",
|
"@openchamber/ui": "workspace:*",
|
||||||
"@opencode-ai/sdk": "^1.1.65",
|
"@opencode-ai/sdk": "^1.2.5",
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
"jsonc-parser": "^3.3.1",
|
"jsonc-parser": "^3.3.1",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
|
|||||||
@@ -430,7 +430,7 @@ const deriveSessionActivity = (payload: Record<string, unknown> | null): Session
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'message.part.updated') {
|
if (type === 'message.part.updated' || type === 'message.part.delta') {
|
||||||
const info = properties?.info as Record<string, unknown> | undefined;
|
const info = properties?.info as Record<string, unknown> | undefined;
|
||||||
const sessionId = info?.sessionID ?? info?.sessionId ?? properties?.sessionID ?? properties?.sessionId;
|
const sessionId = info?.sessionID ?? info?.sessionId ?? properties?.sessionID ?? properties?.sessionId;
|
||||||
const role = info?.role;
|
const role = info?.role;
|
||||||
|
|||||||
@@ -462,7 +462,7 @@ const deriveSessionActivity = (payload: Record<string, unknown> | null): Session
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'message.part.updated') {
|
if (type === 'message.part.updated' || type === 'message.part.delta') {
|
||||||
const info = properties?.info as Record<string, unknown> | undefined;
|
const info = properties?.info as Record<string, unknown> | undefined;
|
||||||
const sessionId = info?.sessionID ?? info?.sessionId ?? properties?.sessionID ?? properties?.sessionId;
|
const sessionId = info?.sessionID ?? info?.sessionId ?? properties?.sessionID ?? properties?.sessionId;
|
||||||
const role = info?.role;
|
const role = info?.role;
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ const deriveSessionActivity = (payload: Record<string, unknown>): SessionActivit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'message.updated' || type === 'message.part.updated') {
|
if (type === 'message.updated' || type === 'message.part.updated' || type === 'message.part.delta') {
|
||||||
const info = properties?.info as Record<string, unknown> | undefined;
|
const info = properties?.info as Record<string, unknown> | undefined;
|
||||||
const sessionId = (info?.sessionID ?? info?.sessionId ?? properties?.sessionID ?? properties?.sessionId) as string;
|
const sessionId = (info?.sessionID ?? info?.sessionId ?? properties?.sessionID ?? properties?.sessionId) as string;
|
||||||
const role = info?.role as string;
|
const role = info?.role as string;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
"@fontsource/ibm-plex-sans": "^5.1.1",
|
"@fontsource/ibm-plex-sans": "^5.1.1",
|
||||||
"@ibm/plex": "^6.4.1",
|
"@ibm/plex": "^6.4.1",
|
||||||
"@octokit/rest": "^22.0.1",
|
"@octokit/rest": "^22.0.1",
|
||||||
"@opencode-ai/sdk": "^1.1.65",
|
"@opencode-ai/sdk": "^1.2.5",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
|
|||||||
@@ -3937,7 +3937,7 @@ function deriveSessionActivityTransitions(payload) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload.type === 'message.part.updated') {
|
if (payload.type === 'message.part.updated' || payload.type === 'message.part.delta') {
|
||||||
const info = payload.properties?.info;
|
const info = payload.properties?.info;
|
||||||
const sessionId = info?.sessionID ?? info?.sessionId ?? payload.properties?.sessionID ?? payload.properties?.sessionId;
|
const sessionId = info?.sessionID ?? info?.sessionId ?? payload.properties?.sessionID ?? payload.properties?.sessionId;
|
||||||
const role = info?.role;
|
const role = info?.role;
|
||||||
@@ -11177,7 +11177,6 @@ Context:
|
|||||||
res.json({ success: true, killedCount });
|
res.json({ success: true, killedCount });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
syncFromHmrState();
|
syncFromHmrState();
|
||||||
if (await isOpenCodeProcessHealthy()) {
|
if (await isOpenCodeProcessHealthy()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user