@hienlh/ppm 0.9.92 → 0.9.94

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 (222) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/web/assets/ai-settings-section-LMO_cfIW.js +1 -0
  3. package/dist/web/assets/api-client-o_6TmLGC.js +1 -0
  4. package/dist/web/assets/api-settings-CoKe_BdR.js +1 -0
  5. package/dist/web/assets/architecture-PBZL5I3N-CUZIB1Vq.js +1 -0
  6. package/dist/web/assets/arrow-up-Dtrfv490.js +1 -0
  7. package/dist/web/assets/chat-tab-DQNdrUvL.js +10 -0
  8. package/dist/web/assets/chevron-right-BzAdxJRG.js +1 -0
  9. package/dist/web/assets/code-CuravVys.js +1 -0
  10. package/dist/web/assets/code-editor-B4XNYHnl.js +8 -0
  11. package/dist/web/assets/columns-2-4fQcE4PF.js +1 -0
  12. package/dist/web/assets/conflict-editor-BcsRDSCw.js +19 -0
  13. package/dist/web/assets/createLucideIcon-BjHrJDVb.js +1 -0
  14. package/dist/web/assets/{csv-preview-BZRICDP0.js → csv-preview-BizIVMyb.js} +2 -2
  15. package/dist/web/assets/database-D4DIhgi-.js +1 -0
  16. package/dist/web/assets/database-viewer-CfzAAtm3.js +2 -0
  17. package/dist/web/assets/diff-viewer-DgA9z9Ux.js +4 -0
  18. package/dist/web/assets/dist-C5IgeqrV.js +1 -0
  19. package/dist/web/assets/dist-im4ynINo.js +11 -0
  20. package/dist/web/assets/esm-K1XIK4vc.js +2 -0
  21. package/dist/web/assets/extension-store-3yZYn07W.js +1 -0
  22. package/dist/web/assets/extension-webview-gHGB2Nw2.js +3 -0
  23. package/dist/web/assets/gitGraph-HDMCJU4V-CtOMUphQ.js +1 -0
  24. package/dist/web/assets/index-B4mGNywE.js +26 -0
  25. package/dist/web/assets/index-BZ4G-2BK.css +2 -0
  26. package/dist/web/assets/info-3K5VOQVL-BCrPCWGY.js +1 -0
  27. package/dist/web/assets/input-CHRMley8.js +1 -0
  28. package/dist/web/assets/keybindings-store-BAuymsWd.js +1 -0
  29. package/dist/web/assets/keybindings-store-BKyNIeFB.js +1 -0
  30. package/dist/web/assets/{lib-DSLzfeW0.js → lib-D_kRA9p6.js} +1 -1
  31. package/dist/web/assets/markdown-renderer-sIjU5LtB.js +3 -0
  32. package/dist/web/assets/packet-RMMSAZCW-D_OqB-zi.js +1 -0
  33. package/dist/web/assets/pie-UPGHQEXC-WUHpLNJz.js +1 -0
  34. package/dist/web/assets/plus-51UQ45rf.js +1 -0
  35. package/dist/web/assets/port-forwarding-tab-BMXnuRuI.js +1 -0
  36. package/dist/web/assets/postgres-viewer-B6Wj5xiN.js +3 -0
  37. package/dist/web/assets/project-store-Ciq-cK1O.js +1 -0
  38. package/dist/web/assets/radar-KQ55EAFF-HQIIecVM.js +1 -0
  39. package/dist/web/assets/react-GqWghJ-L.js +1 -0
  40. package/dist/web/assets/refresh-cw-CSFrDtiu.js +1 -0
  41. package/dist/web/assets/scroll-area-DwWF9FpN.js +1 -0
  42. package/dist/web/assets/settings-store-B470PCWf.js +2 -0
  43. package/dist/web/assets/settings-tab-BKQo79HU.js +1 -0
  44. package/dist/web/assets/{sql-query-editor-DkBHKM2i.js → sql-query-editor-DZ9xskL8.js} +1 -1
  45. package/dist/web/assets/sqlite-viewer-CytNesG3.js +1 -0
  46. package/dist/web/assets/square-nsMa3iMk.js +1 -0
  47. package/dist/web/assets/tab-store-DZbiYk7y.js +1 -0
  48. package/dist/web/assets/table-Dq575bPF.js +1 -0
  49. package/dist/web/assets/terminal-tab-DjfxKMSB.js +1 -0
  50. package/dist/web/assets/text-wrap-Cn6BNQfq.js +1 -0
  51. package/dist/web/assets/trash-2-CJYoLw7Q.js +1 -0
  52. package/dist/web/assets/treemap-KZPCXAKY-0wLgUUTz.js +1 -0
  53. package/dist/web/assets/{use-monaco-theme-BEX-OBkP.js → use-monaco-theme-OY18iXNi.js} +1 -1
  54. package/dist/web/assets/vendor-markdown-0Mxgxy0L.js +295 -0
  55. package/dist/web/assets/vendor-mermaid-B2SLgECS.js +2657 -0
  56. package/dist/web/assets/vendor-ui-B-T_damt.js +45 -0
  57. package/dist/web/assets/vendor-xterm-ejLe7-tK.js +36 -0
  58. package/dist/web/assets/x-DlFGzN8d.js +1 -0
  59. package/dist/web/index.html +26 -21
  60. package/dist/web/sw.js +1 -1
  61. package/docs/code-standards.md +56 -4
  62. package/docs/journals/260415-frontend-memory-optimization.md +73 -0
  63. package/docs/project-changelog.md +11 -1
  64. package/docs/system-architecture.md +36 -0
  65. package/package.json +1 -1
  66. package/packages/ext-git-graph/src/extension.ts +5 -20
  67. package/src/web/components/chat/message-list.tsx +59 -22
  68. package/src/web/components/chat/tool-cards.tsx +11 -4
  69. package/src/web/components/database/data-grid.tsx +2 -1
  70. package/src/web/components/editor/code-editor.tsx +14 -8
  71. package/src/web/components/editor/conflict-editor.tsx +2 -1
  72. package/src/web/components/editor/diff-viewer.tsx +2 -1
  73. package/src/web/components/editor/editor-breadcrumb.tsx +2 -1
  74. package/src/web/components/explorer/file-tree.tsx +6 -5
  75. package/src/web/components/explorer/search-panel.tsx +2 -1
  76. package/src/web/components/extensions/extension-webview.tsx +9 -3
  77. package/src/web/components/git/git-status-panel.tsx +2 -1
  78. package/src/web/components/layout/add-project-form.tsx +2 -1
  79. package/src/web/components/layout/mobile-drawer.tsx +2 -1
  80. package/src/web/components/layout/mobile-nav.tsx +2 -1
  81. package/src/web/components/layout/panel-layout.tsx +3 -3
  82. package/src/web/components/layout/project-bar.tsx +7 -6
  83. package/src/web/components/layout/project-bottom-sheet.tsx +2 -1
  84. package/src/web/components/layout/sidebar.tsx +5 -4
  85. package/src/web/components/layout/status-bar.tsx +5 -4
  86. package/src/web/components/layout/tab-bar.tsx +3 -3
  87. package/src/web/components/layout/tab-content.tsx +2 -1
  88. package/src/web/components/postgres/postgres-viewer.tsx +7 -5
  89. package/src/web/components/settings/settings-tab.tsx +2 -1
  90. package/src/web/components/shared/markdown-code-block.tsx +10 -8
  91. package/src/web/components/terminal/terminal-tab.tsx +3 -3
  92. package/src/web/hooks/use-chat.ts +4 -1
  93. package/src/web/hooks/use-extension-ws.ts +29 -10
  94. package/vite.config.ts +17 -0
  95. package/dist/web/assets/_basePickBy-Bj0dI1ei.js +0 -1
  96. package/dist/web/assets/_baseUniq-CyzdZeQH.js +0 -1
  97. package/dist/web/assets/ai-settings-section-Bo9lCaTd.js +0 -1
  98. package/dist/web/assets/api-client-BvxmRZUi.js +0 -1
  99. package/dist/web/assets/api-settings-CUxg9RE5.js +0 -1
  100. package/dist/web/assets/arc-CxgHJ7Z4.js +0 -1
  101. package/dist/web/assets/architecture-PBZL5I3N-DDFO_NKq.js +0 -1
  102. package/dist/web/assets/architectureDiagram-2XIMDMQ5-D16OotsC.js +0 -36
  103. package/dist/web/assets/array-BFDiaBgf.js +0 -1
  104. package/dist/web/assets/arrow-up-I9-21gkR.js +0 -1
  105. package/dist/web/assets/blockDiagram-WCTKOSBZ-Ct57Wtfk.js +0 -132
  106. package/dist/web/assets/c4Diagram-IC4MRINW-BIymcNsg.js +0 -10
  107. package/dist/web/assets/channel-wumTB1if.js +0 -1
  108. package/dist/web/assets/chat-tab-B3sYWC6s.js +0 -10
  109. package/dist/web/assets/chevron-right-DY_wImxB.js +0 -1
  110. package/dist/web/assets/chunk-4BX2VUAB-CENmY7Kw.js +0 -1
  111. package/dist/web/assets/chunk-55IACEB6-DhZGI1l3.js +0 -1
  112. package/dist/web/assets/chunk-7E7YKBS2-DZcnC7Ow.js +0 -1
  113. package/dist/web/assets/chunk-7R4GIKGN-y8bfHEy-.js +0 -80
  114. package/dist/web/assets/chunk-C72U2L5F-BHPkfQj2.js +0 -1
  115. package/dist/web/assets/chunk-EGIJ26TM-nant2LXl.js +0 -1
  116. package/dist/web/assets/chunk-FMBD7UC4-Bog4cpN-.js +0 -15
  117. package/dist/web/assets/chunk-GEFDOKGD-86LFbsAC.js +0 -2
  118. package/dist/web/assets/chunk-GLR3WWYH-Re-5eSlQ.js +0 -2
  119. package/dist/web/assets/chunk-HHEYEP7N-C45i5G_3.js +0 -1
  120. package/dist/web/assets/chunk-JSJVCQXG-23eG9mgt.js +0 -1
  121. package/dist/web/assets/chunk-KX2RTZJC-CHj8TnTB.js +0 -1
  122. package/dist/web/assets/chunk-KYZI473N-gqRLpJ4w.js +0 -53
  123. package/dist/web/assets/chunk-L3YUKLVL-DnSMmNFC.js +0 -1
  124. package/dist/web/assets/chunk-MX3YWQON-B6g1ZH9X.js +0 -1
  125. package/dist/web/assets/chunk-NQ4KR5QH-DX32345Y.js +0 -220
  126. package/dist/web/assets/chunk-O4XLMI2P-Vp_V4P-b.js +0 -7
  127. package/dist/web/assets/chunk-OZEHJAEY-lKq2SWjA.js +0 -1
  128. package/dist/web/assets/chunk-PQ6SQG4A-Bik13fTV.js +0 -1
  129. package/dist/web/assets/chunk-PU5JKC2W-DD95Rx35.js +0 -70
  130. package/dist/web/assets/chunk-QZHKN3VN-N3VXx1VH.js +0 -1
  131. package/dist/web/assets/chunk-R5LLSJPH-dRhXRnrb.js +0 -1
  132. package/dist/web/assets/chunk-WL4C6EOR-B1iIvLOG.js +0 -189
  133. package/dist/web/assets/chunk-XIRO2GV7-DZBoNl1_.js +0 -1
  134. package/dist/web/assets/chunk-XPW4576I-CgLyyW03.js +0 -32
  135. package/dist/web/assets/chunk-XZSTWKYB-DjV8xl5A.js +0 -94
  136. package/dist/web/assets/chunk-YBOYWFTD-D_ILLe6_.js +0 -1
  137. package/dist/web/assets/classDiagram-VBA2DB6C-mr-Cb1me.js +0 -1
  138. package/dist/web/assets/classDiagram-v2-RAHNMMFH-BKe8_uda.js +0 -1
  139. package/dist/web/assets/clone--z5KLAuR.js +0 -1
  140. package/dist/web/assets/code-editor-GQ-BPapW.js +0 -8
  141. package/dist/web/assets/columns-2-IeETSfON.js +0 -1
  142. package/dist/web/assets/conflict-editor-BzEbBMdo.js +0 -19
  143. package/dist/web/assets/cose-bilkent-S5V4N54A-BGNPFv3x.js +0 -1
  144. package/dist/web/assets/cytoscape.esm-C8i2jUzT.js +0 -321
  145. package/dist/web/assets/dagre-CkhlMHnx.js +0 -1
  146. package/dist/web/assets/dagre-KLK3FWXG-Cnp996VG.js +0 -4
  147. package/dist/web/assets/database-CgTomMxt.js +0 -1
  148. package/dist/web/assets/database-viewer-hoe7AAOc.js +0 -2
  149. package/dist/web/assets/defaultLocale-ZeknFqNe.js +0 -1
  150. package/dist/web/assets/diagram-E7M64L7V-BZF0tSOr.js +0 -24
  151. package/dist/web/assets/diagram-IFDJBPK2-nUcO8sN8.js +0 -43
  152. package/dist/web/assets/diagram-P4PSJMXO-CW0eCkwC.js +0 -24
  153. package/dist/web/assets/diff-viewer-tjZrVKtL.js +0 -4
  154. package/dist/web/assets/dist-CM0oD8tQ.js +0 -1
  155. package/dist/web/assets/dist-DZmJeHOA.js +0 -1
  156. package/dist/web/assets/erDiagram-INFDFZHY-DSkriYZ9.js +0 -70
  157. package/dist/web/assets/extension-webview-BeqBWL7v.js +0 -3
  158. package/dist/web/assets/flowDiagram-PKNHOUZH-CFYAfZBx.js +0 -162
  159. package/dist/web/assets/ganttDiagram-A5KZAMGK-KSn4XAU4.js +0 -292
  160. package/dist/web/assets/gitGraph-HDMCJU4V-OkvBPi6H.js +0 -1
  161. package/dist/web/assets/gitGraphDiagram-K3NZZRJ6-BMgjjVys.js +0 -65
  162. package/dist/web/assets/graphlib-BWe1iK_s.js +0 -1
  163. package/dist/web/assets/index-Chf0otez.css +0 -2
  164. package/dist/web/assets/index-fzSsl_Dg.js +0 -26
  165. package/dist/web/assets/info-3K5VOQVL-BDU2_bYD.js +0 -1
  166. package/dist/web/assets/infoDiagram-LFFYTUFH-Diq4Cyc3.js +0 -2
  167. package/dist/web/assets/init-0VJVrkRJ.js +0 -1
  168. package/dist/web/assets/input-BHj0veau.js +0 -45
  169. package/dist/web/assets/isArrayLikeObject-ClzWCpcm.js +0 -1
  170. package/dist/web/assets/isEmpty-BfLnxq-B.js +0 -1
  171. package/dist/web/assets/ishikawaDiagram-PHBUUO56-CiVEvp8o.js +0 -70
  172. package/dist/web/assets/journeyDiagram-4ABVD52K-CG_v5Aho.js +0 -139
  173. package/dist/web/assets/jsx-runtime-BRW_vwa9.js +0 -1
  174. package/dist/web/assets/kanban-definition-K7BYSVSG-miB0-_Zq.js +0 -89
  175. package/dist/web/assets/keybindings-store-DpSn1fWY.js +0 -1
  176. package/dist/web/assets/line-CSuSrJ9J.js +0 -1
  177. package/dist/web/assets/linear-DFN_MPsw.js +0 -1
  178. package/dist/web/assets/markdown-renderer-Pch6Q5AB.js +0 -306
  179. package/dist/web/assets/math-CRc16Nj6.js +0 -1
  180. package/dist/web/assets/mermaid-parser.core-CFdP1Z5_.js +0 -4
  181. package/dist/web/assets/mindmap-definition-YRQLILUH-pYPWwASE.js +0 -68
  182. package/dist/web/assets/ordinal-DpFn432U.js +0 -1
  183. package/dist/web/assets/packet-RMMSAZCW-BwpIpYB3.js +0 -1
  184. package/dist/web/assets/path-INs8XTPH.js +0 -1
  185. package/dist/web/assets/pie-UPGHQEXC-BPgAfmes.js +0 -1
  186. package/dist/web/assets/pieDiagram-SKSYHLDU-Dovdlvhu.js +0 -30
  187. package/dist/web/assets/plus-DQGIb4mQ.js +0 -1
  188. package/dist/web/assets/port-forwarding-tab-2emRdRba.js +0 -1
  189. package/dist/web/assets/postgres-viewer-3yrH60xU.js +0 -13
  190. package/dist/web/assets/preload-helper-mr3rCizq.js +0 -1
  191. package/dist/web/assets/quadrantDiagram-337W2JSQ-TXe6cU_F.js +0 -7
  192. package/dist/web/assets/radar-KQ55EAFF-TqxBkWx-.js +0 -1
  193. package/dist/web/assets/react-0tkk-ztn.js +0 -1
  194. package/dist/web/assets/react-dom-Bpkvzu3U.js +0 -1
  195. package/dist/web/assets/react-nm2Ru1Pt.js +0 -1
  196. package/dist/web/assets/refresh-cw-Clk8fdUD.js +0 -1
  197. package/dist/web/assets/requirementDiagram-Z7DCOOCP-CuiiuGS9.js +0 -73
  198. package/dist/web/assets/rough.esm-eLccZ4OJ.js +0 -1
  199. package/dist/web/assets/sankeyDiagram-WA2Y5GQK-BbRmhv0t.js +0 -10
  200. package/dist/web/assets/scroll-area-BpXCNme3.js +0 -1
  201. package/dist/web/assets/sequenceDiagram-2WXFIKYE-B2D8IQDb.js +0 -145
  202. package/dist/web/assets/settings-tab-DdyI-_Vr.js +0 -1
  203. package/dist/web/assets/sqlite-viewer-DnCkYz-y.js +0 -1
  204. package/dist/web/assets/square-vBdqj0bF.js +0 -1
  205. package/dist/web/assets/src-CqyWLlNZ.js +0 -1
  206. package/dist/web/assets/stateDiagram-RAJIS63D-ylr4HxPu.js +0 -1
  207. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-D6zvxf3M.js +0 -1
  208. package/dist/web/assets/table-Bi27fEaN.js +0 -1
  209. package/dist/web/assets/terminal-tab-C5lCh18X.js +0 -36
  210. package/dist/web/assets/text-wrap-D_OmSzhp.js +0 -1
  211. package/dist/web/assets/timeline-definition-YZTLITO2-pMv1grvM.js +0 -61
  212. package/dist/web/assets/trash-2-CNuB-htI.js +0 -1
  213. package/dist/web/assets/treemap-KZPCXAKY-Kck06FKU.js +0 -1
  214. package/dist/web/assets/vennDiagram-LZ73GAT5-C-rkIUbo.js +0 -34
  215. package/dist/web/assets/x-Dw3TjeY_.js +0 -1
  216. package/dist/web/assets/xychartDiagram-JWTSCODW-CtpjAakO.js +0 -7
  217. /package/dist/web/assets/{csv-parser-i7fjqP2H.js → csv-parser--2WJNgS7.js} +0 -0
  218. /package/dist/web/assets/{katex-DR0kdMDv.js → katex-CKoArbIw.js} +0 -0
  219. /package/dist/web/assets/{chunk-CFjPhJqf.js → rolldown-runtime-FhOqtrmT.js} +0 -0
  220. /package/dist/web/assets/{sql-completion-provider-B8uUWWej.js → sql-completion-provider-C3cq9j99.js} +0 -0
  221. /package/dist/web/assets/{utils-DX8jb5qv.js → utils-ChWX7pZv.js} +0 -0
  222. /package/dist/web/assets/{terminal-tab-BrP-ENHg.css → vendor-xterm-BrP-ENHg.css} +0 -0
@@ -2,8 +2,10 @@
2
2
  * Tool card components for chat message rendering.
3
3
  * Handles summary + details for all SDK tool types.
4
4
  */
5
- import { useState, useMemo } from "react";
6
- import { MarkdownRenderer } from "@/components/shared/markdown-renderer";
5
+ import { useState, useMemo, lazy, Suspense } from "react";
6
+ const MarkdownRenderer = lazy(() =>
7
+ import("@/components/shared/markdown-renderer").then((m) => ({ default: m.MarkdownRenderer }))
8
+ );
7
9
  import {
8
10
  ChevronDown,
9
11
  ChevronRight,
@@ -20,6 +22,7 @@ import {
20
22
  Columns2,
21
23
  } from "lucide-react";
22
24
  import type { ChatEvent } from "../../../types/chat";
25
+ import { useShallow } from "zustand/react/shallow";
23
26
  import { useTabStore } from "@/stores/tab-store";
24
27
  import { basename } from "@/lib/utils";
25
28
 
@@ -159,7 +162,7 @@ function ToolDetails({
159
162
  projectName?: string;
160
163
  }) {
161
164
  const s = (v: unknown) => String(v ?? "");
162
- const { openTab } = useTabStore();
165
+ const { openTab } = useTabStore(useShallow((state) => ({ openTab: state.openTab })));
163
166
 
164
167
  /** Open a file in a new editor tab */
165
168
  const openFile = (filePath: string) => {
@@ -451,7 +454,11 @@ function SubagentChildren({ events, projectName }: { events: ChatEvent[]; projec
451
454
 
452
455
  /** Inline markdown renderer for tool details (prompt, result) */
453
456
  function MiniMarkdown({ content, maxHeight = "max-h-48" }: { content: string; maxHeight?: string }) {
454
- return <MarkdownRenderer content={content} className={`text-text-secondary overflow-auto ${maxHeight}`} />;
457
+ return (
458
+ <Suspense fallback={<div className="animate-pulse h-4 bg-muted rounded" />}>
459
+ <MarkdownRenderer content={content} className={`text-text-secondary overflow-auto ${maxHeight}`} />
460
+ </Suspense>
461
+ );
455
462
  }
456
463
 
457
464
 
@@ -1,5 +1,6 @@
1
1
  import { useState, useCallback, useMemo, useRef, memo, useEffect } from "react";
2
2
  import { Loader2, ChevronLeft, ChevronRight, ChevronUp, ChevronDown, Trash2, Plus, Search, X, Eye, Filter, Pin, PinOff, Columns3 } from "lucide-react";
3
+ import { useShallow } from "zustand/react/shallow";
3
4
  import { useTabStore } from "@/stores/tab-store";
4
5
  import type { DbColumnInfo } from "./use-database";
5
6
  import { ExportButton } from "./export-button";
@@ -42,7 +43,7 @@ export function DataGrid({
42
43
  const [insertValues, setInsertValues] = useState<Record<string, string>>({});
43
44
  const [insertError, setInsertError] = useState<string | null>(null);
44
45
  const [confirmBulkDelete, setConfirmBulkDelete] = useState(false);
45
- const { openTab } = useTabStore();
46
+ const { openTab } = useTabStore(useShallow((s) => ({ openTab: s.openTab })));
46
47
  const openCellViewer = useCallback((cell: { col: string; value: string }) => {
47
48
  openTab({
48
49
  type: "editor",
@@ -1,8 +1,8 @@
1
- import { useEffect, useState, useCallback, useRef, useMemo } from "react";
1
+ import { useEffect, useState, useCallback, useRef, useMemo, memo, lazy, Suspense } from "react";
2
2
  import Editor, { type OnMount } from "@monaco-editor/react";
3
3
  import type * as MonacoType from "monaco-editor";
4
- import { MarkdownRenderer } from "@/components/shared/markdown-renderer";
5
4
  import { api, projectUrl, getAuthToken } from "@/lib/api-client";
5
+ import { useShallow } from "zustand/react/shallow";
6
6
  import { useTabStore } from "@/stores/tab-store";
7
7
  import { usePanelStore } from "@/stores/panel-store";
8
8
  import { useSettingsStore } from "@/stores/settings-store";
@@ -12,10 +12,12 @@ import { Loader2, FileWarning, ExternalLink, Play, Database } from "lucide-react
12
12
  import { EditorBreadcrumb } from "./editor-breadcrumb";
13
13
  import { EditorToolbar } from "./editor-toolbar";
14
14
  import { SaveAsDialog } from "./save-as-dialog";
15
- import { lazy, Suspense } from "react";
16
15
  import { createSqlCompletionProvider, clearCompletionCache, type SchemaInfo } from "../database/sql-completion-provider";
17
16
  import { useConnections, type Connection } from "../database/use-connections";
18
17
 
18
+ const MarkdownRenderer = lazy(() =>
19
+ import("@/components/shared/markdown-renderer").then((m) => ({ default: m.MarkdownRenderer }))
20
+ );
19
21
  const CsvPreview = lazy(() => import("./csv-preview").then((m) => ({ default: m.CsvPreview })));
20
22
 
21
23
  /** Image extensions renderable inline */
@@ -47,7 +49,7 @@ interface CodeEditorProps {
47
49
  tabId?: string;
48
50
  }
49
51
 
50
- export function CodeEditor({ metadata, tabId }: CodeEditorProps) {
52
+ export const CodeEditor = memo(function CodeEditor({ metadata, tabId }: CodeEditorProps) {
51
53
  const filePath = metadata?.filePath as string | undefined;
52
54
  const projectName = metadata?.projectName as string | undefined;
53
55
  // Inline content mode: read-only Monaco with pre-loaded content (e.g. cell viewer)
@@ -61,8 +63,8 @@ export function CodeEditor({ metadata, tabId }: CodeEditorProps) {
61
63
  const saveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
62
64
  const latestContentRef = useRef<string>("");
63
65
  const editorRef = useRef<MonacoType.editor.IStandaloneCodeEditor | null>(null);
64
- const { tabs, updateTab } = useTabStore();
65
- const { wordWrap, toggleWordWrap } = useSettingsStore();
66
+ const { tabs, updateTab } = useTabStore(useShallow((s) => ({ tabs: s.tabs, updateTab: s.updateTab })));
67
+ const { wordWrap, toggleWordWrap } = useSettingsStore(useShallow((s) => ({ wordWrap: s.wordWrap, toggleWordWrap: s.toggleWordWrap })));
66
68
  const monacoTheme = useMonacoTheme();
67
69
 
68
70
  const isUntitled = metadata?.isUntitled === true;
@@ -553,10 +555,14 @@ export function CodeEditor({ metadata, tabId }: CodeEditorProps) {
553
555
  )}
554
556
  </div>
555
557
  );
556
- }
558
+ });
557
559
 
558
560
  function MarkdownPreview({ content }: { content: string }) {
559
- return <MarkdownRenderer content={content} className="flex-1 overflow-auto p-4" />;
561
+ return (
562
+ <Suspense fallback={<div className="animate-pulse h-4 bg-muted rounded m-4" />}>
563
+ <MarkdownRenderer content={content} className="flex-1 overflow-auto p-4" />
564
+ </Suspense>
565
+ );
560
566
  }
561
567
 
562
568
  function ImagePreview({ filePath, projectName }: { filePath: string; projectName: string }) {
@@ -2,6 +2,7 @@ import { useEffect, useState, useRef, useCallback } from "react";
2
2
  import Editor, { type OnMount } from "@monaco-editor/react";
3
3
  import type * as MonacoType from "monaco-editor";
4
4
  import { api, projectUrl } from "@/lib/api-client";
5
+ import { useShallow } from "zustand/react/shallow";
5
6
  import { useSettingsStore } from "@/stores/settings-store";
6
7
  import { useMonacoTheme } from "@/lib/use-monaco-theme";
7
8
  import { Loader2 } from "lucide-react";
@@ -100,7 +101,7 @@ export function ConflictEditor({ metadata }: ConflictEditorProps) {
100
101
  const widgetsRef = useRef<MonacoType.editor.IContentWidget[]>([]);
101
102
  const decorationsRef = useRef<MonacoType.editor.IEditorDecorationsCollection | null>(null);
102
103
 
103
- const { wordWrap } = useSettingsStore();
104
+ const { wordWrap } = useSettingsStore(useShallow((s) => ({ wordWrap: s.wordWrap })));
104
105
  const monacoTheme = useMonacoTheme();
105
106
 
106
107
  const containerRef = useRef<HTMLDivElement>(null);
@@ -1,6 +1,7 @@
1
1
  import { useEffect, useState, useMemo, useRef } from "react";
2
2
  import { DiffEditor } from "@monaco-editor/react";
3
3
  import { api, projectUrl } from "@/lib/api-client";
4
+ import { useShallow } from "zustand/react/shallow";
4
5
  import { useSettingsStore } from "@/stores/settings-store";
5
6
  import { useMonacoTheme } from "@/lib/use-monaco-theme";
6
7
  import { Loader2, FileCode, PanelLeftOpen, PanelRightOpen, Columns2, WrapText } from "lucide-react";
@@ -40,7 +41,7 @@ export function DiffViewer({ metadata }: DiffViewerProps) {
40
41
  const [loading, setLoading] = useState(!isInline);
41
42
  const [error, setError] = useState<string | null>(null);
42
43
  const [expandMode, setExpandMode] = useState<"both" | "left" | "right">("both");
43
- const { wordWrap, toggleWordWrap } = useSettingsStore();
44
+ const { wordWrap, toggleWordWrap } = useSettingsStore(useShallow((s) => ({ wordWrap: s.wordWrap, toggleWordWrap: s.toggleWordWrap })));
44
45
  const monacoTheme = useMonacoTheme();
45
46
 
46
47
  // Measure container height — Monaco needs explicit pixel height on mobile
@@ -10,6 +10,7 @@ import {
10
10
  DropdownMenuSubContent,
11
11
  } from "@/components/ui/dropdown-menu";
12
12
  import { useFileStore, type FileNode } from "@/stores/file-store";
13
+ import { useShallow } from "zustand/react/shallow";
13
14
  import { useTabStore } from "@/stores/tab-store";
14
15
  import { basename } from "@/lib/utils";
15
16
 
@@ -83,7 +84,7 @@ interface EditorBreadcrumbProps {
83
84
 
84
85
  export function EditorBreadcrumb({ filePath, projectName, tabId, className }: EditorBreadcrumbProps) {
85
86
  const tree = useFileStore((s) => s.tree);
86
- const { updateTab, openTab } = useTabStore();
87
+ const { updateTab, openTab } = useTabStore(useShallow((s) => ({ updateTab: s.updateTab, openTab: s.openTab })));
87
88
  const scrollRef = useRef<HTMLDivElement>(null);
88
89
 
89
90
  const segments = useMemo(
@@ -1,4 +1,4 @@
1
- import { useEffect, useCallback, useState } from "react";
1
+ import { useEffect, useCallback, useState, memo } from "react";
2
2
  import {
3
3
  Folder,
4
4
  FolderOpen,
@@ -12,6 +12,7 @@ import {
12
12
  Download,
13
13
  Loader2,
14
14
  } from "lucide-react";
15
+ import { useShallow } from "zustand/react/shallow";
15
16
  import { useFileStore, type FileNode } from "@/stores/file-store";
16
17
  import { useProjectStore } from "@/stores/project-store";
17
18
  import { useTabStore } from "@/stores/tab-store";
@@ -58,8 +59,8 @@ interface TreeNodeProps {
58
59
  onFileOpen?: () => void;
59
60
  }
60
61
 
61
- function TreeNode({ node, depth, projectName, onAction, onFileOpen }: TreeNodeProps) {
62
- const { expandedPaths, toggleExpand, selectedFiles, toggleFileSelect } = useFileStore();
62
+ const TreeNode = memo(function TreeNode({ node, depth, projectName, onAction, onFileOpen }: TreeNodeProps) {
63
+ const { expandedPaths, toggleExpand, selectedFiles, toggleFileSelect } = useFileStore(useShallow((s) => ({ expandedPaths: s.expandedPaths, toggleExpand: s.toggleExpand, selectedFiles: s.selectedFiles, toggleFileSelect: s.toggleFileSelect })));
63
64
  const openTab = useTabStore((s) => s.openTab);
64
65
  const isExpanded = expandedPaths.has(node.path);
65
66
  const isDir = node.type === "directory";
@@ -187,14 +188,14 @@ function TreeNode({ node, depth, projectName, onAction, onFileOpen }: TreeNodePr
187
188
  ))}
188
189
  </div>
189
190
  );
190
- }
191
+ });
191
192
 
192
193
  interface FileTreeProps {
193
194
  onFileOpen?: () => void;
194
195
  }
195
196
 
196
197
  export function FileTree({ onFileOpen }: FileTreeProps = {}) {
197
- const { tree, loading, error, fetchTree, reset, selectedFiles, clearSelection } = useFileStore();
198
+ const { tree, loading, error, fetchTree, reset, selectedFiles, clearSelection } = useFileStore(useShallow((s) => ({ tree: s.tree, loading: s.loading, error: s.error, fetchTree: s.fetchTree, reset: s.reset, selectedFiles: s.selectedFiles, clearSelection: s.clearSelection })));
198
199
  const activeProject = useProjectStore((s) => s.activeProject);
199
200
  const openTab = useTabStore((s) => s.openTab);
200
201
  const [actionState, setActionState] = useState<{
@@ -1,5 +1,6 @@
1
1
  import { useState, useRef, useCallback, useEffect } from "react";
2
2
  import { Search, CaseSensitive, ChevronRight, ChevronDown, FileText, X, Loader2, WholeWord, Regex, ReplaceAll } from "lucide-react";
3
+ import { useShallow } from "zustand/react/shallow";
3
4
  import { useProjectStore } from "@/stores/project-store";
4
5
  import { useTabStore } from "@/stores/tab-store";
5
6
  import { projectUrl, api } from "@/lib/api-client";
@@ -62,7 +63,7 @@ function OptionButton({ active, onClick, title, children }: { active: boolean; o
62
63
  }
63
64
 
64
65
  export function SearchPanel() {
65
- const { activeProject } = useProjectStore();
66
+ const { activeProject } = useProjectStore(useShallow((s) => ({ activeProject: s.activeProject })));
66
67
  const openTab = useTabStore((s) => s.openTab);
67
68
 
68
69
  const [query, setQuery] = useState("");
@@ -30,7 +30,8 @@ export function ExtensionWebview({ metadata }: ExtensionWebviewProps) {
30
30
  const panelId = metadata?.panelId as string | undefined;
31
31
  const viewType = metadata?.viewType as string | undefined;
32
32
  const currentProject = useTabStore((s) => s.currentProject);
33
- const projectName = (metadata?.projectName as string | undefined) || currentProject || undefined;
33
+ // Prefer currentProject (reflects URL/active project) over stale tab metadata
34
+ const projectName = currentProject || (metadata?.projectName as string | undefined) || undefined;
34
35
  const [timedOut, setTimedOut] = useState(false);
35
36
 
36
37
  // Match panel: prefer panelId (exact), fallback to viewType match (reload recovery)
@@ -57,6 +58,8 @@ export function ExtensionWebview({ metadata }: ExtensionWebviewProps) {
57
58
  // Retry needed because WS connection may not be ready on first attempt
58
59
  useEffect(() => {
59
60
  if (panel || !viewType) return;
61
+ // Mark project as "dispatched" so project-sync effect doesn't double-dispatch
62
+ if (projectName) prevProjectRef.current = projectName;
60
63
  const command = viewType.includes(".") ? viewType : `${viewType}.view`;
61
64
  let cancelled = false;
62
65
  let resolvedArgs: unknown[] | null = null;
@@ -199,8 +202,10 @@ export function ExtensionWebview({ metadata }: ExtensionWebviewProps) {
199
202
  return () => window.removeEventListener("ext:webview:message", handler);
200
203
  }, [resolvedPanelId]);
201
204
 
202
- // Loading state — waiting for extension to create the panel
203
- if (!panel) {
205
+ // Loading state — waiting for extension to create the panel AND deliver HTML.
206
+ // We must wait for HTML before mounting the iframe because browsers don't
207
+ // re-execute scripts when React updates the srcDoc attribute from "" to content.
208
+ if (!panel || !rawHtml) {
204
209
  return (
205
210
  <div className="flex flex-col items-center justify-center h-full gap-3 text-sm text-text-subtle">
206
211
  {timedOut ? (
@@ -230,6 +235,7 @@ export function ExtensionWebview({ metadata }: ExtensionWebviewProps) {
230
235
  <div className="h-full w-full relative">
231
236
  <iframe
232
237
  ref={iframeRef}
238
+ key={resolvedPanelId}
233
239
  srcDoc={html}
234
240
  sandbox="allow-scripts"
235
241
  className="w-full h-full border-0 bg-white dark:bg-zinc-900"
@@ -15,6 +15,7 @@ import {
15
15
  } from "lucide-react";
16
16
  import { api, projectUrl } from "@/lib/api-client";
17
17
  import { basename } from "@/lib/utils";
18
+ import { useShallow } from "zustand/react/shallow";
18
19
  import { useTabStore } from "@/stores/tab-store";
19
20
  import { useSettingsStore } from "@/stores/settings-store";
20
21
  import { useProjectStore } from "@/stores/project-store";
@@ -117,7 +118,7 @@ export function GitStatusPanel({ metadata, tabId, onNavigate }: GitStatusPanelPr
117
118
  label: string;
118
119
  files: string[];
119
120
  } | null>(null);
120
- const { openTab } = useTabStore();
121
+ const { openTab } = useTabStore(useShallow((s) => ({ openTab: s.openTab })));
121
122
  const viewMode = useSettingsStore((s) => s.gitStatusViewMode);
122
123
  const setViewMode = useSettingsStore((s) => s.setGitStatusViewMode);
123
124
  const activeProjectPath = useProjectStore((s) =>
@@ -1,5 +1,6 @@
1
1
  import { useState, useEffect, useRef } from "react";
2
2
  import { Loader2, FolderOpen } from "lucide-react";
3
+ import { useShallow } from "zustand/react/shallow";
3
4
  import { useProjectStore } from "@/stores/project-store";
4
5
  import { api } from "@/lib/api-client";
5
6
  import { cn } from "@/lib/utils";
@@ -18,7 +19,7 @@ interface AddProjectFormProps {
18
19
  }
19
20
 
20
21
  export function AddProjectForm({ onSuccess, onCancel, footerClassName }: AddProjectFormProps) {
21
- const { addProject } = useProjectStore();
22
+ const { addProject } = useProjectStore(useShallow((s) => ({ addProject: s.addProject })));
22
23
  const [path, setPath] = useState("");
23
24
  const [name, setName] = useState("");
24
25
  const [suggestions, setSuggestions] = useState<SuggestedDir[]>([]);
@@ -2,6 +2,7 @@ import { useState, useCallback, useEffect } from "react";
2
2
  import {
3
3
  X, Bug, FolderOpen, GitBranch, Settings, Database,
4
4
  } from "lucide-react";
5
+ import { useShallow } from "zustand/react/shallow";
5
6
  import { useProjectStore } from "@/stores/project-store";
6
7
  import { useSettingsStore } from "@/stores/settings-store";
7
8
  import { FileTree } from "@/components/explorer/file-tree";
@@ -28,7 +29,7 @@ interface MobileDrawerProps {
28
29
  }
29
30
 
30
31
  export function MobileDrawer({ isOpen, onClose, initialTab }: MobileDrawerProps) {
31
- const { activeProject } = useProjectStore();
32
+ const { activeProject } = useProjectStore(useShallow((s) => ({ activeProject: s.activeProject })));
32
33
  const version = useSettingsStore((s) => s.version);
33
34
  const [activeTab, setActiveTab] = useState<DrawerTab>(initialTab ?? "explorer");
34
35
 
@@ -5,6 +5,7 @@ import {
5
5
  ChevronRight, Globe, Puzzle, Copy, Download, Pencil, Trash2,
6
6
  } from "lucide-react";
7
7
  import { usePanelStore } from "@/stores/panel-store";
8
+ import { useShallow } from "zustand/react/shallow";
8
9
  import { useProjectStore, resolveOrder } from "@/stores/project-store";
9
10
  import { useFileStore, type FileNode } from "@/stores/file-store";
10
11
  import { findPanelPosition, MAX_ROWS } from "@/stores/panel-utils";
@@ -152,7 +153,7 @@ export function MobileNav({ onMenuPress, onProjectsPress }: MobileNavProps) {
152
153
  }
153
154
 
154
155
  // Active project avatar for the Projects button
155
- const { activeProject, projects, customOrder } = useProjectStore();
156
+ const { activeProject, projects, customOrder } = useProjectStore(useShallow((s) => ({ activeProject: s.activeProject, projects: s.projects, customOrder: s.customOrder })));
156
157
  const ordered = resolveOrder(projects, customOrder ?? null);
157
158
  const allNames = ordered.map((p) => p.name);
158
159
  const activeIdx = ordered.findIndex((p) => p.name === activeProject?.name);
@@ -1,4 +1,4 @@
1
- import { useEffect } from "react";
1
+ import { useEffect, memo } from "react";
2
2
  import { Panel, Group, Separator } from "react-resizable-panels";
3
3
  import { GripVertical, GripHorizontal } from "lucide-react";
4
4
  import { usePanelStore } from "@/stores/panel-store";
@@ -10,7 +10,7 @@ interface PanelLayoutProps {
10
10
  projectName: string;
11
11
  }
12
12
 
13
- export function PanelLayout({ projectName }: PanelLayoutProps) {
13
+ export const PanelLayout = memo(function PanelLayout({ projectName }: PanelLayoutProps) {
14
14
  const isDesktop = useMediaQuery("(min-width: 768px)");
15
15
  const grid = usePanelStore((s) =>
16
16
  s.currentProject === projectName ? s.grid : (s.projectGrids[projectName] ?? [[]]),
@@ -51,7 +51,7 @@ export function PanelLayout({ projectName }: PanelLayoutProps) {
51
51
  ))}
52
52
  </Group>
53
53
  );
54
- }
54
+ });
55
55
 
56
56
  function RowGroup({ row, rowIdx, totalRows, projectName }: { row: string[]; rowIdx: number; totalRows: number; projectName: string }) {
57
57
  const defaultSize = `${Math.round(100 / totalRows)}%`;
@@ -1,8 +1,9 @@
1
- import { useState, useCallback, useMemo, useRef, useEffect } from "react";
1
+ import { useState, useCallback, useMemo, useRef, useEffect, memo } from "react";
2
2
  import { createPortal } from "react-dom";
3
3
  import { Plus, Settings, Pencil, Trash2, Palette, Bug, Cloud, X, Copy } from "lucide-react";
4
4
  import { CloudSharePopover } from "./cloud-share-popover";
5
5
  import { openBugReportPopup } from "@/lib/report-bug";
6
+ import { useShallow } from "zustand/react/shallow";
6
7
  import { useProjectStore, resolveOrder } from "@/stores/project-store";
7
8
  import { useTabStore } from "@/stores/tab-store";
8
9
  import { useSettingsStore } from "@/stores/settings-store";
@@ -29,7 +30,7 @@ import { cn } from "@/lib/utils";
29
30
  // ---------------------------------------------------------------------------
30
31
  // Avatar circle
31
32
  // ---------------------------------------------------------------------------
32
- function ProjectAvatar({ name, color, active, allNames }: {
33
+ const ProjectAvatar = memo(function ProjectAvatar({ name, color, active, allNames }: {
33
34
  name: string; color: string; active: boolean; allNames: string[];
34
35
  }) {
35
36
  const initials = getProjectInitials(name, allNames);
@@ -51,7 +52,7 @@ function ProjectAvatar({ name, color, active, allNames }: {
51
52
  )}
52
53
  </div>
53
54
  );
54
- }
55
+ });
55
56
 
56
57
  // ---------------------------------------------------------------------------
57
58
  // Color picker popover (inline in dialog)
@@ -78,8 +79,8 @@ function ColorPicker({ current, onChange }: { current: string; onChange: (c: str
78
79
  // ---------------------------------------------------------------------------
79
80
  // ProjectBar
80
81
  // ---------------------------------------------------------------------------
81
- export function ProjectBar() {
82
- const { projects, activeProject, setActiveProject, setProjectColor, reorderProjects, renameProject, deleteProject, customOrder } = useProjectStore();
82
+ export const ProjectBar = memo(function ProjectBar() {
83
+ const { projects, activeProject, setActiveProject, setProjectColor, reorderProjects, renameProject, deleteProject, customOrder } = useProjectStore(useShallow((s) => ({ projects: s.projects, activeProject: s.activeProject, setActiveProject: s.setActiveProject, setProjectColor: s.setProjectColor, reorderProjects: s.reorderProjects, renameProject: s.renameProject, deleteProject: s.deleteProject, customOrder: s.customOrder })));
83
84
  const openTab = useTabStore((s) => s.openTab);
84
85
  const version = useSettingsStore((s) => s.version);
85
86
  const handleReportBug = useCallback(() => openBugReportPopup(version), [version]);
@@ -435,4 +436,4 @@ export function ProjectBar() {
435
436
  </aside>
436
437
  </div>
437
438
  );
438
- }
439
+ });
@@ -1,5 +1,6 @@
1
1
  import { useState, useRef, useCallback } from "react";
2
2
  import { X, Check, Plus, Settings, ChevronUp, ChevronDown, Pencil, Trash2, Palette, ArrowLeft } from "lucide-react";
3
+ import { useShallow } from "zustand/react/shallow";
3
4
  import { useProjectStore, resolveOrder } from "@/stores/project-store";
4
5
  import { useTabStore } from "@/stores/tab-store";
5
6
  import { useSettingsStore } from "@/stores/settings-store";
@@ -34,7 +35,7 @@ function ProjectAvatar({ name, color, allNames }: { name: string; color: string;
34
35
  }
35
36
 
36
37
  export function ProjectBottomSheet({ isOpen, onClose }: ProjectBottomSheetProps) {
37
- const { projects, activeProject, setActiveProject, setProjectColor, reorderProjects, renameProject, deleteProject, customOrder } = useProjectStore();
38
+ const { projects, activeProject, setActiveProject, setProjectColor, reorderProjects, renameProject, deleteProject, customOrder } = useProjectStore(useShallow((s) => ({ projects: s.projects, activeProject: s.activeProject, setActiveProject: s.setActiveProject, setProjectColor: s.setProjectColor, reorderProjects: s.reorderProjects, renameProject: s.renameProject, deleteProject: s.deleteProject, customOrder: s.customOrder })));
38
39
 
39
40
  const openTab = useTabStore((s) => s.openTab);
40
41
  const version = useSettingsStore((s) => s.version);
@@ -1,5 +1,6 @@
1
- import { useCallback, useRef, useMemo } from "react";
1
+ import { useCallback, useRef, useMemo, memo } from "react";
2
2
  import { PanelLeftClose, PanelLeftOpen, FolderOpen, GitBranch, Settings, Database, Search, Puzzle } from "lucide-react";
3
+ import { useShallow } from "zustand/react/shallow";
3
4
  import { useProjectStore } from "@/stores/project-store";
4
5
  import { useSettingsStore, type SidebarActiveTab } from "@/stores/settings-store";
5
6
  import { useExtensionStore } from "@/stores/extension-store";
@@ -58,8 +59,8 @@ function ResizeHandle({ onResize }: { onResize: (width: number) => void }) {
58
59
  );
59
60
  }
60
61
 
61
- export function Sidebar() {
62
- const { activeProject } = useProjectStore();
62
+ export const Sidebar = memo(function Sidebar() {
63
+ const { activeProject } = useProjectStore(useShallow((s) => ({ activeProject: s.activeProject })));
63
64
  const sidebarCollapsed = useSettingsStore((s) => s.sidebarCollapsed);
64
65
  const sidebarWidth = useSettingsStore((s) => s.sidebarWidth);
65
66
  const toggleSidebar = useSettingsStore((s) => s.toggleSidebar);
@@ -169,4 +170,4 @@ export function Sidebar() {
169
170
  <ResizeHandle onResize={setSidebarWidth} />
170
171
  </aside>
171
172
  );
172
- }
173
+ });
@@ -1,8 +1,9 @@
1
+ import { memo } from "react";
1
2
  import { useExtensionStore, type StatusBarItemUI } from "@/stores/extension-store";
2
3
  import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
3
4
 
4
5
  /** Fixed status bar at the bottom of the editor area (hidden on mobile) */
5
- export function StatusBar() {
6
+ export const StatusBar = memo(function StatusBar() {
6
7
  const items = useExtensionStore((s) => s.statusBarItems);
7
8
 
8
9
  const left = items
@@ -27,9 +28,9 @@ export function StatusBar() {
27
28
  </div>
28
29
  </div>
29
30
  );
30
- }
31
+ });
31
32
 
32
- function StatusBarEntry({ item }: { item: StatusBarItemUI }) {
33
+ const StatusBarEntry = memo(function StatusBarEntry({ item }: { item: StatusBarItemUI }) {
33
34
  const content = (
34
35
  <button
35
36
  className={`truncate px-1 rounded-sm transition-colors ${
@@ -61,4 +62,4 @@ function StatusBarEntry({ item }: { item: StatusBarItemUI }) {
61
62
  }
62
63
 
63
64
  return content;
64
- }
65
+ });
@@ -1,4 +1,4 @@
1
- import { useEffect, useRef, useCallback, useState } from "react";
1
+ import { useEffect, useRef, useCallback, useState, memo } from "react";
2
2
  import {
3
3
  Plus,
4
4
  Terminal,
@@ -47,7 +47,7 @@ interface TabBarProps {
47
47
  panelId?: string;
48
48
  }
49
49
 
50
- export function TabBar({ panelId }: TabBarProps) {
50
+ export const TabBar = memo(function TabBar({ panelId }: TabBarProps) {
51
51
  const activeProject = useProjectStore((s) => s.activeProject);
52
52
  const tabRefs = useRef<Map<string, HTMLButtonElement>>(new Map());
53
53
  const scrollRef = useRef<HTMLDivElement>(null);
@@ -267,5 +267,5 @@ export function TabBar({ panelId }: TabBarProps) {
267
267
  )}
268
268
  </>
269
269
  );
270
- }
270
+ });
271
271
 
@@ -1,4 +1,5 @@
1
1
  import { Suspense, lazy } from "react";
2
+ import { useShallow } from "zustand/react/shallow";
2
3
  import { useTabStore, type TabType } from "@/stores/tab-store";
3
4
  import { Loader2 } from "lucide-react";
4
5
 
@@ -74,7 +75,7 @@ function LoadingFallback() {
74
75
  }
75
76
 
76
77
  export function TabContent() {
77
- const { tabs, activeTabId } = useTabStore();
78
+ const { tabs, activeTabId } = useTabStore(useShallow((s) => ({ tabs: s.tabs, activeTabId: s.activeTabId })));
78
79
 
79
80
  if (tabs.length === 0) {
80
81
  return (
@@ -1,7 +1,7 @@
1
- import { useState, useCallback, useMemo, useEffect, useRef } from "react";
1
+ import { useState, useCallback, useMemo, useEffect, useRef, lazy, Suspense } from "react";
2
2
  import { Database, Loader2, AlertCircle, Play, ChevronLeft, ChevronRight, Table, RefreshCw } from "lucide-react";
3
3
  import { useReactTable, getCoreRowModel, flexRender, type ColumnDef } from "@tanstack/react-table";
4
- import CodeMirror from "@uiw/react-codemirror";
4
+ const CodeMirror = lazy(() => import("@uiw/react-codemirror"));
5
5
  import { sql, PostgreSQL } from "@codemirror/lang-sql";
6
6
  import { usePostgres, type PgColumnInfo, type PgQueryResult } from "./use-postgres";
7
7
 
@@ -244,9 +244,11 @@ function PgQueryEditor({ onExecute, result, error, loading }: {
244
244
  <div className="flex flex-col h-full overflow-hidden">
245
245
  <div className="flex items-start gap-1 border-b border-border bg-background" onKeyDown={handleKeyDown}>
246
246
  <div className="flex-1 max-h-[120px] overflow-auto">
247
- <CodeMirror value={query} onChange={setQuery} extensions={[sql({ dialect: PostgreSQL })]}
248
- basicSetup={{ lineNumbers: false, foldGutter: false, highlightActiveLine: false }}
249
- className="text-xs [&_.cm-editor]:!outline-none [&_.cm-scroller]:!overflow-auto" />
247
+ <Suspense fallback={<div className="p-2 text-xs text-text-secondary">Loading editor...</div>}>
248
+ <CodeMirror value={query} onChange={setQuery} extensions={[sql({ dialect: PostgreSQL })]}
249
+ basicSetup={{ lineNumbers: false, foldGutter: false, highlightActiveLine: false }}
250
+ className="text-xs [&_.cm-editor]:!outline-none [&_.cm-scroller]:!overflow-auto" />
251
+ </Suspense>
250
252
  </div>
251
253
  <button type="button" onClick={handleExecute} disabled={loading} title="Execute (Cmd+Enter)"
252
254
  className="shrink-0 m-1 p-1.5 rounded bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 transition-colors">
@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button";
7
7
  import { Input } from "@/components/ui/input";
8
8
  import { ScrollArea } from "@/components/ui/scroll-area";
9
9
  import { Separator } from "@/components/ui/separator";
10
+ import { useShallow } from "zustand/react/shallow";
10
11
  import { useSettingsStore, type Theme } from "@/stores/settings-store";
11
12
  import { cn } from "@/lib/utils";
12
13
  import { AISettingsSection } from "./ai-settings-section";
@@ -41,7 +42,7 @@ const CATEGORIES: { value: SettingsCategory; label: string; subtitle: string; ic
41
42
  ];
42
43
 
43
44
  export function SettingsTab() {
44
- const { theme, setTheme, deviceName, setDeviceName, version } = useSettingsStore();
45
+ const { theme, setTheme, deviceName, setDeviceName, version } = useSettingsStore(useShallow((s) => ({ theme: s.theme, setTheme: s.setTheme, deviceName: s.deviceName, setDeviceName: s.setDeviceName, version: s.version })));
45
46
  const { permission, isSubscribed, loading, error: pushError, subscribe, unsubscribe } = usePushNotification();
46
47
  const [activeCategory, setActiveCategory] = useState<SettingsCategory | null>(null);
47
48
  const [nameInput, setNameInput] = useState(deviceName ?? "");
@@ -1,15 +1,18 @@
1
1
  import { useState, useEffect, useRef, type ReactNode } from "react";
2
- import mermaid from "mermaid";
3
2
  import { useMdContext, FILE_EXT_RE, GLOB_CHARS_RE } from "./markdown-context";
4
3
  import { useTabStore } from "@/stores/tab-store";
5
4
 
6
5
  const MERMAID_KEYWORDS = /^(sequenceDiagram|flowchart|graph\s|classDiagram|stateDiagram|erDiagram|journey|gantt|pie|quadrantChart|requirementDiagram|gitGraph|mindmap|timeline|sankey|xychart|block-beta|packet-beta|architecture-beta|kanban)\b/;
7
6
 
8
- let mermaidInitialized = false;
9
- function ensureMermaidInit() {
10
- if (mermaidInitialized) return;
11
- mermaid.initialize({ startOnLoad: false, theme: "default", securityLevel: "loose", fontFamily: "ui-sans-serif, system-ui, sans-serif" });
12
- mermaidInitialized = true;
7
+ let mermaidPromise: Promise<typeof import("mermaid")["default"]> | null = null;
8
+ function getMermaid() {
9
+ if (!mermaidPromise) {
10
+ mermaidPromise = import("mermaid").then((mod) => {
11
+ mod.default.initialize({ startOnLoad: false, theme: "default", securityLevel: "loose", fontFamily: "ui-sans-serif, system-ui, sans-serif" });
12
+ return mod.default;
13
+ });
14
+ }
15
+ return mermaidPromise;
13
16
  }
14
17
 
15
18
  /** Extract plain text from a hast node tree */
@@ -89,9 +92,8 @@ function MermaidDiagram({ source }: { source: string }) {
89
92
  const [svg, setSvg] = useState<string | null>(null);
90
93
 
91
94
  useEffect(() => {
92
- ensureMermaidInit();
93
95
  const id = `mermaid-${Math.random().toString(36).slice(2, 9)}`;
94
- mermaid.render(id, source).then(({ svg }) => setSvg(svg)).catch(() => {});
96
+ getMermaid().then((m) => m.render(id, source)).then(({ svg }) => setSvg(svg)).catch(() => {});
95
97
  }, [source]);
96
98
 
97
99
  if (!svg) return <pre><code>{source}</code></pre>;
@@ -1,4 +1,4 @@
1
- import { useRef, useEffect, useState, useCallback } from "react";
1
+ import { useRef, useEffect, useState, useCallback, memo } from "react";
2
2
  import { useTerminal } from "@/hooks/use-terminal";
3
3
  import { cn } from "@/lib/utils";
4
4
  import { Copy, ClipboardPaste } from "lucide-react";
@@ -18,7 +18,7 @@ const MOBILE_KEYS = [
18
18
  { label: "\u2192", value: "\x1b[C" },
19
19
  ] as const;
20
20
 
21
- export function TerminalTab({ metadata }: TerminalTabProps) {
21
+ export const TerminalTab = memo(function TerminalTab({ metadata }: TerminalTabProps) {
22
22
  const sessionId = (metadata?.sessionId as string) ?? "new";
23
23
  const projectName = metadata?.projectName as string | undefined;
24
24
  const containerRef = useRef<HTMLDivElement>(null);
@@ -153,4 +153,4 @@ export function TerminalTab({ metadata }: TerminalTabProps) {
153
153
  )}
154
154
  </div>
155
155
  );
156
- }
156
+ });