mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
feat: add quick install templates and index status to CLI hooks and home locales
feat: enhance MCP manager with interactive question feature and update locales feat: implement tags and available models management in settings page fix: improve process termination logic in stop command for React frontend fix: update view command to default to 'js' frontend feat: add Recommended MCP Wizard component for dynamic server configuration
This commit is contained in:
@@ -1,46 +1,38 @@
|
||||
export default {
|
||||
"__comp---theme-debug-config-23-a-2ff": [() => import(/* webpackChunkName: "__comp---theme-debug-config-23-a-2ff" */ "@theme/DebugConfig"), "@theme/DebugConfig", require.resolveWeak("@theme/DebugConfig")],
|
||||
"__comp---theme-debug-contentba-8-ce7": [() => import(/* webpackChunkName: "__comp---theme-debug-contentba-8-ce7" */ "@theme/DebugContent"), "@theme/DebugContent", require.resolveWeak("@theme/DebugContent")],
|
||||
"__comp---theme-debug-global-dataede-0fa": [() => import(/* webpackChunkName: "__comp---theme-debug-global-dataede-0fa" */ "@theme/DebugGlobalData"), "@theme/DebugGlobalData", require.resolveWeak("@theme/DebugGlobalData")],
|
||||
"__comp---theme-debug-registry-679-501": [() => import(/* webpackChunkName: "__comp---theme-debug-registry-679-501" */ "@theme/DebugRegistry"), "@theme/DebugRegistry", require.resolveWeak("@theme/DebugRegistry")],
|
||||
"__comp---theme-debug-routes-946-699": [() => import(/* webpackChunkName: "__comp---theme-debug-routes-946-699" */ "@theme/DebugRoutes"), "@theme/DebugRoutes", require.resolveWeak("@theme/DebugRoutes")],
|
||||
"__comp---theme-debug-site-metadata-68-e-3d4": [() => import(/* webpackChunkName: "__comp---theme-debug-site-metadata-68-e-3d4" */ "@theme/DebugSiteMetadata"), "@theme/DebugSiteMetadata", require.resolveWeak("@theme/DebugSiteMetadata")],
|
||||
"__comp---theme-doc-item-178-a40": [() => import(/* webpackChunkName: "__comp---theme-doc-item-178-a40" */ "@theme/DocItem"), "@theme/DocItem", require.resolveWeak("@theme/DocItem")],
|
||||
"__comp---theme-doc-roota-94-67a": [() => import(/* webpackChunkName: "__comp---theme-doc-roota-94-67a" */ "@theme/DocRoot"), "@theme/DocRoot", require.resolveWeak("@theme/DocRoot")],
|
||||
"__comp---theme-doc-version-roota-7-b-5de": [() => import(/* webpackChunkName: "__comp---theme-doc-version-roota-7-b-5de" */ "@theme/DocVersionRoot"), "@theme/DocVersionRoot", require.resolveWeak("@theme/DocVersionRoot")],
|
||||
"__comp---theme-docs-root-5-e-9-0b6": [() => import(/* webpackChunkName: "__comp---theme-docs-root-5-e-9-0b6" */ "@theme/DocsRoot"), "@theme/DocsRoot", require.resolveWeak("@theme/DocsRoot")],
|
||||
"__props---docs-docsa-20-f19": [() => import(/* webpackChunkName: "__props---docs-docsa-20-f19" */ "@generated/docusaurus-plugin-content-docs/default/p/docs-docs-fbb.json"), "@generated/docusaurus-plugin-content-docs/default/p/docs-docs-fbb.json", require.resolveWeak("@generated/docusaurus-plugin-content-docs/default/p/docs-docs-fbb.json")],
|
||||
"__props---docs-docusaurus-debug-content-344-8d5": [() => import(/* webpackChunkName: "__props---docs-docusaurus-debug-content-344-8d5" */ "@generated/docusaurus-plugin-debug/default/p/docs-docusaurus-debug-content-a52.json"), "@generated/docusaurus-plugin-debug/default/p/docs-docusaurus-debug-content-a52.json", require.resolveWeak("@generated/docusaurus-plugin-debug/default/p/docs-docusaurus-debug-content-a52.json")],
|
||||
"content---docs-docs-commands-cli-cli-init-056-2d3": [() => import(/* webpackChunkName: "content---docs-docs-commands-cli-cli-init-056-2d3" */ "@site/docs/commands/cli/cli-init.mdx"), "@site/docs/commands/cli/cli-init.mdx", require.resolveWeak("@site/docs/commands/cli/cli-init.mdx")],
|
||||
"content---docs-docs-commands-cli-codex-reviewf-1-b-532": [() => import(/* webpackChunkName: "content---docs-docs-commands-cli-codex-reviewf-1-b-532" */ "@site/docs/commands/cli/codex-review.mdx"), "@site/docs/commands/cli/codex-review.mdx", require.resolveWeak("@site/docs/commands/cli/codex-review.mdx")],
|
||||
"content---docs-docs-commands-general-ccw-coordinatord-55-a04": [() => import(/* webpackChunkName: "content---docs-docs-commands-general-ccw-coordinatord-55-a04" */ "@site/docs/commands/general/ccw-coordinator.mdx"), "@site/docs/commands/general/ccw-coordinator.mdx", require.resolveWeak("@site/docs/commands/general/ccw-coordinator.mdx")],
|
||||
"content---docs-docs-commands-general-ccw-debug-97-c-2c8": [() => import(/* webpackChunkName: "content---docs-docs-commands-general-ccw-debug-97-c-2c8" */ "@site/docs/commands/general/ccw-debug.mdx"), "@site/docs/commands/general/ccw-debug.mdx", require.resolveWeak("@site/docs/commands/general/ccw-debug.mdx")],
|
||||
"content---docs-docs-commands-general-ccw-plan-04-d-aa3": [() => import(/* webpackChunkName: "content---docs-docs-commands-general-ccw-plan-04-d-aa3" */ "@site/docs/commands/general/ccw-plan.mdx"), "@site/docs/commands/general/ccw-plan.mdx", require.resolveWeak("@site/docs/commands/general/ccw-plan.mdx")],
|
||||
"content---docs-docs-commands-general-ccw-testcce-a5d": [() => import(/* webpackChunkName: "content---docs-docs-commands-general-ccw-testcce-a5d" */ "@site/docs/commands/general/ccw-test.mdx"), "@site/docs/commands/general/ccw-test.mdx", require.resolveWeak("@site/docs/commands/general/ccw-test.mdx")],
|
||||
"content---docs-docs-commands-general-ccwf-48-4f9": [() => import(/* webpackChunkName: "content---docs-docs-commands-general-ccwf-48-4f9" */ "@site/docs/commands/general/ccw.mdx"), "@site/docs/commands/general/ccw.mdx", require.resolveWeak("@site/docs/commands/general/ccw.mdx")],
|
||||
"content---docs-docs-commands-general-codex-coordinatorf-92-965": [() => import(/* webpackChunkName: "content---docs-docs-commands-general-codex-coordinatorf-92-965" */ "@site/docs/commands/general/codex-coordinator.mdx"), "@site/docs/commands/general/codex-coordinator.mdx", require.resolveWeak("@site/docs/commands/general/codex-coordinator.mdx")],
|
||||
"content---docs-docs-commands-general-flow-createfab-5a7": [() => import(/* webpackChunkName: "content---docs-docs-commands-general-flow-createfab-5a7" */ "@site/docs/commands/general/flow-create.mdx"), "@site/docs/commands/general/flow-create.mdx", require.resolveWeak("@site/docs/commands/general/flow-create.mdx")],
|
||||
"content---docs-docs-commands-issue-issue-convert-to-plan-5-c-7-428": [() => import(/* webpackChunkName: "content---docs-docs-commands-issue-issue-convert-to-plan-5-c-7-428" */ "@site/docs/commands/issue/issue-convert-to-plan.md"), "@site/docs/commands/issue/issue-convert-to-plan.md", require.resolveWeak("@site/docs/commands/issue/issue-convert-to-plan.md")],
|
||||
"content---docs-docs-commands-issue-issue-discover-1-e-3-61d": [() => import(/* webpackChunkName: "content---docs-docs-commands-issue-issue-discover-1-e-3-61d" */ "@site/docs/commands/issue/issue-discover.md"), "@site/docs/commands/issue/issue-discover.md", require.resolveWeak("@site/docs/commands/issue/issue-discover.md")],
|
||||
"content---docs-docs-commands-issue-issue-executefe-8-121": [() => import(/* webpackChunkName: "content---docs-docs-commands-issue-issue-executefe-8-121" */ "@site/docs/commands/issue/issue-execute.md"), "@site/docs/commands/issue/issue-execute.md", require.resolveWeak("@site/docs/commands/issue/issue-execute.md")],
|
||||
"content---docs-docs-commands-issue-issue-from-brainstorm-2-ec-ca5": [() => import(/* webpackChunkName: "content---docs-docs-commands-issue-issue-from-brainstorm-2-ec-ca5" */ "@site/docs/commands/issue/issue-from-brainstorm.md"), "@site/docs/commands/issue/issue-from-brainstorm.md", require.resolveWeak("@site/docs/commands/issue/issue-from-brainstorm.md")],
|
||||
"content---docs-docs-commands-issue-issue-new-4-ad-afb": [() => import(/* webpackChunkName: "content---docs-docs-commands-issue-issue-new-4-ad-afb" */ "@site/docs/commands/issue/issue-new.md"), "@site/docs/commands/issue/issue-new.md", require.resolveWeak("@site/docs/commands/issue/issue-new.md")],
|
||||
"content---docs-docs-commands-issue-issue-plana-6-c-1b0": [() => import(/* webpackChunkName: "content---docs-docs-commands-issue-issue-plana-6-c-1b0" */ "@site/docs/commands/issue/issue-plan.md"), "@site/docs/commands/issue/issue-plan.md", require.resolveWeak("@site/docs/commands/issue/issue-plan.md")],
|
||||
"content---docs-docs-commands-issue-issue-queue-1-ba-06f": [() => import(/* webpackChunkName: "content---docs-docs-commands-issue-issue-queue-1-ba-06f" */ "@site/docs/commands/issue/issue-queue.md"), "@site/docs/commands/issue/issue-queue.md", require.resolveWeak("@site/docs/commands/issue/issue-queue.md")],
|
||||
"content---docs-docs-commands-memory-memory-compact-7-a-1-f85": [() => import(/* webpackChunkName: "content---docs-docs-commands-memory-memory-compact-7-a-1-f85" */ "@site/docs/commands/memory/memory-compact.mdx"), "@site/docs/commands/memory/memory-compact.mdx", require.resolveWeak("@site/docs/commands/memory/memory-compact.mdx")],
|
||||
"content---docs-docs-commands-memory-memory-docs-full-cli-4-cc-b15": [() => import(/* webpackChunkName: "content---docs-docs-commands-memory-memory-docs-full-cli-4-cc-b15" */ "@site/docs/commands/memory/memory-docs-full-cli.mdx"), "@site/docs/commands/memory/memory-docs-full-cli.mdx", require.resolveWeak("@site/docs/commands/memory/memory-docs-full-cli.mdx")],
|
||||
"content---docs-docs-commands-memory-memory-docs-related-cli-60-e-28a": [() => import(/* webpackChunkName: "content---docs-docs-commands-memory-memory-docs-related-cli-60-e-28a" */ "@site/docs/commands/memory/memory-docs-related-cli.mdx"), "@site/docs/commands/memory/memory-docs-related-cli.mdx", require.resolveWeak("@site/docs/commands/memory/memory-docs-related-cli.mdx")],
|
||||
"content---docs-docs-commands-memory-memory-load-157-920": [() => import(/* webpackChunkName: "content---docs-docs-commands-memory-memory-load-157-920" */ "@site/docs/commands/memory/memory-load.mdx"), "@site/docs/commands/memory/memory-load.mdx", require.resolveWeak("@site/docs/commands/memory/memory-load.mdx")],
|
||||
"content---docs-docs-commands-memory-memory-update-full-666-28e": [() => import(/* webpackChunkName: "content---docs-docs-commands-memory-memory-update-full-666-28e" */ "@site/docs/commands/memory/memory-update-full.mdx"), "@site/docs/commands/memory/memory-update-full.mdx", require.resolveWeak("@site/docs/commands/memory/memory-update-full.mdx")],
|
||||
"content---docs-docs-commands-memory-memory-update-related-611-f0a": [() => import(/* webpackChunkName: "content---docs-docs-commands-memory-memory-update-related-611-f0a" */ "@site/docs/commands/memory/memory-update-related.mdx"), "@site/docs/commands/memory/memory-update-related.mdx", require.resolveWeak("@site/docs/commands/memory/memory-update-related.mdx")],
|
||||
"content---docs-docs-faqea-3-29f": [() => import(/* webpackChunkName: "content---docs-docs-faqea-3-29f" */ "@site/docs/faq.mdx"), "@site/docs/faq.mdx", require.resolveWeak("@site/docs/faq.mdx")],
|
||||
"content---docs-docs-overview-188-8fe": [() => import(/* webpackChunkName: "content---docs-docs-overview-188-8fe" */ "@site/docs/overview.mdx"), "@site/docs/overview.mdx", require.resolveWeak("@site/docs/overview.mdx")],
|
||||
"content---docs-docs-workflows-faqbcf-a47": [() => import(/* webpackChunkName: "content---docs-docs-workflows-faqbcf-a47" */ "@site/docs/workflows/faq.mdx"), "@site/docs/workflows/faq.mdx", require.resolveWeak("@site/docs/workflows/faq.mdx")],
|
||||
"content---docs-docs-workflows-introduction-9-f-4-dba": [() => import(/* webpackChunkName: "content---docs-docs-workflows-introduction-9-f-4-dba" */ "@site/docs/workflows/introduction.mdx"), "@site/docs/workflows/introduction.mdx", require.resolveWeak("@site/docs/workflows/introduction.mdx")],
|
||||
"content---docs-docs-workflows-level-1-ultra-lightweightc-5-a-692": [() => import(/* webpackChunkName: "content---docs-docs-workflows-level-1-ultra-lightweightc-5-a-692" */ "@site/docs/workflows/level-1-ultra-lightweight.mdx"), "@site/docs/workflows/level-1-ultra-lightweight.mdx", require.resolveWeak("@site/docs/workflows/level-1-ultra-lightweight.mdx")],
|
||||
"content---docs-docs-workflows-level-2-rapid-19-b-a2f": [() => import(/* webpackChunkName: "content---docs-docs-workflows-level-2-rapid-19-b-a2f" */ "@site/docs/workflows/level-2-rapid.mdx"), "@site/docs/workflows/level-2-rapid.mdx", require.resolveWeak("@site/docs/workflows/level-2-rapid.mdx")],
|
||||
"content---docs-docs-workflows-level-3-standardbdb-19a": [() => import(/* webpackChunkName: "content---docs-docs-workflows-level-3-standardbdb-19a" */ "@site/docs/workflows/level-3-standard.mdx"), "@site/docs/workflows/level-3-standard.mdx", require.resolveWeak("@site/docs/workflows/level-3-standard.mdx")],
|
||||
"content---docs-docs-workflows-level-4-brainstormd-04-69a": [() => import(/* webpackChunkName: "content---docs-docs-workflows-level-4-brainstormd-04-69a" */ "@site/docs/workflows/level-4-brainstorm.mdx"), "@site/docs/workflows/level-4-brainstorm.mdx", require.resolveWeak("@site/docs/workflows/level-4-brainstorm.mdx")],
|
||||
"content---docs-docs-workflows-level-5-intelligent-186-435": [() => import(/* webpackChunkName: "content---docs-docs-workflows-level-5-intelligent-186-435" */ "@site/docs/workflows/level-5-intelligent.mdx"), "@site/docs/workflows/level-5-intelligent.mdx", require.resolveWeak("@site/docs/workflows/level-5-intelligent.mdx")],
|
||||
"plugin---docs-docsaba-31e": [() => import(/* webpackChunkName: "plugin---docs-docsaba-31e" */ "@generated/docusaurus-plugin-content-docs/default/__plugin.json"), "@generated/docusaurus-plugin-content-docs/default/__plugin.json", require.resolveWeak("@generated/docusaurus-plugin-content-docs/default/__plugin.json")],
|
||||
"plugin---docs-docusaurus-debugb-38-c84": [() => import(/* webpackChunkName: "plugin---docs-docusaurus-debugb-38-c84" */ "@generated/docusaurus-plugin-debug/default/__plugin.json"), "@generated/docusaurus-plugin-debug/default/__plugin.json", require.resolveWeak("@generated/docusaurus-plugin-debug/default/__plugin.json")],};
|
||||
"04db0a2e": [() => import(/* webpackChunkName: "04db0a2e" */ "@site/docs/commands/general/ccw-plan.mdx"), "@site/docs/commands/general/ccw-plan.mdx", require.resolveWeak("@site/docs/commands/general/ccw-plan.mdx")],
|
||||
"0566a0a8": [() => import(/* webpackChunkName: "0566a0a8" */ "@site/docs/commands/cli/cli-init.mdx"), "@site/docs/commands/cli/cli-init.mdx", require.resolveWeak("@site/docs/commands/cli/cli-init.mdx")],
|
||||
"157db180": [() => import(/* webpackChunkName: "157db180" */ "@site/docs/commands/memory/memory-load.mdx"), "@site/docs/commands/memory/memory-load.mdx", require.resolveWeak("@site/docs/commands/memory/memory-load.mdx")],
|
||||
"17896441": [() => import(/* webpackChunkName: "17896441" */ "@theme/DocItem"), "@theme/DocItem", require.resolveWeak("@theme/DocItem")],
|
||||
"186dcf4e": [() => import(/* webpackChunkName: "186dcf4e" */ "@site/docs/workflows/level-5-intelligent.mdx"), "@site/docs/workflows/level-5-intelligent.mdx", require.resolveWeak("@site/docs/workflows/level-5-intelligent.mdx")],
|
||||
"18891827": [() => import(/* webpackChunkName: "18891827" */ "@site/docs/overview.mdx"), "@site/docs/overview.mdx", require.resolveWeak("@site/docs/overview.mdx")],
|
||||
"19b64556": [() => import(/* webpackChunkName: "19b64556" */ "@site/docs/workflows/level-2-rapid.mdx"), "@site/docs/workflows/level-2-rapid.mdx", require.resolveWeak("@site/docs/workflows/level-2-rapid.mdx")],
|
||||
"1bac9067": [() => import(/* webpackChunkName: "1bac9067" */ "@site/docs/commands/issue/issue-queue.md"), "@site/docs/commands/issue/issue-queue.md", require.resolveWeak("@site/docs/commands/issue/issue-queue.md")],
|
||||
"1e3006f3": [() => import(/* webpackChunkName: "1e3006f3" */ "@site/docs/commands/issue/issue-discover.md"), "@site/docs/commands/issue/issue-discover.md", require.resolveWeak("@site/docs/commands/issue/issue-discover.md")],
|
||||
"2ecf8b4a": [() => import(/* webpackChunkName: "2ecf8b4a" */ "@site/docs/commands/issue/issue-from-brainstorm.md"), "@site/docs/commands/issue/issue-from-brainstorm.md", require.resolveWeak("@site/docs/commands/issue/issue-from-brainstorm.md")],
|
||||
"4ad7db0f": [() => import(/* webpackChunkName: "4ad7db0f" */ "@site/docs/commands/issue/issue-new.md"), "@site/docs/commands/issue/issue-new.md", require.resolveWeak("@site/docs/commands/issue/issue-new.md")],
|
||||
"4cc74730": [() => import(/* webpackChunkName: "4cc74730" */ "@site/docs/commands/memory/memory-docs-full-cli.mdx"), "@site/docs/commands/memory/memory-docs-full-cli.mdx", require.resolveWeak("@site/docs/commands/memory/memory-docs-full-cli.mdx")],
|
||||
"5c7b2278": [() => import(/* webpackChunkName: "5c7b2278" */ "@site/docs/commands/issue/issue-convert-to-plan.md"), "@site/docs/commands/issue/issue-convert-to-plan.md", require.resolveWeak("@site/docs/commands/issue/issue-convert-to-plan.md")],
|
||||
"5e95c892": [() => import(/* webpackChunkName: "5e95c892" */ "@theme/DocsRoot"), "@theme/DocsRoot", require.resolveWeak("@theme/DocsRoot")],
|
||||
"60eef997": [() => import(/* webpackChunkName: "60eef997" */ "@site/docs/commands/memory/memory-docs-related-cli.mdx"), "@site/docs/commands/memory/memory-docs-related-cli.mdx", require.resolveWeak("@site/docs/commands/memory/memory-docs-related-cli.mdx")],
|
||||
"611877e1": [() => import(/* webpackChunkName: "611877e1" */ "@site/docs/commands/memory/memory-update-related.mdx"), "@site/docs/commands/memory/memory-update-related.mdx", require.resolveWeak("@site/docs/commands/memory/memory-update-related.mdx")],
|
||||
"666bb1bf": [() => import(/* webpackChunkName: "666bb1bf" */ "@site/docs/commands/memory/memory-update-full.mdx"), "@site/docs/commands/memory/memory-update-full.mdx", require.resolveWeak("@site/docs/commands/memory/memory-update-full.mdx")],
|
||||
"7a1ee27c": [() => import(/* webpackChunkName: "7a1ee27c" */ "@site/docs/commands/memory/memory-compact.mdx"), "@site/docs/commands/memory/memory-compact.mdx", require.resolveWeak("@site/docs/commands/memory/memory-compact.mdx")],
|
||||
"97c6e66a": [() => import(/* webpackChunkName: "97c6e66a" */ "@site/docs/commands/general/ccw-debug.mdx"), "@site/docs/commands/general/ccw-debug.mdx", require.resolveWeak("@site/docs/commands/general/ccw-debug.mdx")],
|
||||
"9f4ca91e": [() => import(/* webpackChunkName: "9f4ca91e" */ "@site/docs/workflows/introduction.mdx"), "@site/docs/workflows/introduction.mdx", require.resolveWeak("@site/docs/workflows/introduction.mdx")],
|
||||
"a2065270": [() => import(/* webpackChunkName: "a2065270" */ "@generated/docusaurus-plugin-content-docs/default/p/docs-docs-fbb.json"), "@generated/docusaurus-plugin-content-docs/default/p/docs-docs-fbb.json", require.resolveWeak("@generated/docusaurus-plugin-content-docs/default/p/docs-docs-fbb.json")],
|
||||
"a6c3df16": [() => import(/* webpackChunkName: "a6c3df16" */ "@site/docs/commands/issue/issue-plan.md"), "@site/docs/commands/issue/issue-plan.md", require.resolveWeak("@site/docs/commands/issue/issue-plan.md")],
|
||||
"a7bd4aaa": [() => import(/* webpackChunkName: "a7bd4aaa" */ "@theme/DocVersionRoot"), "@theme/DocVersionRoot", require.resolveWeak("@theme/DocVersionRoot")],
|
||||
"a94703ab": [() => import(/* webpackChunkName: "a94703ab" */ "@theme/DocRoot"), "@theme/DocRoot", require.resolveWeak("@theme/DocRoot")],
|
||||
"aba21aa0": [() => import(/* webpackChunkName: "aba21aa0" */ "@generated/docusaurus-plugin-content-docs/default/__plugin.json"), "@generated/docusaurus-plugin-content-docs/default/__plugin.json", require.resolveWeak("@generated/docusaurus-plugin-content-docs/default/__plugin.json")],
|
||||
"bcf6b37c": [() => import(/* webpackChunkName: "bcf6b37c" */ "@site/docs/workflows/faq.mdx"), "@site/docs/workflows/faq.mdx", require.resolveWeak("@site/docs/workflows/faq.mdx")],
|
||||
"bdb2b105": [() => import(/* webpackChunkName: "bdb2b105" */ "@site/docs/workflows/level-3-standard.mdx"), "@site/docs/workflows/level-3-standard.mdx", require.resolveWeak("@site/docs/workflows/level-3-standard.mdx")],
|
||||
"c5a82d8d": [() => import(/* webpackChunkName: "c5a82d8d" */ "@site/docs/workflows/level-1-ultra-lightweight.mdx"), "@site/docs/workflows/level-1-ultra-lightweight.mdx", require.resolveWeak("@site/docs/workflows/level-1-ultra-lightweight.mdx")],
|
||||
"ccef5d0f": [() => import(/* webpackChunkName: "ccef5d0f" */ "@site/docs/commands/general/ccw-test.mdx"), "@site/docs/commands/general/ccw-test.mdx", require.resolveWeak("@site/docs/commands/general/ccw-test.mdx")],
|
||||
"d045285b": [() => import(/* webpackChunkName: "d045285b" */ "@site/docs/workflows/level-4-brainstorm.mdx"), "@site/docs/workflows/level-4-brainstorm.mdx", require.resolveWeak("@site/docs/workflows/level-4-brainstorm.mdx")],
|
||||
"d550a629": [() => import(/* webpackChunkName: "d550a629" */ "@site/docs/commands/general/ccw-coordinator.mdx"), "@site/docs/commands/general/ccw-coordinator.mdx", require.resolveWeak("@site/docs/commands/general/ccw-coordinator.mdx")],
|
||||
"ea313555": [() => import(/* webpackChunkName: "ea313555" */ "@site/docs/faq.mdx"), "@site/docs/faq.mdx", require.resolveWeak("@site/docs/faq.mdx")],
|
||||
"f1bf82ec": [() => import(/* webpackChunkName: "f1bf82ec" */ "@site/docs/commands/cli/codex-review.mdx"), "@site/docs/commands/cli/codex-review.mdx", require.resolveWeak("@site/docs/commands/cli/codex-review.mdx")],
|
||||
"f4817052": [() => import(/* webpackChunkName: "f4817052" */ "@site/docs/commands/general/ccw.mdx"), "@site/docs/commands/general/ccw.mdx", require.resolveWeak("@site/docs/commands/general/ccw.mdx")],
|
||||
"f9222419": [() => import(/* webpackChunkName: "f9222419" */ "@site/docs/commands/general/codex-coordinator.mdx"), "@site/docs/commands/general/codex-coordinator.mdx", require.resolveWeak("@site/docs/commands/general/codex-coordinator.mdx")],
|
||||
"fabaf1c8": [() => import(/* webpackChunkName: "fabaf1c8" */ "@site/docs/commands/general/flow-create.mdx"), "@site/docs/commands/general/flow-create.mdx", require.resolveWeak("@site/docs/commands/general/flow-create.mdx")],
|
||||
"fe8e3dcf": [() => import(/* webpackChunkName: "fe8e3dcf" */ "@site/docs/commands/issue/issue-execute.md"), "@site/docs/commands/issue/issue-execute.md", require.resolveWeak("@site/docs/commands/issue/issue-execute.md")],};
|
||||
|
||||
@@ -2,41 +2,6 @@ import React from 'react';
|
||||
import ComponentCreator from '@docusaurus/ComponentCreator';
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '/docs/__docusaurus/debug',
|
||||
component: ComponentCreator('/docs/__docusaurus/debug', 'e58'),
|
||||
exact: true
|
||||
},
|
||||
{
|
||||
path: '/docs/__docusaurus/debug/config',
|
||||
component: ComponentCreator('/docs/__docusaurus/debug/config', '2ce'),
|
||||
exact: true
|
||||
},
|
||||
{
|
||||
path: '/docs/__docusaurus/debug/content',
|
||||
component: ComponentCreator('/docs/__docusaurus/debug/content', '11b'),
|
||||
exact: true
|
||||
},
|
||||
{
|
||||
path: '/docs/__docusaurus/debug/globalData',
|
||||
component: ComponentCreator('/docs/__docusaurus/debug/globalData', 'f13'),
|
||||
exact: true
|
||||
},
|
||||
{
|
||||
path: '/docs/__docusaurus/debug/metadata',
|
||||
component: ComponentCreator('/docs/__docusaurus/debug/metadata', 'bff'),
|
||||
exact: true
|
||||
},
|
||||
{
|
||||
path: '/docs/__docusaurus/debug/registry',
|
||||
component: ComponentCreator('/docs/__docusaurus/debug/registry', '830'),
|
||||
exact: true
|
||||
},
|
||||
{
|
||||
path: '/docs/__docusaurus/debug/routes',
|
||||
component: ComponentCreator('/docs/__docusaurus/debug/routes', '13e'),
|
||||
exact: true
|
||||
},
|
||||
{
|
||||
path: '/docs/docs',
|
||||
component: ComponentCreator('/docs/docs', '942'),
|
||||
|
||||
@@ -1,182 +1,139 @@
|
||||
{
|
||||
"/docs/__docusaurus/debug-e58": {
|
||||
"__comp": "__comp---theme-debug-config-23-a-2ff",
|
||||
"__context": {
|
||||
"plugin": "plugin---docs-docusaurus-debugb-38-c84"
|
||||
}
|
||||
},
|
||||
"/docs/__docusaurus/debug/config-2ce": {
|
||||
"__comp": "__comp---theme-debug-config-23-a-2ff",
|
||||
"__context": {
|
||||
"plugin": "plugin---docs-docusaurus-debugb-38-c84"
|
||||
}
|
||||
},
|
||||
"/docs/__docusaurus/debug/content-11b": {
|
||||
"__comp": "__comp---theme-debug-contentba-8-ce7",
|
||||
"__context": {
|
||||
"plugin": "plugin---docs-docusaurus-debugb-38-c84"
|
||||
},
|
||||
"__props": "__props---docs-docusaurus-debug-content-344-8d5"
|
||||
},
|
||||
"/docs/__docusaurus/debug/globalData-f13": {
|
||||
"__comp": "__comp---theme-debug-global-dataede-0fa",
|
||||
"__context": {
|
||||
"plugin": "plugin---docs-docusaurus-debugb-38-c84"
|
||||
}
|
||||
},
|
||||
"/docs/__docusaurus/debug/metadata-bff": {
|
||||
"__comp": "__comp---theme-debug-site-metadata-68-e-3d4",
|
||||
"__context": {
|
||||
"plugin": "plugin---docs-docusaurus-debugb-38-c84"
|
||||
}
|
||||
},
|
||||
"/docs/__docusaurus/debug/registry-830": {
|
||||
"__comp": "__comp---theme-debug-registry-679-501",
|
||||
"__context": {
|
||||
"plugin": "plugin---docs-docusaurus-debugb-38-c84"
|
||||
}
|
||||
},
|
||||
"/docs/__docusaurus/debug/routes-13e": {
|
||||
"__comp": "__comp---theme-debug-routes-946-699",
|
||||
"__context": {
|
||||
"plugin": "plugin---docs-docusaurus-debugb-38-c84"
|
||||
}
|
||||
},
|
||||
"/docs/docs-942": {
|
||||
"__comp": "__comp---theme-docs-root-5-e-9-0b6",
|
||||
"__comp": "5e95c892",
|
||||
"__context": {
|
||||
"plugin": "plugin---docs-docsaba-31e"
|
||||
"plugin": "aba21aa0"
|
||||
}
|
||||
},
|
||||
"/docs/docs-a90": {
|
||||
"__comp": "__comp---theme-doc-version-roota-7-b-5de",
|
||||
"__props": "__props---docs-docsa-20-f19"
|
||||
"__comp": "a7bd4aaa",
|
||||
"__props": "a2065270"
|
||||
},
|
||||
"/docs/docs-c2e": {
|
||||
"__comp": "__comp---theme-doc-roota-94-67a"
|
||||
"__comp": "a94703ab"
|
||||
},
|
||||
"/docs/docs/commands/cli/cli-init-c74": {
|
||||
"__comp": "__comp---theme-doc-item-178-a40",
|
||||
"content": "content---docs-docs-commands-cli-cli-init-056-2d3"
|
||||
"__comp": "17896441",
|
||||
"content": "0566a0a8"
|
||||
},
|
||||
"/docs/docs/commands/cli/codex-review-937": {
|
||||
"__comp": "__comp---theme-doc-item-178-a40",
|
||||
"content": "content---docs-docs-commands-cli-codex-reviewf-1-b-532"
|
||||
"__comp": "17896441",
|
||||
"content": "f1bf82ec"
|
||||
},
|
||||
"/docs/docs/commands/general/ccw-3fb": {
|
||||
"__comp": "__comp---theme-doc-item-178-a40",
|
||||
"content": "content---docs-docs-commands-general-ccwf-48-4f9"
|
||||
"__comp": "17896441",
|
||||
"content": "f4817052"
|
||||
},
|
||||
"/docs/docs/commands/general/ccw-coordinator-a90": {
|
||||
"__comp": "__comp---theme-doc-item-178-a40",
|
||||
"content": "content---docs-docs-commands-general-ccw-coordinatord-55-a04"
|
||||
"__comp": "17896441",
|
||||
"content": "d550a629"
|
||||
},
|
||||
"/docs/docs/commands/general/ccw-debug-663": {
|
||||
"__comp": "__comp---theme-doc-item-178-a40",
|
||||
"content": "content---docs-docs-commands-general-ccw-debug-97-c-2c8"
|
||||
"__comp": "17896441",
|
||||
"content": "97c6e66a"
|
||||
},
|
||||
"/docs/docs/commands/general/ccw-plan-40b": {
|
||||
"__comp": "__comp---theme-doc-item-178-a40",
|
||||
"content": "content---docs-docs-commands-general-ccw-plan-04-d-aa3"
|
||||
"__comp": "17896441",
|
||||
"content": "04db0a2e"
|
||||
},
|
||||
"/docs/docs/commands/general/ccw-test-99d": {
|
||||
"__comp": "__comp---theme-doc-item-178-a40",
|
||||
"content": "content---docs-docs-commands-general-ccw-testcce-a5d"
|
||||
"__comp": "17896441",
|
||||
"content": "ccef5d0f"
|
||||
},
|
||||
"/docs/docs/commands/general/codex-coordinator-996": {
|
||||
"__comp": "__comp---theme-doc-item-178-a40",
|
||||
"content": "content---docs-docs-commands-general-codex-coordinatorf-92-965"
|
||||
"__comp": "17896441",
|
||||
"content": "f9222419"
|
||||
},
|
||||
"/docs/docs/commands/general/flow-create-d91": {
|
||||
"__comp": "__comp---theme-doc-item-178-a40",
|
||||
"content": "content---docs-docs-commands-general-flow-createfab-5a7"
|
||||
"__comp": "17896441",
|
||||
"content": "fabaf1c8"
|
||||
},
|
||||
"/docs/docs/commands/issue/issue-convert-to-plan-d90": {
|
||||
"__comp": "__comp---theme-doc-item-178-a40",
|
||||
"content": "content---docs-docs-commands-issue-issue-convert-to-plan-5-c-7-428"
|
||||
"__comp": "17896441",
|
||||
"content": "5c7b2278"
|
||||
},
|
||||
"/docs/docs/commands/issue/issue-discover-2a1": {
|
||||
"__comp": "__comp---theme-doc-item-178-a40",
|
||||
"content": "content---docs-docs-commands-issue-issue-discover-1-e-3-61d"
|
||||
"__comp": "17896441",
|
||||
"content": "1e3006f3"
|
||||
},
|
||||
"/docs/docs/commands/issue/issue-execute-abb": {
|
||||
"__comp": "__comp---theme-doc-item-178-a40",
|
||||
"content": "content---docs-docs-commands-issue-issue-executefe-8-121"
|
||||
"__comp": "17896441",
|
||||
"content": "fe8e3dcf"
|
||||
},
|
||||
"/docs/docs/commands/issue/issue-from-brainstorm-72b": {
|
||||
"__comp": "__comp---theme-doc-item-178-a40",
|
||||
"content": "content---docs-docs-commands-issue-issue-from-brainstorm-2-ec-ca5"
|
||||
"__comp": "17896441",
|
||||
"content": "2ecf8b4a"
|
||||
},
|
||||
"/docs/docs/commands/issue/issue-new-c58": {
|
||||
"__comp": "__comp---theme-doc-item-178-a40",
|
||||
"content": "content---docs-docs-commands-issue-issue-new-4-ad-afb"
|
||||
"__comp": "17896441",
|
||||
"content": "4ad7db0f"
|
||||
},
|
||||
"/docs/docs/commands/issue/issue-plan-fd2": {
|
||||
"__comp": "__comp---theme-doc-item-178-a40",
|
||||
"content": "content---docs-docs-commands-issue-issue-plana-6-c-1b0"
|
||||
"__comp": "17896441",
|
||||
"content": "a6c3df16"
|
||||
},
|
||||
"/docs/docs/commands/issue/issue-queue-1ce": {
|
||||
"__comp": "__comp---theme-doc-item-178-a40",
|
||||
"content": "content---docs-docs-commands-issue-issue-queue-1-ba-06f"
|
||||
"__comp": "17896441",
|
||||
"content": "1bac9067"
|
||||
},
|
||||
"/docs/docs/commands/memory/memory-compact-74c": {
|
||||
"__comp": "__comp---theme-doc-item-178-a40",
|
||||
"content": "content---docs-docs-commands-memory-memory-compact-7-a-1-f85"
|
||||
"__comp": "17896441",
|
||||
"content": "7a1ee27c"
|
||||
},
|
||||
"/docs/docs/commands/memory/memory-docs-full-cli-7a4": {
|
||||
"__comp": "__comp---theme-doc-item-178-a40",
|
||||
"content": "content---docs-docs-commands-memory-memory-docs-full-cli-4-cc-b15"
|
||||
"__comp": "17896441",
|
||||
"content": "4cc74730"
|
||||
},
|
||||
"/docs/docs/commands/memory/memory-docs-related-cli-fb4": {
|
||||
"__comp": "__comp---theme-doc-item-178-a40",
|
||||
"content": "content---docs-docs-commands-memory-memory-docs-related-cli-60-e-28a"
|
||||
"__comp": "17896441",
|
||||
"content": "60eef997"
|
||||
},
|
||||
"/docs/docs/commands/memory/memory-load-c66": {
|
||||
"__comp": "__comp---theme-doc-item-178-a40",
|
||||
"content": "content---docs-docs-commands-memory-memory-load-157-920"
|
||||
"__comp": "17896441",
|
||||
"content": "157db180"
|
||||
},
|
||||
"/docs/docs/commands/memory/memory-update-full-b80": {
|
||||
"__comp": "__comp---theme-doc-item-178-a40",
|
||||
"content": "content---docs-docs-commands-memory-memory-update-full-666-28e"
|
||||
"__comp": "17896441",
|
||||
"content": "666bb1bf"
|
||||
},
|
||||
"/docs/docs/commands/memory/memory-update-related-f0d": {
|
||||
"__comp": "__comp---theme-doc-item-178-a40",
|
||||
"content": "content---docs-docs-commands-memory-memory-update-related-611-f0a"
|
||||
"__comp": "17896441",
|
||||
"content": "611877e1"
|
||||
},
|
||||
"/docs/docs/faq-4b2": {
|
||||
"__comp": "__comp---theme-doc-item-178-a40",
|
||||
"content": "content---docs-docs-faqea-3-29f"
|
||||
"__comp": "17896441",
|
||||
"content": "ea313555"
|
||||
},
|
||||
"/docs/docs/overview-7df": {
|
||||
"__comp": "__comp---theme-doc-item-178-a40",
|
||||
"content": "content---docs-docs-overview-188-8fe"
|
||||
"__comp": "17896441",
|
||||
"content": "18891827"
|
||||
},
|
||||
"/docs/docs/workflows/faq-f47": {
|
||||
"__comp": "__comp---theme-doc-item-178-a40",
|
||||
"content": "content---docs-docs-workflows-faqbcf-a47"
|
||||
"__comp": "17896441",
|
||||
"content": "bcf6b37c"
|
||||
},
|
||||
"/docs/docs/workflows/introduction-4cb": {
|
||||
"__comp": "__comp---theme-doc-item-178-a40",
|
||||
"content": "content---docs-docs-workflows-introduction-9-f-4-dba"
|
||||
"__comp": "17896441",
|
||||
"content": "9f4ca91e"
|
||||
},
|
||||
"/docs/docs/workflows/level-1-ultra-lightweight-5c4": {
|
||||
"__comp": "__comp---theme-doc-item-178-a40",
|
||||
"content": "content---docs-docs-workflows-level-1-ultra-lightweightc-5-a-692"
|
||||
"__comp": "17896441",
|
||||
"content": "c5a82d8d"
|
||||
},
|
||||
"/docs/docs/workflows/level-2-rapid-ad8": {
|
||||
"__comp": "__comp---theme-doc-item-178-a40",
|
||||
"content": "content---docs-docs-workflows-level-2-rapid-19-b-a2f"
|
||||
"__comp": "17896441",
|
||||
"content": "19b64556"
|
||||
},
|
||||
"/docs/docs/workflows/level-3-standard-3ea": {
|
||||
"__comp": "__comp---theme-doc-item-178-a40",
|
||||
"content": "content---docs-docs-workflows-level-3-standardbdb-19a"
|
||||
"__comp": "17896441",
|
||||
"content": "bdb2b105"
|
||||
},
|
||||
"/docs/docs/workflows/level-4-brainstorm-f4f": {
|
||||
"__comp": "__comp---theme-doc-item-178-a40",
|
||||
"content": "content---docs-docs-workflows-level-4-brainstormd-04-69a"
|
||||
"__comp": "17896441",
|
||||
"content": "d045285b"
|
||||
},
|
||||
"/docs/docs/workflows/level-5-intelligent-84a": {
|
||||
"__comp": "__comp---theme-doc-item-178-a40",
|
||||
"content": "content---docs-docs-workflows-level-5-intelligent-186-435"
|
||||
"__comp": "17896441",
|
||||
"content": "186dcf4e"
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,9 @@
|
||||
"name": "@docusaurus/plugin-content-pages",
|
||||
"version": "3.9.2"
|
||||
},
|
||||
"docusaurus-plugin-debug": {
|
||||
"docusaurus-plugin-sitemap": {
|
||||
"type": "package",
|
||||
"name": "@docusaurus/plugin-debug",
|
||||
"name": "@docusaurus/plugin-sitemap",
|
||||
"version": "3.9.2"
|
||||
},
|
||||
"docusaurus-plugin-svgr": {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Sparkline } from '@/components/charts/Sparkline';
|
||||
import { useWorkflowStatusCounts, generateMockWorkflowStatusCounts } from '@/hooks/useWorkflowStatusCounts';
|
||||
import { useDashboardStats } from '@/hooks/useDashboardStats';
|
||||
import { useProjectOverview } from '@/hooks/useProjectOverview';
|
||||
import { useIndexStatus } from '@/hooks/useIndex';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
ListChecks,
|
||||
@@ -37,6 +38,7 @@ import {
|
||||
Sparkles,
|
||||
BarChart3,
|
||||
PieChart as PieChartIcon,
|
||||
Database,
|
||||
} from 'lucide-react';
|
||||
|
||||
export interface WorkflowTaskWidgetProps {
|
||||
@@ -210,6 +212,7 @@ function WorkflowTaskWidgetComponent({ className }: WorkflowTaskWidgetProps) {
|
||||
const { data, isLoading } = useWorkflowStatusCounts();
|
||||
const { stats, isLoading: statsLoading } = useDashboardStats({ refetchInterval: 60000 });
|
||||
const { projectOverview, isLoading: projectLoading } = useProjectOverview();
|
||||
const { status: indexStatus } = useIndexStatus({ refetchInterval: 30000 });
|
||||
|
||||
const chartData = data || generateMockWorkflowStatusCounts();
|
||||
const total = chartData.reduce((sum, item) => sum + item.count, 0);
|
||||
@@ -320,6 +323,35 @@ function WorkflowTaskWidgetComponent({ className }: WorkflowTaskWidgetProps) {
|
||||
<span className="font-semibold">{projectOverview?.developmentIndex?.enhancement?.length || 0}</span>
|
||||
<span className="text-muted-foreground">{formatMessage({ id: 'projectOverview.devIndex.category.enhancements' })}</span>
|
||||
</div>
|
||||
|
||||
{/* Index Status Indicator */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative">
|
||||
<Database className={cn(
|
||||
"h-3.5 w-3.5",
|
||||
indexStatus?.status === 'building' && "text-blue-600 animate-pulse",
|
||||
indexStatus?.status === 'completed' && "text-emerald-600",
|
||||
indexStatus?.status === 'idle' && "text-slate-500",
|
||||
indexStatus?.status === 'failed' && "text-red-600"
|
||||
)} />
|
||||
{indexStatus?.status === 'building' && (
|
||||
<span className="absolute -top-0.5 -right-0.5 flex h-2 w-2">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-500"></span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className={cn(
|
||||
"font-semibold",
|
||||
indexStatus?.status === 'building' && "text-blue-600",
|
||||
indexStatus?.status === 'completed' && "text-emerald-600",
|
||||
indexStatus?.status === 'idle' && "text-slate-500",
|
||||
indexStatus?.status === 'failed' && "text-red-600"
|
||||
)}>
|
||||
{indexStatus?.totalFiles || 0}
|
||||
</span>
|
||||
<span className="text-muted-foreground">{formatMessage({ id: 'home.indexStatus.label' })}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Date + Expand Button */}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// ========================================
|
||||
// Sidebar Component
|
||||
// ========================================
|
||||
// Collapsible navigation sidebar with 6-group accordion structure
|
||||
// Collapsible navigation sidebar with 5-group accordion structure
|
||||
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
Home,
|
||||
FolderKanban,
|
||||
Workflow,
|
||||
RefreshCw,
|
||||
AlertCircle,
|
||||
Sparkles,
|
||||
Terminal,
|
||||
@@ -60,7 +59,7 @@ interface NavGroupDef {
|
||||
}>;
|
||||
}
|
||||
|
||||
// Define the 6 navigation groups with their items
|
||||
// Define the 5 navigation groups with their items
|
||||
const navGroupDefinitions: NavGroupDef[] = [
|
||||
{
|
||||
id: 'overview',
|
||||
@@ -80,8 +79,8 @@ const navGroupDefinitions: NavGroupDef[] = [
|
||||
{ path: '/lite-tasks', labelKey: 'navigation.main.liteTasks', icon: Zap },
|
||||
{ path: '/orchestrator', labelKey: 'navigation.main.orchestrator', icon: Workflow },
|
||||
{ path: '/coordinator', labelKey: 'navigation.main.coordinator', icon: GitFork },
|
||||
{ path: '/loops', labelKey: 'navigation.main.loops', icon: RefreshCw },
|
||||
{ path: '/history', labelKey: 'navigation.main.history', icon: Clock },
|
||||
{ path: '/issues', labelKey: 'navigation.main.issues', icon: AlertCircle },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -93,14 +92,7 @@ const navGroupDefinitions: NavGroupDef[] = [
|
||||
{ path: '/prompts', labelKey: 'navigation.main.prompts', icon: History },
|
||||
{ path: '/skills', labelKey: 'navigation.main.skills', icon: Sparkles },
|
||||
{ path: '/commands', labelKey: 'navigation.main.commands', icon: Terminal },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'issues',
|
||||
titleKey: 'navigation.groups.issues',
|
||||
icon: AlertCircle,
|
||||
items: [
|
||||
{ path: '/issues', labelKey: 'navigation.main.issues', icon: AlertCircle },
|
||||
{ path: '/settings/rules', labelKey: 'navigation.main.rules', icon: Shield },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -109,6 +101,7 @@ const navGroupDefinitions: NavGroupDef[] = [
|
||||
icon: Wrench,
|
||||
items: [
|
||||
{ path: '/hooks', labelKey: 'navigation.main.hooks', icon: GitFork },
|
||||
{ path: '/settings/mcp', labelKey: 'navigation.main.mcp', icon: Server },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -116,11 +109,9 @@ const navGroupDefinitions: NavGroupDef[] = [
|
||||
titleKey: 'navigation.groups.configuration',
|
||||
icon: Cog,
|
||||
items: [
|
||||
{ path: '/settings', labelKey: 'navigation.main.settings', icon: Settings },
|
||||
{ path: '/settings/mcp', labelKey: 'navigation.main.mcp', icon: Server },
|
||||
{ path: '/settings/rules', labelKey: 'navigation.main.rules', icon: Shield },
|
||||
{ path: '/settings/codexlens', labelKey: 'navigation.main.codexlens', icon: Sparkles },
|
||||
{ path: '/api-settings', labelKey: 'navigation.main.apiSettings', icon: Server },
|
||||
{ path: '/settings', labelKey: 'navigation.main.settings', icon: Settings },
|
||||
{ path: '/help', labelKey: 'navigation.main.help', icon: HelpCircle },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -15,6 +15,9 @@ import {
|
||||
Database,
|
||||
FileText,
|
||||
HardDrive,
|
||||
MessageCircleQuestion,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
} from 'lucide-react';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
@@ -83,6 +86,7 @@ export const CCW_MCP_TOOLS: CcwTool[] = [
|
||||
{ name: 'edit_file', desc: 'Edit/replace content', core: true },
|
||||
{ name: 'read_file', desc: 'Read file contents', core: true },
|
||||
{ name: 'core_memory', desc: 'Core memory management', core: true },
|
||||
{ name: 'ask_question', desc: 'Interactive questions (A2UI)', core: false },
|
||||
];
|
||||
|
||||
// ========== Component ==========
|
||||
@@ -211,13 +215,9 @@ export function CcwToolsMcpCard({
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{isExpanded ? (
|
||||
<div className="w-5 h-5 flex items-center justify-center text-muted-foreground">
|
||||
▼
|
||||
</div>
|
||||
<ChevronDown className="w-5 h-5 text-muted-foreground" />
|
||||
) : (
|
||||
<div className="w-5 h-5 flex items-center justify-center text-muted-foreground">
|
||||
▶
|
||||
</div>
|
||||
<ChevronRight className="w-5 h-5 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -425,6 +425,8 @@ function getToolIcon(toolName: string): React.ReactElement {
|
||||
return <Database {...iconProps} />;
|
||||
case 'core_memory':
|
||||
return <Settings {...iconProps} />;
|
||||
case 'ask_question':
|
||||
return <MessageCircleQuestion {...iconProps} />;
|
||||
default:
|
||||
return <Settings {...iconProps} />;
|
||||
}
|
||||
|
||||
@@ -162,56 +162,32 @@ export function ConfigTypeToggle({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
{/* Label */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'mcp.configType.label' })}
|
||||
</span>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{getConfigFileExtension(internalType)}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Toggle Buttons */}
|
||||
<div className="flex gap-2 p-1 bg-muted rounded-lg">
|
||||
<Button
|
||||
variant={internalType === 'mcp-json' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => handleTypeClick('mcp-json')}
|
||||
className={cn(
|
||||
'flex-1',
|
||||
internalType === 'mcp-json' && 'shadow-sm'
|
||||
)}
|
||||
>
|
||||
<span className="text-sm">
|
||||
{formatMessage({ id: 'mcp.configType.claudeJson' })}
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant={internalType === 'claude-json' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => handleTypeClick('claude-json')}
|
||||
className={cn(
|
||||
'flex-1',
|
||||
internalType === 'claude-json' && 'shadow-sm'
|
||||
)}
|
||||
>
|
||||
<span className="text-sm">
|
||||
{formatMessage({ id: 'mcp.configType.claudeJson' })}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Current Format Display */}
|
||||
<div className="flex items-center gap-2 px-3 py-2 bg-muted/50 rounded-md border border-border">
|
||||
<code className="text-xs text-muted-foreground font-mono">
|
||||
{getConfigFileExtension(internalType)}
|
||||
</code>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'mcp.configType.' + internalType.replace('-', '') })}
|
||||
</span>
|
||||
</div>
|
||||
{/* Compact inline toggle */}
|
||||
<div className="flex items-center gap-1.5 p-0.5 bg-muted rounded-md h-9">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleTypeClick('mcp-json')}
|
||||
className={cn(
|
||||
'px-2.5 py-1 text-xs font-medium rounded transition-all',
|
||||
internalType === 'mcp-json'
|
||||
? 'bg-primary text-primary-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
.mcp.json
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleTypeClick('claude-json')}
|
||||
className={cn(
|
||||
'px-2.5 py-1 text-xs font-medium rounded transition-all',
|
||||
internalType === 'claude-json'
|
||||
? 'bg-primary text-primary-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
.claude.json
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Warning Dialog */}
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
} from '@/lib/api';
|
||||
import { mcpServersKeys, useMcpTemplates } from '@/hooks';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ConfigTypeToggle, type McpConfigType } from './ConfigTypeToggle';
|
||||
|
||||
// ========== Types ==========
|
||||
|
||||
@@ -90,6 +91,7 @@ export function McpServerDialog({
|
||||
const [errors, setErrors] = useState<FormErrors>({});
|
||||
const [argsInput, setArgsInput] = useState('');
|
||||
const [envInput, setEnvInput] = useState('');
|
||||
const [configType, setConfigType] = useState<McpConfigType>('mcp-json');
|
||||
|
||||
// Initialize form from server prop (edit mode)
|
||||
useEffect(() => {
|
||||
@@ -458,6 +460,20 @@ export function McpServerDialog({
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Config Type Toggle - Only for project scope */}
|
||||
{formData.scope === 'project' && (
|
||||
<div className="flex items-center gap-2 mt-2 pl-6">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'mcp.configType.format' })}:
|
||||
</span>
|
||||
<ConfigTypeToggle
|
||||
currentType={configType}
|
||||
onTypeChange={setConfigType}
|
||||
showWarning={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Enabled */}
|
||||
|
||||
@@ -1,52 +1,30 @@
|
||||
// ========================================
|
||||
// Recommended MCP Section Component
|
||||
// ========================================
|
||||
// Display recommended MCP servers with one-click install functionality
|
||||
// Display recommended MCP servers with wizard-based install functionality
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
Search,
|
||||
Globe,
|
||||
Sparkles,
|
||||
Download,
|
||||
Check,
|
||||
Loader2,
|
||||
Settings,
|
||||
Key,
|
||||
Zap,
|
||||
Code2,
|
||||
} from 'lucide-react';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from '@/components/ui/Dialog';
|
||||
import {
|
||||
createMcpServer,
|
||||
fetchMcpServers,
|
||||
} from '@/lib/api';
|
||||
import { mcpServersKeys } from '@/hooks';
|
||||
import { useNotifications } from '@/hooks/useNotifications';
|
||||
import { RecommendedMcpWizard, RecommendedMcpDefinition } from './RecommendedMcpWizard';
|
||||
import { fetchMcpConfig } from '@/lib/api';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
// ========== Types ==========
|
||||
|
||||
/**
|
||||
* Recommended server configuration
|
||||
*/
|
||||
export interface RecommendedServer {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
command: string;
|
||||
args: string[];
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
category: 'search' | 'browser' | 'ai';
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for RecommendedMcpSection component
|
||||
*/
|
||||
@@ -55,61 +33,135 @@ export interface RecommendedMcpSectionProps {
|
||||
onInstallComplete?: () => void;
|
||||
}
|
||||
|
||||
interface RecommendedServerCardProps {
|
||||
server: RecommendedServer;
|
||||
isInstalled: boolean;
|
||||
isInstalling: boolean;
|
||||
onInstall: (server: RecommendedServer) => void;
|
||||
}
|
||||
|
||||
// ========== Constants ==========
|
||||
// ========== Platform Detection ==========
|
||||
const isWindows = typeof navigator !== 'undefined' && navigator.platform?.toLowerCase().includes('win');
|
||||
|
||||
/**
|
||||
* Pre-configured recommended MCP servers
|
||||
* Build cross-platform MCP config
|
||||
* On Windows, wraps npx/node/python commands with cmd /c for proper execution
|
||||
*/
|
||||
const RECOMMENDED_SERVERS: RecommendedServer[] = [
|
||||
function buildCrossPlatformMcpConfig(
|
||||
command: string,
|
||||
args: string[] = [],
|
||||
options: { env?: Record<string, string>; type?: string } = {}
|
||||
) {
|
||||
const { env, type } = options;
|
||||
|
||||
const windowsWrappedCommands = ['npx', 'npm', 'node', 'python', 'python3', 'pip', 'pip3', 'pnpm', 'yarn', 'bun'];
|
||||
const needsWindowsWrapper = isWindows && windowsWrappedCommands.includes(command.toLowerCase());
|
||||
|
||||
const config: { command: string; args: string[]; env?: Record<string, string>; type?: string } = needsWindowsWrapper
|
||||
? { command: 'cmd', args: ['/c', command, ...args] }
|
||||
: { command, args };
|
||||
|
||||
if (type) config.type = type;
|
||||
if (env && Object.keys(env).length > 0) config.env = env;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
// ========== Recommended MCP Definitions ==========
|
||||
|
||||
/**
|
||||
* Pre-configured recommended MCP servers with field definitions
|
||||
* Matches original JS version structure for full wizard support
|
||||
*/
|
||||
const RECOMMENDED_MCP_DEFINITIONS: RecommendedMcpDefinition[] = [
|
||||
{
|
||||
id: 'ace-tool',
|
||||
name: 'ACE Tool',
|
||||
description: 'Advanced code search and context engine for intelligent code discovery',
|
||||
command: 'mcp__ace-tool__search_context',
|
||||
args: [],
|
||||
icon: Search,
|
||||
nameKey: 'mcp.ace-tool.name',
|
||||
descKey: 'mcp.ace-tool.desc',
|
||||
icon: 'search-code',
|
||||
category: 'search',
|
||||
fields: [
|
||||
{
|
||||
key: 'baseUrl',
|
||||
labelKey: 'mcp.ace-tool.field.baseUrl',
|
||||
type: 'text',
|
||||
default: 'https://acemcp.heroman.wtf/relay/',
|
||||
placeholder: 'https://acemcp.heroman.wtf/relay/',
|
||||
required: true,
|
||||
descKey: 'mcp.ace-tool.field.baseUrl.desc',
|
||||
},
|
||||
{
|
||||
key: 'token',
|
||||
labelKey: 'mcp.ace-tool.field.token',
|
||||
type: 'password',
|
||||
default: '',
|
||||
placeholder: 'ace_xxxxxxxxxxxxxxxx',
|
||||
required: true,
|
||||
descKey: 'mcp.ace-tool.field.token.desc',
|
||||
},
|
||||
],
|
||||
buildConfig: (values) => buildCrossPlatformMcpConfig('npx', [
|
||||
'ace-tool',
|
||||
'--base-url',
|
||||
values.baseUrl || 'https://acemcp.heroman.wtf/relay/',
|
||||
'--token',
|
||||
values.token,
|
||||
]),
|
||||
},
|
||||
{
|
||||
id: 'chrome-devtools',
|
||||
name: 'Chrome DevTools',
|
||||
description: 'Browser automation and debugging tools for web development',
|
||||
command: 'mcp__chrome-devtools',
|
||||
args: [],
|
||||
icon: Globe,
|
||||
nameKey: 'mcp.chrome-devtools.name',
|
||||
descKey: 'mcp.chrome-devtools.desc',
|
||||
icon: 'chrome',
|
||||
category: 'browser',
|
||||
fields: [],
|
||||
buildConfig: () => buildCrossPlatformMcpConfig('npx', ['chrome-devtools-mcp@latest'], { type: 'stdio' }),
|
||||
},
|
||||
{
|
||||
id: 'exa-search',
|
||||
name: 'Exa Search',
|
||||
description: 'AI-powered web search with real-time crawling capabilities',
|
||||
command: 'mcp__exa__search',
|
||||
args: [],
|
||||
icon: Sparkles,
|
||||
category: 'ai',
|
||||
id: 'exa',
|
||||
nameKey: 'mcp.exa.name',
|
||||
descKey: 'mcp.exa.desc',
|
||||
icon: 'globe-2',
|
||||
category: 'search',
|
||||
fields: [
|
||||
{
|
||||
key: 'apiKey',
|
||||
labelKey: 'mcp.exa.field.apiKey',
|
||||
type: 'password',
|
||||
default: '',
|
||||
placeholder: 'your-exa-api-key',
|
||||
required: false,
|
||||
descKey: 'mcp.exa.field.apiKey.desc',
|
||||
},
|
||||
],
|
||||
buildConfig: (values) => {
|
||||
const env = values.apiKey ? { EXA_API_KEY: values.apiKey } : undefined;
|
||||
return buildCrossPlatformMcpConfig('npx', ['-y', 'exa-mcp-server'], { env });
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// ========== Icon Map ==========
|
||||
|
||||
const ICON_MAP: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
'search-code': Search,
|
||||
'chrome': Globe,
|
||||
'globe-2': Sparkles,
|
||||
'code-2': Code2,
|
||||
};
|
||||
|
||||
// ========== Helper Component ==========
|
||||
|
||||
interface RecommendedServerCardProps {
|
||||
definition: RecommendedMcpDefinition;
|
||||
isInstalled: boolean;
|
||||
onInstall: (definition: RecommendedMcpDefinition) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Individual recommended server card
|
||||
*/
|
||||
function RecommendedServerCard({
|
||||
server,
|
||||
definition,
|
||||
isInstalled,
|
||||
isInstalling,
|
||||
onInstall,
|
||||
}: RecommendedServerCardProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
const Icon = server.icon;
|
||||
const Icon = ICON_MAP[definition.icon] || Settings;
|
||||
const hasFields = definition.fields.length > 0;
|
||||
|
||||
return (
|
||||
<Card className="p-4 hover:shadow-md transition-shadow">
|
||||
@@ -129,7 +181,7 @@ function RecommendedServerCard({
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h4 className="text-sm font-medium text-foreground truncate">
|
||||
{server.name}
|
||||
{formatMessage({ id: definition.nameKey })}
|
||||
</h4>
|
||||
{isInstalled && (
|
||||
<Badge variant="default" className="text-xs">
|
||||
@@ -138,39 +190,40 @@ function RecommendedServerCard({
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground line-clamp-2 mb-3">
|
||||
{server.description}
|
||||
{formatMessage({ id: definition.descKey })}
|
||||
</p>
|
||||
|
||||
{/* Install Button */}
|
||||
{!isInstalled && (
|
||||
{/* Config info + Install */}
|
||||
<div className="flex items-center justify-between">
|
||||
{hasFields ? (
|
||||
<span className="text-xs text-muted-foreground flex items-center gap-1">
|
||||
<Key className="w-3 h-3" />
|
||||
{definition.fields.length} {formatMessage({ id: 'mcp.configRequired' })}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs text-green-600 dark:text-green-400 flex items-center gap-1">
|
||||
<Zap className="w-3 h-3" />
|
||||
{formatMessage({ id: 'mcp.noConfigNeeded' })}
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
variant={isInstalled ? 'outline' : 'default'}
|
||||
size="sm"
|
||||
onClick={() => onInstall(server)}
|
||||
disabled={isInstalling}
|
||||
className="w-full"
|
||||
onClick={() => onInstall(definition)}
|
||||
>
|
||||
{isInstalling ? (
|
||||
{isInstalled ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-1 animate-spin" />
|
||||
{formatMessage({ id: 'mcp.recommended.actions.installing' })}
|
||||
<Settings className="w-3.5 h-3.5 mr-1" />
|
||||
{formatMessage({ id: 'mcp.reconfigure' })}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="w-4 h-4 mr-1" />
|
||||
<Download className="w-3.5 h-3.5 mr-1" />
|
||||
{formatMessage({ id: 'mcp.recommended.actions.install' })}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Installed Indicator */}
|
||||
{isInstalled && (
|
||||
<div className="flex items-center gap-1 text-xs text-primary">
|
||||
<Check className="w-4 h-4" />
|
||||
<span>{formatMessage({ id: 'mcp.recommended.actions.installed' })}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -180,88 +233,64 @@ function RecommendedServerCard({
|
||||
// ========== Main Component ==========
|
||||
|
||||
/**
|
||||
* Recommended MCP servers section with one-click install
|
||||
* Recommended MCP servers section with wizard-based install
|
||||
*/
|
||||
export function RecommendedMcpSection({
|
||||
onInstallComplete,
|
||||
}: RecommendedMcpSectionProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
const queryClient = useQueryClient();
|
||||
const { success, error } = useNotifications();
|
||||
|
||||
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
|
||||
const [selectedServer, setSelectedServer] = useState<RecommendedServer | null>(null);
|
||||
const [installingServerId, setInstallingServerId] = useState<string | null>(null);
|
||||
const [wizardOpen, setWizardOpen] = useState(false);
|
||||
const [selectedDefinition, setSelectedDefinition] = useState<RecommendedMcpDefinition | null>(null);
|
||||
const [installedServerIds, setInstalledServerIds] = useState<Set<string>>(new Set());
|
||||
|
||||
// Check which servers are already installed
|
||||
const checkInstalledServers = async () => {
|
||||
try {
|
||||
const data = await fetchMcpServers();
|
||||
const allServers = [...data.project, ...data.global];
|
||||
const installedIds = new Set(
|
||||
allServers
|
||||
.filter(s => s.command.startsWith('mcp__'))
|
||||
.map(s => s.command)
|
||||
);
|
||||
const data = await fetchMcpConfig();
|
||||
const installedIds = new Set<string>();
|
||||
|
||||
const globalServers = data.globalServers || {};
|
||||
const userServers = data.userServers || {};
|
||||
for (const name of Object.keys(globalServers)) installedIds.add(name);
|
||||
for (const name of Object.keys(userServers)) installedIds.add(name);
|
||||
|
||||
const projects = data.projects || {};
|
||||
for (const proj of Object.values(projects)) {
|
||||
const servers = (proj as any).mcpServers || {};
|
||||
for (const name of Object.keys(servers)) installedIds.add(name);
|
||||
}
|
||||
|
||||
if ((data as any).codex?.servers) {
|
||||
for (const name of Object.keys((data as any).codex.servers)) installedIds.add(name);
|
||||
}
|
||||
|
||||
setInstalledServerIds(installedIds);
|
||||
} catch {
|
||||
// Ignore errors during check
|
||||
}
|
||||
};
|
||||
|
||||
// Check on mount
|
||||
useState(() => {
|
||||
useEffect(() => {
|
||||
checkInstalledServers();
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Create server mutation
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (server: Omit<RecommendedServer, 'id' | 'icon' | 'category'>) =>
|
||||
createMcpServer({
|
||||
command: server.command,
|
||||
args: server.args,
|
||||
scope: 'global',
|
||||
enabled: true,
|
||||
}),
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: mcpServersKeys.all });
|
||||
setInstalledServerIds(prev => new Set(prev).add(variables.command));
|
||||
setInstallingServerId(null);
|
||||
setConfirmDialogOpen(false);
|
||||
setSelectedServer(null);
|
||||
success(
|
||||
formatMessage({ id: 'mcp.recommended.actions.installed' }),
|
||||
formatMessage({ id: 'mcp.recommended.servers.' + selectedServer?.id + '.name' })
|
||||
);
|
||||
onInstallComplete?.();
|
||||
},
|
||||
onError: () => {
|
||||
setInstallingServerId(null);
|
||||
error(
|
||||
formatMessage({ id: 'mcp.dialog.validation.nameRequired' }),
|
||||
formatMessage({ id: 'mcp.dialog.validation.commandRequired' })
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// Handle install click
|
||||
const handleInstallClick = (server: RecommendedServer) => {
|
||||
setSelectedServer(server);
|
||||
setConfirmDialogOpen(true);
|
||||
// Handle install click - open wizard
|
||||
const handleInstallClick = (definition: RecommendedMcpDefinition) => {
|
||||
setSelectedDefinition(definition);
|
||||
setWizardOpen(true);
|
||||
};
|
||||
|
||||
// Handle confirm install
|
||||
const handleConfirmInstall = () => {
|
||||
if (!selectedServer) return;
|
||||
setInstallingServerId(selectedServer.id);
|
||||
setConfirmDialogOpen(false);
|
||||
createMutation.mutate(selectedServer);
|
||||
// Handle wizard close
|
||||
const handleWizardClose = () => {
|
||||
setWizardOpen(false);
|
||||
setSelectedDefinition(null);
|
||||
};
|
||||
|
||||
// Check if server is installed
|
||||
const isServerInstalled = (server: RecommendedServer) => {
|
||||
return installedServerIds.has(server.command);
|
||||
// Handle install complete
|
||||
const handleInstallComplete = () => {
|
||||
checkInstalledServers();
|
||||
onInstallComplete?.();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -279,66 +308,24 @@ export function RecommendedMcpSection({
|
||||
|
||||
{/* Server Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
{RECOMMENDED_SERVERS.map((server) => (
|
||||
{RECOMMENDED_MCP_DEFINITIONS.map((definition) => (
|
||||
<RecommendedServerCard
|
||||
key={server.id}
|
||||
server={server}
|
||||
isInstalled={isServerInstalled(server)}
|
||||
isInstalling={installingServerId === server.id}
|
||||
key={definition.id}
|
||||
definition={definition}
|
||||
isInstalled={installedServerIds.has(definition.id)}
|
||||
onInstall={handleInstallClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Confirmation Dialog */}
|
||||
<Dialog open={confirmDialogOpen} onOpenChange={setConfirmDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{formatMessage({ id: 'mcp.recommended.actions.install' })} {selectedServer?.name}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatMessage(
|
||||
{ id: 'mcp.recommended.description' },
|
||||
{ server: selectedServer?.name }
|
||||
)}
|
||||
</p>
|
||||
<div className="mt-4 p-3 bg-muted rounded-lg">
|
||||
<code className="text-xs font-mono">
|
||||
{selectedServer?.command} {selectedServer?.args.join(' ')}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setConfirmDialogOpen(false)}
|
||||
disabled={createMutation.isPending}
|
||||
>
|
||||
{formatMessage({ id: 'mcp.dialog.actions.cancel' })}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleConfirmInstall}
|
||||
disabled={createMutation.isPending}
|
||||
>
|
||||
{createMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-1 animate-spin" />
|
||||
{formatMessage({ id: 'mcp.recommended.actions.installing' })}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="w-4 h-4 mr-1" />
|
||||
{formatMessage({ id: 'mcp.recommended.actions.install' })}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{/* Wizard Dialog */}
|
||||
<RecommendedMcpWizard
|
||||
open={wizardOpen}
|
||||
onClose={handleWizardClose}
|
||||
mcpDefinition={selectedDefinition}
|
||||
onInstallComplete={handleInstallComplete}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
363
ccw/frontend/src/components/mcp/RecommendedMcpWizard.tsx
Normal file
363
ccw/frontend/src/components/mcp/RecommendedMcpWizard.tsx
Normal file
@@ -0,0 +1,363 @@
|
||||
// ========================================
|
||||
// Recommended MCP Wizard Component
|
||||
// ========================================
|
||||
// Dynamic configuration wizard for recommended MCP servers
|
||||
// Supports text, password, and multi-select field types
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Download, Loader2, X } from 'lucide-react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from '@/components/ui/Dialog';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Label } from '@/components/ui/Label';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import {
|
||||
addGlobalMcpServer,
|
||||
addProjectMcpServer,
|
||||
} from '@/lib/api';
|
||||
import { mcpServersKeys } from '@/hooks';
|
||||
import { useNotifications } from '@/hooks/useNotifications';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
// ========== Types ==========
|
||||
|
||||
/**
|
||||
* Field definition for wizard
|
||||
*/
|
||||
export interface WizardField {
|
||||
key: string;
|
||||
labelKey: string;
|
||||
descKey?: string;
|
||||
type: 'text' | 'password' | 'multi-select';
|
||||
default?: string | string[];
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
options?: Array<{
|
||||
value: string;
|
||||
label: string;
|
||||
desc?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recommended MCP server definition
|
||||
*/
|
||||
export interface RecommendedMcpDefinition {
|
||||
id: string;
|
||||
nameKey: string;
|
||||
descKey: string;
|
||||
icon: string;
|
||||
category: string;
|
||||
fields: WizardField[];
|
||||
buildConfig: (values: Record<string, any>) => {
|
||||
command: string;
|
||||
args: string[];
|
||||
env?: Record<string, string>;
|
||||
type?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for RecommendedMcpWizard component
|
||||
*/
|
||||
export interface RecommendedMcpWizardProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
mcpDefinition: RecommendedMcpDefinition | null;
|
||||
onInstallComplete?: () => void;
|
||||
}
|
||||
|
||||
// ========== Main Component ==========
|
||||
|
||||
/**
|
||||
* Wizard for installing recommended MCP servers with configuration
|
||||
*/
|
||||
export function RecommendedMcpWizard({
|
||||
open,
|
||||
onClose,
|
||||
mcpDefinition,
|
||||
onInstallComplete,
|
||||
}: RecommendedMcpWizardProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
const queryClient = useQueryClient();
|
||||
const { success: showSuccess, error: showError } = useNotifications();
|
||||
|
||||
// State for field values
|
||||
const [fieldValues, setFieldValues] = useState<Record<string, any>>({});
|
||||
const [selectedScope, setSelectedScope] = useState<'project' | 'global'>('global');
|
||||
|
||||
// Initialize field values when dialog opens
|
||||
const initializeFieldValues = () => {
|
||||
if (!mcpDefinition) return;
|
||||
|
||||
const initialValues: Record<string, any> = {};
|
||||
for (const field of mcpDefinition.fields) {
|
||||
if (field.default !== undefined) {
|
||||
initialValues[field.key] = field.default;
|
||||
} else if (field.type === 'multi-select') {
|
||||
initialValues[field.key] = [];
|
||||
} else {
|
||||
initialValues[field.key] = '';
|
||||
}
|
||||
}
|
||||
setFieldValues(initialValues);
|
||||
};
|
||||
|
||||
// Reset on open/close
|
||||
const handleOpenChange = (newOpen: boolean) => {
|
||||
if (newOpen && mcpDefinition) {
|
||||
initializeFieldValues();
|
||||
} else {
|
||||
setFieldValues({});
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
// Install mutation
|
||||
const installMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!mcpDefinition) throw new Error('No MCP definition');
|
||||
|
||||
const serverConfig = mcpDefinition.buildConfig(fieldValues);
|
||||
|
||||
if (selectedScope === 'global') {
|
||||
return addGlobalMcpServer(mcpDefinition.id, serverConfig);
|
||||
} else {
|
||||
return addProjectMcpServer(mcpDefinition.id, serverConfig);
|
||||
}
|
||||
},
|
||||
onSuccess: (result) => {
|
||||
if (result.success) {
|
||||
queryClient.invalidateQueries({ queryKey: mcpServersKeys.all });
|
||||
showSuccess(
|
||||
formatMessage({ id: 'mcp.wizard.installSuccess' }),
|
||||
formatMessage({ id: mcpDefinition!.nameKey })
|
||||
);
|
||||
handleOpenChange(false);
|
||||
onInstallComplete?.();
|
||||
} else {
|
||||
showError(
|
||||
formatMessage({ id: 'mcp.wizard.installError' }),
|
||||
result.error || 'Unknown error'
|
||||
);
|
||||
}
|
||||
},
|
||||
onError: (err: Error) => {
|
||||
showError(
|
||||
formatMessage({ id: 'mcp.wizard.installError' }),
|
||||
err.message
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// Handle field value change
|
||||
const handleFieldChange = (key: string, value: any) => {
|
||||
setFieldValues(prev => ({
|
||||
...prev,
|
||||
[key]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
// Handle multi-select toggle
|
||||
const handleMultiSelectToggle = (key: string, value: string) => {
|
||||
const current = fieldValues[key] || [];
|
||||
const newValue = current.includes(value)
|
||||
? current.filter((v: string) => v !== value)
|
||||
: [...current, value];
|
||||
handleFieldChange(key, newValue);
|
||||
};
|
||||
|
||||
// Validate required fields
|
||||
const validateFields = (): boolean => {
|
||||
if (!mcpDefinition) return false;
|
||||
|
||||
for (const field of mcpDefinition.fields) {
|
||||
if (field.required) {
|
||||
const value = fieldValues[field.key];
|
||||
if (field.type === 'multi-select') {
|
||||
if (!value || value.length === 0) return false;
|
||||
} else {
|
||||
if (!value || value.trim() === '') return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// Handle submit
|
||||
const handleSubmit = () => {
|
||||
if (!validateFields()) {
|
||||
showError(
|
||||
formatMessage({ id: 'mcp.wizard.validation' }),
|
||||
formatMessage({ id: 'mcp.wizard.requiredFields' })
|
||||
);
|
||||
return;
|
||||
}
|
||||
installMutation.mutate();
|
||||
};
|
||||
|
||||
if (!mcpDefinition) return null;
|
||||
|
||||
const hasFields = mcpDefinition.fields.length > 0;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="max-w-lg max-h-[90vh] overflow-y-auto">
|
||||
{/* Header */}
|
||||
<DialogHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-primary/10 rounded-lg flex items-center justify-center">
|
||||
<i className={cn('lucide w-5 h-5 text-primary', `lucide-${mcpDefinition.icon}`)} />
|
||||
</div>
|
||||
<div>
|
||||
<DialogTitle>
|
||||
{formatMessage({ id: 'mcp.wizard.install' })} {formatMessage({ id: mcpDefinition.nameKey })}
|
||||
</DialogTitle>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatMessage({ id: mcpDefinition.descKey })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Content */}
|
||||
<div className="space-y-4 py-4">
|
||||
{/* Fields */}
|
||||
{hasFields && (
|
||||
<div className="space-y-3">
|
||||
{mcpDefinition.fields.map((field) => (
|
||||
<div key={field.key} className="space-y-1.5">
|
||||
<Label className="flex items-center gap-1.5 text-sm font-medium">
|
||||
{formatMessage({ id: field.labelKey })}
|
||||
{field.required && <span className="text-destructive">*</span>}
|
||||
</Label>
|
||||
{field.descKey && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: field.descKey })}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Text/Password Input */}
|
||||
{(field.type === 'text' || field.type === 'password') && (
|
||||
<Input
|
||||
type={field.type}
|
||||
value={fieldValues[field.key] || ''}
|
||||
onChange={(e) => handleFieldChange(field.key, e.target.value)}
|
||||
placeholder={field.placeholder}
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Multi-Select */}
|
||||
{field.type === 'multi-select' && field.options && (
|
||||
<div className="space-y-2 p-2 bg-muted/30 border border-border rounded-lg max-h-48 overflow-y-auto">
|
||||
{field.options.map((option) => {
|
||||
const isSelected = (fieldValues[field.key] || []).includes(option.value);
|
||||
return (
|
||||
<div
|
||||
key={option.value}
|
||||
className={cn(
|
||||
'flex items-start gap-2 p-2 rounded transition-colors cursor-pointer',
|
||||
isSelected ? 'bg-primary/10' : 'hover:bg-muted/50'
|
||||
)}
|
||||
onClick={() => handleMultiSelectToggle(field.key, option.value)}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected}
|
||||
onChange={() => {}}
|
||||
className="mt-0.5 w-4 h-4"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
{option.label}
|
||||
</div>
|
||||
{option.desc && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{option.desc}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Scope Selection */}
|
||||
<div className="space-y-2 pt-3 border-t border-border">
|
||||
<Label className="text-sm font-medium">
|
||||
{formatMessage({ id: 'mcp.wizard.scope' })}
|
||||
</Label>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant={selectedScope === 'global' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setSelectedScope('global')}
|
||||
className="flex-1"
|
||||
>
|
||||
{formatMessage({ id: 'mcp.wizard.scope.global' })}
|
||||
</Button>
|
||||
<Button
|
||||
variant={selectedScope === 'project' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setSelectedScope('project')}
|
||||
className="flex-1"
|
||||
>
|
||||
{formatMessage({ id: 'mcp.wizard.scope.project' })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* No Configuration Needed Message */}
|
||||
{!hasFields && (
|
||||
<div className="p-3 bg-success/10 border border-success/20 rounded-lg text-sm text-success">
|
||||
{formatMessage({ id: 'mcp.noConfigNeeded' })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleOpenChange(false)}
|
||||
disabled={installMutation.isPending}
|
||||
>
|
||||
{formatMessage({ id: 'mcp.dialog.actions.cancel' })}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={installMutation.isPending}
|
||||
>
|
||||
{installMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-1 animate-spin" />
|
||||
{formatMessage({ id: 'mcp.wizard.installing' })}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="w-4 h-4 mr-1" />
|
||||
{formatMessage({ id: 'mcp.wizard.install' })}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default RecommendedMcpWizard;
|
||||
@@ -2015,6 +2015,21 @@ export interface McpServersResponse {
|
||||
global: McpServer[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch complete MCP configuration from all sources
|
||||
* Returns raw config including projects, globalServers, userServers, enterpriseServers
|
||||
*/
|
||||
export async function fetchMcpConfig(): Promise<{
|
||||
projects: Record<string, { mcpServers: Record<string, any>; disabledMcpServers?: string[] }>;
|
||||
globalServers: Record<string, any>;
|
||||
userServers: Record<string, any>;
|
||||
enterpriseServers: Record<string, any>;
|
||||
configSources: string[];
|
||||
codex?: { servers: Record<string, any>; configPath: string };
|
||||
}> {
|
||||
return fetchApi('/api/mcp-config');
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all MCP servers (project and global scope) for a specific workspace
|
||||
* @param projectPath - Optional project path to filter data by workspace
|
||||
@@ -2551,6 +2566,47 @@ export async function deleteRule(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add MCP server to global scope (~/.claude.json mcpServers)
|
||||
*/
|
||||
export async function addGlobalMcpServer(
|
||||
serverName: string,
|
||||
serverConfig: {
|
||||
command: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
type?: string;
|
||||
}
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
return fetchApi<{ success: boolean; error?: string }>('/api/mcp-add-global-server', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ serverName, serverConfig }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy/Add MCP server to project (.mcp.json or .claude.json)
|
||||
*/
|
||||
export async function copyMcpServerToProject(
|
||||
serverName: string,
|
||||
serverConfig: {
|
||||
command: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
type?: string;
|
||||
},
|
||||
projectPath?: string,
|
||||
configType: 'mcp' | 'claude' = 'mcp'
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
// Use current project path from URL or fallback
|
||||
const path = projectPath || window.location.pathname.split('/').filter(Boolean)[0] || '';
|
||||
|
||||
return fetchApi<{ success: boolean; error?: string }>('/api/mcp-copy-server', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ projectPath: path, serverName, serverConfig, configType }),
|
||||
});
|
||||
}
|
||||
|
||||
// ========== CCW Tools MCP API ==========
|
||||
|
||||
/**
|
||||
@@ -2565,15 +2621,111 @@ export interface CcwMcpConfig {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch CCW Tools MCP configuration
|
||||
* Platform detection for cross-platform MCP config
|
||||
*/
|
||||
export async function fetchCcwMcpConfig(): Promise<CcwMcpConfig> {
|
||||
const data = await fetchApi<CcwMcpConfig>('/api/mcp/ccw-config');
|
||||
return data;
|
||||
const isWindows = typeof navigator !== 'undefined' && navigator.platform?.toLowerCase().includes('win');
|
||||
|
||||
/**
|
||||
* Build CCW MCP server config
|
||||
*/
|
||||
function buildCcwMcpServerConfig(config: {
|
||||
enabledTools?: string[];
|
||||
projectRoot?: string;
|
||||
allowedDirs?: string;
|
||||
disableSandbox?: boolean;
|
||||
}): { command: string; args: string[]; env: Record<string, string> } {
|
||||
const env: Record<string, string> = {};
|
||||
|
||||
if (config.enabledTools && config.enabledTools.length > 0) {
|
||||
env.CCW_ENABLED_TOOLS = config.enabledTools.join(',');
|
||||
} else {
|
||||
env.CCW_ENABLED_TOOLS = 'all';
|
||||
}
|
||||
|
||||
if (config.projectRoot) {
|
||||
env.CCW_PROJECT_ROOT = config.projectRoot;
|
||||
}
|
||||
if (config.allowedDirs) {
|
||||
env.CCW_ALLOWED_DIRS = config.allowedDirs;
|
||||
}
|
||||
if (config.disableSandbox) {
|
||||
env.CCW_DISABLE_SANDBOX = '1';
|
||||
}
|
||||
|
||||
// Cross-platform config
|
||||
if (isWindows) {
|
||||
return {
|
||||
command: 'cmd',
|
||||
args: ['/c', 'npx', '-y', 'ccw-mcp'],
|
||||
env
|
||||
};
|
||||
}
|
||||
return {
|
||||
command: 'npx',
|
||||
args: ['-y', 'ccw-mcp'],
|
||||
env
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update CCW Tools MCP configuration
|
||||
* Fetch CCW Tools MCP configuration by checking if ccw-tools server exists
|
||||
*/
|
||||
export async function fetchCcwMcpConfig(): Promise<CcwMcpConfig> {
|
||||
try {
|
||||
const config = await fetchMcpConfig();
|
||||
|
||||
// Check if ccw-tools server exists in any config
|
||||
let ccwServer: any = null;
|
||||
|
||||
// Check global servers
|
||||
if (config.globalServers?.['ccw-tools']) {
|
||||
ccwServer = config.globalServers['ccw-tools'];
|
||||
}
|
||||
// Check user servers
|
||||
if (!ccwServer && config.userServers?.['ccw-tools']) {
|
||||
ccwServer = config.userServers['ccw-tools'];
|
||||
}
|
||||
// Check project servers
|
||||
if (!ccwServer && config.projects) {
|
||||
for (const proj of Object.values(config.projects)) {
|
||||
if (proj.mcpServers?.['ccw-tools']) {
|
||||
ccwServer = proj.mcpServers['ccw-tools'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!ccwServer) {
|
||||
return {
|
||||
isInstalled: false,
|
||||
enabledTools: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Parse enabled tools from env
|
||||
const env = ccwServer.env || {};
|
||||
const enabledToolsStr = env.CCW_ENABLED_TOOLS || 'all';
|
||||
const enabledTools = enabledToolsStr === 'all'
|
||||
? ['write_file', 'edit_file', 'read_file', 'core_memory', 'ask_question']
|
||||
: enabledToolsStr.split(',').map((t: string) => t.trim());
|
||||
|
||||
return {
|
||||
isInstalled: true,
|
||||
enabledTools,
|
||||
projectRoot: env.CCW_PROJECT_ROOT,
|
||||
allowedDirs: env.CCW_ALLOWED_DIRS,
|
||||
disableSandbox: env.CCW_DISABLE_SANDBOX === '1',
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
isInstalled: false,
|
||||
enabledTools: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update CCW Tools MCP configuration (re-install with new config)
|
||||
*/
|
||||
export async function updateCcwConfig(config: {
|
||||
enabledTools?: string[];
|
||||
@@ -2581,27 +2733,40 @@ export async function updateCcwConfig(config: {
|
||||
allowedDirs?: string;
|
||||
disableSandbox?: boolean;
|
||||
}): Promise<CcwMcpConfig> {
|
||||
return fetchApi<CcwMcpConfig>('/api/mcp/ccw-config', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(config),
|
||||
});
|
||||
const serverConfig = buildCcwMcpServerConfig(config);
|
||||
|
||||
// Install/update to global config
|
||||
const result = await addGlobalMcpServer('ccw-tools', serverConfig);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to update CCW config');
|
||||
}
|
||||
|
||||
return fetchCcwMcpConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Install CCW Tools MCP server
|
||||
*/
|
||||
export async function installCcwMcp(): Promise<CcwMcpConfig> {
|
||||
return fetchApi<CcwMcpConfig>('/api/mcp/ccw-install', {
|
||||
method: 'POST',
|
||||
const serverConfig = buildCcwMcpServerConfig({
|
||||
enabledTools: ['write_file', 'edit_file', 'read_file', 'core_memory', 'ask_question'],
|
||||
});
|
||||
|
||||
const result = await addGlobalMcpServer('ccw-tools', serverConfig);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to install CCW MCP');
|
||||
}
|
||||
|
||||
return fetchCcwMcpConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall CCW Tools MCP server
|
||||
*/
|
||||
export async function uninstallCcwMcp(): Promise<void> {
|
||||
await fetchApi<void>('/api/mcp/ccw-uninstall', {
|
||||
await fetchApi<{ success: boolean }>('/api/mcp-remove-global-server', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ serverName: 'ccw-tools' }),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,9 @@
|
||||
"description": "Create your first hook to automate your CLI workflow",
|
||||
"noHooksInEvent": "No hooks configured for this event"
|
||||
},
|
||||
"quickTemplates": {
|
||||
"title": "Quick Install Templates"
|
||||
},
|
||||
"templates": {
|
||||
"title": "Quick Install Templates",
|
||||
"description": "One-click installation for common hook patterns",
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
"runningLoops": "Running Loops",
|
||||
"openIssues": "Open Issues"
|
||||
},
|
||||
"indexStatus": {
|
||||
"label": "Indexed"
|
||||
},
|
||||
"sections": {
|
||||
"statistics": "Statistics",
|
||||
"recentSessions": "Recent Sessions",
|
||||
|
||||
@@ -128,6 +128,10 @@
|
||||
"core_memory": {
|
||||
"name": "core_memory",
|
||||
"desc": "Manage core memory entries"
|
||||
},
|
||||
"ask_question": {
|
||||
"name": "ask_question",
|
||||
"desc": "Ask interactive questions through A2UI interface"
|
||||
}
|
||||
},
|
||||
"paths": {
|
||||
|
||||
@@ -58,6 +58,9 @@
|
||||
"description": "创建您的第一个钩子以自动化 CLI 工作流",
|
||||
"noHooksInEvent": "此事件未配置钩子"
|
||||
},
|
||||
"quickTemplates": {
|
||||
"title": "快速安装模板"
|
||||
},
|
||||
"templates": {
|
||||
"title": "快速安装模板",
|
||||
"description": "常见钩子模式的一键安装",
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
"runningLoops": "运行中的循环",
|
||||
"openIssues": "开放问题"
|
||||
},
|
||||
"indexStatus": {
|
||||
"label": "索引"
|
||||
},
|
||||
"sections": {
|
||||
"statistics": "统计",
|
||||
"recentSessions": "最近会话",
|
||||
|
||||
@@ -128,6 +128,10 @@
|
||||
"core_memory": {
|
||||
"name": "core_memory",
|
||||
"desc": "管理核心内存条目"
|
||||
},
|
||||
"ask_question": {
|
||||
"name": "ask_question",
|
||||
"desc": "通过 A2UI 界面发起交互式问答"
|
||||
}
|
||||
},
|
||||
"paths": {
|
||||
|
||||
@@ -31,7 +31,6 @@ import { CodexMcpEditableCard } from '@/components/mcp/CodexMcpEditableCard';
|
||||
import { CcwToolsMcpCard } from '@/components/mcp/CcwToolsMcpCard';
|
||||
import { McpTemplatesSection } from '@/components/mcp/McpTemplatesSection';
|
||||
import { RecommendedMcpSection } from '@/components/mcp/RecommendedMcpSection';
|
||||
import { ConfigTypeToggle } from '@/components/mcp/ConfigTypeToggle';
|
||||
import { WindowsCompatibilityWarning } from '@/components/mcp/WindowsCompatibilityWarning';
|
||||
import { CrossCliCopyButton } from '@/components/mcp/CrossCliCopyButton';
|
||||
import { AllProjectsTable } from '@/components/mcp/AllProjectsTable';
|
||||
@@ -207,7 +206,6 @@ export function McpManagerPage() {
|
||||
const [editingServer, setEditingServer] = useState<McpServer | undefined>(undefined);
|
||||
const [cliMode, setCliMode] = useState<CliMode>('claude');
|
||||
const [codexExpandedServers, setCodexExpandedServers] = useState<Set<string>>(new Set());
|
||||
const [configType, setConfigType] = useState<'mcp-json' | 'claude-json'>('mcp-json');
|
||||
|
||||
const {
|
||||
servers,
|
||||
@@ -391,14 +389,24 @@ export function McpManagerPage() {
|
||||
<div className="space-y-6">
|
||||
{/* Page Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-foreground flex items-center gap-2">
|
||||
<Server className="w-6 h-6 text-primary" />
|
||||
{formatMessage({ id: 'mcp.title' })}
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
{formatMessage({ id: 'mcp.description' })}
|
||||
</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-foreground flex items-center gap-2">
|
||||
<Server className="w-6 h-6 text-primary" />
|
||||
{formatMessage({ id: 'mcp.title' })}
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
{formatMessage({ id: 'mcp.description' })}
|
||||
</p>
|
||||
</div>
|
||||
{/* CLI Mode Badge Switcher */}
|
||||
<div className="ml-3 flex-shrink-0">
|
||||
<CliModeToggle
|
||||
currentMode={cliMode}
|
||||
onModeChange={handleModeChange}
|
||||
codexConfigPath={codexConfigPath}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={() => currentRefetch()} disabled={currentIsFetching}>
|
||||
@@ -414,13 +422,6 @@ export function McpManagerPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CLI Mode Toggle */}
|
||||
<CliModeToggle
|
||||
currentMode={cliMode}
|
||||
onModeChange={handleModeChange}
|
||||
codexConfigPath={codexConfigPath}
|
||||
/>
|
||||
|
||||
{/* Tabbed Interface */}
|
||||
<TabsNavigation
|
||||
value={activeTab}
|
||||
@@ -434,7 +435,11 @@ export function McpManagerPage() {
|
||||
|
||||
{/* Tab Content: Templates */}
|
||||
{activeTab === 'templates' && (
|
||||
<div className="mt-4">
|
||||
<div className="mt-4 space-y-4">
|
||||
{/* Recommended MCP Servers */}
|
||||
<RecommendedMcpSection onInstallComplete={() => refetch()} />
|
||||
|
||||
{/* Templates Section */}
|
||||
<McpTemplatesSection
|
||||
onInstallTemplate={handleInstallTemplate}
|
||||
onSaveAsTemplate={handleSaveAsTemplate}
|
||||
@@ -448,20 +453,6 @@ export function McpManagerPage() {
|
||||
{/* Windows Compatibility Warning */}
|
||||
<WindowsCompatibilityWarning />
|
||||
|
||||
{/* Recommended MCP Servers */}
|
||||
{cliMode === 'claude' && (
|
||||
<RecommendedMcpSection onInstallComplete={() => refetch()} />
|
||||
)}
|
||||
|
||||
{/* Config Type Toggle */}
|
||||
{cliMode === 'claude' && (
|
||||
<ConfigTypeToggle
|
||||
currentType={configType}
|
||||
onTypeChange={setConfigType}
|
||||
existingServersCount={totalCount}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Stats Cards - Claude mode only */}
|
||||
{cliMode === 'claude' && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
|
||||
@@ -17,11 +17,7 @@ import {
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Languages,
|
||||
GitFork,
|
||||
Scale,
|
||||
Search,
|
||||
Power,
|
||||
PowerOff,
|
||||
Plus,
|
||||
} from 'lucide-react';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
@@ -29,12 +25,10 @@ import { Input } from '@/components/ui/Input';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { ThemeSelector } from '@/components/shared/ThemeSelector';
|
||||
import { useTheme } from '@/hooks';
|
||||
import { useHooks, useRules, useToggleHook, useToggleRule } from '@/hooks';
|
||||
import { useConfigStore, selectCliTools, selectDefaultCliTool, selectUserPreferences } from '@/stores/configStore';
|
||||
import type { CliToolConfig, UserPreferences } from '@/types/store';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { LanguageSwitcher } from '@/components/layout/LanguageSwitcher';
|
||||
import { IndexManager } from '@/components/shared/IndexManager';
|
||||
|
||||
// ========== CLI Tool Card Component ==========
|
||||
|
||||
@@ -47,6 +41,9 @@ interface CliToolCardProps {
|
||||
onToggleEnabled: () => void;
|
||||
onSetDefault: () => void;
|
||||
onUpdateModel: (field: 'primaryModel' | 'secondaryModel', value: string) => void;
|
||||
onUpdateTags: (tags: string[]) => void;
|
||||
onUpdateAvailableModels: (models: string[]) => void;
|
||||
onUpdateSettingsFile: (settingsFile: string | undefined) => void;
|
||||
}
|
||||
|
||||
function CliToolCard({
|
||||
@@ -58,9 +55,49 @@ function CliToolCard({
|
||||
onToggleEnabled,
|
||||
onSetDefault,
|
||||
onUpdateModel,
|
||||
onUpdateTags,
|
||||
onUpdateAvailableModels,
|
||||
onUpdateSettingsFile,
|
||||
}: CliToolCardProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
// Local state for tag and model input
|
||||
const [tagInput, setTagInput] = useState('');
|
||||
const [modelInput, setModelInput] = useState('');
|
||||
|
||||
// Handler for adding tags
|
||||
const handleAddTag = () => {
|
||||
const newTag = tagInput.trim();
|
||||
if (newTag && !config.tags.includes(newTag)) {
|
||||
onUpdateTags([...config.tags, newTag]);
|
||||
setTagInput('');
|
||||
}
|
||||
};
|
||||
|
||||
// Handler for removing tags
|
||||
const handleRemoveTag = (tagToRemove: string) => {
|
||||
onUpdateTags(config.tags.filter((t) => t !== tagToRemove));
|
||||
};
|
||||
|
||||
// Handler for adding available models
|
||||
const handleAddModel = () => {
|
||||
const newModel = modelInput.trim();
|
||||
const currentModels = config.availableModels || [];
|
||||
if (newModel && !currentModels.includes(newModel)) {
|
||||
onUpdateAvailableModels([...currentModels, newModel]);
|
||||
setModelInput('');
|
||||
}
|
||||
};
|
||||
|
||||
// Handler for removing available models
|
||||
const handleRemoveModel = (modelToRemove: string) => {
|
||||
const currentModels = config.availableModels || [];
|
||||
onUpdateAvailableModels(currentModels.filter((m) => m !== modelToRemove));
|
||||
};
|
||||
|
||||
// Predefined tags
|
||||
const predefinedTags = ['分析', 'Debug', 'implementation', 'refactoring', 'testing'];
|
||||
|
||||
return (
|
||||
<Card className={cn('overflow-hidden', !config.enabled && 'opacity-60')}>
|
||||
{/* Header */}
|
||||
@@ -157,6 +194,146 @@ function CliToolCard({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tags Section */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'apiSettings.cliSettings.tags' })}
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'apiSettings.cliSettings.tagsDescription' })}
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1 flex flex-wrap gap-1.5 p-2 border border-input bg-background rounded-md min-h-[38px] focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2">
|
||||
{config.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="inline-flex items-center gap-1 px-2 py-0.5 bg-primary/10 text-primary rounded text-xs h-6"
|
||||
>
|
||||
{tag}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveTag(tag)}
|
||||
className="hover:text-destructive transition-colors"
|
||||
aria-label={formatMessage({ id: 'apiSettings.cliSettings.removeTag' })}
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
<input
|
||||
type="text"
|
||||
value={tagInput}
|
||||
onChange={(e) => setTagInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleAddTag();
|
||||
}
|
||||
}}
|
||||
placeholder={config.tags.length === 0 ? formatMessage({ id: 'apiSettings.cliSettings.tagInputPlaceholder' }) : ''}
|
||||
className="flex-1 min-w-[120px] bg-transparent border-0 outline-none text-sm placeholder:text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
onClick={handleAddTag}
|
||||
variant="outline"
|
||||
className="shrink-0"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{/* Predefined Tags */}
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'apiSettings.cliSettings.predefinedTags' })}:
|
||||
</span>
|
||||
{predefinedTags.map((predefinedTag) => (
|
||||
<button
|
||||
key={predefinedTag}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!config.tags.includes(predefinedTag)) {
|
||||
onUpdateTags([...config.tags, predefinedTag]);
|
||||
}
|
||||
}}
|
||||
disabled={config.tags.includes(predefinedTag)}
|
||||
className="text-xs px-2 py-0.5 rounded border border-border hover:bg-muted disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{predefinedTag}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Available Models Section */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'apiSettings.cliSettings.availableModels' })}
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1 flex flex-wrap gap-1.5 p-2 border border-input bg-background rounded-md min-h-[38px] focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2">
|
||||
{(config.availableModels || []).map((model) => (
|
||||
<span
|
||||
key={model}
|
||||
className="inline-flex items-center gap-1 px-2 py-0.5 bg-primary/10 text-primary rounded text-xs h-6"
|
||||
>
|
||||
{model}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveModel(model)}
|
||||
className="hover:text-destructive transition-colors"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
<input
|
||||
type="text"
|
||||
value={modelInput}
|
||||
onChange={(e) => setModelInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleAddModel();
|
||||
}
|
||||
}}
|
||||
placeholder={(config.availableModels || []).length === 0 ? formatMessage({ id: 'apiSettings.cliSettings.availableModelsPlaceholder' }) : ''}
|
||||
className="flex-1 min-w-[120px] bg-transparent border-0 outline-none text-sm placeholder:text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
onClick={handleAddModel}
|
||||
variant="outline"
|
||||
className="shrink-0"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'apiSettings.cliSettings.availableModelsHint' })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Settings File */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'apiSettings.cliSettings.settingsFile' })}
|
||||
</label>
|
||||
<Input
|
||||
value={config.settingsFile || ''}
|
||||
onChange={(e) => onUpdateSettingsFile(e.target.value || undefined)}
|
||||
placeholder={formatMessage({ id: 'apiSettings.cliSettings.settingsFilePlaceholder' })}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'apiSettings.cliSettings.settingsFileHint' })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!isDefault && config.enabled && (
|
||||
<Button variant="outline" size="sm" onClick={onSetDefault}>
|
||||
{formatMessage({ id: 'settings.cliTools.setDefault' })}
|
||||
@@ -168,210 +345,6 @@ function CliToolCard({
|
||||
);
|
||||
}
|
||||
|
||||
// ========== Hooks Section Component ==========
|
||||
|
||||
function HooksSection() {
|
||||
const { formatMessage } = useIntl();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const { hooks, enabledCount, totalCount, isLoading } = useHooks();
|
||||
const { toggleHook, isToggling } = useToggleHook();
|
||||
|
||||
const filteredHooks = hooks.filter(h =>
|
||||
h.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
(h.description && h.description.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
);
|
||||
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold text-foreground flex items-center gap-2">
|
||||
<GitFork className="w-5 h-5" />
|
||||
{formatMessage({ id: 'settings.sections.hooks' })}
|
||||
</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{enabledCount}/{totalCount} {formatMessage({ id: 'cliHooks.stats.enabled' })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative mb-4">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder={formatMessage({ id: 'cliHooks.filters.searchPlaceholder' })}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{isLoading ? (
|
||||
<div className="space-y-2">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="h-16 bg-muted animate-pulse rounded-lg" />
|
||||
))}
|
||||
</div>
|
||||
) : filteredHooks.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<GitFork className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
||||
<p>{formatMessage({ id: 'cliHooks.emptyState.title' })}</p>
|
||||
</div>
|
||||
) : (
|
||||
filteredHooks.map((hook) => (
|
||||
<div
|
||||
key={hook.name}
|
||||
className="flex items-center justify-between p-3 rounded-lg border border-border hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={cn(
|
||||
'p-2 rounded-lg',
|
||||
hook.enabled ? 'bg-primary/10' : 'bg-muted'
|
||||
)}>
|
||||
<GitFork className={cn(
|
||||
'w-4 h-4',
|
||||
hook.enabled ? 'text-primary' : 'text-muted-foreground'
|
||||
)} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground">{hook.name}</p>
|
||||
{hook.description && (
|
||||
<p className="text-xs text-muted-foreground">{hook.description}</p>
|
||||
)}
|
||||
<Badge variant="outline" className="text-xs mt-1">
|
||||
{formatMessage({ id: `cliHooks.trigger.${hook.trigger}` })}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant={hook.enabled ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
className="h-8"
|
||||
onClick={() => toggleHook(hook.name, !hook.enabled)}
|
||||
disabled={isToggling}
|
||||
>
|
||||
{hook.enabled ? (
|
||||
<><Power className="w-4 h-4 mr-1" />{formatMessage({ id: 'settings.cliTools.enabled' })}</>
|
||||
) : (
|
||||
<><PowerOff className="w-4 h-4 mr-1" />{formatMessage({ id: 'settings.cliTools.disabled' })}</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== Rules Section Component ==========
|
||||
|
||||
function RulesSection() {
|
||||
const { formatMessage } = useIntl();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const { rules, enabledCount, totalCount, isLoading } = useRules();
|
||||
const { toggleRule, isToggling } = useToggleRule();
|
||||
|
||||
const filteredRules = rules.filter(r =>
|
||||
r.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
(r.description && r.description.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
);
|
||||
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold text-foreground flex items-center gap-2">
|
||||
<Scale className="w-5 h-5" />
|
||||
{formatMessage({ id: 'settings.sections.rules' })}
|
||||
</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{enabledCount}/{totalCount} {formatMessage({ id: 'cliRules.stats.enabled' })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative mb-4">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder={formatMessage({ id: 'cliRules.filters.searchPlaceholder' })}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{isLoading ? (
|
||||
<div className="space-y-2">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="h-16 bg-muted animate-pulse rounded-lg" />
|
||||
))}
|
||||
</div>
|
||||
) : filteredRules.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<Scale className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
||||
<p>{formatMessage({ id: 'cliRules.emptyState.title' })}</p>
|
||||
</div>
|
||||
) : (
|
||||
filteredRules.map((rule) => (
|
||||
<div
|
||||
key={rule.id}
|
||||
className="flex items-center justify-between p-3 rounded-lg border border-border hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={cn(
|
||||
'p-2 rounded-lg',
|
||||
rule.enabled ? 'bg-primary/10' : 'bg-muted'
|
||||
)}>
|
||||
<Scale className={cn(
|
||||
'w-4 h-4',
|
||||
rule.enabled ? 'text-primary' : 'text-muted-foreground'
|
||||
)} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground">{rule.name}</p>
|
||||
{rule.description && (
|
||||
<p className="text-xs text-muted-foreground">{rule.description}</p>
|
||||
)}
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
{rule.category && (
|
||||
<Badge variant="secondary" className="text-xs">{rule.category}</Badge>
|
||||
)}
|
||||
{rule.severity && (
|
||||
<Badge
|
||||
variant={rule.severity === 'error' ? 'destructive' : 'outline'}
|
||||
className="text-xs"
|
||||
>
|
||||
{formatMessage({ id: `cliRules.severity.${rule.severity}` })}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant={rule.enabled ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
className="h-8"
|
||||
onClick={() => toggleRule(rule.id, !rule.enabled)}
|
||||
disabled={isToggling}
|
||||
>
|
||||
{rule.enabled ? (
|
||||
<><Power className="w-4 h-4 mr-1" />{formatMessage({ id: 'settings.cliTools.enabled' })}</>
|
||||
) : (
|
||||
<><PowerOff className="w-4 h-4 mr-1" />{formatMessage({ id: 'settings.cliTools.disabled' })}</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== Main Page Component ==========
|
||||
|
||||
export function SettingsPage() {
|
||||
@@ -408,6 +381,18 @@ export function SettingsPage() {
|
||||
updateCliTool(toolId, { [field]: value });
|
||||
};
|
||||
|
||||
const handleUpdateTags = (toolId: string, tags: string[]) => {
|
||||
updateCliTool(toolId, { tags });
|
||||
};
|
||||
|
||||
const handleUpdateAvailableModels = (toolId: string, availableModels: string[]) => {
|
||||
updateCliTool(toolId, { availableModels });
|
||||
};
|
||||
|
||||
const handleUpdateSettingsFile = (toolId: string, settingsFile: string | undefined) => {
|
||||
updateCliTool(toolId, { settingsFile });
|
||||
};
|
||||
|
||||
const handlePreferenceChange = (key: keyof UserPreferences, value: unknown) => {
|
||||
setUserPreferences({ [key]: value });
|
||||
};
|
||||
@@ -502,6 +487,9 @@ export function SettingsPage() {
|
||||
onToggleEnabled={() => handleToggleToolEnabled(toolId)}
|
||||
onSetDefault={() => handleSetDefaultTool(toolId)}
|
||||
onUpdateModel={(field, value) => handleUpdateModel(toolId, field, value)}
|
||||
onUpdateTags={(tags) => handleUpdateTags(toolId, tags)}
|
||||
onUpdateAvailableModels={(models) => handleUpdateAvailableModels(toolId, models)}
|
||||
onUpdateSettingsFile={(settingsFile) => handleUpdateSettingsFile(toolId, settingsFile)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -621,15 +609,6 @@ export function SettingsPage() {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Git Hooks */}
|
||||
<HooksSection />
|
||||
|
||||
{/* Rules */}
|
||||
<RulesSection />
|
||||
|
||||
{/* Index Manager */}
|
||||
<IndexManager />
|
||||
|
||||
{/* Reset Settings */}
|
||||
<Card className="p-6 border-destructive/50">
|
||||
<h2 className="text-lg font-semibold text-foreground flex items-center gap-2 mb-4">
|
||||
|
||||
@@ -308,6 +308,7 @@ export interface CliToolConfig {
|
||||
tags: string[];
|
||||
type: 'builtin' | 'cli-wrapper' | 'api-endpoint';
|
||||
settingsFile?: string;
|
||||
availableModels?: string[];
|
||||
}
|
||||
|
||||
export interface ApiEndpoints {
|
||||
|
||||
@@ -35,10 +35,17 @@ async function findProcessOnPort(port: number): Promise<string | null> {
|
||||
*/
|
||||
async function killProcess(pid: string): Promise<boolean> {
|
||||
try {
|
||||
await execAsync(`taskkill /PID ${pid} /F`);
|
||||
// Use PowerShell to avoid Git Bash path expansion issues with /PID
|
||||
await execAsync(`powershell -Command "Stop-Process -Id ${pid} -Force -ErrorAction Stop"`);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
// Fallback to taskkill via cmd
|
||||
try {
|
||||
await execAsync(`cmd /c "taskkill /PID ${pid} /F"`);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +59,7 @@ export async function stopCommand(options: StopOptions): Promise<void> {
|
||||
const force = options.force || false;
|
||||
|
||||
console.log(chalk.blue.bold('\n CCW Dashboard\n'));
|
||||
console.log(chalk.gray(` Checking server on port ${port}...`));
|
||||
console.log(chalk.gray(` Checking server on port ${port} and ${reactPort}...`));
|
||||
|
||||
try {
|
||||
// Try graceful shutdown via API first
|
||||
@@ -139,11 +146,18 @@ export async function stopCommand(options: StopOptions): Promise<void> {
|
||||
console.log(chalk.green(' Main server killed successfully!'));
|
||||
|
||||
// Also try to kill React frontend
|
||||
console.log(chalk.gray(` Checking React frontend on port ${reactPort}...`));
|
||||
const reactPid = await findProcessOnPort(reactPort);
|
||||
if (reactPid) {
|
||||
console.log(chalk.cyan(` Cleaning up React frontend on port ${reactPort}...`));
|
||||
await killProcess(reactPid);
|
||||
console.log(chalk.green(' React frontend stopped!'));
|
||||
console.log(chalk.cyan(` Cleaning up React frontend (PID: ${reactPid})...`));
|
||||
const reactKilled = await killProcess(reactPid);
|
||||
if (reactKilled) {
|
||||
console.log(chalk.green(' React frontend stopped!'));
|
||||
} else {
|
||||
console.log(chalk.yellow(' Failed to stop React frontend'));
|
||||
}
|
||||
} else {
|
||||
console.log(chalk.gray(' No React frontend running'));
|
||||
}
|
||||
|
||||
console.log(chalk.green.bold('\n All processes stopped successfully!\n'));
|
||||
@@ -153,6 +167,12 @@ export async function stopCommand(options: StopOptions): Promise<void> {
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
// Also check React frontend port
|
||||
const reactPid = await findProcessOnPort(reactPort);
|
||||
if (reactPid) {
|
||||
console.log(chalk.yellow(` React frontend running on port ${reactPort} (PID: ${reactPid})`));
|
||||
}
|
||||
|
||||
console.log(chalk.gray(`\n This is not a CCW server. Use --force to kill it:`));
|
||||
console.log(chalk.white(` ccw stop --force\n`));
|
||||
process.exit(0);
|
||||
|
||||
@@ -78,7 +78,7 @@ export async function viewCommand(options: ViewOptions): Promise<void> {
|
||||
const host = options.host || '127.0.0.1';
|
||||
const browserHost = host === '0.0.0.0' || host === '::' ? 'localhost' : host;
|
||||
// --new flag is shorthand for --frontend react
|
||||
const frontend = options.new ? 'react' : (options.frontend || 'both');
|
||||
const frontend = options.new ? 'react' : (options.frontend || 'js');
|
||||
|
||||
// Resolve workspace path
|
||||
let workspacePath = process.cwd();
|
||||
|
||||
@@ -8,6 +8,8 @@ const CCW_MCP_TOOLS = [
|
||||
{ name: 'edit_file', desc: 'Edit/replace content', core: true },
|
||||
{ name: 'read_file', desc: 'Read file contents', core: true },
|
||||
{ name: 'core_memory', desc: 'Core memory management', core: true },
|
||||
// Optional tools
|
||||
{ name: 'ask_question', desc: 'Interactive questions (A2UI)', core: false },
|
||||
];
|
||||
|
||||
// Get currently enabled tools from installed config (Claude)
|
||||
|
||||
Reference in New Issue
Block a user