@hienlh/ppm 0.8.86 → 0.8.88

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 (237) hide show
  1. package/CHANGELOG.md +216 -4
  2. package/bun.lock +5 -0
  3. package/dist/web/assets/{_basePickBy-5eBmZ_lt.js → _basePickBy-3Xe18azI.js} +1 -1
  4. package/dist/web/assets/{_baseUniq-DimLlN0y.js → _baseUniq-Yy35llnn.js} +1 -1
  5. package/dist/web/assets/api-settings-Dh4oFOpX.js +1 -0
  6. package/dist/web/assets/{arc-D4SasZrA.js → arc-B9n1Gvb5.js} +1 -1
  7. package/dist/web/assets/architecture-PBZL5I3N-CFzkFKEL.js +1 -0
  8. package/dist/web/assets/{architectureDiagram-2XIMDMQ5-nv0WbM7d.js → architectureDiagram-2XIMDMQ5-DqAZP_F6.js} +1 -1
  9. package/dist/web/assets/arrow-up--LjUXLEt.js +1 -0
  10. package/dist/web/assets/{blockDiagram-WCTKOSBZ-C1XvYrb8.js → blockDiagram-WCTKOSBZ-h3cDF2vI.js} +1 -1
  11. package/dist/web/assets/browser-tab-DJLH0eDY.js +1 -0
  12. package/dist/web/assets/{c4Diagram-IC4MRINW-CygDrbWJ.js → c4Diagram-IC4MRINW--pF1r5lr.js} +1 -1
  13. package/dist/web/assets/channel-C2fMafck.js +1 -0
  14. package/dist/web/assets/chat-tab-C8HFXqGS.js +8 -0
  15. package/dist/web/assets/chevron-right-CHnjJt4E.js +1 -0
  16. package/dist/web/assets/{chunk-4BX2VUAB-C2FDgsgT.js → chunk-4BX2VUAB-C3aZvW7B.js} +1 -1
  17. package/dist/web/assets/{chunk-55IACEB6-jF4w6cat.js → chunk-55IACEB6-D5cABeB9.js} +1 -1
  18. package/dist/web/assets/{chunk-7E7YKBS2-BVCECZFi.js → chunk-7E7YKBS2-CkFGv6Zs.js} +1 -1
  19. package/dist/web/assets/{chunk-7R4GIKGN-DXTbeu5d.js → chunk-7R4GIKGN-Dvbyu4Zw.js} +2 -2
  20. package/dist/web/assets/{chunk-C72U2L5F-BaZqOsTs.js → chunk-C72U2L5F-CtqKiH4q.js} +1 -1
  21. package/dist/web/assets/{chunk-EGIJ26TM-Bky2tcH7.js → chunk-EGIJ26TM-Cpr87sBR.js} +1 -1
  22. package/dist/web/assets/{chunk-FMBD7UC4-Cp4BK9A8.js → chunk-FMBD7UC4-D23YVTOU.js} +1 -1
  23. package/dist/web/assets/{chunk-GEFDOKGD-BosFEH7G.js → chunk-GEFDOKGD-tDjHsAUs.js} +1 -1
  24. package/dist/web/assets/chunk-GLR3WWYH-DBdWQ3zy.js +2 -0
  25. package/dist/web/assets/chunk-HHEYEP7N-BBw_z0fW.js +1 -0
  26. package/dist/web/assets/{chunk-JSJVCQXG-H5Gbjsbr.js → chunk-JSJVCQXG-BBmymCjA.js} +1 -1
  27. package/dist/web/assets/{chunk-KX2RTZJC-CWerSUwS.js → chunk-KX2RTZJC-DP36BDiU.js} +1 -1
  28. package/dist/web/assets/{chunk-KYZI473N-FvwP7jUy.js → chunk-KYZI473N-Djw13C-3.js} +1 -1
  29. package/dist/web/assets/{chunk-L3YUKLVL-D1PI_ORP.js → chunk-L3YUKLVL-HG_eMj_C.js} +1 -1
  30. package/dist/web/assets/{chunk-MX3YWQON-C7Vzk_AI.js → chunk-MX3YWQON-C2UEioMs.js} +1 -1
  31. package/dist/web/assets/{chunk-NQ4KR5QH-BceYBGYX.js → chunk-NQ4KR5QH-DXUTQ-BL.js} +1 -1
  32. package/dist/web/assets/{chunk-O4XLMI2P-WPtzgxql.js → chunk-O4XLMI2P-BsUWb9d0.js} +1 -1
  33. package/dist/web/assets/{chunk-OZEHJAEY-DlHXDeLY.js → chunk-OZEHJAEY-rG0P22U9.js} +1 -1
  34. package/dist/web/assets/{chunk-PQ6SQG4A-Ci_Prygb.js → chunk-PQ6SQG4A-DX0xW7kO.js} +1 -1
  35. package/dist/web/assets/{chunk-PU5JKC2W-CO0zMN-z.js → chunk-PU5JKC2W-C7Gry6md.js} +1 -1
  36. package/dist/web/assets/chunk-QZHKN3VN-DFKFM_C1.js +1 -0
  37. package/dist/web/assets/{chunk-R5LLSJPH-IAEEzfpM.js → chunk-R5LLSJPH-CMY0PkRK.js} +1 -1
  38. package/dist/web/assets/{chunk-WL4C6EOR-BLXalOgc.js → chunk-WL4C6EOR-CXuQvlyu.js} +1 -1
  39. package/dist/web/assets/{chunk-XIRO2GV7-Dx1Ri_p2.js → chunk-XIRO2GV7-DRJEb7Zb.js} +1 -1
  40. package/dist/web/assets/{chunk-XPW4576I-m9pPGKn7.js → chunk-XPW4576I-BPEX8KhL.js} +1 -1
  41. package/dist/web/assets/{chunk-XZSTWKYB-B_08ExbI.js → chunk-XZSTWKYB-Cb0iqycX.js} +1 -1
  42. package/dist/web/assets/{chunk-YBOYWFTD-DqSOVcYe.js → chunk-YBOYWFTD-av5aeHLq.js} +1 -1
  43. package/dist/web/assets/classDiagram-VBA2DB6C-Dp4Kk3Yb.js +1 -0
  44. package/dist/web/assets/classDiagram-v2-RAHNMMFH-D8IvcV_B.js +1 -0
  45. package/dist/web/assets/clone-B2hUek6n.js +1 -0
  46. package/dist/web/assets/code-editor-CaGdx-lS.js +2 -0
  47. package/dist/web/assets/{cose-bilkent-S5V4N54A-DlL82QHu.js → cose-bilkent-S5V4N54A-qudEiMCT.js} +1 -1
  48. package/dist/web/assets/csv-preview-DUbHtTAS.js +10 -0
  49. package/dist/web/assets/{dagre-BmVoh2At.js → dagre-BFcnKyBF.js} +1 -1
  50. package/dist/web/assets/{dagre-KLK3FWXG-sDrRW9MQ.js → dagre-KLK3FWXG-C3O-MTLf.js} +1 -1
  51. package/dist/web/assets/database-viewer-i4Ddk6mO.js +1 -0
  52. package/dist/web/assets/{diagram-E7M64L7V-ChnAhgni.js → diagram-E7M64L7V-DxPjK7_c.js} +1 -1
  53. package/dist/web/assets/{diagram-IFDJBPK2-DW1J1uJd.js → diagram-IFDJBPK2-sqTog_XV.js} +1 -1
  54. package/dist/web/assets/{diagram-P4PSJMXO-CQ32hyG_.js → diagram-P4PSJMXO-hzmp0GHK.js} +1 -1
  55. package/dist/web/assets/diff-viewer-DQDS7yjv.js +4 -0
  56. package/dist/web/assets/dist-CALwEtco.js +41 -0
  57. package/dist/web/assets/dist-DGDPTxs1.js +13 -0
  58. package/dist/web/assets/{erDiagram-INFDFZHY-6CHo6nOw.js → erDiagram-INFDFZHY-DLeYhAAT.js} +1 -1
  59. package/dist/web/assets/{flowDiagram-PKNHOUZH-DroDiNT0.js → flowDiagram-PKNHOUZH-CRxlE9Sr.js} +1 -1
  60. package/dist/web/assets/{ganttDiagram-A5KZAMGK-DP0QBh8w.js → ganttDiagram-A5KZAMGK-BdjmoMLS.js} +1 -1
  61. package/dist/web/assets/git-graph-DUs-TN1u.js +1 -0
  62. package/dist/web/assets/gitGraph-HDMCJU4V-CNlas3Rz.js +1 -0
  63. package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-DvU3JGZn.js → gitGraphDiagram-K3NZZRJ6-BeHSX7kk.js} +1 -1
  64. package/dist/web/assets/{graphlib-CQBb2thr.js → graphlib-Duh_bWLa.js} +1 -1
  65. package/dist/web/assets/index-DhtLEnPD.css +2 -0
  66. package/dist/web/assets/index-Dm6RN1A1.js +37 -0
  67. package/dist/web/assets/info-3K5VOQVL-BDzTLc11.js +1 -0
  68. package/dist/web/assets/infoDiagram-LFFYTUFH-ZZmpgc6t.js +2 -0
  69. package/dist/web/assets/{isEmpty-B4kqZBtn.js → isEmpty-B9L-Ge-H.js} +1 -1
  70. package/dist/web/assets/{ishikawaDiagram-PHBUUO56-46yibrV5.js → ishikawaDiagram-PHBUUO56-Cu0Rt1Ok.js} +1 -1
  71. package/dist/web/assets/{journeyDiagram-4ABVD52K-BcmRwjK-.js → journeyDiagram-4ABVD52K-CgDI-UG4.js} +1 -1
  72. package/dist/web/assets/{kanban-definition-K7BYSVSG-B619K53y.js → kanban-definition-K7BYSVSG-h4g10UHL.js} +1 -1
  73. package/dist/web/assets/keybindings-store-qVLDZz97.js +1 -0
  74. package/dist/web/assets/lib-BeaDXEkP.js +4 -0
  75. package/dist/web/assets/{line-1gcO63_w.js → line-B75-Rx70.js} +1 -1
  76. package/dist/web/assets/{linear-DfRqDoVd.js → linear-Bcjv9FQt.js} +1 -1
  77. package/dist/web/assets/markdown-renderer-L1NgC2Rw.js +69 -0
  78. package/dist/web/assets/{mermaid-parser.core-XtjZQOeM.js → mermaid-parser.core-8u2leTXI.js} +2 -2
  79. package/dist/web/assets/{mindmap-definition-YRQLILUH-CifOFo_q.js → mindmap-definition-YRQLILUH-BaOBwb-W.js} +1 -1
  80. package/dist/web/assets/{ordinal-BJYw-iDX.js → ordinal-LFEjVtwQ.js} +1 -1
  81. package/dist/web/assets/packet-RMMSAZCW-IVa5F-go.js +1 -0
  82. package/dist/web/assets/pie-UPGHQEXC-CvXHKAzp.js +1 -0
  83. package/dist/web/assets/{pieDiagram-SKSYHLDU-BuHUh_fO.js → pieDiagram-SKSYHLDU-At5Kz0KK.js} +1 -1
  84. package/dist/web/assets/postgres-viewer-_uDispGW.js +1 -0
  85. package/dist/web/assets/{quadrantDiagram-337W2JSQ-Bau_hj6Z.js → quadrantDiagram-337W2JSQ-CdjGIDfw.js} +1 -1
  86. package/dist/web/assets/radar-KQ55EAFF-Z-Tr5wtS.js +1 -0
  87. package/dist/web/assets/react-dom-Bpkvzu3U.js +1 -0
  88. package/dist/web/assets/{requirementDiagram-Z7DCOOCP-Cq2b-uwp.js → requirementDiagram-Z7DCOOCP-B9F_Cx_p.js} +1 -1
  89. package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-DrdGQxWQ.js → sankeyDiagram-WA2Y5GQK-RolPi8bU.js} +1 -1
  90. package/dist/web/assets/{sequenceDiagram-2WXFIKYE-qPxiTUcS.js → sequenceDiagram-2WXFIKYE-DM-tMAhx.js} +1 -1
  91. package/dist/web/assets/settings-tab-Bp4041i6.js +1 -0
  92. package/dist/web/assets/sqlite-viewer-GW-QCjHn.js +1 -0
  93. package/dist/web/assets/{stateDiagram-RAJIS63D-Dulj2oa8.js → stateDiagram-RAJIS63D-C4EMl6jf.js} +1 -1
  94. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-B-UjZch3.js +1 -0
  95. package/dist/web/assets/tab-store--SlERlDs.js +1 -0
  96. package/dist/web/assets/{terminal-tab-wKgpSPAT.js → terminal-tab-E4cWujj4.js} +2 -2
  97. package/dist/web/assets/{timeline-definition-YZTLITO2-BWyDnCYq.js → timeline-definition-YZTLITO2-A4PN_Efm.js} +1 -1
  98. package/dist/web/assets/treemap-KZPCXAKY-C9TYRE0k.js +1 -0
  99. package/dist/web/assets/use-monaco-theme-zABXAAla.js +11 -0
  100. package/dist/web/assets/{vennDiagram-LZ73GAT5-B9Iv2bNV.js → vennDiagram-LZ73GAT5-ywK7LMaH.js} +1 -1
  101. package/dist/web/assets/{xychartDiagram-JWTSCODW-ChXcMzBQ.js → xychartDiagram-JWTSCODW-DylHYNtJ.js} +1 -1
  102. package/dist/web/index.html +11 -11
  103. package/dist/web/sw.js +1 -1
  104. package/docs/code-standards.md +386 -6
  105. package/docs/codebase-summary.md +270 -98
  106. package/docs/design-guidelines.md +21 -0
  107. package/docs/project-changelog.md +150 -1
  108. package/docs/project-roadmap.md +41 -19
  109. package/docs/system-architecture.md +363 -15
  110. package/package.json +3 -2
  111. package/src/cli/commands/autostart.ts +1 -1
  112. package/src/cli/commands/restart.ts +9 -1
  113. package/src/cli/commands/status.ts +19 -0
  114. package/src/index.ts +2 -3
  115. package/src/providers/claude-agent-sdk.ts +316 -107
  116. package/src/providers/cli-provider-base.ts +238 -0
  117. package/src/providers/cursor-cli/cursor-event-mapper.ts +85 -0
  118. package/src/providers/cursor-cli/cursor-history.ts +207 -0
  119. package/src/providers/cursor-cli/cursor-provider.ts +146 -0
  120. package/src/providers/mock-provider.ts +7 -2
  121. package/src/providers/provider.interface.ts +1 -0
  122. package/src/providers/registry.ts +43 -4
  123. package/src/server/index.ts +44 -166
  124. package/src/server/routes/browser-preview.ts +159 -0
  125. package/src/server/routes/chat.ts +66 -6
  126. package/src/server/routes/mcp.ts +84 -0
  127. package/src/server/routes/project-scoped.ts +2 -0
  128. package/src/server/routes/proxy.ts +46 -53
  129. package/src/server/routes/settings.ts +14 -0
  130. package/src/server/routes/tunnel.ts +0 -32
  131. package/src/server/routes/workspace.ts +35 -0
  132. package/src/server/ws/chat.ts +302 -195
  133. package/src/services/account-selector.service.ts +16 -8
  134. package/src/services/account.service.ts +19 -13
  135. package/src/services/chat.service.ts +10 -15
  136. package/src/services/claude-usage.service.ts +48 -11
  137. package/src/services/cloud-ws.service.ts +227 -0
  138. package/src/services/cloud.service.ts +10 -6
  139. package/src/services/db.service.ts +119 -6
  140. package/src/services/mcp-config.service.ts +102 -0
  141. package/src/services/proxy.service.ts +4 -19
  142. package/src/services/supervisor.ts +285 -25
  143. package/src/types/api.ts +10 -2
  144. package/src/types/chat.ts +25 -2
  145. package/src/types/config.ts +33 -11
  146. package/src/types/mcp.ts +47 -0
  147. package/src/utils/ndjson-line-parser.ts +36 -0
  148. package/src/web/app.tsx +41 -35
  149. package/src/web/components/browser/browser-tab.tsx +106 -97
  150. package/src/web/components/chat/account-rotation-settings.tsx +163 -0
  151. package/src/web/components/chat/chat-history-bar.tsx +116 -31
  152. package/src/web/components/chat/chat-tab.tsx +31 -10
  153. package/src/web/components/chat/chat-welcome.tsx +148 -0
  154. package/src/web/components/chat/message-input.tsx +169 -16
  155. package/src/web/components/chat/message-list.tsx +27 -15
  156. package/src/web/components/chat/provider-selector.tsx +150 -0
  157. package/src/web/components/chat/session-picker.tsx +80 -31
  158. package/src/web/components/chat/usage-badge.tsx +11 -1
  159. package/src/web/components/editor/code-editor.tsx +36 -26
  160. package/src/web/components/editor/csv-preview.tsx +228 -0
  161. package/src/web/components/editor/editor-breadcrumb.tsx +216 -0
  162. package/src/web/components/editor/editor-toolbar.tsx +74 -0
  163. package/src/web/components/layout/command-palette.tsx +3 -1
  164. package/src/web/components/layout/editor-panel.tsx +162 -18
  165. package/src/web/components/layout/panel-layout.tsx +17 -1
  166. package/src/web/components/settings/ai-settings-section.tsx +196 -137
  167. package/src/web/components/settings/mcp-server-dialog.tsx +208 -0
  168. package/src/web/components/settings/mcp-settings-section.tsx +143 -0
  169. package/src/web/components/settings/proxy-settings-section.tsx +40 -42
  170. package/src/web/components/settings/settings-tab.tsx +5 -2
  171. package/src/web/components/shared/connection-lost-overlay.tsx +89 -0
  172. package/src/web/hooks/use-chat.ts +234 -207
  173. package/src/web/hooks/use-global-keybindings.ts +25 -2
  174. package/src/web/hooks/use-server-reload.ts +9 -0
  175. package/src/web/hooks/use-url-sync.ts +173 -21
  176. package/src/web/hooks/use-voice-input.ts +111 -0
  177. package/src/web/lib/api-mcp.ts +38 -0
  178. package/src/web/lib/csv-parser.ts +134 -0
  179. package/src/web/stores/connection-store.ts +39 -0
  180. package/src/web/stores/keybindings-store.ts +1 -0
  181. package/src/web/stores/panel-store.ts +73 -19
  182. package/src/web/stores/panel-utils.ts +145 -3
  183. package/dist/web/assets/api-settings-CFw-lh5k.js +0 -1
  184. package/dist/web/assets/architecture-PBZL5I3N-CJupe6q_.js +0 -1
  185. package/dist/web/assets/browser-tab-CmsL5eny.js +0 -1
  186. package/dist/web/assets/channel-DmKoFTd_.js +0 -1
  187. package/dist/web/assets/chat-tab-CFWsf13Z.js +0 -7
  188. package/dist/web/assets/chunk-GLR3WWYH-BnP-hOp6.js +0 -2
  189. package/dist/web/assets/chunk-HHEYEP7N-DKDPTPEZ.js +0 -1
  190. package/dist/web/assets/chunk-QZHKN3VN-C_wpI9wz.js +0 -1
  191. package/dist/web/assets/classDiagram-VBA2DB6C-B1T5uY-F.js +0 -1
  192. package/dist/web/assets/classDiagram-v2-RAHNMMFH-xs5vI3xC.js +0 -1
  193. package/dist/web/assets/clone-CijCFRT5.js +0 -1
  194. package/dist/web/assets/code-editor-H_dAh_fJ.js +0 -1
  195. package/dist/web/assets/database-viewer-DBzsgEJ8.js +0 -1
  196. package/dist/web/assets/diff-viewer-DzS-OnAR.js +0 -4
  197. package/dist/web/assets/dist-0Va_2L7G.js +0 -16
  198. package/dist/web/assets/dist-D9irYETY.js +0 -41
  199. package/dist/web/assets/git-graph-D3C7F8o3.js +0 -1
  200. package/dist/web/assets/gitGraph-HDMCJU4V-B0KvGQG8.js +0 -1
  201. package/dist/web/assets/index-CIkjfera.js +0 -31
  202. package/dist/web/assets/index-WKLuYsBY.css +0 -2
  203. package/dist/web/assets/info-3K5VOQVL-1uJ6_hCm.js +0 -1
  204. package/dist/web/assets/infoDiagram-LFFYTUFH-DLA5Q-3y.js +0 -2
  205. package/dist/web/assets/input-CGp1nFIg.js +0 -1
  206. package/dist/web/assets/keybindings-store-BdaoLwSo.js +0 -1
  207. package/dist/web/assets/markdown-renderer-DH49Zag7.js +0 -69
  208. package/dist/web/assets/packet-RMMSAZCW-34C4o9yj.js +0 -1
  209. package/dist/web/assets/pie-UPGHQEXC-D9ekKlh9.js +0 -1
  210. package/dist/web/assets/postgres-viewer-B9FYk8sD.js +0 -1
  211. package/dist/web/assets/radar-KQ55EAFF-DEuXOXSD.js +0 -1
  212. package/dist/web/assets/settings-store-DWXGVHsE.js +0 -2
  213. package/dist/web/assets/settings-tab-D-q8pd-5.js +0 -1
  214. package/dist/web/assets/sqlite-viewer-CDqcTePw.js +0 -1
  215. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-CAkzLlhk.js +0 -1
  216. package/dist/web/assets/tab-store-BPeiymiH.js +0 -1
  217. package/dist/web/assets/treemap-KZPCXAKY-nc7a1Ia1.js +0 -1
  218. package/dist/web/assets/use-monaco-theme-CCBTQ0S3.js +0 -11
  219. package/src/services/port-tunnel.service.ts +0 -97
  220. /package/dist/web/assets/{api-client-DOElml5u.js → api-client-BKIT_Qeg.js} +0 -0
  221. /package/dist/web/assets/{array-CYkMkqnU.js → array-DqLCdDFv.js} +0 -0
  222. /package/dist/web/assets/{columns-2-ChOTgl3e.js → columns-2-DbesTfa7.js} +0 -0
  223. /package/dist/web/assets/{cytoscape.esm-HeHO0VhB.js → cytoscape.esm-CWPXKqbJ.js} +0 -0
  224. /package/dist/web/assets/{defaultLocale-Beh6XjaL.js → defaultLocale-CrJzLgRD.js} +0 -0
  225. /package/dist/web/assets/{dist-BUYzeuKe.js → dist-Cep75xXf.js} +0 -0
  226. /package/dist/web/assets/{init-Rr1s_RiX.js → init-C0r9Gk5G.js} +0 -0
  227. /package/dist/web/assets/{isArrayLikeObject-BB-mzMLb.js → isArrayLikeObject-CGBoxvCD.js} +0 -0
  228. /package/dist/web/assets/{katex-CKoArbIw.js → katex-DzXRfQ_m.js} +0 -0
  229. /package/dist/web/assets/{math-B7b0HgJF.js → math-y9zN1W-N.js} +0 -0
  230. /package/dist/web/assets/{path-BAQ3hXlG.js → path-DIKpVbHL.js} +0 -0
  231. /package/dist/web/assets/{preload-helper-DeiOTZKJ.js → preload-helper-Bf_JiD2A.js} +0 -0
  232. /package/dist/web/assets/{react-Dev-wu-s.js → react-SKk5z-bm.js} +0 -0
  233. /package/dist/web/assets/{rough.esm-Dwml_la6.js → rough.esm-nHaDi0Kw.js} +0 -0
  234. /package/dist/web/assets/{src-B_cC68fH.js → src-Dw4QhedI.js} +0 -0
  235. /package/dist/web/assets/{table-COiJDPRA.js → table-CQVQM2SB.js} +0 -0
  236. /package/dist/web/assets/{tag-LMq02LfE.js → tag-Q2dZiSPX.js} +0 -0
  237. /package/dist/web/assets/{utils-btZ8C8-R.js → utils-DMiycH3O.js} +0 -0
@@ -0,0 +1,216 @@
1
+ import { useMemo, useRef, useEffect } from "react";
2
+ import { ChevronRight, Folder, File, FileCode, FileJson, FileText, FileType } from "lucide-react";
3
+ import {
4
+ DropdownMenu,
5
+ DropdownMenuContent,
6
+ DropdownMenuItem,
7
+ DropdownMenuTrigger,
8
+ DropdownMenuSub,
9
+ DropdownMenuSubTrigger,
10
+ DropdownMenuSubContent,
11
+ } from "@/components/ui/dropdown-menu";
12
+ import { useFileStore, type FileNode } from "@/stores/file-store";
13
+ import { useTabStore } from "@/stores/tab-store";
14
+ import { basename } from "@/lib/utils";
15
+
16
+ const ICON_MAP: Record<string, React.ComponentType<{ className?: string }>> = {
17
+ ts: FileCode, tsx: FileCode, js: FileCode, jsx: FileCode,
18
+ py: FileCode, rs: FileCode, go: FileCode, html: FileCode,
19
+ css: FileCode, scss: FileCode,
20
+ json: FileJson,
21
+ md: FileText, txt: FileText,
22
+ yaml: FileType, yml: FileType,
23
+ };
24
+
25
+ function getIcon(name: string, isDir: boolean) {
26
+ if (isDir) return Folder;
27
+ const ext = name.split(".").pop()?.toLowerCase() ?? "";
28
+ return ICON_MAP[ext] ?? File;
29
+ }
30
+
31
+ interface BreadcrumbSegment {
32
+ name: string;
33
+ fullPath: string;
34
+ node: FileNode | null;
35
+ siblings: FileNode[];
36
+ }
37
+
38
+ function walkTree(tree: FileNode[], segments: string[]): BreadcrumbSegment[] {
39
+ const result: BreadcrumbSegment[] = [];
40
+ let current: FileNode[] = tree;
41
+
42
+ for (let i = 0; i < segments.length; i++) {
43
+ const seg = segments[i]!;
44
+ const fullPath = segments.slice(0, i + 1).join("/");
45
+ const match = current.find((n) => n.name === seg);
46
+ result.push({
47
+ name: seg,
48
+ fullPath,
49
+ node: match ?? null,
50
+ siblings: current,
51
+ });
52
+ if (match?.children) {
53
+ current = match.children;
54
+ } else {
55
+ // Remaining segments have no tree data — add as plain
56
+ for (let j = i + 1; j < segments.length; j++) {
57
+ result.push({
58
+ name: segments[j]!,
59
+ fullPath: segments.slice(0, j + 1).join("/"),
60
+ node: null,
61
+ siblings: [],
62
+ });
63
+ }
64
+ break;
65
+ }
66
+ }
67
+ return result;
68
+ }
69
+
70
+ function sortNodes(nodes: FileNode[]): FileNode[] {
71
+ return [...nodes].sort((a, b) => {
72
+ if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
73
+ return a.name.localeCompare(b.name);
74
+ });
75
+ }
76
+
77
+ interface EditorBreadcrumbProps {
78
+ filePath: string;
79
+ projectName: string;
80
+ tabId: string;
81
+ className?: string;
82
+ }
83
+
84
+ export function EditorBreadcrumb({ filePath, projectName, tabId, className }: EditorBreadcrumbProps) {
85
+ const tree = useFileStore((s) => s.tree);
86
+ const { updateTab, openTab } = useTabStore();
87
+ const scrollRef = useRef<HTMLDivElement>(null);
88
+
89
+ const segments = useMemo(
90
+ () => walkTree(tree, filePath.split("/").filter(Boolean)),
91
+ [tree, filePath],
92
+ );
93
+
94
+ // Auto-scroll to rightmost segment
95
+ useEffect(() => {
96
+ if (scrollRef.current) {
97
+ scrollRef.current.scrollLeft = scrollRef.current.scrollWidth;
98
+ }
99
+ }, [segments]);
100
+
101
+ function handleFileClick(path: string, e: React.MouseEvent) {
102
+ const name = basename(path);
103
+ if (e.metaKey || e.ctrlKey) {
104
+ openTab({ type: "editor", title: name, metadata: { filePath: path, projectName }, projectId: projectName, closable: true });
105
+ } else {
106
+ updateTab(tabId, { title: name, metadata: { filePath: path, projectName } });
107
+ }
108
+ }
109
+
110
+ return (
111
+ <div ref={scrollRef} className={className}>
112
+ {segments.map((seg, i) => (
113
+ <div key={seg.fullPath} className="flex items-center shrink-0">
114
+ {i > 0 && <ChevronRight className="size-3 text-muted-foreground shrink-0 mx-0.5" />}
115
+ {seg.siblings.length > 0 ? (
116
+ <SegmentDropdown
117
+ segment={seg}
118
+ isLast={i === segments.length - 1}
119
+ projectName={projectName}
120
+ onFileClick={handleFileClick}
121
+ />
122
+ ) : (
123
+ <span className="text-xs text-muted-foreground px-1 py-0.5">{seg.name}</span>
124
+ )}
125
+ </div>
126
+ ))}
127
+ </div>
128
+ );
129
+ }
130
+
131
+ interface SegmentDropdownProps {
132
+ segment: BreadcrumbSegment;
133
+ isLast: boolean;
134
+ projectName: string;
135
+ onFileClick: (path: string, e: React.MouseEvent) => void;
136
+ }
137
+
138
+ function SegmentDropdown({ segment, isLast, projectName, onFileClick }: SegmentDropdownProps) {
139
+ const sorted = useMemo(() => sortNodes(segment.siblings), [segment.siblings]);
140
+
141
+ return (
142
+ <DropdownMenu>
143
+ <DropdownMenuTrigger asChild>
144
+ <button
145
+ type="button"
146
+ className={`text-xs px-1 py-0.5 rounded hover:bg-muted transition-colors truncate max-w-[120px] ${
147
+ isLast ? "text-foreground font-medium" : "text-muted-foreground"
148
+ }`}
149
+ >
150
+ {segment.name}
151
+ </button>
152
+ </DropdownMenuTrigger>
153
+ <DropdownMenuContent align="start" className="max-h-[300px] p-1">
154
+ {sorted.map((node) => (
155
+ <NodeMenuItem
156
+ key={node.path}
157
+ node={node}
158
+ projectName={projectName}
159
+ activePath={segment.fullPath}
160
+ onFileClick={onFileClick}
161
+ />
162
+ ))}
163
+ </DropdownMenuContent>
164
+ </DropdownMenu>
165
+ );
166
+ }
167
+
168
+ interface NodeMenuItemProps {
169
+ node: FileNode;
170
+ projectName: string;
171
+ activePath: string;
172
+ onFileClick: (path: string, e: React.MouseEvent) => void;
173
+ }
174
+
175
+ function NodeMenuItem({ node, projectName, activePath, onFileClick }: NodeMenuItemProps) {
176
+ const Icon = getIcon(node.name, node.type === "directory");
177
+ const isActive = node.path === activePath;
178
+
179
+ if (node.type === "directory" && node.children && node.children.length > 0) {
180
+ return (
181
+ <DropdownMenuSub>
182
+ <DropdownMenuSubTrigger className={`text-xs gap-1.5 ${isActive ? "bg-muted" : ""}`}>
183
+ <Icon className="size-3.5 shrink-0 text-muted-foreground" />
184
+ <span className="truncate">{node.name}</span>
185
+ </DropdownMenuSubTrigger>
186
+ <DropdownMenuSubContent className="max-h-[300px] overflow-y-auto p-1">
187
+ {sortNodes(node.children).map((child) => (
188
+ <NodeMenuItem
189
+ key={child.path}
190
+ node={child}
191
+ projectName={projectName}
192
+ activePath={activePath}
193
+ onFileClick={onFileClick}
194
+ />
195
+ ))}
196
+ </DropdownMenuSubContent>
197
+ </DropdownMenuSub>
198
+ );
199
+ }
200
+
201
+ return (
202
+ <DropdownMenuItem
203
+ className={`text-xs gap-1.5 cursor-pointer ${isActive ? "bg-muted" : ""}`}
204
+ onSelect={(e) => {
205
+ // onSelect doesn't give MouseEvent, use click handler for Ctrl detection
206
+ }}
207
+ onClick={(e) => {
208
+ if (node.type === "directory") return;
209
+ onFileClick(node.path, e);
210
+ }}
211
+ >
212
+ <Icon className="size-3.5 shrink-0 text-muted-foreground" />
213
+ <span className="truncate">{node.name}</span>
214
+ </DropdownMenuItem>
215
+ );
216
+ }
@@ -0,0 +1,74 @@
1
+ import { Code, Eye, WrapText, Table } from "lucide-react";
2
+
3
+ interface EditorToolbarProps {
4
+ ext: string;
5
+ mdMode?: "edit" | "preview";
6
+ onMdModeChange?: (mode: "edit" | "preview") => void;
7
+ csvMode?: "table" | "raw";
8
+ onCsvModeChange?: (mode: "table" | "raw") => void;
9
+ wordWrap: boolean;
10
+ onToggleWordWrap: () => void;
11
+ className?: string;
12
+ }
13
+
14
+ function ToolbarButton({
15
+ active,
16
+ onClick,
17
+ icon: Icon,
18
+ label,
19
+ }: {
20
+ active: boolean;
21
+ onClick: () => void;
22
+ icon: React.ComponentType<{ className?: string }>;
23
+ label: string;
24
+ }) {
25
+ return (
26
+ <button
27
+ type="button"
28
+ onClick={onClick}
29
+ className={`flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors ${
30
+ active ? "bg-muted text-foreground" : "text-muted-foreground hover:text-foreground"
31
+ }`}
32
+ >
33
+ <Icon className="size-3" />
34
+ <span className="hidden sm:inline">{label}</span>
35
+ </button>
36
+ );
37
+ }
38
+
39
+ export function EditorToolbar({
40
+ ext,
41
+ mdMode,
42
+ onMdModeChange,
43
+ csvMode,
44
+ onCsvModeChange,
45
+ wordWrap,
46
+ onToggleWordWrap,
47
+ className,
48
+ }: EditorToolbarProps) {
49
+ const isMarkdown = ext === "md" || ext === "mdx";
50
+ const isCsv = ext === "csv";
51
+
52
+ return (
53
+ <div className={className}>
54
+ {isMarkdown && onMdModeChange && (
55
+ <>
56
+ <ToolbarButton active={mdMode === "edit"} onClick={() => onMdModeChange("edit")} icon={Code} label="Edit" />
57
+ <ToolbarButton active={mdMode === "preview"} onClick={() => onMdModeChange("preview")} icon={Eye} label="Preview" />
58
+ </>
59
+ )}
60
+ {isCsv && onCsvModeChange && (
61
+ <>
62
+ <ToolbarButton active={csvMode === "table"} onClick={() => onCsvModeChange("table")} icon={Table} label="Table" />
63
+ <ToolbarButton active={csvMode === "raw"} onClick={() => onCsvModeChange("raw")} icon={Code} label="Raw" />
64
+ </>
65
+ )}
66
+ <ToolbarButton
67
+ active={wordWrap}
68
+ onClick={onToggleWordWrap}
69
+ icon={WrapText}
70
+ label="Wrap"
71
+ />
72
+ </div>
73
+ );
74
+ }
@@ -11,6 +11,7 @@ import {
11
11
  FolderOpen,
12
12
  Loader2,
13
13
  Globe,
14
+ Mic,
14
15
  } from "lucide-react";
15
16
  import { useTabStore, type TabType } from "@/stores/tab-store";
16
17
  import { useProjectStore } from "@/stores/project-store";
@@ -157,8 +158,9 @@ export function CommandPalette({ open, onClose, initialQuery = "" }: { open: boo
157
158
  { id: "chat", label: "New AI Chat", icon: MessageSquare, action: openNewTab("chat", "AI Chat"), keywords: "ai assistant claude", group: "action", shortcut: formatShortcut(getBinding("open-chat")) },
158
159
  { id: "terminal", label: "New Terminal", icon: Terminal, action: openNewTab("terminal", "Terminal"), keywords: "bash shell console", group: "action", shortcut: formatShortcut(getBinding("open-terminal")) },
159
160
  { id: "git-graph", label: "Git Graph", icon: GitBranch, action: openNewTab("git-graph", "Git Graph"), keywords: "branch history log", group: "action", shortcut: formatShortcut(getBinding("open-git-graph")) },
161
+ { id: "browser", label: "Open Browser", icon: Globe, action: openNewTab("browser", "Browser"), keywords: "web preview localhost iframe url", group: "action" },
160
162
  { id: "postgres", label: "PostgreSQL", icon: Database, action: openNewTab("postgres", "PostgreSQL"), keywords: "database pg sql query", group: "action" },
161
- { id: "browser", label: "Browser (Port Tunnel)", icon: Globe, action: openNewTab("browser", "Browser"), keywords: "preview tunnel port iframe web", group: "action" },
163
+ { id: "voice-input", label: "Voice Input", icon: Mic, action: () => { window.dispatchEvent(new CustomEvent("toggle-voice-input")); onClose(); }, keywords: "speech microphone dictate voice", group: "action", shortcut: formatShortcut(getBinding("voice-input")) },
162
164
  { id: "git-status", label: "Git Status", icon: GitCommitHorizontal, action: () => { setSidebarActiveTab("git"); onClose(); }, keywords: "changes diff staged", group: "action", shortcut: formatShortcut(getBinding("open-git-status")) },
163
165
  {
164
166
  id: "settings", label: "Settings", icon: Settings,
@@ -1,8 +1,10 @@
1
- import { Suspense, lazy } from "react";
2
- import { Loader2, Terminal, MessageSquare, GitBranch } from "lucide-react";
1
+ import { Suspense, lazy, useEffect, useState, useCallback } from "react";
2
+ import { ChevronDown, ChevronUp, Loader2, Terminal, MessageSquare, GitBranch, Pin, PinOff } from "lucide-react";
3
3
  import { usePanelStore } from "@/stores/panel-store";
4
4
  import { useProjectStore } from "@/stores/project-store";
5
5
  import type { TabType } from "@/stores/tab-store";
6
+ import { api, projectUrl } from "@/lib/api-client";
7
+ import type { SessionInfo } from "../../../types/chat";
6
8
  import { TabBar } from "./tab-bar";
7
9
  import { SplitDropOverlay } from "./split-drop-overlay";
8
10
  import { cn } from "@/lib/utils";
@@ -74,8 +76,70 @@ export function EditorPanel({ panelId, projectName }: EditorPanelProps) {
74
76
  );
75
77
  }
76
78
 
79
+ function formatRelativeDate(iso: string): string {
80
+ try {
81
+ const date = new Date(iso);
82
+ const now = new Date();
83
+ const diffMs = now.getTime() - date.getTime();
84
+ const diffMin = Math.floor(diffMs / 60_000);
85
+ if (diffMin < 1) return "Just now";
86
+ if (diffMin < 60) return `${diffMin}m ago`;
87
+ const diffHr = Math.floor(diffMin / 60);
88
+ if (diffHr < 24) return `${diffHr}h ago`;
89
+ const diffDay = Math.floor(diffHr / 24);
90
+ if (diffDay < 7) return `${diffDay}d ago`;
91
+ return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
92
+ } catch {
93
+ return "";
94
+ }
95
+ }
96
+
97
+ const MAX_RECENT_SESSIONS = 5;
98
+ const FETCH_SESSIONS_LIMIT = 20;
99
+
77
100
  function EmptyPanel({ panelId }: { panelId: string }) {
78
101
  const activeProject = useProjectStore((s) => s.activeProject);
102
+ const [sessions, setSessions] = useState<SessionInfo[]>([]);
103
+ const [loadingSessions, setLoadingSessions] = useState(false);
104
+ const [showAll, setShowAll] = useState(false);
105
+
106
+ const loadSessions = useCallback(async () => {
107
+ if (!activeProject?.name) return;
108
+ setLoadingSessions(true);
109
+ try {
110
+ const data = await api.get<SessionInfo[]>(`${projectUrl(activeProject.name)}/chat/sessions`);
111
+ setSessions(data.slice(0, FETCH_SESSIONS_LIMIT));
112
+ } catch {
113
+ // silently ignore — empty state still functional without sessions
114
+ } finally {
115
+ setLoadingSessions(false);
116
+ }
117
+ }, [activeProject?.name]);
118
+
119
+ useEffect(() => { loadSessions(); }, [loadSessions]);
120
+
121
+ const togglePin = useCallback(async (e: React.MouseEvent, session: SessionInfo) => {
122
+ e.stopPropagation();
123
+ if (!activeProject?.name) return;
124
+ const url = `${projectUrl(activeProject.name)}/chat/sessions/${session.id}/pin`;
125
+ try {
126
+ if (session.pinned) {
127
+ await api.del(url);
128
+ } else {
129
+ await api.put(url);
130
+ }
131
+ setSessions((prev) => {
132
+ const updated = prev.map((s) => s.id === session.id ? { ...s, pinned: !s.pinned } : s);
133
+ return updated.sort((a, b) => {
134
+ if (a.pinned && !b.pinned) return -1;
135
+ if (!a.pinned && b.pinned) return 1;
136
+ return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
137
+ });
138
+ });
139
+ } catch {
140
+ // silently ignore
141
+ }
142
+ }, [activeProject?.name]);
79
143
 
80
144
  function openTab(type: TabType) {
81
145
  const needsProject = type !== "settings";
@@ -86,23 +150,103 @@ function EmptyPanel({ panelId }: { panelId: string }) {
86
150
  );
87
151
  }
88
152
 
153
+ function openSession(session: SessionInfo) {
154
+ usePanelStore.getState().openTab(
155
+ {
156
+ type: "chat",
157
+ title: session.title || "Chat",
158
+ projectId: activeProject?.name ?? null,
159
+ metadata: { projectName: activeProject?.name, sessionId: session.id, providerId: session.providerId },
160
+ closable: true,
161
+ },
162
+ panelId,
163
+ );
164
+ }
165
+
166
+ const pinnedSessions = sessions.filter((s) => s.pinned);
167
+ const allRecentSessions = sessions.filter((s) => !s.pinned);
168
+ const recentSessions = showAll ? allRecentSessions : allRecentSessions.slice(0, MAX_RECENT_SESSIONS);
169
+ const hasMore = allRecentSessions.length > MAX_RECENT_SESSIONS;
170
+
171
+ function renderSessionRow(session: SessionInfo) {
172
+ return (
173
+ <button
174
+ key={session.id}
175
+ onClick={() => openSession(session)}
176
+ className="group flex items-center gap-2.5 w-full px-3 py-2.5 text-left hover:bg-surface-elevated active:bg-surface-elevated transition-colors border-b border-border/50 last:border-0"
177
+ >
178
+ <MessageSquare className="size-3.5 shrink-0 text-text-subtle" />
179
+ <span className="flex-1 min-w-0 text-xs font-medium truncate text-text-primary">
180
+ {session.title || "Untitled"}
181
+ </span>
182
+ {session.updatedAt && (
183
+ <span className="text-[10px] text-text-subtle shrink-0">
184
+ {formatRelativeDate(session.updatedAt)}
185
+ </span>
186
+ )}
187
+ <span
188
+ role="button"
189
+ tabIndex={0}
190
+ onClick={(e) => togglePin(e, session)}
191
+ className={`p-1 rounded transition-colors shrink-0 ${
192
+ session.pinned
193
+ ? "text-primary hover:text-primary/70"
194
+ : "text-text-subtle md:opacity-0 md:group-hover:opacity-100 hover:text-text-primary"
195
+ }`}
196
+ aria-label={session.pinned ? "Unpin session" : "Pin session"}
197
+ >
198
+ {session.pinned ? <PinOff className="size-3" /> : <Pin className="size-3" />}
199
+ </span>
200
+ </button>
201
+ );
202
+ }
203
+
89
204
  return (
90
- <div className="flex flex-col items-center justify-center h-full gap-4 text-text-secondary">
91
- <p className="text-sm">Open a tab to get started</p>
92
- <div className="flex flex-col md:flex-row flex-wrap justify-center gap-2">
93
- {QUICK_OPEN_TABS.map((opt) => {
94
- const Icon = opt.icon;
95
- return (
96
- <button
97
- key={opt.type}
98
- onClick={() => openTab(opt.type)}
99
- className="flex items-center gap-2 px-4 py-2 rounded-md border border-border bg-surface hover:bg-surface-elevated text-sm text-foreground transition-colors"
100
- >
101
- <Icon className="size-4" />
102
- {opt.label}
103
- </button>
104
- );
105
- })}
205
+ <div className="flex flex-col h-full overflow-y-auto text-text-secondary">
206
+ <div className="flex flex-col items-center justify-center gap-6 px-4 flex-1">
207
+ <p className="text-sm">Open a tab to get started</p>
208
+ <div className="grid grid-cols-3 gap-2 w-full max-w-sm">
209
+ {QUICK_OPEN_TABS.map((opt) => {
210
+ const Icon = opt.icon;
211
+ return (
212
+ <button
213
+ key={opt.type}
214
+ onClick={() => openTab(opt.type)}
215
+ className="flex flex-col items-center justify-center gap-1.5 px-2 py-3 rounded-md border border-border bg-surface hover:bg-surface-elevated active:bg-surface-elevated text-xs text-foreground transition-colors"
216
+ >
217
+ <Icon className="size-5" />
218
+ {opt.label}
219
+ </button>
220
+ );
221
+ })}
222
+ </div>
223
+
224
+ {activeProject && !loadingSessions && pinnedSessions.length > 0 && (
225
+ <div className="flex flex-col gap-2 w-full max-w-sm">
226
+ <p className="text-xs text-text-subtle text-center">Pinned</p>
227
+ <div className="w-full rounded-md border border-border bg-surface overflow-hidden">
228
+ {pinnedSessions.map(renderSessionRow)}
229
+ </div>
230
+ </div>
231
+ )}
232
+
233
+ {activeProject && !loadingSessions && recentSessions.length > 0 && (
234
+ <div className="flex flex-col gap-2 w-full max-w-sm">
235
+ <p className="text-xs text-text-subtle text-center">Recent chats</p>
236
+ <div className="w-full rounded-md border border-border bg-surface overflow-hidden">
237
+ {recentSessions.map(renderSessionRow)}
238
+ </div>
239
+ {hasMore && (
240
+ <button
241
+ onClick={() => setShowAll(!showAll)}
242
+ className="flex items-center justify-center gap-1 text-[11px] text-text-subtle hover:text-text-primary transition-colors py-1"
243
+ >
244
+ {showAll ? <ChevronUp className="size-3" /> : <ChevronDown className="size-3" />}
245
+ {showAll ? "Show less" : `Show more (${allRecentSessions.length - MAX_RECENT_SESSIONS})`}
246
+ </button>
247
+ )}
248
+ </div>
249
+ )}
106
250
  </div>
107
251
  </div>
108
252
  );
@@ -1,6 +1,8 @@
1
+ import { useEffect } from "react";
1
2
  import { Panel, Group, Separator } from "react-resizable-panels";
2
3
  import { GripVertical, GripHorizontal } from "lucide-react";
3
4
  import { usePanelStore } from "@/stores/panel-store";
5
+ import { createPanel } from "@/stores/panel-utils";
4
6
  import { EditorPanel } from "./editor-panel";
5
7
 
6
8
  interface PanelLayoutProps {
@@ -13,7 +15,21 @@ export function PanelLayout({ projectName }: PanelLayoutProps) {
13
15
  );
14
16
  const panelCount = grid.flat().length;
15
17
 
16
- if (panelCount <= 1 && grid[0]?.[0]) {
18
+ // Recover from empty grid (corrupt persisted state or edge-case bug)
19
+ useEffect(() => {
20
+ if (panelCount === 0) {
21
+ const p = createPanel();
22
+ usePanelStore.setState((s) => ({
23
+ panels: { ...s.panels, [p.id]: p },
24
+ grid: [[p.id]],
25
+ focusedPanelId: p.id,
26
+ }));
27
+ }
28
+ }, [panelCount]);
29
+
30
+ if (panelCount === 0) return null;
31
+
32
+ if (panelCount === 1 && grid[0]?.[0]) {
17
33
  return <EditorPanel panelId={grid[0][0]} projectName={projectName} />;
18
34
  }
19
35