@hienlh/ppm 0.8.85 → 0.8.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 (224) 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 +5 -187
  4. package/bun.lock +0 -5
  5. package/dist/web/assets/{_basePickBy-5PGDJbfF.js → _basePickBy-5eBmZ_lt.js} +1 -1
  6. package/dist/web/assets/{_baseUniq-BT4Ow4Kk.js → _baseUniq-DimLlN0y.js} +1 -1
  7. package/dist/web/assets/api-settings-CFw-lh5k.js +1 -0
  8. package/dist/web/assets/{arc-BAOivWpI.js → arc-D4SasZrA.js} +1 -1
  9. package/dist/web/assets/architecture-PBZL5I3N-CJupe6q_.js +1 -0
  10. package/dist/web/assets/{architectureDiagram-2XIMDMQ5-DWBCPMLF.js → architectureDiagram-2XIMDMQ5-nv0WbM7d.js} +1 -1
  11. package/dist/web/assets/{blockDiagram-WCTKOSBZ-TEF8Ally.js → blockDiagram-WCTKOSBZ-C1XvYrb8.js} +1 -1
  12. package/dist/web/assets/browser-tab-CmsL5eny.js +1 -0
  13. package/dist/web/assets/{c4Diagram-IC4MRINW-dV22iAsY.js → c4Diagram-IC4MRINW-CygDrbWJ.js} +1 -1
  14. package/dist/web/assets/channel-DmKoFTd_.js +1 -0
  15. package/dist/web/assets/chat-tab-CFWsf13Z.js +7 -0
  16. package/dist/web/assets/{chunk-4BX2VUAB-D4tOov49.js → chunk-4BX2VUAB-C2FDgsgT.js} +1 -1
  17. package/dist/web/assets/{chunk-55IACEB6-DJ6BynZ4.js → chunk-55IACEB6-jF4w6cat.js} +1 -1
  18. package/dist/web/assets/{chunk-7E7YKBS2-CiyUJxNI.js → chunk-7E7YKBS2-BVCECZFi.js} +1 -1
  19. package/dist/web/assets/{chunk-7R4GIKGN-BbIFzsIv.js → chunk-7R4GIKGN-DXTbeu5d.js} +2 -2
  20. package/dist/web/assets/{chunk-C72U2L5F-D21mS_6G.js → chunk-C72U2L5F-BaZqOsTs.js} +1 -1
  21. package/dist/web/assets/{chunk-EGIJ26TM-DzqmU2Z7.js → chunk-EGIJ26TM-Bky2tcH7.js} +1 -1
  22. package/dist/web/assets/{chunk-FMBD7UC4-DXncblvW.js → chunk-FMBD7UC4-Cp4BK9A8.js} +1 -1
  23. package/dist/web/assets/{chunk-GEFDOKGD-BbQkJu8C.js → chunk-GEFDOKGD-BosFEH7G.js} +1 -1
  24. package/dist/web/assets/chunk-GLR3WWYH-BnP-hOp6.js +2 -0
  25. package/dist/web/assets/chunk-HHEYEP7N-DKDPTPEZ.js +1 -0
  26. package/dist/web/assets/{chunk-JSJVCQXG-23tyvw8k.js → chunk-JSJVCQXG-H5Gbjsbr.js} +1 -1
  27. package/dist/web/assets/{chunk-KX2RTZJC-sQ0o-39C.js → chunk-KX2RTZJC-CWerSUwS.js} +1 -1
  28. package/dist/web/assets/{chunk-KYZI473N-BcUZNnwd.js → chunk-KYZI473N-FvwP7jUy.js} +1 -1
  29. package/dist/web/assets/{chunk-L3YUKLVL-C7qGJrfV.js → chunk-L3YUKLVL-D1PI_ORP.js} +1 -1
  30. package/dist/web/assets/{chunk-MX3YWQON-BpS_PtKp.js → chunk-MX3YWQON-C7Vzk_AI.js} +1 -1
  31. package/dist/web/assets/{chunk-NQ4KR5QH-wMgTlP7f.js → chunk-NQ4KR5QH-BceYBGYX.js} +1 -1
  32. package/dist/web/assets/{chunk-O4XLMI2P-JC6EGoUz.js → chunk-O4XLMI2P-WPtzgxql.js} +1 -1
  33. package/dist/web/assets/{chunk-OZEHJAEY-BXhYx3nO.js → chunk-OZEHJAEY-DlHXDeLY.js} +1 -1
  34. package/dist/web/assets/{chunk-PQ6SQG4A-D6BTbCQw.js → chunk-PQ6SQG4A-Ci_Prygb.js} +1 -1
  35. package/dist/web/assets/{chunk-PU5JKC2W-Dw8ClWch.js → chunk-PU5JKC2W-CO0zMN-z.js} +1 -1
  36. package/dist/web/assets/chunk-QZHKN3VN-C_wpI9wz.js +1 -0
  37. package/dist/web/assets/{chunk-R5LLSJPH-CFwSJijQ.js → chunk-R5LLSJPH-IAEEzfpM.js} +1 -1
  38. package/dist/web/assets/{chunk-WL4C6EOR-DfofndiH.js → chunk-WL4C6EOR-BLXalOgc.js} +1 -1
  39. package/dist/web/assets/{chunk-XIRO2GV7-Djlmrely.js → chunk-XIRO2GV7-Dx1Ri_p2.js} +1 -1
  40. package/dist/web/assets/{chunk-XPW4576I-BPQQBakK.js → chunk-XPW4576I-m9pPGKn7.js} +1 -1
  41. package/dist/web/assets/{chunk-XZSTWKYB-DxAOx4hG.js → chunk-XZSTWKYB-B_08ExbI.js} +1 -1
  42. package/dist/web/assets/{chunk-YBOYWFTD-CeU4Q-xC.js → chunk-YBOYWFTD-DqSOVcYe.js} +1 -1
  43. package/dist/web/assets/classDiagram-VBA2DB6C-B1T5uY-F.js +1 -0
  44. package/dist/web/assets/classDiagram-v2-RAHNMMFH-xs5vI3xC.js +1 -0
  45. package/dist/web/assets/clone-CijCFRT5.js +1 -0
  46. package/dist/web/assets/code-editor-H_dAh_fJ.js +1 -0
  47. package/dist/web/assets/{cose-bilkent-S5V4N54A-B_AWZsOP.js → cose-bilkent-S5V4N54A-DlL82QHu.js} +1 -1
  48. package/dist/web/assets/{dagre-Dbb5k38K.js → dagre-BmVoh2At.js} +1 -1
  49. package/dist/web/assets/{dagre-KLK3FWXG-BH7aWGRP.js → dagre-KLK3FWXG-sDrRW9MQ.js} +1 -1
  50. package/dist/web/assets/database-viewer-DBzsgEJ8.js +1 -0
  51. package/dist/web/assets/{diagram-E7M64L7V-B1Qz70Do.js → diagram-E7M64L7V-ChnAhgni.js} +1 -1
  52. package/dist/web/assets/{diagram-IFDJBPK2-k55eVqVU.js → diagram-IFDJBPK2-DW1J1uJd.js} +1 -1
  53. package/dist/web/assets/{diagram-P4PSJMXO-BkfNRc9U.js → diagram-P4PSJMXO-CQ32hyG_.js} +1 -1
  54. package/dist/web/assets/diff-viewer-DzS-OnAR.js +4 -0
  55. package/dist/web/assets/dist-0Va_2L7G.js +16 -0
  56. package/dist/web/assets/dist-D9irYETY.js +41 -0
  57. package/dist/web/assets/{erDiagram-INFDFZHY-CKzVujYI.js → erDiagram-INFDFZHY-6CHo6nOw.js} +1 -1
  58. package/dist/web/assets/{flowDiagram-PKNHOUZH-DIqcTrDV.js → flowDiagram-PKNHOUZH-DroDiNT0.js} +1 -1
  59. package/dist/web/assets/{ganttDiagram-A5KZAMGK-D4v7ZbVE.js → ganttDiagram-A5KZAMGK-DP0QBh8w.js} +1 -1
  60. package/dist/web/assets/git-graph-D3C7F8o3.js +1 -0
  61. package/dist/web/assets/gitGraph-HDMCJU4V-B0KvGQG8.js +1 -0
  62. package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-BTXo57mF.js → gitGraphDiagram-K3NZZRJ6-DvU3JGZn.js} +1 -1
  63. package/dist/web/assets/{graphlib-BcsNnGcW.js → graphlib-CQBb2thr.js} +1 -1
  64. package/dist/web/assets/index-CIkjfera.js +31 -0
  65. package/dist/web/assets/index-WKLuYsBY.css +2 -0
  66. package/dist/web/assets/info-3K5VOQVL-1uJ6_hCm.js +1 -0
  67. package/dist/web/assets/infoDiagram-LFFYTUFH-DLA5Q-3y.js +2 -0
  68. package/dist/web/assets/input-CGp1nFIg.js +1 -0
  69. package/dist/web/assets/{isEmpty-bnrF3Qbc.js → isEmpty-B4kqZBtn.js} +1 -1
  70. package/dist/web/assets/{ishikawaDiagram-PHBUUO56-BOyvKMmB.js → ishikawaDiagram-PHBUUO56-46yibrV5.js} +1 -1
  71. package/dist/web/assets/{journeyDiagram-4ABVD52K-ufoasAy6.js → journeyDiagram-4ABVD52K-BcmRwjK-.js} +1 -1
  72. package/dist/web/assets/{kanban-definition-K7BYSVSG-Bi0UTUeN.js → kanban-definition-K7BYSVSG-B619K53y.js} +1 -1
  73. package/dist/web/assets/keybindings-store-BdaoLwSo.js +1 -0
  74. package/dist/web/assets/{line-B78g-52T.js → line-1gcO63_w.js} +1 -1
  75. package/dist/web/assets/{linear-DP4mkX3m.js → linear-DfRqDoVd.js} +1 -1
  76. package/dist/web/assets/markdown-renderer-DH49Zag7.js +69 -0
  77. package/dist/web/assets/{mermaid-parser.core-DMIWdgEW.js → mermaid-parser.core-XtjZQOeM.js} +2 -2
  78. package/dist/web/assets/{mindmap-definition-YRQLILUH-BsfWvIoO.js → mindmap-definition-YRQLILUH-CifOFo_q.js} +1 -1
  79. package/dist/web/assets/{ordinal-_K3x1fkz.js → ordinal-BJYw-iDX.js} +1 -1
  80. package/dist/web/assets/packet-RMMSAZCW-34C4o9yj.js +1 -0
  81. package/dist/web/assets/pie-UPGHQEXC-D9ekKlh9.js +1 -0
  82. package/dist/web/assets/{pieDiagram-SKSYHLDU-WP0XXw51.js → pieDiagram-SKSYHLDU-BuHUh_fO.js} +1 -1
  83. package/dist/web/assets/postgres-viewer-B9FYk8sD.js +1 -0
  84. package/dist/web/assets/{quadrantDiagram-337W2JSQ-FHMogtsh.js → quadrantDiagram-337W2JSQ-Bau_hj6Z.js} +1 -1
  85. package/dist/web/assets/radar-KQ55EAFF-DEuXOXSD.js +1 -0
  86. package/dist/web/assets/{requirementDiagram-Z7DCOOCP-BatTxyWb.js → requirementDiagram-Z7DCOOCP-Cq2b-uwp.js} +1 -1
  87. package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-ClJuW3Hv.js → sankeyDiagram-WA2Y5GQK-DrdGQxWQ.js} +1 -1
  88. package/dist/web/assets/{sequenceDiagram-2WXFIKYE-ByxQqGgs.js → sequenceDiagram-2WXFIKYE-qPxiTUcS.js} +1 -1
  89. package/dist/web/assets/settings-store-DWXGVHsE.js +2 -0
  90. package/dist/web/assets/settings-tab-D-q8pd-5.js +1 -0
  91. package/dist/web/assets/sqlite-viewer-CDqcTePw.js +1 -0
  92. package/dist/web/assets/{stateDiagram-RAJIS63D-f8opcZNY.js → stateDiagram-RAJIS63D-Dulj2oa8.js} +1 -1
  93. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-CAkzLlhk.js +1 -0
  94. package/dist/web/assets/tab-store-BPeiymiH.js +1 -0
  95. package/dist/web/assets/{terminal-tab-CCDLZA5Y.js → terminal-tab-wKgpSPAT.js} +2 -2
  96. package/dist/web/assets/{timeline-definition-YZTLITO2-58BlOSf9.js → timeline-definition-YZTLITO2-BWyDnCYq.js} +1 -1
  97. package/dist/web/assets/treemap-KZPCXAKY-nc7a1Ia1.js +1 -0
  98. package/dist/web/assets/use-monaco-theme-CCBTQ0S3.js +11 -0
  99. package/dist/web/assets/{vennDiagram-LZ73GAT5-BOSy9ma9.js → vennDiagram-LZ73GAT5-B9Iv2bNV.js} +1 -1
  100. package/dist/web/assets/{xychartDiagram-JWTSCODW-z5MVJauZ.js → xychartDiagram-JWTSCODW-ChXcMzBQ.js} +1 -1
  101. package/dist/web/index.html +11 -12
  102. package/dist/web/sw.js +1 -1
  103. package/docs/code-standards.md +7 -232
  104. package/docs/codebase-summary.md +3 -9
  105. package/docs/design-guidelines.md +0 -21
  106. package/docs/project-changelog.md +1 -115
  107. package/docs/project-roadmap.md +19 -41
  108. package/docs/system-architecture.md +15 -212
  109. package/package.json +2 -3
  110. package/src/cli/commands/autostart.ts +1 -1
  111. package/src/cli/commands/restart.ts +1 -9
  112. package/src/cli/commands/status.ts +0 -19
  113. package/src/index.ts +3 -2
  114. package/src/providers/claude-agent-sdk.ts +31 -94
  115. package/src/providers/mock-provider.ts +1 -6
  116. package/src/server/index.ts +166 -38
  117. package/src/server/routes/chat.ts +3 -52
  118. package/src/server/routes/project-scoped.ts +0 -2
  119. package/src/server/routes/proxy.ts +53 -46
  120. package/src/server/routes/tunnel.ts +32 -0
  121. package/src/server/ws/chat.ts +146 -207
  122. package/src/services/account-selector.service.ts +8 -16
  123. package/src/services/account.service.ts +13 -19
  124. package/src/services/claude-usage.service.ts +11 -48
  125. package/src/services/cloud.service.ts +6 -10
  126. package/src/services/db.service.ts +6 -111
  127. package/src/services/port-tunnel.service.ts +97 -0
  128. package/src/services/proxy.service.ts +19 -4
  129. package/src/services/supervisor.ts +25 -285
  130. package/src/types/api.ts +1 -9
  131. package/src/types/chat.ts +1 -3
  132. package/src/web/app.tsx +35 -41
  133. package/src/web/components/browser/browser-tab.tsx +97 -106
  134. package/src/web/components/chat/chat-history-bar.tsx +6 -72
  135. package/src/web/components/chat/chat-tab.tsx +16 -32
  136. package/src/web/components/chat/message-input.tsx +13 -107
  137. package/src/web/components/chat/message-list.tsx +15 -27
  138. package/src/web/components/chat/session-picker.tsx +31 -78
  139. package/src/web/components/chat/usage-badge.tsx +1 -11
  140. package/src/web/components/editor/code-editor.tsx +26 -36
  141. package/src/web/components/layout/command-palette.tsx +1 -3
  142. package/src/web/components/layout/editor-panel.tsx +18 -162
  143. package/src/web/components/layout/panel-layout.tsx +1 -17
  144. package/src/web/components/settings/proxy-settings-section.tsx +42 -40
  145. package/src/web/hooks/use-chat.ts +201 -211
  146. package/src/web/hooks/use-global-keybindings.ts +2 -25
  147. package/src/web/hooks/use-server-reload.ts +0 -9
  148. package/src/web/hooks/use-url-sync.ts +21 -173
  149. package/src/web/stores/keybindings-store.ts +0 -1
  150. package/src/web/stores/panel-store.ts +19 -73
  151. package/src/web/stores/panel-utils.ts +3 -145
  152. package/dist/web/assets/api-settings-Bx1GaNmQ.js +0 -1
  153. package/dist/web/assets/architecture-PBZL5I3N-DEO2f3VD.js +0 -1
  154. package/dist/web/assets/arrow-up--LjUXLEt.js +0 -1
  155. package/dist/web/assets/browser-tab-DaHGm_0i.js +0 -1
  156. package/dist/web/assets/channel-wrd-NHWf.js +0 -1
  157. package/dist/web/assets/chat-tab-BDYE0KHF.js +0 -8
  158. package/dist/web/assets/chevron-right-DeV0ehiG.js +0 -1
  159. package/dist/web/assets/chunk-GLR3WWYH-CzYx4w-r.js +0 -2
  160. package/dist/web/assets/chunk-HHEYEP7N-HRhYy3kG.js +0 -1
  161. package/dist/web/assets/chunk-QZHKN3VN-CYaTbeZf.js +0 -1
  162. package/dist/web/assets/classDiagram-VBA2DB6C-lse8oZoJ.js +0 -1
  163. package/dist/web/assets/classDiagram-v2-RAHNMMFH-CxkwuInd.js +0 -1
  164. package/dist/web/assets/clone-LRxlvnMj.js +0 -1
  165. package/dist/web/assets/code-editor-DTA3c9Y8.js +0 -2
  166. package/dist/web/assets/csv-preview-DLqYtXxt.js +0 -10
  167. package/dist/web/assets/database-viewer-DXk79Nel.js +0 -1
  168. package/dist/web/assets/diff-viewer-HhIcsOQE.js +0 -4
  169. package/dist/web/assets/dist-DylI9XxN.js +0 -13
  170. package/dist/web/assets/dist-lF8CoYII.js +0 -41
  171. package/dist/web/assets/git-graph-CQtWu8yE.js +0 -1
  172. package/dist/web/assets/gitGraph-HDMCJU4V-Bwna3and.js +0 -1
  173. package/dist/web/assets/index-CgQXpBb_.css +0 -2
  174. package/dist/web/assets/index-DEeeRoka.js +0 -37
  175. package/dist/web/assets/info-3K5VOQVL-_vRxVNUm.js +0 -1
  176. package/dist/web/assets/infoDiagram-LFFYTUFH-B1CX0pbC.js +0 -2
  177. package/dist/web/assets/input-BglMT33g.js +0 -1
  178. package/dist/web/assets/keybindings-store-1CJ7VX57.js +0 -1
  179. package/dist/web/assets/lib-BQ34Db2e.js +0 -4
  180. package/dist/web/assets/markdown-renderer-Brj8_LQM.js +0 -69
  181. package/dist/web/assets/packet-RMMSAZCW-DY5PNnZU.js +0 -1
  182. package/dist/web/assets/pie-UPGHQEXC-BHncZutv.js +0 -1
  183. package/dist/web/assets/postgres-viewer-CwkTGmqy.js +0 -1
  184. package/dist/web/assets/radar-KQ55EAFF-DH0AOkUy.js +0 -1
  185. package/dist/web/assets/react-dom-Bpkvzu3U.js +0 -1
  186. package/dist/web/assets/settings-tab-BDE1MsIh.js +0 -1
  187. package/dist/web/assets/sqlite-viewer-CFYTwgA8.js +0 -1
  188. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-DrxVDY9q.js +0 -1
  189. package/dist/web/assets/tab-store-BJw7OCmy.js +0 -1
  190. package/dist/web/assets/treemap-KZPCXAKY-B2Xkyv-K.js +0 -1
  191. package/dist/web/assets/use-monaco-theme-CNzekTN3.js +0 -11
  192. package/docs/streaming-input-guide.md +0 -267
  193. package/snapshot-state.md +0 -1526
  194. package/src/server/routes/browser-preview.ts +0 -159
  195. package/src/server/routes/workspace.ts +0 -35
  196. package/src/services/cloud-ws.service.ts +0 -227
  197. package/src/web/components/chat/account-rotation-settings.tsx +0 -163
  198. package/src/web/components/chat/chat-welcome.tsx +0 -148
  199. package/src/web/components/editor/csv-preview.tsx +0 -228
  200. package/src/web/components/editor/editor-breadcrumb.tsx +0 -216
  201. package/src/web/components/editor/editor-toolbar.tsx +0 -74
  202. package/src/web/components/shared/connection-lost-overlay.tsx +0 -89
  203. package/src/web/hooks/use-voice-input.ts +0 -111
  204. package/src/web/lib/csv-parser.ts +0 -134
  205. package/src/web/stores/connection-store.ts +0 -39
  206. package/test-tokens.mjs +0 -212
  207. /package/dist/web/assets/{api-client-BfBM3I7n.js → api-client-DOElml5u.js} +0 -0
  208. /package/dist/web/assets/{array-B9UHiPd-.js → array-CYkMkqnU.js} +0 -0
  209. /package/dist/web/assets/{columns-2-DpsNbZOc.js → columns-2-ChOTgl3e.js} +0 -0
  210. /package/dist/web/assets/{cytoscape.esm-BW-DbntU.js → cytoscape.esm-HeHO0VhB.js} +0 -0
  211. /package/dist/web/assets/{defaultLocale-5eAKkKJC.js → defaultLocale-Beh6XjaL.js} +0 -0
  212. /package/dist/web/assets/{dist-CSJdAyA9.js → dist-BUYzeuKe.js} +0 -0
  213. /package/dist/web/assets/{init-DlZdxViB.js → init-Rr1s_RiX.js} +0 -0
  214. /package/dist/web/assets/{isArrayLikeObject-B_v2FtYn.js → isArrayLikeObject-BB-mzMLb.js} +0 -0
  215. /package/dist/web/assets/{katex-Bqvo_ZG0.js → katex-CKoArbIw.js} +0 -0
  216. /package/dist/web/assets/{math-069Z4SuC.js → math-B7b0HgJF.js} +0 -0
  217. /package/dist/web/assets/{path-6uRLdFF7.js → path-BAQ3hXlG.js} +0 -0
  218. /package/dist/web/assets/{preload-helper-uTix4PVD.js → preload-helper-DeiOTZKJ.js} +0 -0
  219. /package/dist/web/assets/{react-ER-4DN55.js → react-Dev-wu-s.js} +0 -0
  220. /package/dist/web/assets/{rough.esm-JX0wREDd.js → rough.esm-Dwml_la6.js} +0 -0
  221. /package/dist/web/assets/{src-BqX54PbV.js → src-B_cC68fH.js} +0 -0
  222. /package/dist/web/assets/{table-C7X5UAEI.js → table-COiJDPRA.js} +0 -0
  223. /package/dist/web/assets/{tag-CCtdV063.js → tag-LMq02LfE.js} +0 -0
  224. /package/dist/web/assets/{utils-BNytJOb1.js → utils-btZ8C8-R.js} +0 -0
@@ -1,148 +0,0 @@
1
- import { useState, useEffect, useCallback } from "react";
2
- import { Bot, ChevronDown, ChevronUp, MessageSquare, Pin, PinOff } from "lucide-react";
3
- import { api, projectUrl } from "@/lib/api-client";
4
- import type { SessionInfo } from "../../../types/chat";
5
-
6
- const MAX_RECENT_SESSIONS = 5;
7
- const FETCH_SESSIONS_LIMIT = 20;
8
-
9
- function formatRelativeDate(iso: string): string {
10
- try {
11
- const date = new Date(iso);
12
- const now = new Date();
13
- const diffMs = now.getTime() - date.getTime();
14
- const diffMin = Math.floor(diffMs / 60_000);
15
- if (diffMin < 1) return "just now";
16
- if (diffMin < 60) return `${diffMin}m ago`;
17
- const diffHr = Math.floor(diffMin / 60);
18
- if (diffHr < 24) return `${diffHr}h ago`;
19
- const diffDay = Math.floor(diffHr / 24);
20
- if (diffDay < 7) return `${diffDay}d ago`;
21
- return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
22
- } catch {
23
- return "";
24
- }
25
- }
26
-
27
- interface ChatWelcomeProps {
28
- projectName: string;
29
- onSelectSession: (session: SessionInfo) => void;
30
- }
31
-
32
- export function ChatWelcome({ projectName, onSelectSession }: ChatWelcomeProps) {
33
- const [sessions, setSessions] = useState<SessionInfo[]>([]);
34
- const [loading, setLoading] = useState(false);
35
- const [showAll, setShowAll] = useState(false);
36
-
37
- const loadSessions = useCallback(async () => {
38
- if (!projectName) return;
39
- setLoading(true);
40
- try {
41
- const data = await api.get<SessionInfo[]>(`${projectUrl(projectName)}/chat/sessions`);
42
- setSessions(data.slice(0, FETCH_SESSIONS_LIMIT));
43
- } catch {
44
- // silently ignore
45
- } finally {
46
- setLoading(false);
47
- }
48
- }, [projectName]);
49
-
50
- useEffect(() => { loadSessions(); }, [loadSessions]);
51
-
52
- const togglePin = useCallback(async (e: React.MouseEvent, session: SessionInfo) => {
53
- e.stopPropagation();
54
- if (!projectName) return;
55
- const url = `${projectUrl(projectName)}/chat/sessions/${session.id}/pin`;
56
- try {
57
- if (session.pinned) {
58
- await api.del(url);
59
- } else {
60
- await api.put(url);
61
- }
62
- setSessions((prev) => {
63
- const updated = prev.map((s) => s.id === session.id ? { ...s, pinned: !s.pinned } : s);
64
- return updated.sort((a, b) => {
65
- if (a.pinned && !b.pinned) return -1;
66
- if (!a.pinned && b.pinned) return 1;
67
- return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
68
- });
69
- });
70
- } catch {
71
- // silently ignore
72
- }
73
- }, [projectName]);
74
-
75
- const pinnedSessions = sessions.filter((s) => s.pinned);
76
- const allRecentSessions = sessions.filter((s) => !s.pinned);
77
- const recentSessions = showAll ? allRecentSessions : allRecentSessions.slice(0, MAX_RECENT_SESSIONS);
78
- const hasMore = allRecentSessions.length > MAX_RECENT_SESSIONS;
79
-
80
- function renderSessionRow(session: SessionInfo) {
81
- return (
82
- <button
83
- key={session.id}
84
- onClick={() => onSelectSession(session)}
85
- 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"
86
- >
87
- <MessageSquare className="size-3.5 shrink-0 text-text-subtle" />
88
- <span className="flex-1 min-w-0 text-xs font-medium truncate text-text-primary">
89
- {session.title || "Untitled"}
90
- </span>
91
- {session.updatedAt && (
92
- <span className="text-[10px] text-text-subtle shrink-0">
93
- {formatRelativeDate(session.updatedAt)}
94
- </span>
95
- )}
96
- <span
97
- role="button"
98
- tabIndex={0}
99
- onClick={(e) => togglePin(e, session)}
100
- className={`p-1 rounded transition-colors shrink-0 ${
101
- session.pinned
102
- ? "text-primary hover:text-primary/70"
103
- : "text-text-subtle md:opacity-0 md:group-hover:opacity-100 hover:text-text-primary"
104
- }`}
105
- aria-label={session.pinned ? "Unpin session" : "Pin session"}
106
- >
107
- {session.pinned ? <PinOff className="size-3" /> : <Pin className="size-3" />}
108
- </span>
109
- </button>
110
- );
111
- }
112
-
113
- return (
114
- <div className="flex flex-col items-center justify-center h-full gap-6 text-text-secondary overflow-y-auto">
115
- <div className="flex flex-col items-center gap-3">
116
- <Bot className="size-10 text-text-subtle" />
117
- <p className="text-sm">Send a message to start a new conversation</p>
118
- </div>
119
-
120
- {!loading && pinnedSessions.length > 0 && (
121
- <div className="flex flex-col gap-2 w-full max-w-sm px-4">
122
- <p className="text-xs text-text-subtle text-center">Pinned</p>
123
- <div className="w-full rounded-md border border-border bg-surface overflow-hidden">
124
- {pinnedSessions.map(renderSessionRow)}
125
- </div>
126
- </div>
127
- )}
128
-
129
- {!loading && recentSessions.length > 0 && (
130
- <div className="flex flex-col gap-2 w-full max-w-sm px-4">
131
- <p className="text-xs text-text-subtle text-center">Recent chats</p>
132
- <div className="w-full rounded-md border border-border bg-surface overflow-hidden">
133
- {recentSessions.map(renderSessionRow)}
134
- </div>
135
- {hasMore && (
136
- <button
137
- onClick={() => setShowAll(!showAll)}
138
- className="flex items-center justify-center gap-1 text-[11px] text-text-subtle hover:text-text-primary transition-colors py-1"
139
- >
140
- {showAll ? <ChevronUp className="size-3" /> : <ChevronDown className="size-3" />}
141
- {showAll ? "Show less" : `Show more (${allRecentSessions.length - MAX_RECENT_SESSIONS})`}
142
- </button>
143
- )}
144
- </div>
145
- )}
146
- </div>
147
- );
148
- }
@@ -1,228 +0,0 @@
1
- import { useState, useMemo, useRef, useCallback, useEffect } from "react";
2
- import {
3
- useReactTable,
4
- getCoreRowModel,
5
- getSortedRowModel,
6
- flexRender,
7
- type ColumnDef,
8
- type SortingState,
9
- } from "@tanstack/react-table";
10
- import { useVirtualizer } from "@tanstack/react-virtual";
11
- import { parseCsv, serializeCsv } from "@/lib/csv-parser";
12
- import { ArrowUp, ArrowDown } from "lucide-react";
13
-
14
- interface CsvPreviewProps {
15
- content: string;
16
- onContentChange: (csv: string) => void;
17
- wordWrap?: boolean;
18
- }
19
-
20
- export function CsvPreview({ content, onContentChange, wordWrap }: CsvPreviewProps) {
21
- const parsed = useMemo(() => parseCsv(content), [content]);
22
- const [rows, setRows] = useState<string[][]>(() => parsed.rows);
23
- const [sorting, setSorting] = useState<SortingState>([]);
24
- const scrollRef = useRef<HTMLDivElement>(null);
25
- const internalEditRef = useRef(false);
26
-
27
- // Sync when content changes externally (e.g. file reload) — skip if we triggered it
28
- useEffect(() => {
29
- if (internalEditRef.current) {
30
- internalEditRef.current = false;
31
- return;
32
- }
33
- setRows(parsed.rows);
34
- }, [parsed.rows]);
35
-
36
- const headers = parsed.headers;
37
-
38
- const updateCell = useCallback(
39
- (rowIndex: number, colIndex: number, value: string) => {
40
- setRows((prev) => {
41
- const next = prev.map((r, i) => (i === rowIndex ? [...r] : r));
42
- next[rowIndex]![colIndex] = value;
43
- internalEditRef.current = true;
44
- onContentChange(serializeCsv(headers, next));
45
- return next;
46
- });
47
- },
48
- [headers, onContentChange],
49
- );
50
-
51
- const columns = useMemo<ColumnDef<string[], string>[]>(
52
- () =>
53
- headers.map((h, i) => ({
54
- id: `col-${i}`,
55
- header: h || `Column ${i + 1}`,
56
- accessorFn: (row: string[]) => row[i] ?? "",
57
- cell: ({ row, getValue }) => (
58
- <CsvCell
59
- value={getValue()}
60
- onSave={(v) => updateCell(row.index, i, v)}
61
- wordWrap={wordWrap}
62
- />
63
- ),
64
- size: 150,
65
- minSize: 80,
66
- })),
67
- [headers, updateCell, wordWrap],
68
- );
69
-
70
- const table = useReactTable({
71
- data: rows,
72
- columns,
73
- state: { sorting },
74
- onSortingChange: setSorting,
75
- getCoreRowModel: getCoreRowModel(),
76
- getSortedRowModel: getSortedRowModel(),
77
- enableColumnResizing: true,
78
- columnResizeMode: "onChange",
79
- });
80
-
81
- const { rows: tableRows } = table.getRowModel();
82
-
83
- const virtualizer = useVirtualizer({
84
- count: tableRows.length,
85
- getScrollElement: () => scrollRef.current,
86
- estimateSize: () => 32,
87
- overscan: 20,
88
- });
89
-
90
- if (headers.length === 0) {
91
- return (
92
- <div className="flex items-center justify-center h-full text-muted-foreground text-sm">
93
- Empty CSV file
94
- </div>
95
- );
96
- }
97
-
98
- return (
99
- <div ref={scrollRef} className="flex-1 overflow-auto">
100
- <table className="w-full text-xs font-mono border-collapse">
101
- <thead className="sticky top-0 bg-background z-10 border-b border-border block">
102
- {table.getHeaderGroups().map((hg) => (
103
- <tr key={hg.id} className="flex w-full">
104
- {hg.headers.map((header) => (
105
- <th
106
- key={header.id}
107
- className="relative text-left px-2 py-1.5 font-medium text-muted-foreground select-none cursor-pointer hover:bg-muted/50 border-r border-border last:border-r-0"
108
- style={{ width: header.getSize(), minWidth: header.getSize() }}
109
- onClick={header.column.getToggleSortingHandler()}
110
- >
111
- <div className="flex items-center gap-1">
112
- <span className="truncate">
113
- {flexRender(header.column.columnDef.header, header.getContext())}
114
- </span>
115
- {header.column.getIsSorted() === "asc" && <ArrowUp className="size-3 shrink-0" />}
116
- {header.column.getIsSorted() === "desc" && <ArrowDown className="size-3 shrink-0" />}
117
- </div>
118
- {/* Resize handle */}
119
- <div
120
- onMouseDown={header.getResizeHandler()}
121
- onTouchStart={header.getResizeHandler()}
122
- onClick={(e) => e.stopPropagation()}
123
- className="absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-primary/50 active:bg-primary"
124
- />
125
- </th>
126
- ))}
127
- </tr>
128
- ))}
129
- </thead>
130
- <tbody style={{ height: virtualizer.getTotalSize(), position: "relative", display: "block" }}>
131
- {virtualizer.getVirtualItems().map((vRow) => {
132
- const row = tableRows[vRow.index]!;
133
- return (
134
- <tr
135
- key={row.id}
136
- data-index={vRow.index}
137
- ref={(node) => virtualizer.measureElement(node)}
138
- style={{
139
- position: "absolute",
140
- top: 0,
141
- left: 0,
142
- width: "100%",
143
- transform: `translateY(${vRow.start}px)`,
144
- display: "flex",
145
- }}
146
- >
147
- {row.getVisibleCells().map((cell) => (
148
- <td
149
- key={cell.id}
150
- className={`px-2 py-1 border-b border-border/50 border-r border-r-border/30 last:border-r-0 ${wordWrap ? "whitespace-pre-wrap break-words" : "truncate"}`}
151
- style={{ width: cell.column.getSize(), minWidth: cell.column.getSize() }}
152
- >
153
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
154
- </td>
155
- ))}
156
- </tr>
157
- );
158
- })}
159
- </tbody>
160
- </table>
161
- </div>
162
- );
163
- }
164
-
165
- function CsvCell({ value, onSave, wordWrap }: { value: string; onSave: (v: string) => void; wordWrap?: boolean }) {
166
- const [editing, setEditing] = useState(false);
167
- const [draft, setDraft] = useState(value);
168
- const textareaRef = useRef<HTMLTextAreaElement>(null);
169
-
170
- // Auto-resize textarea to fit content
171
- const autoResize = useCallback((el: HTMLTextAreaElement | null) => {
172
- if (!el) return;
173
- el.style.height = "auto";
174
- el.style.height = `${el.scrollHeight}px`;
175
- }, []);
176
-
177
- useEffect(() => {
178
- if (editing && textareaRef.current) {
179
- textareaRef.current.focus();
180
- autoResize(textareaRef.current);
181
- }
182
- }, [editing, autoResize]);
183
-
184
- if (!editing) {
185
- return (
186
- <span
187
- className={`block cursor-text ${wordWrap ? "whitespace-pre-wrap break-words" : "truncate"}`}
188
- onClick={() => {
189
- setDraft(value);
190
- setEditing(true);
191
- }}
192
- >
193
- {value || "\u00A0"}
194
- </span>
195
- );
196
- }
197
-
198
- const isMultiline = draft.includes("\n");
199
-
200
- return (
201
- <textarea
202
- ref={textareaRef}
203
- className="w-full bg-transparent outline-none border border-primary/50 rounded text-xs font-mono resize-none p-0.5"
204
- style={{ minHeight: isMultiline ? 48 : 20 }}
205
- rows={1}
206
- value={draft}
207
- onChange={(e) => {
208
- setDraft(e.target.value);
209
- autoResize(e.target);
210
- }}
211
- onBlur={() => {
212
- setEditing(false);
213
- if (draft !== value) onSave(draft);
214
- }}
215
- onKeyDown={(e) => {
216
- if (e.key === "Enter" && !e.shiftKey) {
217
- // Enter = save, Shift+Enter = newline
218
- e.preventDefault();
219
- setEditing(false);
220
- if (draft !== value) onSave(draft);
221
- } else if (e.key === "Escape") {
222
- setEditing(false);
223
- setDraft(value);
224
- }
225
- }}
226
- />
227
- );
228
- }
@@ -1,216 +0,0 @@
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
- }
@@ -1,74 +0,0 @@
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
- }