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

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 +20 -0
  2. package/bun.lock +5 -0
  3. package/dist/web/assets/{_basePickBy-CZovQgWd.js → _basePickBy-3Xe18azI.js} +1 -1
  4. package/dist/web/assets/{_baseUniq-ClnvscgW.js → _baseUniq-Yy35llnn.js} +1 -1
  5. package/dist/web/assets/{api-settings--eVrUeZM.js → api-settings-CEMxVMCV.js} +1 -1
  6. package/dist/web/assets/{arc-C2Qaz-ch.js → arc-B9n1Gvb5.js} +1 -1
  7. package/dist/web/assets/architecture-PBZL5I3N-CFzkFKEL.js +1 -0
  8. package/dist/web/assets/{architectureDiagram-2XIMDMQ5-Jq91S_rs.js → architectureDiagram-2XIMDMQ5-DqAZP_F6.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-h3cDF2vI.js} +1 -1
  11. package/dist/web/assets/{browser-tab-DAvH4mv0.js → browser-tab-D1Zua62g.js} +1 -1
  12. package/dist/web/assets/{c4Diagram-IC4MRINW-BNP2L9r_.js → c4Diagram-IC4MRINW--pF1r5lr.js} +1 -1
  13. package/dist/web/assets/channel-C2fMafck.js +1 -0
  14. package/dist/web/assets/chat-tab-BnD27Vp9.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-C3aZvW7B.js} +1 -1
  17. package/dist/web/assets/{chunk-55IACEB6-C4mUdyio.js → chunk-55IACEB6-D5cABeB9.js} +1 -1
  18. package/dist/web/assets/{chunk-7E7YKBS2-6xAQfBwa.js → chunk-7E7YKBS2-CkFGv6Zs.js} +1 -1
  19. package/dist/web/assets/{chunk-7R4GIKGN-DXaGAn_K.js → chunk-7R4GIKGN-Dvbyu4Zw.js} +2 -2
  20. package/dist/web/assets/{chunk-C72U2L5F-DOtEiN5f.js → chunk-C72U2L5F-CtqKiH4q.js} +1 -1
  21. package/dist/web/assets/{chunk-EGIJ26TM-D0KJTa_T.js → chunk-EGIJ26TM-Cpr87sBR.js} +1 -1
  22. package/dist/web/assets/{chunk-FMBD7UC4-C_1aG0eb.js → chunk-FMBD7UC4-D23YVTOU.js} +1 -1
  23. package/dist/web/assets/{chunk-GEFDOKGD-DwVPiYfW.js → chunk-GEFDOKGD-tDjHsAUs.js} +1 -1
  24. package/dist/web/assets/chunk-GLR3WWYH-DBdWQ3zy.js +2 -0
  25. package/dist/web/assets/chunk-HHEYEP7N-BBw_z0fW.js +1 -0
  26. package/dist/web/assets/{chunk-JSJVCQXG-BSrqCL_3.js → chunk-JSJVCQXG-BBmymCjA.js} +1 -1
  27. package/dist/web/assets/{chunk-KX2RTZJC-BCxGmbzy.js → chunk-KX2RTZJC-DP36BDiU.js} +1 -1
  28. package/dist/web/assets/{chunk-KYZI473N-BKO5gMeU.js → chunk-KYZI473N-Djw13C-3.js} +1 -1
  29. package/dist/web/assets/{chunk-L3YUKLVL-3wBgkSvL.js → chunk-L3YUKLVL-HG_eMj_C.js} +1 -1
  30. package/dist/web/assets/{chunk-MX3YWQON-BgjSEzus.js → chunk-MX3YWQON-C2UEioMs.js} +1 -1
  31. package/dist/web/assets/{chunk-NQ4KR5QH-DLrZwBEm.js → chunk-NQ4KR5QH-DXUTQ-BL.js} +1 -1
  32. package/dist/web/assets/{chunk-O4XLMI2P-BurQy8tt.js → chunk-O4XLMI2P-BsUWb9d0.js} +1 -1
  33. package/dist/web/assets/{chunk-OZEHJAEY-YTn24bGg.js → chunk-OZEHJAEY-rG0P22U9.js} +1 -1
  34. package/dist/web/assets/{chunk-PQ6SQG4A-BxtUGYhW.js → chunk-PQ6SQG4A-DX0xW7kO.js} +1 -1
  35. package/dist/web/assets/{chunk-PU5JKC2W-B66ELkQm.js → chunk-PU5JKC2W-C7Gry6md.js} +1 -1
  36. package/dist/web/assets/chunk-QZHKN3VN-DFKFM_C1.js +1 -0
  37. package/dist/web/assets/{chunk-R5LLSJPH-euR2RxLN.js → chunk-R5LLSJPH-CMY0PkRK.js} +1 -1
  38. package/dist/web/assets/{chunk-WL4C6EOR-_2CBOJdI.js → chunk-WL4C6EOR-CXuQvlyu.js} +1 -1
  39. package/dist/web/assets/{chunk-XIRO2GV7-kqQ0g6wW.js → chunk-XIRO2GV7-DRJEb7Zb.js} +1 -1
  40. package/dist/web/assets/{chunk-XPW4576I-CtcaMb09.js → chunk-XPW4576I-BPEX8KhL.js} +1 -1
  41. package/dist/web/assets/{chunk-XZSTWKYB-BYxFzZwS.js → chunk-XZSTWKYB-Cb0iqycX.js} +1 -1
  42. package/dist/web/assets/{chunk-YBOYWFTD-Dx_fX35n.js → chunk-YBOYWFTD-av5aeHLq.js} +1 -1
  43. package/dist/web/assets/classDiagram-VBA2DB6C-Dp4Kk3Yb.js +1 -0
  44. package/dist/web/assets/classDiagram-v2-RAHNMMFH-D8IvcV_B.js +1 -0
  45. package/dist/web/assets/clone-B2hUek6n.js +1 -0
  46. package/dist/web/assets/code-editor-DGRg8stf.js +2 -0
  47. package/dist/web/assets/{cose-bilkent-S5V4N54A-CHHjH2dV.js → cose-bilkent-S5V4N54A-qudEiMCT.js} +1 -1
  48. package/dist/web/assets/csv-preview-DUbHtTAS.js +10 -0
  49. package/dist/web/assets/{dagre-CNtSxiE_.js → dagre-BFcnKyBF.js} +1 -1
  50. package/dist/web/assets/{dagre-KLK3FWXG-ChenfPp1.js → dagre-KLK3FWXG-C3O-MTLf.js} +1 -1
  51. package/dist/web/assets/database-viewer-DxCXZQcE.js +1 -0
  52. package/dist/web/assets/{diagram-E7M64L7V-CzKYZM0Y.js → diagram-E7M64L7V-DxPjK7_c.js} +1 -1
  53. package/dist/web/assets/{diagram-IFDJBPK2-ChB_paPo.js → diagram-IFDJBPK2-sqTog_XV.js} +1 -1
  54. package/dist/web/assets/{diagram-P4PSJMXO-D1eW1dkL.js → diagram-P4PSJMXO-hzmp0GHK.js} +1 -1
  55. package/dist/web/assets/diff-viewer-C1sDJG35.js +4 -0
  56. package/dist/web/assets/dist-CALwEtco.js +41 -0
  57. package/dist/web/assets/dist-DGDPTxs1.js +13 -0
  58. package/dist/web/assets/{erDiagram-INFDFZHY-mCvUFSn6.js → erDiagram-INFDFZHY-DLeYhAAT.js} +1 -1
  59. package/dist/web/assets/{flowDiagram-PKNHOUZH-14ohZ1M1.js → flowDiagram-PKNHOUZH-CRxlE9Sr.js} +1 -1
  60. package/dist/web/assets/{ganttDiagram-A5KZAMGK-DIX0pLbk.js → ganttDiagram-A5KZAMGK-BdjmoMLS.js} +1 -1
  61. package/dist/web/assets/git-graph-BDn-EiGE.js +1 -0
  62. package/dist/web/assets/gitGraph-HDMCJU4V-CNlas3Rz.js +1 -0
  63. package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-yEWZbdf_.js → gitGraphDiagram-K3NZZRJ6-BeHSX7kk.js} +1 -1
  64. package/dist/web/assets/{graphlib-DhOZxqsh.js → graphlib-Duh_bWLa.js} +1 -1
  65. package/dist/web/assets/index-Bun94AK3.js +37 -0
  66. package/dist/web/assets/index-Db8uky1a.css +2 -0
  67. package/dist/web/assets/info-3K5VOQVL-BDzTLc11.js +1 -0
  68. package/dist/web/assets/infoDiagram-LFFYTUFH-ZZmpgc6t.js +2 -0
  69. package/dist/web/assets/{isEmpty-C0YYdhYj.js → isEmpty-B9L-Ge-H.js} +1 -1
  70. package/dist/web/assets/{ishikawaDiagram-PHBUUO56-olazD6dZ.js → ishikawaDiagram-PHBUUO56-Cu0Rt1Ok.js} +1 -1
  71. package/dist/web/assets/{journeyDiagram-4ABVD52K-CttDH9bb.js → journeyDiagram-4ABVD52K-CgDI-UG4.js} +1 -1
  72. package/dist/web/assets/{kanban-definition-K7BYSVSG-BBXbI37U.js → kanban-definition-K7BYSVSG-h4g10UHL.js} +1 -1
  73. package/dist/web/assets/keybindings-store-COmK4Dte.js +1 -0
  74. package/dist/web/assets/lib-BeaDXEkP.js +4 -0
  75. package/dist/web/assets/{line-DBLLF7lH.js → line-B75-Rx70.js} +1 -1
  76. package/dist/web/assets/{linear-BLFWatDe.js → linear-Bcjv9FQt.js} +1 -1
  77. package/dist/web/assets/markdown-renderer-VIZB1GXE.js +69 -0
  78. package/dist/web/assets/{mermaid-parser.core-BKiGOTjR.js → mermaid-parser.core-8u2leTXI.js} +2 -2
  79. package/dist/web/assets/{mindmap-definition-YRQLILUH-DoT7m4Sz.js → mindmap-definition-YRQLILUH-BaOBwb-W.js} +1 -1
  80. package/dist/web/assets/{ordinal-CCj7PWgZ.js → ordinal-LFEjVtwQ.js} +1 -1
  81. package/dist/web/assets/packet-RMMSAZCW-IVa5F-go.js +1 -0
  82. package/dist/web/assets/pie-UPGHQEXC-CvXHKAzp.js +1 -0
  83. package/dist/web/assets/{pieDiagram-SKSYHLDU-Bkh2E4zE.js → pieDiagram-SKSYHLDU-At5Kz0KK.js} +1 -1
  84. package/dist/web/assets/postgres-viewer-CvQZ8gkh.js +1 -0
  85. package/dist/web/assets/{quadrantDiagram-337W2JSQ-B7zgALOL.js → quadrantDiagram-337W2JSQ-CdjGIDfw.js} +1 -1
  86. package/dist/web/assets/radar-KQ55EAFF-Z-Tr5wtS.js +1 -0
  87. package/dist/web/assets/react-dom-Bpkvzu3U.js +1 -0
  88. package/dist/web/assets/{requirementDiagram-Z7DCOOCP-D_5GXNRo.js → requirementDiagram-Z7DCOOCP-B9F_Cx_p.js} +1 -1
  89. package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-BA9EFAAe.js → sankeyDiagram-WA2Y5GQK-RolPi8bU.js} +1 -1
  90. package/dist/web/assets/{sequenceDiagram-2WXFIKYE-fyWIrHiG.js → sequenceDiagram-2WXFIKYE-DM-tMAhx.js} +1 -1
  91. package/dist/web/assets/settings-tab-RCnvZ29H.js +1 -0
  92. package/dist/web/assets/sqlite-viewer-CEEm2W4C.js +1 -0
  93. package/dist/web/assets/{stateDiagram-RAJIS63D-DfRBcaBu.js → stateDiagram-RAJIS63D-C4EMl6jf.js} +1 -1
  94. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-B-UjZch3.js +1 -0
  95. package/dist/web/assets/{tab-store-DcIBZTD4.js → tab-store-Bjh6bXFP.js} +1 -1
  96. package/dist/web/assets/{terminal-tab-CAZtLK6i.js → terminal-tab-XhKfb4ei.js} +2 -2
  97. package/dist/web/assets/{timeline-definition-YZTLITO2-DYfwJ1jM.js → timeline-definition-YZTLITO2-A4PN_Efm.js} +1 -1
  98. package/dist/web/assets/treemap-KZPCXAKY-C9TYRE0k.js +1 -0
  99. package/dist/web/assets/use-monaco-theme-0p0-84jJ.js +11 -0
  100. package/dist/web/assets/{vennDiagram-LZ73GAT5-DqbKNRD9.js → vennDiagram-LZ73GAT5-ywK7LMaH.js} +1 -1
  101. package/dist/web/assets/{xychartDiagram-JWTSCODW-DhUL86qT.js → xychartDiagram-JWTSCODW-DylHYNtJ.js} +1 -1
  102. package/dist/web/index.html +11 -10
  103. package/dist/web/sw.js +1 -1
  104. package/docs/codebase-summary.md +17 -5
  105. package/docs/design-guidelines.md +21 -0
  106. package/docs/project-changelog.md +28 -1
  107. package/docs/project-roadmap.md +2 -2
  108. package/docs/system-architecture.md +151 -0
  109. package/package.json +2 -1
  110. package/src/providers/claude-agent-sdk.ts +32 -10
  111. package/src/server/index.ts +6 -0
  112. package/src/server/routes/chat.ts +4 -2
  113. package/src/server/routes/mcp.ts +84 -0
  114. package/src/server/ws/chat.ts +18 -12
  115. package/src/services/account-selector.service.ts +8 -2
  116. package/src/services/account.service.ts +13 -8
  117. package/src/services/claude-usage.service.ts +37 -18
  118. package/src/services/cloud.service.ts +10 -6
  119. package/src/services/db.service.ts +53 -6
  120. package/src/services/mcp-config.service.ts +102 -0
  121. package/src/services/supervisor.ts +12 -2
  122. package/src/types/mcp.ts +47 -0
  123. package/src/web/components/editor/code-editor.tsx +36 -26
  124. package/src/web/components/editor/csv-preview.tsx +228 -0
  125. package/src/web/components/editor/editor-breadcrumb.tsx +216 -0
  126. package/src/web/components/editor/editor-toolbar.tsx +74 -0
  127. package/src/web/components/settings/mcp-server-dialog.tsx +208 -0
  128. package/src/web/components/settings/mcp-settings-section.tsx +143 -0
  129. package/src/web/components/settings/settings-tab.tsx +5 -2
  130. package/src/web/lib/api-mcp.ts +38 -0
  131. package/src/web/lib/csv-parser.ts +134 -0
  132. package/dist/web/assets/architecture-PBZL5I3N-ChOahOB7.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-BKIT_Qeg.js} +0 -0
  165. /package/dist/web/assets/{array-BGFCBI0e.js → array-DqLCdDFv.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-CWPXKqbJ.js} +0 -0
  168. /package/dist/web/assets/{defaultLocale-CRZydyG6.js → defaultLocale-CrJzLgRD.js} +0 -0
  169. /package/dist/web/assets/{dist-Cce3efmT.js → dist-Cep75xXf.js} +0 -0
  170. /package/dist/web/assets/{init-B8gtcn7T.js → init-C0r9Gk5G.js} +0 -0
  171. /package/dist/web/assets/{isArrayLikeObject-B4pdpV8V.js → isArrayLikeObject-CGBoxvCD.js} +0 -0
  172. /package/dist/web/assets/{katex-Bbu770d9.js → katex-DzXRfQ_m.js} +0 -0
  173. /package/dist/web/assets/{math-DwgHI-Cu.js → math-y9zN1W-N.js} +0 -0
  174. /package/dist/web/assets/{path-DZF-JdEe.js → path-DIKpVbHL.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-nHaDi0Kw.js} +0 -0
  178. /package/dist/web/assets/{src-BoSBNdA_.js → src-Dw4QhedI.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-DMiycH3O.js} +0 -0
@@ -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>
@@ -0,0 +1,38 @@
1
+ import { api } from "./api-client";
2
+ import type { McpServerConfig } from "../../types/mcp";
3
+
4
+ export interface McpServerEntry {
5
+ name: string;
6
+ transport: string;
7
+ config: McpServerConfig;
8
+ createdAt: string;
9
+ updatedAt: string;
10
+ }
11
+
12
+ export function getMcpServers(): Promise<McpServerEntry[]> {
13
+ return api.get<McpServerEntry[]>("/api/settings/mcp");
14
+ }
15
+
16
+ export function getMcpServer(name: string): Promise<McpServerConfig> {
17
+ return api.get<McpServerConfig>(`/api/settings/mcp/${encodeURIComponent(name)}`);
18
+ }
19
+
20
+ export function addMcpServer(name: string, config: McpServerConfig): Promise<{ name: string }> {
21
+ return api.post<{ name: string }>("/api/settings/mcp", { name, config });
22
+ }
23
+
24
+ export function updateMcpServer(name: string, config: McpServerConfig): Promise<{ name: string }> {
25
+ return api.put<{ name: string }>(`/api/settings/mcp/${encodeURIComponent(name)}`, config);
26
+ }
27
+
28
+ export function deleteMcpServer(name: string): Promise<void> {
29
+ return api.del(`/api/settings/mcp/${encodeURIComponent(name)}`);
30
+ }
31
+
32
+ export function importMcpServers(): Promise<{ imported: number; skipped: number }> {
33
+ return api.post<{ imported: number; skipped: number }>("/api/settings/mcp/import", {});
34
+ }
35
+
36
+ export function previewMcpImport(): Promise<{ available: boolean; servers: Record<string, McpServerConfig> }> {
37
+ return api.get<{ available: boolean; servers: Record<string, McpServerConfig> }>("/api/settings/mcp/import/preview");
38
+ }
@@ -0,0 +1,134 @@
1
+ /** Simple state-machine CSV parser handling quoted fields, embedded commas, newlines */
2
+
3
+ const enum State {
4
+ FIELD_START,
5
+ UNQUOTED,
6
+ QUOTED,
7
+ QUOTE_IN_QUOTED,
8
+ }
9
+
10
+ export interface CsvData {
11
+ headers: string[];
12
+ rows: string[][];
13
+ }
14
+
15
+ export function parseCsv(content: string): CsvData {
16
+ const rows: string[][] = [];
17
+ let row: string[] = [];
18
+ let field = "";
19
+ let state: State = State.FIELD_START;
20
+
21
+ for (let i = 0; i < content.length; i++) {
22
+ const ch = content[i]!;
23
+
24
+ switch (state) {
25
+ case State.FIELD_START:
26
+ if (ch === '"') {
27
+ state = State.QUOTED;
28
+ } else if (ch === ",") {
29
+ row.push(field);
30
+ field = "";
31
+ } else if (ch === "\r") {
32
+ // skip \r, handle \n next
33
+ } else if (ch === "\n") {
34
+ row.push(field);
35
+ field = "";
36
+ rows.push(row);
37
+ row = [];
38
+ } else {
39
+ field += ch;
40
+ state = State.UNQUOTED;
41
+ }
42
+ break;
43
+
44
+ case State.UNQUOTED:
45
+ if (ch === ",") {
46
+ row.push(field);
47
+ field = "";
48
+ state = State.FIELD_START;
49
+ } else if (ch === "\r") {
50
+ // skip
51
+ } else if (ch === "\n") {
52
+ row.push(field);
53
+ field = "";
54
+ rows.push(row);
55
+ row = [];
56
+ state = State.FIELD_START;
57
+ } else {
58
+ field += ch;
59
+ }
60
+ break;
61
+
62
+ case State.QUOTED:
63
+ if (ch === '"') {
64
+ state = State.QUOTE_IN_QUOTED;
65
+ } else {
66
+ field += ch;
67
+ }
68
+ break;
69
+
70
+ case State.QUOTE_IN_QUOTED:
71
+ if (ch === '"') {
72
+ // Escaped quote ""
73
+ field += '"';
74
+ state = State.QUOTED;
75
+ } else if (ch === ",") {
76
+ row.push(field);
77
+ field = "";
78
+ state = State.FIELD_START;
79
+ } else if (ch === "\r") {
80
+ // skip
81
+ } else if (ch === "\n") {
82
+ row.push(field);
83
+ field = "";
84
+ rows.push(row);
85
+ row = [];
86
+ state = State.FIELD_START;
87
+ } else {
88
+ // Malformed — treat closing quote as literal
89
+ field += ch;
90
+ state = State.UNQUOTED;
91
+ }
92
+ break;
93
+ }
94
+ }
95
+
96
+ // Flush last field/row
97
+ if (field || row.length > 0) {
98
+ row.push(field);
99
+ rows.push(row);
100
+ }
101
+
102
+ if (rows.length === 0) return { headers: [], rows: [] };
103
+
104
+ const headers = rows[0]!;
105
+ const dataRows = rows.slice(1);
106
+
107
+ // Normalize column count — pad short rows, truncate long ones
108
+ const colCount = headers.length;
109
+ for (let i = 0; i < dataRows.length; i++) {
110
+ const r = dataRows[i]!;
111
+ if (r.length < colCount) {
112
+ while (r.length < colCount) r.push("");
113
+ } else if (r.length > colCount) {
114
+ dataRows[i] = r.slice(0, colCount);
115
+ }
116
+ }
117
+
118
+ return { headers, rows: dataRows };
119
+ }
120
+
121
+ export function serializeCsv(headers: string[], rows: string[][]): string {
122
+ const escape = (val: string): string => {
123
+ if (val.includes(",") || val.includes('"') || val.includes("\n")) {
124
+ return `"${val.replace(/"/g, '""')}"`;
125
+ }
126
+ return val;
127
+ };
128
+
129
+ const lines = [headers.map(escape).join(",")];
130
+ for (const row of rows) {
131
+ lines.push(row.map(escape).join(","));
132
+ }
133
+ return lines.join("\n");
134
+ }
@@ -1 +0,0 @@
1
- import"./chunk-XZSTWKYB-BYxFzZwS.js";import{n as e}from"./chunk-R5LLSJPH-euR2RxLN.js";export{e as createArchitectureServices};
@@ -1 +0,0 @@
1
- import{it as e,rt as t}from"./chunk-7R4GIKGN-DXaGAn_K.js";var n=(n,r)=>e.lang.round(t.parse(n)[r]);export{n as t};