From 12169ac3ab0bc0255dcc29926a9cfa79c61c7006 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Jan 2026 00:47:04 +0000 Subject: [PATCH 1/7] fix(ui): improve review dialog rendering for tool calls and tables - Replace Markdown component with LogViewer in plan-approval-dialog to properly format tool calls with collapsible sections and JSON highlighting - Add remark-gfm plugin to Markdown component for GitHub Flavored Markdown support including tables, task lists, and strikethrough - Add table styling classes to Markdown component for proper table rendering - Install remark-gfm and rehype-sanitize dependencies Fixes mixed/broken rendering in review dialog where tool calls showed as raw text and markdown tables showed as pipe-separated text. --- apps/ui/package.json | 2 + apps/ui/src/components/ui/markdown.tsx | 9 +- .../dialogs/plan-approval-dialog.tsx | 9 +- package-lock.json | 312 +++++++++++++++++- 4 files changed, 323 insertions(+), 9 deletions(-) diff --git a/apps/ui/package.json b/apps/ui/package.json index e66433fdc..5a5ac3490 100644 --- a/apps/ui/package.json +++ b/apps/ui/package.json @@ -102,6 +102,8 @@ "react-markdown": "10.1.0", "react-resizable-panels": "3.0.6", "rehype-raw": "7.0.0", + "rehype-sanitize": "^6.0.0", + "remark-gfm": "^4.0.1", "sonner": "2.0.7", "tailwind-merge": "3.4.0", "usehooks-ts": "3.1.1", diff --git a/apps/ui/src/components/ui/markdown.tsx b/apps/ui/src/components/ui/markdown.tsx index 1d4f8ef9d..74556fef8 100644 --- a/apps/ui/src/components/ui/markdown.tsx +++ b/apps/ui/src/components/ui/markdown.tsx @@ -1,6 +1,7 @@ import ReactMarkdown from 'react-markdown'; import rehypeRaw from 'rehype-raw'; import rehypeSanitize from 'rehype-sanitize'; +import remarkGfm from 'remark-gfm'; import { cn } from '@/lib/utils'; interface MarkdownProps { @@ -42,10 +43,16 @@ export function Markdown({ children, className }: MarkdownProps) { '[&_hr]:border-border [&_hr]:my-4', // Images '[&_img]:max-w-full [&_img]:h-auto [&_img]:rounded-lg [&_img]:my-2 [&_img]:border [&_img]:border-border', + // Tables + '[&_table]:w-full [&_table]:border-collapse [&_table]:my-4', + '[&_th]:border [&_th]:border-border [&_th]:bg-muted [&_th]:px-3 [&_th]:py-2 [&_th]:text-left [&_th]:text-foreground [&_th]:font-semibold', + '[&_td]:border [&_td]:border-border [&_td]:px-3 [&_td]:py-2 [&_td]:text-foreground-secondary', className )} > - {children} + + {children} + ); } diff --git a/apps/ui/src/components/views/board-view/dialogs/plan-approval-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/plan-approval-dialog.tsx index d49d408e0..0645baa48 100644 --- a/apps/ui/src/components/views/board-view/dialogs/plan-approval-dialog.tsx +++ b/apps/ui/src/components/views/board-view/dialogs/plan-approval-dialog.tsx @@ -11,7 +11,7 @@ import { } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; -import { Markdown } from '@/components/ui/markdown'; +import { LogViewer } from '@/components/ui/log-viewer'; import { Label } from '@/components/ui/label'; import { Feature } from '@/store/app-store'; import { Check, RefreshCw, Edit2, Eye } from 'lucide-react'; @@ -135,9 +135,10 @@ export function PlanApprovalDialog({ disabled={isLoading} /> ) : ( -
- {editedPlan || 'No plan content available.'} -
+ )} diff --git a/package-lock.json b/package-lock.json index c86ba4aa9..405cf2c6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -150,6 +150,8 @@ "react-markdown": "10.1.0", "react-resizable-panels": "3.0.6", "rehype-raw": "7.0.0", + "rehype-sanitize": "^6.0.0", + "remark-gfm": "^4.0.1", "sonner": "2.0.7", "tailwind-merge": "3.4.0", "usehooks-ts": "3.1.1", @@ -1532,7 +1534,7 @@ }, "node_modules/@electron/node-gyp": { "version": "10.2.0-electron.1", - "resolved": "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", + "resolved": "git+ssh://git@github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", "integrity": "sha512-4MSBTT8y07YUDqf69/vSh80Hh791epYqGtWHO3zSKhYFwQg+gx9wi1PqbqP6YqC4WMsNxZ5l9oDmnWdK5pfCKQ==", "dev": true, "license": "MIT", @@ -6218,7 +6220,6 @@ "version": "19.2.7", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", - "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -6228,7 +6229,7 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -8439,7 +8440,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, "node_modules/d3-color": { @@ -11333,6 +11333,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11354,6 +11355,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11375,6 +11377,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11396,6 +11399,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11417,6 +11421,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11438,6 +11443,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11459,6 +11465,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11480,6 +11487,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11501,6 +11509,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11522,6 +11531,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11543,6 +11553,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -12108,6 +12119,16 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -12131,6 +12152,34 @@ "node": ">= 0.4" } }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mdast-util-from-markdown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", @@ -12155,6 +12204,107 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-mdx-expression": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", @@ -12374,6 +12524,127 @@ "micromark-util-types": "^2.0.0" } }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/micromark-factory-destination": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", @@ -14162,6 +14433,24 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-parse": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", @@ -14195,6 +14484,21 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", From 219693516f50f05ad3463a631a81aa6f7da28fcb Mon Sep 17 00:00:00 2001 From: Stefan de Vogelaere Date: Thu, 22 Jan 2026 16:22:50 +0100 Subject: [PATCH 2/7] chore: fix git+ssh URL and prettier formatting - Convert git+ssh:// to git+https:// in package-lock.json for @electron/node-gyp - Apply prettier formatting to plan-approval-dialog.tsx --- .../board-view/dialogs/plan-approval-dialog.tsx | 5 +---- package-lock.json | 17 ++++------------- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/apps/ui/src/components/views/board-view/dialogs/plan-approval-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/plan-approval-dialog.tsx index 0645baa48..f76011c92 100644 --- a/apps/ui/src/components/views/board-view/dialogs/plan-approval-dialog.tsx +++ b/apps/ui/src/components/views/board-view/dialogs/plan-approval-dialog.tsx @@ -135,10 +135,7 @@ export function PlanApprovalDialog({ disabled={isLoading} /> ) : ( - + )} diff --git a/package-lock.json b/package-lock.json index 405cf2c6a..4726a7d51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1534,7 +1534,7 @@ }, "node_modules/@electron/node-gyp": { "version": "10.2.0-electron.1", - "resolved": "git+ssh://git@github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", + "resolved": "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", "integrity": "sha512-4MSBTT8y07YUDqf69/vSh80Hh791epYqGtWHO3zSKhYFwQg+gx9wi1PqbqP6YqC4WMsNxZ5l9oDmnWdK5pfCKQ==", "dev": true, "license": "MIT", @@ -6220,6 +6220,7 @@ "version": "19.2.7", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -6229,7 +6230,7 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -8440,6 +8441,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, "license": "MIT" }, "node_modules/d3-color": { @@ -11333,7 +11335,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11355,7 +11356,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11377,7 +11377,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11399,7 +11398,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11421,7 +11419,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11443,7 +11440,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11465,7 +11461,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11487,7 +11482,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11509,7 +11503,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11531,7 +11524,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11553,7 +11545,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, From 09eb2895a4609336bf052a517417d557d5667436 Mon Sep 17 00:00:00 2001 From: Stefan de Vogelaere Date: Thu, 22 Jan 2026 16:57:00 +0100 Subject: [PATCH 3/7] fix(ui): create PlanContentViewer for better plan display The previous LogViewer approach showed tool calls prominently but hid the actual plan/specification markdown content. The new PlanContentViewer: - Separates tool calls (exploration) from plan markdown - Shows the plan/specification markdown prominently using Markdown component - Collapses tool calls by default in an "Exploration" section - Properly renders GFM tables in the plan content This provides a better UX where users see the important plan content first, with tool calls available but not distracting. --- .../dialogs/plan-approval-dialog.tsx | 4 +- .../dialogs/plan-content-viewer.tsx | 208 ++++++++++++++++++ 2 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 apps/ui/src/components/views/board-view/dialogs/plan-content-viewer.tsx diff --git a/apps/ui/src/components/views/board-view/dialogs/plan-approval-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/plan-approval-dialog.tsx index f76011c92..e5fc5d6e6 100644 --- a/apps/ui/src/components/views/board-view/dialogs/plan-approval-dialog.tsx +++ b/apps/ui/src/components/views/board-view/dialogs/plan-approval-dialog.tsx @@ -11,7 +11,7 @@ import { } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; -import { LogViewer } from '@/components/ui/log-viewer'; +import { PlanContentViewer } from './plan-content-viewer'; import { Label } from '@/components/ui/label'; import { Feature } from '@/store/app-store'; import { Check, RefreshCw, Edit2, Eye } from 'lucide-react'; @@ -135,7 +135,7 @@ export function PlanApprovalDialog({ disabled={isLoading} /> ) : ( - + )} diff --git a/apps/ui/src/components/views/board-view/dialogs/plan-content-viewer.tsx b/apps/ui/src/components/views/board-view/dialogs/plan-content-viewer.tsx new file mode 100644 index 000000000..02c50c5c5 --- /dev/null +++ b/apps/ui/src/components/views/board-view/dialogs/plan-content-viewer.tsx @@ -0,0 +1,208 @@ +'use client'; + +import { useMemo, useState } from 'react'; +import { ChevronDown, ChevronRight, Wrench } from 'lucide-react'; +import { Markdown } from '@/components/ui/markdown'; +import { cn } from '@/lib/utils'; + +interface ToolCall { + tool: string; + input: string; +} + +interface ParsedPlanContent { + toolCalls: ToolCall[]; + planMarkdown: string; +} + +/** + * Parses plan content to separate tool calls from the actual plan/specification markdown. + * Tool calls appear at the beginning (exploration phase), followed by the plan markdown. + */ +function parsePlanContent(content: string): ParsedPlanContent { + const lines = content.split('\n'); + const toolCalls: ToolCall[] = []; + let planStartIndex = -1; + + let currentTool: string | null = null; + let currentInput: string[] = []; + let inJsonBlock = false; + let braceDepth = 0; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const trimmed = line.trim(); + + // Check if this line starts the actual plan/spec (markdown heading) + // Plans typically start with # or ## headings + if ( + !inJsonBlock && + !currentTool && + (trimmed.match(/^#{1,3}\s+\w/) || // Markdown headings + trimmed.startsWith('---') || // Horizontal rule often used as separator + trimmed.match(/^\*\*[A-Z]/)) // Bold text starting a section + ) { + planStartIndex = i; + break; + } + + // Detect tool call start + const toolMatch = trimmed.match(/^(?:🔧\s*)?Tool:\s*(\w+)/i); + if (toolMatch && !inJsonBlock) { + // Save previous tool call if exists + if (currentTool && currentInput.length > 0) { + toolCalls.push({ + tool: currentTool, + input: currentInput.join('\n').trim(), + }); + } + currentTool = toolMatch[1]; + currentInput = []; + continue; + } + + // Detect Input: line + if (trimmed.startsWith('Input:') && currentTool) { + const inputContent = trimmed.replace(/^Input:\s*/, ''); + if (inputContent) { + currentInput.push(inputContent); + // Check if JSON starts + if (inputContent.includes('{')) { + braceDepth = + (inputContent.match(/\{/g) || []).length - (inputContent.match(/\}/g) || []).length; + inJsonBlock = braceDepth > 0; + } + } + continue; + } + + // If we're collecting input for a tool + if (currentTool) { + if (inJsonBlock) { + currentInput.push(line); + braceDepth += (trimmed.match(/\{/g) || []).length - (trimmed.match(/\}/g) || []).length; + if (braceDepth <= 0) { + inJsonBlock = false; + // Save tool call + toolCalls.push({ + tool: currentTool, + input: currentInput.join('\n').trim(), + }); + currentTool = null; + currentInput = []; + } + } else if (trimmed.startsWith('{')) { + // JSON block starting + currentInput.push(line); + braceDepth = (trimmed.match(/\{/g) || []).length - (trimmed.match(/\}/g) || []).length; + inJsonBlock = braceDepth > 0; + if (!inJsonBlock) { + // Single-line JSON + toolCalls.push({ + tool: currentTool, + input: currentInput.join('\n').trim(), + }); + currentTool = null; + currentInput = []; + } + } else if (trimmed === '') { + // Empty line might end the tool call section + if (currentInput.length > 0) { + toolCalls.push({ + tool: currentTool, + input: currentInput.join('\n').trim(), + }); + currentTool = null; + currentInput = []; + } + } + } + } + + // Save any remaining tool call + if (currentTool && currentInput.length > 0) { + toolCalls.push({ + tool: currentTool, + input: currentInput.join('\n').trim(), + }); + } + + // Extract plan markdown + let planMarkdown = ''; + if (planStartIndex >= 0) { + planMarkdown = lines.slice(planStartIndex).join('\n').trim(); + } else if (toolCalls.length === 0) { + // No tool calls found, treat entire content as markdown + planMarkdown = content.trim(); + } + + return { toolCalls, planMarkdown }; +} + +interface PlanContentViewerProps { + content: string; + className?: string; +} + +export function PlanContentViewer({ content, className }: PlanContentViewerProps) { + const [showToolCalls, setShowToolCalls] = useState(false); + + const { toolCalls, planMarkdown } = useMemo(() => parsePlanContent(content), [content]); + + if (!content || !content.trim()) { + return ( +
+ No plan content available. +
+ ); + } + + return ( +
+ {/* Tool Calls Section - Collapsed by default */} + {toolCalls.length > 0 && ( +
+ + + {showToolCalls && ( +
+ {toolCalls.map((tc, idx) => ( +
+
Tool: {tc.tool}
+
+                    {tc.input}
+                  
+
+ ))} +
+ )} +
+ )} + + {/* Plan/Specification Content - Main focus */} + {planMarkdown ? ( +
+ {planMarkdown} +
+ ) : toolCalls.length > 0 ? ( +
+

No specification content found.

+

The plan appears to only contain exploration tool calls.

+
+ ) : null} +
+ ); +} From 56505f029cd93ea2888bfbbff59b95af20d925c5 Mon Sep 17 00:00:00 2001 From: Stefan de Vogelaere Date: Thu, 22 Jan 2026 17:05:08 +0100 Subject: [PATCH 4/7] fix(ui): add show more/less toggle for feature description The feature description in the plan approval dialog header was truncated at 150 characters with no way to see the full text. Now users can click "show more" to expand and "show less" to collapse. --- .../dialogs/plan-approval-dialog.tsx | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/apps/ui/src/components/views/board-view/dialogs/plan-approval-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/plan-approval-dialog.tsx index e5fc5d6e6..8a379694d 100644 --- a/apps/ui/src/components/views/board-view/dialogs/plan-approval-dialog.tsx +++ b/apps/ui/src/components/views/board-view/dialogs/plan-approval-dialog.tsx @@ -42,6 +42,9 @@ export function PlanApprovalDialog({ const [editedPlan, setEditedPlan] = useState(planContent); const [showRejectFeedback, setShowRejectFeedback] = useState(false); const [rejectFeedback, setRejectFeedback] = useState(''); + const [showFullDescription, setShowFullDescription] = useState(false); + + const DESCRIPTION_LIMIT = 150; // Reset state when dialog opens or plan content changes useEffect(() => { @@ -50,6 +53,7 @@ export function PlanApprovalDialog({ setIsEditMode(false); setShowRejectFeedback(false); setRejectFeedback(''); + setShowFullDescription(false); } }, [open, planContent]); @@ -89,8 +93,19 @@ export function PlanApprovalDialog({ : 'Review the generated plan before implementation begins.'} {feature && ( - Feature: {feature.description.slice(0, 150)} - {feature.description.length > 150 ? '...' : ''} + Feature:{' '} + {showFullDescription || feature.description.length <= DESCRIPTION_LIMIT + ? feature.description + : `${feature.description.slice(0, DESCRIPTION_LIMIT)}...`} + {feature.description.length > DESCRIPTION_LIMIT && ( + + )} )} From 820498769a0827f0267a80a16fa3dc14f72d2288 Mon Sep 17 00:00:00 2001 From: Stefan de Vogelaere Date: Thu, 22 Jan 2026 17:06:52 +0100 Subject: [PATCH 5/7] fix(ui): increase description limit and add feature title to dialog - Increase description character limit from 150 to 250 characters - Add feature title to dialog header (e.g., "Review Plan - Feature Title") only if title exists and is <= 50 characters --- .../views/board-view/dialogs/plan-approval-dialog.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/ui/src/components/views/board-view/dialogs/plan-approval-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/plan-approval-dialog.tsx index 8a379694d..f0e64102d 100644 --- a/apps/ui/src/components/views/board-view/dialogs/plan-approval-dialog.tsx +++ b/apps/ui/src/components/views/board-view/dialogs/plan-approval-dialog.tsx @@ -44,7 +44,8 @@ export function PlanApprovalDialog({ const [rejectFeedback, setRejectFeedback] = useState(''); const [showFullDescription, setShowFullDescription] = useState(false); - const DESCRIPTION_LIMIT = 150; + const DESCRIPTION_LIMIT = 250; + const TITLE_LIMIT = 50; // Reset state when dialog opens or plan content changes useEffect(() => { @@ -86,7 +87,12 @@ export function PlanApprovalDialog({ - {viewOnly ? 'View Plan' : 'Review Plan'} + + {viewOnly ? 'View Plan' : 'Review Plan'} + {feature?.title && feature.title.length <= TITLE_LIMIT && ( + - {feature.title} + )} + {viewOnly ? 'View the generated plan for this feature.' From 376077e4e8c83e718377a10bec3c226f2140c9b6 Mon Sep 17 00:00:00 2001 From: Stefan de Vogelaere Date: Thu, 22 Jan 2026 17:11:58 +0100 Subject: [PATCH 6/7] feat(ui): render tasks code blocks as proper checkbox lists When markdown contains a ```tasks code block, it now renders as: - Phase headers (## Phase 1: ...) as styled section headings - Task items (- [ ] or - [x]) with proper checkbox icons - Checked items show green checkmark and strikethrough text - Unchecked items show empty square icon This makes implementation task lists in plans much more readable compared to rendering them as raw code blocks. --- apps/ui/src/components/ui/markdown.tsx | 91 +++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 2 deletions(-) diff --git a/apps/ui/src/components/ui/markdown.tsx b/apps/ui/src/components/ui/markdown.tsx index 74556fef8..ff7facbf1 100644 --- a/apps/ui/src/components/ui/markdown.tsx +++ b/apps/ui/src/components/ui/markdown.tsx @@ -1,14 +1,97 @@ -import ReactMarkdown from 'react-markdown'; +import ReactMarkdown, { Components } from 'react-markdown'; import rehypeRaw from 'rehype-raw'; import rehypeSanitize from 'rehype-sanitize'; import remarkGfm from 'remark-gfm'; import { cn } from '@/lib/utils'; +import { Square, CheckSquare } from 'lucide-react'; interface MarkdownProps { children: string; className?: string; } +/** + * Renders a tasks code block as a proper task list with checkboxes + */ +function TasksBlock({ content }: { content: string }) { + const lines = content.split('\n'); + + return ( +
+ {lines.map((line, idx) => { + const trimmed = line.trim(); + + // Check for phase/section headers (## Phase 1: ...) + const headerMatch = trimmed.match(/^##\s+(.+)$/); + if (headerMatch) { + return ( +
+ {headerMatch[1]} +
+ ); + } + + // Check for task items (- [ ] or - [x]) + const taskMatch = trimmed.match(/^-\s*\[([ xX])\]\s*(.+)$/); + if (taskMatch) { + const isChecked = taskMatch[1].toLowerCase() === 'x'; + const taskText = taskMatch[2]; + + return ( +
+ {isChecked ? ( + + ) : ( + + )} + + {taskText} + +
+ ); + } + + // Empty lines + if (!trimmed) { + return
; + } + + // Other content (render as-is) + return ( +
+ {trimmed} +
+ ); + })} +
+ ); +} + +/** + * Custom components for ReactMarkdown + */ +const markdownComponents: Components = { + // Handle code blocks - special case for 'tasks' language + code({ className, children }) { + const match = /language-(\w+)/.exec(className || ''); + const language = match ? match[1] : ''; + const content = String(children).replace(/\n$/, ''); + + // Special handling for tasks code blocks + if (language === 'tasks') { + return ; + } + + // Regular code (inline or block) + return {children}; + }, +}; + /** * Reusable Markdown component for rendering markdown content * Theme-aware styling that adapts to all predefined themes @@ -50,7 +133,11 @@ export function Markdown({ children, className }: MarkdownProps) { className )} > - + {children}
From 19d9a2951e7c0d977c96a082ef1a816a7ace8dab Mon Sep 17 00:00:00 2001 From: Stefan de Vogelaere Date: Thu, 22 Jan 2026 17:31:37 +0100 Subject: [PATCH 7/7] fix(ui): improve plan content parsing robustness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address CodeRabbit review feedback: 1. Relax heading detection regex to match emoji and non-word chars - Change \w to \S so headings like "## ✅ Plan" are detected - Change \*\*[A-Z] to \*\*\S for bold section detection 2. Flush active tool call when heading is detected - Prevents plan content being dropped when heading follows tool call without a blank line separator 3. Support tool names with dots/hyphens - Change \w+ to [^\s]+ so names like "web.run" or "file-read" work --- .../board-view/dialogs/plan-content-viewer.tsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/apps/ui/src/components/views/board-view/dialogs/plan-content-viewer.tsx b/apps/ui/src/components/views/board-view/dialogs/plan-content-viewer.tsx index 02c50c5c5..dd90b0f42 100644 --- a/apps/ui/src/components/views/board-view/dialogs/plan-content-viewer.tsx +++ b/apps/ui/src/components/views/board-view/dialogs/plan-content-viewer.tsx @@ -37,17 +37,25 @@ function parsePlanContent(content: string): ParsedPlanContent { // Plans typically start with # or ## headings if ( !inJsonBlock && - !currentTool && - (trimmed.match(/^#{1,3}\s+\w/) || // Markdown headings + (trimmed.match(/^#{1,3}\s+\S/) || // Markdown headings (including emoji like ## ✅ Plan) trimmed.startsWith('---') || // Horizontal rule often used as separator - trimmed.match(/^\*\*[A-Z]/)) // Bold text starting a section + trimmed.match(/^\*\*\S/)) // Bold text starting a section ) { + // Flush any active tool call before starting the plan + if (currentTool && currentInput.length > 0) { + toolCalls.push({ + tool: currentTool, + input: currentInput.join('\n').trim(), + }); + currentTool = null; + currentInput = []; + } planStartIndex = i; break; } - // Detect tool call start - const toolMatch = trimmed.match(/^(?:🔧\s*)?Tool:\s*(\w+)/i); + // Detect tool call start (supports tool names with dots/hyphens like web.run, file-read) + const toolMatch = trimmed.match(/^(?:🔧\s*)?Tool:\s*([^\s]+)/i); if (toolMatch && !inJsonBlock) { // Save previous tool call if exists if (currentTool && currentInput.length > 0) {