@hienlh/ppm 0.8.87 → 0.8.89

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 (190) hide show
  1. package/CHANGELOG.md +75 -40
  2. package/dist/web/assets/{_basePickBy-5PGDJbfF.js → _basePickBy-3Xe18azI.js} +1 -1
  3. package/dist/web/assets/{_baseUniq-BT4Ow4Kk.js → _baseUniq-Yy35llnn.js} +1 -1
  4. package/dist/web/assets/api-settings-Bid0NHuI.js +1 -0
  5. package/dist/web/assets/{arc-BAOivWpI.js → arc-B9n1Gvb5.js} +1 -1
  6. package/dist/web/assets/architecture-PBZL5I3N-CFzkFKEL.js +1 -0
  7. package/dist/web/assets/{architectureDiagram-2XIMDMQ5-DWBCPMLF.js → architectureDiagram-2XIMDMQ5-DqAZP_F6.js} +1 -1
  8. package/dist/web/assets/{blockDiagram-WCTKOSBZ-TEF8Ally.js → blockDiagram-WCTKOSBZ-h3cDF2vI.js} +1 -1
  9. package/dist/web/assets/{browser-tab-DaHGm_0i.js → browser-tab-DSWumOSG.js} +1 -1
  10. package/dist/web/assets/{c4Diagram-IC4MRINW-dV22iAsY.js → c4Diagram-IC4MRINW--pF1r5lr.js} +1 -1
  11. package/dist/web/assets/channel-C2fMafck.js +1 -0
  12. package/dist/web/assets/chat-tab-Ccwf-c6M.js +8 -0
  13. package/dist/web/assets/{chunk-4BX2VUAB-D4tOov49.js → chunk-4BX2VUAB-C3aZvW7B.js} +1 -1
  14. package/dist/web/assets/{chunk-55IACEB6-DJ6BynZ4.js → chunk-55IACEB6-D5cABeB9.js} +1 -1
  15. package/dist/web/assets/{chunk-7E7YKBS2-CiyUJxNI.js → chunk-7E7YKBS2-CkFGv6Zs.js} +1 -1
  16. package/dist/web/assets/{chunk-7R4GIKGN-BbIFzsIv.js → chunk-7R4GIKGN-Dvbyu4Zw.js} +2 -2
  17. package/dist/web/assets/{chunk-C72U2L5F-D21mS_6G.js → chunk-C72U2L5F-CtqKiH4q.js} +1 -1
  18. package/dist/web/assets/{chunk-EGIJ26TM-DzqmU2Z7.js → chunk-EGIJ26TM-Cpr87sBR.js} +1 -1
  19. package/dist/web/assets/{chunk-FMBD7UC4-DXncblvW.js → chunk-FMBD7UC4-D23YVTOU.js} +1 -1
  20. package/dist/web/assets/{chunk-GEFDOKGD-BbQkJu8C.js → chunk-GEFDOKGD-tDjHsAUs.js} +1 -1
  21. package/dist/web/assets/chunk-GLR3WWYH-DBdWQ3zy.js +2 -0
  22. package/dist/web/assets/chunk-HHEYEP7N-BBw_z0fW.js +1 -0
  23. package/dist/web/assets/{chunk-JSJVCQXG-23tyvw8k.js → chunk-JSJVCQXG-BBmymCjA.js} +1 -1
  24. package/dist/web/assets/{chunk-KX2RTZJC-sQ0o-39C.js → chunk-KX2RTZJC-DP36BDiU.js} +1 -1
  25. package/dist/web/assets/{chunk-KYZI473N-BcUZNnwd.js → chunk-KYZI473N-Djw13C-3.js} +1 -1
  26. package/dist/web/assets/{chunk-L3YUKLVL-C7qGJrfV.js → chunk-L3YUKLVL-HG_eMj_C.js} +1 -1
  27. package/dist/web/assets/{chunk-MX3YWQON-BpS_PtKp.js → chunk-MX3YWQON-C2UEioMs.js} +1 -1
  28. package/dist/web/assets/{chunk-NQ4KR5QH-wMgTlP7f.js → chunk-NQ4KR5QH-DXUTQ-BL.js} +1 -1
  29. package/dist/web/assets/{chunk-O4XLMI2P-JC6EGoUz.js → chunk-O4XLMI2P-BsUWb9d0.js} +1 -1
  30. package/dist/web/assets/{chunk-OZEHJAEY-BXhYx3nO.js → chunk-OZEHJAEY-rG0P22U9.js} +1 -1
  31. package/dist/web/assets/{chunk-PQ6SQG4A-D6BTbCQw.js → chunk-PQ6SQG4A-DX0xW7kO.js} +1 -1
  32. package/dist/web/assets/{chunk-PU5JKC2W-Dw8ClWch.js → chunk-PU5JKC2W-C7Gry6md.js} +1 -1
  33. package/dist/web/assets/chunk-QZHKN3VN-DFKFM_C1.js +1 -0
  34. package/dist/web/assets/{chunk-R5LLSJPH-CFwSJijQ.js → chunk-R5LLSJPH-CMY0PkRK.js} +1 -1
  35. package/dist/web/assets/{chunk-WL4C6EOR-DfofndiH.js → chunk-WL4C6EOR-CXuQvlyu.js} +1 -1
  36. package/dist/web/assets/{chunk-XIRO2GV7-Djlmrely.js → chunk-XIRO2GV7-DRJEb7Zb.js} +1 -1
  37. package/dist/web/assets/{chunk-XPW4576I-BPQQBakK.js → chunk-XPW4576I-BPEX8KhL.js} +1 -1
  38. package/dist/web/assets/{chunk-XZSTWKYB-DxAOx4hG.js → chunk-XZSTWKYB-Cb0iqycX.js} +1 -1
  39. package/dist/web/assets/{chunk-YBOYWFTD-CeU4Q-xC.js → chunk-YBOYWFTD-av5aeHLq.js} +1 -1
  40. package/dist/web/assets/classDiagram-VBA2DB6C-Dp4Kk3Yb.js +1 -0
  41. package/dist/web/assets/classDiagram-v2-RAHNMMFH-D8IvcV_B.js +1 -0
  42. package/dist/web/assets/clone-B2hUek6n.js +1 -0
  43. package/dist/web/assets/code-editor-DLTcPb55.js +2 -0
  44. package/dist/web/assets/{cose-bilkent-S5V4N54A-B_AWZsOP.js → cose-bilkent-S5V4N54A-qudEiMCT.js} +1 -1
  45. package/dist/web/assets/{csv-preview-DLqYtXxt.js → csv-preview-DUbHtTAS.js} +1 -1
  46. package/dist/web/assets/{dagre-Dbb5k38K.js → dagre-BFcnKyBF.js} +1 -1
  47. package/dist/web/assets/{dagre-KLK3FWXG-BH7aWGRP.js → dagre-KLK3FWXG-C3O-MTLf.js} +1 -1
  48. package/dist/web/assets/{database-viewer-DXk79Nel.js → database-viewer-BrpPlYG7.js} +1 -1
  49. package/dist/web/assets/{diagram-E7M64L7V-B1Qz70Do.js → diagram-E7M64L7V-DxPjK7_c.js} +1 -1
  50. package/dist/web/assets/{diagram-IFDJBPK2-k55eVqVU.js → diagram-IFDJBPK2-sqTog_XV.js} +1 -1
  51. package/dist/web/assets/{diagram-P4PSJMXO-BkfNRc9U.js → diagram-P4PSJMXO-hzmp0GHK.js} +1 -1
  52. package/dist/web/assets/diff-viewer-Dx96kcTu.js +4 -0
  53. package/dist/web/assets/{erDiagram-INFDFZHY-CKzVujYI.js → erDiagram-INFDFZHY-DLeYhAAT.js} +1 -1
  54. package/dist/web/assets/{flowDiagram-PKNHOUZH-DIqcTrDV.js → flowDiagram-PKNHOUZH-CRxlE9Sr.js} +1 -1
  55. package/dist/web/assets/{ganttDiagram-A5KZAMGK-D4v7ZbVE.js → ganttDiagram-A5KZAMGK-BdjmoMLS.js} +1 -1
  56. package/dist/web/assets/git-graph-CoN6voTp.js +1 -0
  57. package/dist/web/assets/gitGraph-HDMCJU4V-CNlas3Rz.js +1 -0
  58. package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-BTXo57mF.js → gitGraphDiagram-K3NZZRJ6-BeHSX7kk.js} +1 -1
  59. package/dist/web/assets/{graphlib-BcsNnGcW.js → graphlib-Duh_bWLa.js} +1 -1
  60. package/dist/web/assets/index-CtbNK_ih.css +2 -0
  61. package/dist/web/assets/index-DRdx_Wqn.js +37 -0
  62. package/dist/web/assets/info-3K5VOQVL-BDzTLc11.js +1 -0
  63. package/dist/web/assets/infoDiagram-LFFYTUFH-ZZmpgc6t.js +2 -0
  64. package/dist/web/assets/{isEmpty-bnrF3Qbc.js → isEmpty-B9L-Ge-H.js} +1 -1
  65. package/dist/web/assets/{ishikawaDiagram-PHBUUO56-BOyvKMmB.js → ishikawaDiagram-PHBUUO56-Cu0Rt1Ok.js} +1 -1
  66. package/dist/web/assets/{journeyDiagram-4ABVD52K-ufoasAy6.js → journeyDiagram-4ABVD52K-CgDI-UG4.js} +1 -1
  67. package/dist/web/assets/{kanban-definition-K7BYSVSG-Bi0UTUeN.js → kanban-definition-K7BYSVSG-h4g10UHL.js} +1 -1
  68. package/dist/web/assets/keybindings-store-DHGoLYnP.js +1 -0
  69. package/dist/web/assets/{line-B78g-52T.js → line-B75-Rx70.js} +1 -1
  70. package/dist/web/assets/{linear-DP4mkX3m.js → linear-Bcjv9FQt.js} +1 -1
  71. package/dist/web/assets/{markdown-renderer-Brj8_LQM.js → markdown-renderer-BqsXIW9n.js} +5 -5
  72. package/dist/web/assets/{mermaid-parser.core-DMIWdgEW.js → mermaid-parser.core-8u2leTXI.js} +2 -2
  73. package/dist/web/assets/{mindmap-definition-YRQLILUH-BsfWvIoO.js → mindmap-definition-YRQLILUH-BaOBwb-W.js} +1 -1
  74. package/dist/web/assets/{ordinal-_K3x1fkz.js → ordinal-LFEjVtwQ.js} +1 -1
  75. package/dist/web/assets/packet-RMMSAZCW-IVa5F-go.js +1 -0
  76. package/dist/web/assets/pie-UPGHQEXC-CvXHKAzp.js +1 -0
  77. package/dist/web/assets/{pieDiagram-SKSYHLDU-WP0XXw51.js → pieDiagram-SKSYHLDU-At5Kz0KK.js} +1 -1
  78. package/dist/web/assets/{postgres-viewer-CwkTGmqy.js → postgres-viewer-Lw8xaGfc.js} +1 -1
  79. package/dist/web/assets/{quadrantDiagram-337W2JSQ-FHMogtsh.js → quadrantDiagram-337W2JSQ-CdjGIDfw.js} +1 -1
  80. package/dist/web/assets/radar-KQ55EAFF-Z-Tr5wtS.js +1 -0
  81. package/dist/web/assets/{requirementDiagram-Z7DCOOCP-BatTxyWb.js → requirementDiagram-Z7DCOOCP-B9F_Cx_p.js} +1 -1
  82. package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-ClJuW3Hv.js → sankeyDiagram-WA2Y5GQK-RolPi8bU.js} +1 -1
  83. package/dist/web/assets/{sequenceDiagram-2WXFIKYE-ByxQqGgs.js → sequenceDiagram-2WXFIKYE-DM-tMAhx.js} +1 -1
  84. package/dist/web/assets/settings-tab-DDCC58we.js +1 -0
  85. package/dist/web/assets/{sqlite-viewer-CFYTwgA8.js → sqlite-viewer-DECA802J.js} +1 -1
  86. package/dist/web/assets/{stateDiagram-RAJIS63D-f8opcZNY.js → stateDiagram-RAJIS63D-C4EMl6jf.js} +1 -1
  87. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-B-UjZch3.js +1 -0
  88. package/dist/web/assets/{tab-store-BJw7OCmy.js → tab-store--SlERlDs.js} +1 -1
  89. package/dist/web/assets/{terminal-tab-CCDLZA5Y.js → terminal-tab-DneNM6WP.js} +2 -2
  90. package/dist/web/assets/{timeline-definition-YZTLITO2-58BlOSf9.js → timeline-definition-YZTLITO2-A4PN_Efm.js} +1 -1
  91. package/dist/web/assets/treemap-KZPCXAKY-C9TYRE0k.js +1 -0
  92. package/dist/web/assets/{use-monaco-theme-CNzekTN3.js → use-monaco-theme-CrtYAJMR.js} +1 -1
  93. package/dist/web/assets/{vennDiagram-LZ73GAT5-BOSy9ma9.js → vennDiagram-LZ73GAT5-ywK7LMaH.js} +1 -1
  94. package/dist/web/assets/{xychartDiagram-JWTSCODW-z5MVJauZ.js → xychartDiagram-JWTSCODW-DylHYNtJ.js} +1 -1
  95. package/dist/web/index.html +10 -11
  96. package/dist/web/sw.js +1 -1
  97. package/docs/code-standards.md +155 -0
  98. package/docs/codebase-summary.md +261 -95
  99. package/docs/project-changelog.md +38 -3
  100. package/docs/project-roadmap.md +2 -2
  101. package/docs/streaming-input-guide.md +267 -0
  102. package/docs/system-architecture.md +151 -0
  103. package/package.json +1 -1
  104. package/snapshot-state.md +1526 -0
  105. package/src/providers/claude-agent-sdk.ts +244 -102
  106. package/src/providers/cli-provider-base.ts +238 -0
  107. package/src/providers/cursor-cli/cursor-event-mapper.ts +85 -0
  108. package/src/providers/cursor-cli/cursor-history.ts +207 -0
  109. package/src/providers/cursor-cli/cursor-provider.ts +146 -0
  110. package/src/providers/mock-provider.ts +1 -1
  111. package/src/providers/provider.interface.ts +1 -0
  112. package/src/providers/registry.ts +43 -4
  113. package/src/server/index.ts +6 -0
  114. package/src/server/routes/chat.ts +14 -3
  115. package/src/server/routes/mcp.ts +84 -0
  116. package/src/server/routes/settings.ts +14 -0
  117. package/src/server/ws/chat.ts +127 -81
  118. package/src/services/account.service.ts +2 -2
  119. package/src/services/chat.service.ts +10 -15
  120. package/src/services/claude-usage.service.ts +2 -7
  121. package/src/services/db.service.ts +8 -0
  122. package/src/services/mcp-config.service.ts +111 -0
  123. package/src/types/api.ts +1 -1
  124. package/src/types/chat.ts +23 -2
  125. package/src/types/config.ts +33 -11
  126. package/src/types/mcp.ts +47 -0
  127. package/src/utils/ndjson-line-parser.ts +36 -0
  128. package/src/web/components/chat/chat-history-bar.tsx +48 -29
  129. package/src/web/components/chat/chat-tab.tsx +29 -24
  130. package/src/web/components/chat/message-input.tsx +64 -5
  131. package/src/web/components/chat/provider-selector.tsx +150 -0
  132. package/src/web/components/chat/session-picker.tsx +3 -1
  133. package/src/web/components/chat/usage-badge.tsx +58 -8
  134. package/src/web/components/settings/ai-settings-section.tsx +196 -137
  135. package/src/web/components/settings/mcp-server-dialog.tsx +208 -0
  136. package/src/web/components/settings/mcp-settings-section.tsx +143 -0
  137. package/src/web/components/settings/settings-tab.tsx +5 -2
  138. package/src/web/hooks/use-chat.ts +32 -15
  139. package/src/web/lib/api-mcp.ts +38 -0
  140. package/test-tokens.mjs +212 -0
  141. package/.claude.bak/agent-memory/tester/MEMORY.md +0 -3
  142. package/.claude.bak/agent-memory/tester/project-ppm-test-conventions.md +0 -32
  143. package/dist/web/assets/api-settings-Bx1GaNmQ.js +0 -1
  144. package/dist/web/assets/architecture-PBZL5I3N-DEO2f3VD.js +0 -1
  145. package/dist/web/assets/channel-wrd-NHWf.js +0 -1
  146. package/dist/web/assets/chat-tab-BDYE0KHF.js +0 -8
  147. package/dist/web/assets/chunk-GLR3WWYH-CzYx4w-r.js +0 -2
  148. package/dist/web/assets/chunk-HHEYEP7N-HRhYy3kG.js +0 -1
  149. package/dist/web/assets/chunk-QZHKN3VN-CYaTbeZf.js +0 -1
  150. package/dist/web/assets/classDiagram-VBA2DB6C-lse8oZoJ.js +0 -1
  151. package/dist/web/assets/classDiagram-v2-RAHNMMFH-CxkwuInd.js +0 -1
  152. package/dist/web/assets/clone-LRxlvnMj.js +0 -1
  153. package/dist/web/assets/code-editor-DTA3c9Y8.js +0 -2
  154. package/dist/web/assets/diff-viewer-HhIcsOQE.js +0 -4
  155. package/dist/web/assets/git-graph-CQtWu8yE.js +0 -1
  156. package/dist/web/assets/gitGraph-HDMCJU4V-Bwna3and.js +0 -1
  157. package/dist/web/assets/index-CgQXpBb_.css +0 -2
  158. package/dist/web/assets/index-DEeeRoka.js +0 -37
  159. package/dist/web/assets/info-3K5VOQVL-_vRxVNUm.js +0 -1
  160. package/dist/web/assets/infoDiagram-LFFYTUFH-B1CX0pbC.js +0 -2
  161. package/dist/web/assets/input-BglMT33g.js +0 -1
  162. package/dist/web/assets/keybindings-store-1CJ7VX57.js +0 -1
  163. package/dist/web/assets/packet-RMMSAZCW-DY5PNnZU.js +0 -1
  164. package/dist/web/assets/pie-UPGHQEXC-BHncZutv.js +0 -1
  165. package/dist/web/assets/radar-KQ55EAFF-DH0AOkUy.js +0 -1
  166. package/dist/web/assets/settings-tab-BDE1MsIh.js +0 -1
  167. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-DrxVDY9q.js +0 -1
  168. package/dist/web/assets/treemap-KZPCXAKY-B2Xkyv-K.js +0 -1
  169. /package/dist/web/assets/{api-client-BfBM3I7n.js → api-client-BKIT_Qeg.js} +0 -0
  170. /package/dist/web/assets/{array-B9UHiPd-.js → array-DqLCdDFv.js} +0 -0
  171. /package/dist/web/assets/{chevron-right-DeV0ehiG.js → chevron-right-CHnjJt4E.js} +0 -0
  172. /package/dist/web/assets/{columns-2-DpsNbZOc.js → columns-2-DbesTfa7.js} +0 -0
  173. /package/dist/web/assets/{cytoscape.esm-BW-DbntU.js → cytoscape.esm-CWPXKqbJ.js} +0 -0
  174. /package/dist/web/assets/{defaultLocale-5eAKkKJC.js → defaultLocale-CrJzLgRD.js} +0 -0
  175. /package/dist/web/assets/{dist-lF8CoYII.js → dist-CALwEtco.js} +0 -0
  176. /package/dist/web/assets/{dist-CSJdAyA9.js → dist-Cep75xXf.js} +0 -0
  177. /package/dist/web/assets/{dist-DylI9XxN.js → dist-DGDPTxs1.js} +0 -0
  178. /package/dist/web/assets/{init-DlZdxViB.js → init-C0r9Gk5G.js} +0 -0
  179. /package/dist/web/assets/{isArrayLikeObject-B_v2FtYn.js → isArrayLikeObject-CGBoxvCD.js} +0 -0
  180. /package/dist/web/assets/{katex-Bqvo_ZG0.js → katex-DzXRfQ_m.js} +0 -0
  181. /package/dist/web/assets/{lib-BQ34Db2e.js → lib-BeaDXEkP.js} +0 -0
  182. /package/dist/web/assets/{math-069Z4SuC.js → math-y9zN1W-N.js} +0 -0
  183. /package/dist/web/assets/{path-6uRLdFF7.js → path-DIKpVbHL.js} +0 -0
  184. /package/dist/web/assets/{preload-helper-uTix4PVD.js → preload-helper-Bf_JiD2A.js} +0 -0
  185. /package/dist/web/assets/{react-ER-4DN55.js → react-SKk5z-bm.js} +0 -0
  186. /package/dist/web/assets/{rough.esm-JX0wREDd.js → rough.esm-nHaDi0Kw.js} +0 -0
  187. /package/dist/web/assets/{src-BqX54PbV.js → src-Dw4QhedI.js} +0 -0
  188. /package/dist/web/assets/{table-C7X5UAEI.js → table-CQVQM2SB.js} +0 -0
  189. /package/dist/web/assets/{tag-CCtdV063.js → tag-Q2dZiSPX.js} +0 -0
  190. /package/dist/web/assets/{utils-BNytJOb1.js → utils-DMiycH3O.js} +0 -0
@@ -9,12 +9,9 @@ import {
9
9
  SelectValue,
10
10
  } from "@/components/ui/select";
11
11
  import { getAISettings, updateAISettings, type AISettings } from "@/lib/api-settings";
12
-
13
- const MODEL_OPTIONS = [
14
- { value: "claude-sonnet-4-6", label: "Claude Sonnet 4.6" },
15
- { value: "claude-opus-4-6", label: "Claude Opus 4.6" },
16
- { value: "claude-haiku-4-5", label: "Claude Haiku 4.5" },
17
- ];
12
+ import { api } from "@/lib/api-client";
13
+ import { ProviderBadge } from "@/components/chat/provider-selector";
14
+ import type { ModelOption } from "../../../types/chat";
18
15
 
19
16
  const EFFORT_OPTIONS = [
20
17
  { value: "low", label: "Low" },
@@ -29,19 +26,47 @@ const PERMISSION_MODE_OPTIONS = [
29
26
  { value: "plan", label: "Plan mode" },
30
27
  ];
31
28
 
29
+ const PROVIDER_NAMES: Record<string, string> = {
30
+ claude: "Claude",
31
+ cursor: "Cursor",
32
+ codex: "Codex",
33
+ gemini: "Gemini",
34
+ };
35
+
32
36
  export function AISettingsSection({ compact }: { compact?: boolean } = {}) {
33
37
  const [settings, setSettings] = useState<AISettings | null>(null);
38
+ const [activeTab, setActiveTab] = useState<string>("");
39
+ const [models, setModels] = useState<ModelOption[]>([]);
40
+ const [modelsLoading, setModelsLoading] = useState(false);
34
41
  const [saving, setSaving] = useState(false);
35
42
  const [error, setError] = useState<string | null>(null);
36
- // Revision counter forces number inputs to re-render with fresh defaultValue after save
37
43
  const [revision, setRevision] = useState(0);
38
44
 
39
45
  useEffect(() => {
40
- getAISettings().then(setSettings).catch((e) => setError(e.message));
46
+ getAISettings().then((s) => {
47
+ setSettings(s);
48
+ setActiveTab(s.default_provider ?? "claude");
49
+ }).catch((e) => setError(e.message));
41
50
  }, []);
42
51
 
43
- const providerName = settings?.default_provider ?? "claude";
44
- const config = settings?.providers[providerName];
52
+ // Fetch models when active tab changes — uses global settings endpoint
53
+ useEffect(() => {
54
+ if (!activeTab) return;
55
+ setModelsLoading(true);
56
+ api.get<ModelOption[]>(`/api/settings/ai/providers/${activeTab}/models`)
57
+ .then(setModels)
58
+ .catch(() => setModels([]))
59
+ .finally(() => setModelsLoading(false));
60
+ }, [activeTab]);
61
+
62
+ const providerTabs = settings
63
+ ? Object.keys(settings.providers)
64
+ .filter((k) => k !== "mock")
65
+ .map((id) => ({ id, name: PROVIDER_NAMES[id] ?? id }))
66
+ : [];
67
+
68
+ const config = settings?.providers[activeTab];
69
+ const isSdkProvider = config?.type === "agent-sdk" || (!config?.type && activeTab === "claude");
45
70
 
46
71
  const handleSave = async (field: string, value: unknown) => {
47
72
  if (!settings) return;
@@ -49,7 +74,7 @@ export function AISettingsSection({ compact }: { compact?: boolean } = {}) {
49
74
  setError(null);
50
75
  try {
51
76
  const updated = await updateAISettings({
52
- providers: { [providerName]: { [field]: value } },
77
+ providers: { [activeTab]: { [field]: value } },
53
78
  });
54
79
  setSettings(updated);
55
80
  setRevision((r) => r + 1);
@@ -69,7 +94,7 @@ export function AISettingsSection({ compact }: { compact?: boolean } = {}) {
69
94
  if (!settings) {
70
95
  return (
71
96
  <div className={innerGap}>
72
- <h3 className={`${headingSize} font-medium text-text-secondary`}>AI Provider</h3>
97
+ <h3 className={`${headingSize} font-medium text-text-secondary`}>AI Settings</h3>
73
98
  <p className={`${labelSize} text-text-subtle`}>
74
99
  {error ? `Error: ${error}` : "Loading..."}
75
100
  </p>
@@ -77,139 +102,173 @@ export function AISettingsSection({ compact }: { compact?: boolean } = {}) {
77
102
  );
78
103
  }
79
104
 
105
+ // Model select options: use fetched models, with "auto" option for non-SDK providers
106
+ const modelOptions = isSdkProvider
107
+ ? models
108
+ : [{ value: "__default__", label: "Auto (default)" }, ...models];
109
+
80
110
  return (
81
111
  <div className={gapSize}>
82
- <h3 className={`${headingSize} font-medium text-text-secondary`}>AI Provider</h3>
112
+ <h3 className={`${headingSize} font-medium text-text-secondary`}>AI Settings</h3>
83
113
 
84
- <div className={innerGap}>
85
- <div className={fieldGap}>
86
- <Label htmlFor="ai-model" className={compact ? labelSize : undefined}>Model</Label>
87
- <Select
88
- value={config?.model ?? "claude-sonnet-4-6"}
89
- onValueChange={(v) => handleSave("model", v)}
90
- >
91
- <SelectTrigger id="ai-model" className={`w-full ${compact ? "h-7 text-[11px]" : ""}`}>
92
- <SelectValue />
93
- </SelectTrigger>
94
- <SelectContent>
95
- {MODEL_OPTIONS.map((opt) => (
96
- <SelectItem key={opt.value} value={opt.value}>
97
- {opt.label}
98
- </SelectItem>
99
- ))}
100
- </SelectContent>
101
- </Select>
114
+ {/* Provider tabs */}
115
+ {providerTabs.length > 1 && (
116
+ <div className="flex gap-0.5 border-b border-border/50 -mx-1 px-1">
117
+ {providerTabs.map((p) => (
118
+ <button
119
+ key={p.id}
120
+ onClick={() => setActiveTab(p.id)}
121
+ className={`flex items-center gap-1 px-2 py-1 text-[11px] rounded-t transition-colors ${
122
+ activeTab === p.id
123
+ ? "text-primary border-b-2 border-primary font-medium"
124
+ : "text-text-subtle hover:text-text-secondary"
125
+ }`}
126
+ >
127
+ <ProviderBadge providerId={p.id} />
128
+ <span className="capitalize">{p.name}</span>
129
+ </button>
130
+ ))}
102
131
  </div>
132
+ )}
103
133
 
104
- <div className={fieldGap}>
105
- <Label htmlFor="ai-base-url" className={compact ? labelSize : undefined}>Base URL</Label>
106
- <Input
107
- key={`baseurl-${revision}`}
108
- id="ai-base-url"
109
- type="url"
110
- defaultValue={config?.base_url ?? ""}
111
- placeholder="https://api.anthropic.com (default)"
112
- className={compact ? "h-7 text-[11px]" : undefined}
113
- onBlur={(e) => {
114
- const val = e.target.value.trim();
115
- handleSave("base_url", val || undefined);
116
- }}
117
- />
118
- </div>
134
+ <div className={innerGap}>
135
+ {/* Model selector dynamic, works for all providers */}
136
+ {models.length > 0 && (
137
+ <div className={fieldGap}>
138
+ <Label htmlFor="ai-model" className={compact ? labelSize : undefined}>Model</Label>
139
+ <Select
140
+ value={isSdkProvider ? (config?.model ?? models[0]?.value) : (config?.model || "__default__")}
141
+ onValueChange={(v) => handleSave("model", v === "__default__" ? undefined : v)}
142
+ disabled={modelsLoading}
143
+ >
144
+ <SelectTrigger id="ai-model" className={`w-full ${compact ? "h-7 text-[11px]" : ""}`}>
145
+ <SelectValue placeholder={modelsLoading ? "Loading models..." : "Select model"} />
146
+ </SelectTrigger>
147
+ <SelectContent className="max-h-[300px]">
148
+ {modelOptions.map((opt) => (
149
+ <SelectItem key={opt.value} value={opt.value}>
150
+ {opt.label}
151
+ </SelectItem>
152
+ ))}
153
+ </SelectContent>
154
+ </Select>
155
+ </div>
156
+ )}
119
157
 
120
- <div className={fieldGap}>
121
- <Label htmlFor="ai-api-key" className={compact ? labelSize : undefined}>API Key / Token</Label>
122
- <Input
123
- key={`apikey-${revision}`}
124
- id="ai-api-key"
125
- type="password"
126
- defaultValue={config?.api_key ?? ""}
127
- placeholder="sk-ant-... (optional, overrides accounts)"
128
- className={compact ? "h-7 text-[11px] font-mono" : "font-mono"}
129
- onBlur={(e) => {
130
- const val = e.target.value.trim();
131
- // Don't save if it's the masked value
132
- if (val.startsWith("••••")) return;
133
- handleSave("api_key", val || undefined);
134
- }}
135
- />
136
- <p className={`${compact ? "text-[9px]" : "text-[11px]"} text-muted-foreground`}>
137
- Direct API key or OAuth token. Leave empty to use connected accounts.
138
- </p>
139
- </div>
158
+ {/* SDK-specific fields */}
159
+ {isSdkProvider && (
160
+ <>
161
+ <div className={fieldGap}>
162
+ <Label htmlFor="ai-base-url" className={compact ? labelSize : undefined}>Base URL</Label>
163
+ <Input
164
+ key={`baseurl-${activeTab}-${revision}`}
165
+ id="ai-base-url"
166
+ type="url"
167
+ defaultValue={config?.base_url ?? ""}
168
+ placeholder="https://api.anthropic.com (default)"
169
+ className={compact ? "h-7 text-[11px]" : undefined}
170
+ onBlur={(e) => {
171
+ const val = e.target.value.trim();
172
+ handleSave("base_url", val || undefined);
173
+ }}
174
+ />
175
+ </div>
140
176
 
141
- <div className={fieldGap}>
142
- <Label htmlFor="ai-effort" className={compact ? labelSize : undefined}>Effort</Label>
143
- <Select
144
- value={config?.effort ?? "high"}
145
- onValueChange={(v) => handleSave("effort", v)}
146
- >
147
- <SelectTrigger id="ai-effort" className={`w-full ${compact ? "h-7 text-[11px]" : ""}`}>
148
- <SelectValue />
149
- </SelectTrigger>
150
- <SelectContent>
151
- {EFFORT_OPTIONS.map((opt) => (
152
- <SelectItem key={opt.value} value={opt.value}>
153
- {opt.label}
154
- </SelectItem>
155
- ))}
156
- </SelectContent>
157
- </Select>
158
- </div>
177
+ <div className={fieldGap}>
178
+ <Label htmlFor="ai-api-key" className={compact ? labelSize : undefined}>API Key / Token</Label>
179
+ <Input
180
+ key={`apikey-${activeTab}-${revision}`}
181
+ id="ai-api-key"
182
+ type="password"
183
+ defaultValue={config?.api_key ?? ""}
184
+ placeholder="sk-ant-... (optional, overrides accounts)"
185
+ className={compact ? "h-7 text-[11px] font-mono" : "font-mono"}
186
+ onBlur={(e) => {
187
+ const val = e.target.value.trim();
188
+ if (val.startsWith("••••")) return;
189
+ handleSave("api_key", val || undefined);
190
+ }}
191
+ />
192
+ <p className={`${compact ? "text-[9px]" : "text-[11px]"} text-muted-foreground`}>
193
+ Direct API key or OAuth token. Leave empty to use connected accounts.
194
+ </p>
195
+ </div>
159
196
 
160
- <div className={fieldGap}>
161
- <Label htmlFor="ai-max-turns" className={compact ? labelSize : undefined}>Max Turns (1-500)</Label>
162
- <Input
163
- key={`turns-${revision}`}
164
- id="ai-max-turns"
165
- type="number"
166
- min={1}
167
- max={500}
168
- defaultValue={config?.max_turns ?? 100}
169
- className={compact ? "h-7 text-[11px]" : undefined}
170
- onBlur={(e) => {
171
- const val = parseInt(e.target.value);
172
- if (!isNaN(val)) handleSave("max_turns", val);
173
- }}
174
- />
175
- </div>
197
+ <div className={fieldGap}>
198
+ <Label htmlFor="ai-effort" className={compact ? labelSize : undefined}>Effort</Label>
199
+ <Select
200
+ value={config?.effort ?? "high"}
201
+ onValueChange={(v) => handleSave("effort", v)}
202
+ >
203
+ <SelectTrigger id="ai-effort" className={`w-full ${compact ? "h-7 text-[11px]" : ""}`}>
204
+ <SelectValue />
205
+ </SelectTrigger>
206
+ <SelectContent>
207
+ {EFFORT_OPTIONS.map((opt) => (
208
+ <SelectItem key={opt.value} value={opt.value}>
209
+ {opt.label}
210
+ </SelectItem>
211
+ ))}
212
+ </SelectContent>
213
+ </Select>
214
+ </div>
176
215
 
177
- <div className={fieldGap}>
178
- <Label htmlFor="ai-budget" className={compact ? labelSize : undefined}>Max Budget (USD)</Label>
179
- <Input
180
- key={`budget-${revision}`}
181
- id="ai-budget"
182
- type="number"
183
- step={0.1}
184
- min={0.01}
185
- max={50}
186
- defaultValue={config?.max_budget_usd ?? ""}
187
- placeholder="No limit"
188
- className={compact ? "h-7 text-[11px]" : undefined}
189
- onBlur={(e) => {
190
- const val = parseFloat(e.target.value);
191
- handleSave("max_budget_usd", isNaN(val) ? undefined : val);
192
- }}
193
- />
194
- </div>
216
+ <div className={fieldGap}>
217
+ <Label htmlFor="ai-max-turns" className={compact ? labelSize : undefined}>Max Turns (1-500)</Label>
218
+ <Input
219
+ key={`turns-${activeTab}-${revision}`}
220
+ id="ai-max-turns"
221
+ type="number"
222
+ min={1}
223
+ max={500}
224
+ defaultValue={config?.max_turns ?? 100}
225
+ className={compact ? "h-7 text-[11px]" : undefined}
226
+ onBlur={(e) => {
227
+ const val = parseInt(e.target.value);
228
+ if (!isNaN(val)) handleSave("max_turns", val);
229
+ }}
230
+ />
231
+ </div>
195
232
 
196
- <div className={fieldGap}>
197
- <Label htmlFor="ai-thinking" className={compact ? labelSize : undefined}>Thinking Budget (tokens)</Label>
198
- <Input
199
- key={`thinking-${revision}`}
200
- id="ai-thinking"
201
- type="number"
202
- min={0}
203
- defaultValue={config?.thinking_budget_tokens ?? ""}
204
- placeholder="Disabled"
205
- className={compact ? "h-7 text-[11px]" : undefined}
206
- onBlur={(e) => {
207
- const val = parseInt(e.target.value);
208
- handleSave("thinking_budget_tokens", isNaN(val) ? undefined : val);
209
- }}
210
- />
211
- </div>
233
+ <div className={fieldGap}>
234
+ <Label htmlFor="ai-budget" className={compact ? labelSize : undefined}>Max Budget (USD)</Label>
235
+ <Input
236
+ key={`budget-${activeTab}-${revision}`}
237
+ id="ai-budget"
238
+ type="number"
239
+ step={0.1}
240
+ min={0.01}
241
+ max={50}
242
+ defaultValue={config?.max_budget_usd ?? ""}
243
+ placeholder="No limit"
244
+ className={compact ? "h-7 text-[11px]" : undefined}
245
+ onBlur={(e) => {
246
+ const val = parseFloat(e.target.value);
247
+ handleSave("max_budget_usd", isNaN(val) ? undefined : val);
248
+ }}
249
+ />
250
+ </div>
251
+
252
+ <div className={fieldGap}>
253
+ <Label htmlFor="ai-thinking" className={compact ? labelSize : undefined}>Thinking Budget (tokens)</Label>
254
+ <Input
255
+ key={`thinking-${activeTab}-${revision}`}
256
+ id="ai-thinking"
257
+ type="number"
258
+ min={0}
259
+ defaultValue={config?.thinking_budget_tokens ?? ""}
260
+ placeholder="Disabled"
261
+ className={compact ? "h-7 text-[11px]" : undefined}
262
+ onBlur={(e) => {
263
+ const val = parseInt(e.target.value);
264
+ handleSave("thinking_budget_tokens", isNaN(val) ? undefined : val);
265
+ }}
266
+ />
267
+ </div>
268
+ </>
269
+ )}
212
270
 
271
+ {/* Common fields: permission mode + system prompt (all providers) */}
213
272
  <div className={fieldGap}>
214
273
  <Label htmlFor="ai-permission-mode" className={compact ? labelSize : undefined}>Default Permission Mode</Label>
215
274
  <Select
@@ -232,11 +291,11 @@ export function AISettingsSection({ compact }: { compact?: boolean } = {}) {
232
291
  <div className={fieldGap}>
233
292
  <Label htmlFor="ai-system-prompt" className={compact ? labelSize : undefined}>Additional Instructions</Label>
234
293
  <textarea
235
- key={`sysprompt-${revision}`}
294
+ key={`sysprompt-${activeTab}-${revision}`}
236
295
  id="ai-system-prompt"
237
- rows={4}
296
+ rows={compact ? 3 : 4}
238
297
  defaultValue={config?.system_prompt ?? ""}
239
- placeholder="Enter additional instructions for Claude..."
298
+ placeholder={`Enter additional instructions for ${activeTab}...`}
240
299
  className={`w-full rounded-md border border-input bg-background px-3 py-2 ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${compact ? "text-[11px]" : "text-sm"}`}
241
300
  onBlur={(e) => {
242
301
  const val = e.target.value.trim();
@@ -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
+ }