@hienlh/ppm 0.9.0-beta.9 → 0.9.2

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 (256) hide show
  1. package/.claude.bak/agent-memory/tester/MEMORY.md +3 -0
  2. package/.claude.bak/agent-memory/tester/project-ppm-test-conventions.md +32 -0
  3. package/CHANGELOG.md +240 -0
  4. package/bun.lock +17 -0
  5. package/dist/web/assets/{_basePickBy-3Xe18azI.js → _basePickBy-5PGDJbfF.js} +1 -1
  6. package/dist/web/assets/{_baseUniq-Yy35llnn.js → _baseUniq-BT4Ow4Kk.js} +1 -1
  7. package/dist/web/assets/api-settings-BUvk6Saw.js +1 -0
  8. package/dist/web/assets/{arc-B9n1Gvb5.js → arc-BAOivWpI.js} +1 -1
  9. package/dist/web/assets/architecture-PBZL5I3N-DEO2f3VD.js +1 -0
  10. package/dist/web/assets/{architectureDiagram-2XIMDMQ5-DqAZP_F6.js → architectureDiagram-2XIMDMQ5-Z-4eN4za.js} +1 -1
  11. package/dist/web/assets/arrow-up-BYhx9ckd.js +1 -0
  12. package/dist/web/assets/{blockDiagram-WCTKOSBZ-h3cDF2vI.js → blockDiagram-WCTKOSBZ-BCLqzhuZ.js} +1 -1
  13. package/dist/web/assets/browser-tab-CjUzlPYv.js +1 -0
  14. package/dist/web/assets/{c4Diagram-IC4MRINW--pF1r5lr.js → c4Diagram-IC4MRINW-0Vp0Jeas.js} +1 -1
  15. package/dist/web/assets/channel-By7bn0Yq.js +1 -0
  16. package/dist/web/assets/chat-tab-moB4W7-w.js +8 -0
  17. package/dist/web/assets/chevron-right-5HgK6l7K.js +1 -0
  18. package/dist/web/assets/{chunk-4BX2VUAB-C3aZvW7B.js → chunk-4BX2VUAB-D4tOov49.js} +1 -1
  19. package/dist/web/assets/{chunk-55IACEB6-D5cABeB9.js → chunk-55IACEB6-DJ6BynZ4.js} +1 -1
  20. package/dist/web/assets/{chunk-7E7YKBS2-CkFGv6Zs.js → chunk-7E7YKBS2-CiyUJxNI.js} +1 -1
  21. package/dist/web/assets/{chunk-7R4GIKGN-Dvbyu4Zw.js → chunk-7R4GIKGN-Dv-4cAYn.js} +2 -2
  22. package/dist/web/assets/{chunk-C72U2L5F-CtqKiH4q.js → chunk-C72U2L5F-D21mS_6G.js} +1 -1
  23. package/dist/web/assets/{chunk-EGIJ26TM-Cpr87sBR.js → chunk-EGIJ26TM-DzqmU2Z7.js} +1 -1
  24. package/dist/web/assets/{chunk-FMBD7UC4-D23YVTOU.js → chunk-FMBD7UC4-DXncblvW.js} +1 -1
  25. package/dist/web/assets/{chunk-GEFDOKGD-tDjHsAUs.js → chunk-GEFDOKGD-D-pKjlVd.js} +1 -1
  26. package/dist/web/assets/chunk-GLR3WWYH-DKikpoJM.js +2 -0
  27. package/dist/web/assets/chunk-HHEYEP7N-C7vxA5i9.js +1 -0
  28. package/dist/web/assets/{chunk-JSJVCQXG-BBmymCjA.js → chunk-JSJVCQXG-99JzIdPr.js} +1 -1
  29. package/dist/web/assets/{chunk-KX2RTZJC-DP36BDiU.js → chunk-KX2RTZJC-CRq1OBZv.js} +1 -1
  30. package/dist/web/assets/{chunk-KYZI473N-Djw13C-3.js → chunk-KYZI473N-Bb0MCaIO.js} +1 -1
  31. package/dist/web/assets/{chunk-L3YUKLVL-HG_eMj_C.js → chunk-L3YUKLVL-C7qGJrfV.js} +1 -1
  32. package/dist/web/assets/{chunk-MX3YWQON-C2UEioMs.js → chunk-MX3YWQON-BpS_PtKp.js} +1 -1
  33. package/dist/web/assets/{chunk-NQ4KR5QH-DXUTQ-BL.js → chunk-NQ4KR5QH-z_blpjxi.js} +1 -1
  34. package/dist/web/assets/{chunk-O4XLMI2P-BsUWb9d0.js → chunk-O4XLMI2P-nDhi_cVu.js} +1 -1
  35. package/dist/web/assets/{chunk-OZEHJAEY-rG0P22U9.js → chunk-OZEHJAEY-BXhYx3nO.js} +1 -1
  36. package/dist/web/assets/{chunk-PQ6SQG4A-DX0xW7kO.js → chunk-PQ6SQG4A-TF58UVMU.js} +1 -1
  37. package/dist/web/assets/{chunk-PU5JKC2W-C7Gry6md.js → chunk-PU5JKC2W-ek7k4QVB.js} +1 -1
  38. package/dist/web/assets/chunk-QZHKN3VN-CYaTbeZf.js +1 -0
  39. package/dist/web/assets/{chunk-R5LLSJPH-CMY0PkRK.js → chunk-R5LLSJPH-CFwSJijQ.js} +1 -1
  40. package/dist/web/assets/{chunk-WL4C6EOR-CXuQvlyu.js → chunk-WL4C6EOR-ByUrSRin.js} +1 -1
  41. package/dist/web/assets/{chunk-XIRO2GV7-DRJEb7Zb.js → chunk-XIRO2GV7-Djlmrely.js} +1 -1
  42. package/dist/web/assets/{chunk-XPW4576I-BPEX8KhL.js → chunk-XPW4576I-BPQQBakK.js} +1 -1
  43. package/dist/web/assets/{chunk-XZSTWKYB-Cb0iqycX.js → chunk-XZSTWKYB-DxAOx4hG.js} +1 -1
  44. package/dist/web/assets/{chunk-YBOYWFTD-av5aeHLq.js → chunk-YBOYWFTD-rQG3QH5s.js} +1 -1
  45. package/dist/web/assets/classDiagram-VBA2DB6C-BA8Nj-_C.js +1 -0
  46. package/dist/web/assets/classDiagram-v2-RAHNMMFH-DjYu-6mn.js +1 -0
  47. package/dist/web/assets/clone-LRxlvnMj.js +1 -0
  48. package/dist/web/assets/code-editor-aQQZUc2m.js +2 -0
  49. package/dist/web/assets/columns-2-cEVJHYd7.js +1 -0
  50. package/dist/web/assets/{cose-bilkent-S5V4N54A-qudEiMCT.js → cose-bilkent-S5V4N54A-B_AWZsOP.js} +1 -1
  51. package/dist/web/assets/createLucideIcon-PuMiQgHl.js +1 -0
  52. package/dist/web/assets/{csv-preview-DUbHtTAS.js → csv-preview-ncSOnJSC.js} +2 -2
  53. package/dist/web/assets/{dagre-BFcnKyBF.js → dagre-DHq9bhnd.js} +1 -1
  54. package/dist/web/assets/{dagre-KLK3FWXG-C3O-MTLf.js → dagre-KLK3FWXG-BdJr7Byp.js} +1 -1
  55. package/dist/web/assets/database-viewer-ChyP1N3c.js +1 -0
  56. package/dist/web/assets/{diagram-E7M64L7V-DxPjK7_c.js → diagram-E7M64L7V-_db4pBVA.js} +1 -1
  57. package/dist/web/assets/{diagram-IFDJBPK2-sqTog_XV.js → diagram-IFDJBPK2-xKoeuiJx.js} +1 -1
  58. package/dist/web/assets/{diagram-P4PSJMXO-hzmp0GHK.js → diagram-P4PSJMXO-C8tjJsev.js} +1 -1
  59. package/dist/web/assets/diff-viewer-ktwO5JbX.js +4 -0
  60. package/dist/web/assets/{dist-CALwEtco.js → dist-DIV6WgAG.js} +1 -1
  61. package/dist/web/assets/{dist-DGDPTxs1.js → dist-ovWkrgO-.js} +1 -1
  62. package/dist/web/assets/{erDiagram-INFDFZHY-DLeYhAAT.js → erDiagram-INFDFZHY-BSh2z9Df.js} +1 -1
  63. package/dist/web/assets/extension-webview-Bx1TlP6q.js +3 -0
  64. package/dist/web/assets/{flowDiagram-PKNHOUZH-CRxlE9Sr.js → flowDiagram-PKNHOUZH-oYaovqyp.js} +1 -1
  65. package/dist/web/assets/{ganttDiagram-A5KZAMGK-BdjmoMLS.js → ganttDiagram-A5KZAMGK-DmL26q2P.js} +1 -1
  66. package/dist/web/assets/git-graph-BIrGMX6e.js +1 -0
  67. package/dist/web/assets/gitGraph-HDMCJU4V-Bwna3and.js +1 -0
  68. package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-BeHSX7kk.js → gitGraphDiagram-K3NZZRJ6-CMoukSrY.js} +1 -1
  69. package/dist/web/assets/{graphlib-Duh_bWLa.js → graphlib-BcsNnGcW.js} +1 -1
  70. package/dist/web/assets/index-C6KLr58u.js +37 -0
  71. package/dist/web/assets/index-DpBKDbIW.css +2 -0
  72. package/dist/web/assets/info-3K5VOQVL-_vRxVNUm.js +1 -0
  73. package/dist/web/assets/infoDiagram-LFFYTUFH-DWwumDkq.js +2 -0
  74. package/dist/web/assets/{isEmpty-B9L-Ge-H.js → isEmpty-bnrF3Qbc.js} +1 -1
  75. package/dist/web/assets/{ishikawaDiagram-PHBUUO56-Cu0Rt1Ok.js → ishikawaDiagram-PHBUUO56-D05_LyL7.js} +1 -1
  76. package/dist/web/assets/{journeyDiagram-4ABVD52K-CgDI-UG4.js → journeyDiagram-4ABVD52K-B_L20qMe.js} +1 -1
  77. package/dist/web/assets/jsx-runtime-kMwlnEGE.js +1 -0
  78. package/dist/web/assets/{kanban-definition-K7BYSVSG-h4g10UHL.js → kanban-definition-K7BYSVSG-CZ535BbZ.js} +1 -1
  79. package/dist/web/assets/keybindings-store-D3Y5c5uS.js +1 -0
  80. package/dist/web/assets/{line-B75-Rx70.js → line-CVvo3dRu.js} +1 -1
  81. package/dist/web/assets/{linear-Bcjv9FQt.js → linear-DP4mkX3m.js} +1 -1
  82. package/dist/web/assets/{markdown-renderer-VIZB1GXE.js → markdown-renderer-A7J2gdKT.js} +5 -5
  83. package/dist/web/assets/{mermaid-parser.core-8u2leTXI.js → mermaid-parser.core-C7UwoIh6.js} +2 -2
  84. package/dist/web/assets/{mindmap-definition-YRQLILUH-BaOBwb-W.js → mindmap-definition-YRQLILUH-x0MTutJp.js} +1 -1
  85. package/dist/web/assets/{ordinal-LFEjVtwQ.js → ordinal-_K3x1fkz.js} +1 -1
  86. package/dist/web/assets/packet-RMMSAZCW-DY5PNnZU.js +1 -0
  87. package/dist/web/assets/pie-UPGHQEXC-BHncZutv.js +1 -0
  88. package/dist/web/assets/{pieDiagram-SKSYHLDU-At5Kz0KK.js → pieDiagram-SKSYHLDU-C1Gjrtzy.js} +1 -1
  89. package/dist/web/assets/postgres-viewer-C9-Acry_.js +1 -0
  90. package/dist/web/assets/{quadrantDiagram-337W2JSQ-CdjGIDfw.js → quadrantDiagram-337W2JSQ-C8bzJCjQ.js} +1 -1
  91. package/dist/web/assets/radar-KQ55EAFF-DH0AOkUy.js +1 -0
  92. package/dist/web/assets/{requirementDiagram-Z7DCOOCP-B9F_Cx_p.js → requirementDiagram-Z7DCOOCP-pQyah6WB.js} +1 -1
  93. package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-RolPi8bU.js → sankeyDiagram-WA2Y5GQK-T6RgG-N8.js} +1 -1
  94. package/dist/web/assets/{sequenceDiagram-2WXFIKYE-DM-tMAhx.js → sequenceDiagram-2WXFIKYE-BQDJ4CVs.js} +1 -1
  95. package/dist/web/assets/settings-tab-C17exmRv.js +1 -0
  96. package/dist/web/assets/sqlite-viewer-Dr5oWCWA.js +1 -0
  97. package/dist/web/assets/{stateDiagram-RAJIS63D-C4EMl6jf.js → stateDiagram-RAJIS63D-66vhiIuk.js} +1 -1
  98. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-BGVqj_g9.js +1 -0
  99. package/dist/web/assets/tab-store-BOgTrqRr.js +1 -0
  100. package/dist/web/assets/table-DFevCOMd.js +1 -0
  101. package/dist/web/assets/tag-CXMT0QB6.js +1 -0
  102. package/dist/web/assets/{terminal-tab-XhKfb4ei.js → terminal-tab-CpyKvyfC.js} +1 -1
  103. package/dist/web/assets/{timeline-definition-YZTLITO2-A4PN_Efm.js → timeline-definition-YZTLITO2-DwZqB3nn.js} +1 -1
  104. package/dist/web/assets/treemap-KZPCXAKY-B2Xkyv-K.js +1 -0
  105. package/dist/web/assets/{use-monaco-theme-0p0-84jJ.js → use-monaco-theme-BjPAik5w.js} +1 -1
  106. package/dist/web/assets/{vennDiagram-LZ73GAT5-ywK7LMaH.js → vennDiagram-LZ73GAT5-s9Z71fz-.js} +1 -1
  107. package/dist/web/assets/{xychartDiagram-JWTSCODW-DylHYNtJ.js → xychartDiagram-JWTSCODW-DRa_TH4B.js} +1 -1
  108. package/dist/web/index.html +10 -9
  109. package/dist/web/sw.js +1 -1
  110. package/docs/code-standards.md +128 -1
  111. package/docs/codebase-summary.md +79 -12
  112. package/docs/extension-development-guide.md +532 -0
  113. package/docs/project-changelog.md +51 -1
  114. package/docs/project-roadmap.md +9 -3
  115. package/docs/system-architecture.md +432 -3
  116. package/package.json +6 -3
  117. package/packages/ext-database/package.json +41 -0
  118. package/packages/ext-database/src/connection-tree.ts +142 -0
  119. package/packages/ext-database/src/extension.ts +346 -0
  120. package/packages/ext-database/src/query-panel.ts +120 -0
  121. package/packages/ext-database/src/table-viewer-panel.ts +410 -0
  122. package/packages/ext-database/tsconfig.json +8 -0
  123. package/packages/vscode-compat/package.json +16 -0
  124. package/packages/vscode-compat/src/commands.ts +39 -0
  125. package/packages/vscode-compat/src/context.ts +65 -0
  126. package/packages/vscode-compat/src/disposable.ts +21 -0
  127. package/packages/vscode-compat/src/env.ts +20 -0
  128. package/packages/vscode-compat/src/event-emitter.ts +28 -0
  129. package/packages/vscode-compat/src/index.ts +93 -0
  130. package/packages/vscode-compat/src/not-supported.ts +15 -0
  131. package/packages/vscode-compat/src/types.ts +167 -0
  132. package/packages/vscode-compat/src/uri.ts +65 -0
  133. package/packages/vscode-compat/src/window.ts +229 -0
  134. package/packages/vscode-compat/src/workspace.ts +76 -0
  135. package/packages/vscode-compat/tsconfig.json +10 -0
  136. package/src/cli/commands/autostart.ts +1 -1
  137. package/src/cli/commands/ext-cmd.ts +121 -0
  138. package/src/cli/commands/restart.ts +9 -1
  139. package/src/cli/commands/status.ts +19 -0
  140. package/src/index.ts +5 -3
  141. package/src/providers/claude-agent-sdk.ts +221 -17
  142. package/src/providers/cli-provider-base.ts +6 -0
  143. package/src/server/index.ts +55 -155
  144. package/src/server/routes/chat.ts +81 -11
  145. package/src/server/routes/extensions.ts +81 -0
  146. package/src/server/routes/project-scoped.ts +2 -0
  147. package/src/server/routes/settings.ts +27 -0
  148. package/src/server/routes/workspace.ts +35 -0
  149. package/src/server/ws/chat.ts +9 -3
  150. package/src/server/ws/extensions.ts +175 -0
  151. package/src/services/account-selector.service.ts +14 -5
  152. package/src/services/account.service.ts +7 -7
  153. package/src/services/claude-usage.service.ts +11 -11
  154. package/src/services/cloud-ws.service.ts +228 -0
  155. package/src/services/cloud.service.ts +1 -0
  156. package/src/services/contribution-registry.ts +110 -0
  157. package/src/services/db.service.ts +181 -4
  158. package/src/services/extension-host-worker.ts +160 -0
  159. package/src/services/extension-installer.ts +112 -0
  160. package/src/services/extension-manifest.ts +65 -0
  161. package/src/services/extension-rpc-handlers.ts +235 -0
  162. package/src/services/extension-rpc.ts +105 -0
  163. package/src/services/extension.service.ts +228 -0
  164. package/src/services/mcp-config.service.ts +15 -6
  165. package/src/services/supervisor.ts +271 -25
  166. package/src/types/api.ts +1 -0
  167. package/src/types/chat.ts +4 -0
  168. package/src/types/extension-messages.ts +64 -0
  169. package/src/types/extension.ts +131 -0
  170. package/src/web/app.tsx +69 -48
  171. package/src/web/components/chat/account-rotation-settings.tsx +163 -0
  172. package/src/web/components/chat/chat-history-bar.tsx +106 -10
  173. package/src/web/components/chat/chat-tab.tsx +15 -10
  174. package/src/web/components/chat/chat-welcome.tsx +148 -0
  175. package/src/web/components/chat/message-input.tsx +29 -29
  176. package/src/web/components/chat/message-list.tsx +19 -6
  177. package/src/web/components/chat/session-picker.tsx +80 -32
  178. package/src/web/components/chat/usage-badge.tsx +83 -10
  179. package/src/web/components/extensions/extension-inputbox.tsx +92 -0
  180. package/src/web/components/extensions/extension-quickpick.tsx +194 -0
  181. package/src/web/components/extensions/extension-tree-view.tsx +240 -0
  182. package/src/web/components/extensions/extension-webview.tsx +83 -0
  183. package/src/web/components/layout/command-palette.tsx +22 -2
  184. package/src/web/components/layout/editor-panel.tsx +163 -18
  185. package/src/web/components/layout/mobile-nav.tsx +2 -1
  186. package/src/web/components/layout/sidebar.tsx +21 -3
  187. package/src/web/components/layout/status-bar.tsx +64 -0
  188. package/src/web/components/layout/tab-bar.tsx +2 -0
  189. package/src/web/components/layout/tab-content.tsx +5 -0
  190. package/src/web/components/layout/upgrade-banner.tsx +15 -5
  191. package/src/web/components/settings/change-password-section.tsx +128 -0
  192. package/src/web/components/settings/extension-manager-section.tsx +214 -0
  193. package/src/web/components/settings/settings-tab.tsx +9 -2
  194. package/src/web/components/shared/connection-lost-overlay.tsx +89 -0
  195. package/src/web/hooks/use-chat.ts +28 -0
  196. package/src/web/hooks/use-extension-ws.ts +181 -0
  197. package/src/web/hooks/use-global-keybindings.ts +18 -2
  198. package/src/web/hooks/use-server-reload.ts +9 -0
  199. package/src/web/hooks/use-url-sync.ts +173 -21
  200. package/src/web/stores/connection-store.ts +39 -0
  201. package/src/web/stores/extension-store.ts +204 -0
  202. package/src/web/stores/panel-store.ts +63 -9
  203. package/src/web/stores/panel-utils.ts +145 -3
  204. package/src/web/stores/settings-store.ts +7 -2
  205. package/src/web/stores/tab-store.ts +2 -1
  206. package/tsconfig.json +3 -1
  207. package/dist/web/assets/api-settings-CEMxVMCV.js +0 -1
  208. package/dist/web/assets/architecture-PBZL5I3N-CFzkFKEL.js +0 -1
  209. package/dist/web/assets/arrow-up--LjUXLEt.js +0 -1
  210. package/dist/web/assets/browser-tab-D1Zua62g.js +0 -1
  211. package/dist/web/assets/channel-C2fMafck.js +0 -1
  212. package/dist/web/assets/chat-tab-BnD27Vp9.js +0 -7
  213. package/dist/web/assets/chevron-right-CHnjJt4E.js +0 -1
  214. package/dist/web/assets/chunk-GLR3WWYH-DBdWQ3zy.js +0 -2
  215. package/dist/web/assets/chunk-HHEYEP7N-BBw_z0fW.js +0 -1
  216. package/dist/web/assets/chunk-QZHKN3VN-DFKFM_C1.js +0 -1
  217. package/dist/web/assets/classDiagram-VBA2DB6C-Dp4Kk3Yb.js +0 -1
  218. package/dist/web/assets/classDiagram-v2-RAHNMMFH-D8IvcV_B.js +0 -1
  219. package/dist/web/assets/clone-B2hUek6n.js +0 -1
  220. package/dist/web/assets/code-editor-DGRg8stf.js +0 -2
  221. package/dist/web/assets/columns-2-DbesTfa7.js +0 -1
  222. package/dist/web/assets/database-viewer-DxCXZQcE.js +0 -1
  223. package/dist/web/assets/diff-viewer-C1sDJG35.js +0 -4
  224. package/dist/web/assets/git-graph-BDn-EiGE.js +0 -1
  225. package/dist/web/assets/gitGraph-HDMCJU4V-CNlas3Rz.js +0 -1
  226. package/dist/web/assets/index-Bun94AK3.js +0 -37
  227. package/dist/web/assets/index-Db8uky1a.css +0 -2
  228. package/dist/web/assets/info-3K5VOQVL-BDzTLc11.js +0 -1
  229. package/dist/web/assets/infoDiagram-LFFYTUFH-ZZmpgc6t.js +0 -2
  230. package/dist/web/assets/jsx-runtime-BRW_vwa9.js +0 -1
  231. package/dist/web/assets/keybindings-store-COmK4Dte.js +0 -1
  232. package/dist/web/assets/packet-RMMSAZCW-IVa5F-go.js +0 -1
  233. package/dist/web/assets/pie-UPGHQEXC-CvXHKAzp.js +0 -1
  234. package/dist/web/assets/postgres-viewer-CvQZ8gkh.js +0 -1
  235. package/dist/web/assets/radar-KQ55EAFF-Z-Tr5wtS.js +0 -1
  236. package/dist/web/assets/settings-tab-RCnvZ29H.js +0 -1
  237. package/dist/web/assets/sqlite-viewer-CEEm2W4C.js +0 -1
  238. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-B-UjZch3.js +0 -1
  239. package/dist/web/assets/tab-store-Bjh6bXFP.js +0 -1
  240. package/dist/web/assets/table-CQVQM2SB.js +0 -1
  241. package/dist/web/assets/tag-Q2dZiSPX.js +0 -1
  242. package/dist/web/assets/treemap-KZPCXAKY-C9TYRE0k.js +0 -1
  243. /package/dist/web/assets/{api-client-BKIT_Qeg.js → api-client-BfBM3I7n.js} +0 -0
  244. /package/dist/web/assets/{array-DqLCdDFv.js → array-B9UHiPd-.js} +0 -0
  245. /package/dist/web/assets/{cytoscape.esm-CWPXKqbJ.js → cytoscape.esm-BW-DbntU.js} +0 -0
  246. /package/dist/web/assets/{defaultLocale-CrJzLgRD.js → defaultLocale-5eAKkKJC.js} +0 -0
  247. /package/dist/web/assets/{dist-Cep75xXf.js → dist-CSJdAyA9.js} +0 -0
  248. /package/dist/web/assets/{init-C0r9Gk5G.js → init-DlZdxViB.js} +0 -0
  249. /package/dist/web/assets/{isArrayLikeObject-CGBoxvCD.js → isArrayLikeObject-B_v2FtYn.js} +0 -0
  250. /package/dist/web/assets/{katex-DzXRfQ_m.js → katex-Bqvo_ZG0.js} +0 -0
  251. /package/dist/web/assets/{lib-BeaDXEkP.js → lib-BQ34Db2e.js} +0 -0
  252. /package/dist/web/assets/{math-y9zN1W-N.js → math-069Z4SuC.js} +0 -0
  253. /package/dist/web/assets/{path-DIKpVbHL.js → path-6uRLdFF7.js} +0 -0
  254. /package/dist/web/assets/{rough.esm-nHaDi0Kw.js → rough.esm-JX0wREDd.js} +0 -0
  255. /package/dist/web/assets/{src-Dw4QhedI.js → src-BqX54PbV.js} +0 -0
  256. /package/dist/web/assets/{utils-DMiycH3O.js → utils-BNytJOb1.js} +0 -0
@@ -0,0 +1,214 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+ import { Plus, Trash2, Power, PowerOff, Puzzle, FolderSymlink, Loader2 } from "lucide-react";
3
+ import { Button } from "@/components/ui/button";
4
+ import { Input } from "@/components/ui/input";
5
+ import { api } from "@/lib/api-client";
6
+ import { toast } from "sonner";
7
+
8
+ interface ExtensionInfo {
9
+ id: string;
10
+ version: string;
11
+ displayName: string;
12
+ description: string;
13
+ icon: string;
14
+ enabled: boolean;
15
+ activated: boolean;
16
+ }
17
+
18
+ export function ExtensionManagerSection() {
19
+ const [extensions, setExtensions] = useState<ExtensionInfo[]>([]);
20
+ const [loading, setLoading] = useState(true);
21
+ const [installName, setInstallName] = useState("");
22
+ const [installing, setInstalling] = useState(false);
23
+ const [devPath, setDevPath] = useState("");
24
+ const [showDevLink, setShowDevLink] = useState(false);
25
+ const [togglingId, setTogglingId] = useState<string | null>(null);
26
+ const [deletingId, setDeletingId] = useState<string | null>(null);
27
+
28
+ const fetchExtensions = useCallback(async () => {
29
+ try {
30
+ const data = await api.get<ExtensionInfo[]>("/api/extensions");
31
+ setExtensions(data);
32
+ } catch (e) {
33
+ console.error("Failed to load extensions:", e);
34
+ } finally {
35
+ setLoading(false);
36
+ }
37
+ }, []);
38
+
39
+ useEffect(() => { fetchExtensions(); }, [fetchExtensions]);
40
+
41
+ const handleInstall = async () => {
42
+ const name = installName.trim();
43
+ if (!name) return;
44
+ setInstalling(true);
45
+ try {
46
+ await api.post("/api/extensions/install", { name });
47
+ toast.success(`Installed ${name}`);
48
+ setInstallName("");
49
+ fetchExtensions();
50
+ } catch (e: any) {
51
+ toast.error(e.message || "Install failed");
52
+ } finally {
53
+ setInstalling(false);
54
+ }
55
+ };
56
+
57
+ const handleToggle = async (ext: ExtensionInfo) => {
58
+ setTogglingId(ext.id);
59
+ try {
60
+ await api.patch(`/api/extensions/${ext.id}`, { enabled: !ext.enabled });
61
+ fetchExtensions();
62
+ } catch (e: any) {
63
+ toast.error(e.message || "Toggle failed");
64
+ } finally {
65
+ setTogglingId(null);
66
+ }
67
+ };
68
+
69
+ const handleRemove = async (id: string) => {
70
+ setDeletingId(id);
71
+ try {
72
+ await api.del(`/api/extensions/${id}`);
73
+ toast.success(`Removed ${id}`);
74
+ fetchExtensions();
75
+ } catch (e: any) {
76
+ toast.error(e.message || "Remove failed");
77
+ } finally {
78
+ setDeletingId(null);
79
+ }
80
+ };
81
+
82
+ if (loading) {
83
+ return (
84
+ <div className="flex items-center justify-center py-8">
85
+ <Loader2 className="size-4 animate-spin text-muted-foreground" />
86
+ </div>
87
+ );
88
+ }
89
+
90
+ return (
91
+ <div className="space-y-4">
92
+ {/* Install */}
93
+ <section className="space-y-2">
94
+ <h3 className="text-xs font-medium text-muted-foreground">Install Extension</h3>
95
+ <div className="flex gap-1.5">
96
+ <Input
97
+ value={installName}
98
+ onChange={(e) => setInstallName(e.target.value)}
99
+ onKeyDown={(e) => { if (e.key === "Enter") handleInstall(); }}
100
+ placeholder="npm package name (e.g. @ppm/ext-database)"
101
+ className="h-8 text-xs flex-1"
102
+ />
103
+ <Button
104
+ variant="outline"
105
+ size="sm"
106
+ className="h-8 text-xs px-3 gap-1 cursor-pointer"
107
+ disabled={!installName.trim() || installing}
108
+ onClick={handleInstall}
109
+ >
110
+ {installing ? <Loader2 className="size-3 animate-spin" /> : <Plus className="size-3" />}
111
+ Install
112
+ </Button>
113
+ </div>
114
+ <button
115
+ onClick={() => setShowDevLink(!showDevLink)}
116
+ className="text-[11px] text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
117
+ >
118
+ {showDevLink ? "Hide" : "Dev link local extension..."}
119
+ </button>
120
+ {showDevLink && (
121
+ <div className="flex gap-1.5">
122
+ <Input
123
+ value={devPath}
124
+ onChange={(e) => setDevPath(e.target.value)}
125
+ placeholder="Local path (e.g. ./packages/ext-database)"
126
+ className="h-8 text-xs flex-1"
127
+ />
128
+ <Button
129
+ variant="outline"
130
+ size="sm"
131
+ className="h-8 text-xs px-3 gap-1 cursor-pointer"
132
+ disabled={!devPath.trim()}
133
+ onClick={async () => {
134
+ try {
135
+ await api.post("/api/extensions/dev-link", { path: devPath.trim() });
136
+ toast.success("Dev-linked successfully");
137
+ setDevPath("");
138
+ fetchExtensions();
139
+ } catch (e: any) {
140
+ toast.error(e.message || "Dev link failed");
141
+ }
142
+ }}
143
+ >
144
+ <FolderSymlink className="size-3" />
145
+ Link
146
+ </Button>
147
+ </div>
148
+ )}
149
+ </section>
150
+
151
+ {/* Extension list */}
152
+ <section className="space-y-2">
153
+ <h3 className="text-xs font-medium text-muted-foreground">
154
+ Installed ({extensions.length})
155
+ </h3>
156
+ {extensions.length === 0 ? (
157
+ <p className="text-[11px] text-muted-foreground py-4 text-center">
158
+ No extensions installed
159
+ </p>
160
+ ) : (
161
+ <div className="space-y-1">
162
+ {extensions.map((ext) => (
163
+ <div
164
+ key={ext.id}
165
+ className="flex items-center gap-2 px-2.5 py-2 rounded-lg bg-muted/50 hover:bg-muted transition-colors"
166
+ >
167
+ <div className="size-8 rounded-md bg-background flex items-center justify-center shrink-0">
168
+ <Puzzle className="size-4 text-muted-foreground" />
169
+ </div>
170
+ <div className="flex-1 min-w-0">
171
+ <p className="text-xs font-medium truncate">{ext.displayName || ext.id}</p>
172
+ <p className="text-[11px] text-muted-foreground truncate">
173
+ {ext.id} v{ext.version}
174
+ {ext.activated && <span className="ml-1 text-green-500">active</span>}
175
+ </p>
176
+ </div>
177
+ <Button
178
+ variant="ghost"
179
+ size="icon"
180
+ className="size-7 shrink-0 cursor-pointer"
181
+ title={ext.enabled ? "Disable" : "Enable"}
182
+ disabled={togglingId === ext.id}
183
+ onClick={() => handleToggle(ext)}
184
+ >
185
+ {togglingId === ext.id ? (
186
+ <Loader2 className="size-3.5 animate-spin" />
187
+ ) : ext.enabled ? (
188
+ <Power className="size-3.5 text-green-500" />
189
+ ) : (
190
+ <PowerOff className="size-3.5 text-muted-foreground" />
191
+ )}
192
+ </Button>
193
+ <Button
194
+ variant="ghost"
195
+ size="icon"
196
+ className="size-7 shrink-0 cursor-pointer text-destructive hover:text-destructive"
197
+ title="Remove"
198
+ disabled={deletingId === ext.id}
199
+ onClick={() => handleRemove(ext.id)}
200
+ >
201
+ {deletingId === ext.id ? (
202
+ <Loader2 className="size-3.5 animate-spin" />
203
+ ) : (
204
+ <Trash2 className="size-3.5" />
205
+ )}
206
+ </Button>
207
+ </div>
208
+ ))}
209
+ </div>
210
+ )}
211
+ </section>
212
+ </div>
213
+ );
214
+ }
@@ -1,7 +1,7 @@
1
1
  import { useState, useCallback, useRef } from "react";
2
2
  import {
3
3
  Moon, Sun, Monitor, Bell, BellOff, Check, ChevronRight, ArrowLeft,
4
- Bot, BellRing, Keyboard, Globe, Plug,
4
+ Bot, BellRing, Keyboard, Globe, Plug, Puzzle,
5
5
  } from "lucide-react";
6
6
  import { Button } from "@/components/ui/button";
7
7
  import { Input } from "@/components/ui/input";
@@ -14,6 +14,8 @@ import { KeyboardShortcutsSection } from "./keyboard-shortcuts-section";
14
14
  import { TelegramSettingsSection } from "./telegram-settings-section";
15
15
  import { ProxySettingsSection } from "./proxy-settings-section";
16
16
  import { McpSettingsSection } from "./mcp-settings-section";
17
+ import { ExtensionManagerSection } from "./extension-manager-section";
18
+ import { ChangePasswordSection } from "./change-password-section";
17
19
  import { usePushNotification } from "@/hooks/use-push-notification";
18
20
 
19
21
  const THEME_OPTIONS: { value: Theme; label: string; icon: React.ElementType }[] = [
@@ -26,7 +28,7 @@ const pushSupported = "PushManager" in window && "serviceWorker" in navigator;
26
28
  const isIosNonPwa = /iPhone|iPad/.test(navigator.userAgent) &&
27
29
  !window.matchMedia("(display-mode: standalone)").matches;
28
30
 
29
- type SettingsCategory = "ai" | "notifications" | "proxy" | "shortcuts" | "mcp";
31
+ type SettingsCategory = "ai" | "notifications" | "proxy" | "shortcuts" | "mcp" | "extensions";
30
32
 
31
33
  const CATEGORIES: { value: SettingsCategory; label: string; subtitle: string; icon: React.ElementType }[] = [
32
34
  { value: "ai", label: "AI Provider", subtitle: "Model, execution mode, limits", icon: Bot },
@@ -34,6 +36,7 @@ const CATEGORIES: { value: SettingsCategory; label: string; subtitle: string; ic
34
36
  { value: "proxy", label: "API Proxy", subtitle: "Expose accounts as Anthropic API", icon: Globe },
35
37
  { value: "shortcuts", label: "Keyboard Shortcuts", subtitle: "Customize key bindings", icon: Keyboard },
36
38
  { value: "mcp", label: "MCP Servers", subtitle: "Model Context Protocol tools", icon: Plug },
39
+ { value: "extensions", label: "Extensions", subtitle: "Install and manage extensions", icon: Puzzle },
37
40
  ];
38
41
 
39
42
  export function SettingsTab() {
@@ -87,6 +90,7 @@ export function SettingsTab() {
87
90
  {activeCategory === "proxy" && <ProxySettingsSection />}
88
91
  {activeCategory === "shortcuts" && <KeyboardShortcutsSection />}
89
92
  {activeCategory === "mcp" && <McpSettingsSection />}
93
+ {activeCategory === "extensions" && <ExtensionManagerSection />}
90
94
  </div>
91
95
  </ScrollArea>
92
96
  </div>
@@ -128,6 +132,9 @@ export function SettingsTab() {
128
132
  </p>
129
133
  </section>
130
134
 
135
+ {/* Security: Change Password */}
136
+ <ChangePasswordSection />
137
+
131
138
  {/* Quick: Theme */}
132
139
  <section className="space-y-2">
133
140
  <h3 className="text-xs font-medium text-muted-foreground">Theme</h3>
@@ -0,0 +1,89 @@
1
+ import { WifiOff, ServerOff, RefreshCw } from "lucide-react";
2
+ import { useState } from "react";
3
+ import { useConnectionStore } from "@/stores/connection-store";
4
+
5
+ const CLOUD_URL = "https://ppm.hienle.tech";
6
+
7
+ function isTunnelDomain(): boolean {
8
+ return window.location.hostname.endsWith(".trycloudflare.com");
9
+ }
10
+
11
+ export function ConnectionLostOverlay() {
12
+ const showOverlay = useConnectionStore((s) => s.showOverlay);
13
+ const [retrying, setRetrying] = useState(false);
14
+
15
+ if (!showOverlay) return null;
16
+
17
+ const isTunnel = isTunnelDomain();
18
+
19
+ async function handleRetry() {
20
+ setRetrying(true);
21
+ try {
22
+ const res = await fetch("/api/health", { cache: "no-store" });
23
+ if (res.ok) {
24
+ useConnectionStore.getState().markUp();
25
+ if ("caches" in window) {
26
+ const keys = await caches.keys();
27
+ await Promise.all(keys.map((k) => caches.delete(k)));
28
+ }
29
+ window.location.reload();
30
+ return;
31
+ }
32
+ } catch {
33
+ // still down
34
+ }
35
+ setRetrying(false);
36
+ }
37
+
38
+ const Icon = isTunnel ? WifiOff : ServerOff;
39
+
40
+ return (
41
+ <div className="fixed inset-0 z-[200] bg-background/95 backdrop-blur-sm flex items-center justify-center p-4">
42
+ <div className="max-w-sm w-full text-center space-y-6">
43
+ <div className="flex justify-center">
44
+ <div className="rounded-full bg-destructive/10 p-4">
45
+ <Icon className="h-10 w-10 text-destructive" />
46
+ </div>
47
+ </div>
48
+
49
+ <div className="space-y-2">
50
+ <h2 className="text-xl font-semibold text-foreground">
51
+ {isTunnel ? "Connection Lost" : "Server Unreachable"}
52
+ </h2>
53
+ <p className="text-sm text-muted-foreground leading-relaxed">
54
+ {isTunnel
55
+ ? "The tunnel appears to have closed. The server may have restarted with a new URL."
56
+ : "Cannot connect to the PPM server. It may have stopped or is restarting."}
57
+ </p>
58
+ </div>
59
+
60
+ <div className="flex flex-col gap-3">
61
+ {isTunnel && (
62
+ <a
63
+ href={CLOUD_URL}
64
+ target="_blank"
65
+ rel="noopener noreferrer"
66
+ className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
67
+ >
68
+ Open PPM Cloud
69
+ </a>
70
+ )}
71
+ <button
72
+ onClick={handleRetry}
73
+ disabled={retrying}
74
+ className="inline-flex items-center justify-center gap-2 rounded-md border border-border px-4 py-2.5 text-sm font-medium text-foreground hover:bg-accent transition-colors disabled:opacity-50"
75
+ >
76
+ <RefreshCw className={`h-4 w-4 ${retrying ? "animate-spin" : ""}`} />
77
+ {retrying ? "Retrying…" : "Retry Connection"}
78
+ </button>
79
+ </div>
80
+
81
+ {!isTunnel && (
82
+ <p className="text-xs text-muted-foreground">
83
+ If the server was stopped, run <code className="bg-muted px-1 py-0.5 rounded text-[11px]">ppm start</code> to restart it.
84
+ </p>
85
+ )}
86
+ </div>
87
+ </div>
88
+ );
89
+ }
@@ -22,6 +22,7 @@ interface UseChatReturn {
22
22
  connectingElapsed: number;
23
23
  pendingApproval: ApprovalRequest | null;
24
24
  contextWindowPct: number | null;
25
+ compactStatus: "compacting" | null;
25
26
  sessionTitle: string | null;
26
27
  /** When CLI provider assigns a different session ID, this holds the new ID */
27
28
  migratedSessionId: string | null;
@@ -51,6 +52,7 @@ export function useChat(sessionId: string | null, providerId = "claude", project
51
52
  const [connectingElapsed, setConnectingElapsed] = useState(0);
52
53
  const [pendingApproval, setPendingApproval] = useState<ApprovalRequest | null>(null);
53
54
  const [contextWindowPct, setContextWindowPct] = useState<number | null>(null);
55
+ const [compactStatus, setCompactStatus] = useState<"compacting" | null>(null);
54
56
  const [sessionTitle, setSessionTitle] = useState<string | null>(null);
55
57
  const [isConnected, setIsConnected] = useState(false);
56
58
  const [migratedSessionId, setMigratedSessionId] = useState<string | null>(null);
@@ -121,6 +123,17 @@ export function useChat(sessionId: string | null, providerId = "claude", project
121
123
  break;
122
124
  }
123
125
 
126
+ case "account_retry": {
127
+ // Update streaming account to the new one being tried
128
+ if (ev.accountId && ev.accountLabel) {
129
+ streamingAccountRef.current = { accountId: ev.accountId, accountLabel: ev.accountLabel };
130
+ }
131
+ // Surface retry as a system-level event in the stream
132
+ streamingEventsRef.current.push(ev as ChatEvent);
133
+ syncMessages();
134
+ break;
135
+ }
136
+
124
137
  case "text": {
125
138
  const pid = ev.parentToolUseId as string | undefined;
126
139
  if (pid && routeToParent(ev as ChatEvent, pid)) {
@@ -259,6 +272,19 @@ export function useChat(sessionId: string | null, providerId = "claude", project
259
272
  return;
260
273
  }
261
274
 
275
+ // Handle compact status events
276
+ if ((data as any).type === "compact_status") {
277
+ const status = (data as any).status;
278
+ if (status === "compacting") {
279
+ setCompactStatus("compacting");
280
+ } else if (status === "done") {
281
+ setCompactStatus(null);
282
+ // Refresh messages to show compacted history
283
+ refetchRef.current?.();
284
+ }
285
+ return;
286
+ }
287
+
262
288
  // Handle phase transitions from BE
263
289
  if ((data as any).type === "phase_changed") {
264
290
  const p = (data as any).phase as SessionPhase;
@@ -351,6 +377,7 @@ export function useChat(sessionId: string | null, providerId = "claude", project
351
377
  setPhase("idle");
352
378
  phaseRef.current = "idle";
353
379
  setPendingApproval(null);
380
+ setCompactStatus(null);
354
381
  streamingContentRef.current = "";
355
382
  streamingEventsRef.current = [];
356
383
  setIsConnected(false);
@@ -539,6 +566,7 @@ export function useChat(sessionId: string | null, providerId = "claude", project
539
566
  connectingElapsed,
540
567
  pendingApproval,
541
568
  contextWindowPct,
569
+ compactStatus,
542
570
  sessionTitle,
543
571
  migratedSessionId,
544
572
  sendMessage,
@@ -0,0 +1,181 @@
1
+ import { useEffect, useCallback, useRef } from "react";
2
+ import { WsClient } from "@/lib/ws-client";
3
+ import { useExtensionStore } from "@/stores/extension-store";
4
+ import { useTabStore } from "@/stores/tab-store";
5
+ import { getAuthToken } from "@/lib/api-client";
6
+ import type { ExtServerMsg, ExtClientMsg } from "../../types/extension-messages.ts";
7
+ import { toast } from "sonner";
8
+
9
+ /**
10
+ * Hook that manages the WebSocket connection for extension UI bridge.
11
+ * Dispatches server messages into the extension Zustand store.
12
+ * Only connects when `enabled` is true (after auth).
13
+ */
14
+ export function useExtensionWs(enabled = true) {
15
+ const clientRef = useRef<WsClient | null>(null);
16
+
17
+ const send = useCallback((msg: ExtClientMsg) => {
18
+ clientRef.current?.send(JSON.stringify(msg));
19
+ }, []);
20
+
21
+ useEffect(() => {
22
+ if (!enabled) return;
23
+
24
+ // Pass auth token as query param for WS auth
25
+ const token = getAuthToken();
26
+ const wsUrl = token ? `/ws/extensions?token=${encodeURIComponent(token)}` : "/ws/extensions";
27
+ const client = new WsClient(wsUrl);
28
+ clientRef.current = client;
29
+
30
+ client.onMessage((event) => {
31
+ let msg: ExtServerMsg;
32
+ try {
33
+ msg = JSON.parse(event.data) as ExtServerMsg;
34
+ } catch {
35
+ return;
36
+ }
37
+
38
+ const store = useExtensionStore.getState();
39
+
40
+ switch (msg.type) {
41
+ case "contributions:update":
42
+ store.setContributions(msg.contributions);
43
+ break;
44
+
45
+ case "statusbar:update":
46
+ store.addStatusBarItem(msg.item);
47
+ break;
48
+
49
+ case "statusbar:remove":
50
+ store.removeStatusBarItem(msg.itemId);
51
+ break;
52
+
53
+ case "tree:update":
54
+ if (msg.parentId) {
55
+ store.updateTreeChildren(msg.viewId, msg.parentId, msg.items);
56
+ } else {
57
+ store.updateTree(msg.viewId, msg.items);
58
+ }
59
+ break;
60
+
61
+ case "tree:refresh":
62
+ store.removeTree(msg.viewId);
63
+ break;
64
+
65
+ case "notification": {
66
+ const toastFn = msg.level === "error" ? toast.error
67
+ : msg.level === "warn" ? toast.warning
68
+ : toast.info;
69
+ if (msg.actions && msg.actions.length > 0) {
70
+ const toastOpts: Record<string, unknown> = {
71
+ action: {
72
+ label: msg.actions[0],
73
+ onClick: () => send({ type: "notification:action", id: msg.id, action: msg.actions![0] ?? null }),
74
+ },
75
+ onDismiss: () => send({ type: "notification:action", id: msg.id, action: null }),
76
+ };
77
+ // Support a second action button via cancel
78
+ if (msg.actions.length > 1) {
79
+ toastOpts.cancel = {
80
+ label: msg.actions[1],
81
+ onClick: () => send({ type: "notification:action", id: msg.id, action: msg.actions![1] ?? null }),
82
+ };
83
+ }
84
+ toastFn(msg.message, toastOpts);
85
+ } else {
86
+ toastFn(msg.message);
87
+ }
88
+ break;
89
+ }
90
+
91
+ case "quickpick:show":
92
+ store.showQuickPick(
93
+ msg.items,
94
+ msg.options,
95
+ ).then((selected) => {
96
+ send({
97
+ type: "quickpick:resolve",
98
+ requestId: msg.requestId,
99
+ selected: selected ?? null,
100
+ });
101
+ });
102
+ break;
103
+
104
+ case "inputbox:show":
105
+ store.showInputBox(msg.options).then((value) => {
106
+ send({
107
+ type: "inputbox:resolve",
108
+ requestId: msg.requestId,
109
+ value: value ?? null,
110
+ });
111
+ });
112
+ break;
113
+
114
+ case "webview:create":
115
+ store.addWebviewPanel({
116
+ id: msg.panelId,
117
+ extensionId: msg.extensionId,
118
+ viewType: msg.viewType,
119
+ title: msg.title,
120
+ html: "",
121
+ });
122
+ // Open a tab to display the webview panel
123
+ useTabStore.getState().openTab({
124
+ type: "extension-webview",
125
+ title: msg.title,
126
+ projectId: null,
127
+ closable: true,
128
+ metadata: { panelId: msg.panelId, extensionId: msg.extensionId },
129
+ });
130
+ break;
131
+
132
+ case "webview:html":
133
+ store.updateWebviewPanel(msg.panelId, { html: msg.html });
134
+ break;
135
+
136
+ case "webview:dispose":
137
+ store.removeWebviewPanel(msg.panelId);
138
+ break;
139
+
140
+ case "webview:postMessage":
141
+ window.dispatchEvent(new CustomEvent("ext:webview:message", {
142
+ detail: { panelId: msg.panelId, message: msg.message },
143
+ }));
144
+ break;
145
+ }
146
+ });
147
+
148
+ // Listen for iframe→server messages (dispatched by ExtensionWebview component)
149
+ const webviewSendHandler = (e: Event) => {
150
+ const { panelId, message } = (e as CustomEvent).detail;
151
+ client.send(JSON.stringify({ type: "webview:message", panelId, message }));
152
+ };
153
+ window.addEventListener("ext:webview:send", webviewSendHandler);
154
+
155
+ // Listen for tree:expand requests (dispatched by ExtensionTreeView component)
156
+ const treeExpandHandler = (e: Event) => {
157
+ const { viewId, itemId } = (e as CustomEvent).detail;
158
+ client.send(JSON.stringify({ type: "tree:expand", viewId, itemId }));
159
+ };
160
+ window.addEventListener("ext:tree:expand", treeExpandHandler);
161
+
162
+ // Listen for command:execute requests (dispatched by StatusBar / TreeView)
163
+ const commandHandler = (e: Event) => {
164
+ const { command, args } = (e as CustomEvent).detail;
165
+ client.send(JSON.stringify({ type: "command:execute", command, args }));
166
+ };
167
+ window.addEventListener("ext:command:execute", commandHandler);
168
+
169
+ client.connect();
170
+
171
+ return () => {
172
+ window.removeEventListener("ext:webview:send", webviewSendHandler);
173
+ window.removeEventListener("ext:tree:expand", treeExpandHandler);
174
+ window.removeEventListener("ext:command:execute", commandHandler);
175
+ client.disconnect();
176
+ clientRef.current = null;
177
+ };
178
+ }, [send, enabled]);
179
+
180
+ return { send };
181
+ }
@@ -21,6 +21,7 @@ export function useGlobalKeybindings() {
21
21
 
22
22
  useEffect(() => {
23
23
  let lastShiftUp = 0;
24
+ let shiftAlone = false; // true if Shift was pressed without any other key
24
25
  const { matchesEvent } = useKeybindingsStore.getState();
25
26
 
26
27
  let composing = false;
@@ -28,9 +29,24 @@ export function useGlobalKeybindings() {
28
29
  function onCompositionEnd() { composing = false; }
29
30
 
30
31
  function handler(e: KeyboardEvent) {
32
+ // Track whether Shift is pressed alone (not as a modifier for another key)
33
+ if (e.type === "keydown" && e.key === "Shift") {
34
+ shiftAlone = true;
35
+ return;
36
+ }
37
+ // Any non-Shift keydown while Shift is held means Shift is used as modifier
38
+ if (e.type === "keydown" && e.shiftKey) {
39
+ shiftAlone = false;
40
+ }
41
+ // Any non-Shift key resets the double-tap timer (user is typing, not double-tapping)
42
+ if (e.type === "keydown" && e.key !== "Shift") {
43
+ lastShiftUp = 0;
44
+ }
45
+
31
46
  // Double-Shift detection (on keyup to avoid repeats) — always active
32
- // Skip during IME composition (e.g. Vietnamese Telex) to prevent false triggers
33
- if (e.type === "keyup" && e.key === "Shift" && !e.ctrlKey && !e.metaKey && !e.altKey && !composing && !e.isComposing) {
47
+ // Only counts if Shift was pressed alone (not used as modifier e.g. Shift+T for uppercase)
48
+ // Also skip during IME composition (e.g. Vietnamese Telex) to prevent false triggers
49
+ if (e.type === "keyup" && e.key === "Shift" && shiftAlone && !e.ctrlKey && !e.metaKey && !e.altKey && !composing && !e.isComposing) {
34
50
  const now = Date.now();
35
51
  if (now - lastShiftUp < 400) {
36
52
  lastShiftUp = 0;
@@ -1,4 +1,5 @@
1
1
  import { useEffect } from "react";
2
+ import { useConnectionStore } from "@/stores/connection-store";
2
3
 
3
4
  const POLL_NORMAL_MS = 10_000; // 10s when server is up
4
5
  const POLL_DOWN_MS = 2_000; // 2s when server is down (waiting for it to come back)
@@ -8,17 +9,21 @@ const POLL_DOWN_MS = 2_000; // 2s when server is down (waiting for it to com
8
9
  * When the server goes down and comes back up (restart/stop+start),
9
10
  * clears all browser/SW caches and reloads the page so the user
10
11
  * always gets fresh assets.
12
+ *
13
+ * Also updates the connection store to drive the ConnectionLostOverlay.
11
14
  */
12
15
  export function useServerReload() {
13
16
  useEffect(() => {
14
17
  let serverWasDown = false;
15
18
  let timer: ReturnType<typeof setTimeout>;
19
+ const { markDown, markUp } = useConnectionStore.getState();
16
20
 
17
21
  async function check() {
18
22
  try {
19
23
  const res = await fetch("/api/health", { cache: "no-store" });
20
24
  if (res.ok && serverWasDown) {
21
25
  // Server came back — clear caches then reload
26
+ markUp();
22
27
  if ("caches" in window) {
23
28
  const keys = await caches.keys();
24
29
  await Promise.all(keys.map((k) => caches.delete(k)));
@@ -26,9 +31,13 @@ export function useServerReload() {
26
31
  window.location.reload();
27
32
  return;
28
33
  }
34
+ if (res.ok) {
35
+ markUp();
36
+ }
29
37
  serverWasDown = false;
30
38
  } catch {
31
39
  serverWasDown = true;
40
+ markDown();
32
41
  }
33
42
  timer = setTimeout(check, serverWasDown ? POLL_DOWN_MS : POLL_NORMAL_MS);
34
43
  }