@hienlh/ppm 0.9.93 → 0.9.95

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 (240) hide show
  1. package/.opencode/.env.example +98 -0
  2. package/.opencode/skills/ads-management/scripts/.env.example +13 -0
  3. package/.opencode/skills/ai-multimodal/.env.example +230 -0
  4. package/.opencode/skills/cip-design/.env.example +6 -0
  5. package/.opencode/skills/devops/.env.example +76 -0
  6. package/.opencode/skills/docs-seeker/.env.example +15 -0
  7. package/.opencode/skills/elevenlabs/.env.example +3 -0
  8. package/.opencode/skills/marketing-dashboard/.env.example +15 -0
  9. package/.opencode/skills/marketing-dashboard/app/.env.example +2 -0
  10. package/.opencode/skills/marketing-dashboard/server/.env.example +2 -0
  11. package/.opencode/skills/mcp-management/scripts/dist/analyze-tools.js +70 -0
  12. package/.opencode/skills/mcp-management/scripts/dist/cli.js +160 -0
  13. package/.opencode/skills/mcp-management/scripts/dist/mcp-client.js +183 -0
  14. package/.opencode/skills/payment-integration/scripts/.env.example +20 -0
  15. package/.opencode/skills/sequential-thinking/.env.example +8 -0
  16. package/CHANGELOG.md +15 -0
  17. package/bun.lock +6 -0
  18. package/dist/web/assets/ai-settings-section-LMO_cfIW.js +1 -0
  19. package/dist/web/assets/api-client-o_6TmLGC.js +1 -0
  20. package/dist/web/assets/api-settings-CoKe_BdR.js +1 -0
  21. package/dist/web/assets/architecture-PBZL5I3N-CUZIB1Vq.js +1 -0
  22. package/dist/web/assets/arrow-up-Dtrfv490.js +1 -0
  23. package/dist/web/assets/chat-tab-BRM81W0L.js +10 -0
  24. package/dist/web/assets/chevron-right-BzAdxJRG.js +1 -0
  25. package/dist/web/assets/code-CuravVys.js +1 -0
  26. package/dist/web/assets/code-editor-lxeFhLDX.js +8 -0
  27. package/dist/web/assets/columns-2-4fQcE4PF.js +1 -0
  28. package/dist/web/assets/conflict-editor-CO6NyEYJ.js +19 -0
  29. package/dist/web/assets/createLucideIcon-BjHrJDVb.js +1 -0
  30. package/dist/web/assets/{csv-preview-BZRICDP0.js → csv-preview-BizIVMyb.js} +2 -2
  31. package/dist/web/assets/database-D4DIhgi-.js +1 -0
  32. package/dist/web/assets/database-viewer-CU2X8VC-.js +2 -0
  33. package/dist/web/assets/diff-viewer-EybMrfw9.js +4 -0
  34. package/dist/web/assets/dist-C5IgeqrV.js +1 -0
  35. package/dist/web/assets/dist-im4ynINo.js +11 -0
  36. package/dist/web/assets/esm-K1XIK4vc.js +2 -0
  37. package/dist/web/assets/extension-store-3yZYn07W.js +1 -0
  38. package/dist/web/assets/extension-webview-DkacDy3f.js +3 -0
  39. package/dist/web/assets/gitGraph-HDMCJU4V-CtOMUphQ.js +1 -0
  40. package/dist/web/assets/index-BZ4G-2BK.css +2 -0
  41. package/dist/web/assets/index-D7PJ14mf.js +26 -0
  42. package/dist/web/assets/info-3K5VOQVL-BCrPCWGY.js +1 -0
  43. package/dist/web/assets/input-CHRMley8.js +1 -0
  44. package/dist/web/assets/keybindings-store-BAuymsWd.js +1 -0
  45. package/dist/web/assets/keybindings-store-BKyNIeFB.js +1 -0
  46. package/dist/web/assets/{lib-DSLzfeW0.js → lib-D_kRA9p6.js} +1 -1
  47. package/dist/web/assets/markdown-renderer-ocvtw_4F.js +3 -0
  48. package/dist/web/assets/packet-RMMSAZCW-D_OqB-zi.js +1 -0
  49. package/dist/web/assets/pie-UPGHQEXC-WUHpLNJz.js +1 -0
  50. package/dist/web/assets/plus-51UQ45rf.js +1 -0
  51. package/dist/web/assets/port-forwarding-tab-Chz3t_rM.js +1 -0
  52. package/dist/web/assets/postgres-viewer-cf8Xbssy.js +3 -0
  53. package/dist/web/assets/project-store-Ciq-cK1O.js +1 -0
  54. package/dist/web/assets/radar-KQ55EAFF-HQIIecVM.js +1 -0
  55. package/dist/web/assets/react-GqWghJ-L.js +1 -0
  56. package/dist/web/assets/refresh-cw-CSFrDtiu.js +1 -0
  57. package/dist/web/assets/scroll-area-DwWF9FpN.js +1 -0
  58. package/dist/web/assets/settings-store-B470PCWf.js +2 -0
  59. package/dist/web/assets/settings-tab-1jARRAlz.js +1 -0
  60. package/dist/web/assets/{sql-query-editor-DaePHpQI.js → sql-query-editor-DZ9xskL8.js} +1 -1
  61. package/dist/web/assets/sqlite-viewer-CveDk6KG.js +1 -0
  62. package/dist/web/assets/square-nsMa3iMk.js +1 -0
  63. package/dist/web/assets/tab-store-DZbiYk7y.js +1 -0
  64. package/dist/web/assets/table-Dq575bPF.js +1 -0
  65. package/dist/web/assets/terminal-tab-RvacNDWY.js +1 -0
  66. package/dist/web/assets/text-wrap-Cn6BNQfq.js +1 -0
  67. package/dist/web/assets/trash-2-CJYoLw7Q.js +1 -0
  68. package/dist/web/assets/treemap-KZPCXAKY-0wLgUUTz.js +1 -0
  69. package/dist/web/assets/{use-monaco-theme-CM4IMROI.js → use-monaco-theme-OY18iXNi.js} +1 -1
  70. package/dist/web/assets/vendor-markdown-0Mxgxy0L.js +295 -0
  71. package/dist/web/assets/vendor-mermaid-B2SLgECS.js +2657 -0
  72. package/dist/web/assets/vendor-ui-B-T_damt.js +45 -0
  73. package/dist/web/assets/vendor-xterm-ejLe7-tK.js +36 -0
  74. package/dist/web/assets/x-DlFGzN8d.js +1 -0
  75. package/dist/web/index.html +26 -21
  76. package/dist/web/sw.js +1 -1
  77. package/docs/code-standards.md +56 -4
  78. package/docs/journals/260415-frontend-memory-optimization.md +73 -0
  79. package/docs/project-changelog.md +11 -1
  80. package/docs/system-architecture.md +36 -0
  81. package/package.json +1 -1
  82. package/src/cli/commands/stop.ts +31 -0
  83. package/src/server/index.ts +68 -17
  84. package/src/services/autostart-generator.ts +1 -1
  85. package/src/services/autostart-register.ts +34 -29
  86. package/src/web/components/chat/message-list.tsx +59 -22
  87. package/src/web/components/chat/tool-cards.tsx +11 -4
  88. package/src/web/components/database/data-grid.tsx +2 -1
  89. package/src/web/components/editor/code-editor.tsx +14 -8
  90. package/src/web/components/editor/conflict-editor.tsx +2 -1
  91. package/src/web/components/editor/diff-viewer.tsx +2 -1
  92. package/src/web/components/editor/editor-breadcrumb.tsx +2 -1
  93. package/src/web/components/explorer/file-tree.tsx +6 -5
  94. package/src/web/components/explorer/search-panel.tsx +2 -1
  95. package/src/web/components/extensions/extension-webview.tsx +16 -30
  96. package/src/web/components/git/git-status-panel.tsx +2 -1
  97. package/src/web/components/layout/add-project-form.tsx +2 -1
  98. package/src/web/components/layout/mobile-drawer.tsx +2 -1
  99. package/src/web/components/layout/mobile-nav.tsx +2 -1
  100. package/src/web/components/layout/panel-layout.tsx +3 -3
  101. package/src/web/components/layout/project-bar.tsx +7 -6
  102. package/src/web/components/layout/project-bottom-sheet.tsx +2 -1
  103. package/src/web/components/layout/sidebar.tsx +5 -4
  104. package/src/web/components/layout/status-bar.tsx +5 -4
  105. package/src/web/components/layout/tab-bar.tsx +3 -3
  106. package/src/web/components/layout/tab-content.tsx +2 -1
  107. package/src/web/components/postgres/postgres-viewer.tsx +7 -5
  108. package/src/web/components/settings/settings-tab.tsx +2 -1
  109. package/src/web/components/shared/markdown-code-block.tsx +10 -8
  110. package/src/web/components/terminal/terminal-tab.tsx +3 -3
  111. package/src/web/hooks/use-chat.ts +4 -1
  112. package/vite.config.ts +17 -0
  113. package/dist/web/assets/_basePickBy-Bj0dI1ei.js +0 -1
  114. package/dist/web/assets/_baseUniq-CyzdZeQH.js +0 -1
  115. package/dist/web/assets/ai-settings-section-Bo9lCaTd.js +0 -1
  116. package/dist/web/assets/api-client-BvxmRZUi.js +0 -1
  117. package/dist/web/assets/api-settings-CUxg9RE5.js +0 -1
  118. package/dist/web/assets/arc-CxgHJ7Z4.js +0 -1
  119. package/dist/web/assets/architecture-PBZL5I3N-DDFO_NKq.js +0 -1
  120. package/dist/web/assets/architectureDiagram-2XIMDMQ5-D16OotsC.js +0 -36
  121. package/dist/web/assets/array-BFDiaBgf.js +0 -1
  122. package/dist/web/assets/arrow-up-I9-21gkR.js +0 -1
  123. package/dist/web/assets/blockDiagram-WCTKOSBZ-Ct57Wtfk.js +0 -132
  124. package/dist/web/assets/c4Diagram-IC4MRINW-BIymcNsg.js +0 -10
  125. package/dist/web/assets/channel-wumTB1if.js +0 -1
  126. package/dist/web/assets/chat-tab-CC721_mQ.js +0 -10
  127. package/dist/web/assets/chevron-right-DY_wImxB.js +0 -1
  128. package/dist/web/assets/chunk-4BX2VUAB-CENmY7Kw.js +0 -1
  129. package/dist/web/assets/chunk-55IACEB6-DhZGI1l3.js +0 -1
  130. package/dist/web/assets/chunk-7E7YKBS2-DZcnC7Ow.js +0 -1
  131. package/dist/web/assets/chunk-7R4GIKGN-y8bfHEy-.js +0 -80
  132. package/dist/web/assets/chunk-C72U2L5F-BHPkfQj2.js +0 -1
  133. package/dist/web/assets/chunk-EGIJ26TM-nant2LXl.js +0 -1
  134. package/dist/web/assets/chunk-FMBD7UC4-Bog4cpN-.js +0 -15
  135. package/dist/web/assets/chunk-GEFDOKGD-86LFbsAC.js +0 -2
  136. package/dist/web/assets/chunk-GLR3WWYH-Re-5eSlQ.js +0 -2
  137. package/dist/web/assets/chunk-HHEYEP7N-C45i5G_3.js +0 -1
  138. package/dist/web/assets/chunk-JSJVCQXG-23eG9mgt.js +0 -1
  139. package/dist/web/assets/chunk-KX2RTZJC-CHj8TnTB.js +0 -1
  140. package/dist/web/assets/chunk-KYZI473N-gqRLpJ4w.js +0 -53
  141. package/dist/web/assets/chunk-L3YUKLVL-DnSMmNFC.js +0 -1
  142. package/dist/web/assets/chunk-MX3YWQON-B6g1ZH9X.js +0 -1
  143. package/dist/web/assets/chunk-NQ4KR5QH-DX32345Y.js +0 -220
  144. package/dist/web/assets/chunk-O4XLMI2P-Vp_V4P-b.js +0 -7
  145. package/dist/web/assets/chunk-OZEHJAEY-lKq2SWjA.js +0 -1
  146. package/dist/web/assets/chunk-PQ6SQG4A-Bik13fTV.js +0 -1
  147. package/dist/web/assets/chunk-PU5JKC2W-DD95Rx35.js +0 -70
  148. package/dist/web/assets/chunk-QZHKN3VN-N3VXx1VH.js +0 -1
  149. package/dist/web/assets/chunk-R5LLSJPH-dRhXRnrb.js +0 -1
  150. package/dist/web/assets/chunk-WL4C6EOR-B1iIvLOG.js +0 -189
  151. package/dist/web/assets/chunk-XIRO2GV7-DZBoNl1_.js +0 -1
  152. package/dist/web/assets/chunk-XPW4576I-CgLyyW03.js +0 -32
  153. package/dist/web/assets/chunk-XZSTWKYB-DjV8xl5A.js +0 -94
  154. package/dist/web/assets/chunk-YBOYWFTD-D_ILLe6_.js +0 -1
  155. package/dist/web/assets/classDiagram-VBA2DB6C-mr-Cb1me.js +0 -1
  156. package/dist/web/assets/classDiagram-v2-RAHNMMFH-BKe8_uda.js +0 -1
  157. package/dist/web/assets/clone--z5KLAuR.js +0 -1
  158. package/dist/web/assets/code-editor-BZ0xwZ4Z.js +0 -8
  159. package/dist/web/assets/columns-2-IeETSfON.js +0 -1
  160. package/dist/web/assets/conflict-editor-Bwls2-yk.js +0 -19
  161. package/dist/web/assets/cose-bilkent-S5V4N54A-BGNPFv3x.js +0 -1
  162. package/dist/web/assets/cytoscape.esm-C8i2jUzT.js +0 -321
  163. package/dist/web/assets/dagre-CkhlMHnx.js +0 -1
  164. package/dist/web/assets/dagre-KLK3FWXG-Cnp996VG.js +0 -4
  165. package/dist/web/assets/database-CgTomMxt.js +0 -1
  166. package/dist/web/assets/database-viewer-DiXWqOJH.js +0 -2
  167. package/dist/web/assets/defaultLocale-ZeknFqNe.js +0 -1
  168. package/dist/web/assets/diagram-E7M64L7V-BZF0tSOr.js +0 -24
  169. package/dist/web/assets/diagram-IFDJBPK2-nUcO8sN8.js +0 -43
  170. package/dist/web/assets/diagram-P4PSJMXO-CW0eCkwC.js +0 -24
  171. package/dist/web/assets/diff-viewer-CC-RmeV5.js +0 -4
  172. package/dist/web/assets/dist-CM0oD8tQ.js +0 -1
  173. package/dist/web/assets/dist-DZmJeHOA.js +0 -1
  174. package/dist/web/assets/erDiagram-INFDFZHY-DSkriYZ9.js +0 -70
  175. package/dist/web/assets/extension-webview-C1d6fezE.js +0 -3
  176. package/dist/web/assets/flowDiagram-PKNHOUZH-CFYAfZBx.js +0 -162
  177. package/dist/web/assets/ganttDiagram-A5KZAMGK-KSn4XAU4.js +0 -292
  178. package/dist/web/assets/gitGraph-HDMCJU4V-OkvBPi6H.js +0 -1
  179. package/dist/web/assets/gitGraphDiagram-K3NZZRJ6-BMgjjVys.js +0 -65
  180. package/dist/web/assets/graphlib-BWe1iK_s.js +0 -1
  181. package/dist/web/assets/index-BcIyrJiY.js +0 -26
  182. package/dist/web/assets/index-Chf0otez.css +0 -2
  183. package/dist/web/assets/info-3K5VOQVL-BDU2_bYD.js +0 -1
  184. package/dist/web/assets/infoDiagram-LFFYTUFH-Diq4Cyc3.js +0 -2
  185. package/dist/web/assets/init-0VJVrkRJ.js +0 -1
  186. package/dist/web/assets/input-BHj0veau.js +0 -45
  187. package/dist/web/assets/isArrayLikeObject-ClzWCpcm.js +0 -1
  188. package/dist/web/assets/isEmpty-BfLnxq-B.js +0 -1
  189. package/dist/web/assets/ishikawaDiagram-PHBUUO56-CiVEvp8o.js +0 -70
  190. package/dist/web/assets/journeyDiagram-4ABVD52K-CG_v5Aho.js +0 -139
  191. package/dist/web/assets/jsx-runtime-BRW_vwa9.js +0 -1
  192. package/dist/web/assets/kanban-definition-K7BYSVSG-miB0-_Zq.js +0 -89
  193. package/dist/web/assets/keybindings-store-BIufrOzJ.js +0 -1
  194. package/dist/web/assets/line-CSuSrJ9J.js +0 -1
  195. package/dist/web/assets/linear-DFN_MPsw.js +0 -1
  196. package/dist/web/assets/markdown-renderer-C5UPA1-7.js +0 -306
  197. package/dist/web/assets/math-CRc16Nj6.js +0 -1
  198. package/dist/web/assets/mermaid-parser.core-CFdP1Z5_.js +0 -4
  199. package/dist/web/assets/mindmap-definition-YRQLILUH-pYPWwASE.js +0 -68
  200. package/dist/web/assets/ordinal-DpFn432U.js +0 -1
  201. package/dist/web/assets/packet-RMMSAZCW-BwpIpYB3.js +0 -1
  202. package/dist/web/assets/path-INs8XTPH.js +0 -1
  203. package/dist/web/assets/pie-UPGHQEXC-BPgAfmes.js +0 -1
  204. package/dist/web/assets/pieDiagram-SKSYHLDU-Dovdlvhu.js +0 -30
  205. package/dist/web/assets/plus-DQGIb4mQ.js +0 -1
  206. package/dist/web/assets/port-forwarding-tab-DmifthYH.js +0 -1
  207. package/dist/web/assets/postgres-viewer-Bo7jEQfQ.js +0 -13
  208. package/dist/web/assets/preload-helper-mr3rCizq.js +0 -1
  209. package/dist/web/assets/quadrantDiagram-337W2JSQ-TXe6cU_F.js +0 -7
  210. package/dist/web/assets/radar-KQ55EAFF-TqxBkWx-.js +0 -1
  211. package/dist/web/assets/react-0tkk-ztn.js +0 -1
  212. package/dist/web/assets/react-dom-Bpkvzu3U.js +0 -1
  213. package/dist/web/assets/react-nm2Ru1Pt.js +0 -1
  214. package/dist/web/assets/refresh-cw-Clk8fdUD.js +0 -1
  215. package/dist/web/assets/requirementDiagram-Z7DCOOCP-CuiiuGS9.js +0 -73
  216. package/dist/web/assets/rough.esm-eLccZ4OJ.js +0 -1
  217. package/dist/web/assets/sankeyDiagram-WA2Y5GQK-BbRmhv0t.js +0 -10
  218. package/dist/web/assets/scroll-area-BpXCNme3.js +0 -1
  219. package/dist/web/assets/sequenceDiagram-2WXFIKYE-B2D8IQDb.js +0 -145
  220. package/dist/web/assets/settings-tab-D9GicyA9.js +0 -1
  221. package/dist/web/assets/sqlite-viewer-pacZlViY.js +0 -1
  222. package/dist/web/assets/square-vBdqj0bF.js +0 -1
  223. package/dist/web/assets/src-CqyWLlNZ.js +0 -1
  224. package/dist/web/assets/stateDiagram-RAJIS63D-ylr4HxPu.js +0 -1
  225. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-D6zvxf3M.js +0 -1
  226. package/dist/web/assets/table-Bi27fEaN.js +0 -1
  227. package/dist/web/assets/terminal-tab-DpzE3yoD.js +0 -36
  228. package/dist/web/assets/text-wrap-D_OmSzhp.js +0 -1
  229. package/dist/web/assets/timeline-definition-YZTLITO2-pMv1grvM.js +0 -61
  230. package/dist/web/assets/trash-2-CNuB-htI.js +0 -1
  231. package/dist/web/assets/treemap-KZPCXAKY-Kck06FKU.js +0 -1
  232. package/dist/web/assets/vennDiagram-LZ73GAT5-C-rkIUbo.js +0 -34
  233. package/dist/web/assets/x-Dw3TjeY_.js +0 -1
  234. package/dist/web/assets/xychartDiagram-JWTSCODW-CtpjAakO.js +0 -7
  235. /package/dist/web/assets/{csv-parser-i7fjqP2H.js → csv-parser--2WJNgS7.js} +0 -0
  236. /package/dist/web/assets/{katex-DR0kdMDv.js → katex-CKoArbIw.js} +0 -0
  237. /package/dist/web/assets/{chunk-CFjPhJqf.js → rolldown-runtime-FhOqtrmT.js} +0 -0
  238. /package/dist/web/assets/{sql-completion-provider-B8uUWWej.js → sql-completion-provider-C3cq9j99.js} +0 -0
  239. /package/dist/web/assets/{utils-DX8jb5qv.js → utils-ChWX7pZv.js} +0 -0
  240. /package/dist/web/assets/{terminal-tab-BrP-ENHg.css → vendor-xterm-BrP-ENHg.css} +0 -0
@@ -60,34 +60,37 @@ function removeMetadata(): void {
60
60
 
61
61
  // ─── macOS ──────────────────────────────────────────────────────────────
62
62
 
63
- async function enableMacOS(config: AutoStartConfig): Promise<string> {
63
+ async function enableMacOS(config: AutoStartConfig, opts?: { skipStart?: boolean }): Promise<string> {
64
64
  const plistPath = getPlistPath();
65
65
  const plistDir = dirname(plistPath);
66
66
 
67
67
  if (!existsSync(plistDir)) mkdirSync(plistDir, { recursive: true });
68
68
  writeFileSync(plistPath, generatePlist(config));
69
69
 
70
- // Unload first if already loaded (ignore errors)
71
- Bun.spawnSync({
72
- cmd: ["launchctl", "bootout", `gui/${process.getuid!()}`, plistPath],
73
- stdout: "ignore", stderr: "ignore",
74
- });
75
-
76
- // Load the agent
77
- const result = Bun.spawnSync({
78
- cmd: ["launchctl", "bootstrap", `gui/${process.getuid!()}`, plistPath],
79
- stdout: "pipe", stderr: "pipe",
80
- });
70
+ // Skip loading if supervisor is already running from direct spawn
71
+ if (!opts?.skipStart) {
72
+ // Unload first if already loaded (ignore errors)
73
+ Bun.spawnSync({
74
+ cmd: ["launchctl", "bootout", `gui/${process.getuid!()}`, plistPath],
75
+ stdout: "ignore", stderr: "ignore",
76
+ });
81
77
 
82
- if (result.exitCode !== 0) {
83
- // Fallback to legacy syntax
84
- const legacy = Bun.spawnSync({
85
- cmd: ["launchctl", "load", plistPath],
78
+ // Load the agent
79
+ const result = Bun.spawnSync({
80
+ cmd: ["launchctl", "bootstrap", `gui/${process.getuid!()}`, plistPath],
86
81
  stdout: "pipe", stderr: "pipe",
87
82
  });
88
- if (legacy.exitCode !== 0) {
89
- const err = legacy.stderr.toString().trim();
90
- throw new Error(`launchctl load failed: ${err}`);
83
+
84
+ if (result.exitCode !== 0) {
85
+ // Fallback to legacy syntax
86
+ const legacy = Bun.spawnSync({
87
+ cmd: ["launchctl", "load", plistPath],
88
+ stdout: "pipe", stderr: "pipe",
89
+ });
90
+ if (legacy.exitCode !== 0) {
91
+ const err = legacy.stderr.toString().trim();
92
+ throw new Error(`launchctl load failed: ${err}`);
93
+ }
91
94
  }
92
95
  }
93
96
 
@@ -146,7 +149,7 @@ function statusMacOS(): AutoStartStatus {
146
149
 
147
150
  // ─── Linux ──────────────────────────────────────────────────────────────
148
151
 
149
- async function enableLinux(config: AutoStartConfig): Promise<string> {
152
+ async function enableLinux(config: AutoStartConfig, opts?: { skipStart?: boolean }): Promise<string> {
150
153
  const servicePath = getServicePath();
151
154
  const serviceDir = dirname(servicePath);
152
155
 
@@ -171,11 +174,13 @@ async function enableLinux(config: AutoStartConfig): Promise<string> {
171
174
  throw new Error(`systemctl enable failed: ${enable.stderr.toString().trim()}`);
172
175
  }
173
176
 
174
- // Start
175
- Bun.spawnSync({
176
- cmd: ["systemctl", "--user", "start", "ppm.service"],
177
- stdout: "ignore", stderr: "ignore",
178
- });
177
+ // Start (skip if supervisor is already running from direct spawn)
178
+ if (!opts?.skipStart) {
179
+ Bun.spawnSync({
180
+ cmd: ["systemctl", "--user", "start", "ppm.service"],
181
+ stdout: "ignore", stderr: "ignore",
182
+ });
183
+ }
179
184
 
180
185
  // Enable lingering so service runs at boot without login
181
186
  Bun.spawnSync({
@@ -312,11 +317,11 @@ function statusWindows(): AutoStartStatus {
312
317
 
313
318
  // ─── Public API ─────────────────────────────────────────────────────────
314
319
 
315
- /** Enable auto-start for the current platform */
316
- export async function enableAutoStart(config: AutoStartConfig): Promise<string> {
320
+ /** Enable auto-start for the current platform. skipStart=true registers without starting (when supervisor is already running). */
321
+ export async function enableAutoStart(config: AutoStartConfig, opts?: { skipStart?: boolean }): Promise<string> {
317
322
  const platform = process.platform;
318
- if (platform === "darwin") return enableMacOS(config);
319
- if (platform === "linux") return enableLinux(config);
323
+ if (platform === "darwin") return enableMacOS(config, opts);
324
+ if (platform === "linux") return enableLinux(config, opts);
320
325
  if (platform === "win32") return enableWindows(config);
321
326
  throw new Error(`Auto-start not supported on ${platform}`);
322
327
  }
@@ -1,10 +1,12 @@
1
- import { useEffect, useRef, useState, useMemo, useCallback } from "react";
1
+ import { useEffect, useRef, useState, useMemo, useCallback, memo, lazy, Suspense } from "react";
2
2
  import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
3
3
  import { getAuthToken } from "@/lib/api-client";
4
4
  import type { ChatMessage, ChatEvent } from "../../../types/chat";
5
5
  import type { SessionPhase } from "../../../types/api";
6
6
  import { ToolCard } from "./tool-cards";
7
- import { MarkdownRenderer } from "@/components/shared/markdown-renderer";
7
+ const MarkdownRenderer = lazy(() =>
8
+ import("@/components/shared/markdown-renderer").then((m) => ({ default: m.MarkdownRenderer }))
9
+ );
8
10
  import { cn, basename } from "@/lib/utils";
9
11
 
10
12
  import {
@@ -67,6 +69,34 @@ export function MessageList({
67
69
  }: MessageListProps) {
68
70
  // Scroll handled by StickToBottom wrapper — no manual scroll logic needed
69
71
 
72
+ const PAGE_SIZE = 50;
73
+ const [visibleCount, setVisibleCount] = useState(PAGE_SIZE);
74
+
75
+ // Reset visible count when conversation identity changes (not on every streaming tick)
76
+ const conversationId = messages[0]?.id;
77
+ useEffect(() => { setVisibleCount(PAGE_SIZE); }, [conversationId]);
78
+
79
+ const filtered = useMemo(() => messages.filter((msg) => {
80
+ const hasContent = msg.content && msg.content.trim().length > 0;
81
+ const hasEvents = msg.events && msg.events.length > 0;
82
+ // User bubbles only render text — hide SDK tool-result user messages
83
+ // that have no text content (their events are merged into assistant)
84
+ if (msg.role === "user") return hasContent;
85
+ return hasContent || hasEvents;
86
+ }), [messages]);
87
+
88
+ const displayed = useMemo(() => {
89
+ const start = Math.max(0, filtered.length - visibleCount);
90
+ return filtered.slice(start);
91
+ }, [filtered, visibleCount]);
92
+
93
+ const hasMore = visibleCount < filtered.length;
94
+
95
+ // Stable fork handler — avoids new closure per message (preserves MessageBubble memo)
96
+ const handleFork = useCallback((msgContent: string, msgId: string | undefined) => {
97
+ onFork?.(msgContent, msgId);
98
+ }, [onFork]);
99
+
70
100
  if (messagesLoading) {
71
101
  return (
72
102
  <div className="flex flex-col items-center justify-center h-full gap-3 text-text-secondary">
@@ -85,32 +115,30 @@ export function MessageList({
85
115
  );
86
116
  }
87
117
 
88
- const filtered = useMemo(() => messages.filter((msg) => {
89
- const hasContent = msg.content && msg.content.trim().length > 0;
90
- const hasEvents = msg.events && msg.events.length > 0;
91
- // User bubbles only render text — hide SDK tool-result user messages
92
- // that have no text content (their events are merged into assistant)
93
- if (msg.role === "user") return hasContent;
94
- return hasContent || hasEvents;
95
- }), [messages]);
96
-
97
118
  return (
98
119
  <div className="relative flex-1 overflow-hidden flex flex-col min-h-0">
99
120
  <StickToBottom className="flex-1 overflow-y-auto overflow-x-hidden" resize="smooth" initial="instant">
100
121
  <StickToBottom.Content className="p-4 space-y-4">
101
- {filtered.map((msg, idx) => (
122
+ {hasMore && (
123
+ <button onClick={() => setVisibleCount((c) => c + PAGE_SIZE)}
124
+ className="w-full py-2 text-xs text-text-secondary hover:text-text-primary bg-surface-elevated/50 hover:bg-surface-elevated rounded-md border border-border/50 transition-colors">
125
+ Load {Math.min(PAGE_SIZE, filtered.length - visibleCount)} more messages...
126
+ </button>
127
+ )}
128
+ {displayed.map((msg, idx) => {
129
+ const globalIdx = filtered.length - displayed.length + idx;
130
+ const prevMsg = globalIdx > 0 ? filtered[globalIdx - 1] : undefined;
131
+ return (
102
132
  <MessageBubble
103
133
  key={msg.id}
104
134
  message={msg}
105
135
  isStreaming={isStreaming && msg.id.startsWith("streaming-")}
106
136
  projectName={projectName}
107
- onFork={msg.role === "user" && onFork ? () => {
108
- // Pass the SDK UUID of the previous assistant message for fork (JSONL-level message ID)
109
- const prevMsg = idx > 0 ? filtered[idx - 1] : undefined;
110
- onFork(msg.content, prevMsg?.sdkUuid ?? prevMsg?.id);
111
- } : undefined}
137
+ onFork={msg.role === "user" && onFork ? handleFork : undefined}
138
+ prevMsgId={prevMsg?.sdkUuid ?? prevMsg?.id}
112
139
  />
113
- ))}
140
+ );
141
+ })}
114
142
 
115
143
  {pendingApproval && (
116
144
  pendingApproval.tool === "AskUserQuestion"
@@ -142,10 +170,15 @@ function ScrollToBottomButton() {
142
170
  );
143
171
  }
144
172
 
145
- function MessageBubble({ message, isStreaming, projectName, onFork }: { message: ChatMessage; isStreaming: boolean; projectName?: string; onFork?: () => void }) {
173
+ const MessageBubble = memo(function MessageBubble({ message, isStreaming, projectName, onFork, prevMsgId }: {
174
+ message: ChatMessage; isStreaming: boolean; projectName?: string;
175
+ onFork?: (content: string, messageId: string | undefined) => void;
176
+ prevMsgId?: string
177
+ }) {
146
178
  if (message.role === "user") {
179
+ const handleFork = onFork ? () => onFork(message.content, prevMsgId) : undefined;
147
180
  return (
148
- <UserBubble content={message.content} projectName={projectName} onFork={onFork} />
181
+ <UserBubble content={message.content} projectName={projectName} onFork={handleFork} />
149
182
  );
150
183
  }
151
184
 
@@ -175,7 +208,7 @@ function MessageBubble({ message, isStreaming, projectName, onFork }: { message:
175
208
  )}
176
209
  </div>
177
210
  );
178
- }
211
+ });
179
212
 
180
213
  /** Image extensions that can be previewed inline */
181
214
  const IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
@@ -843,7 +876,11 @@ function stripTeammateMessages(text: string): string {
843
876
  function MarkdownContent({ content, projectName, isStreaming }: { content: string; projectName?: string; isStreaming?: boolean }) {
844
877
  const cleaned = stripTeammateMessages(content);
845
878
  if (!cleaned) return null;
846
- return <MarkdownRenderer content={cleaned} projectName={projectName} codeActions isStreaming={isStreaming} />;
879
+ return (
880
+ <Suspense fallback={<div className="animate-pulse h-4 bg-muted rounded" />}>
881
+ <MarkdownRenderer content={cleaned} projectName={projectName} codeActions isStreaming={isStreaming} />
882
+ </Suspense>
883
+ );
847
884
  }
848
885
 
849
886
  /* ToolCard, ToolSummary, ToolDetails extracted to ./tool-cards.tsx */
@@ -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("");
@@ -54,47 +54,33 @@ export function ExtensionWebview({ metadata }: ExtensionWebviewProps) {
54
54
  const rawHtml = panel?.html ?? "";
55
55
  const html = injectVscodeApiShim(rawHtml);
56
56
 
57
- // On reload: resolve project path, then dispatch command with retry
58
- // Retry needed because WS connection may not be ready on first attempt
57
+ // On reload: resolve project path and dispatch command once
58
+ // No retry if it fails, user closes tab and reopens to retry
59
59
  useEffect(() => {
60
60
  if (panel || !viewType) return;
61
- // Mark project as "dispatched" so project-sync effect doesn't double-dispatch
62
61
  if (projectName) prevProjectRef.current = projectName;
63
62
  const command = viewType.includes(".") ? viewType : `${viewType}.view`;
64
63
  let cancelled = false;
65
- let resolvedArgs: unknown[] | null = null;
66
64
 
67
- async function resolveArgs(): Promise<unknown[]> {
68
- if (resolvedArgs) return resolvedArgs;
69
- if (!projectName) return [];
70
- try {
71
- const res = await fetch("/api/projects");
72
- const json = await res.json() as { ok: boolean; data?: { name: string; path: string }[] };
73
- const match = json.data?.find((p) => p.name === projectName);
74
- resolvedArgs = match ? [match.path] : [];
75
- } catch {
76
- resolvedArgs = [];
65
+ async function dispatch() {
66
+ let args: unknown[] = [];
67
+ if (projectName) {
68
+ try {
69
+ const res = await fetch("/api/projects");
70
+ const json = await res.json() as { ok: boolean; data?: { name: string; path: string }[] };
71
+ const match = json.data?.find((p) => p.name === projectName);
72
+ if (match) args = [match.path];
73
+ } catch {}
77
74
  }
78
- return resolvedArgs;
79
- }
80
-
81
- async function attempt() {
82
- const args = await resolveArgs();
83
75
  if (cancelled) return;
84
76
  window.dispatchEvent(new CustomEvent("ext:command:execute", {
85
77
  detail: { command, args },
86
78
  }));
87
79
  }
88
80
 
89
- // First attempt after short delay (let WS connect), then retry every 2s
90
- const initialTimer = setTimeout(() => {
91
- if (!cancelled) attempt();
92
- }, 500);
93
- const retryTimer = setInterval(() => {
94
- if (!cancelled) attempt();
95
- }, 2_000);
96
-
97
- return () => { cancelled = true; clearTimeout(initialTimer); clearInterval(retryTimer); };
81
+ // Short delay to let WS connect after page load
82
+ const timer = setTimeout(dispatch, 500);
83
+ return () => { cancelled = true; clearTimeout(timer); };
98
84
  }, [panel, viewType, projectName]);
99
85
 
100
86
  // When panel exists, ensure correct project is loaded.
@@ -168,10 +154,10 @@ export function ExtensionWebview({ metadata }: ExtensionWebviewProps) {
168
154
  };
169
155
  }, []);
170
156
 
171
- // Timeout: if panel doesn't appear within 10s, show error
157
+ // Timeout: if panel doesn't appear within 5s, show error
172
158
  useEffect(() => {
173
159
  if (panel) { setTimedOut(false); return; }
174
- const timer = setTimeout(() => setTimedOut(true), 10_000);
160
+ const timer = setTimeout(() => setTimedOut(true), 5_000);
175
161
  return () => clearTimeout(timer);
176
162
  }, [panel]);
177
163
 
@@ -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)}%`;