diff --git a/ccw/frontend/package-lock.json b/ccw/frontend/package-lock.json
index 31e60397..8f95adad 100644
--- a/ccw/frontend/package-lock.json
+++ b/ccw/frontend/package-lock.json
@@ -12,6 +12,7 @@
"@hello-pangea/dnd": "^18.0.1",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-checkbox": "^1.3.3",
+ "@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.0",
"@radix-ui/react-dropdown-menu": "^2.1.0",
"@radix-ui/react-label": "^2.1.8",
@@ -25,11 +26,15 @@
"@xyflow/react": "^12.10.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
+ "highlight.js": "^11.11.1",
"lucide-react": "^0.460.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-intl": "^6.8.9",
+ "react-markdown": "^10.1.0",
"react-router-dom": "^6.28.0",
+ "rehype-highlight": "^7.0.2",
+ "remark-gfm": "^4.0.1",
"tailwind-merge": "^2.5.0",
"zod": "^3.23.8",
"zustand": "^5.0.0"
@@ -1467,6 +1472,36 @@
}
}
},
+ "node_modules/@radix-ui/react-collapsible": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz",
+ "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-collection": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
@@ -2966,13 +3001,39 @@
"@types/d3-selection": "*"
}
},
+ "node_modules/@types/debug": {
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
+ "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/ms": "*"
+ }
+ },
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
- "dev": true,
"license": "MIT"
},
+ "node_modules/@types/estree-jsx": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
+ "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*"
+ }
+ },
+ "node_modules/@types/hast": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
"node_modules/@types/hoist-non-react-statics": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz",
@@ -2985,6 +3046,21 @@
"@types/react": "*"
}
},
+ "node_modules/@types/mdast": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
+ "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/ms": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+ "license": "MIT"
+ },
"node_modules/@types/prop-types": {
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
@@ -3011,12 +3087,24 @@
"@types/react": "^18.0.0"
}
},
+ "node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT"
+ },
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
"license": "MIT"
},
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
+ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
+ "license": "ISC"
+ },
"node_modules/@vitejs/plugin-react": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
@@ -3409,6 +3497,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/bail": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
+ "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -3587,6 +3685,16 @@
],
"license": "CC-BY-4.0"
},
+ "node_modules/ccount": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
+ "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/chai": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
@@ -3637,6 +3745,46 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
+ "node_modules/character-entities": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
+ "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-html4": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
+ "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-legacy": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
+ "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-reference-invalid": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
+ "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/check-error": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
@@ -3745,6 +3893,16 @@
"node": ">= 0.8"
}
},
+ "node_modules/comma-separated-tokens": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
+ "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@@ -3956,7 +4114,6 @@
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -3976,6 +4133,19 @@
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
"license": "MIT"
},
+ "node_modules/decode-named-character-reference": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",
+ "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/deep-eql": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
@@ -4069,7 +4239,6 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -4081,6 +4250,19 @@
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
"license": "MIT"
},
+ "node_modules/devlop": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -4280,6 +4462,28 @@
"node": ">=6"
}
},
+ "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/estree-util-is-identifier-name": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
+ "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
@@ -4300,6 +4504,12 @@
"node": ">=12.0.0"
}
},
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "license": "MIT"
+ },
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
@@ -4662,6 +4872,84 @@
"node": ">= 0.4"
}
},
+ "node_modules/hast-util-is-element": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz",
+ "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-to-jsx-runtime": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
+ "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "devlop": "^1.0.0",
+ "estree-util-is-identifier-name": "^3.0.0",
+ "hast-util-whitespace": "^3.0.0",
+ "mdast-util-mdx-expression": "^2.0.0",
+ "mdast-util-mdx-jsx": "^3.0.0",
+ "mdast-util-mdxjs-esm": "^2.0.0",
+ "property-information": "^7.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "style-to-js": "^1.0.0",
+ "unist-util-position": "^5.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-to-text": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz",
+ "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==",
+ "license": "MIT",
+ "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"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-whitespace": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
+ "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/highlight.js": {
+ "version": "11.11.1",
+ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
+ "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@@ -4691,6 +4979,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/html-url-attributes": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
+ "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
@@ -4742,6 +5040,12 @@
"node": ">=8"
}
},
+ "node_modules/inline-style-parser": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
+ "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
+ "license": "MIT"
+ },
"node_modules/internal-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
@@ -4819,6 +5123,30 @@
"tslib": "2"
}
},
+ "node_modules/is-alphabetical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
+ "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-alphanumerical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
+ "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-alphabetical": "^2.0.0",
+ "is-decimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/is-arguments": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
@@ -4946,6 +5274,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-decimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
+ "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -4979,6 +5317,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-hexadecimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
+ "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/is-map": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
@@ -5019,6 +5367,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-plain-obj": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
+ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-potential-custom-element-name": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
@@ -5326,6 +5686,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/longest-streak": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
+ "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -5345,6 +5715,21 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/lowlight": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz",
+ "integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "devlop": "^1.0.0",
+ "highlight.js": "~11.11.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -5425,6 +5810,16 @@
"node": ">=10"
}
},
+ "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/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -5435,6 +5830,276 @@
"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-from-markdown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz",
+ "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark": "^4.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "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",
+ "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
+ "license": "MIT",
+ "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"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-jsx": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",
+ "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
+ "license": "MIT",
+ "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"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdxjs-esm": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
+ "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
+ "license": "MIT",
+ "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"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-phrasing": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
+ "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-hast": {
+ "version": "13.2.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
+ "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "trim-lines": "^3.0.0",
+ "unist-util-position": "^5.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-markdown": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
+ "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "longest-streak": "^3.0.0",
+ "mdast-util-phrasing": "^4.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "unist-util-visit": "^5.0.0",
+ "zwitch": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
+ "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -5445,6 +6110,569 @@
"node": ">= 8"
}
},
+ "node_modules/micromark": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
+ "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@types/debug": "^4.0.0",
+ "debug": "^4.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-core-commonmark": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz",
+ "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "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"
+ }
+ },
+ "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",
+ "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-label": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
+ "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-space": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
+ "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-title": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
+ "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-whitespace": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
+ "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-character": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
+ "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-chunked": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
+ "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-classify-character": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
+ "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-combine-extensions": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
+ "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-numeric-character-reference": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
+ "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-string": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",
+ "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-encode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
+ "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-html-tag-name": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
+ "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-normalize-identifier": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
+ "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-resolve-all": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
+ "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-sanitize-uri": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
+ "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-subtokenize": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",
+ "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-symbol": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
+ "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-types": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
+ "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
@@ -5532,7 +6760,6 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
"license": "MIT"
},
"node_modules/mz": {
@@ -5678,6 +6905,31 @@
"dev": true,
"license": "BlueOak-1.0.0"
},
+ "node_modules/parse-entities": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
+ "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
+ "license": "MIT",
+ "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"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/parse-entities/node_modules/@types/unist": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
+ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
+ "license": "MIT"
+ },
"node_modules/parse5": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
@@ -6031,6 +7283,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/property-information": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
+ "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -6176,6 +7438,33 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
+ "node_modules/react-markdown": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
+ "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "hast-util-to-jsx-runtime": "^2.0.0",
+ "html-url-attributes": "^3.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-rehype": "^11.0.0",
+ "unified": "^11.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18",
+ "react": ">=18"
+ }
+ },
"node_modules/react-redux": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
@@ -6374,6 +7663,89 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/rehype-highlight": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-7.0.2.tgz",
+ "integrity": "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "hast-util-to-text": "^4.0.0",
+ "lowlight": "^3.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "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",
+ "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-rehype": {
+ "version": "11.1.2",
+ "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz",
+ "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",
+ "license": "MIT",
+ "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"
+ },
+ "funding": {
+ "type": "opencollective",
+ "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/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
@@ -6727,6 +8099,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/space-separated-tokens": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
+ "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/stackback": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
@@ -6809,6 +8191,20 @@
"node": ">=8"
}
},
+ "node_modules/stringify-entities": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
+ "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities-html4": "^2.0.0",
+ "character-entities-legacy": "^3.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/strip-ansi": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
@@ -6865,6 +8261,24 @@
"node": ">=8"
}
},
+ "node_modules/style-to-js": {
+ "version": "1.1.21",
+ "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz",
+ "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "style-to-object": "1.0.14"
+ }
+ },
+ "node_modules/style-to-object": {
+ "version": "1.0.14",
+ "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",
+ "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",
+ "license": "MIT",
+ "dependencies": {
+ "inline-style-parser": "0.2.7"
+ }
+ },
"node_modules/sucrase": {
"version": "3.35.1",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
@@ -7184,6 +8598,26 @@
"node": ">=18"
}
},
+ "node_modules/trim-lines": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
+ "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/trough": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
+ "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/ts-interface-checker": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
@@ -7715,6 +9149,107 @@
"node": ">=14.17"
}
},
+ "node_modules/unified": {
+ "version": "11.0.5",
+ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
+ "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "bail": "^2.0.0",
+ "devlop": "^1.0.0",
+ "extend": "^3.0.0",
+ "is-plain-obj": "^4.0.0",
+ "trough": "^2.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-find-after": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz",
+ "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-is": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
+ "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-position": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
+ "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-stringify-position": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
+ "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz",
+ "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.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/unist-util-visit-parents": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
+ "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/update-browserslist-db": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
@@ -7805,6 +9340,34 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/vfile": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
+ "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile-message": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
+ "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/vite": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
@@ -9346,6 +10909,16 @@
"optional": true
}
}
+ },
+ "node_modules/zwitch": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
+ "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
}
}
}
diff --git a/ccw/frontend/package.json b/ccw/frontend/package.json
index d3b9357e..21a13179 100644
--- a/ccw/frontend/package.json
+++ b/ccw/frontend/package.json
@@ -21,6 +21,7 @@
"@hello-pangea/dnd": "^18.0.1",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-checkbox": "^1.3.3",
+ "@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.0",
"@radix-ui/react-dropdown-menu": "^2.1.0",
"@radix-ui/react-label": "^2.1.8",
@@ -34,11 +35,15 @@
"@xyflow/react": "^12.10.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
+ "highlight.js": "^11.11.1",
"lucide-react": "^0.460.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-intl": "^6.8.9",
+ "react-markdown": "^10.1.0",
"react-router-dom": "^6.28.0",
+ "rehype-highlight": "^7.0.2",
+ "remark-gfm": "^4.0.1",
"tailwind-merge": "^2.5.0",
"zod": "^3.23.8",
"zustand": "^5.0.0"
diff --git a/ccw/frontend/src/components/issue/hub/DiscoveryPanel.tsx b/ccw/frontend/src/components/issue/hub/DiscoveryPanel.tsx
new file mode 100644
index 00000000..f99d5faf
--- /dev/null
+++ b/ccw/frontend/src/components/issue/hub/DiscoveryPanel.tsx
@@ -0,0 +1,153 @@
+// ========================================
+// Discovery Panel
+// ========================================
+// Content panel for Discovery tab in IssueHub
+
+import { useIntl } from 'react-intl';
+import { Radar, AlertCircle, Loader2 } from 'lucide-react';
+import { Card } from '@/components/ui/Card';
+import { Badge } from '@/components/ui/Badge';
+import { useIssueDiscovery } from '@/hooks/useIssues';
+import { DiscoveryCard } from '@/components/issue/discovery/DiscoveryCard';
+import { DiscoveryDetail } from '@/components/issue/discovery/DiscoveryDetail';
+
+// ========== Main Panel Component ==========
+
+export function DiscoveryPanel() {
+ const { formatMessage } = useIntl();
+
+ const {
+ sessions,
+ activeSession,
+ findings,
+ isLoadingSessions,
+ isLoadingFindings,
+ error,
+ filters,
+ setFilters,
+ selectSession,
+ exportFindings,
+ } = useIssueDiscovery({ refetchInterval: 3000 });
+
+ if (error) {
+ return (
+
+
+
+ {formatMessage({ id: 'common.error' })}
+
+ {error.message}
+
+ );
+ }
+
+ return (
+
+ {/* Stats Cards */}
+
+
+
+
+ {sessions.length}
+
+
+ {formatMessage({ id: 'issues.discovery.totalSessions' })}
+
+
+
+
+
+ {sessions.filter(s => s.status === 'completed').length}
+
+ {sessions.filter(s => s.status === 'completed').length}
+
+
+ {formatMessage({ id: 'issues.discovery.completedSessions' })}
+
+
+
+
+
+ {sessions.filter(s => s.status === 'running').length}
+
+ {sessions.filter(s => s.status === 'running').length}
+
+
+ {formatMessage({ id: 'issues.discovery.runningSessions' })}
+
+
+
+
+
+ {sessions.reduce((sum, s) => sum + s.findings_count, 0)}
+
+
+
+ {formatMessage({ id: 'issues.discovery.totalFindings' })}
+
+
+
+
+ {/* Main Content: Split Pane */}
+
+ {/* Left: Session List */}
+
+
+ {formatMessage({ id: 'issues.discovery.sessionList' })}
+
+
+ {isLoadingSessions ? (
+
+ {[1, 2, 3].map((i) => (
+
+ ))}
+
+ ) : sessions.length === 0 ? (
+
+
+
+ {formatMessage({ id: 'issues.discovery.noSessions' })}
+
+
+ {formatMessage({ id: 'issues.discovery.noSessionsDescription' })}
+
+
+ ) : (
+
+ {sessions.map((session) => (
+ selectSession(session.id)}
+ />
+ ))}
+
+ )}
+
+
+ {/* Right: Findings Detail */}
+
+
+ {formatMessage({ id: 'issues.discovery.findingsDetail' })}
+
+
+ {isLoadingFindings ? (
+
+
+
+ ) : (
+
+ )}
+
+
+
+ );
+}
diff --git a/ccw/frontend/src/components/issue/hub/IssueHubHeader.tsx b/ccw/frontend/src/components/issue/hub/IssueHubHeader.tsx
new file mode 100644
index 00000000..75f0fcb9
--- /dev/null
+++ b/ccw/frontend/src/components/issue/hub/IssueHubHeader.tsx
@@ -0,0 +1,52 @@
+// ========================================
+// Issue Hub Header
+// ========================================
+// Dynamic header component for IssueHub
+
+import { useIntl } from 'react-intl';
+import { AlertCircle, Radar, ListTodo } from 'lucide-react';
+
+type IssueTab = 'issues' | 'queue' | 'discovery';
+
+interface IssueHubHeaderProps {
+ currentTab: IssueTab;
+}
+
+export function IssueHubHeader({ currentTab }: IssueHubHeaderProps) {
+ const { formatMessage } = useIntl();
+
+ // Tab configuration with icons and labels
+ const tabConfig = {
+ issues: {
+ icon: ,
+ title: formatMessage({ id: 'issues.title' }),
+ description: formatMessage({ id: 'issues.description' }),
+ },
+ queue: {
+ icon: ,
+ title: formatMessage({ id: 'issues.queue.pageTitle' }),
+ description: formatMessage({ id: 'issues.queue.description' }),
+ },
+ discovery: {
+ icon: ,
+ title: formatMessage({ id: 'issues.discovery.pageTitle' }),
+ description: formatMessage({ id: 'issues.discovery.description' }),
+ },
+ };
+
+ const config = tabConfig[currentTab];
+
+ return (
+
+ {config.icon}
+
+
+ {config.title}
+
+
+ {config.description}
+
+
+
+ );
+}
diff --git a/ccw/frontend/src/components/issue/hub/IssueHubTabs.tsx b/ccw/frontend/src/components/issue/hub/IssueHubTabs.tsx
new file mode 100644
index 00000000..083811c8
--- /dev/null
+++ b/ccw/frontend/src/components/issue/hub/IssueHubTabs.tsx
@@ -0,0 +1,45 @@
+// ========================================
+// Issue Hub Tabs
+// ========================================
+// Tab navigation for IssueHub
+
+import { useIntl } from 'react-intl';
+import { Button } from '@/components/ui/Button';
+import { cn } from '@/lib/utils';
+
+export type IssueTab = 'issues' | 'queue' | 'discovery';
+
+interface IssueHubTabsProps {
+ currentTab: IssueTab;
+ onTabChange: (tab: IssueTab) => void;
+}
+
+export function IssueHubTabs({ currentTab, onTabChange }: IssueHubTabsProps) {
+ const { formatMessage } = useIntl();
+
+ const tabs: Array<{ value: IssueTab; label: string }> = [
+ { value: 'issues', label: formatMessage({ id: 'issues.hub.tabs.issues' }) },
+ { value: 'queue', label: formatMessage({ id: 'issues.hub.tabs.queue' }) },
+ { value: 'discovery', label: formatMessage({ id: 'issues.hub.tabs.discovery' }) },
+ ];
+
+ return (
+
+ {tabs.map((tab) => (
+
+ ))}
+
+ );
+}
diff --git a/ccw/frontend/src/components/issue/hub/IssuesPanel.tsx b/ccw/frontend/src/components/issue/hub/IssuesPanel.tsx
new file mode 100644
index 00000000..01545b8f
--- /dev/null
+++ b/ccw/frontend/src/components/issue/hub/IssuesPanel.tsx
@@ -0,0 +1,320 @@
+// ========================================
+// Issues Panel
+// ========================================
+// Issue list panel for IssueHub
+
+import { useState, useMemo } from 'react';
+import { useIntl } from 'react-intl';
+import {
+ Plus,
+ Search,
+ RefreshCw,
+ Loader2,
+ Github,
+ CheckCircle,
+ Clock,
+ AlertTriangle,
+ AlertCircle,
+} from 'lucide-react';
+import { Card } from '@/components/ui/Card';
+import { Button } from '@/components/ui/Button';
+import { Input } from '@/components/ui/Input';
+import { Badge } from '@/components/ui/Badge';
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/Dialog';
+import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/Select';
+import { IssueCard } from '@/components/shared/IssueCard';
+import { useIssues, useIssueMutations } from '@/hooks';
+import type { Issue } from '@/lib/api';
+import { cn } from '@/lib/utils';
+
+type StatusFilter = 'all' | Issue['status'];
+type PriorityFilter = 'all' | Issue['priority'];
+
+interface NewIssueDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ onSubmit: (data: { title: string; context?: string; priority?: Issue['priority'] }) => void;
+ isCreating: boolean;
+}
+
+function NewIssueDialog({ open, onOpenChange, onSubmit, isCreating }: NewIssueDialogProps) {
+ const { formatMessage } = useIntl();
+ const [title, setTitle] = useState('');
+ const [context, setContext] = useState('');
+ const [priority, setPriority] = useState('medium');
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (title.trim()) {
+ onSubmit({ title: title.trim(), context: context.trim() || undefined, priority });
+ setTitle('');
+ setContext('');
+ setPriority('medium');
+ }
+ };
+
+ return (
+
+ );
+}
+
+interface IssueListProps {
+ issues: Issue[];
+ isLoading: boolean;
+ onIssueClick: (issue: Issue) => void;
+ onIssueEdit: (issue: Issue) => void;
+ onIssueDelete: (issue: Issue) => void;
+ onStatusChange: (issue: Issue, status: Issue['status']) => void;
+}
+
+function IssueList({ issues, isLoading, onIssueClick, onIssueEdit, onIssueDelete, onStatusChange }: IssueListProps) {
+ const { formatMessage } = useIntl();
+
+ if (isLoading) {
+ return (
+
+ {[1, 2, 3].map((i) => (
+
+ ))}
+
+ );
+ }
+
+ if (issues.length === 0) {
+ return (
+
+
+ {formatMessage({ id: 'issues.emptyState.title' })}
+ {formatMessage({ id: 'issues.emptyState.message' })}
+
+ );
+ }
+
+ return (
+
+ {issues.map((issue) => (
+
+ ))}
+
+ );
+}
+
+export function IssuesPanel() {
+ const { formatMessage } = useIntl();
+ const [searchQuery, setSearchQuery] = useState('');
+ const [statusFilter, setStatusFilter] = useState('all');
+ const [priorityFilter, setPriorityFilter] = useState('all');
+ const [isNewIssueOpen, setIsNewIssueOpen] = useState(false);
+
+ const { issues, issuesByStatus, openCount, criticalCount, isLoading, isFetching, refetch } = useIssues({
+ filter: {
+ search: searchQuery || undefined,
+ status: statusFilter !== 'all' ? [statusFilter] : undefined,
+ priority: priorityFilter !== 'all' ? [priorityFilter] : undefined,
+ },
+ });
+
+ const { createIssue, updateIssue, deleteIssue, isCreating } = useIssueMutations();
+
+ const statusCounts = useMemo(() => ({
+ all: issues.length,
+ open: issuesByStatus.open?.length || 0,
+ in_progress: issuesByStatus.in_progress?.length || 0,
+ resolved: issuesByStatus.resolved?.length || 0,
+ closed: issuesByStatus.closed?.length || 0,
+ completed: issuesByStatus.completed?.length || 0,
+ }), [issues, issuesByStatus]);
+
+ const handleCreateIssue = async (data: { title: string; context?: string; priority?: Issue['priority'] }) => {
+ await createIssue(data);
+ setIsNewIssueOpen(false);
+ };
+
+ const handleEditIssue = (_issue: Issue) => {};
+
+ const handleDeleteIssue = async (issue: Issue) => {
+ if (confirm(`Delete issue "${issue.title}"?`)) {
+ await deleteIssue(issue.id);
+ }
+ };
+
+ const handleStatusChange = async (issue: Issue, status: Issue['status']) => {
+ await updateIssue(issue.id, { status });
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {formatMessage({ id: 'common.status.openIssues' })}
+
+
+
+
+ {issuesByStatus.in_progress?.length || 0}
+
+ {formatMessage({ id: 'issues.status.inProgress' })}
+
+
+
+ {formatMessage({ id: 'issues.priority.critical' })}
+
+
+
+
+ {issuesByStatus.resolved?.length || 0}
+
+ {formatMessage({ id: 'issues.status.resolved' })}
+
+
+
+
+
+
+ setSearchQuery(e.target.value)}
+ className="pl-9"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{}} onIssueEdit={handleEditIssue} onIssueDelete={handleDeleteIssue} onStatusChange={handleStatusChange} />
+
+
+
+ );
+}
diff --git a/ccw/frontend/src/components/issue/hub/QueuePanel.tsx b/ccw/frontend/src/components/issue/hub/QueuePanel.tsx
new file mode 100644
index 00000000..47f98797
--- /dev/null
+++ b/ccw/frontend/src/components/issue/hub/QueuePanel.tsx
@@ -0,0 +1,268 @@
+// ========================================
+// Queue Panel
+// ========================================
+// Content panel for Queue tab in IssueHub
+
+import { useIntl } from 'react-intl';
+import {
+ RefreshCw,
+ AlertCircle,
+ CheckCircle,
+ Clock,
+ ListTodo,
+ GitMerge,
+} from 'lucide-react';
+import { Card } from '@/components/ui/Card';
+import { Button } from '@/components/ui/Button';
+import { Badge } from '@/components/ui/Badge';
+import { QueueCard } from '@/components/issue/queue/QueueCard';
+import { useIssueQueue, useQueueMutations } from '@/hooks';
+import { cn } from '@/lib/utils';
+
+// ========== Loading Skeleton ==========
+
+function QueuePanelSkeleton() {
+ return (
+
+ {/* Stats Cards Skeleton */}
+
+ {[1, 2, 3, 4].map((i) => (
+
+
+
+
+ ))}
+
+
+ {/* Queue Cards Skeleton */}
+
+ {[1, 2].map((i) => (
+
+
+
+
+
+ ))}
+
+
+ );
+}
+
+// ========== Empty State ==========
+
+function QueueEmptyState() {
+ const { formatMessage } = useIntl();
+
+ return (
+
+
+
+ {formatMessage({ id: 'issues.queue.emptyState.title' })}
+
+
+ {formatMessage({ id: 'issues.queue.emptyState.description' })}
+
+
+ );
+}
+
+// ========== Main Panel Component ==========
+
+export function QueuePanel() {
+ const { formatMessage } = useIntl();
+
+ const { data: queueData, isLoading, isFetching, refetch, error } = useIssueQueue();
+ const {
+ activateQueue,
+ deactivateQueue,
+ deleteQueue,
+ mergeQueues,
+ isActivating,
+ isDeactivating,
+ isDeleting,
+ isMerging,
+ } = useQueueMutations();
+
+ // Get queue data with proper type
+ const queue = queueData;
+ const taskCount = queue?.tasks?.length || 0;
+ const solutionCount = queue?.solutions?.length || 0;
+ const conflictCount = queue?.conflicts?.length || 0;
+ const groupCount = Object.keys(queue?.grouped_items || {}).length;
+ const totalItems = taskCount + solutionCount;
+
+ const handleActivate = async (queueId: string) => {
+ try {
+ await activateQueue(queueId);
+ } catch (err) {
+ console.error('Failed to activate queue:', err);
+ }
+ };
+
+ const handleDeactivate = async () => {
+ try {
+ await deactivateQueue();
+ } catch (err) {
+ console.error('Failed to deactivate queue:', err);
+ }
+ };
+
+ const handleDelete = async (queueId: string) => {
+ try {
+ await deleteQueue(queueId);
+ } catch (err) {
+ console.error('Failed to delete queue:', err);
+ }
+ };
+
+ const handleMerge = async (sourceId: string, targetId: string) => {
+ try {
+ await mergeQueues(sourceId, targetId);
+ } catch (err) {
+ console.error('Failed to merge queues:', err);
+ }
+ };
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (error) {
+ return (
+
+
+
+ {formatMessage({ id: 'issues.queue.error.title' })}
+
+
+ {(error as Error).message || formatMessage({ id: 'issues.queue.error.message' })}
+
+
+ );
+ }
+
+ if (!queue || totalItems === 0) {
+ return ;
+ }
+
+ // Check if queue is active (has items and no conflicts)
+ const isActive = totalItems > 0 && conflictCount === 0;
+
+ return (
+
+ {/* Header Actions */}
+
+
+
+
+ {/* Stats Cards */}
+
+
+
+
+ {totalItems}
+
+
+ {formatMessage({ id: 'issues.queue.stats.totalItems' })}
+
+
+
+
+
+ {groupCount}
+
+
+ {formatMessage({ id: 'issues.queue.stats.groups' })}
+
+
+
+
+
+ {taskCount}
+
+
+ {formatMessage({ id: 'issues.queue.stats.tasks' })}
+
+
+
+
+
+ {solutionCount}
+
+
+ {formatMessage({ id: 'issues.queue.stats.solutions' })}
+
+
+
+
+ {/* Conflicts Warning */}
+ {conflictCount > 0 && (
+
+
+
+
+
+ {formatMessage({ id: 'issues.queue.conflicts.title' })}
+
+
+ {conflictCount} {formatMessage({ id: 'issues.queue.conflicts.description' })}
+
+
+
+
+ )}
+
+ {/* Queue Card */}
+
+
+
+
+ {/* Status Footer */}
+
+
+ {isActive ? (
+ <>
+
+ {formatMessage({ id: 'issues.queue.status.ready' })}
+ >
+ ) : (
+ <>
+
+ {formatMessage({ id: 'issues.queue.status.pending' })}
+ >
+ )}
+
+
+ {isActive ? (
+
+ ) : (
+
+ )}
+ {isActive
+ ? formatMessage({ id: 'issues.queue.status.active' })
+ : formatMessage({ id: 'issues.queue.status.inactive' })
+ }
+
+
+
+ );
+}
diff --git a/ccw/frontend/src/components/issue/hub/index.ts b/ccw/frontend/src/components/issue/hub/index.ts
new file mode 100644
index 00000000..a957f4c6
--- /dev/null
+++ b/ccw/frontend/src/components/issue/hub/index.ts
@@ -0,0 +1,10 @@
+// ========================================
+// Issue Hub Components Export
+// ========================================
+
+export { IssueHubHeader } from './IssueHubHeader';
+export { IssueHubTabs } from './IssueHubTabs';
+export { IssuesPanel } from './IssuesPanel';
+export { QueuePanel } from './QueuePanel';
+export { DiscoveryPanel } from './DiscoveryPanel';
+export { type IssueTab } from './IssueHubTabs';
diff --git a/ccw/frontend/src/components/issue/queue/QueueCard.tsx b/ccw/frontend/src/components/issue/queue/QueueCard.tsx
index 1d9fe69d..6693d1f2 100644
--- a/ccw/frontend/src/components/issue/queue/QueueCard.tsx
+++ b/ccw/frontend/src/components/issue/queue/QueueCard.tsx
@@ -46,7 +46,7 @@ export function QueueCard({
const { formatMessage } = useIntl();
// Get queue ID - IssueQueue interface doesn't have an id field, using tasks array as key
- const queueId = queue.tasks.join(',') || queue.solutions.join(',');
+ const queueId = (queue.tasks || []).join(',') || (queue.solutions || []).join(',') || 'unknown';
// Calculate item counts
const taskCount = queue.tasks?.length || 0;
diff --git a/ccw/frontend/src/components/layout/Sidebar.tsx b/ccw/frontend/src/components/layout/Sidebar.tsx
index 24d11814..e376f8ab 100644
--- a/ccw/frontend/src/components/layout/Sidebar.tsx
+++ b/ccw/frontend/src/components/layout/Sidebar.tsx
@@ -60,8 +60,8 @@ const navItemDefinitions: Omit[] = [
{ path: '/orchestrator', icon: Workflow },
{ path: '/loops', icon: RefreshCw },
{ path: '/issues', icon: AlertCircle },
- { path: '/issues/queue', icon: ListTodo },
- { path: '/issues/discovery', icon: Search },
+ { path: '/issues?tab=queue', icon: ListTodo },
+ { path: '/issues?tab=discovery', icon: Search },
{ path: '/skills', icon: Sparkles },
{ path: '/commands', icon: Terminal },
{ path: '/memory', icon: Brain },
@@ -110,8 +110,8 @@ export function Sidebar({
'/orchestrator': 'main.orchestrator',
'/loops': 'main.loops',
'/issues': 'main.issues',
- '/issues/queue': 'main.issueQueue',
- '/issues/discovery': 'main.issueDiscovery',
+ '/issues?tab=queue': 'main.issueQueue',
+ '/issues?tab=discovery': 'main.issueDiscovery',
'/skills': 'main.skills',
'/commands': 'main.commands',
'/memory': 'main.memory',
@@ -155,8 +155,13 @@ export function Sidebar({
{navItems.map((item) => {
const Icon = item.icon;
- const isActive = location.pathname === item.path ||
- (item.path !== '/' && location.pathname.startsWith(item.path));
+ // Parse item path to extract base path and query params
+ const [basePath, searchParams] = item.path.split('?');
+ const isActive = location.pathname === basePath ||
+ (basePath !== '/' && location.pathname.startsWith(basePath));
+ // For query param items, also check if search matches
+ const isQueryParamActive = searchParams &&
+ location.search.includes(searchParams);
return (
-
@@ -166,7 +171,7 @@ export function Sidebar({
className={cn(
'flex items-center gap-3 px-3 py-2.5 rounded-md text-sm transition-colors',
'hover:bg-hover hover:text-foreground',
- isActive
+ (isActive && !searchParams) || isQueryParamActive
? 'bg-primary/10 text-primary font-medium'
: 'text-muted-foreground',
isCollapsed && 'justify-center px-2'
diff --git a/ccw/frontend/src/components/shared/CliStreamMonitor/CliStreamMonitorNew.tsx b/ccw/frontend/src/components/shared/CliStreamMonitor/CliStreamMonitorNew.tsx
new file mode 100644
index 00000000..4b4da222
--- /dev/null
+++ b/ccw/frontend/src/components/shared/CliStreamMonitor/CliStreamMonitorNew.tsx
@@ -0,0 +1,530 @@
+// ========================================
+// CliStreamMonitor Component (New Layout)
+// ========================================
+// Redesigned CLI streaming monitor with smart parsing and message-based layout
+
+import { useEffect, useState, useCallback, useMemo } from 'react';
+import { useIntl } from 'react-intl';
+import {
+ Terminal,
+} from 'lucide-react';
+import { cn } from '@/lib/utils';
+import { useCliStreamStore, type CliOutputLine } from '@/stores/cliStreamStore';
+import { useNotificationStore, selectWsLastMessage } from '@/stores';
+import { useActiveCliExecutions, useInvalidateActiveCliExecutions } from '@/hooks/useActiveCliExecutions';
+
+// New layout components
+import { MonitorHeader } from './MonitorHeader';
+import { MonitorToolbar, type FilterType, type ViewMode } from './MonitorToolbar';
+import { MonitorBody } from './MonitorBody';
+import {
+ SystemMessage,
+ UserMessage,
+ AssistantMessage,
+ ErrorMessage,
+} from './messages';
+
+// ========== Types for CLI WebSocket Messages ==========
+
+interface CliStreamStartedPayload {
+ executionId: string;
+ tool: string;
+ mode: string;
+ timestamp: string;
+}
+
+interface CliStreamOutputPayload {
+ executionId: string;
+ chunkType: string;
+ data: unknown;
+ unit?: {
+ content: unknown;
+ type?: string;
+ };
+}
+
+interface CliStreamCompletedPayload {
+ executionId: string;
+ success: boolean;
+ duration?: number;
+ timestamp: string;
+}
+
+interface CliStreamErrorPayload {
+ executionId: string;
+ error?: string;
+ timestamp: string;
+}
+
+// ========== Message Type Detection ==========
+
+type MessageType = 'system' | 'user' | 'assistant' | 'error';
+
+interface ParsedMessage {
+ id: string;
+ type: MessageType;
+ timestamp: number;
+ content: string;
+ metadata?: {
+ toolName?: string;
+ mode?: string;
+ status?: string;
+ duration?: number;
+ tokens?: number;
+ model?: string;
+ };
+ raw?: CliOutputLine;
+}
+
+/**
+ * Detect message type from output line
+ */
+function detectMessageType(line: CliOutputLine): MessageType {
+ const content = line.content.trim().toLowerCase();
+
+ // Error detection
+ if (line.type === 'stderr' ||
+ content.includes('error') ||
+ content.includes('failed') ||
+ content.includes('exception')) {
+ return 'error';
+ }
+
+ // System message detection
+ if (line.type === 'system' ||
+ content.startsWith('[system]') ||
+ content.startsWith('[info]') ||
+ content.includes('cli execution started')) {
+ return 'system';
+ }
+
+ // User/assistant detection (based on context)
+ // For now, default to assistant for stdout
+ if (line.type === 'stdout') {
+ return 'assistant';
+ }
+
+ // Tool call metadata
+ if (line.type === 'tool_call') {
+ return 'system';
+ }
+
+ // Default to assistant
+ return 'assistant';
+}
+
+/**
+ * Parse output lines into structured messages
+ */
+function parseOutputToMessages(
+ executionId: string,
+ output: CliOutputLine[]
+): ParsedMessage[] {
+ const messages: ParsedMessage[] = [];
+ let currentMessage: Partial | null = null;
+
+ for (let i = 0; i < output.length; i++) {
+ const line = output[i];
+ const messageType = detectMessageType(line);
+
+ // Start new message if type changes
+ if (!currentMessage || currentMessage.type !== messageType) {
+ if (currentMessage && currentMessage.content) {
+ messages.push({
+ id: `${executionId}-${messages.length}`,
+ type: currentMessage.type!,
+ timestamp: currentMessage.timestamp!,
+ content: currentMessage.content,
+ metadata: currentMessage.metadata,
+ raw: currentMessage.raw,
+ });
+ }
+ currentMessage = {
+ type: messageType,
+ timestamp: line.timestamp,
+ content: '',
+ raw: line,
+ };
+ }
+
+ // Append content
+ const separator = currentMessage.content ? '\n' : '';
+ currentMessage.content = currentMessage.content + separator + line.content;
+ currentMessage.timestamp = Math.max(currentMessage.timestamp || 0, line.timestamp);
+
+ // Extract metadata from tool calls
+ if (line.type === 'tool_call') {
+ const toolMatch = line.content.match(/\[Tool\]\s+(\w+)/);
+ if (toolMatch) {
+ currentMessage.metadata = {
+ ...currentMessage.metadata,
+ toolName: toolMatch[1],
+ };
+ }
+ }
+ }
+
+ // Don't forget the last message
+ if (currentMessage && currentMessage.content) {
+ messages.push({
+ id: `${executionId}-${messages.length}`,
+ type: currentMessage.type!,
+ timestamp: currentMessage.timestamp!,
+ content: currentMessage.content,
+ metadata: currentMessage.metadata,
+ raw: currentMessage.raw,
+ });
+ }
+
+ return messages;
+}
+
+// ========== Helper Functions ==========
+
+function formatDuration(ms: number): string {
+ if (ms < 1000) return `${ms}ms`;
+ const seconds = Math.floor(ms / 1000);
+ if (seconds < 60) return `${seconds}s`;
+ const minutes = Math.floor(seconds / 60);
+ const remainingSeconds = seconds % 60;
+ if (minutes < 60) return `${minutes}m ${remainingSeconds}s`;
+ const hours = Math.floor(minutes / 60);
+ const remainingMinutes = minutes % 60;
+ return `${hours}h ${remainingMinutes}m`;
+}
+
+// ========== Component ==========
+
+export interface CliStreamMonitorNewProps {
+ isOpen: boolean;
+ onClose: () => void;
+}
+
+export function CliStreamMonitorNew({ isOpen, onClose }: CliStreamMonitorNewProps) {
+ const { formatMessage } = useIntl();
+
+ // UI State
+ const [searchQuery, setSearchQuery] = useState('');
+ const [filter, setFilter] = useState('all');
+ const [viewMode, setViewMode] = useState('preview');
+
+ // Store state
+ const executions = useCliStreamStore((state) => state.executions);
+ const currentExecutionId = useCliStreamStore((state) => state.currentExecutionId);
+ const removeExecution = useCliStreamStore((state) => state.removeExecution);
+
+ // Active execution sync
+ const { isLoading: isSyncing, refetch } = useActiveCliExecutions(isOpen);
+ const invalidateActive = useInvalidateActiveCliExecutions();
+
+ // WebSocket last message
+ const lastMessage = useNotificationStore(selectWsLastMessage);
+
+ // Handle WebSocket messages (same as original)
+ useEffect(() => {
+ if (!lastMessage) return;
+
+ const { type, payload } = lastMessage;
+
+ if (type === 'CLI_STARTED') {
+ const p = payload as CliStreamStartedPayload;
+ const startTime = p.timestamp ? new Date(p.timestamp).getTime() : Date.now();
+ useCliStreamStore.getState().upsertExecution(p.executionId, {
+ tool: p.tool || 'cli',
+ mode: p.mode || 'analysis',
+ status: 'running',
+ startTime,
+ output: [
+ {
+ type: 'system',
+ content: `[${new Date(startTime).toLocaleTimeString()}] CLI execution started: ${p.tool} (${p.mode} mode)`,
+ timestamp: startTime
+ }
+ ]
+ });
+ invalidateActive();
+ } else if (type === 'CLI_OUTPUT') {
+ const p = payload as CliStreamOutputPayload;
+ const unitContent = p.unit?.content;
+ const unitType = p.unit?.type || p.chunkType;
+
+ let content: string;
+ if (unitType === 'tool_call' && typeof unitContent === 'object' && unitContent !== null) {
+ const toolCall = unitContent as { action?: string; toolName?: string; parameters?: unknown; status?: string; output?: string };
+ if (toolCall.action === 'invoke') {
+ const params = toolCall.parameters ? JSON.stringify(toolCall.parameters) : '';
+ content = `[Tool] ${toolCall.toolName}(${params})`;
+ } else if (toolCall.action === 'result') {
+ const status = toolCall.status || 'unknown';
+ const output = toolCall.output ? `: ${toolCall.output.substring(0, 200)}${toolCall.output.length > 200 ? '...' : ''}` : '';
+ content = `[Tool Result] ${status}${output}`;
+ } else {
+ content = JSON.stringify(unitContent);
+ }
+ } else {
+ content = typeof p.data === 'string' ? p.data : JSON.stringify(p.data);
+ }
+
+ const lines = content.split('\n');
+ const addOutput = useCliStreamStore.getState().addOutput;
+ lines.forEach(line => {
+ if (line.trim() || lines.length === 1) {
+ addOutput(p.executionId, {
+ type: (unitType as CliOutputLine['type']) || 'stdout',
+ content: line,
+ timestamp: Date.now()
+ });
+ }
+ });
+ } else if (type === 'CLI_COMPLETED') {
+ const p = payload as CliStreamCompletedPayload;
+ const endTime = p.timestamp ? new Date(p.timestamp).getTime() : Date.now();
+ useCliStreamStore.getState().upsertExecution(p.executionId, {
+ status: p.success ? 'completed' : 'error',
+ endTime,
+ output: [
+ {
+ type: 'system',
+ content: `[${new Date(endTime).toLocaleTimeString()}] CLI execution ${p.success ? 'completed successfully' : 'failed'}${p.duration ? ` (${formatDuration(p.duration)})` : ''}`,
+ timestamp: endTime
+ }
+ ]
+ });
+ invalidateActive();
+ } else if (type === 'CLI_ERROR') {
+ const p = payload as CliStreamErrorPayload;
+ const endTime = p.timestamp ? new Date(p.timestamp).getTime() : Date.now();
+ useCliStreamStore.getState().upsertExecution(p.executionId, {
+ status: 'error',
+ endTime,
+ output: [
+ {
+ type: 'stderr',
+ content: `[ERROR] ${p.error || 'Unknown error occurred'}`,
+ timestamp: endTime
+ }
+ ]
+ });
+ invalidateActive();
+ }
+ }, [lastMessage, invalidateActive]);
+
+ // Get execution stats
+ const executionStats = useMemo(() => {
+ const all = Object.values(executions);
+ return {
+ total: all.length,
+ active: all.filter(e => e.status === 'running').length,
+ error: all.filter(e => e.status === 'error').length,
+ completed: all.filter(e => e.status === 'completed').length,
+ };
+ }, [executions]);
+
+ // Get current execution
+ const currentExecution = currentExecutionId ? executions[currentExecutionId] : null;
+
+ // Parse messages from current execution
+ const messages = useMemo(() => {
+ if (!currentExecution?.output) return [];
+ return parseOutputToMessages(currentExecutionId || '', currentExecution.output);
+ }, [currentExecution?.output, currentExecutionId]);
+
+ // Filter messages
+ const filteredMessages = useMemo(() => {
+ let filtered = messages;
+
+ // Apply type filter
+ if (filter !== 'all') {
+ filtered = filtered.filter(m => {
+ if (filter === 'errors') return m.type === 'error';
+ if (filter === 'content') return m.type === 'user' || m.type === 'assistant';
+ if (filter === 'system') return m.type === 'system';
+ return true;
+ });
+ }
+
+ // Apply search filter
+ if (searchQuery) {
+ const query = searchQuery.toLowerCase();
+ filtered = filtered.filter(m =>
+ m.content.toLowerCase().includes(query)
+ );
+ }
+
+ return filtered;
+ }, [messages, filter, searchQuery]);
+
+ // Copy message content
+ const handleCopy = useCallback(async (content: string) => {
+ try {
+ await navigator.clipboard.writeText(content);
+ } catch (err) {
+ console.error('Failed to copy:', err);
+ }
+ }, []);
+
+ // Handle message actions
+ const handleRetry = useCallback((executionId: string) => {
+ // TODO: Implement retry logic
+ console.log('Retry execution:', executionId);
+ }, []);
+
+ const handleDismiss = useCallback((executionId: string) => {
+ removeExecution(executionId);
+ }, [removeExecution]);
+
+ // Don't render if not open
+ if (!isOpen) {
+ return null;
+ }
+
+ return (
+ <>
+ {/* Overlay */}
+
+
+ {/* Main Panel */}
+
+ {/* Header */}
+
+
+ {/* Toolbar */}
+
+
+ {/* Body */}
+
+ {currentExecution ? (
+
+ {filteredMessages.length === 0 ? (
+
+ {searchQuery
+ ? formatMessage({ id: 'cliMonitor.noMatch' })
+ : formatMessage({ id: 'cliMonitor.noMessages' })
+ }
+
+ ) : (
+ filteredMessages.map((message) => {
+ switch (message.type) {
+ case 'system':
+ return (
+
+ );
+
+ case 'user':
+ return (
+
handleCopy(message.content)}
+ />
+ );
+
+ case 'assistant':
+ return (
+ handleCopy(message.content)}
+ />
+ );
+
+ case 'error':
+ return (
+ handleRetry(currentExecutionId!)}
+ onDismiss={() => handleDismiss(currentExecutionId!)}
+ />
+ );
+
+ default:
+ return null;
+ }
+ })
+ )}
+
+ ) : (
+
+
+
+
{formatMessage({ id: 'cliMonitor.noExecutions' })}
+
+ {formatMessage({ id: 'cliMonitor.noExecutionsHint' })}
+
+
+
+ )}
+
+
+ {/* Status Bar */}
+
+
+ {formatMessage(
+ { id: 'cliMonitor.statusBar' },
+ {
+ total: executionStats.total,
+ active: executionStats.active,
+ error: executionStats.error,
+ lines: currentExecution?.output.length || 0
+ }
+ )}
+
+
+
+
+ >
+ );
+}
+
+export default CliStreamMonitorNew;
diff --git a/ccw/frontend/src/components/shared/CliStreamMonitor/MessageRenderer/index.tsx b/ccw/frontend/src/components/shared/CliStreamMonitor/MessageRenderer/index.tsx
new file mode 100644
index 00000000..8e4f90b1
--- /dev/null
+++ b/ccw/frontend/src/components/shared/CliStreamMonitor/MessageRenderer/index.tsx
@@ -0,0 +1,383 @@
+// ========================================
+// MessageRenderer Component
+// ========================================
+// Renders message content with Markdown support, JSON formatting,
+// and escape sequence handling
+
+import { useMemo } from 'react';
+import ReactMarkdown from 'react-markdown';
+import remarkGfm from 'remark-gfm';
+import rehypeHighlight from 'rehype-highlight';
+import { cn } from '@/lib/utils';
+import { JsonFormatter } from '../../LogBlock/JsonFormatter';
+import { detectJsonContent } from '../../LogBlock/jsonUtils';
+
+// Import highlight.js styles for code syntax highlighting
+// Using a base style that works with both light and dark themes
+import 'highlight.js/styles/base16/atelier-forest.css';
+
+// ========== Types ==========
+
+export interface MessageRendererProps {
+ /** Content to render */
+ content: string;
+ /** Additional CSS className */
+ className?: string;
+ /** Format hint (auto-detect if not specified) */
+ format?: 'markdown' | 'text' | 'json';
+ /** Maximum lines to display (for text mode) */
+ maxLines?: number;
+}
+
+// ========== Markdown Component Styles ==========
+
+const markdownComponents: Record = {
+ h1: 'text-xl font-bold mt-4 mb-2 text-foreground',
+ h2: 'text-lg font-semibold mt-3 mb-2 text-foreground',
+ h3: 'text-base font-semibold mt-2 mb-1 text-foreground',
+ p: 'text-sm leading-relaxed mb-2 text-foreground',
+ ul: 'list-disc list-inside mb-2 text-sm space-y-1',
+ ol: 'list-decimal list-inside mb-2 text-sm space-y-1',
+ li: 'text-sm text-foreground',
+ code: 'font-mono text-xs bg-muted px-1.5 py-0.5 rounded text-amber-600 dark:text-amber-400',
+ pre: 'bg-muted/80 dark:bg-muted/30 p-3 rounded-lg overflow-x-auto my-2 border border-border/50',
+ blockquote: 'border-l-4 border-muted-foreground pl-4 italic text-muted-foreground my-2',
+ strong: 'font-semibold text-foreground',
+ a: 'text-blue-600 dark:text-blue-400 hover:underline',
+};
+
+// ========== Helper Components ==========
+
+/**
+ * Inline code renderer for Markdown
+ */
+function MarkdownCode({ className, children, ...props }: React.HTMLAttributes) {
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Code block renderer for Markdown with syntax highlighting
+ */
+function MarkdownPre({ className, children, ...props }: React.HTMLAttributes) {
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Paragraph renderer for Markdown
+ */
+function MarkdownParagraph({ className, children, ...props }: React.HTMLAttributes) {
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Heading renderers for Markdown
+ */
+function MarkdownH1({ className, children, ...props }: React.HTMLAttributes) {
+ return (
+
+ {children}
+
+ );
+}
+
+function MarkdownH2({ className, children, ...props }: React.HTMLAttributes) {
+ return (
+
+ {children}
+
+ );
+}
+
+function MarkdownH3({ className, children, ...props }: React.HTMLAttributes) {
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * List renderers for Markdown
+ */
+function MarkdownUl({ className, children, ...props }: React.HTMLAttributes) {
+ return (
+
+ );
+}
+
+function MarkdownOl({ className, children, ...props }: React.HTMLAttributes) {
+ return (
+
+ {children}
+
+ );
+}
+
+function MarkdownLi({ className, children, ...props }: React.HTMLAttributes) {
+ return (
+ -
+ {children}
+
+ );
+}
+
+/**
+ * Blockquote renderer for Markdown
+ */
+function MarkdownBlockquote({ className, children, ...props }: React.HTMLAttributes) {
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Anchor renderer for Markdown
+ */
+function MarkdownA({ className, children, href, ...props }: React.AnchorHTMLAttributes) {
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Strong/Bold renderer for Markdown
+ */
+function MarkdownStrong({ className, children, ...props }: React.HTMLAttributes) {
+ return (
+
+ {children}
+
+ );
+}
+
+// ========== Markdown Renderer ==========
+
+/**
+ * Markdown content renderer with syntax highlighting
+ */
+function MarkdownRenderer({ content, className }: { content: string; className?: string }) {
+ return (
+
+
+ {content}
+
+
+ );
+}
+
+// ========== Text Renderer ==========
+
+/**
+ * Plain text renderer with escape sequence handling
+ */
+function TextRenderer({ content, maxLines }: { content: string; maxLines?: number }) {
+ const lines = useMemo(() => {
+ return content.split('\n');
+ }, [content]);
+
+ const displayLines = useMemo(() => {
+ if (maxLines && lines.length > maxLines) {
+ return lines.slice(0, maxLines);
+ }
+ return lines;
+ }, [lines, maxLines]);
+
+ const showTruncated = maxLines && lines.length > maxLines;
+
+ return (
+
+ {displayLines.map((line, index) => (
+
{line || '\u00A0'}
+ ))}
+ {showTruncated && (
+
+ // ... {lines.length - maxLines} more lines
+
+ )}
+
+ );
+}
+
+// ========== Content Detection ==========
+
+/**
+ * Auto-detect content format
+ */
+function detectContentFormat(content: string): 'markdown' | 'json' | 'text' {
+ const trimmed = content.trim();
+
+ // Check for JSON first
+ const jsonDetection = detectJsonContent(trimmed);
+ if (jsonDetection.isJson) {
+ return 'json';
+ }
+
+ // Check for Markdown patterns
+ const markdownPatterns = [
+ /^#{1,6}\s+/m, // Headings
+ /^\*{3,}$/m, // Horizontal rule
+ /^\s*[-*+]\s+/m, // Unordered lists
+ /^\s*\d+\.\s+/m, // Ordered lists
+ /\[.*?\]\(.*?\)/, // Links
+ /`{3,}[\s\S]*?`{3,}/, // Code blocks
+ /\*\*.*?\*\*/, // Bold
+ /_.*?_/, // Italic
+ /^\s*>\s+/m, // Blockquotes
+ ];
+
+ for (const pattern of markdownPatterns) {
+ if (pattern.test(trimmed)) {
+ return 'markdown';
+ }
+ }
+
+ // Default to text
+ return 'text';
+}
+
+// ========== Main Component ==========
+
+/**
+ * MessageRenderer Component
+ *
+ * Renders message content with automatic format detection:
+ * - JSON: Structured display with JsonFormatter
+ * - Markdown: Rich text with syntax highlighting
+ * - Text: Plain text with escape sequence handling
+ *
+ * Supports escape sequence handling (e.g., \n → newline)
+ */
+export function MessageRenderer({
+ content,
+ className,
+ format,
+ maxLines,
+}: MessageRendererProps) {
+ // Process escape sequences
+ const processedContent = useMemo(() => {
+ // Replace common escape sequences
+ return content
+ .replace(/\\n/g, '\n')
+ .replace(/\\t/g, ' ')
+ .replace(/\\"/g, '"')
+ .replace(/\\'/g, "'")
+ .replace(/\\\\/g, '\\');
+ }, [content]);
+
+ // Auto-detect format if not specified
+ const detectedFormat = useMemo(() => {
+ if (format) {
+ return format;
+ }
+ return detectContentFormat(processedContent);
+ }, [processedContent, format]);
+
+ // Render based on format
+ switch (detectedFormat) {
+ case 'json':
+ return (
+
+
+
+ );
+
+ case 'markdown':
+ return (
+
+ );
+
+ case 'text':
+ default:
+ return (
+
+
+
+ );
+ }
+}
+
+export default MessageRenderer;
diff --git a/ccw/frontend/src/components/shared/CliStreamMonitor/MonitorBody/index.tsx b/ccw/frontend/src/components/shared/CliStreamMonitor/MonitorBody/index.tsx
new file mode 100644
index 00000000..40c66ae4
--- /dev/null
+++ b/ccw/frontend/src/components/shared/CliStreamMonitor/MonitorBody/index.tsx
@@ -0,0 +1,136 @@
+// ========================================
+// MonitorBody Component
+// ========================================
+// Scrollable container for message list
+
+import { useEffect, useRef, useCallback, forwardRef, ForwardedRef, useState, useImperativeHandle } from 'react';
+import { cn } from '@/lib/utils';
+import { ArrowDownToLine } from 'lucide-react';
+import { Button } from '@/components/ui/Button';
+
+// ========== Types ==========
+
+export interface MonitorBodyProps {
+ children: React.ReactNode;
+ className?: string;
+ autoScroll?: boolean;
+ onScroll?: () => void;
+ showScrollButton?: boolean;
+ scrollThreshold?: number;
+}
+
+export interface MonitorBodyRef {
+ scrollToBottom: () => void;
+ containerRef: React.RefObject;
+}
+
+// ========== Helper Components ==========
+
+interface ScrollToBottomButtonProps {
+ onClick: () => void;
+ className?: string;
+}
+
+function ScrollToBottomButton({ onClick, className }: ScrollToBottomButtonProps) {
+ return (
+
+ );
+}
+
+// ========== Component ==========
+
+function MonitorBodyComponent(
+ props: MonitorBodyProps,
+ ref: ForwardedRef
+) {
+ const {
+ children,
+ className,
+ autoScroll = true,
+ onScroll,
+ showScrollButton = true,
+ scrollThreshold = 50,
+ } = props;
+
+ const containerRef = useRef(null);
+ const logsEndRef = useRef(null);
+ const [isUserScrolling, setIsUserScrolling] = useState(false);
+
+ // Expose methods via ref
+ useImperativeHandle(
+ ref,
+ () => ({
+ scrollToBottom: () => {
+ logsEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+ setIsUserScrolling(false);
+ },
+ containerRef,
+ }),
+ []
+ );
+
+ // Auto-scroll to bottom when children change
+ useEffect(() => {
+ if (autoScroll && !isUserScrolling && logsEndRef.current) {
+ logsEndRef.current.scrollIntoView({ behavior: 'smooth' });
+ }
+ }, [children, autoScroll, isUserScrolling]);
+
+ // Handle scroll to detect user scrolling
+ const handleScroll = useCallback(() => {
+ if (!containerRef.current) return;
+
+ const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
+ const isAtBottom = scrollHeight - scrollTop - clientHeight < scrollThreshold;
+
+ const wasScrolling = isUserScrolling;
+ setIsUserScrolling(!isAtBottom);
+
+ // Call onScroll callback when user starts/stops scrolling
+ if (onScroll && wasScrolling !== !isAtBottom) {
+ onScroll();
+ }
+ }, [scrollThreshold, isUserScrolling, onScroll]);
+
+ return (
+
+
+ {children}
+ {/* Anchor for scroll to bottom */}
+
+
+
+ {/* Show scroll button when user is not at bottom */}
+ {showScrollButton && isUserScrolling && (
+
{
+ logsEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+ setIsUserScrolling(false);
+ }}
+ />
+ )}
+
+ );
+}
+
+// Export with forwardRef
+export const MonitorBody = forwardRef(
+ MonitorBodyComponent
+);
+
+MonitorBody.displayName = 'MonitorBody';
+
+export default MonitorBody;
diff --git a/ccw/frontend/src/components/shared/CliStreamMonitor/MonitorHeader/index.tsx b/ccw/frontend/src/components/shared/CliStreamMonitor/MonitorHeader/index.tsx
new file mode 100644
index 00000000..7fe845c2
--- /dev/null
+++ b/ccw/frontend/src/components/shared/CliStreamMonitor/MonitorHeader/index.tsx
@@ -0,0 +1,114 @@
+// ========================================
+// MonitorHeader Component
+// ========================================
+// Header component for CLI Stream Monitor
+
+import { memo } from 'react';
+import { useIntl } from 'react-intl';
+import { X, Activity, ChevronDown } from 'lucide-react';
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/Button';
+
+export interface MonitorHeaderProps {
+ /** Callback when close button is clicked */
+ onClose: () => void;
+ /** Number of active (running) executions */
+ activeCount?: number;
+ /** Total number of executions */
+ totalCount?: number;
+ /** Number of executions with errors */
+ errorCount?: number;
+}
+
+/**
+ * MonitorHeader - Header component for CLI Stream Monitor
+ *
+ * Displays:
+ * - Left: Close button + title
+ * - Right: Live status indicator + execution count badge
+ */
+export const MonitorHeader = memo(function MonitorHeader({
+ onClose,
+ activeCount = 0,
+ totalCount = 0,
+ errorCount = 0,
+}: MonitorHeaderProps) {
+ const { formatMessage } = useIntl();
+ const hasActive = activeCount > 0;
+ const hasErrors = errorCount > 0;
+
+ return (
+
+ {/* Left side: Close button + Title */}
+
+
+
+
+
+
+ {formatMessage({ id: 'cliMonitor.title' })}
+
+
+
+
+ {/* Right side: Status + Count badge */}
+
+ {/* Live status indicator */}
+ {hasActive && (
+
+
+
+
+ {formatMessage({ id: 'cliMonitor.live' })}
+
+
+
+
+ )}
+
+ {/* Execution count badge */}
+ {totalCount > 0 && (
+
+
+ {formatMessage({ id: 'cliMonitor.executions' }, { count: totalCount })}
+
+ {activeCount > 0 && (
+
+ {formatMessage({ id: 'cliMonitor.active' }, { count: activeCount })}
+
+ )}
+
+ )}
+
+
+ );
+});
+
+export default MonitorHeader;
diff --git a/ccw/frontend/src/components/shared/CliStreamMonitor/MonitorToolbar/index.tsx b/ccw/frontend/src/components/shared/CliStreamMonitor/MonitorToolbar/index.tsx
new file mode 100644
index 00000000..4628235b
--- /dev/null
+++ b/ccw/frontend/src/components/shared/CliStreamMonitor/MonitorToolbar/index.tsx
@@ -0,0 +1,197 @@
+// ========================================
+// MonitorToolbar Component
+// ========================================
+// Toolbar for CLI Stream Monitor with search, filter, and view mode controls
+
+import { Search, Settings, ChevronDown, X } from 'lucide-react';
+import { useIntl } from 'react-intl';
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/Button';
+import { Input } from '@/components/ui/Input';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+} from '@/components/ui/Dropdown';
+
+// ========== Types ==========
+
+export type FilterType = 'all' | 'errors' | 'content' | 'system';
+export type ViewMode = 'preview' | 'json' | 'raw';
+
+export interface MonitorToolbarProps {
+ /** Current search query */
+ searchQuery: string;
+ /** Callback when search query changes */
+ onSearchChange: (value: string) => void;
+ /** Current filter type */
+ filter: FilterType;
+ /** Callback when filter changes */
+ onFilterChange: (filter: FilterType) => void;
+ /** Current view mode */
+ viewMode: ViewMode;
+ /** Callback when view mode changes */
+ onViewModeChange: (mode: ViewMode) => void;
+ /** Optional settings click handler */
+ onSettingsClick?: () => void;
+ /** Optional class name for custom styling */
+ className?: string;
+}
+
+// ========== Filter Button Component ==========
+
+interface FilterButtonProps {
+ active: boolean;
+ onClick: () => void;
+ children: React.ReactNode;
+}
+
+const FilterButton = ({ active, onClick, children }: FilterButtonProps) => (
+
+);
+
+// ========== Main Toolbar Component ==========
+
+export const MonitorToolbar = ({
+ searchQuery,
+ onSearchChange,
+ filter,
+ onFilterChange,
+ viewMode,
+ onViewModeChange,
+ onSettingsClick,
+ className,
+}: MonitorToolbarProps) => {
+ const { formatMessage } = useIntl();
+
+ const filterLabels: Record = {
+ all: formatMessage({ id: 'cliMonitor.filter.all' }),
+ errors: formatMessage({ id: 'cliMonitor.filter.errors' }),
+ content: formatMessage({ id: 'cliMonitor.filter.content' }),
+ system: formatMessage({ id: 'cliMonitor.filter.system' }),
+ };
+
+ const viewModeLabels: Record = {
+ preview: formatMessage({ id: 'cliMonitor.view.preview' }),
+ json: formatMessage({ id: 'cliMonitor.view.json' }),
+ raw: formatMessage({ id: 'cliMonitor.view.raw' }),
+ };
+
+ return (
+
+ {/* Left: Search and Filter */}
+
+ {/* Search Box */}
+
+
+ onSearchChange(e.target.value)}
+ className="h-9 pl-9 pr-8 text-sm w-64 bg-background border border-border"
+ />
+ {searchQuery && (
+
+ )}
+
+
+ {/* Filter Buttons */}
+
+ onFilterChange('all')}
+ >
+ {filterLabels.all}
+
+ onFilterChange('errors')}
+ >
+ {filterLabels.errors}
+
+ onFilterChange('content')}
+ >
+ {filterLabels.content}
+
+ onFilterChange('system')}
+ >
+ {filterLabels.system}
+
+
+
+
+ {/* Right: View Mode and Settings */}
+
+ {/* View Mode Dropdown */}
+
+
+
+
+
+ {formatMessage({ id: 'cliMonitor.viewMode' })}
+
+ onViewModeChange('preview')}>
+ {formatMessage({ id: 'cliMonitor.view.preview' })}
+
+ onViewModeChange('json')}>
+ {formatMessage({ id: 'cliMonitor.view.json' })}
+
+ onViewModeChange('raw')}>
+ {formatMessage({ id: 'cliMonitor.view.raw' })}
+
+
+
+
+ {/* Settings Button */}
+ {onSettingsClick && (
+
+ )}
+
+
+ );
+};
+
+MonitorToolbar.displayName = 'MonitorToolbar';
diff --git a/ccw/frontend/src/components/shared/CliStreamMonitor/index.ts b/ccw/frontend/src/components/shared/CliStreamMonitor/index.ts
new file mode 100644
index 00000000..d00a059f
--- /dev/null
+++ b/ccw/frontend/src/components/shared/CliStreamMonitor/index.ts
@@ -0,0 +1,36 @@
+// ========================================
+// CliStreamMonitor Component Exports
+// ========================================
+// New layout exports for the redesigned CLI Stream Monitor
+
+// Main component (new layout)
+export { CliStreamMonitorNew as CliStreamMonitor } from './CliStreamMonitorNew';
+export type { CliStreamMonitorNewProps as CliStreamMonitorProps } from './CliStreamMonitorNew';
+
+// Layout components
+export { MonitorHeader } from './MonitorHeader';
+export type { MonitorHeaderProps } from './MonitorHeader';
+
+export { MonitorToolbar } from './MonitorToolbar';
+export type { MonitorToolbarProps, FilterType, ViewMode } from './MonitorToolbar';
+
+export { MonitorBody } from './MonitorBody';
+export type { MonitorBodyProps, MonitorBodyRef } from './MonitorBody';
+
+// Message type components
+export {
+ SystemMessage,
+ UserMessage,
+ AssistantMessage,
+ ErrorMessage,
+} from './messages';
+export type {
+ SystemMessageProps,
+ UserMessageProps,
+ AssistantMessageProps,
+ ErrorMessageProps,
+} from './messages';
+
+// Message renderer
+export { MessageRenderer } from './MessageRenderer';
+export type { MessageRendererProps } from './MessageRenderer';
diff --git a/ccw/frontend/src/components/shared/CliStreamMonitor/messages/AssistantMessage.tsx b/ccw/frontend/src/components/shared/CliStreamMonitor/messages/AssistantMessage.tsx
new file mode 100644
index 00000000..ceaea473
--- /dev/null
+++ b/ccw/frontend/src/components/shared/CliStreamMonitor/messages/AssistantMessage.tsx
@@ -0,0 +1,196 @@
+// ========================================
+// AssistantMessage Component
+// ========================================
+
+import { useState, useEffect } from 'react';
+import { useIntl } from 'react-intl';
+import { Bot, ChevronDown, Copy, Check } from 'lucide-react';
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/Button';
+
+// Status indicator component
+interface StatusIndicatorProps {
+ status: 'thinking' | 'streaming' | 'completed' | 'error';
+ duration?: number;
+}
+
+function StatusIndicator({ status, duration }: StatusIndicatorProps) {
+ const { formatMessage } = useIntl();
+
+ if (status === 'thinking') {
+ return (
+
+ {formatMessage({ id: 'cliMonitor.thinking' })}
+ 🟡
+
+ );
+ }
+
+ if (status === 'streaming') {
+ return (
+
+ {formatMessage({ id: 'cliMonitor.streaming' })}
+ 🔵
+
+ );
+ }
+
+ if (status === 'error') {
+ return (
+
+ Error
+ ❌
+
+ );
+ }
+
+ if (duration !== undefined) {
+ const seconds = (duration / 1000).toFixed(1);
+ return (
+
+ {seconds}s
+
+ );
+ }
+
+ return null;
+}
+
+// Format duration helper
+function formatDuration(ms: number): string {
+ if (ms < 1000) return `${ms}ms`;
+ const seconds = Math.floor(ms / 1000);
+ if (seconds < 60) return `${seconds}s`;
+ const minutes = Math.floor(seconds / 60);
+ const remainingSeconds = seconds % 60;
+ if (minutes < 60) return `${minutes}m ${remainingSeconds}s`;
+ const hours = Math.floor(minutes / 60);
+ const remainingMinutes = minutes % 60;
+ return `${hours}h ${remainingMinutes}m`;
+}
+
+export interface AssistantMessageProps {
+ content: string;
+ modelName?: string;
+ status?: 'thinking' | 'streaming' | 'completed' | 'error';
+ duration?: number;
+ tokenCount?: number;
+ timestamp?: number;
+ onCopy?: () => void;
+ className?: string;
+}
+
+export function AssistantMessage({
+ content,
+ modelName = 'AI',
+ status = 'completed',
+ duration,
+ tokenCount,
+ // timestamp is kept for future use but not currently displayed
+ // timestamp,
+ onCopy,
+ className
+}: AssistantMessageProps) {
+ const { formatMessage } = useIntl();
+ const [isExpanded, setIsExpanded] = useState(true);
+ const [copied, setCopied] = useState(false);
+
+ useEffect(() => {
+ if (copied) {
+ const timer = setTimeout(() => setCopied(false), 2000);
+ return () => clearTimeout(timer);
+ }
+ }, [copied]);
+
+ const handleCopy = () => {
+ onCopy?.();
+ setCopied(true);
+ };
+
+ return (
+
+ {/* Header */}
+
setIsExpanded(!isExpanded)}
+ >
+
+
+ {modelName}
+
+
+
+
+
+
+
+
+ {/* Content */}
+ {isExpanded && (
+ <>
+
+
+ {/* Metadata Footer */}
+
e.stopPropagation()}
+ >
+
+ {tokenCount !== undefined && (
+ {formatMessage({ id: 'cliMonitor.tokens' }, { count: tokenCount.toLocaleString() })}
+ )}
+ {duration !== undefined && (
+ {formatMessage({ id: 'cliMonitor.duration' }, { value: formatDuration(duration) })}
+ )}
+ {modelName && {formatMessage({ id: 'cliMonitor.model' }, { name: modelName })}}
+
+
+
+
+ >
+ )}
+
+ );
+}
+
+export default AssistantMessage;
diff --git a/ccw/frontend/src/components/shared/CliStreamMonitor/messages/ErrorMessage.tsx b/ccw/frontend/src/components/shared/CliStreamMonitor/messages/ErrorMessage.tsx
new file mode 100644
index 00000000..401c126f
--- /dev/null
+++ b/ccw/frontend/src/components/shared/CliStreamMonitor/messages/ErrorMessage.tsx
@@ -0,0 +1,88 @@
+// ========================================
+// ErrorMessage Component
+// ========================================
+
+import { useIntl } from 'react-intl';
+import { AlertCircle } from 'lucide-react';
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/Button';
+
+export interface ErrorMessageProps {
+ title: string;
+ message: string;
+ timestamp?: number;
+ onRetry?: () => void;
+ onDismiss?: () => void;
+ className?: string;
+}
+
+export function ErrorMessage({
+ title,
+ message,
+ timestamp,
+ onRetry,
+ onDismiss,
+ className
+}: ErrorMessageProps) {
+ const { formatMessage } = useIntl();
+ const timeString = timestamp
+ ? new Date(timestamp).toLocaleTimeString('en-US', { hour12: false })
+ : '';
+
+ return (
+
+ {/* Header */}
+
+
+ {timeString && (
+
+ [{timeString}]
+
+ )}
+
+ {title}
+
+
+
+ {/* Content */}
+
+
+ {/* Actions */}
+ {(onRetry || onDismiss) && (
+
+ {onRetry && (
+
+ )}
+ {onDismiss && (
+
+ )}
+
+ )}
+
+ );
+}
+
+export default ErrorMessage;
diff --git a/ccw/frontend/src/components/shared/CliStreamMonitor/messages/SystemMessage.tsx b/ccw/frontend/src/components/shared/CliStreamMonitor/messages/SystemMessage.tsx
new file mode 100644
index 00000000..f6928551
--- /dev/null
+++ b/ccw/frontend/src/components/shared/CliStreamMonitor/messages/SystemMessage.tsx
@@ -0,0 +1,75 @@
+// ========================================
+// SystemMessage Component
+// ========================================
+
+import { useState } from 'react';
+import { Info, ChevronRight } from 'lucide-react';
+import { cn } from '@/lib/utils';
+
+export interface SystemMessageProps {
+ title: string;
+ timestamp?: number;
+ metadata?: string;
+ content?: string;
+ className?: string;
+}
+
+export function SystemMessage({
+ title,
+ timestamp,
+ metadata,
+ content,
+ className
+}: SystemMessageProps) {
+ const [isExpanded, setIsExpanded] = useState(false);
+ const timeString = timestamp
+ ? new Date(timestamp).toLocaleTimeString('en-US', { hour12: false })
+ : '';
+
+ return (
+
+ {/* Header */}
+
content && setIsExpanded(!isExpanded)}
+ >
+
+
+ [{timeString}]
+
+
+ {title}
+
+ {metadata && (
+
+ {metadata}
+
+ )}
+ {content && (
+
+ )}
+
+
+ {/* Expandable Content */}
+ {isExpanded && content && (
+
+ )}
+
+ );
+}
+
+export default SystemMessage;
diff --git a/ccw/frontend/src/components/shared/CliStreamMonitor/messages/UserMessage.tsx b/ccw/frontend/src/components/shared/CliStreamMonitor/messages/UserMessage.tsx
new file mode 100644
index 00000000..565f99f6
--- /dev/null
+++ b/ccw/frontend/src/components/shared/CliStreamMonitor/messages/UserMessage.tsx
@@ -0,0 +1,133 @@
+// ========================================
+// UserMessage Component
+// ========================================
+
+import { useState, useEffect } from 'react';
+import { useIntl } from 'react-intl';
+import { User, ChevronDown, Copy, Check } from 'lucide-react';
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/Button';
+
+export interface UserMessageProps {
+ content: string;
+ timestamp?: number;
+ onCopy?: () => void;
+ onViewRaw?: () => void;
+ className?: string;
+}
+
+export function UserMessage({
+ content,
+ timestamp,
+ onCopy,
+ onViewRaw,
+ className
+}: UserMessageProps) {
+ const { formatMessage } = useIntl();
+ const [isExpanded, setIsExpanded] = useState(true);
+ const [copied, setCopied] = useState(false);
+ const timeString = timestamp
+ ? new Date(timestamp).toLocaleTimeString('en-US', { hour12: false })
+ : '';
+
+ // Auto-reset copied state
+ useEffect(() => {
+ if (copied) {
+ const timer = setTimeout(() => setCopied(false), 2000);
+ return () => clearTimeout(timer);
+ }
+ }, [copied]);
+
+ const handleCopy = () => {
+ onCopy?.();
+ setCopied(true);
+ };
+
+ return (
+
+ {/* Header */}
+
setIsExpanded(!isExpanded)}
+ >
+
+
+ {formatMessage({ id: 'cliMonitor.user' })}
+
+
+ {timeString && (
+
+ [{timeString}]
+
+ )}
+
+
+ {/* Content */}
+ {isExpanded && (
+ <>
+
+
+ {/* Actions */}
+
e.stopPropagation()}
+ >
+
+ {onViewRaw && (
+
+ )}
+
+ >
+ )}
+
+ );
+}
+
+export default UserMessage;
diff --git a/ccw/frontend/src/components/shared/CliStreamMonitor/messages/example.tsx b/ccw/frontend/src/components/shared/CliStreamMonitor/messages/example.tsx
new file mode 100644
index 00000000..8606ef5f
--- /dev/null
+++ b/ccw/frontend/src/components/shared/CliStreamMonitor/messages/example.tsx
@@ -0,0 +1,110 @@
+// ========================================
+// Message Components Usage Example
+// ========================================
+// This file demonstrates how to use the message components
+
+import {
+ SystemMessage,
+ UserMessage,
+ AssistantMessage,
+ ErrorMessage
+} from './index';
+
+export function MessageExample() {
+ return (
+
+ {/* System Message Example */}
+
+
+ {/* User Message Example */}
+
console.log('Copied user message')}
+ onViewRaw={() => console.log('View raw JSON')}
+ />
+
+ {/* Assistant Message Example - Thinking */}
+
+
+ {/* Assistant Message Example - Completed */}
+ console.log('Copied assistant message')}
+ />
+
+ {/* Error Message Example */}
+ console.log('Retrying...')}
+ onDismiss={() => console.log('Dismissed')}
+ />
+
+ );
+}
+
+// Props Interface Reference
+/*
+SystemMessageProps:
+ - title: string
+ - timestamp?: number
+ - metadata?: string
+ - content?: string
+ - className?: string
+
+UserMessageProps:
+ - content: string
+ - timestamp?: number
+ - onCopy?: () => void
+ - onViewRaw?: () => void
+ - className?: string
+
+AssistantMessageProps:
+ - content: string
+ - modelName?: string
+ - status?: 'thinking' | 'streaming' | 'completed' | 'error'
+ - duration?: number
+ - tokenCount?: number
+ - timestamp?: number
+ - onCopy?: () => void
+ - className?: string
+
+ErrorMessageProps:
+ - title: string
+ - message: string
+ - timestamp?: number
+ - onRetry?: () => void
+ - onDismiss?: () => void
+ - className?: string
+*/
diff --git a/ccw/frontend/src/components/shared/CliStreamMonitor/messages/index.ts b/ccw/frontend/src/components/shared/CliStreamMonitor/messages/index.ts
new file mode 100644
index 00000000..2a229278
--- /dev/null
+++ b/ccw/frontend/src/components/shared/CliStreamMonitor/messages/index.ts
@@ -0,0 +1,13 @@
+// ========================================
+// Message Components Exports
+// ========================================
+
+export { SystemMessage } from './SystemMessage';
+export { UserMessage } from './UserMessage';
+export { AssistantMessage } from './AssistantMessage';
+export { ErrorMessage } from './ErrorMessage';
+
+export type { SystemMessageProps } from './SystemMessage';
+export type { UserMessageProps } from './UserMessage';
+export type { AssistantMessageProps } from './AssistantMessage';
+export type { ErrorMessageProps } from './ErrorMessage';
diff --git a/ccw/frontend/src/components/shared/CliStreamMonitor.tsx b/ccw/frontend/src/components/shared/CliStreamMonitorLegacy.tsx
similarity index 97%
rename from ccw/frontend/src/components/shared/CliStreamMonitor.tsx
rename to ccw/frontend/src/components/shared/CliStreamMonitorLegacy.tsx
index e0fc054d..6322013b 100644
--- a/ccw/frontend/src/components/shared/CliStreamMonitor.tsx
+++ b/ccw/frontend/src/components/shared/CliStreamMonitorLegacy.tsx
@@ -26,7 +26,7 @@ import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { Input } from '@/components/ui/Input';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/Tabs';
-import { LogBlockList } from '@/components/shared/LogBlock';
+import { LogBlockList, getOutputLineClass } from '@/components/shared/LogBlock';
import { useCliStreamStore, type CliOutputLine } from '@/stores/cliStreamStore';
import { useNotificationStore, selectWsLastMessage } from '@/stores';
import { useActiveCliExecutions, useInvalidateActiveCliExecutions } from '@/hooks/useActiveCliExecutions';
@@ -77,6 +77,7 @@ function formatDuration(ms: number): string {
return `${hours}h ${remainingMinutes}m`;
}
+// Local function for icon rendering (uses JSX, must stay in .tsx file)
function getOutputLineIcon(type: CliOutputLine['type']) {
switch (type) {
case 'thought':
@@ -95,24 +96,6 @@ function getOutputLineIcon(type: CliOutputLine['type']) {
}
}
-function getOutputLineClass(type: CliOutputLine['type']): string {
- switch (type) {
- case 'thought':
- return 'text-purple-400';
- case 'system':
- return 'text-blue-400';
- case 'stderr':
- return 'text-red-400';
- case 'metadata':
- return 'text-yellow-400';
- case 'tool_call':
- return 'text-green-400';
- case 'stdout':
- default:
- return 'text-foreground';
- }
-}
-
// ========== Component ==========
export interface CliStreamMonitorProps {
diff --git a/ccw/frontend/src/components/shared/LogBlock/JsonFormatter.tsx b/ccw/frontend/src/components/shared/LogBlock/JsonFormatter.tsx
new file mode 100644
index 00000000..c3931d9c
--- /dev/null
+++ b/ccw/frontend/src/components/shared/LogBlock/JsonFormatter.tsx
@@ -0,0 +1,353 @@
+// ========================================
+// JsonFormatter Component
+// ========================================
+// Displays JSON content in formatted text or card view
+
+import { useState, useCallback, useMemo } from 'react';
+import { ChevronDown, ChevronRight, Copy, Check, Braces } from 'lucide-react';
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/Button';
+import {
+ detectJsonContent,
+ formatJson,
+ getJsonSummary,
+ getJsonValueTypeColor,
+ type JsonDisplayMode,
+} from './jsonUtils';
+
+// ========== Types ==========
+
+export interface JsonFormatterProps {
+ /** Content to format */
+ content: string;
+ /** Display mode */
+ displayMode?: JsonDisplayMode;
+ /** CSS className */
+ className?: string;
+ /** Maximum lines for text mode (default: 20) */
+ maxLines?: number;
+ /** Whether to show type labels in card mode (default: true) */
+ showTypeLabels?: boolean;
+}
+
+// ========== Helper Components ==========
+
+/**
+ * Copy button with feedback
+ */
+function CopyButton({ text }: { text: string }) {
+ const [copied, setCopied] = useState(false);
+
+ const handleCopy = useCallback(async () => {
+ try {
+ await navigator.clipboard.writeText(text);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ } catch (err) {
+ console.error('Failed to copy:', err);
+ }
+ }, [text]);
+
+ return (
+
+ );
+}
+
+/**
+ * JSON value renderer with syntax highlighting
+ */
+function JsonValue({ value, depth = 0 }: { value: unknown; depth?: number }) {
+ const indent = ' '.repeat(depth);
+ const colorClass = getJsonValueTypeColor(value);
+
+ if (value === null) {
+ return null;
+ }
+
+ if (typeof value === 'boolean') {
+ return {String(value)};
+ }
+
+ if (typeof value === 'number') {
+ return {String(value)};
+ }
+
+ if (typeof value === 'string') {
+ return "{value}";
+ }
+
+ if (Array.isArray(value)) {
+ if (value.length === 0) {
+ return [];
+ }
+
+ return (
+
+
[
+
+ {value.map((item, index) => (
+
+
+ {index < value.length - 1 && ,}
+
+ ))}
+
+
{indent}]
+
+ );
+ }
+
+ if (typeof value === 'object') {
+ const entries = Object.entries(value as Record);
+ if (entries.length === 0) {
+ return {`{}`};
+ }
+
+ return (
+
+
{`{`}
+
+ {entries.map(([key, val], index) => (
+
+ "{key}"
+ :
+
+ {index < entries.length - 1 && ,}
+
+ ))}
+
+
{indent}{`}`}
+
+ );
+ }
+
+ return {String(value)};
+}
+
+/**
+ * Compact JSON view for inline display
+ */
+function JsonCompact({ data }: { data: unknown }) {
+ return (
+
+
+
+ );
+}
+
+/**
+ * Card view for structured JSON display
+ */
+function JsonCard({ data, showTypeLabels = true }: { data: unknown; showTypeLabels?: boolean }) {
+ const [expandedKeys, setExpandedKeys] = useState>(new Set());
+
+ const toggleKey = useCallback((key: string) => {
+ setExpandedKeys((prev) => {
+ const next = new Set(prev);
+ if (next.has(key)) {
+ next.delete(key);
+ } else {
+ next.add(key);
+ }
+ return next;
+ });
+ }, []);
+
+ if (typeof data !== 'object' || data === null || Array.isArray(data)) {
+ // Primitive or array - use inline view
+ return (
+
+
+
+ );
+ }
+
+ const entries = Object.entries(data as Record);
+
+ return (
+
+ {/* Header */}
+
+
+ JSON Data
+
+ ({entries.length} {entries.length === 1 ? 'property' : 'properties'})
+
+
+
+ {/* Properties */}
+
+ {entries.map(([key, value]) => {
+ const isObject = value !== null && typeof value === 'object';
+ const isExpanded = expandedKeys.has(key);
+ const isArray = Array.isArray(value);
+ const summary = getJsonSummary(value);
+
+ return (
+
+
isObject && toggleKey(key)}
+ >
+ {/* Expand/collapse icon */}
+ {isObject && (
+
+ {isExpanded ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+ {/* Key */}
+
"{key}"
+
:
+
+ {/* Value summary or full value */}
+
+ {showTypeLabels && (
+
+ {isArray ? 'array' : isObject ? 'object' : typeof value}
+
+ )}
+ {!isObject ? (
+ {summary}
+ ) : (
+ {summary}
+ )}
+
+
+
+ {/* Expanded nested object */}
+ {isObject && isExpanded && (
+
+ )}
+
+ );
+ })}
+
+
+ );
+}
+
+/**
+ * Text view for formatted JSON
+ */
+function JsonText({ data, maxLines = 20 }: { data: unknown; maxLines?: number }) {
+ const formatted = useMemo(() => formatJson(data), [data]);
+ const lines = formatted.split('\n');
+
+ const showTruncated = maxLines && lines.length > maxLines;
+ const displayLines = showTruncated ? lines.slice(0, maxLines) : lines;
+
+ return (
+
+
+
+ {displayLines.map((line, i) => (
+
+ {line}
+
+ ))}
+ {showTruncated && (
+
+ // ... {lines.length - maxLines} more lines
+
+ )}
+
+
+
+ {/* Copy button */}
+
+
+
+
+ );
+}
+
+// ========== Main Component ==========
+
+/**
+ * JsonFormatter Component
+ *
+ * Displays JSON content in various formats:
+ * - `text`: Formatted JSON text with syntax highlighting
+ * - `card`: Structured card view with collapsible properties
+ * - `inline`: Compact inline display
+ *
+ * Auto-detects JSON from mixed content and validates it.
+ */
+export function JsonFormatter({
+ content,
+ displayMode = 'text',
+ className,
+ maxLines = 20,
+ showTypeLabels = true,
+}: JsonFormatterProps) {
+ // Detect JSON content
+ const detection = useMemo(() => detectJsonContent(content), [content]);
+
+ // Not JSON or invalid - show as plain text
+ if (!detection.isJson) {
+ return (
+
+ {content}
+
+ );
+ }
+
+ // Valid JSON - render based on display mode
+ switch (displayMode) {
+ case 'card':
+ return (
+
+
+
+ );
+
+ case 'inline':
+ return (
+
+
+
+
+
+
+ );
+
+ case 'text':
+ default:
+ return (
+
+
+
+ );
+ }
+}
+
+export default JsonFormatter;
diff --git a/ccw/frontend/src/components/shared/LogBlock/LogBlock.tsx b/ccw/frontend/src/components/shared/LogBlock/LogBlock.tsx
index b5049888..6133c365 100644
--- a/ccw/frontend/src/components/shared/LogBlock/LogBlock.tsx
+++ b/ccw/frontend/src/components/shared/LogBlock/LogBlock.tsx
@@ -22,8 +22,9 @@ import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import type { LogBlockProps, LogLine } from './types';
+import { getOutputLineClass } from './utils';
-// Re-use output line styling helpers from CliStreamMonitor
+// Local function for icon rendering (uses JSX, must stay in .tsx file)
function getOutputLineIcon(type: LogLine['type']) {
switch (type) {
case 'thought':
@@ -42,24 +43,6 @@ function getOutputLineIcon(type: LogLine['type']) {
}
}
-function getOutputLineClass(type: LogLine['type']): string {
- switch (type) {
- case 'thought':
- return 'text-purple-400';
- case 'system':
- return 'text-blue-400';
- case 'stderr':
- return 'text-red-400';
- case 'metadata':
- return 'text-yellow-400';
- case 'tool_call':
- return 'text-green-400';
- case 'stdout':
- default:
- return 'text-foreground';
- }
-}
-
function getBlockBorderClass(status: LogBlockProps['block']['status']): string {
switch (status) {
case 'running':
@@ -247,13 +230,22 @@ export const LogBlock = memo(function LogBlock({
);
}, (prevProps, nextProps) => {
// Custom comparison for performance
+ // Compare all relevant block fields to detect changes
+ const prevBlock = prevProps.block;
+ const nextBlock = nextProps.block;
+
return (
- prevProps.block.id === nextProps.block.id &&
- prevProps.block.status === nextProps.block.status &&
- prevProps.block.lineCount === nextProps.block.lineCount &&
- prevProps.block.duration === nextProps.block.duration &&
prevProps.isExpanded === nextProps.isExpanded &&
- prevProps.className === nextProps.className
+ prevProps.className === nextProps.className &&
+ prevBlock.id === nextBlock.id &&
+ prevBlock.status === nextBlock.status &&
+ prevBlock.title === nextBlock.title &&
+ prevBlock.toolName === nextBlock.toolName &&
+ prevBlock.lineCount === nextBlock.lineCount &&
+ prevBlock.duration === nextBlock.duration
+ // Note: We don't compare block.lines deeply for performance reasons.
+ // The store's getBlocks method returns cached arrays, so if lines change
+ // significantly, a new block object will be created and the id will change.
);
});
diff --git a/ccw/frontend/src/components/shared/LogBlock/LogBlockList.tsx b/ccw/frontend/src/components/shared/LogBlock/LogBlockList.tsx
index 5a62565d..95e4f09d 100644
--- a/ccw/frontend/src/components/shared/LogBlock/LogBlockList.tsx
+++ b/ccw/frontend/src/components/shared/LogBlock/LogBlockList.tsx
@@ -3,208 +3,9 @@
// ========================================
// Container component for displaying grouped CLI output blocks
-import React, { useState, useMemo, useCallback } from 'react';
-import { useCliStreamStore } from '@/stores/cliStreamStore';
+import { useState, useCallback, useMemo } from 'react';
+import { useCliStreamStore, type LogBlockData } from '@/stores/cliStreamStore';
import { LogBlock } from './LogBlock';
-import type { LogBlockData, LogLine } from './types';
-import type { CliOutputLine } from '@/stores/cliStreamStore';
-
-/**
- * Parse tool call metadata from content
- * Expected format: "[Tool] toolName(args)"
- */
-function parseToolCallMetadata(content: string): { toolName: string; args: string } | undefined {
- const toolCallMatch = content.match(/^\[Tool\]\s+(\w+)\((.*)\)$/);
- if (toolCallMatch) {
- return {
- toolName: toolCallMatch[1],
- args: toolCallMatch[2] || '',
- };
- }
- return undefined;
-}
-
-/**
- * Generate block title based on type and content
- */
-function generateBlockTitle(lineType: string, content: string): string {
- switch (lineType) {
- case 'tool_call':
- const metadata = parseToolCallMetadata(content);
- if (metadata) {
- return metadata.args ? `${metadata.toolName}(${metadata.args})` : metadata.toolName;
- }
- return 'Tool Call';
- case 'thought':
- return 'Thought';
- case 'system':
- return 'System';
- case 'stderr':
- return 'Error Output';
- case 'stdout':
- return 'Output';
- case 'metadata':
- return 'Metadata';
- default:
- return 'Log';
- }
-}
-
-/**
- * Get block type for a line
- */
-function getBlockType(lineType: string): LogBlockData['type'] {
- switch (lineType) {
- case 'tool_call':
- return 'tool';
- case 'thought':
- return 'info';
- case 'system':
- return 'info';
- case 'stderr':
- return 'error';
- case 'stdout':
- case 'metadata':
- default:
- return 'output';
- }
-}
-
-/**
- * Check if a line type should start a new block
- */
-function shouldStartNewBlock(lineType: string, currentBlockType: string | null): boolean {
- // No current block exists
- if (!currentBlockType) {
- return true;
- }
-
- // These types always start new blocks
- if (lineType === 'tool_call' || lineType === 'thought' || lineType === 'system') {
- return true;
- }
-
- // stderr starts a new block if not already in stderr
- if (lineType === 'stderr' && currentBlockType !== 'stderr') {
- return true;
- }
-
- // tool_call block captures all following stdout/stderr until next tool_call
- if (currentBlockType === 'tool_call' && (lineType === 'stdout' || lineType === 'stderr')) {
- return false;
- }
-
- // stderr block captures all stderr until next different type
- if (currentBlockType === 'stderr' && lineType === 'stderr') {
- return false;
- }
-
- // stdout merges into current stdout block
- if (currentBlockType === 'stdout' && lineType === 'stdout') {
- return false;
- }
-
- // Different type - start new block
- if (currentBlockType !== lineType) {
- return true;
- }
-
- return false;
-}
-
-/**
- * Group CLI output lines into log blocks
- *
- * Block grouping rules:
- * 1. tool_call starts new block, includes following stdout/stderr until next tool_call
- * 2. thought becomes independent block
- * 3. system becomes independent block
- * 4. stderr becomes highlighted block
- * 5. Other stdout merges into normal blocks
- */
-function groupLinesIntoBlocks(
- lines: CliOutputLine[],
- executionId: string,
- executionStatus: 'running' | 'completed' | 'error'
-): LogBlockData[] {
- const blocks: LogBlockData[] = [];
- let currentLines: LogLine[] = [];
- let currentType: string | null = null;
- let currentTitle = '';
- let currentToolName: string | undefined;
- let blockStartTime = 0;
- let blockIndex = 0;
-
- for (const line of lines) {
- const blockType = getBlockType(line.type);
-
- // Check if we need to start a new block
- if (shouldStartNewBlock(line.type, currentType)) {
- // Save current block if exists
- if (currentLines.length > 0) {
- const duration = blockStartTime > 0 ? line.timestamp - blockStartTime : undefined;
- blocks.push({
- id: `${executionId}-block-${blockIndex}`,
- title: currentTitle || generateBlockTitle(currentType || '', currentLines[0]?.content || ''),
- type: getBlockType(currentType || ''),
- status: executionStatus === 'running' ? 'running' : 'completed',
- toolName: currentToolName,
- lineCount: currentLines.length,
- duration,
- lines: currentLines,
- timestamp: blockStartTime,
- });
- blockIndex++;
- }
-
- // Start new block
- currentType = line.type;
- currentTitle = generateBlockTitle(line.type, line.content);
- currentLines = [
- {
- type: line.type,
- content: line.content,
- timestamp: line.timestamp,
- },
- ];
- blockStartTime = line.timestamp;
-
- // Extract tool name for tool_call blocks
- if (line.type === 'tool_call') {
- const metadata = parseToolCallMetadata(line.content);
- currentToolName = metadata?.toolName;
- } else {
- currentToolName = undefined;
- }
- } else {
- // Add line to current block
- currentLines.push({
- type: line.type,
- content: line.content,
- timestamp: line.timestamp,
- });
- }
- }
-
- // Finalize the last block
- if (currentLines.length > 0) {
- const lastLine = currentLines[currentLines.length - 1];
- const duration = blockStartTime > 0 ? lastLine.timestamp - blockStartTime : undefined;
- blocks.push({
- id: `${executionId}-block-${blockIndex}`,
- title: currentTitle || generateBlockTitle(currentType || '', currentLines[0]?.content || ''),
- type: getBlockType(currentType || ''),
- status: executionStatus === 'running' ? 'running' : 'completed',
- toolName: currentToolName,
- lineCount: currentLines.length,
- duration,
- lines: currentLines,
- timestamp: blockStartTime,
- });
- }
-
- return blocks;
-}
/**
* Props for LogBlockList component
@@ -219,29 +20,26 @@ export interface LogBlockListProps {
/**
* LogBlockList component
* Displays CLI output grouped into collapsible blocks
+ *
+ * Uses the store's getBlocks method to retrieve pre-computed blocks,
+ * avoiding duplicate logic and ensuring consistent block grouping.
*/
export function LogBlockList({ executionId, className }: LogBlockListProps) {
- // Get execution data from store
- const executions = useCliStreamStore((state) => state.executions);
+ // Get blocks directly from store using the getBlocks selector
+ // This avoids duplicate logic and leverages store-side caching
+ const blocks = useCliStreamStore(
+ (state) => executionId ? state.getBlocks(executionId) : [],
+ (a, b) => a === b // Shallow comparison - arrays are cached in store
+ );
- // Get current execution or execution by ID
- const currentExecution = useMemo(() => {
- if (!executionId) return null;
- return executions[executionId] || null;
- }, [executions, executionId]);
+ // Get execution status for empty state display
+ const currentExecution = useCliStreamStore((state) =>
+ executionId ? state.executions[executionId] : null
+ );
// Manage expanded blocks state
const [expandedBlocks, setExpandedBlocks] = useState>(new Set());
- // Group output lines into blocks
- const blocks = useMemo(() => {
- if (!currentExecution?.output || currentExecution.output.length === 0) {
- return [];
- }
-
- return groupLinesIntoBlocks(currentExecution.output, executionId!, currentExecution.status);
- }, [currentExecution, executionId]);
-
// Toggle block expand/collapse
const toggleBlockExpand = useCallback((blockId: string) => {
setExpandedBlocks((prev) => {
diff --git a/ccw/frontend/src/components/shared/LogBlock/index.ts b/ccw/frontend/src/components/shared/LogBlock/index.ts
index 77cf1db6..0b6c2bc8 100644
--- a/ccw/frontend/src/components/shared/LogBlock/index.ts
+++ b/ccw/frontend/src/components/shared/LogBlock/index.ts
@@ -4,4 +4,5 @@
export { LogBlock, default } from './LogBlock';
export { LogBlockList, type LogBlockListProps } from './LogBlockList';
+export { getOutputLineClass } from './utils';
export type { LogBlockProps, LogBlockData, LogLine } from './types';
diff --git a/ccw/frontend/src/components/shared/LogBlock/jsonUtils.ts b/ccw/frontend/src/components/shared/LogBlock/jsonUtils.ts
new file mode 100644
index 00000000..51d8b97a
--- /dev/null
+++ b/ccw/frontend/src/components/shared/LogBlock/jsonUtils.ts
@@ -0,0 +1,187 @@
+// ========================================
+// LogBlock JSON Utilities
+// ========================================
+// JSON content detection and formatting utilities
+
+/**
+ * JSON content type detection result
+ */
+export interface JsonDetectionResult {
+ isJson: boolean;
+ parsed?: unknown;
+ error?: string;
+ format: 'object' | 'array' | 'primitive' | 'invalid';
+}
+
+/**
+ * Display mode for JSON content
+ */
+export type JsonDisplayMode = 'text' | 'card' | 'inline';
+
+/**
+ * Detect if content is valid JSON
+ *
+ * @param content - Content string to check
+ * @returns Detection result with parsed data if valid
+ */
+export function detectJson(content: string): JsonDetectionResult {
+ const trimmed = content.trim();
+
+ // Quick check for JSON patterns
+ if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
+ return { isJson: false, format: 'invalid' };
+ }
+
+ try {
+ const parsed = JSON.parse(trimmed);
+
+ // Determine format type
+ let format: JsonDetectionResult['format'] = 'primitive';
+ if (Array.isArray(parsed)) {
+ format = 'array';
+ } else if (parsed !== null && typeof parsed === 'object') {
+ format = 'object';
+ }
+
+ return { isJson: true, parsed, format };
+ } catch (error) {
+ return {
+ isJson: false,
+ format: 'invalid',
+ error: error instanceof Error ? error.message : 'Unknown parse error'
+ };
+ }
+}
+
+/**
+ * Extract JSON from mixed content
+ * Handles cases where JSON is embedded in text output
+ *
+ * @param content - Content that may contain JSON
+ * @returns Extracted JSON string or null if not found
+ */
+export function extractJson(content: string): string | null {
+ const trimmed = content.trim();
+
+ // Direct JSON
+ if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
+ // Find the matching bracket
+ let depth = 0;
+ let inString = false;
+ let escape = false;
+ let end = -1;
+
+ for (let i = 0; i < trimmed.length; i++) {
+ const char = trimmed[i];
+
+ if (escape) {
+ escape = false;
+ continue;
+ }
+
+ if (char === '\\') {
+ escape = true;
+ continue;
+ }
+
+ if (char === '"') {
+ inString = !inString;
+ continue;
+ }
+
+ if (!inString) {
+ if (char === '{' || char === '[') {
+ depth++;
+ } else if (char === '}' || char === ']') {
+ depth--;
+ if (depth === 0) {
+ end = i + 1;
+ break;
+ }
+ }
+ }
+ }
+
+ if (end > 0) {
+ return trimmed.substring(0, end);
+ }
+ }
+
+ // Try to find JSON in code blocks
+ const codeBlockMatch = content.match(/```(?:json)?\s*\n([\s\S]*?)\n```/);
+ if (codeBlockMatch) {
+ return codeBlockMatch[1].trim();
+ }
+
+ return null;
+}
+
+/**
+ * Detect if content should be displayed as JSON
+ * Combines extraction and validation
+ *
+ * @param content - Content to check
+ * @returns Detection result
+ */
+export function detectJsonContent(content: string): JsonDetectionResult & { extracted: string | null } {
+ const extracted = extractJson(content);
+
+ if (!extracted) {
+ return { isJson: false, format: 'invalid', extracted: null };
+ }
+
+ const result = detectJson(extracted);
+ return { ...result, extracted };
+}
+
+/**
+ * Format JSON for display
+ *
+ * @param data - Parsed JSON data
+ * @param indent - Indentation spaces (default: 2)
+ * @returns Formatted JSON string
+ */
+export function formatJson(data: unknown, indent: number = 2): string {
+ return JSON.stringify(data, null, indent);
+}
+
+/**
+ * Get a summary string for JSON data
+ *
+ * @param data - Parsed JSON data
+ * @returns Summary description
+ */
+export function getJsonSummary(data: unknown): string {
+ if (data === null) return 'null';
+ if (typeof data === 'boolean') return data ? 'true' : 'false';
+ if (typeof data === 'number') return String(data);
+ if (typeof data === 'string') return `"${data.length > 30 ? data.substring(0, 30) + '...' : data}"`;
+
+ if (Array.isArray(data)) {
+ const length = data.length;
+ return `Array[${length}]${length > 0 ? ` (${getJsonSummary(data[0])}, ...)` : ''}`;
+ }
+
+ if (typeof data === 'object') {
+ const keys = Object.keys(data);
+ return `Object{${keys.length}}${keys.length > 0 ? ` (${keys.slice(0, 3).join(', ')}${keys.length > 3 ? ', ...' : ''})` : ''}`;
+ }
+
+ return String(data);
+}
+
+/**
+ * Get color class for JSON value type
+ *
+ * @param value - JSON value
+ * @returns Tailwind color class
+ */
+export function getJsonValueTypeColor(value: unknown): string {
+ if (value === null) return 'text-muted-foreground';
+ if (typeof value === 'boolean') return 'text-purple-400';
+ if (typeof value === 'number') return 'text-orange-400';
+ if (typeof value === 'string') return 'text-green-400';
+ if (Array.isArray(value)) return 'text-blue-400';
+ if (typeof value === 'object') return 'text-yellow-400';
+ return 'text-foreground';
+}
diff --git a/ccw/frontend/src/components/shared/LogBlock/utils.ts b/ccw/frontend/src/components/shared/LogBlock/utils.ts
new file mode 100644
index 00000000..e3d70eba
--- /dev/null
+++ b/ccw/frontend/src/components/shared/LogBlock/utils.ts
@@ -0,0 +1,30 @@
+// ========================================
+// LogBlock Utility Functions
+// ========================================
+// Shared helper functions for LogBlock components
+
+import type { CliOutputLine } from '@/stores/cliStreamStore';
+
+/**
+ * Get the CSS class name for a given output line type
+ *
+ * @param type - The output line type
+ * @returns The CSS class name for styling the line
+ */
+export function getOutputLineClass(type: CliOutputLine['type']): string {
+ switch (type) {
+ case 'thought':
+ return 'text-purple-400';
+ case 'system':
+ return 'text-blue-400';
+ case 'stderr':
+ return 'text-red-400';
+ case 'metadata':
+ return 'text-yellow-400';
+ case 'tool_call':
+ return 'text-green-400';
+ case 'stdout':
+ default:
+ return 'text-foreground';
+ }
+}
diff --git a/ccw/frontend/src/components/shared/index.ts b/ccw/frontend/src/components/shared/index.ts
index 63d4e08f..cd8d37a5 100644
--- a/ccw/frontend/src/components/shared/index.ts
+++ b/ccw/frontend/src/components/shared/index.ts
@@ -60,12 +60,72 @@ export type { FlowchartProps } from './Flowchart';
export { CliStreamPanel } from './CliStreamPanel';
export type { CliStreamPanelProps } from './CliStreamPanel';
-export { CliStreamMonitor } from './CliStreamMonitor';
-export type { CliStreamMonitorProps } from './CliStreamMonitor';
+// New CliStreamMonitor with message-based layout
+export { CliStreamMonitor } from './CliStreamMonitor/index';
+export type { CliStreamMonitorProps } from './CliStreamMonitor/index';
+
+// Legacy CliStreamMonitor (old layout)
+export { default as CliStreamMonitorLegacy } from './CliStreamMonitorLegacy';
+export type { CliStreamMonitorProps as CliStreamMonitorLegacyProps } from './CliStreamMonitorLegacy';
export { StreamingOutput } from './StreamingOutput';
export type { StreamingOutputProps } from './StreamingOutput';
+// CliStreamMonitor sub-components
+export { MonitorHeader } from './CliStreamMonitor/index';
+export type { MonitorHeaderProps } from './CliStreamMonitor/index';
+
+export { MonitorToolbar } from './CliStreamMonitor/index';
+export type { MonitorToolbarProps, FilterType, ViewMode } from './CliStreamMonitor/index';
+
+export { MonitorBody } from './CliStreamMonitor/index';
+export type { MonitorBodyProps, MonitorBodyRef } from './CliStreamMonitor/index';
+
+export { MessageRenderer } from './CliStreamMonitor/index';
+export type { MessageRendererProps } from './CliStreamMonitor/index';
+
+// Message components for CLI streaming
+export {
+ SystemMessage,
+ UserMessage,
+ AssistantMessage,
+ ErrorMessage
+} from './CliStreamMonitor/messages';
+export type {
+ SystemMessageProps,
+ UserMessageProps,
+ AssistantMessageProps,
+ ErrorMessageProps
+} from './CliStreamMonitor/messages';
+
+// LogBlock components
+export {
+ LogBlock,
+ LogBlockList,
+ getOutputLineClass,
+} from './LogBlock';
+export type {
+ LogBlockProps,
+ LogBlockData,
+ LogLine,
+ LogBlockListProps,
+} from './LogBlock';
+
+// JsonFormatter
+export { JsonFormatter } from './LogBlock/JsonFormatter';
+export type { JsonFormatterProps, JsonDisplayMode } from './LogBlock/JsonFormatter';
+
+// JSON utilities
+export {
+ detectJson,
+ detectJsonContent,
+ extractJson,
+ formatJson,
+ getJsonSummary,
+ getJsonValueTypeColor,
+} from './LogBlock/jsonUtils';
+export type { JsonDetectionResult, JsonDisplayMode as JsonMode } from './LogBlock/jsonUtils';
+
// Dialog components
export { RuleDialog } from './RuleDialog';
export type { RuleDialogProps } from './RuleDialog';
diff --git a/ccw/frontend/src/components/ui/Collapsible.tsx b/ccw/frontend/src/components/ui/Collapsible.tsx
new file mode 100644
index 00000000..78aba2d5
--- /dev/null
+++ b/ccw/frontend/src/components/ui/Collapsible.tsx
@@ -0,0 +1,24 @@
+import * as React from "react";
+import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
+import { cn } from "@/lib/utils";
+
+const Collapsible = CollapsiblePrimitive.Root;
+
+const CollapsibleTrigger = CollapsiblePrimitive.Trigger;
+
+const CollapsibleContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+CollapsibleContent.displayName = CollapsiblePrimitive.Content.displayName;
+
+export { Collapsible, CollapsibleTrigger, CollapsibleContent };
diff --git a/ccw/frontend/src/components/ui/index.ts b/ccw/frontend/src/components/ui/index.ts
index 28c7354e..c969a245 100644
--- a/ccw/frontend/src/components/ui/index.ts
+++ b/ccw/frontend/src/components/ui/index.ts
@@ -85,3 +85,10 @@ export {
ToastClose,
ToastAction,
} from "./Toast";
+
+// Collapsible (Radix)
+export {
+ Collapsible,
+ CollapsibleTrigger,
+ CollapsibleContent,
+} from "./Collapsible";
diff --git a/ccw/frontend/src/lib/api.ts b/ccw/frontend/src/lib/api.ts
index 92769c47..80025fdc 100644
--- a/ccw/frontend/src/lib/api.ts
+++ b/ccw/frontend/src/lib/api.ts
@@ -1033,6 +1033,7 @@ export interface SessionDetailResponse {
session: SessionMetadata;
context?: SessionDetailContext;
summary?: string;
+ summaries?: Array<{ name: string; content: string }>;
implPlan?: unknown;
conflicts?: unknown[];
review?: unknown;
@@ -1061,10 +1062,17 @@ export async function fetchSessionDetail(sessionId: string, projectPath?: string
const detailData = await fetchApi(`/api/session-detail?path=${encodeURIComponent(pathParam)}&type=all`);
// Step 3: Transform the response to match SessionDetailResponse interface
+ // Also check for summaries array and extract first one if summary is empty
+ let finalSummary = detailData.summary;
+ if (!finalSummary && detailData.summaries && detailData.summaries.length > 0) {
+ finalSummary = detailData.summaries[0].content || detailData.summaries[0].name || '';
+ }
+
return {
session,
context: detailData.context,
- summary: detailData.summary,
+ summary: finalSummary,
+ summaries: detailData.summaries,
implPlan: detailData.implPlan,
conflicts: detailData.conflicts,
review: detailData.review,
diff --git a/ccw/frontend/src/locales/en/cli-monitor.json b/ccw/frontend/src/locales/en/cli-monitor.json
index 086311c2..636e581f 100644
--- a/ccw/frontend/src/locales/en/cli-monitor.json
+++ b/ccw/frontend/src/locales/en/cli-monitor.json
@@ -1,6 +1,6 @@
{
"title": "CLI Stream Monitor",
- "searchPlaceholder": "Search output...",
+ "searchPlaceholder": "Search logs...",
"noExecutions": "No active CLI executions",
"noExecutionsHint": "Start a CLI command to see streaming output",
"selectExecution": "Select an execution to view output",
@@ -14,5 +14,36 @@
"autoScroll": "Auto-scroll",
"scrollToBottom": "Scroll to bottom",
"close": "Close",
- "refresh": "Refresh"
+ "refresh": "Refresh",
+ "refreshing": "Refreshing...",
+ "live": "Live",
+ "executions": "{count} execution{count, plural, =1 {} other {s}}",
+ "active": "{count} active",
+ "filter": {
+ "all": "All",
+ "errors": "Errors",
+ "content": "Content",
+ "system": "System"
+ },
+ "view": {
+ "preview": "Preview",
+ "json": "JSON",
+ "raw": "Raw"
+ },
+ "viewMode": "View Mode",
+ "settings": "Settings",
+ "noMessages": "Waiting for messages...",
+ "noMatch": "No matching messages found",
+ "statusBar": "{total} executions | {active} active | {error} error | {lines} lines",
+ "copy": "Copy",
+ "copied": "Copied",
+ "rawJson": "Raw JSON",
+ "retry": "Retry",
+ "dismiss": "Dismiss",
+ "thinking": "Thinking...",
+ "streaming": "Streaming...",
+ "tokens": "Tokens: {count}",
+ "duration": "Duration: {value}",
+ "model": "Model: {name}",
+ "user": "User"
}
diff --git a/ccw/frontend/src/locales/en/issues.json b/ccw/frontend/src/locales/en/issues.json
index fc326312..1f25f97d 100644
--- a/ccw/frontend/src/locales/en/issues.json
+++ b/ccw/frontend/src/locales/en/issues.json
@@ -89,6 +89,14 @@
"title": "Discovery",
"pageTitle": "Issue Discovery",
"description": "View and manage issue discovery sessions",
+ "totalSessions": "Total Sessions",
+ "completedSessions": "Completed",
+ "runningSessions": "Running",
+ "totalFindings": "Findings",
+ "sessionList": "Session List",
+ "noSessions": "No sessions found",
+ "noSessionsDescription": "Start a new discovery session to begin",
+ "findingsDetail": "Findings Detail",
"stats": {
"totalSessions": "Total Sessions",
"completed": "Completed",
@@ -135,5 +143,14 @@
"export": "Export Findings",
"refresh": "Refresh"
}
+ },
+ "hub": {
+ "title": "Issue Hub",
+ "description": "Unified management for issues, queues, and discoveries",
+ "tabs": {
+ "issues": "Issues",
+ "queue": "Queue",
+ "discovery": "Discovery"
+ }
}
}
diff --git a/ccw/frontend/src/locales/en/navigation.json b/ccw/frontend/src/locales/en/navigation.json
index c8ae7534..c906d9d4 100644
--- a/ccw/frontend/src/locales/en/navigation.json
+++ b/ccw/frontend/src/locales/en/navigation.json
@@ -41,5 +41,42 @@
"sessions": "Sessions",
"detail": "Details",
"settings": "Settings"
+ },
+ "cliMonitor": {
+ "title": "CLI Stream Monitor",
+ "live": "Live",
+ "executions": "executions",
+ "active": "active",
+ "errors": "errors",
+ "lines": "lines",
+ "refresh": "Refresh",
+ "refreshing": "Refreshing...",
+ "searchPlaceholder": "Search logs...",
+ "clear": "Clear",
+ "filterAll": "All",
+ "filterErrors": "Errors",
+ "filterContent": "Content",
+ "filterSystem": "System",
+ "viewPreview": "Preview",
+ "viewJson": "JSON",
+ "viewRaw": "Raw",
+ "settings": "Settings",
+ "noExecutions": "No active CLI executions",
+ "noExecutionsHint": "Start a CLI command to see streaming output",
+ "noMessages": "Waiting for messages...",
+ "noMatch": "No matching messages found",
+ "statusBar": "{total} executions | {active} active | {errors} error | {lines} lines",
+ "copy": "Copy",
+ "copied": "Copied!",
+ "rawJson": "Raw JSON",
+ "expand": "Expand",
+ "collapse": "Collapse",
+ "retry": "Retry",
+ "dismiss": "Dismiss",
+ "thinking": "Thinking...",
+ "completed": "Completed",
+ "tokens": "Tokens",
+ "duration": "Duration",
+ "model": "Model"
}
}
diff --git a/ccw/frontend/src/locales/zh/cli-monitor.json b/ccw/frontend/src/locales/zh/cli-monitor.json
index be6134c6..0aef085d 100644
--- a/ccw/frontend/src/locales/zh/cli-monitor.json
+++ b/ccw/frontend/src/locales/zh/cli-monitor.json
@@ -1,6 +1,6 @@
{
"title": "CLI 流式监控",
- "searchPlaceholder": "搜索输出...",
+ "searchPlaceholder": "搜索日志...",
"noExecutions": "没有正在执行的 CLI 任务",
"noExecutionsHint": "启动 CLI 命令以查看实时输出",
"selectExecution": "选择一个任务以查看输出",
@@ -14,5 +14,36 @@
"autoScroll": "自动滚动",
"scrollToBottom": "滚动到底部",
"close": "关闭",
- "refresh": "刷新"
+ "refresh": "刷新",
+ "refreshing": "刷新中...",
+ "live": "实时",
+ "executions": "{count} 个执行",
+ "active": "{count} 个活跃",
+ "filter": {
+ "all": "全部",
+ "errors": "错误",
+ "content": "内容",
+ "system": "系统"
+ },
+ "view": {
+ "preview": "预览",
+ "json": "JSON",
+ "raw": "原始"
+ },
+ "viewMode": "视图模式",
+ "settings": "设置",
+ "noMessages": "等待消息...",
+ "noMatch": "没有匹配的消息",
+ "statusBar": "{total} 个执行 | {active} 个活跃 | {error} 个错误 | {lines} 行",
+ "copy": "复制",
+ "copied": "已复制",
+ "rawJson": "原始 JSON",
+ "retry": "重试",
+ "dismiss": "关闭",
+ "thinking": "思考中...",
+ "streaming": "流式输出中...",
+ "tokens": "令牌: {count}",
+ "duration": "时长: {value}",
+ "model": "模型: {name}",
+ "user": "用户"
}
diff --git a/ccw/frontend/src/locales/zh/issues.json b/ccw/frontend/src/locales/zh/issues.json
index af3523a5..0918cf98 100644
--- a/ccw/frontend/src/locales/zh/issues.json
+++ b/ccw/frontend/src/locales/zh/issues.json
@@ -89,6 +89,14 @@
"title": "发现",
"pageTitle": "问题发现",
"description": "查看和管理问题发现会话",
+ "totalSessions": "总会话数",
+ "completedSessions": "已完成",
+ "runningSessions": "运行中",
+ "totalFindings": "发现",
+ "sessionList": "会话列表",
+ "noSessions": "未发现会话",
+ "noSessionsDescription": "启动新的问题发现会话以开始",
+ "findingsDetail": "发现详情",
"stats": {
"totalSessions": "总会话数",
"completed": "已完成",
@@ -135,5 +143,14 @@
"export": "导出发现",
"refresh": "刷新"
}
+ },
+ "hub": {
+ "title": "问题中心",
+ "description": "统一管理问题、队列和发现",
+ "tabs": {
+ "issues": "问题列表",
+ "queue": "执行队列",
+ "discovery": "问题发现"
+ }
}
}
diff --git a/ccw/frontend/src/locales/zh/navigation.json b/ccw/frontend/src/locales/zh/navigation.json
index 0d03eecf..d14fe435 100644
--- a/ccw/frontend/src/locales/zh/navigation.json
+++ b/ccw/frontend/src/locales/zh/navigation.json
@@ -41,5 +41,42 @@
"sessions": "会话",
"detail": "详情",
"settings": "设置"
+ },
+ "cliMonitor": {
+ "title": "CLI 流式监控",
+ "live": "实时",
+ "executions": "个执行",
+ "active": "活跃",
+ "errors": "错误",
+ "lines": "行",
+ "refresh": "刷新",
+ "refreshing": "刷新中...",
+ "searchPlaceholder": "搜索日志...",
+ "clear": "清除",
+ "filterAll": "全部",
+ "filterErrors": "错误",
+ "filterContent": "内容",
+ "filterSystem": "系统",
+ "viewPreview": "预览",
+ "viewJson": "JSON",
+ "viewRaw": "原始",
+ "settings": "设置",
+ "noExecutions": "无活跃的 CLI 执行",
+ "noExecutionsHint": "启动 CLI 命令以查看流式输出",
+ "noMessages": "等待消息...",
+ "noMatch": "未找到匹配的消息",
+ "statusBar": "{total} 个执行 | {active} 个活跃 | {errors} 个错误 | {lines} 行",
+ "copy": "复制",
+ "copied": "已复制!",
+ "rawJson": "原始 JSON",
+ "expand": "展开",
+ "collapse": "折叠",
+ "retry": "重试",
+ "dismiss": "忽略",
+ "thinking": "思考中...",
+ "completed": "已完成",
+ "tokens": "令牌数",
+ "duration": "耗时",
+ "model": "模型"
}
}
diff --git a/ccw/frontend/src/pages/IssueHubPage.tsx b/ccw/frontend/src/pages/IssueHubPage.tsx
new file mode 100644
index 00000000..b83b4afc
--- /dev/null
+++ b/ccw/frontend/src/pages/IssueHubPage.tsx
@@ -0,0 +1,32 @@
+// ========================================
+// Issue Hub Page
+// ========================================
+// Unified page for issues, queue, and discovery with tab navigation
+
+import { useSearchParams } from 'react-router-dom';
+import { IssueHubHeader } from '@/components/issue/hub/IssueHubHeader';
+import { IssueHubTabs, type IssueTab } from '@/components/issue/hub/IssueHubTabs';
+import { IssuesPanel } from '@/components/issue/hub/IssuesPanel';
+import { QueuePanel } from '@/components/issue/hub/QueuePanel';
+import { DiscoveryPanel } from '@/components/issue/hub/DiscoveryPanel';
+
+export function IssueHubPage() {
+ const [searchParams, setSearchParams] = useSearchParams();
+ const currentTab = (searchParams.get('tab') as IssueTab) || 'issues';
+
+ const setCurrentTab = (tab: IssueTab) => {
+ setSearchParams({ tab });
+ };
+
+ return (
+
+
+
+ {currentTab === 'issues' && }
+ {currentTab === 'queue' && }
+ {currentTab === 'discovery' && }
+
+ );
+}
+
+export default IssueHubPage;
diff --git a/ccw/frontend/src/pages/LiteTaskDetailPage.tsx b/ccw/frontend/src/pages/LiteTaskDetailPage.tsx
index 29da3729..79894926 100644
--- a/ccw/frontend/src/pages/LiteTaskDetailPage.tsx
+++ b/ccw/frontend/src/pages/LiteTaskDetailPage.tsx
@@ -1,7 +1,12 @@
// ========================================
// LiteTaskDetailPage Component
// ========================================
-// Lite task detail page with flowchart visualization
+// Lite task detail page with multi-tab task view supporting:
+// - Lite-Plan/Lite-Fix: Tasks, Plan, Diagnoses, Context, Summary tabs
+// - Multi-CLI: Tasks, Discussion, Context, Summary tabs
+// - Context Package parsing with collapsible sections
+// - Exploration packages with multiple analysis angles
+// - Flowchart visualization for implementation steps
import * as React from 'react';
import { useParams, useNavigate } from 'react-router-dom';
@@ -17,32 +22,219 @@ import {
Clock,
Code,
Zap,
+ ListTodo,
+ Package,
+ FileCode,
+ Settings,
+ BookOpen,
+ Search,
+ Folder,
+ MessageSquare,
+ FileText,
ChevronDown,
ChevronRight,
+ Ruler,
+ Stethoscope,
} from 'lucide-react';
import { useLiteTaskSession } from '@/hooks/useLiteTasks';
import { Flowchart } from '@/components/shared/Flowchart';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
-import { Card, CardContent } from '@/components/ui/Card';
-import type { LiteTask } from '@/lib/api';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
+import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/Tabs';
+import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@/components/ui/Collapsible';
+import type { LiteTask, LiteTaskSession } from '@/lib/api';
+
+// ========================================
+// Type Definitions
+// ========================================
+
+type SessionType = 'lite-plan' | 'lite-fix' | 'multi-cli-plan';
+
+type LitePlanTab = 'tasks' | 'plan' | 'diagnoses' | 'context' | 'summary';
+type MultiCliTab = 'tasks' | 'discussion' | 'context' | 'summary';
+
+type TaskTabValue = 'task' | 'context';
+
+// Context Package Structure
+interface ContextPackage {
+ task_description?: string;
+ constraints?: string[];
+ focus_paths?: string[];
+ relevant_files?: Array;
+ dependencies?: string[] | Array<{ name: string; type: string; version: string }>;
+ conflict_risks?: string[] | Array<{ description: string; severity: string }>;
+ session_id?: string;
+ metadata?: {
+ created_at: string;
+ version: string;
+ source: string;
+ };
+}
+
+// Exploration Structure
+interface Exploration {
+ name: string;
+ path: string;
+ content?: string;
+}
+
+interface ExplorationData {
+ manifest?: {
+ task_description: string;
+ complexity: 'low' | 'medium' | 'high';
+ exploration_count: number;
+ created_at: string;
+ };
+ data?: {
+ architecture?: ExplorationAngle;
+ dependencies?: ExplorationAngle;
+ patterns?: ExplorationAngle;
+ 'integration-points'?: ExplorationAngle;
+ testing?: ExplorationAngle;
+ };
+}
+
+interface ExplorationAngle {
+ findings: string[];
+ recommendations: string[];
+ patterns: string[];
+ risks: string[];
+}
+
+// Diagnosis Structure
+interface Diagnosis {
+ symptom: string;
+ root_cause: string;
+ issues: Array<{
+ file: string;
+ line: number;
+ severity: 'high' | 'medium' | 'low';
+ message: string;
+ }>;
+ affected_files: string[];
+ fix_hints: string[];
+ recommendations: string[];
+}
+
+// Discussion/Round Structure
+interface DiscussionRound {
+ metadata: {
+ roundId: number;
+ timestamp: string;
+ durationSeconds: number;
+ contributingAgents: Array<{ name: string; id: string }>;
+ };
+ solutions: DiscussionSolution[];
+ _internal: {
+ convergence: {
+ score: number;
+ recommendation: 'proceed' | 'continue' | 'pause';
+ reasoning: string;
+ };
+ cross_verification: {
+ agreements: string[];
+ disagreements: string[];
+ resolution: string;
+ };
+ };
+}
+
+interface DiscussionSolution {
+ id: string;
+ name: string;
+ summary: string | { en: string; zh: string };
+ feasibility: number;
+ effort: 'low' | 'medium' | 'high';
+ risk: 'low' | 'medium' | 'high';
+ source_cli: string[];
+ implementation_plan: {
+ approach: string;
+ tasks: ImplementationTask[];
+ milestones: Milestone[];
+ };
+}
+
+// Synthesis Structure
+interface Synthesis {
+ convergence: {
+ summary: string | { en: string; zh: string };
+ score: number;
+ recommendation: 'proceed' | 'continue' | 'pause' | 'complete' | 'halt';
+ };
+ cross_verification: {
+ agreements: string[];
+ disagreements: string[];
+ resolution: string;
+ };
+ final_solution: DiscussionSolution;
+ alternative_solutions: DiscussionSolution[];
+}
+
+// ========================================
+// Helper Functions
+// ========================================
/**
- * LiteTaskDetailPage component - Display single lite task session with flowchart
+ * Get i18n text (handles both string and {en, zh} object)
+ */
+function getI18nText(text: string | { en?: string; zh?: string } | undefined, locale: string = 'zh'): string {
+ if (!text) return '';
+ if (typeof text === 'string') return text;
+ return text[locale as keyof typeof text] || text.en || text.zh || '';
+}
+
+/**
+ * Get task status badge configuration
+ */
+function getTaskStatusBadge(
+ status: LiteTask['status'],
+ formatMessage: (key: { id: string }) => string
+) {
+ switch (status) {
+ case 'completed':
+ return { variant: 'success' as const, label: formatMessage({ id: 'sessionDetail.status.completed' }), icon: CheckCircle };
+ case 'in_progress':
+ return { variant: 'warning' as const, label: formatMessage({ id: 'sessionDetail.status.inProgress' }), icon: Loader2 };
+ case 'blocked':
+ return { variant: 'destructive' as const, label: formatMessage({ id: 'sessionDetail.status.blocked' }), icon: XCircle };
+ case 'failed':
+ return { variant: 'destructive' as const, label: formatMessage({ id: 'fixSession.status.failed' }), icon: XCircle };
+ default:
+ return { variant: 'secondary' as const, label: formatMessage({ id: 'sessionDetail.status.pending' }), icon: Clock };
+ }
+}
+
+// ========================================
+// Main Component
+// ========================================
+
+/**
+ * LiteTaskDetailPage component - Display single lite task session with multi-tab view
+ * Supports:
+ * - Lite-Plan/Lite-Fix: Tasks, Plan, Diagnoses, Context, Summary tabs
+ * - Multi-CLI: Tasks, Discussion, Context, Summary tabs
+ * - Context Package parsing with collapsible sections
+ * - Exploration packages with multiple analysis angles
+ * - Flowchart visualization for implementation steps
*/
export function LiteTaskDetailPage() {
const { sessionId } = useParams<{ sessionId: string }>();
const navigate = useNavigate();
- const { formatMessage } = useIntl();
+ const { formatMessage, locale } = useIntl();
- // Determine type from URL or state
- const [sessionType, setSessionType] = React.useState<'lite-plan' | 'lite-fix' | 'multi-cli-plan'>('lite-plan');
+ // Session type state
+ const [sessionType, setSessionType] = React.useState('lite-plan');
+
+ // Fetch session data
const { session, isLoading, error, refetch } = useLiteTaskSession(sessionId, sessionType);
- // Track expanded tasks
- const [expandedTasks, setExpandedTasks] = React.useState>(new Set());
+ // Tab states
+ const [litePlanActiveTab, setLitePlanActiveTab] = React.useState('tasks');
+ const [multiCliActiveTab, setMultiCliActiveTab] = React.useState('tasks');
+ const [activeTaskTabs, setActiveTaskTabs] = React.useState>({});
- // Try to detect type from session data
+ // Detect session type from data
React.useEffect(() => {
if (session?.type) {
setSessionType(session.type);
@@ -53,32 +245,8 @@ export function LiteTaskDetailPage() {
navigate('/lite-tasks');
};
- const toggleTaskExpanded = (taskId: string) => {
- setExpandedTasks(prev => {
- const next = new Set(prev);
- if (next.has(taskId)) {
- next.delete(taskId);
- } else {
- next.add(taskId);
- }
- return next;
- });
- };
-
- // Get task status badge
- const getTaskStatusBadge = (task: LiteTask) => {
- switch (task.status) {
- case 'completed':
- return { variant: 'success' as const, label: formatMessage({ id: 'sessionDetail.status.completed' }), icon: CheckCircle };
- case 'in_progress':
- return { variant: 'warning' as const, label: formatMessage({ id: 'sessionDetail.status.inProgress' }), icon: Loader2 };
- case 'blocked':
- return { variant: 'destructive' as const, label: formatMessage({ id: 'sessionDetail.status.blocked' }), icon: XCircle };
- case 'failed':
- return { variant: 'destructive' as const, label: formatMessage({ id: 'fixSession.status.failed' }), icon: XCircle };
- default:
- return { variant: 'secondary' as const, label: formatMessage({ id: 'sessionDetail.status.pending' }), icon: Clock };
- }
+ const handleTaskTabChange = (taskId: string, tab: TaskTabValue) => {
+ setActiveTaskTabs(prev => ({ ...prev, [taskId]: tab }));
};
// Loading state
@@ -113,7 +281,7 @@ export function LiteTaskDetailPage() {
);
}
- // Session not found
+ // Not found state
if (!session) {
return (
@@ -132,9 +300,9 @@ export function LiteTaskDetailPage() {
);
}
- const tasks = session.tasks || [];
- const completedTasks = tasks.filter(t => t.status === 'completed').length;
const isLitePlan = session.type === 'lite-plan';
+ const isLiteFix = session.type === 'lite-fix';
+ const isMultiCli = session.type === 'multi-cli-plan';
return (
@@ -154,162 +322,280 @@ export function LiteTaskDetailPage() {
)}
-
- {isLitePlan ? : }
- {formatMessage({ id: isLitePlan ? 'liteTasks.type.plan' : 'liteTasks.type.fix' })}
+
+ {isLitePlan ? : isLiteFix ? : }
+ {formatMessage({ id: isLitePlan ? 'liteTasks.type.plan' : isLiteFix ? 'liteTasks.type.fix' : 'liteTasks.type.multiCli' })}
- {/* Info Bar */}
-
-
-
- {formatMessage({ id: 'sessionDetail.info.created' })}:{' '}
- {session.createdAt ? new Date(session.createdAt).toLocaleString() : 'N/A'}
-
-
-
- {formatMessage({ id: 'sessionDetail.info.tasks' })}:{' '}
- {completedTasks}/{tasks.length}
-
-
-
- {/* Description (if exists) */}
- {session.description && (
-
-
- {formatMessage({ id: 'sessionDetail.info.description' })}
-
-
{session.description}
-
+ {/* Session Type-Specific Tabs */}
+ {isMultiCli ? (
+ setMultiCliActiveTab(v as MultiCliTab)}>
+
+
+
+ {formatMessage({ id: 'liteTasksDetail.tabs.tasks' })}
+
+
+
+ {formatMessage({ id: 'liteTasksDetail.tabs.discussion' })}
+
+
+
+ {formatMessage({ id: 'liteTasksDetail.tabs.context' })}
+
+
+
+ {formatMessage({ id: 'liteTasksDetail.tabs.summary' })}
+
+
+
+ ) : (
+ setLitePlanActiveTab(v as LitePlanTab)}>
+
+
+
+ {formatMessage({ id: 'liteTasksDetail.tabs.tasks' })}
+
+
+
+ {formatMessage({ id: 'liteTasksDetail.tabs.plan' })}
+
+ {isLiteFix && (
+
+
+ {formatMessage({ id: 'liteTasksDetail.tabs.diagnoses' })}
+
+ )}
+
+
+ {formatMessage({ id: 'liteTasksDetail.tabs.context' })}
+
+
+
+ {formatMessage({ id: 'liteTasksDetail.tabs.summary' })}
+
+
+
)}
- {/* Tasks List */}
- {tasks.length === 0 ? (
-
-
-
-
- {formatMessage({ id: 'liteTasksDetail.empty.title' })}
-
-
- {formatMessage({ id: 'liteTasksDetail.empty.message' })}
-
-
-
- ) : (
-
- {tasks.map((task, index) => {
- const taskId = task.task_id || task.id || `T${index + 1}`;
- const isExpanded = expandedTasks.has(taskId);
- const statusBadge = getTaskStatusBadge(task);
- const StatusIcon = statusBadge.icon;
- const hasFlowchart = task.flow_control?.implementation_approach &&
- task.flow_control.implementation_approach.length > 0;
+ {/* Task List with Multi-Tab Content */}
+
+ {session.tasks?.map((task, index) => {
+ const taskId = task.task_id || task.id || `T${index + 1}`;
+ const activeTaskTab = activeTaskTabs[taskId] || 'task';
+ const hasFlowchart = task.flow_control?.implementation_approach && task.flow_control.implementation_approach.length > 0;
- return (
-
-
- {/* Task Header */}
- toggleTaskExpanded(taskId)}
- >
-
-
- {taskId}
-
-
- {statusBadge.label}
-
- {task.priority && (
-
- {task.priority}
-
- )}
- {hasFlowchart && (
-
-
- {formatMessage({ id: 'liteTasksDetail.flowchart' })}
-
- )}
-
-
- {task.title || formatMessage({ id: 'sessionDetail.tasks.untitled' })}
-
- {task.description && (
-
- {task.description}
-
+ return (
+
+ {/* Task Header */}
+
+
+
+
+ {taskId}
+
+ {task.status}
+
+ {task.priority && (
+ {task.priority}
)}
- {task.context?.depends_on && task.context.depends_on.length > 0 && (
-
+ {hasFlowchart && (
+
- Depends on: {task.context.depends_on.join(', ')}
-
+ Flowchart
+
)}
-
-
+
+
{task.title || 'Untitled Task'}
+ {task.description && (
+
{task.description}
+ )}
+
+
- {/* Expanded Content */}
- {isExpanded && (
-
- {/* Flowchart */}
- {hasFlowchart && task.flow_control && (
-
-
-
- {formatMessage({ id: 'liteTasksDetail.implementationFlow' })}
-
-
-
- )}
+ {/* Multi-Tab Content */}
+
handleTaskTabChange(taskId, v as TaskTabValue)}
+ className="w-full"
+ >
+
+
+
+ Task
+
+
+
+ Context
+
+
- {/* Focus Paths */}
- {task.context?.focus_paths && task.context.focus_paths.length > 0 && (
-
-
- {formatMessage({ id: 'liteTasksDetail.focusPaths' })}
-
-
- {task.context.focus_paths.map((path, idx) => (
-
- {path}
-
- ))}
-
-
- )}
-
- {/* Acceptance Criteria */}
- {task.context?.acceptance && task.context.acceptance.length > 0 && (
-
-
- {formatMessage({ id: 'liteTasksDetail.acceptanceCriteria' })}
-
-
- {task.context.acceptance.map((criteria, idx) => (
- -
- {idx + 1}.
- {criteria}
-
- ))}
-
-
- )}
+ {/* Task Tab - Implementation Details */}
+
+ {/* Flowchart */}
+ {hasFlowchart && task.flow_control && (
+
+
+
+ Implementation Flow
+
+
)}
-
-
- );
- })}
-
+
+ {/* Target Files */}
+ {task.flow_control?.target_files && task.flow_control.target_files.length > 0 && (
+
+
+
+ Target Files
+
+
+ {task.flow_control.target_files.map((file, idx) => {
+ const displayPath = typeof file === 'string' ? file : (file.path || file.name || 'Unknown');
+ return (
+
+ {displayPath}
+
+ );
+ })}
+
+
+ )}
+
+ {/* Dependencies */}
+ {task.context?.depends_on && task.context.depends_on.length > 0 && (
+
+
Dependencies
+
+ {task.context.depends_on.map((dep, idx) => (
+ {dep}
+ ))}
+
+
+ )}
+
+
+ {/* Context Tab - Planning Context */}
+
+ {/* Focus Paths */}
+ {task.context?.focus_paths && task.context.focus_paths.length > 0 && (
+
+
+
+ Focus Paths
+
+
+ {task.context.focus_paths.map((path, idx) => (
+ {path}
+ ))}
+
+
+ )}
+
+ {/* Acceptance Criteria */}
+ {task.context?.acceptance && task.context.acceptance.length > 0 && (
+
+
+
+ Acceptance Criteria
+
+
+ {task.context.acceptance.map((criteria, idx) => (
+ -
+ {idx + 1}.
+ {criteria}
+
+ ))}
+
+
+ )}
+
+ {/* Tech Stack from Session Metadata */}
+ {session.metadata?.tech_stack && (
+
+
+
+ Tech Stack
+
+
+ {(session.metadata.tech_stack as string[]).map((tech, idx) => (
+ {tech}
+ ))}
+
+
+ )}
+
+ {/* Conventions from Session Metadata */}
+ {session.metadata?.conventions && (
+
+
+
+ Conventions
+
+
+ {(session.metadata.conventions as string[]).map((conv, idx) => (
+ -
+ •
+ {conv}
+
+ ))}
+
+
+ )}
+
+
+
+ );
+ })}
+
+
+ {/* Session-Level Explorations (if available) */}
+ {session.metadata?.explorations && (
+
+
+
+
+ Explorations
+ {(session.metadata.explorations as Exploration[]).length}
+
+
+
+
+ {(session.metadata.explorations as Exploration[]).map((exp, idx) => (
+
+
+
+
+ {exp.name}
+
+ {exp.content && (
+
+ Has Content
+
+ )}
+
+
+
+ {exp.content ? (
+
+ {exp.content}
+
+ ) : (
+
+ No content available for this exploration.
+
+ )}
+
+
+ ))}
+
+
+
)}
);
diff --git a/ccw/frontend/src/pages/index.ts b/ccw/frontend/src/pages/index.ts
index b61acaa1..13b40782 100644
--- a/ccw/frontend/src/pages/index.ts
+++ b/ccw/frontend/src/pages/index.ts
@@ -11,6 +11,7 @@ export { SessionDetailPage } from './SessionDetailPage';
export { HistoryPage } from './HistoryPage';
export { OrchestratorPage } from './orchestrator';
export { LoopMonitorPage } from './LoopMonitorPage';
+export { IssueHubPage } from './IssueHubPage';
export { IssueManagerPage } from './IssueManagerPage';
export { QueuePage } from './QueuePage';
export { DiscoveryPage } from './DiscoveryPage';
diff --git a/ccw/frontend/src/router.tsx b/ccw/frontend/src/router.tsx
index f3fa5ce6..835eb974 100644
--- a/ccw/frontend/src/router.tsx
+++ b/ccw/frontend/src/router.tsx
@@ -3,7 +3,7 @@
// ========================================
// React Router v6 configuration with all dashboard routes
-import { createBrowserRouter, RouteObject } from 'react-router-dom';
+import { createBrowserRouter, RouteObject, Navigate } from 'react-router-dom';
import { AppShell } from '@/components/layout';
import {
HomePage,
@@ -14,6 +14,7 @@ import {
HistoryPage,
OrchestratorPage,
LoopMonitorPage,
+ IssueHubPage,
IssueManagerPage,
QueuePage,
DiscoveryPage,
@@ -93,15 +94,16 @@ const routes: RouteObject[] = [
},
{
path: 'issues',
- element:
,
+ element:
,
},
+ // Legacy routes - redirect to hub with tab parameter
{
path: 'issues/queue',
- element:
,
+ element:
,
},
{
path: 'issues/discovery',
- element:
,
+ element:
,
},
{
path: 'skills',
@@ -191,8 +193,9 @@ export const ROUTES = {
EXECUTIONS: '/executions',
LOOPS: '/loops',
ISSUES: '/issues',
- ISSUE_QUEUE: '/issues/queue',
- ISSUE_DISCOVERY: '/issues/discovery',
+ // Legacy issue routes - use ISSUES with ?tab parameter instead
+ ISSUE_QUEUE: '/issues?tab=queue',
+ ISSUE_DISCOVERY: '/issues?tab=discovery',
SKILLS: '/skills',
COMMANDS: '/commands',
MEMORY: '/memory',