@hienlh/ppm 0.9.0-beta.2 → 0.9.0-beta.4

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 (187) hide show
  1. package/CHANGELOG.md +10 -26
  2. package/dist/web/assets/{_basePickBy-CZovQgWd.js → _basePickBy-COwDPZl_.js} +1 -1
  3. package/dist/web/assets/{_baseUniq-ClnvscgW.js → _baseUniq-DCb0mkTp.js} +1 -1
  4. package/dist/web/assets/{api-settings--eVrUeZM.js → api-settings-CuUkz5gb.js} +1 -1
  5. package/dist/web/assets/{arc-C2Qaz-ch.js → arc-D0bJaFyD.js} +1 -1
  6. package/dist/web/assets/architecture-PBZL5I3N-281eTKQ3.js +1 -0
  7. package/dist/web/assets/{architectureDiagram-2XIMDMQ5-Jq91S_rs.js → architectureDiagram-2XIMDMQ5-BVEUkQYB.js} +1 -1
  8. package/dist/web/assets/arrow-left-C_j9Ki73.js +1 -0
  9. package/dist/web/assets/{blockDiagram-WCTKOSBZ-CKGufRTy.js → blockDiagram-WCTKOSBZ-CU2t4NHJ.js} +1 -1
  10. package/dist/web/assets/browser-tab-BhTdeeZd.js +1 -0
  11. package/dist/web/assets/{c4Diagram-IC4MRINW-BNP2L9r_.js → c4Diagram-IC4MRINW-DzjR91sM.js} +1 -1
  12. package/dist/web/assets/channel-CKNZAqoN.js +1 -0
  13. package/dist/web/assets/chat-tab-ZiiUVOxM.js +7 -0
  14. package/dist/web/assets/{chunk-4BX2VUAB-BptTlTyl.js → chunk-4BX2VUAB-0YMkpW2S.js} +1 -1
  15. package/dist/web/assets/{chunk-55IACEB6-C4mUdyio.js → chunk-55IACEB6-Dp0pTM5r.js} +1 -1
  16. package/dist/web/assets/{chunk-7E7YKBS2-6xAQfBwa.js → chunk-7E7YKBS2-CuYKSUgJ.js} +1 -1
  17. package/dist/web/assets/{chunk-7R4GIKGN-DXaGAn_K.js → chunk-7R4GIKGN-DvbvLUIN.js} +2 -2
  18. package/dist/web/assets/{chunk-C72U2L5F-DOtEiN5f.js → chunk-C72U2L5F-CcEW1AMZ.js} +1 -1
  19. package/dist/web/assets/{chunk-EGIJ26TM-D0KJTa_T.js → chunk-EGIJ26TM-Cgt-qg75.js} +1 -1
  20. package/dist/web/assets/{chunk-FMBD7UC4-C_1aG0eb.js → chunk-FMBD7UC4-JCLgVcaC.js} +1 -1
  21. package/dist/web/assets/{chunk-GEFDOKGD-DwVPiYfW.js → chunk-GEFDOKGD-B82RP9ow.js} +1 -1
  22. package/dist/web/assets/chunk-GLR3WWYH-Bx2UL5jF.js +2 -0
  23. package/dist/web/assets/chunk-HHEYEP7N-BnRVfNc5.js +1 -0
  24. package/dist/web/assets/{chunk-JSJVCQXG-BSrqCL_3.js → chunk-JSJVCQXG-Pb-JMOgO.js} +1 -1
  25. package/dist/web/assets/{chunk-KX2RTZJC-BCxGmbzy.js → chunk-KX2RTZJC-BRj-ZEvL.js} +1 -1
  26. package/dist/web/assets/{chunk-KYZI473N-BKO5gMeU.js → chunk-KYZI473N-CBRPKraG.js} +1 -1
  27. package/dist/web/assets/{chunk-L3YUKLVL-3wBgkSvL.js → chunk-L3YUKLVL-DNFj84V6.js} +1 -1
  28. package/dist/web/assets/{chunk-MX3YWQON-BgjSEzus.js → chunk-MX3YWQON-BnPzQK-O.js} +1 -1
  29. package/dist/web/assets/{chunk-NQ4KR5QH-DLrZwBEm.js → chunk-NQ4KR5QH-BRj25yO7.js} +1 -1
  30. package/dist/web/assets/{chunk-O4XLMI2P-BurQy8tt.js → chunk-O4XLMI2P-BdXwVXjJ.js} +1 -1
  31. package/dist/web/assets/{chunk-OZEHJAEY-YTn24bGg.js → chunk-OZEHJAEY-LfXT4p8B.js} +1 -1
  32. package/dist/web/assets/{chunk-PQ6SQG4A-BxtUGYhW.js → chunk-PQ6SQG4A-EdgQyTqa.js} +1 -1
  33. package/dist/web/assets/{chunk-PU5JKC2W-B66ELkQm.js → chunk-PU5JKC2W-D3thuSok.js} +1 -1
  34. package/dist/web/assets/chunk-QZHKN3VN-gaBt0Rbd.js +1 -0
  35. package/dist/web/assets/{chunk-R5LLSJPH-euR2RxLN.js → chunk-R5LLSJPH-LdG7RqsM.js} +1 -1
  36. package/dist/web/assets/{chunk-WL4C6EOR-_2CBOJdI.js → chunk-WL4C6EOR-BHFnnXOt.js} +1 -1
  37. package/dist/web/assets/{chunk-XIRO2GV7-kqQ0g6wW.js → chunk-XIRO2GV7-DUmQrLsF.js} +1 -1
  38. package/dist/web/assets/{chunk-XPW4576I-CtcaMb09.js → chunk-XPW4576I-CsGTseUr.js} +1 -1
  39. package/dist/web/assets/{chunk-XZSTWKYB-BYxFzZwS.js → chunk-XZSTWKYB-5W2emiq4.js} +1 -1
  40. package/dist/web/assets/{chunk-YBOYWFTD-Dx_fX35n.js → chunk-YBOYWFTD-COdZIaX4.js} +1 -1
  41. package/dist/web/assets/classDiagram-VBA2DB6C-CqaIqYPn.js +1 -0
  42. package/dist/web/assets/classDiagram-v2-RAHNMMFH-Bo5WN2ok.js +1 -0
  43. package/dist/web/assets/clone-DNDy9Sms.js +1 -0
  44. package/dist/web/assets/{code-editor-CQ7gq0Vj.js → code-editor-BRMOypkX.js} +1 -1
  45. package/dist/web/assets/{cose-bilkent-S5V4N54A-CHHjH2dV.js → cose-bilkent-S5V4N54A-C1QJ6GPW.js} +1 -1
  46. package/dist/web/assets/{dagre-CNtSxiE_.js → dagre-CWo8w9wK.js} +1 -1
  47. package/dist/web/assets/{dagre-KLK3FWXG-ChenfPp1.js → dagre-KLK3FWXG-Br4t5TRV.js} +1 -1
  48. package/dist/web/assets/database-viewer-CEoDpzPz.js +1 -0
  49. package/dist/web/assets/{diagram-E7M64L7V-CzKYZM0Y.js → diagram-E7M64L7V-CkDC2uAj.js} +1 -1
  50. package/dist/web/assets/{diagram-IFDJBPK2-ChB_paPo.js → diagram-IFDJBPK2-NvhckwcA.js} +1 -1
  51. package/dist/web/assets/{diagram-P4PSJMXO-D1eW1dkL.js → diagram-P4PSJMXO--nUaNiyB.js} +1 -1
  52. package/dist/web/assets/{diff-viewer-BjtTemkK.js → diff-viewer-jDU2bcGj.js} +1 -1
  53. package/dist/web/assets/{erDiagram-INFDFZHY-mCvUFSn6.js → erDiagram-INFDFZHY-DK4QEZYh.js} +1 -1
  54. package/dist/web/assets/{flowDiagram-PKNHOUZH-14ohZ1M1.js → flowDiagram-PKNHOUZH-B9h_Ba-v.js} +1 -1
  55. package/dist/web/assets/{ganttDiagram-A5KZAMGK-DIX0pLbk.js → ganttDiagram-A5KZAMGK-BVlftqyZ.js} +1 -1
  56. package/dist/web/assets/git-graph-DMQzw4Sp.js +1 -0
  57. package/dist/web/assets/gitGraph-HDMCJU4V-D5qEPjgs.js +1 -0
  58. package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-yEWZbdf_.js → gitGraphDiagram-K3NZZRJ6-L7sj3Bs-.js} +1 -1
  59. package/dist/web/assets/{graphlib-DhOZxqsh.js → graphlib-BbbiUImY.js} +1 -1
  60. package/dist/web/assets/index-B4Iz1Wbi.css +2 -0
  61. package/dist/web/assets/index-QiSWS6f-.js +37 -0
  62. package/dist/web/assets/info-3K5VOQVL-CbpovIYU.js +1 -0
  63. package/dist/web/assets/infoDiagram-LFFYTUFH-DFh9c-S2.js +2 -0
  64. package/dist/web/assets/input-DGlv6gt_.js +41 -0
  65. package/dist/web/assets/{isEmpty-C0YYdhYj.js → isEmpty-DXomfd7J.js} +1 -1
  66. package/dist/web/assets/{ishikawaDiagram-PHBUUO56-olazD6dZ.js → ishikawaDiagram-PHBUUO56-cW7SMLa_.js} +1 -1
  67. package/dist/web/assets/{journeyDiagram-4ABVD52K-CttDH9bb.js → journeyDiagram-4ABVD52K-DFQXUZsc.js} +1 -1
  68. package/dist/web/assets/{kanban-definition-K7BYSVSG-BBXbI37U.js → kanban-definition-K7BYSVSG-BMUhjxqj.js} +1 -1
  69. package/dist/web/assets/keybindings-store-BplH-yiN.js +1 -0
  70. package/dist/web/assets/{line-DBLLF7lH.js → line--xyfYP3x.js} +1 -1
  71. package/dist/web/assets/{linear-BLFWatDe.js → linear-BdqW7iQu.js} +1 -1
  72. package/dist/web/assets/{markdown-renderer-BtPXdzTv.js → markdown-renderer-BCjJbGP8.js} +5 -5
  73. package/dist/web/assets/{mermaid-parser.core-BKiGOTjR.js → mermaid-parser.core-BY8JfkE_.js} +2 -2
  74. package/dist/web/assets/{mindmap-definition-YRQLILUH-DoT7m4Sz.js → mindmap-definition-YRQLILUH-DIv-LMXG.js} +1 -1
  75. package/dist/web/assets/{ordinal-CCj7PWgZ.js → ordinal-CIoJK3nc.js} +1 -1
  76. package/dist/web/assets/packet-RMMSAZCW-BbzPU9BK.js +1 -0
  77. package/dist/web/assets/pie-UPGHQEXC-B0h6hM1j.js +1 -0
  78. package/dist/web/assets/{pieDiagram-SKSYHLDU-Bkh2E4zE.js → pieDiagram-SKSYHLDU-seSK40d1.js} +1 -1
  79. package/dist/web/assets/postgres-viewer-s0snZ9CL.js +1 -0
  80. package/dist/web/assets/{quadrantDiagram-337W2JSQ-B7zgALOL.js → quadrantDiagram-337W2JSQ-BaRFqlsA.js} +1 -1
  81. package/dist/web/assets/radar-KQ55EAFF-CHptMqVT.js +1 -0
  82. package/dist/web/assets/{requirementDiagram-Z7DCOOCP-D_5GXNRo.js → requirementDiagram-Z7DCOOCP-1WWjMQB_.js} +1 -1
  83. package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-BA9EFAAe.js → sankeyDiagram-WA2Y5GQK-DEGGYsk7.js} +1 -1
  84. package/dist/web/assets/{sequenceDiagram-2WXFIKYE-fyWIrHiG.js → sequenceDiagram-2WXFIKYE-BtRvoUTC.js} +1 -1
  85. package/dist/web/assets/{settings-store-Bbhg_ptG.js → settings-store-D3dJqGhB.js} +2 -2
  86. package/dist/web/assets/settings-tab-2YkgmrY0.js +1 -0
  87. package/dist/web/assets/sqlite-viewer-B5GNwXaG.js +1 -0
  88. package/dist/web/assets/{stateDiagram-RAJIS63D-DfRBcaBu.js → stateDiagram-RAJIS63D-C16aO8tn.js} +1 -1
  89. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-D7qSAjnK.js +1 -0
  90. package/dist/web/assets/switch-mjGtIVDJ.js +1 -0
  91. package/dist/web/assets/{tab-store-dpsCvqhH.js → tab-store-DSz5PQI0.js} +1 -1
  92. package/dist/web/assets/{terminal-tab--Gw14HP3.js → terminal-tab-MRg8y1xF.js} +2 -2
  93. package/dist/web/assets/{timeline-definition-YZTLITO2-DYfwJ1jM.js → timeline-definition-YZTLITO2-DrjxCpEM.js} +1 -1
  94. package/dist/web/assets/treemap-KZPCXAKY-BL9OJq3X.js +1 -0
  95. package/dist/web/assets/{use-monaco-theme-DHbyUrzJ.js → use-monaco-theme-BQzvItNE.js} +1 -1
  96. package/dist/web/assets/{vennDiagram-LZ73GAT5-DqbKNRD9.js → vennDiagram-LZ73GAT5-DfYFnniI.js} +1 -1
  97. package/dist/web/assets/{xychartDiagram-JWTSCODW-DhUL86qT.js → xychartDiagram-JWTSCODW-BRvXOVlG.js} +1 -1
  98. package/dist/web/index.html +12 -10
  99. package/dist/web/sw.js +1 -1
  100. package/docs/code-standards.md +260 -7
  101. package/docs/codebase-summary.md +255 -95
  102. package/docs/project-changelog.md +88 -1
  103. package/docs/system-architecture.md +177 -12
  104. package/package.json +1 -1
  105. package/src/providers/claude-agent-sdk.ts +9 -0
  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 +8 -0
  114. package/src/server/routes/browser-preview.ts +89 -0
  115. package/src/server/routes/chat.ts +14 -3
  116. package/src/server/routes/settings.ts +14 -0
  117. package/src/server/ws/chat.ts +24 -8
  118. package/src/services/chat.service.ts +10 -15
  119. package/src/types/chat.ts +21 -2
  120. package/src/types/config.ts +33 -11
  121. package/src/utils/ndjson-line-parser.ts +36 -0
  122. package/src/web/components/browser/browser-tab.tsx +269 -0
  123. package/src/web/components/chat/chat-history-bar.tsx +49 -29
  124. package/src/web/components/chat/chat-tab.tsx +14 -2
  125. package/src/web/components/chat/message-input.tsx +91 -3
  126. package/src/web/components/chat/provider-selector.tsx +150 -0
  127. package/src/web/components/chat/session-picker.tsx +3 -1
  128. package/src/web/components/layout/command-palette.tsx +4 -0
  129. package/src/web/components/layout/editor-panel.tsx +1 -0
  130. package/src/web/components/layout/mobile-nav.tsx +2 -2
  131. package/src/web/components/layout/panel-layout.tsx +17 -1
  132. package/src/web/components/layout/tab-bar.tsx +2 -0
  133. package/src/web/components/layout/tab-content.tsx +5 -0
  134. package/src/web/components/settings/ai-settings-section.tsx +196 -137
  135. package/src/web/hooks/use-chat.ts +11 -0
  136. package/src/web/hooks/use-global-keybindings.ts +7 -0
  137. package/src/web/hooks/use-voice-input.ts +111 -0
  138. package/src/web/stores/keybindings-store.ts +1 -0
  139. package/src/web/stores/panel-store.ts +10 -10
  140. package/src/web/stores/tab-store.ts +2 -1
  141. package/dist/web/assets/architecture-PBZL5I3N-ChOahOB7.js +0 -1
  142. package/dist/web/assets/channel-w7yboq56.js +0 -1
  143. package/dist/web/assets/chat-tab-DmF14O6G.js +0 -7
  144. package/dist/web/assets/chunk-GLR3WWYH-D9pZakqr.js +0 -2
  145. package/dist/web/assets/chunk-HHEYEP7N-Dld5BpGB.js +0 -1
  146. package/dist/web/assets/chunk-QZHKN3VN-DwSXwtjH.js +0 -1
  147. package/dist/web/assets/classDiagram-VBA2DB6C-BpJ6Oog2.js +0 -1
  148. package/dist/web/assets/classDiagram-v2-RAHNMMFH-Bj8gIhkP.js +0 -1
  149. package/dist/web/assets/clone-BSi6cgDh.js +0 -1
  150. package/dist/web/assets/database-viewer-B27aRtdQ.js +0 -1
  151. package/dist/web/assets/git-graph-BGXo0o-J.js +0 -1
  152. package/dist/web/assets/gitGraph-HDMCJU4V-CEee2FCA.js +0 -1
  153. package/dist/web/assets/index-BAioKo_2.css +0 -2
  154. package/dist/web/assets/index-CfClIVo2.js +0 -37
  155. package/dist/web/assets/info-3K5VOQVL-ce_pi3En.js +0 -1
  156. package/dist/web/assets/infoDiagram-LFFYTUFH-BzqyoqXw.js +0 -2
  157. package/dist/web/assets/input-Brjz2Vv-.js +0 -41
  158. package/dist/web/assets/keybindings-store-nDbczFnq.js +0 -1
  159. package/dist/web/assets/packet-RMMSAZCW-CdYSLjRL.js +0 -1
  160. package/dist/web/assets/pie-UPGHQEXC-Bm5LiD-6.js +0 -1
  161. package/dist/web/assets/postgres-viewer-BMg-qFcO.js +0 -1
  162. package/dist/web/assets/radar-KQ55EAFF-C4PnyG7_.js +0 -1
  163. package/dist/web/assets/settings-tab-NPuwQHzs.js +0 -1
  164. package/dist/web/assets/sqlite-viewer-CAsUczio.js +0 -1
  165. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-CMN4M2Em.js +0 -1
  166. package/dist/web/assets/treemap-KZPCXAKY-2_y-mhkz.js +0 -1
  167. package/snapshot-state.md +0 -1526
  168. package/test-tokens.mjs +0 -212
  169. /package/dist/web/assets/{api-client-DpGMOZNf.js → api-client-icCZ-07C.js} +0 -0
  170. /package/dist/web/assets/{array-BGFCBI0e.js → array-CLwNaqU1.js} +0 -0
  171. /package/dist/web/assets/{columns-2-ChOTgl3e.js → columns-2-Bcg3QJBg.js} +0 -0
  172. /package/dist/web/assets/{cytoscape.esm-Ccan6xou.js → cytoscape.esm-B-QQuWwK.js} +0 -0
  173. /package/dist/web/assets/{defaultLocale-CRZydyG6.js → defaultLocale-D_VMtRaY.js} +0 -0
  174. /package/dist/web/assets/{dist-T0Vhi0Mh.js → dist-CMmNEgEP.js} +0 -0
  175. /package/dist/web/assets/{dist-Cce3efmT.js → dist-Ckxnw5rl.js} +0 -0
  176. /package/dist/web/assets/{init-B8gtcn7T.js → init-vVpfz1D6.js} +0 -0
  177. /package/dist/web/assets/{isArrayLikeObject-B4pdpV8V.js → isArrayLikeObject-DvHDmeBe.js} +0 -0
  178. /package/dist/web/assets/{katex-Bbu770d9.js → katex-C3cZrCvP.js} +0 -0
  179. /package/dist/web/assets/{math-DwgHI-Cu.js → math-a44lmFDa.js} +0 -0
  180. /package/dist/web/assets/{path-DZF-JdEe.js → path-CuyvWNAH.js} +0 -0
  181. /package/dist/web/assets/{preload-helper-qlgyTAkD.js → preload-helper-CsoeaaUJ.js} +0 -0
  182. /package/dist/web/assets/{react-BGf7KNLk.js → react-BPIfZRKM.js} +0 -0
  183. /package/dist/web/assets/{rough.esm-VLpapkIG.js → rough.esm-c4PR5shF.js} +0 -0
  184. /package/dist/web/assets/{src-BoSBNdA_.js → src-CLWraeNW.js} +0 -0
  185. /package/dist/web/assets/{table-Yo02WRH-.js → table-C9jDaRl2.js} +0 -0
  186. /package/dist/web/assets/{tag-CaC1ng2E.js → tag-CENGyt_L.js} +0 -0
  187. /package/dist/web/assets/{utils-btZ8C8-R.js → utils-Bslrbb-G.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();
@@ -23,6 +23,8 @@ interface UseChatReturn {
23
23
  pendingApproval: ApprovalRequest | null;
24
24
  contextWindowPct: number | null;
25
25
  sessionTitle: string | null;
26
+ /** When CLI provider assigns a different session ID, this holds the new ID */
27
+ migratedSessionId: string | null;
26
28
  sendMessage: (content: string, opts?: { permissionMode?: string }) => void;
27
29
  respondToApproval: (requestId: string, approved: boolean, data?: unknown) => void;
28
30
  cancelStreaming: () => void;
@@ -51,6 +53,7 @@ export function useChat(sessionId: string | null, providerId = "claude", project
51
53
  const [contextWindowPct, setContextWindowPct] = useState<number | null>(null);
52
54
  const [sessionTitle, setSessionTitle] = useState<string | null>(null);
53
55
  const [isConnected, setIsConnected] = useState(false);
56
+ const [migratedSessionId, setMigratedSessionId] = useState<string | null>(null);
54
57
  const streamingContentRef = useRef("");
55
58
  const streamingEventsRef = useRef<ChatEvent[]>([]);
56
59
  const streamingAccountRef = useRef<{ accountId: string; accountLabel: string } | null>(null);
@@ -243,6 +246,13 @@ export function useChat(sessionId: string | null, providerId = "claude", project
243
246
  // Ignore keepalive pings
244
247
  if ((data as any).type === "ping") return;
245
248
 
249
+ // Handle session ID migration (CLI provider assigned different ID)
250
+ if ((data as any).type === "session_migrated") {
251
+ const newId = (data as any).newSessionId as string;
252
+ if (newId) setMigratedSessionId(newId);
253
+ return;
254
+ }
255
+
246
256
  // Handle title updates from SDK summary
247
257
  if ((data as any).type === "title_updated") {
248
258
  setSessionTitle((data as any).title ?? null);
@@ -518,6 +528,7 @@ export function useChat(sessionId: string | null, providerId = "claude", project
518
528
  pendingApproval,
519
529
  contextWindowPct,
520
530
  sessionTitle,
531
+ migratedSessionId,
521
532
  sendMessage,
522
533
  respondToApproval,
523
534
  cancelStreaming,
@@ -124,6 +124,13 @@ export function useGlobalKeybindings() {
124
124
  return;
125
125
  }
126
126
 
127
+ // Toggle voice input in chat
128
+ if (match(e, "voice-input")) {
129
+ e.preventDefault();
130
+ window.dispatchEvent(new CustomEvent("toggle-voice-input"));
131
+ return;
132
+ }
133
+
127
134
  // Open search (sidebar)
128
135
  if (match(e, "open-search")) {
129
136
  e.preventDefault();
@@ -0,0 +1,111 @@
1
+ import { useState, useRef, useCallback } from "react";
2
+
3
+ // Extend Window for webkit prefix
4
+ interface SpeechRecognitionEvent extends Event {
5
+ results: SpeechRecognitionResultList;
6
+ resultIndex: number;
7
+ }
8
+
9
+ type SpeechRecognitionInstance = {
10
+ lang: string;
11
+ continuous: boolean;
12
+ interimResults: boolean;
13
+ start(): void;
14
+ stop(): void;
15
+ abort(): void;
16
+ onresult: ((event: SpeechRecognitionEvent) => void) | null;
17
+ onend: (() => void) | null;
18
+ onerror: ((event: Event & { error: string }) => void) | null;
19
+ };
20
+
21
+ type SpeechRecognitionConstructor = new () => SpeechRecognitionInstance;
22
+
23
+ function getSpeechRecognition(): SpeechRecognitionConstructor | null {
24
+ const w = window as unknown as {
25
+ SpeechRecognition?: SpeechRecognitionConstructor;
26
+ webkitSpeechRecognition?: SpeechRecognitionConstructor;
27
+ };
28
+ return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;
29
+ }
30
+
31
+ export function useVoiceInput(options?: { lang?: string }) {
32
+ const [isListening, setIsListening] = useState(false);
33
+ const [interimText, setInterimText] = useState("");
34
+ const recognitionRef = useRef<SpeechRecognitionInstance | null>(null);
35
+ // Accumulate finalized text across multiple result events
36
+ const finalizedRef = useRef("");
37
+
38
+ const supported = typeof window !== "undefined" && getSpeechRecognition() !== null;
39
+
40
+ const start = useCallback(
41
+ (onResult: (text: string, isFinal: boolean) => void) => {
42
+ const SR = getSpeechRecognition();
43
+ if (!SR) return;
44
+
45
+ // Stop any existing session
46
+ recognitionRef.current?.abort();
47
+
48
+ const recognition = new SR();
49
+ recognition.lang = options?.lang ?? "vi-VN";
50
+ recognition.continuous = true;
51
+ recognition.interimResults = true;
52
+
53
+ finalizedRef.current = "";
54
+
55
+ recognition.onresult = (event: SpeechRecognitionEvent) => {
56
+ let interim = "";
57
+ let newFinalized = "";
58
+
59
+ for (let i = 0; i < event.results.length; i++) {
60
+ const result = event.results[i]!;
61
+ if (result.isFinal) {
62
+ newFinalized += result[0]!.transcript;
63
+ } else {
64
+ interim += result[0]!.transcript;
65
+ }
66
+ }
67
+
68
+ // Update finalized accumulator
69
+ if (newFinalized) {
70
+ finalizedRef.current = newFinalized;
71
+ }
72
+
73
+ const fullText = (finalizedRef.current + " " + interim).trim();
74
+ setInterimText(interim);
75
+ onResult(fullText, interim.length === 0 && finalizedRef.current.length > 0);
76
+ };
77
+
78
+ recognition.onend = () => {
79
+ setIsListening(false);
80
+ setInterimText("");
81
+ // Deliver final text if any
82
+ if (finalizedRef.current) {
83
+ onResult(finalizedRef.current.trim(), true);
84
+ }
85
+ };
86
+
87
+ recognition.onerror = (event) => {
88
+ // "no-speech" and "aborted" are expected, not real errors
89
+ if (event.error !== "no-speech" && event.error !== "aborted") {
90
+ console.warn("[voice-input] error:", event.error);
91
+ }
92
+ setIsListening(false);
93
+ setInterimText("");
94
+ };
95
+
96
+ recognitionRef.current = recognition;
97
+ recognition.start();
98
+ setIsListening(true);
99
+ },
100
+ [options?.lang],
101
+ );
102
+
103
+ const stop = useCallback(() => {
104
+ recognitionRef.current?.stop();
105
+ recognitionRef.current = null;
106
+ setIsListening(false);
107
+ setInterimText("");
108
+ }, []);
109
+
110
+ return { isListening, interimText, start, stop, supported };
111
+ }
@@ -36,6 +36,7 @@ export const KEY_ACTIONS: KeyAction[] = [
36
36
  { id: "open-git-graph", label: "Git Graph", category: "tabs", defaultKey: "Mod+G" },
37
37
  { id: "open-git-status", label: "Git Status (sidebar)", category: "tabs", defaultKey: "Mod+Shift+E" },
38
38
  { id: "open-search", label: "Search Files (sidebar)", category: "tabs", defaultKey: "Mod+Shift+F" },
39
+ { id: "voice-input", label: "Voice Input", category: "general", defaultKey: "Mod+Shift+V", note: "Toggle speech-to-text in chat" },
39
40
  // Projects — Mod+1..9
40
41
  ...Array.from({ length: 9 }, (_, i) => ({
41
42
  id: `switch-project-${i + 1}`,
@@ -257,9 +257,9 @@ export const usePanelStore = create<PanelStore>()((set, get) => {
257
257
  : newTabs[newTabs.length - 1]?.id ?? null;
258
258
  }
259
259
 
260
- // Auto-close panel if empty and not the last one
261
- const panelIds = Object.keys(s.panels);
262
- if (newTabs.length === 0 && panelIds.length > 1) {
260
+ // Auto-close panel if empty and not the last one in current grid
261
+ const gridPanelCount = s.grid.flat().length;
262
+ if (newTabs.length === 0 && gridPanelCount > 1) {
263
263
  const { [pid]: _, ...rest } = s.panels;
264
264
  const newGrid = gridRemovePanel(s.grid, pid);
265
265
  const newFocused = s.focusedPanelId === pid ? Object.keys(rest)[0]! : s.focusedPanelId;
@@ -331,9 +331,9 @@ export const usePanelStore = create<PanelStore>()((set, get) => {
331
331
  else toTabs.push(tab);
332
332
 
333
333
  set((s) => {
334
- const panelIds = Object.keys(s.panels);
335
- // Auto-close empty source panel if not last
336
- if (fromTabs.length === 0 && panelIds.length > 1) {
334
+ const gridPanelCount = s.grid.flat().length;
335
+ // Auto-close empty source panel if not last in current grid
336
+ if (fromTabs.length === 0 && gridPanelCount > 1) {
337
337
  const { [fromPanelId]: _, ...rest } = s.panels;
338
338
  return {
339
339
  panels: {
@@ -405,14 +405,14 @@ export const usePanelStore = create<PanelStore>()((set, get) => {
405
405
  }
406
406
 
407
407
  set((s) => {
408
- const panelIds = Object.keys(s.panels);
408
+ const gridPanelCount = s.grid.flat().length;
409
409
  let updatedPanels = {
410
410
  ...s.panels,
411
411
  [newPanel.id]: newPanel,
412
412
  };
413
413
 
414
- // If source is now empty and not last panel, remove it
415
- if (srcTabs.length === 0 && panelIds.length > 1) {
414
+ // If source is now empty and not last panel in grid, remove it
415
+ if (srcTabs.length === 0 && gridPanelCount > 1) {
416
416
  const { [sourcePanelId]: _, ...rest } = updatedPanels;
417
417
  updatedPanels = rest;
418
418
  newGrid = gridRemovePanel(newGrid, sourcePanelId);
@@ -428,7 +428,7 @@ export const usePanelStore = create<PanelStore>()((set, get) => {
428
428
 
429
429
  closePanel: (panelId) => {
430
430
  const { panels, grid } = get();
431
- if (Object.keys(panels).length <= 1) return;
431
+ if (grid.flat().length <= 1) return;
432
432
 
433
433
  const panel = panels[panelId];
434
434
  if (!panel) return;
@@ -10,7 +10,8 @@ export type TabType =
10
10
  | "postgres"
11
11
  | "git-graph"
12
12
  | "git-diff"
13
- | "settings";
13
+ | "settings"
14
+ | "browser";
14
15
 
15
16
  export interface Tab {
16
17
  id: string;
@@ -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};