@hienlh/ppm 0.9.84 → 0.9.86

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 (252) hide show
  1. package/260413-1354-new-file-editor-tab/reports/code-reviewer-260413-1420-new-file-tab-review.md +210 -0
  2. package/CHANGELOG.md +23 -0
  3. package/bun.lock +259 -9
  4. package/dist/web/assets/{_basePickBy-5PGDJbfF.js → _basePickBy-Bj0dI1ei.js} +1 -1
  5. package/dist/web/assets/{_baseUniq-BT4Ow4Kk.js → _baseUniq-CyzdZeQH.js} +1 -1
  6. package/dist/web/assets/ai-settings-section-Bo9lCaTd.js +1 -0
  7. package/dist/web/assets/{api-settings-Bn-bIxD1.js → api-settings-CUxg9RE5.js} +1 -1
  8. package/dist/web/assets/{arc-BAOivWpI.js → arc-CxgHJ7Z4.js} +1 -1
  9. package/dist/web/assets/architecture-PBZL5I3N-DDFO_NKq.js +1 -0
  10. package/dist/web/assets/{architectureDiagram-2XIMDMQ5-Z-4eN4za.js → architectureDiagram-2XIMDMQ5-D16OotsC.js} +1 -1
  11. package/dist/web/assets/arrow-up-I9-21gkR.js +1 -0
  12. package/dist/web/assets/{blockDiagram-WCTKOSBZ-BCLqzhuZ.js → blockDiagram-WCTKOSBZ-Ct57Wtfk.js} +1 -1
  13. package/dist/web/assets/{c4Diagram-IC4MRINW-0Vp0Jeas.js → c4Diagram-IC4MRINW-BIymcNsg.js} +1 -1
  14. package/dist/web/assets/channel-wumTB1if.js +1 -0
  15. package/dist/web/assets/chat-tab-BEEd-Km4.js +10 -0
  16. package/dist/web/assets/chevron-right-DY_wImxB.js +1 -0
  17. package/dist/web/assets/{chunk-4BX2VUAB-D4tOov49.js → chunk-4BX2VUAB-CENmY7Kw.js} +1 -1
  18. package/dist/web/assets/{chunk-55IACEB6-DJ6BynZ4.js → chunk-55IACEB6-DhZGI1l3.js} +1 -1
  19. package/dist/web/assets/{chunk-7E7YKBS2-CiyUJxNI.js → chunk-7E7YKBS2-DZcnC7Ow.js} +1 -1
  20. package/dist/web/assets/{chunk-7R4GIKGN-Dv-4cAYn.js → chunk-7R4GIKGN-y8bfHEy-.js} +2 -2
  21. package/dist/web/assets/{chunk-C72U2L5F-D21mS_6G.js → chunk-C72U2L5F-BHPkfQj2.js} +1 -1
  22. package/dist/web/assets/{chunk-EGIJ26TM-DzqmU2Z7.js → chunk-EGIJ26TM-nant2LXl.js} +1 -1
  23. package/dist/web/assets/{chunk-FMBD7UC4-DXncblvW.js → chunk-FMBD7UC4-Bog4cpN-.js} +1 -1
  24. package/dist/web/assets/{chunk-GEFDOKGD-D-pKjlVd.js → chunk-GEFDOKGD-86LFbsAC.js} +1 -1
  25. package/dist/web/assets/chunk-GLR3WWYH-Re-5eSlQ.js +2 -0
  26. package/dist/web/assets/chunk-HHEYEP7N-C45i5G_3.js +1 -0
  27. package/dist/web/assets/{chunk-JSJVCQXG-99JzIdPr.js → chunk-JSJVCQXG-23eG9mgt.js} +1 -1
  28. package/dist/web/assets/{chunk-KX2RTZJC-CRq1OBZv.js → chunk-KX2RTZJC-CHj8TnTB.js} +1 -1
  29. package/dist/web/assets/{chunk-KYZI473N-Bb0MCaIO.js → chunk-KYZI473N-gqRLpJ4w.js} +1 -1
  30. package/dist/web/assets/{chunk-L3YUKLVL-C7qGJrfV.js → chunk-L3YUKLVL-DnSMmNFC.js} +1 -1
  31. package/dist/web/assets/{chunk-MX3YWQON-BpS_PtKp.js → chunk-MX3YWQON-B6g1ZH9X.js} +1 -1
  32. package/dist/web/assets/{chunk-NQ4KR5QH-z_blpjxi.js → chunk-NQ4KR5QH-DX32345Y.js} +1 -1
  33. package/dist/web/assets/{chunk-O4XLMI2P-nDhi_cVu.js → chunk-O4XLMI2P-Vp_V4P-b.js} +1 -1
  34. package/dist/web/assets/{chunk-OZEHJAEY-BXhYx3nO.js → chunk-OZEHJAEY-lKq2SWjA.js} +1 -1
  35. package/dist/web/assets/{chunk-PQ6SQG4A-TF58UVMU.js → chunk-PQ6SQG4A-Bik13fTV.js} +1 -1
  36. package/dist/web/assets/{chunk-PU5JKC2W-ek7k4QVB.js → chunk-PU5JKC2W-DD95Rx35.js} +1 -1
  37. package/dist/web/assets/chunk-QZHKN3VN-N3VXx1VH.js +1 -0
  38. package/dist/web/assets/{chunk-R5LLSJPH-CFwSJijQ.js → chunk-R5LLSJPH-dRhXRnrb.js} +1 -1
  39. package/dist/web/assets/{chunk-WL4C6EOR-ByUrSRin.js → chunk-WL4C6EOR-B1iIvLOG.js} +1 -1
  40. package/dist/web/assets/{chunk-XIRO2GV7-Djlmrely.js → chunk-XIRO2GV7-DZBoNl1_.js} +1 -1
  41. package/dist/web/assets/{chunk-XPW4576I-BPQQBakK.js → chunk-XPW4576I-CgLyyW03.js} +1 -1
  42. package/dist/web/assets/{chunk-XZSTWKYB-DxAOx4hG.js → chunk-XZSTWKYB-DjV8xl5A.js} +1 -1
  43. package/dist/web/assets/{chunk-YBOYWFTD-rQG3QH5s.js → chunk-YBOYWFTD-D_ILLe6_.js} +1 -1
  44. package/dist/web/assets/classDiagram-VBA2DB6C-mr-Cb1me.js +1 -0
  45. package/dist/web/assets/classDiagram-v2-RAHNMMFH-BKe8_uda.js +1 -0
  46. package/dist/web/assets/clone--z5KLAuR.js +1 -0
  47. package/dist/web/assets/code-editor-Ij4p30cr.js +8 -0
  48. package/dist/web/assets/columns-2-IeETSfON.js +1 -0
  49. package/dist/web/assets/{cose-bilkent-S5V4N54A-B_AWZsOP.js → cose-bilkent-S5V4N54A-BGNPFv3x.js} +1 -1
  50. package/dist/web/assets/{csv-preview-D2pJJj3K.js → csv-preview-CwQnOa3E.js} +2 -2
  51. package/dist/web/assets/{dagre-DHq9bhnd.js → dagre-CkhlMHnx.js} +1 -1
  52. package/dist/web/assets/{dagre-KLK3FWXG-BdJr7Byp.js → dagre-KLK3FWXG-Cnp996VG.js} +1 -1
  53. package/dist/web/assets/database-CgTomMxt.js +1 -0
  54. package/dist/web/assets/{database-viewer-Camu01H4.js → database-viewer-C1UHSgft.js} +2 -2
  55. package/dist/web/assets/{diagram-E7M64L7V-_db4pBVA.js → diagram-E7M64L7V-BZF0tSOr.js} +1 -1
  56. package/dist/web/assets/{diagram-IFDJBPK2-xKoeuiJx.js → diagram-IFDJBPK2-nUcO8sN8.js} +1 -1
  57. package/dist/web/assets/{diagram-P4PSJMXO-C8tjJsev.js → diagram-P4PSJMXO-CW0eCkwC.js} +1 -1
  58. package/dist/web/assets/diff-viewer-CVx5naBA.js +4 -0
  59. package/dist/web/assets/dist-CM0oD8tQ.js +1 -0
  60. package/dist/web/assets/{erDiagram-INFDFZHY-BSh2z9Df.js → erDiagram-INFDFZHY-DSkriYZ9.js} +1 -1
  61. package/dist/web/assets/extension-webview-CHVVpV34.js +3 -0
  62. package/dist/web/assets/{flowDiagram-PKNHOUZH-oYaovqyp.js → flowDiagram-PKNHOUZH-CFYAfZBx.js} +1 -1
  63. package/dist/web/assets/{ganttDiagram-A5KZAMGK-DmL26q2P.js → ganttDiagram-A5KZAMGK-KSn4XAU4.js} +1 -1
  64. package/dist/web/assets/gitGraph-HDMCJU4V-OkvBPi6H.js +1 -0
  65. package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-CMoukSrY.js → gitGraphDiagram-K3NZZRJ6-BMgjjVys.js} +1 -1
  66. package/dist/web/assets/{graphlib-BcsNnGcW.js → graphlib-BWe1iK_s.js} +1 -1
  67. package/dist/web/assets/index-OqgGFmh8.js +26 -0
  68. package/dist/web/assets/index-vA7juDri.css +2 -0
  69. package/dist/web/assets/info-3K5VOQVL-BDU2_bYD.js +1 -0
  70. package/dist/web/assets/infoDiagram-LFFYTUFH-Diq4Cyc3.js +2 -0
  71. package/dist/web/assets/input-BHj0veau.js +45 -0
  72. package/dist/web/assets/{isEmpty-bnrF3Qbc.js → isEmpty-BfLnxq-B.js} +1 -1
  73. package/dist/web/assets/{ishikawaDiagram-PHBUUO56-D05_LyL7.js → ishikawaDiagram-PHBUUO56-CiVEvp8o.js} +1 -1
  74. package/dist/web/assets/{journeyDiagram-4ABVD52K-B_L20qMe.js → journeyDiagram-4ABVD52K-CG_v5Aho.js} +1 -1
  75. package/dist/web/assets/jsx-runtime-BRW_vwa9.js +1 -0
  76. package/dist/web/assets/{kanban-definition-K7BYSVSG-CZ535BbZ.js → kanban-definition-K7BYSVSG-miB0-_Zq.js} +1 -1
  77. package/dist/web/assets/keybindings-store-BQxgPV5o.js +1 -0
  78. package/dist/web/assets/{line-CVvo3dRu.js → line-CSuSrJ9J.js} +1 -1
  79. package/dist/web/assets/{linear-DP4mkX3m.js → linear-DFN_MPsw.js} +1 -1
  80. package/dist/web/assets/markdown-renderer-CRy8xw2B.js +306 -0
  81. package/dist/web/assets/{mermaid-parser.core-C7UwoIh6.js → mermaid-parser.core-CFdP1Z5_.js} +2 -2
  82. package/dist/web/assets/{mindmap-definition-YRQLILUH-x0MTutJp.js → mindmap-definition-YRQLILUH-pYPWwASE.js} +1 -1
  83. package/dist/web/assets/{ordinal-_K3x1fkz.js → ordinal-DpFn432U.js} +1 -1
  84. package/dist/web/assets/packet-RMMSAZCW-BwpIpYB3.js +1 -0
  85. package/dist/web/assets/pie-UPGHQEXC-BPgAfmes.js +1 -0
  86. package/dist/web/assets/{pieDiagram-SKSYHLDU-C1Gjrtzy.js → pieDiagram-SKSYHLDU-Dovdlvhu.js} +1 -1
  87. package/dist/web/assets/plus-DQGIb4mQ.js +1 -0
  88. package/dist/web/assets/port-forwarding-tab-Biua8ov5.js +1 -0
  89. package/dist/web/assets/{postgres-viewer-BQdPMowm.js → postgres-viewer-BcVjCAl4.js} +3 -3
  90. package/dist/web/assets/{quadrantDiagram-337W2JSQ-C8bzJCjQ.js → quadrantDiagram-337W2JSQ-TXe6cU_F.js} +1 -1
  91. package/dist/web/assets/radar-KQ55EAFF-TqxBkWx-.js +1 -0
  92. package/dist/web/assets/refresh-cw-Clk8fdUD.js +1 -0
  93. package/dist/web/assets/{requirementDiagram-Z7DCOOCP-pQyah6WB.js → requirementDiagram-Z7DCOOCP-CuiiuGS9.js} +1 -1
  94. package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-T6RgG-N8.js → sankeyDiagram-WA2Y5GQK-BbRmhv0t.js} +1 -1
  95. package/dist/web/assets/scroll-area-BpXCNme3.js +1 -0
  96. package/dist/web/assets/{sequenceDiagram-2WXFIKYE-BQDJ4CVs.js → sequenceDiagram-2WXFIKYE-B2D8IQDb.js} +1 -1
  97. package/dist/web/assets/settings-tab-C9X-N8hE.js +1 -0
  98. package/dist/web/assets/{sql-query-editor-CY61vWBg.js → sql-query-editor-BFvRvJn0.js} +1 -1
  99. package/dist/web/assets/sqlite-viewer-CPfvwFl4.js +1 -0
  100. package/dist/web/assets/square-vBdqj0bF.js +1 -0
  101. package/dist/web/assets/{stateDiagram-RAJIS63D-66vhiIuk.js → stateDiagram-RAJIS63D-ylr4HxPu.js} +1 -1
  102. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-D6zvxf3M.js +1 -0
  103. package/dist/web/assets/table-Bi27fEaN.js +1 -0
  104. package/dist/web/assets/{terminal-tab-TIJmxHl6.js → terminal-tab-mWwk_weB.js} +2 -2
  105. package/dist/web/assets/text-wrap-D_OmSzhp.js +1 -0
  106. package/dist/web/assets/{timeline-definition-YZTLITO2-DwZqB3nn.js → timeline-definition-YZTLITO2-pMv1grvM.js} +1 -1
  107. package/dist/web/assets/trash-2-CNuB-htI.js +1 -0
  108. package/dist/web/assets/treemap-KZPCXAKY-Kck06FKU.js +1 -0
  109. package/dist/web/assets/{use-monaco-theme-BHn-LEm7.js → use-monaco-theme-CPaeSMAA.js} +1 -1
  110. package/dist/web/assets/{vennDiagram-LZ73GAT5-s9Z71fz-.js → vennDiagram-LZ73GAT5-C-rkIUbo.js} +1 -1
  111. package/dist/web/assets/x-Dw3TjeY_.js +1 -0
  112. package/dist/web/assets/{xychartDiagram-JWTSCODW-DRa_TH4B.js → xychartDiagram-JWTSCODW-CtpjAakO.js} +1 -1
  113. package/dist/web/index.html +18 -12
  114. package/dist/web/sw.js +1 -1
  115. package/docs/codebase-summary.md +134 -11
  116. package/docs/extension-development-guide.md +98 -1
  117. package/docs/journals/260414-1400-ext-git-graph-port-complete.md +147 -0
  118. package/docs/journals/260414-1452-git-graph-faithful-port.md +144 -0
  119. package/docs/journals/260414-1810-git-graph-ui-improvements-complete.md +261 -0
  120. package/docs/journals/260414-2001-bundled-extensions.md +219 -0
  121. package/docs/project-changelog.md +63 -22
  122. package/docs/project-roadmap.md +1 -0
  123. package/docs/system-architecture.md +33 -5
  124. package/package.json +9 -3
  125. package/packages/ext-git-graph/package.json +30 -0
  126. package/packages/ext-git-graph/src/extension-integration.test.ts +230 -0
  127. package/packages/ext-git-graph/src/extension-parsers.test.ts +193 -0
  128. package/packages/ext-git-graph/src/extension.ts +800 -0
  129. package/packages/ext-git-graph/src/git-log-parser.test.ts +271 -0
  130. package/packages/ext-git-graph/src/git-log-parser.ts +38 -0
  131. package/packages/ext-git-graph/src/types.ts +181 -0
  132. package/packages/ext-git-graph/src/webview-html.test.ts +142 -0
  133. package/packages/ext-git-graph/src/webview-html.ts +2199 -0
  134. package/packages/vscode-compat/src/index.ts +4 -0
  135. package/packages/vscode-compat/src/process.ts +25 -0
  136. package/packages/vscode-compat/src/window.ts +10 -0
  137. package/src/cli/commands/ext-cmd.ts +3 -1
  138. package/src/server/index.ts +1 -1
  139. package/src/server/ws/extensions.ts +6 -2
  140. package/src/services/contribution-registry.ts +14 -1
  141. package/src/services/extension-host-worker.ts +7 -3
  142. package/src/services/extension-manifest.ts +18 -1
  143. package/src/services/extension-rpc-handlers.ts +68 -2
  144. package/src/services/extension.service.ts +46 -6
  145. package/src/types/extension-messages.ts +2 -0
  146. package/src/types/extension.ts +8 -0
  147. package/src/web/components/editor/code-editor.tsx +83 -8
  148. package/src/web/components/editor/save-as-dialog.tsx +75 -0
  149. package/src/web/components/extensions/extension-webview.tsx +111 -12
  150. package/src/web/components/layout/command-palette.tsx +43 -17
  151. package/src/web/components/layout/draggable-tab.tsx +120 -67
  152. package/src/web/components/layout/editor-panel.tsx +15 -4
  153. package/src/web/components/layout/mobile-nav.tsx +74 -7
  154. package/src/web/components/layout/tab-bar.tsx +76 -4
  155. package/src/web/components/layout/tab-content.tsx +12 -5
  156. package/src/web/components/layout/upgrade-banner.tsx +3 -0
  157. package/src/web/components/settings/keyboard-shortcuts-section.tsx +46 -1
  158. package/src/web/components/shared/markdown-code-block.tsx +142 -0
  159. package/src/web/components/shared/markdown-context.ts +20 -0
  160. package/src/web/components/shared/markdown-renderer.tsx +113 -288
  161. package/src/web/hooks/use-extension-ws.ts +22 -4
  162. package/src/web/hooks/use-global-keybindings.ts +31 -2
  163. package/src/web/hooks/use-url-sync.ts +8 -3
  164. package/src/web/main.tsx +1 -0
  165. package/src/web/stores/keybindings-store.ts +3 -3
  166. package/src/web/stores/panel-store.ts +2 -2
  167. package/src/web/stores/panel-utils.ts +17 -2
  168. package/src/web/stores/tab-store.ts +17 -1
  169. package/src/web/styles/globals.css +6 -0
  170. package/.opencode/.env.example +0 -98
  171. package/.opencode/skills/ads-management/scripts/.env.example +0 -13
  172. package/.opencode/skills/ai-multimodal/.env.example +0 -230
  173. package/.opencode/skills/cip-design/.env.example +0 -6
  174. package/.opencode/skills/devops/.env.example +0 -76
  175. package/.opencode/skills/docs-seeker/.env.example +0 -15
  176. package/.opencode/skills/elevenlabs/.env.example +0 -3
  177. package/.opencode/skills/marketing-dashboard/.env.example +0 -15
  178. package/.opencode/skills/marketing-dashboard/app/.env.example +0 -2
  179. package/.opencode/skills/marketing-dashboard/server/.env.example +0 -2
  180. package/.opencode/skills/mcp-management/scripts/dist/analyze-tools.js +0 -70
  181. package/.opencode/skills/mcp-management/scripts/dist/cli.js +0 -160
  182. package/.opencode/skills/mcp-management/scripts/dist/mcp-client.js +0 -183
  183. package/.opencode/skills/payment-integration/scripts/.env.example +0 -20
  184. package/.opencode/skills/sequential-thinking/.env.example +0 -8
  185. package/dist/web/assets/architecture-PBZL5I3N-DEO2f3VD.js +0 -1
  186. package/dist/web/assets/arrow-up-BYhx9ckd.js +0 -1
  187. package/dist/web/assets/channel-By7bn0Yq.js +0 -1
  188. package/dist/web/assets/chat-tab-CT2XUgsc.js +0 -10
  189. package/dist/web/assets/chevron-right-4zq1jPv6.js +0 -1
  190. package/dist/web/assets/chunk-GLR3WWYH-DKikpoJM.js +0 -2
  191. package/dist/web/assets/chunk-HHEYEP7N-C7vxA5i9.js +0 -1
  192. package/dist/web/assets/chunk-QZHKN3VN-CYaTbeZf.js +0 -1
  193. package/dist/web/assets/classDiagram-VBA2DB6C-BA8Nj-_C.js +0 -1
  194. package/dist/web/assets/classDiagram-v2-RAHNMMFH-DjYu-6mn.js +0 -1
  195. package/dist/web/assets/clone-LRxlvnMj.js +0 -1
  196. package/dist/web/assets/code-editor-DQiPtcNd.js +0 -8
  197. package/dist/web/assets/columns-2-BoZAN-iw.js +0 -1
  198. package/dist/web/assets/createLucideIcon-PuMiQgHl.js +0 -1
  199. package/dist/web/assets/diff-viewer-CTwcVIP_.js +0 -4
  200. package/dist/web/assets/dist-DIV6WgAG.js +0 -41
  201. package/dist/web/assets/extension-webview-pU1xJyoc.js +0 -3
  202. package/dist/web/assets/git-graph-BnFbmpom.js +0 -1
  203. package/dist/web/assets/gitGraph-HDMCJU4V-Bwna3and.js +0 -1
  204. package/dist/web/assets/index-CP9KnaGh.js +0 -30
  205. package/dist/web/assets/index-Cxz7oGXY.css +0 -2
  206. package/dist/web/assets/info-3K5VOQVL-_vRxVNUm.js +0 -1
  207. package/dist/web/assets/infoDiagram-LFFYTUFH-DWwumDkq.js +0 -2
  208. package/dist/web/assets/jsx-runtime-kMwlnEGE.js +0 -1
  209. package/dist/web/assets/keybindings-store-DdhEeehv.js +0 -1
  210. package/dist/web/assets/markdown-renderer-BjYurPV4.js +0 -326
  211. package/dist/web/assets/packet-RMMSAZCW-DY5PNnZU.js +0 -1
  212. package/dist/web/assets/pie-UPGHQEXC-BHncZutv.js +0 -1
  213. package/dist/web/assets/port-forwarding-tab-Bgr8dmsw.js +0 -1
  214. package/dist/web/assets/radar-KQ55EAFF-DH0AOkUy.js +0 -1
  215. package/dist/web/assets/settings-tab-BNoboN6E.js +0 -1
  216. package/dist/web/assets/sqlite-viewer-srSbGg1D.js +0 -1
  217. package/dist/web/assets/square-oPKIkJiw.js +0 -1
  218. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-BGVqj_g9.js +0 -1
  219. package/dist/web/assets/table-DFevCOMd.js +0 -1
  220. package/dist/web/assets/tag-CXMT0QB6.js +0 -1
  221. package/dist/web/assets/text-wrap-BWNOVswA.js +0 -1
  222. package/dist/web/assets/treemap-KZPCXAKY-B2Xkyv-K.js +0 -1
  223. package/dist/web/assets/x-D2_KzIET.js +0 -1
  224. package/src/web/components/git/git-graph-branch-label.tsx +0 -124
  225. package/src/web/components/git/git-graph-constants.ts +0 -185
  226. package/src/web/components/git/git-graph-detail.tsx +0 -107
  227. package/src/web/components/git/git-graph-dialog.tsx +0 -72
  228. package/src/web/components/git/git-graph-row.tsx +0 -167
  229. package/src/web/components/git/git-graph-settings-dialog.tsx +0 -104
  230. package/src/web/components/git/git-graph-svg.tsx +0 -54
  231. package/src/web/components/git/git-graph-toolbar.tsx +0 -195
  232. package/src/web/components/git/git-graph.tsx +0 -193
  233. package/src/web/components/git/use-column-resize.ts +0 -33
  234. package/src/web/components/git/use-git-graph.ts +0 -201
  235. /package/dist/web/assets/{api-client-BfBM3I7n.js → api-client-BvxmRZUi.js} +0 -0
  236. /package/dist/web/assets/{array-B9UHiPd-.js → array-BFDiaBgf.js} +0 -0
  237. /package/dist/web/assets/{csv-parser-CNNw2RVA.js → csv-parser-i7fjqP2H.js} +0 -0
  238. /package/dist/web/assets/{cytoscape.esm-BW-DbntU.js → cytoscape.esm-C8i2jUzT.js} +0 -0
  239. /package/dist/web/assets/{defaultLocale-5eAKkKJC.js → defaultLocale-ZeknFqNe.js} +0 -0
  240. /package/dist/web/assets/{dist-CSJdAyA9.js → dist-DZmJeHOA.js} +0 -0
  241. /package/dist/web/assets/{init-DlZdxViB.js → init-0VJVrkRJ.js} +0 -0
  242. /package/dist/web/assets/{isArrayLikeObject-B_v2FtYn.js → isArrayLikeObject-ClzWCpcm.js} +0 -0
  243. /package/dist/web/assets/{katex-Bqvo_ZG0.js → katex-DR0kdMDv.js} +0 -0
  244. /package/dist/web/assets/{lib-DurwGtQO.js → lib-CeBVkQ-7.js} +0 -0
  245. /package/dist/web/assets/{math-069Z4SuC.js → math-CRc16Nj6.js} +0 -0
  246. /package/dist/web/assets/{path-6uRLdFF7.js → path-INs8XTPH.js} +0 -0
  247. /package/dist/web/assets/{preload-helper-Bf_JiD2A.js → preload-helper-mr3rCizq.js} +0 -0
  248. /package/dist/web/assets/{react-SKk5z-bm.js → react-0tkk-ztn.js} +0 -0
  249. /package/dist/web/assets/{rough.esm-JX0wREDd.js → rough.esm-eLccZ4OJ.js} +0 -0
  250. /package/dist/web/assets/{sql-completion-provider-DM9Qov6L.js → sql-completion-provider-B8uUWWej.js} +0 -0
  251. /package/dist/web/assets/{src-BqX54PbV.js → src-CqyWLlNZ.js} +0 -0
  252. /package/dist/web/assets/{utils-BNytJOb1.js → utils-DX8jb5qv.js} +0 -0
@@ -1,6 +1,11 @@
1
- import { useMemo, useRef, useEffect } from "react";
2
- import { marked } from "marked";
3
- import markedKatex from "marked-katex-extension";
1
+ import { useMemo, useCallback, useState, useEffect } from "react";
2
+ import ReactMarkdown from "react-markdown";
3
+ import remarkGfm from "remark-gfm";
4
+ import remarkMath from "remark-math";
5
+ import remarkBreaks from "remark-breaks";
6
+ import rehypeRaw from "rehype-raw";
7
+ import rehypeKatex from "rehype-katex";
8
+ import rehypeHighlight from "rehype-highlight";
4
9
  import { useTabStore } from "@/stores/tab-store";
5
10
  import { useFileStore, type FileNode } from "@/stores/file-store";
6
11
  import { useImageOverlay } from "@/stores/image-overlay-store";
@@ -8,35 +13,8 @@ import { useDiagramOverlay } from "@/stores/diagram-overlay-store";
8
13
  import { openCommandPalette } from "@/hooks/use-global-keybindings";
9
14
  import { api, projectUrl, getAuthToken } from "@/lib/api-client";
10
15
  import { basename } from "@/lib/utils";
11
- import mermaid from "mermaid";
12
-
13
- /** Mermaid keywords that start a diagram definition */
14
- 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/;
15
-
16
- let mermaidInitialized = false;
17
- function ensureMermaidInit() {
18
- if (mermaidInitialized) return;
19
- mermaid.initialize({
20
- startOnLoad: false,
21
- theme: "default",
22
- securityLevel: "loose",
23
- fontFamily: "ui-sans-serif, system-ui, sans-serif",
24
- });
25
- mermaidInitialized = true;
26
- }
27
-
28
- /** Detect local absolute file paths (Unix or Windows) */
29
- const LOCAL_PATH_RE = /^(\/|[A-Za-z]:[/\\])/;
30
-
31
- // Configure marked globally
32
- marked.use({ gfm: true, breaks: true });
33
- marked.use(markedKatex({ throwOnError: false }));
34
-
35
- /** Common text file extensions that PPM can open as editor tabs */
36
- const FILE_EXTS = "ts|tsx|js|jsx|mjs|cjs|py|json|md|mdx|yaml|yml|toml|css|scss|less|html|htm|sh|bash|zsh|go|rs|sql|rb|java|kt|swift|c|cpp|h|hpp|cs|vue|svelte|txt|env|cfg|conf|ini|xml|csv|log|dockerfile|makefile|gradle";
37
- const FILE_EXT_RE = new RegExp(`\\.(${FILE_EXTS})$`, "i");
38
- /** Glob/regex chars that indicate a pattern, not a real file */
39
- const GLOB_CHARS_RE = /[*?{}\[\]]/;
16
+ import { MdContext, useMdContext, FILE_EXT_RE, GLOB_CHARS_RE, LOCAL_PATH_RE } from "./markdown-context";
17
+ import { MdPre, MdCode } from "./markdown-code-block";
40
18
 
41
19
  interface MarkdownRendererProps {
42
20
  content: string;
@@ -46,286 +24,133 @@ interface MarkdownRendererProps {
46
24
  isStreaming?: boolean;
47
25
  }
48
26
 
49
- /**
50
- * Transform HTML string:
51
- * - Wrap tables in scrollable container
52
- * - Add target=_blank to external links
53
- * - Mark <a> file paths with data-file-path
54
- * - Make inline <code> with file names clickable (via HTML transform, not DOM)
55
- */
56
- function transformHtml(raw: string): string {
57
- let html = raw;
27
+ /** Plugin arrays — stable references to avoid re-creating on each render */
28
+ const remarkPlugins = [remarkGfm, remarkMath, remarkBreaks] as any;
29
+ const rehypePlugins = [rehypeRaw, rehypeKatex, rehypeHighlight] as any;
58
30
 
59
- // Wrap <table> in scroll container
60
- html = html.replace(/<table/g, '<div class="table-scroll-wrapper overflow-x-auto"><table');
61
- html = html.replace(/<\/table>/g, "</table></div>");
31
+ /** Component map stable references; dynamic state flows through MdContext */
32
+ const mdComponents = { a: MdLink, img: MdImage, pre: MdPre, code: MdCode, table: MdTable };
62
33
 
63
- // External links target=_blank
64
- html = html.replace(
65
- /<a\s+href="(https?:\/\/[^"]+)"/g,
66
- '<a href="$1" target="_blank" rel="noopener noreferrer"',
67
- );
68
-
69
- // <a> with file paths → add data-file-path (only files, not folders or glob patterns)
70
- html = html.replace(/<a\s+href="([^"]+)"/g, (match, href: string) => {
71
- if (/^https?:\/\//.test(href)) return match; // already handled
72
- if (GLOB_CHARS_RE.test(href)) return match; // skip glob/regex patterns
73
- if (!FILE_EXT_RE.test(href)) return match; // must have a file extension
74
- return `<a href="${href}" data-file-path="${href}"`;
75
- });
76
-
77
- // Inline <code> with file-like names → make clickable
78
- // Split by <pre>...</pre> blocks to avoid transforming code inside them
79
- const parts = html.split(/(<pre[\s\S]*?<\/pre>)/g);
80
- html = parts.map((part) => {
81
- // Skip <pre> blocks
82
- if (part.startsWith("<pre")) return part;
83
- // Transform inline <code> in non-pre content
84
- return part.replace(
85
- /<code>([^<]+)<\/code>/g,
86
- (match, text: string) => {
87
- const trimmed = text.trim();
88
- if (!trimmed || trimmed.includes(" ")) return match;
89
- if (GLOB_CHARS_RE.test(trimmed)) return match; // skip glob/regex patterns
90
- if (!FILE_EXT_RE.test(trimmed)) return match; // must have a file extension
91
- return `<code data-file-clickable="${trimmed}" style="cursor:pointer;text-decoration:underline;text-decoration-style:dotted">${text}</code>`;
92
- },
93
- );
94
- }).join("");
95
-
96
- return html;
34
+ function findInTree(nodes: FileNode[], name: string): string[] {
35
+ const results: string[] = [];
36
+ for (const n of nodes) {
37
+ if (n.type === "file" && n.name === name) results.push(n.path);
38
+ if (n.children) results.push(...findInTree(n.children, name));
39
+ }
40
+ return results;
97
41
  }
98
42
 
99
43
  export function MarkdownRenderer({ content, projectName, className = "", codeActions = false, isStreaming = false }: MarkdownRendererProps) {
100
- const html = useMemo(() => {
101
- try {
102
- const raw = marked.parse(content) as string;
103
- return transformHtml(raw);
104
- } catch {
105
- return content;
106
- }
107
- }, [content]);
108
-
109
- const containerRef = useRef<HTMLDivElement>(null);
110
44
  const openTab = useTabStore((s) => s.openTab);
111
45
  const fileTree = useFileStore((s) => s.tree);
112
- const openImageOverlay = useImageOverlay((s) => s.open);
113
- const openDiagramOverlay = useDiagramOverlay((s) => s.open);
46
+ const openImageOverlayFn = useImageOverlay((s) => s.open);
47
+ const openDiagramOverlayFn = useDiagramOverlay((s) => s.open);
114
48
 
115
- useEffect(() => {
116
- const container = containerRef.current;
117
- if (!container) return;
118
-
119
- // --- Render mermaid diagrams ---
120
- const renderMermaid = async () => {
121
- ensureMermaidInit();
122
- const pres = container.querySelectorAll("pre");
123
- for (const pre of pres) {
124
- const code = pre.querySelector("code");
125
- if (!code) continue;
126
- const langClass = code.className ?? "";
127
- const text = (code.textContent ?? "").trim();
128
- const isMermaid = langClass.includes("language-mermaid") || MERMAID_KEYWORDS.test(text);
129
- if (!isMermaid) continue;
130
-
131
- try {
132
- const id = `mermaid-${Math.random().toString(36).slice(2, 9)}`;
133
- const { svg } = await mermaid.render(id, text);
134
- const wrapper = document.createElement("div");
135
- wrapper.className = "mermaid-diagram group relative cursor-pointer rounded-lg border border-border bg-white dark:bg-zinc-50 p-3 overflow-x-auto my-2";
136
- wrapper.innerHTML = svg;
137
- // Expand icon hint
138
- const hint = document.createElement("div");
139
- hint.className = "absolute top-2 right-2 flex items-center gap-1 px-2 py-1 rounded bg-black/60 text-white text-xs can-hover:opacity-0 can-hover:group-hover:opacity-100 transition-opacity pointer-events-none";
140
- hint.textContent = "Click to expand";
141
- wrapper.appendChild(hint);
142
- // Click to open overlay
143
- wrapper.addEventListener("click", () => openDiagramOverlay(svg));
144
- pre.replaceWith(wrapper);
145
- } catch {
146
- // Render failed — leave as code block
147
- }
148
- }
149
- };
150
- renderMermaid();
151
-
152
- // --- Click handler for file links and clickable code ---
153
- const handleClick = (e: MouseEvent) => {
154
- const target = e.target as HTMLElement;
49
+ const openFileOrSearch = useCallback((filePath: string) => {
50
+ if (!filePath) return;
51
+ const isAbsolute = /^(\/|[A-Za-z]:[/\\])/.test(filePath);
52
+ const isRelative = /^(\.\/|\.\.\/)/.test(filePath);
53
+ const fileName = basename(filePath);
155
54
 
156
- // Check <a data-file-path>
157
- const link = target.closest("a[data-file-path]") as HTMLAnchorElement | null;
158
- if (link && container.contains(link)) {
159
- e.preventDefault();
160
- openFileOrSearch(link.getAttribute("data-file-path") ?? "");
161
- return;
162
- }
163
-
164
- // Check clickable <code>
165
- const code = target.closest("code[data-file-clickable]") as HTMLElement | null;
166
- if (code && container.contains(code)) {
167
- openFileOrSearch(code.getAttribute("data-file-clickable") ?? "");
168
- return;
55
+ const searchAndOpen = (fp: string) => {
56
+ const matches = findInTree(fileTree, basename(fp));
57
+ if (matches.length === 1) {
58
+ openTab({ type: "editor", title: basename(fp), metadata: { filePath: matches[0], projectName }, projectId: projectName ?? null, closable: true });
59
+ } else {
60
+ openCommandPalette(fp);
169
61
  }
170
62
  };
171
63
 
172
- /** Search file tree for matches by filename */
173
- function findInTree(nodes: FileNode[], name: string): string[] {
174
- const results: string[] = [];
175
- for (const node of nodes) {
176
- if (node.type === "file" && node.name === name) results.push(node.path);
177
- if (node.children) results.push(...findInTree(node.children, name));
178
- }
179
- return results;
180
- }
181
-
182
- function openFileOrSearch(filePath: string) {
183
- if (!filePath) return;
184
- const isAbsolute = /^(\/|[A-Za-z]:[/\\])/.test(filePath);
185
- const isRelative = /^(\.\/|\.\.\/)/.test(filePath);
186
- const fileName = basename(filePath);
187
-
188
- // Absolute path → verify then open
189
- if (isAbsolute) {
190
- const meta: Record<string, unknown> = { filePath };
191
- if (projectName) meta.projectName = projectName;
192
- api.get(`/api/fs/read?path=${encodeURIComponent(filePath)}`).then(() => {
193
- openTab({ type: "editor", title: fileName, metadata: meta, projectId: null, closable: true });
194
- }).catch(() => openCommandPalette(filePath));
195
- return;
196
- }
197
-
198
- // Relative path with ./ or ../ → try exact path in project
199
- if (isRelative && projectName) {
200
- const meta: Record<string, unknown> = { filePath, projectName };
201
- api.get(`${projectUrl(projectName)}/files/read?path=${encodeURIComponent(filePath)}`)
202
- .then(() => {
203
- openTab({ type: "editor", title: fileName, metadata: meta, projectId: projectName, closable: true });
204
- })
205
- .catch(() => searchAndOpen(filePath));
206
- return;
207
- }
208
-
209
- // Just a filename → search in project tree
210
- searchAndOpen(filePath);
64
+ if (isAbsolute) {
65
+ const meta: Record<string, unknown> = { filePath };
66
+ if (projectName) meta.projectName = projectName;
67
+ api.get(`/api/fs/read?path=${encodeURIComponent(filePath)}`).then(() => {
68
+ openTab({ type: "editor", title: fileName, metadata: meta, projectId: null, closable: true });
69
+ }).catch(() => openCommandPalette(filePath));
70
+ return;
211
71
  }
212
72
 
213
- /** Search project file tree; if 1 match open directly, else → command palette with full path */
214
- function searchAndOpen(filePath: string) {
215
- const fileName = basename(filePath);
216
- const matches = findInTree(fileTree, fileName);
217
- if (matches.length === 1) {
218
- const match = matches[0]!;
219
- openTab({
220
- type: "editor",
221
- title: fileName,
222
- metadata: { filePath: match, projectName },
223
- projectId: projectName ?? null,
224
- closable: true,
225
- });
226
- } else {
227
- openCommandPalette(filePath);
228
- }
73
+ if (isRelative && projectName) {
74
+ api.get(`${projectUrl(projectName)}/files/read?path=${encodeURIComponent(filePath)}`)
75
+ .then(() => openTab({ type: "editor", title: fileName, metadata: { filePath, projectName }, projectId: projectName, closable: true }))
76
+ .catch(() => searchAndOpen(filePath));
77
+ return;
229
78
  }
230
79
 
231
- container.addEventListener("click", handleClick);
232
-
233
- // --- Auth-load images with local file paths ---
234
- const blobUrls: string[] = [];
235
- container.querySelectorAll("img").forEach((img) => {
236
- const src = img.getAttribute("src") ?? "";
237
- // Only intercept local file paths, not http/data/blob URLs
238
- if (!LOCAL_PATH_RE.test(src)) return;
239
- // Mark as loading
240
- img.style.opacity = "0.3";
241
- img.style.minHeight = "48px";
242
- img.style.minWidth = "48px";
243
- const token = getAuthToken();
244
- fetch(`/api/fs/raw?path=${encodeURIComponent(src)}`, {
245
- headers: token ? { Authorization: `Bearer ${token}` } : {},
246
- })
247
- .then((r) => {
248
- if (!r.ok) throw new Error("Failed to load");
249
- return r.blob();
250
- })
251
- .then((blob) => {
252
- const url = URL.createObjectURL(blob);
253
- blobUrls.push(url);
254
- img.src = url;
255
- img.style.opacity = "";
256
- img.style.minHeight = "";
257
- img.style.minWidth = "";
258
- // Style: constrain size, add border like AuthImage
259
- img.style.maxHeight = "400px";
260
- img.style.maxWidth = "100%";
261
- img.style.objectFit = "contain";
262
- img.style.borderRadius = "0.375rem";
263
- img.style.border = "1px solid var(--color-border)";
264
- img.style.cursor = "pointer";
265
- img.onclick = () => openImageOverlay(url, img.alt || basename(src));
266
- })
267
- .catch(() => {
268
- img.style.opacity = "0.5";
269
- img.alt = `[Image not found: ${basename(src)}]`;
270
- });
271
- });
80
+ searchAndOpen(filePath);
81
+ }, [openTab, fileTree, projectName]);
272
82
 
273
- // --- Code block copy/run buttons ---
274
- if (codeActions) {
275
- container.querySelectorAll("pre").forEach((pre) => {
276
- if (pre.querySelector(".code-actions")) return;
277
- const code = pre.querySelector("code");
278
- const text = code?.textContent ?? pre.textContent ?? "";
279
- const langClass = code?.className ?? "";
280
- const isBash = /language-(bash|sh|shell|zsh)/.test(langClass)
281
- || (!langClass.includes("language-") && text.startsWith("$"));
83
+ const ctx = useMemo(() => ({
84
+ projectName, codeActions, openFileOrSearch,
85
+ openImageOverlay: openImageOverlayFn,
86
+ openDiagramOverlay: openDiagramOverlayFn,
87
+ }), [projectName, codeActions, openFileOrSearch, openImageOverlayFn, openDiagramOverlayFn]);
282
88
 
283
- pre.style.position = "relative";
284
- pre.classList.add("group");
285
-
286
- const actions = document.createElement("div");
287
- actions.className = "code-actions absolute top-1 right-1 flex gap-1";
89
+ return (
90
+ <MdContext.Provider value={ctx}>
91
+ <div className={`markdown-content prose-sm ${isStreaming ? "is-streaming" : ""} ${className}`}>
92
+ <ReactMarkdown remarkPlugins={remarkPlugins} rehypePlugins={rehypePlugins} components={mdComponents}>
93
+ {content}
94
+ </ReactMarkdown>
95
+ </div>
96
+ </MdContext.Provider>
97
+ );
98
+ }
288
99
 
289
- const copyBtn = document.createElement("button");
290
- copyBtn.className = "flex items-center justify-center size-6 rounded bg-surface-elevated/80 hover:bg-surface-elevated text-text-secondary hover:text-text-primary transition-colors border border-border/50";
291
- copyBtn.title = "Copy";
292
- copyBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`;
293
- copyBtn.addEventListener("click", () => {
294
- navigator.clipboard.writeText(text);
295
- copyBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`;
296
- setTimeout(() => {
297
- copyBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`;
298
- }, 2000);
299
- });
300
- actions.appendChild(copyBtn);
100
+ /** Link external links open in new tab; file paths open in editor */
101
+ function MdLink({ href, children, node, ...props }: any) {
102
+ const { openFileOrSearch } = useMdContext();
103
+ if (href?.match(/^https?:\/\//)) {
104
+ return <a href={href} target="_blank" rel="noopener noreferrer" {...props}>{children}</a>;
105
+ }
106
+ if (href && !GLOB_CHARS_RE.test(href) && FILE_EXT_RE.test(href)) {
107
+ return <a href={href} onClick={(e: React.MouseEvent) => { e.preventDefault(); openFileOrSearch(href); }} {...props}>{children}</a>;
108
+ }
109
+ return <a href={href} {...props}>{children}</a>;
110
+ }
301
111
 
302
- if (isBash && projectName) {
303
- const runBtn = document.createElement("button");
304
- runBtn.className = "flex items-center justify-center size-6 rounded bg-surface-elevated/80 hover:bg-surface-elevated text-text-secondary hover:text-text-primary transition-colors border border-border/50";
305
- runBtn.title = "Run in terminal";
306
- runBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>`;
307
- runBtn.addEventListener("click", () => {
308
- navigator.clipboard.writeText(text.replace(/^\$\s*/gm, ""));
309
- openTab({ type: "terminal", title: "Terminal", metadata: { projectName }, projectId: projectName, closable: true });
310
- });
311
- actions.appendChild(runBtn);
312
- }
112
+ /** Image auth-loads local file paths via API, click to open overlay */
113
+ function MdImage({ src, alt, node, ...props }: any) {
114
+ const { openImageOverlay } = useMdContext();
115
+ const [blobUrl, setBlobUrl] = useState<string | null>(null);
116
+ const [loading, setLoading] = useState(false);
313
117
 
314
- pre.appendChild(actions);
315
- });
316
- }
118
+ useEffect(() => {
119
+ if (!src || !LOCAL_PATH_RE.test(src)) return;
120
+ setLoading(true);
121
+ let cancelled = false;
122
+ let url: string | null = null;
123
+ const token = getAuthToken();
124
+ fetch(`/api/fs/raw?path=${encodeURIComponent(src)}`, {
125
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
126
+ })
127
+ .then((r) => { if (!r.ok) throw new Error(); return r.blob(); })
128
+ .then((blob) => {
129
+ if (cancelled) return;
130
+ url = URL.createObjectURL(blob);
131
+ setBlobUrl(url);
132
+ setLoading(false);
133
+ })
134
+ .catch(() => { if (!cancelled) setLoading(false); });
135
+ return () => { cancelled = true; if (url) URL.revokeObjectURL(url); };
136
+ }, [src]);
317
137
 
318
- return () => {
319
- container.removeEventListener("click", handleClick);
320
- blobUrls.forEach((u) => URL.revokeObjectURL(u));
321
- };
322
- }, [html, projectName, openTab, codeActions, openImageOverlay, openDiagramOverlay]);
138
+ const displaySrc = blobUrl || src || "";
139
+ const name = alt || (src ? basename(src) : "");
323
140
 
324
141
  return (
325
- <div
326
- ref={containerRef}
327
- className={`markdown-content prose-sm ${isStreaming ? "is-streaming" : ""} ${className}`}
328
- dangerouslySetInnerHTML={{ __html: html }}
142
+ <img
143
+ src={displaySrc}
144
+ alt={name}
145
+ onClick={() => displaySrc && openImageOverlay(displaySrc, name)}
146
+ className="max-h-[400px] max-w-full object-contain rounded-md border border-border cursor-pointer"
147
+ style={{ opacity: loading ? 0.3 : 1, minHeight: loading ? 48 : undefined, minWidth: loading ? 48 : undefined }}
148
+ {...props}
329
149
  />
330
150
  );
331
151
  }
152
+
153
+ /** Table — wrap in scrollable container */
154
+ function MdTable({ children, node, ...props }: any) {
155
+ return <div className="table-scroll-wrapper overflow-x-auto"><table {...props}>{children}</table></div>;
156
+ }
@@ -111,7 +111,8 @@ export function useExtensionWs(enabled = true) {
111
111
  });
112
112
  break;
113
113
 
114
- case "webview:create":
114
+ case "webview:create": {
115
+ const viewTypeSlug = msg.viewType.replace(/\.view$/, "");
115
116
  store.addWebviewPanel({
116
117
  id: msg.panelId,
117
118
  extensionId: msg.extensionId,
@@ -119,15 +120,18 @@ export function useExtensionWs(enabled = true) {
119
120
  title: msg.title,
120
121
  html: "",
121
122
  });
122
- // Open a tab to display the webview panel
123
+ // Open a tab use stable viewType slug as identifier (survives reload)
124
+ // Include projectName so reload can resolve project path for re-trigger
125
+ const currentProject = useTabStore.getState().currentProject;
123
126
  useTabStore.getState().openTab({
124
- type: "extension-webview",
127
+ type: "extension",
125
128
  title: msg.title,
126
129
  projectId: null,
127
130
  closable: true,
128
- metadata: { panelId: msg.panelId, extensionId: msg.extensionId },
131
+ metadata: { viewType: viewTypeSlug, panelId: msg.panelId, extensionId: msg.extensionId, ...(currentProject && { projectName: currentProject }) },
129
132
  });
130
133
  break;
134
+ }
131
135
 
132
136
  case "webview:html":
133
137
  store.updateWebviewPanel(msg.panelId, { html: msg.html });
@@ -142,6 +146,20 @@ export function useExtensionWs(enabled = true) {
142
146
  detail: { panelId: msg.panelId, message: msg.message },
143
147
  }));
144
148
  break;
149
+
150
+ case "tab:open":
151
+ useTabStore.getState().openTab({
152
+ type: (msg as any).tabType,
153
+ title: (msg as any).title,
154
+ projectId: (msg as any).projectId ?? null,
155
+ closable: (msg as any).closable ?? true,
156
+ metadata: (msg as any).metadata,
157
+ });
158
+ break;
159
+
160
+ case "project:switch":
161
+ useTabStore.getState().switchProject((msg as any).projectName);
162
+ break;
145
163
  }
146
164
  });
147
165
 
@@ -2,7 +2,8 @@ import { useEffect, useState, useCallback } from "react";
2
2
  import { useTabStore } from "@/stores/tab-store";
3
3
  import { useSettingsStore } from "@/stores/settings-store";
4
4
  import { useProjectStore } from "@/stores/project-store";
5
- import { useKeybindingsStore } from "@/stores/keybindings-store";
5
+ import { useKeybindingsStore, parseCombo, eventMatchesCombo } from "@/stores/keybindings-store";
6
+ import { useExtensionStore } from "@/stores/extension-store";
6
7
 
7
8
  /** Dispatch this event to open the command palette from anywhere, optionally with initial query */
8
9
  export function openCommandPalette(initialQuery?: string) {
@@ -101,11 +102,17 @@ export function useGlobalKeybindings() {
101
102
  return;
102
103
  }
103
104
 
105
+ // New file
106
+ if (match(e, "new-file")) {
107
+ e.preventDefault();
108
+ useTabStore.getState().openNewFile();
109
+ return;
110
+ }
111
+
104
112
  // Open tab shortcuts
105
113
  const tabShortcuts: { action: string; type: string; title: string }[] = [
106
114
  { action: "open-chat", type: "chat", title: "AI Chat" },
107
115
  { action: "open-terminal", type: "terminal", title: "Terminal" },
108
- { action: "open-git-graph", type: "git-graph", title: "Git Graph" },
109
116
  ];
110
117
  for (const s of tabShortcuts) {
111
118
  if (match(e, s.action)) {
@@ -169,6 +176,28 @@ export function useGlobalKeybindings() {
169
176
  return;
170
177
  }
171
178
  }
179
+
180
+ // Extension-contributed keybindings (with user override support)
181
+ const extKbs = useExtensionStore.getState().contributions?.keybindings;
182
+ if (extKbs) {
183
+ const mac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent);
184
+ const { getBinding: getBind } = useKeybindingsStore.getState();
185
+ for (const kb of extKbs) {
186
+ // User override via "ext:<command>" key, fallback to extension default
187
+ const overrideCombo = getBind(`ext:${kb.command}`);
188
+ const combo = overrideCombo || ((mac && kb.mac) ? kb.mac : kb.key);
189
+ if (combo && eventMatchesCombo(e, parseCombo(combo))) {
190
+ e.preventDefault();
191
+ const project = useProjectStore.getState().activeProject;
192
+ const args: unknown[] = [];
193
+ if (project?.path) args.push(project.path);
194
+ window.dispatchEvent(new CustomEvent("ext:command:execute", {
195
+ detail: { command: kb.command, args },
196
+ }));
197
+ return;
198
+ }
199
+ }
200
+ }
172
201
  }
173
202
 
174
203
  // Custom event listener for programmatic opening
@@ -14,7 +14,8 @@ export interface UrlState {
14
14
 
15
15
  const VALID_TAB_TYPES: TabType[] = [
16
16
  "terminal", "chat", "editor", "database", "sqlite",
17
- "postgres", "git-graph", "git-diff", "settings", "ports",
17
+ "postgres", "git-diff", "settings", "ports",
18
+ "extension",
18
19
  ];
19
20
 
20
21
  // ---------------------------------------------------------------------------
@@ -108,7 +109,6 @@ function buildMetadataFromUrl(
108
109
  return { ...(sessionId && { sessionId }), ...(providerId && { providerId }), projectName };
109
110
  }
110
111
  case "terminal": return { terminalIndex: parseInt(identifier ?? "1", 10), projectName };
111
- case "git-graph": return { projectName };
112
112
  case "git-diff": return identifier ? { filePath: identifier, projectName } : null;
113
113
  case "settings": return {};
114
114
  case "database": {
@@ -121,6 +121,7 @@ function buildMetadataFromUrl(
121
121
  return connId ? { connectionId: connId, tableName: tableName ?? "" } : null;
122
122
  }
123
123
  case "ports": return null;
124
+ case "extension": return identifier ? { viewType: identifier, projectName } : null;
124
125
  default: return null;
125
126
  }
126
127
  }
@@ -130,13 +131,17 @@ function buildTitleFromUrl(type: TabType, identifier: string | null): string {
130
131
  case "editor": return identifier?.split("/").pop() ?? "File";
131
132
  case "chat": return "Chat";
132
133
  case "terminal": return `Terminal ${identifier ?? "1"}`;
133
- case "git-graph": return "Git Graph";
134
134
  case "git-diff": return identifier?.split("/").pop() ?? "Diff";
135
135
  case "settings": return "Settings";
136
136
  case "database": return identifier ?? "Database";
137
137
  case "sqlite": return identifier?.split("/").pop() ?? "SQLite";
138
138
  case "postgres": return identifier ?? "PostgreSQL";
139
139
  case "ports": return "Ports";
140
+ case "extension": {
141
+ if (!identifier) return "Extension";
142
+ // "git-graph" → "Git Graph"
143
+ return identifier.split("-").map(w => (w[0]?.toUpperCase() ?? "") + w.slice(1)).join(" ");
144
+ }
140
145
  default: return type;
141
146
  }
142
147
  }
package/src/web/main.tsx CHANGED
@@ -3,6 +3,7 @@ import { createRoot } from "react-dom/client";
3
3
  import { App } from "./app.tsx";
4
4
  import "./styles/globals.css";
5
5
  import "katex/dist/katex.min.css";
6
+ import "highlight.js/styles/github-dark-dimmed.min.css";
6
7
 
7
8
  createRoot(document.getElementById("root")!).render(
8
9
  <StrictMode>
@@ -30,10 +30,10 @@ export const KEY_ACTIONS: KeyAction[] = [
30
30
  // Tabs
31
31
  { id: "next-tab", label: "Next Tab", category: "tabs", defaultKey: "Alt+]" },
32
32
  { id: "prev-tab", label: "Previous Tab", category: "tabs", defaultKey: "Alt+[" },
33
+ { id: "new-file", label: "New File", category: "tabs", defaultKey: "Mod+N" },
33
34
  { id: "open-chat", label: "Open Chat", category: "tabs", defaultKey: "Mod+L" },
34
35
  { id: "open-terminal", label: "Open Terminal", category: "tabs", defaultKey: "Mod+'" },
35
36
  { id: "open-settings", label: "Open Settings", category: "tabs", defaultKey: "Mod+," },
36
- { id: "open-git-graph", label: "Git Graph", category: "tabs", defaultKey: "Mod+G" },
37
37
  { id: "open-git-status", label: "Git Status (sidebar)", category: "tabs", defaultKey: "Mod+Shift+E" },
38
38
  { id: "open-search", label: "Search Files (sidebar)", category: "tabs", defaultKey: "Mod+Shift+F" },
39
39
  { id: "voice-input", label: "Voice Input", category: "general", defaultKey: "Mod+Shift+V", note: "Toggle speech-to-text in chat" },
@@ -61,7 +61,7 @@ interface ParsedCombo {
61
61
  key: string; // lowercase
62
62
  }
63
63
 
64
- function parseCombo(combo: string): ParsedCombo {
64
+ export function parseCombo(combo: string): ParsedCombo {
65
65
  const parts = combo.split("+");
66
66
  const result: ParsedCombo = { ctrl: false, meta: false, alt: false, shift: false, key: "" };
67
67
  for (const part of parts) {
@@ -81,7 +81,7 @@ function parseCombo(combo: string): ParsedCombo {
81
81
  return result;
82
82
  }
83
83
 
84
- function eventMatchesCombo(e: KeyboardEvent, combo: ParsedCombo): boolean {
84
+ export function eventMatchesCombo(e: KeyboardEvent, combo: ParsedCombo): boolean {
85
85
  if (e.ctrlKey !== combo.ctrl) return false;
86
86
  if (e.metaKey !== combo.meta) return false;
87
87
  if (e.altKey !== combo.alt) return false;
@@ -16,10 +16,10 @@ import {
16
16
  } from "./panel-utils";
17
17
 
18
18
  /** Tab types that can only have 1 instance per project */
19
- const SINGLETON_TYPES = new Set<TabType>(["git-graph", "settings"]);
19
+ const SINGLETON_TYPES = new Set<TabType>(["settings"]);
20
20
 
21
21
  /** Tab types removed in a prior version — filter them out when loading persisted state */
22
- const OBSOLETE_TAB_TYPES = new Set(["projects", "git-status"]);
22
+ const OBSOLETE_TAB_TYPES = new Set(["projects", "git-status", "git-graph"]);
23
23
 
24
24
  function pushHistory(history: string[], id: string): string[] {
25
25
  const filtered = history.filter((h) => h !== id);