@hienlh/ppm 0.9.0-beta.7 → 0.9.0-beta.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/bun.lock +5 -0
  3. package/dist/web/assets/{_basePickBy-CZovQgWd.js → _basePickBy-5PGDJbfF.js} +1 -1
  4. package/dist/web/assets/{_baseUniq-ClnvscgW.js → _baseUniq-BT4Ow4Kk.js} +1 -1
  5. package/dist/web/assets/{api-settings--eVrUeZM.js → api-settings-D21InCnR.js} +1 -1
  6. package/dist/web/assets/{arc-C2Qaz-ch.js → arc-BAOivWpI.js} +1 -1
  7. package/dist/web/assets/architecture-PBZL5I3N-DEO2f3VD.js +1 -0
  8. package/dist/web/assets/{architectureDiagram-2XIMDMQ5-Jq91S_rs.js → architectureDiagram-2XIMDMQ5-Z-4eN4za.js} +1 -1
  9. package/dist/web/assets/arrow-up--LjUXLEt.js +1 -0
  10. package/dist/web/assets/{blockDiagram-WCTKOSBZ-CKGufRTy.js → blockDiagram-WCTKOSBZ-BCLqzhuZ.js} +1 -1
  11. package/dist/web/assets/browser-tab-BEe89aSD.js +1 -0
  12. package/dist/web/assets/{c4Diagram-IC4MRINW-BNP2L9r_.js → c4Diagram-IC4MRINW-0Vp0Jeas.js} +1 -1
  13. package/dist/web/assets/channel-By7bn0Yq.js +1 -0
  14. package/dist/web/assets/chat-tab-9lqvWozA.js +7 -0
  15. package/dist/web/assets/chevron-right-CHnjJt4E.js +1 -0
  16. package/dist/web/assets/{chunk-4BX2VUAB-BptTlTyl.js → chunk-4BX2VUAB-D4tOov49.js} +1 -1
  17. package/dist/web/assets/{chunk-55IACEB6-C4mUdyio.js → chunk-55IACEB6-DJ6BynZ4.js} +1 -1
  18. package/dist/web/assets/{chunk-7E7YKBS2-6xAQfBwa.js → chunk-7E7YKBS2-CiyUJxNI.js} +1 -1
  19. package/dist/web/assets/{chunk-7R4GIKGN-DXaGAn_K.js → chunk-7R4GIKGN-Dv-4cAYn.js} +2 -2
  20. package/dist/web/assets/{chunk-C72U2L5F-DOtEiN5f.js → chunk-C72U2L5F-D21mS_6G.js} +1 -1
  21. package/dist/web/assets/{chunk-EGIJ26TM-D0KJTa_T.js → chunk-EGIJ26TM-DzqmU2Z7.js} +1 -1
  22. package/dist/web/assets/{chunk-FMBD7UC4-C_1aG0eb.js → chunk-FMBD7UC4-DXncblvW.js} +1 -1
  23. package/dist/web/assets/{chunk-GEFDOKGD-DwVPiYfW.js → chunk-GEFDOKGD-D-pKjlVd.js} +1 -1
  24. package/dist/web/assets/chunk-GLR3WWYH-DKikpoJM.js +2 -0
  25. package/dist/web/assets/chunk-HHEYEP7N-C7vxA5i9.js +1 -0
  26. package/dist/web/assets/{chunk-JSJVCQXG-BSrqCL_3.js → chunk-JSJVCQXG-99JzIdPr.js} +1 -1
  27. package/dist/web/assets/{chunk-KX2RTZJC-BCxGmbzy.js → chunk-KX2RTZJC-CRq1OBZv.js} +1 -1
  28. package/dist/web/assets/{chunk-KYZI473N-BKO5gMeU.js → chunk-KYZI473N-Bb0MCaIO.js} +1 -1
  29. package/dist/web/assets/{chunk-L3YUKLVL-3wBgkSvL.js → chunk-L3YUKLVL-C7qGJrfV.js} +1 -1
  30. package/dist/web/assets/{chunk-MX3YWQON-BgjSEzus.js → chunk-MX3YWQON-BpS_PtKp.js} +1 -1
  31. package/dist/web/assets/{chunk-NQ4KR5QH-DLrZwBEm.js → chunk-NQ4KR5QH-z_blpjxi.js} +1 -1
  32. package/dist/web/assets/{chunk-O4XLMI2P-BurQy8tt.js → chunk-O4XLMI2P-nDhi_cVu.js} +1 -1
  33. package/dist/web/assets/{chunk-OZEHJAEY-YTn24bGg.js → chunk-OZEHJAEY-BXhYx3nO.js} +1 -1
  34. package/dist/web/assets/{chunk-PQ6SQG4A-BxtUGYhW.js → chunk-PQ6SQG4A-TF58UVMU.js} +1 -1
  35. package/dist/web/assets/{chunk-PU5JKC2W-B66ELkQm.js → chunk-PU5JKC2W-ek7k4QVB.js} +1 -1
  36. package/dist/web/assets/chunk-QZHKN3VN-CYaTbeZf.js +1 -0
  37. package/dist/web/assets/{chunk-R5LLSJPH-euR2RxLN.js → chunk-R5LLSJPH-CFwSJijQ.js} +1 -1
  38. package/dist/web/assets/{chunk-WL4C6EOR-_2CBOJdI.js → chunk-WL4C6EOR-ByUrSRin.js} +1 -1
  39. package/dist/web/assets/{chunk-XIRO2GV7-kqQ0g6wW.js → chunk-XIRO2GV7-Djlmrely.js} +1 -1
  40. package/dist/web/assets/{chunk-XPW4576I-CtcaMb09.js → chunk-XPW4576I-BPQQBakK.js} +1 -1
  41. package/dist/web/assets/{chunk-XZSTWKYB-BYxFzZwS.js → chunk-XZSTWKYB-DxAOx4hG.js} +1 -1
  42. package/dist/web/assets/{chunk-YBOYWFTD-Dx_fX35n.js → chunk-YBOYWFTD-rQG3QH5s.js} +1 -1
  43. package/dist/web/assets/classDiagram-VBA2DB6C-BA8Nj-_C.js +1 -0
  44. package/dist/web/assets/classDiagram-v2-RAHNMMFH-DjYu-6mn.js +1 -0
  45. package/dist/web/assets/clone-LRxlvnMj.js +1 -0
  46. package/dist/web/assets/code-editor-COAIZx-B.js +2 -0
  47. package/dist/web/assets/{cose-bilkent-S5V4N54A-CHHjH2dV.js → cose-bilkent-S5V4N54A-B_AWZsOP.js} +1 -1
  48. package/dist/web/assets/csv-preview-DLqYtXxt.js +10 -0
  49. package/dist/web/assets/{dagre-CNtSxiE_.js → dagre-DHq9bhnd.js} +1 -1
  50. package/dist/web/assets/{dagre-KLK3FWXG-ChenfPp1.js → dagre-KLK3FWXG-BdJr7Byp.js} +1 -1
  51. package/dist/web/assets/database-viewer-aRR9n_Ui.js +1 -0
  52. package/dist/web/assets/{diagram-E7M64L7V-CzKYZM0Y.js → diagram-E7M64L7V-_db4pBVA.js} +1 -1
  53. package/dist/web/assets/{diagram-IFDJBPK2-ChB_paPo.js → diagram-IFDJBPK2-xKoeuiJx.js} +1 -1
  54. package/dist/web/assets/{diagram-P4PSJMXO-D1eW1dkL.js → diagram-P4PSJMXO-C8tjJsev.js} +1 -1
  55. package/dist/web/assets/diff-viewer-C4KMvpHr.js +4 -0
  56. package/dist/web/assets/dist-CALwEtco.js +41 -0
  57. package/dist/web/assets/dist-CVTST7Gc.js +1 -0
  58. package/dist/web/assets/dist-DGDPTxs1.js +13 -0
  59. package/dist/web/assets/{erDiagram-INFDFZHY-mCvUFSn6.js → erDiagram-INFDFZHY-BSh2z9Df.js} +1 -1
  60. package/dist/web/assets/{flowDiagram-PKNHOUZH-14ohZ1M1.js → flowDiagram-PKNHOUZH-oYaovqyp.js} +1 -1
  61. package/dist/web/assets/{ganttDiagram-A5KZAMGK-DIX0pLbk.js → ganttDiagram-A5KZAMGK-DmL26q2P.js} +1 -1
  62. package/dist/web/assets/git-graph-CfJjl4E3.js +1 -0
  63. package/dist/web/assets/gitGraph-HDMCJU4V-Bwna3and.js +1 -0
  64. package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-yEWZbdf_.js → gitGraphDiagram-K3NZZRJ6-CMoukSrY.js} +1 -1
  65. package/dist/web/assets/{graphlib-DhOZxqsh.js → graphlib-BcsNnGcW.js} +1 -1
  66. package/dist/web/assets/index-Db8uky1a.css +2 -0
  67. package/dist/web/assets/index-DxZuwBDe.js +37 -0
  68. package/dist/web/assets/info-3K5VOQVL-_vRxVNUm.js +1 -0
  69. package/dist/web/assets/infoDiagram-LFFYTUFH-DWwumDkq.js +2 -0
  70. package/dist/web/assets/{isEmpty-C0YYdhYj.js → isEmpty-bnrF3Qbc.js} +1 -1
  71. package/dist/web/assets/{ishikawaDiagram-PHBUUO56-olazD6dZ.js → ishikawaDiagram-PHBUUO56-D05_LyL7.js} +1 -1
  72. package/dist/web/assets/{journeyDiagram-4ABVD52K-CttDH9bb.js → journeyDiagram-4ABVD52K-B_L20qMe.js} +1 -1
  73. package/dist/web/assets/{kanban-definition-K7BYSVSG-BBXbI37U.js → kanban-definition-K7BYSVSG-CZ535BbZ.js} +1 -1
  74. package/dist/web/assets/keybindings-store-_uWVCZMv.js +1 -0
  75. package/dist/web/assets/lib-BQ34Db2e.js +4 -0
  76. package/dist/web/assets/{line-DBLLF7lH.js → line-CVvo3dRu.js} +1 -1
  77. package/dist/web/assets/{linear-BLFWatDe.js → linear-DP4mkX3m.js} +1 -1
  78. package/dist/web/assets/markdown-renderer-DklUd_Gv.js +69 -0
  79. package/dist/web/assets/{mermaid-parser.core-BKiGOTjR.js → mermaid-parser.core-C7UwoIh6.js} +2 -2
  80. package/dist/web/assets/{mindmap-definition-YRQLILUH-DoT7m4Sz.js → mindmap-definition-YRQLILUH-x0MTutJp.js} +1 -1
  81. package/dist/web/assets/{ordinal-CCj7PWgZ.js → ordinal-_K3x1fkz.js} +1 -1
  82. package/dist/web/assets/packet-RMMSAZCW-DY5PNnZU.js +1 -0
  83. package/dist/web/assets/pie-UPGHQEXC-BHncZutv.js +1 -0
  84. package/dist/web/assets/{pieDiagram-SKSYHLDU-Bkh2E4zE.js → pieDiagram-SKSYHLDU-C1Gjrtzy.js} +1 -1
  85. package/dist/web/assets/postgres-viewer-DEAvAyaX.js +1 -0
  86. package/dist/web/assets/{quadrantDiagram-337W2JSQ-B7zgALOL.js → quadrantDiagram-337W2JSQ-C8bzJCjQ.js} +1 -1
  87. package/dist/web/assets/radar-KQ55EAFF-DH0AOkUy.js +1 -0
  88. package/dist/web/assets/react-dom-Bpkvzu3U.js +1 -0
  89. package/dist/web/assets/{requirementDiagram-Z7DCOOCP-D_5GXNRo.js → requirementDiagram-Z7DCOOCP-pQyah6WB.js} +1 -1
  90. package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-BA9EFAAe.js → sankeyDiagram-WA2Y5GQK-T6RgG-N8.js} +1 -1
  91. package/dist/web/assets/{sequenceDiagram-2WXFIKYE-fyWIrHiG.js → sequenceDiagram-2WXFIKYE-BQDJ4CVs.js} +1 -1
  92. package/dist/web/assets/settings-tab-BQedc-No.js +1 -0
  93. package/dist/web/assets/sqlite-viewer-BPA5idzT.js +1 -0
  94. package/dist/web/assets/{stateDiagram-RAJIS63D-DfRBcaBu.js → stateDiagram-RAJIS63D-66vhiIuk.js} +1 -1
  95. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-BGVqj_g9.js +1 -0
  96. package/dist/web/assets/{tab-store-DcIBZTD4.js → tab-store-DhK6EpBT.js} +1 -1
  97. package/dist/web/assets/{terminal-tab-CAZtLK6i.js → terminal-tab-CqRuiIFn.js} +2 -2
  98. package/dist/web/assets/{timeline-definition-YZTLITO2-DYfwJ1jM.js → timeline-definition-YZTLITO2-DwZqB3nn.js} +1 -1
  99. package/dist/web/assets/treemap-KZPCXAKY-B2Xkyv-K.js +1 -0
  100. package/dist/web/assets/use-monaco-theme-Dcz3aLAE.js +11 -0
  101. package/dist/web/assets/{vennDiagram-LZ73GAT5-DqbKNRD9.js → vennDiagram-LZ73GAT5-s9Z71fz-.js} +1 -1
  102. package/dist/web/assets/{xychartDiagram-JWTSCODW-DhUL86qT.js → xychartDiagram-JWTSCODW-DRa_TH4B.js} +1 -1
  103. package/dist/web/index.html +12 -10
  104. package/dist/web/sw.js +1 -1
  105. package/docs/codebase-summary.md +17 -5
  106. package/docs/design-guidelines.md +21 -0
  107. package/docs/project-changelog.md +28 -1
  108. package/docs/project-roadmap.md +2 -2
  109. package/docs/system-architecture.md +151 -0
  110. package/package.json +2 -1
  111. package/src/providers/claude-agent-sdk.ts +32 -10
  112. package/src/server/index.ts +6 -0
  113. package/src/server/routes/chat.ts +4 -2
  114. package/src/server/routes/mcp.ts +84 -0
  115. package/src/server/ws/chat.ts +18 -12
  116. package/src/services/account-selector.service.ts +8 -2
  117. package/src/services/claude-usage.service.ts +24 -10
  118. package/src/services/db.service.ts +53 -6
  119. package/src/services/mcp-config.service.ts +102 -0
  120. package/src/services/supervisor.ts +12 -2
  121. package/src/types/mcp.ts +47 -0
  122. package/src/web/components/editor/code-editor.tsx +36 -26
  123. package/src/web/components/editor/csv-preview.tsx +228 -0
  124. package/src/web/components/editor/editor-breadcrumb.tsx +225 -0
  125. package/src/web/components/editor/editor-toolbar.tsx +74 -0
  126. package/src/web/components/settings/mcp-server-dialog.tsx +208 -0
  127. package/src/web/components/settings/mcp-settings-section.tsx +143 -0
  128. package/src/web/components/settings/settings-tab.tsx +5 -2
  129. package/src/web/lib/api-mcp.ts +38 -0
  130. package/src/web/lib/csv-parser.ts +134 -0
  131. package/dist/web/assets/architecture-PBZL5I3N-ChOahOB7.js +0 -1
  132. package/dist/web/assets/browser-tab-DAvH4mv0.js +0 -1
  133. package/dist/web/assets/channel-w7yboq56.js +0 -1
  134. package/dist/web/assets/chat-tab-WEBXxGgN.js +0 -7
  135. package/dist/web/assets/chunk-GLR3WWYH-D9pZakqr.js +0 -2
  136. package/dist/web/assets/chunk-HHEYEP7N-Dld5BpGB.js +0 -1
  137. package/dist/web/assets/chunk-QZHKN3VN-DwSXwtjH.js +0 -1
  138. package/dist/web/assets/classDiagram-VBA2DB6C-BpJ6Oog2.js +0 -1
  139. package/dist/web/assets/classDiagram-v2-RAHNMMFH-Bj8gIhkP.js +0 -1
  140. package/dist/web/assets/clone-BSi6cgDh.js +0 -1
  141. package/dist/web/assets/code-editor-B5sg_uJQ.js +0 -1
  142. package/dist/web/assets/database-viewer-CwtyWCkE.js +0 -1
  143. package/dist/web/assets/diff-viewer-CzE5M-Wd.js +0 -4
  144. package/dist/web/assets/dist-T0Vhi0Mh.js +0 -16
  145. package/dist/web/assets/git-graph-6yxCeeN9.js +0 -1
  146. package/dist/web/assets/gitGraph-HDMCJU4V-CEee2FCA.js +0 -1
  147. package/dist/web/assets/index-DE8b9u8F.css +0 -2
  148. package/dist/web/assets/index-wuWZBO9y.js +0 -37
  149. package/dist/web/assets/info-3K5VOQVL-ce_pi3En.js +0 -1
  150. package/dist/web/assets/infoDiagram-LFFYTUFH-BzqyoqXw.js +0 -2
  151. package/dist/web/assets/input-Brjz2Vv-.js +0 -41
  152. package/dist/web/assets/keybindings-store-mkBHnWN1.js +0 -1
  153. package/dist/web/assets/markdown-renderer-CxWxvrzT.js +0 -69
  154. package/dist/web/assets/packet-RMMSAZCW-CdYSLjRL.js +0 -1
  155. package/dist/web/assets/pie-UPGHQEXC-Bm5LiD-6.js +0 -1
  156. package/dist/web/assets/postgres-viewer-UP3yv9Yh.js +0 -1
  157. package/dist/web/assets/radar-KQ55EAFF-C4PnyG7_.js +0 -1
  158. package/dist/web/assets/settings-store-Bbhg_ptG.js +0 -2
  159. package/dist/web/assets/settings-tab-BoBXlVHe.js +0 -1
  160. package/dist/web/assets/sqlite-viewer-lzRVvM5j.js +0 -1
  161. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-CMN4M2Em.js +0 -1
  162. package/dist/web/assets/treemap-KZPCXAKY-2_y-mhkz.js +0 -1
  163. package/dist/web/assets/use-monaco-theme-vwto-Vlf.js +0 -11
  164. /package/dist/web/assets/{api-client-DpGMOZNf.js → api-client-BfBM3I7n.js} +0 -0
  165. /package/dist/web/assets/{array-BGFCBI0e.js → array-B9UHiPd-.js} +0 -0
  166. /package/dist/web/assets/{columns-2-ChOTgl3e.js → columns-2-DbesTfa7.js} +0 -0
  167. /package/dist/web/assets/{cytoscape.esm-Ccan6xou.js → cytoscape.esm-BW-DbntU.js} +0 -0
  168. /package/dist/web/assets/{defaultLocale-CRZydyG6.js → defaultLocale-5eAKkKJC.js} +0 -0
  169. /package/dist/web/assets/{dist-Cce3efmT.js → dist-CSJdAyA9.js} +0 -0
  170. /package/dist/web/assets/{init-B8gtcn7T.js → init-DlZdxViB.js} +0 -0
  171. /package/dist/web/assets/{isArrayLikeObject-B4pdpV8V.js → isArrayLikeObject-B_v2FtYn.js} +0 -0
  172. /package/dist/web/assets/{katex-Bbu770d9.js → katex-Bqvo_ZG0.js} +0 -0
  173. /package/dist/web/assets/{math-DwgHI-Cu.js → math-069Z4SuC.js} +0 -0
  174. /package/dist/web/assets/{path-DZF-JdEe.js → path-6uRLdFF7.js} +0 -0
  175. /package/dist/web/assets/{preload-helper-qlgyTAkD.js → preload-helper-Bf_JiD2A.js} +0 -0
  176. /package/dist/web/assets/{react-BGf7KNLk.js → react-SKk5z-bm.js} +0 -0
  177. /package/dist/web/assets/{rough.esm-VLpapkIG.js → rough.esm-JX0wREDd.js} +0 -0
  178. /package/dist/web/assets/{src-BoSBNdA_.js → src-BqX54PbV.js} +0 -0
  179. /package/dist/web/assets/{table-Yo02WRH-.js → table-CQVQM2SB.js} +0 -0
  180. /package/dist/web/assets/{tag-CaC1ng2E.js → tag-Q2dZiSPX.js} +0 -0
  181. /package/dist/web/assets/{utils-btZ8C8-R.js → utils-BNytJOb1.js} +0 -0
@@ -0,0 +1,225 @@
1
+ import { useMemo, useRef, useEffect } from "react";
2
+ import { ChevronRight, Folder, File, FileCode, FileJson, FileText, FileType } from "lucide-react";
3
+ import {
4
+ DropdownMenu,
5
+ DropdownMenuContent,
6
+ DropdownMenuItem,
7
+ DropdownMenuTrigger,
8
+ DropdownMenuSub,
9
+ DropdownMenuSubTrigger,
10
+ DropdownMenuSubContent,
11
+ } from "@/components/ui/dropdown-menu";
12
+ import { ScrollArea } from "@/components/ui/scroll-area";
13
+ import { useFileStore, type FileNode } from "@/stores/file-store";
14
+ import { useTabStore } from "@/stores/tab-store";
15
+ import { basename } from "@/lib/utils";
16
+
17
+ const ICON_MAP: Record<string, React.ComponentType<{ className?: string }>> = {
18
+ ts: FileCode, tsx: FileCode, js: FileCode, jsx: FileCode,
19
+ py: FileCode, rs: FileCode, go: FileCode, html: FileCode,
20
+ css: FileCode, scss: FileCode,
21
+ json: FileJson,
22
+ md: FileText, txt: FileText,
23
+ yaml: FileType, yml: FileType,
24
+ };
25
+
26
+ function getIcon(name: string, isDir: boolean) {
27
+ if (isDir) return Folder;
28
+ const ext = name.split(".").pop()?.toLowerCase() ?? "";
29
+ return ICON_MAP[ext] ?? File;
30
+ }
31
+
32
+ interface BreadcrumbSegment {
33
+ name: string;
34
+ fullPath: string;
35
+ node: FileNode | null;
36
+ siblings: FileNode[];
37
+ }
38
+
39
+ function walkTree(tree: FileNode[], segments: string[]): BreadcrumbSegment[] {
40
+ const result: BreadcrumbSegment[] = [];
41
+ let current: FileNode[] = tree;
42
+
43
+ for (let i = 0; i < segments.length; i++) {
44
+ const seg = segments[i]!;
45
+ const fullPath = segments.slice(0, i + 1).join("/");
46
+ const match = current.find((n) => n.name === seg);
47
+ result.push({
48
+ name: seg,
49
+ fullPath,
50
+ node: match ?? null,
51
+ siblings: current,
52
+ });
53
+ if (match?.children) {
54
+ current = match.children;
55
+ } else {
56
+ // Remaining segments have no tree data — add as plain
57
+ for (let j = i + 1; j < segments.length; j++) {
58
+ result.push({
59
+ name: segments[j]!,
60
+ fullPath: segments.slice(0, j + 1).join("/"),
61
+ node: null,
62
+ siblings: [],
63
+ });
64
+ }
65
+ break;
66
+ }
67
+ }
68
+ return result;
69
+ }
70
+
71
+ function sortNodes(nodes: FileNode[]): FileNode[] {
72
+ return [...nodes].sort((a, b) => {
73
+ if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
74
+ return a.name.localeCompare(b.name);
75
+ });
76
+ }
77
+
78
+ interface EditorBreadcrumbProps {
79
+ filePath: string;
80
+ projectName: string;
81
+ tabId: string;
82
+ className?: string;
83
+ }
84
+
85
+ export function EditorBreadcrumb({ filePath, projectName, tabId, className }: EditorBreadcrumbProps) {
86
+ const tree = useFileStore((s) => s.tree);
87
+ const { updateTab, openTab } = useTabStore();
88
+ const scrollRef = useRef<HTMLDivElement>(null);
89
+
90
+ const segments = useMemo(
91
+ () => walkTree(tree, filePath.split("/").filter(Boolean)),
92
+ [tree, filePath],
93
+ );
94
+
95
+ // Auto-scroll to rightmost segment
96
+ useEffect(() => {
97
+ if (scrollRef.current) {
98
+ scrollRef.current.scrollLeft = scrollRef.current.scrollWidth;
99
+ }
100
+ }, [segments]);
101
+
102
+ function handleFileClick(path: string, e: React.MouseEvent) {
103
+ const name = basename(path);
104
+ if (e.metaKey || e.ctrlKey) {
105
+ openTab({ type: "editor", title: name, metadata: { filePath: path, projectName }, projectId: projectName, closable: true });
106
+ } else {
107
+ updateTab(tabId, { title: name, metadata: { filePath: path, projectName } });
108
+ }
109
+ }
110
+
111
+ return (
112
+ <div ref={scrollRef} className={className}>
113
+ {segments.map((seg, i) => (
114
+ <div key={seg.fullPath} className="flex items-center shrink-0">
115
+ {i > 0 && <ChevronRight className="size-3 text-muted-foreground shrink-0 mx-0.5" />}
116
+ {seg.siblings.length > 0 ? (
117
+ <SegmentDropdown
118
+ segment={seg}
119
+ isLast={i === segments.length - 1}
120
+ projectName={projectName}
121
+ onFileClick={handleFileClick}
122
+ />
123
+ ) : (
124
+ <span className="text-xs text-muted-foreground px-1 py-0.5">{seg.name}</span>
125
+ )}
126
+ </div>
127
+ ))}
128
+ </div>
129
+ );
130
+ }
131
+
132
+ interface SegmentDropdownProps {
133
+ segment: BreadcrumbSegment;
134
+ isLast: boolean;
135
+ projectName: string;
136
+ onFileClick: (path: string, e: React.MouseEvent) => void;
137
+ }
138
+
139
+ function SegmentDropdown({ segment, isLast, projectName, onFileClick }: SegmentDropdownProps) {
140
+ const sorted = useMemo(() => sortNodes(segment.siblings), [segment.siblings]);
141
+
142
+ return (
143
+ <DropdownMenu>
144
+ <DropdownMenuTrigger asChild>
145
+ <button
146
+ type="button"
147
+ className={`text-xs px-1 py-0.5 rounded hover:bg-muted transition-colors truncate max-w-[120px] ${
148
+ isLast ? "text-foreground font-medium" : "text-muted-foreground"
149
+ }`}
150
+ >
151
+ {segment.name}
152
+ </button>
153
+ </DropdownMenuTrigger>
154
+ <DropdownMenuContent align="start" className="max-h-[300px] overflow-hidden p-0">
155
+ <ScrollArea className="max-h-[300px]">
156
+ <div className="p-1">
157
+ {sorted.map((node) => (
158
+ <NodeMenuItem
159
+ key={node.path}
160
+ node={node}
161
+ projectName={projectName}
162
+ activePath={segment.fullPath}
163
+ onFileClick={onFileClick}
164
+ />
165
+ ))}
166
+ </div>
167
+ </ScrollArea>
168
+ </DropdownMenuContent>
169
+ </DropdownMenu>
170
+ );
171
+ }
172
+
173
+ interface NodeMenuItemProps {
174
+ node: FileNode;
175
+ projectName: string;
176
+ activePath: string;
177
+ onFileClick: (path: string, e: React.MouseEvent) => void;
178
+ }
179
+
180
+ function NodeMenuItem({ node, projectName, activePath, onFileClick }: NodeMenuItemProps) {
181
+ const Icon = getIcon(node.name, node.type === "directory");
182
+ const isActive = node.path === activePath;
183
+
184
+ if (node.type === "directory" && node.children && node.children.length > 0) {
185
+ return (
186
+ <DropdownMenuSub>
187
+ <DropdownMenuSubTrigger className={`text-xs gap-1.5 ${isActive ? "bg-muted" : ""}`}>
188
+ <Icon className="size-3.5 shrink-0 text-muted-foreground" />
189
+ <span className="truncate">{node.name}</span>
190
+ </DropdownMenuSubTrigger>
191
+ <DropdownMenuSubContent className="max-h-[300px] overflow-hidden p-0">
192
+ <ScrollArea className="max-h-[300px]">
193
+ <div className="p-1">
194
+ {sortNodes(node.children).map((child) => (
195
+ <NodeMenuItem
196
+ key={child.path}
197
+ node={child}
198
+ projectName={projectName}
199
+ activePath={activePath}
200
+ onFileClick={onFileClick}
201
+ />
202
+ ))}
203
+ </div>
204
+ </ScrollArea>
205
+ </DropdownMenuSubContent>
206
+ </DropdownMenuSub>
207
+ );
208
+ }
209
+
210
+ return (
211
+ <DropdownMenuItem
212
+ className={`text-xs gap-1.5 cursor-pointer ${isActive ? "bg-muted" : ""}`}
213
+ onSelect={(e) => {
214
+ // onSelect doesn't give MouseEvent, use click handler for Ctrl detection
215
+ }}
216
+ onClick={(e) => {
217
+ if (node.type === "directory") return;
218
+ onFileClick(node.path, e);
219
+ }}
220
+ >
221
+ <Icon className="size-3.5 shrink-0 text-muted-foreground" />
222
+ <span className="truncate">{node.name}</span>
223
+ </DropdownMenuItem>
224
+ );
225
+ }
@@ -0,0 +1,74 @@
1
+ import { Code, Eye, WrapText, Table } from "lucide-react";
2
+
3
+ interface EditorToolbarProps {
4
+ ext: string;
5
+ mdMode?: "edit" | "preview";
6
+ onMdModeChange?: (mode: "edit" | "preview") => void;
7
+ csvMode?: "table" | "raw";
8
+ onCsvModeChange?: (mode: "table" | "raw") => void;
9
+ wordWrap: boolean;
10
+ onToggleWordWrap: () => void;
11
+ className?: string;
12
+ }
13
+
14
+ function ToolbarButton({
15
+ active,
16
+ onClick,
17
+ icon: Icon,
18
+ label,
19
+ }: {
20
+ active: boolean;
21
+ onClick: () => void;
22
+ icon: React.ComponentType<{ className?: string }>;
23
+ label: string;
24
+ }) {
25
+ return (
26
+ <button
27
+ type="button"
28
+ onClick={onClick}
29
+ className={`flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors ${
30
+ active ? "bg-muted text-foreground" : "text-muted-foreground hover:text-foreground"
31
+ }`}
32
+ >
33
+ <Icon className="size-3" />
34
+ <span className="hidden sm:inline">{label}</span>
35
+ </button>
36
+ );
37
+ }
38
+
39
+ export function EditorToolbar({
40
+ ext,
41
+ mdMode,
42
+ onMdModeChange,
43
+ csvMode,
44
+ onCsvModeChange,
45
+ wordWrap,
46
+ onToggleWordWrap,
47
+ className,
48
+ }: EditorToolbarProps) {
49
+ const isMarkdown = ext === "md" || ext === "mdx";
50
+ const isCsv = ext === "csv";
51
+
52
+ return (
53
+ <div className={className}>
54
+ {isMarkdown && onMdModeChange && (
55
+ <>
56
+ <ToolbarButton active={mdMode === "edit"} onClick={() => onMdModeChange("edit")} icon={Code} label="Edit" />
57
+ <ToolbarButton active={mdMode === "preview"} onClick={() => onMdModeChange("preview")} icon={Eye} label="Preview" />
58
+ </>
59
+ )}
60
+ {isCsv && onCsvModeChange && (
61
+ <>
62
+ <ToolbarButton active={csvMode === "table"} onClick={() => onCsvModeChange("table")} icon={Table} label="Table" />
63
+ <ToolbarButton active={csvMode === "raw"} onClick={() => onCsvModeChange("raw")} icon={Code} label="Raw" />
64
+ </>
65
+ )}
66
+ <ToolbarButton
67
+ active={wordWrap}
68
+ onClick={onToggleWordWrap}
69
+ icon={WrapText}
70
+ label="Wrap"
71
+ />
72
+ </div>
73
+ );
74
+ }
@@ -0,0 +1,208 @@
1
+ import { useState, useEffect } from "react";
2
+ import { Plus, X } from "lucide-react";
3
+ import { Button } from "@/components/ui/button";
4
+ import { Input } from "@/components/ui/input";
5
+ import {
6
+ Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription,
7
+ } from "@/components/ui/dialog";
8
+ import { addMcpServer, updateMcpServer, type McpServerEntry } from "@/lib/api-mcp";
9
+ import { validateMcpName, validateMcpConfig, type McpTransportType } from "../../../types/mcp";
10
+
11
+ interface Props {
12
+ open: boolean;
13
+ onClose: (saved?: boolean) => void;
14
+ editServer: McpServerEntry | null;
15
+ }
16
+
17
+ const TRANSPORTS: McpTransportType[] = ["stdio", "http", "sse"];
18
+
19
+ export function McpServerDialog({ open, onClose, editServer }: Props) {
20
+ const isEdit = !!editServer;
21
+ const [name, setName] = useState("");
22
+ const [transport, setTransport] = useState<McpTransportType>("stdio");
23
+ const [command, setCommand] = useState("");
24
+ const [args, setArgs] = useState("");
25
+ const [url, setUrl] = useState("");
26
+ const [kvPairs, setKvPairs] = useState<Array<{ key: string; value: string }>>([]);
27
+ const [saving, setSaving] = useState(false);
28
+ const [error, setError] = useState<string | null>(null);
29
+
30
+ // Reset form when dialog opens
31
+ useEffect(() => {
32
+ if (!open) return;
33
+ setError(null);
34
+ setSaving(false);
35
+ if (editServer) {
36
+ setName(editServer.name);
37
+ setTransport((editServer.transport as McpTransportType) || "stdio");
38
+ const c = editServer.config;
39
+ if ("command" in c) {
40
+ setCommand(c.command || "");
41
+ setArgs((c.args ?? []).join(" "));
42
+ setKvPairs(objToKv(c.env));
43
+ } else if ("url" in c) {
44
+ setUrl(c.url || "");
45
+ setKvPairs(objToKv(c.headers));
46
+ }
47
+ } else {
48
+ setName(""); setTransport("stdio"); setCommand(""); setArgs(""); setUrl("");
49
+ setKvPairs([]);
50
+ }
51
+ }, [open, editServer]);
52
+
53
+ const buildConfig = () => {
54
+ const kv = kvToObj(kvPairs);
55
+ if (transport === "stdio") {
56
+ return {
57
+ type: "stdio" as const,
58
+ command,
59
+ ...(args.trim() && { args: args.trim().split(/\s+/) }),
60
+ ...(Object.keys(kv).length > 0 && { env: kv }),
61
+ };
62
+ }
63
+ return {
64
+ type: transport,
65
+ url,
66
+ ...(Object.keys(kv).length > 0 && { headers: kv }),
67
+ };
68
+ };
69
+
70
+ const handleSave = async () => {
71
+ setError(null);
72
+ if (!isEdit) {
73
+ const nameErr = validateMcpName(name);
74
+ if (nameErr) { setError(nameErr); return; }
75
+ }
76
+ const config = buildConfig();
77
+ const configErrs = validateMcpConfig(config);
78
+ if (configErrs.length) { setError(configErrs.join("; ")); return; }
79
+
80
+ setSaving(true);
81
+ try {
82
+ if (isEdit) {
83
+ await updateMcpServer(name, config);
84
+ } else {
85
+ await addMcpServer(name, config);
86
+ }
87
+ onClose(true);
88
+ } catch (e: any) {
89
+ setError(e.message || "Save failed");
90
+ } finally {
91
+ setSaving(false);
92
+ }
93
+ };
94
+
95
+ const addKvPair = () => setKvPairs([...kvPairs, { key: "", value: "" }]);
96
+ const removeKvPair = (i: number) => setKvPairs(kvPairs.filter((_, idx) => idx !== i));
97
+ const updateKv = (i: number, field: "key" | "value", val: string) => {
98
+ setKvPairs(kvPairs.map((p, idx) =>
99
+ idx === i ? { key: field === "key" ? val : p.key, value: field === "value" ? val : p.value } : p
100
+ ));
101
+ };
102
+
103
+ const isStdio = transport === "stdio";
104
+ const kvLabel = isStdio ? "Environment Variables" : "Headers";
105
+
106
+ return (
107
+ <Dialog open={open} onOpenChange={(v) => { if (!v) onClose(); }}>
108
+ <DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-md">
109
+ <DialogHeader>
110
+ <DialogTitle className="text-sm">{isEdit ? "Edit MCP Server" : "Add MCP Server"}</DialogTitle>
111
+ <DialogDescription className="text-[11px]">
112
+ Configure a Model Context Protocol server connection.
113
+ </DialogDescription>
114
+ </DialogHeader>
115
+
116
+ <div className="space-y-3">
117
+ {/* Name */}
118
+ <div className="space-y-1">
119
+ <label className="text-[11px] font-medium text-muted-foreground">Name</label>
120
+ <Input
121
+ value={name} onChange={(e) => setName(e.target.value)}
122
+ placeholder="my-mcp-server" className="h-8 text-xs" disabled={isEdit}
123
+ />
124
+ </div>
125
+
126
+ {/* Transport toggle */}
127
+ <div className="space-y-1">
128
+ <label className="text-[11px] font-medium text-muted-foreground">Transport</label>
129
+ <div className="flex gap-1">
130
+ {TRANSPORTS.map((t) => (
131
+ <Button key={t} variant={transport === t ? "default" : "outline"}
132
+ size="sm" className="flex-1 h-7 text-xs cursor-pointer"
133
+ onClick={() => { setTransport(t); setKvPairs([]); setCommand(""); setArgs(""); setUrl(""); }}
134
+ >{t}</Button>
135
+ ))}
136
+ </div>
137
+ </div>
138
+
139
+ {/* Conditional fields */}
140
+ {isStdio ? (
141
+ <>
142
+ <div className="space-y-1">
143
+ <label className="text-[11px] font-medium text-muted-foreground">Command *</label>
144
+ <Input value={command} onChange={(e) => setCommand(e.target.value)}
145
+ placeholder="npx" className="h-8 text-xs" />
146
+ </div>
147
+ <div className="space-y-1">
148
+ <label className="text-[11px] font-medium text-muted-foreground">Arguments (space-separated)</label>
149
+ <Input value={args} onChange={(e) => setArgs(e.target.value)}
150
+ placeholder="@playwright/mcp@latest" className="h-8 text-xs" />
151
+ </div>
152
+ </>
153
+ ) : (
154
+ <div className="space-y-1">
155
+ <label className="text-[11px] font-medium text-muted-foreground">URL *</label>
156
+ <Input value={url} onChange={(e) => setUrl(e.target.value)}
157
+ placeholder="https://mcp.example.com" className="h-8 text-xs" />
158
+ </div>
159
+ )}
160
+
161
+ {/* Key-value pairs */}
162
+ <div className="space-y-1.5">
163
+ <label className="text-[11px] font-medium text-muted-foreground">{kvLabel}</label>
164
+ {kvPairs.map((pair, i) => (
165
+ <div key={i} className="flex gap-1 items-center">
166
+ <Input value={pair.key} onChange={(e) => updateKv(i, "key", e.target.value)}
167
+ placeholder="KEY" className="h-7 text-xs flex-1" />
168
+ <Input value={pair.value} onChange={(e) => updateKv(i, "value", e.target.value)}
169
+ placeholder="value" className="h-7 text-xs flex-1" />
170
+ <Button variant="ghost" size="icon" className="size-7 shrink-0 cursor-pointer"
171
+ onClick={() => removeKvPair(i)}>
172
+ <X className="size-3" />
173
+ </Button>
174
+ </div>
175
+ ))}
176
+ <Button variant="outline" size="sm" className="h-7 text-xs gap-1 cursor-pointer" onClick={addKvPair}>
177
+ <Plus className="size-3" /> Add {isStdio ? "Variable" : "Header"}
178
+ </Button>
179
+ </div>
180
+
181
+ {error && <p className="text-[11px] text-destructive">{error}</p>}
182
+ </div>
183
+
184
+ <DialogFooter>
185
+ <Button variant="outline" size="sm" className="h-8 text-xs cursor-pointer" onClick={() => onClose()}>
186
+ Cancel
187
+ </Button>
188
+ <Button size="sm" className="h-8 text-xs cursor-pointer" onClick={handleSave} disabled={saving}>
189
+ {saving ? "Saving..." : "Save"}
190
+ </Button>
191
+ </DialogFooter>
192
+ </DialogContent>
193
+ </Dialog>
194
+ );
195
+ }
196
+
197
+ function objToKv(obj?: Record<string, string>): Array<{ key: string; value: string }> {
198
+ if (!obj) return [];
199
+ return Object.entries(obj).map(([key, value]) => ({ key, value }));
200
+ }
201
+
202
+ function kvToObj(pairs: Array<{ key: string; value: string }>): Record<string, string> {
203
+ const result: Record<string, string> = {};
204
+ for (const { key, value } of pairs) {
205
+ if (key.trim()) result[key.trim()] = value;
206
+ }
207
+ return result;
208
+ }
@@ -0,0 +1,143 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+ import { Plus, Download, Trash2, Pencil, Server } from "lucide-react";
3
+ import { Button } from "@/components/ui/button";
4
+ import {
5
+ getMcpServers, deleteMcpServer, importMcpServers,
6
+ type McpServerEntry,
7
+ } from "@/lib/api-mcp";
8
+ import { McpServerDialog } from "./mcp-server-dialog";
9
+
10
+ export function McpSettingsSection() {
11
+ const [servers, setServers] = useState<McpServerEntry[]>([]);
12
+ const [loading, setLoading] = useState(true);
13
+ const [dialogOpen, setDialogOpen] = useState(false);
14
+ const [editingServer, setEditingServer] = useState<McpServerEntry | null>(null);
15
+ const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
16
+ const [importing, setImporting] = useState(false);
17
+ const [importMsg, setImportMsg] = useState<string | null>(null);
18
+
19
+ const fetchServers = useCallback(async () => {
20
+ try {
21
+ const data = await getMcpServers();
22
+ setServers(data);
23
+ } catch (e) {
24
+ console.error("Failed to load MCP servers:", e);
25
+ } finally {
26
+ setLoading(false);
27
+ }
28
+ }, []);
29
+
30
+ useEffect(() => { fetchServers(); }, [fetchServers]);
31
+
32
+ const handleDelete = async (name: string) => {
33
+ try {
34
+ await deleteMcpServer(name);
35
+ setDeleteConfirm(null);
36
+ fetchServers();
37
+ } catch (e) {
38
+ console.error("Failed to delete:", e);
39
+ }
40
+ };
41
+
42
+ const handleImport = async () => {
43
+ setImporting(true);
44
+ setImportMsg(null);
45
+ try {
46
+ const result = await importMcpServers();
47
+ setImportMsg(`Imported ${result.imported}, skipped ${result.skipped}`);
48
+ fetchServers();
49
+ } catch (e: any) {
50
+ setImportMsg(e.message || "Import failed");
51
+ } finally {
52
+ setImporting(false);
53
+ }
54
+ };
55
+
56
+ const handleDialogClose = (saved?: boolean) => {
57
+ setDialogOpen(false);
58
+ setEditingServer(null);
59
+ if (saved) fetchServers();
60
+ };
61
+
62
+ const openAdd = () => { setEditingServer(null); setDialogOpen(true); };
63
+ const openEdit = (s: McpServerEntry) => { setEditingServer(s); setDialogOpen(true); };
64
+
65
+ if (loading) {
66
+ return (
67
+ <div className="space-y-3">
68
+ {[1, 2, 3].map((i) => (
69
+ <div key={i} className="h-16 rounded-lg bg-muted animate-pulse" />
70
+ ))}
71
+ </div>
72
+ );
73
+ }
74
+
75
+ return (
76
+ <div className="space-y-3">
77
+ {servers.length === 0 ? (
78
+ <div className="text-center py-8 space-y-2">
79
+ <Server className="size-8 mx-auto text-muted-foreground" />
80
+ <p className="text-xs text-muted-foreground">
81
+ No MCP servers configured. Add one or import from Claude Code.
82
+ </p>
83
+ </div>
84
+ ) : (
85
+ <div className="space-y-2">
86
+ {servers.map((s) => (
87
+ <div key={s.name} className="rounded-lg border bg-card p-3 space-y-1.5">
88
+ <div className="flex items-center gap-2">
89
+ <span className="text-xs font-medium truncate flex-1">{s.name}</span>
90
+ <span className="text-[10px] px-1.5 py-0.5 rounded bg-muted text-muted-foreground shrink-0">
91
+ {s.transport}
92
+ </span>
93
+ </div>
94
+ <p className="text-[11px] text-muted-foreground truncate">
95
+ {serverPreview(s)}
96
+ </p>
97
+ <div className="flex justify-end gap-1.5">
98
+ {deleteConfirm === s.name ? (
99
+ <>
100
+ <span className="text-[11px] text-destructive self-center mr-1">Delete?</span>
101
+ <Button variant="destructive" size="sm" className="h-8 min-w-[44px] text-xs cursor-pointer" onClick={() => handleDelete(s.name)}>Yes</Button>
102
+ <Button variant="outline" size="sm" className="h-8 min-w-[44px] text-xs cursor-pointer" onClick={() => setDeleteConfirm(null)}>No</Button>
103
+ </>
104
+ ) : (
105
+ <>
106
+ <Button variant="ghost" size="sm" className="h-7 text-xs gap-1 cursor-pointer" onClick={() => openEdit(s)}>
107
+ <Pencil className="size-3" /> Edit
108
+ </Button>
109
+ <Button variant="ghost" size="sm" className="h-7 text-xs gap-1 text-destructive hover:text-destructive cursor-pointer" onClick={() => setDeleteConfirm(s.name)}>
110
+ <Trash2 className="size-3" /> Delete
111
+ </Button>
112
+ </>
113
+ )}
114
+ </div>
115
+ </div>
116
+ ))}
117
+ </div>
118
+ )}
119
+
120
+ {/* Actions — thumb zone */}
121
+ <div className="space-y-2 pt-1">
122
+ <Button className="w-full h-10 text-xs gap-1.5 cursor-pointer" onClick={openAdd}>
123
+ <Plus className="size-3.5" /> Add MCP Server
124
+ </Button>
125
+ <Button variant="outline" className="w-full h-10 text-xs gap-1.5 cursor-pointer" onClick={handleImport} disabled={importing}>
126
+ <Download className="size-3.5" /> {importing ? "Importing..." : "Import from Claude Code"}
127
+ </Button>
128
+ {importMsg && <p className="text-[11px] text-muted-foreground text-center">{importMsg}</p>}
129
+ </div>
130
+
131
+ <McpServerDialog open={dialogOpen} onClose={handleDialogClose} editServer={editingServer} />
132
+ </div>
133
+ );
134
+ }
135
+
136
+ function serverPreview(s: McpServerEntry): string {
137
+ const c = s.config;
138
+ if (s.transport === "stdio" || !("url" in c)) {
139
+ const stdio = c as { command?: string; args?: string[] };
140
+ return [stdio.command, ...(stdio.args ?? [])].join(" ");
141
+ }
142
+ return (c as { url?: string }).url ?? "";
143
+ }
@@ -1,7 +1,7 @@
1
1
  import { useState, useCallback, useRef } from "react";
2
2
  import {
3
3
  Moon, Sun, Monitor, Bell, BellOff, Check, ChevronRight, ArrowLeft,
4
- Bot, BellRing, Keyboard, Globe,
4
+ Bot, BellRing, Keyboard, Globe, Plug,
5
5
  } from "lucide-react";
6
6
  import { Button } from "@/components/ui/button";
7
7
  import { Input } from "@/components/ui/input";
@@ -13,6 +13,7 @@ import { AISettingsSection } from "./ai-settings-section";
13
13
  import { KeyboardShortcutsSection } from "./keyboard-shortcuts-section";
14
14
  import { TelegramSettingsSection } from "./telegram-settings-section";
15
15
  import { ProxySettingsSection } from "./proxy-settings-section";
16
+ import { McpSettingsSection } from "./mcp-settings-section";
16
17
  import { usePushNotification } from "@/hooks/use-push-notification";
17
18
 
18
19
  const THEME_OPTIONS: { value: Theme; label: string; icon: React.ElementType }[] = [
@@ -25,13 +26,14 @@ const pushSupported = "PushManager" in window && "serviceWorker" in navigator;
25
26
  const isIosNonPwa = /iPhone|iPad/.test(navigator.userAgent) &&
26
27
  !window.matchMedia("(display-mode: standalone)").matches;
27
28
 
28
- type SettingsCategory = "ai" | "notifications" | "proxy" | "shortcuts";
29
+ type SettingsCategory = "ai" | "notifications" | "proxy" | "shortcuts" | "mcp";
29
30
 
30
31
  const CATEGORIES: { value: SettingsCategory; label: string; subtitle: string; icon: React.ElementType }[] = [
31
32
  { value: "ai", label: "AI Provider", subtitle: "Model, execution mode, limits", icon: Bot },
32
33
  { value: "notifications", label: "Notifications", subtitle: "Push & Telegram alerts", icon: BellRing },
33
34
  { value: "proxy", label: "API Proxy", subtitle: "Expose accounts as Anthropic API", icon: Globe },
34
35
  { value: "shortcuts", label: "Keyboard Shortcuts", subtitle: "Customize key bindings", icon: Keyboard },
36
+ { value: "mcp", label: "MCP Servers", subtitle: "Model Context Protocol tools", icon: Plug },
35
37
  ];
36
38
 
37
39
  export function SettingsTab() {
@@ -84,6 +86,7 @@ export function SettingsTab() {
84
86
  {activeCategory === "notifications" && <NotificationsContent isSubscribed={isSubscribed} loading={loading} permission={permission} pushError={pushError} subscribe={subscribe} unsubscribe={unsubscribe} />}
85
87
  {activeCategory === "proxy" && <ProxySettingsSection />}
86
88
  {activeCategory === "shortcuts" && <KeyboardShortcutsSection />}
89
+ {activeCategory === "mcp" && <McpSettingsSection />}
87
90
  </div>
88
91
  </ScrollArea>
89
92
  </div>