@hienlh/ppm 0.9.0-beta.7 → 0.9.0-beta.9

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 (181) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/bun.lock +5 -0
  3. package/dist/web/assets/{_basePickBy-CZovQgWd.js → _basePickBy-3Xe18azI.js} +1 -1
  4. package/dist/web/assets/{_baseUniq-ClnvscgW.js → _baseUniq-Yy35llnn.js} +1 -1
  5. package/dist/web/assets/{api-settings--eVrUeZM.js → api-settings-CEMxVMCV.js} +1 -1
  6. package/dist/web/assets/{arc-C2Qaz-ch.js → arc-B9n1Gvb5.js} +1 -1
  7. package/dist/web/assets/architecture-PBZL5I3N-CFzkFKEL.js +1 -0
  8. package/dist/web/assets/{architectureDiagram-2XIMDMQ5-Jq91S_rs.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-CKGufRTy.js → blockDiagram-WCTKOSBZ-h3cDF2vI.js} +1 -1
  11. package/dist/web/assets/{browser-tab-DAvH4mv0.js → browser-tab-D1Zua62g.js} +1 -1
  12. package/dist/web/assets/{c4Diagram-IC4MRINW-BNP2L9r_.js → c4Diagram-IC4MRINW--pF1r5lr.js} +1 -1
  13. package/dist/web/assets/channel-C2fMafck.js +1 -0
  14. package/dist/web/assets/chat-tab-BnD27Vp9.js +7 -0
  15. package/dist/web/assets/chevron-right-CHnjJt4E.js +1 -0
  16. package/dist/web/assets/{chunk-4BX2VUAB-BptTlTyl.js → chunk-4BX2VUAB-C3aZvW7B.js} +1 -1
  17. package/dist/web/assets/{chunk-55IACEB6-C4mUdyio.js → chunk-55IACEB6-D5cABeB9.js} +1 -1
  18. package/dist/web/assets/{chunk-7E7YKBS2-6xAQfBwa.js → chunk-7E7YKBS2-CkFGv6Zs.js} +1 -1
  19. package/dist/web/assets/{chunk-7R4GIKGN-DXaGAn_K.js → chunk-7R4GIKGN-Dvbyu4Zw.js} +2 -2
  20. package/dist/web/assets/{chunk-C72U2L5F-DOtEiN5f.js → chunk-C72U2L5F-CtqKiH4q.js} +1 -1
  21. package/dist/web/assets/{chunk-EGIJ26TM-D0KJTa_T.js → chunk-EGIJ26TM-Cpr87sBR.js} +1 -1
  22. package/dist/web/assets/{chunk-FMBD7UC4-C_1aG0eb.js → chunk-FMBD7UC4-D23YVTOU.js} +1 -1
  23. package/dist/web/assets/{chunk-GEFDOKGD-DwVPiYfW.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-BSrqCL_3.js → chunk-JSJVCQXG-BBmymCjA.js} +1 -1
  27. package/dist/web/assets/{chunk-KX2RTZJC-BCxGmbzy.js → chunk-KX2RTZJC-DP36BDiU.js} +1 -1
  28. package/dist/web/assets/{chunk-KYZI473N-BKO5gMeU.js → chunk-KYZI473N-Djw13C-3.js} +1 -1
  29. package/dist/web/assets/{chunk-L3YUKLVL-3wBgkSvL.js → chunk-L3YUKLVL-HG_eMj_C.js} +1 -1
  30. package/dist/web/assets/{chunk-MX3YWQON-BgjSEzus.js → chunk-MX3YWQON-C2UEioMs.js} +1 -1
  31. package/dist/web/assets/{chunk-NQ4KR5QH-DLrZwBEm.js → chunk-NQ4KR5QH-DXUTQ-BL.js} +1 -1
  32. package/dist/web/assets/{chunk-O4XLMI2P-BurQy8tt.js → chunk-O4XLMI2P-BsUWb9d0.js} +1 -1
  33. package/dist/web/assets/{chunk-OZEHJAEY-YTn24bGg.js → chunk-OZEHJAEY-rG0P22U9.js} +1 -1
  34. package/dist/web/assets/{chunk-PQ6SQG4A-BxtUGYhW.js → chunk-PQ6SQG4A-DX0xW7kO.js} +1 -1
  35. package/dist/web/assets/{chunk-PU5JKC2W-B66ELkQm.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-euR2RxLN.js → chunk-R5LLSJPH-CMY0PkRK.js} +1 -1
  38. package/dist/web/assets/{chunk-WL4C6EOR-_2CBOJdI.js → chunk-WL4C6EOR-CXuQvlyu.js} +1 -1
  39. package/dist/web/assets/{chunk-XIRO2GV7-kqQ0g6wW.js → chunk-XIRO2GV7-DRJEb7Zb.js} +1 -1
  40. package/dist/web/assets/{chunk-XPW4576I-CtcaMb09.js → chunk-XPW4576I-BPEX8KhL.js} +1 -1
  41. package/dist/web/assets/{chunk-XZSTWKYB-BYxFzZwS.js → chunk-XZSTWKYB-Cb0iqycX.js} +1 -1
  42. package/dist/web/assets/{chunk-YBOYWFTD-Dx_fX35n.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-DGRg8stf.js +2 -0
  47. package/dist/web/assets/{cose-bilkent-S5V4N54A-CHHjH2dV.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-CNtSxiE_.js → dagre-BFcnKyBF.js} +1 -1
  50. package/dist/web/assets/{dagre-KLK3FWXG-ChenfPp1.js → dagre-KLK3FWXG-C3O-MTLf.js} +1 -1
  51. package/dist/web/assets/database-viewer-DxCXZQcE.js +1 -0
  52. package/dist/web/assets/{diagram-E7M64L7V-CzKYZM0Y.js → diagram-E7M64L7V-DxPjK7_c.js} +1 -1
  53. package/dist/web/assets/{diagram-IFDJBPK2-ChB_paPo.js → diagram-IFDJBPK2-sqTog_XV.js} +1 -1
  54. package/dist/web/assets/{diagram-P4PSJMXO-D1eW1dkL.js → diagram-P4PSJMXO-hzmp0GHK.js} +1 -1
  55. package/dist/web/assets/diff-viewer-C1sDJG35.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-mCvUFSn6.js → erDiagram-INFDFZHY-DLeYhAAT.js} +1 -1
  59. package/dist/web/assets/{flowDiagram-PKNHOUZH-14ohZ1M1.js → flowDiagram-PKNHOUZH-CRxlE9Sr.js} +1 -1
  60. package/dist/web/assets/{ganttDiagram-A5KZAMGK-DIX0pLbk.js → ganttDiagram-A5KZAMGK-BdjmoMLS.js} +1 -1
  61. package/dist/web/assets/git-graph-BDn-EiGE.js +1 -0
  62. package/dist/web/assets/gitGraph-HDMCJU4V-CNlas3Rz.js +1 -0
  63. package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-yEWZbdf_.js → gitGraphDiagram-K3NZZRJ6-BeHSX7kk.js} +1 -1
  64. package/dist/web/assets/{graphlib-DhOZxqsh.js → graphlib-Duh_bWLa.js} +1 -1
  65. package/dist/web/assets/index-Bun94AK3.js +37 -0
  66. package/dist/web/assets/index-Db8uky1a.css +2 -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-C0YYdhYj.js → isEmpty-B9L-Ge-H.js} +1 -1
  70. package/dist/web/assets/{ishikawaDiagram-PHBUUO56-olazD6dZ.js → ishikawaDiagram-PHBUUO56-Cu0Rt1Ok.js} +1 -1
  71. package/dist/web/assets/{journeyDiagram-4ABVD52K-CttDH9bb.js → journeyDiagram-4ABVD52K-CgDI-UG4.js} +1 -1
  72. package/dist/web/assets/{kanban-definition-K7BYSVSG-BBXbI37U.js → kanban-definition-K7BYSVSG-h4g10UHL.js} +1 -1
  73. package/dist/web/assets/keybindings-store-COmK4Dte.js +1 -0
  74. package/dist/web/assets/lib-BeaDXEkP.js +4 -0
  75. package/dist/web/assets/{line-DBLLF7lH.js → line-B75-Rx70.js} +1 -1
  76. package/dist/web/assets/{linear-BLFWatDe.js → linear-Bcjv9FQt.js} +1 -1
  77. package/dist/web/assets/markdown-renderer-VIZB1GXE.js +69 -0
  78. package/dist/web/assets/{mermaid-parser.core-BKiGOTjR.js → mermaid-parser.core-8u2leTXI.js} +2 -2
  79. package/dist/web/assets/{mindmap-definition-YRQLILUH-DoT7m4Sz.js → mindmap-definition-YRQLILUH-BaOBwb-W.js} +1 -1
  80. package/dist/web/assets/{ordinal-CCj7PWgZ.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-Bkh2E4zE.js → pieDiagram-SKSYHLDU-At5Kz0KK.js} +1 -1
  84. package/dist/web/assets/postgres-viewer-CvQZ8gkh.js +1 -0
  85. package/dist/web/assets/{quadrantDiagram-337W2JSQ-B7zgALOL.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-D_5GXNRo.js → requirementDiagram-Z7DCOOCP-B9F_Cx_p.js} +1 -1
  89. package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-BA9EFAAe.js → sankeyDiagram-WA2Y5GQK-RolPi8bU.js} +1 -1
  90. package/dist/web/assets/{sequenceDiagram-2WXFIKYE-fyWIrHiG.js → sequenceDiagram-2WXFIKYE-DM-tMAhx.js} +1 -1
  91. package/dist/web/assets/settings-tab-RCnvZ29H.js +1 -0
  92. package/dist/web/assets/sqlite-viewer-CEEm2W4C.js +1 -0
  93. package/dist/web/assets/{stateDiagram-RAJIS63D-DfRBcaBu.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-DcIBZTD4.js → tab-store-Bjh6bXFP.js} +1 -1
  96. package/dist/web/assets/{terminal-tab-CAZtLK6i.js → terminal-tab-XhKfb4ei.js} +2 -2
  97. package/dist/web/assets/{timeline-definition-YZTLITO2-DYfwJ1jM.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-0p0-84jJ.js +11 -0
  100. package/dist/web/assets/{vennDiagram-LZ73GAT5-DqbKNRD9.js → vennDiagram-LZ73GAT5-ywK7LMaH.js} +1 -1
  101. package/dist/web/assets/{xychartDiagram-JWTSCODW-DhUL86qT.js → xychartDiagram-JWTSCODW-DylHYNtJ.js} +1 -1
  102. package/dist/web/index.html +11 -10
  103. package/dist/web/sw.js +1 -1
  104. package/docs/codebase-summary.md +17 -5
  105. package/docs/design-guidelines.md +21 -0
  106. package/docs/project-changelog.md +28 -1
  107. package/docs/project-roadmap.md +2 -2
  108. package/docs/system-architecture.md +151 -0
  109. package/package.json +2 -1
  110. package/src/providers/claude-agent-sdk.ts +32 -10
  111. package/src/server/index.ts +6 -0
  112. package/src/server/routes/chat.ts +4 -2
  113. package/src/server/routes/mcp.ts +84 -0
  114. package/src/server/ws/chat.ts +18 -12
  115. package/src/services/account-selector.service.ts +8 -2
  116. package/src/services/account.service.ts +13 -8
  117. package/src/services/claude-usage.service.ts +37 -18
  118. package/src/services/cloud.service.ts +10 -6
  119. package/src/services/db.service.ts +53 -6
  120. package/src/services/mcp-config.service.ts +102 -0
  121. package/src/services/supervisor.ts +12 -2
  122. package/src/types/mcp.ts +47 -0
  123. package/src/web/components/editor/code-editor.tsx +36 -26
  124. package/src/web/components/editor/csv-preview.tsx +228 -0
  125. package/src/web/components/editor/editor-breadcrumb.tsx +216 -0
  126. package/src/web/components/editor/editor-toolbar.tsx +74 -0
  127. package/src/web/components/settings/mcp-server-dialog.tsx +208 -0
  128. package/src/web/components/settings/mcp-settings-section.tsx +143 -0
  129. package/src/web/components/settings/settings-tab.tsx +5 -2
  130. package/src/web/lib/api-mcp.ts +38 -0
  131. package/src/web/lib/csv-parser.ts +134 -0
  132. package/dist/web/assets/architecture-PBZL5I3N-ChOahOB7.js +0 -1
  133. package/dist/web/assets/channel-w7yboq56.js +0 -1
  134. package/dist/web/assets/chat-tab-WEBXxGgN.js +0 -7
  135. package/dist/web/assets/chunk-GLR3WWYH-D9pZakqr.js +0 -2
  136. package/dist/web/assets/chunk-HHEYEP7N-Dld5BpGB.js +0 -1
  137. package/dist/web/assets/chunk-QZHKN3VN-DwSXwtjH.js +0 -1
  138. package/dist/web/assets/classDiagram-VBA2DB6C-BpJ6Oog2.js +0 -1
  139. package/dist/web/assets/classDiagram-v2-RAHNMMFH-Bj8gIhkP.js +0 -1
  140. package/dist/web/assets/clone-BSi6cgDh.js +0 -1
  141. package/dist/web/assets/code-editor-B5sg_uJQ.js +0 -1
  142. package/dist/web/assets/database-viewer-CwtyWCkE.js +0 -1
  143. package/dist/web/assets/diff-viewer-CzE5M-Wd.js +0 -4
  144. package/dist/web/assets/dist-T0Vhi0Mh.js +0 -16
  145. package/dist/web/assets/git-graph-6yxCeeN9.js +0 -1
  146. package/dist/web/assets/gitGraph-HDMCJU4V-CEee2FCA.js +0 -1
  147. package/dist/web/assets/index-DE8b9u8F.css +0 -2
  148. package/dist/web/assets/index-wuWZBO9y.js +0 -37
  149. package/dist/web/assets/info-3K5VOQVL-ce_pi3En.js +0 -1
  150. package/dist/web/assets/infoDiagram-LFFYTUFH-BzqyoqXw.js +0 -2
  151. package/dist/web/assets/input-Brjz2Vv-.js +0 -41
  152. package/dist/web/assets/keybindings-store-mkBHnWN1.js +0 -1
  153. package/dist/web/assets/markdown-renderer-CxWxvrzT.js +0 -69
  154. package/dist/web/assets/packet-RMMSAZCW-CdYSLjRL.js +0 -1
  155. package/dist/web/assets/pie-UPGHQEXC-Bm5LiD-6.js +0 -1
  156. package/dist/web/assets/postgres-viewer-UP3yv9Yh.js +0 -1
  157. package/dist/web/assets/radar-KQ55EAFF-C4PnyG7_.js +0 -1
  158. package/dist/web/assets/settings-store-Bbhg_ptG.js +0 -2
  159. package/dist/web/assets/settings-tab-BoBXlVHe.js +0 -1
  160. package/dist/web/assets/sqlite-viewer-lzRVvM5j.js +0 -1
  161. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-CMN4M2Em.js +0 -1
  162. package/dist/web/assets/treemap-KZPCXAKY-2_y-mhkz.js +0 -1
  163. package/dist/web/assets/use-monaco-theme-vwto-Vlf.js +0 -11
  164. /package/dist/web/assets/{api-client-DpGMOZNf.js → api-client-BKIT_Qeg.js} +0 -0
  165. /package/dist/web/assets/{array-BGFCBI0e.js → array-DqLCdDFv.js} +0 -0
  166. /package/dist/web/assets/{columns-2-ChOTgl3e.js → columns-2-DbesTfa7.js} +0 -0
  167. /package/dist/web/assets/{cytoscape.esm-Ccan6xou.js → cytoscape.esm-CWPXKqbJ.js} +0 -0
  168. /package/dist/web/assets/{defaultLocale-CRZydyG6.js → defaultLocale-CrJzLgRD.js} +0 -0
  169. /package/dist/web/assets/{dist-Cce3efmT.js → dist-Cep75xXf.js} +0 -0
  170. /package/dist/web/assets/{init-B8gtcn7T.js → init-C0r9Gk5G.js} +0 -0
  171. /package/dist/web/assets/{isArrayLikeObject-B4pdpV8V.js → isArrayLikeObject-CGBoxvCD.js} +0 -0
  172. /package/dist/web/assets/{katex-Bbu770d9.js → katex-DzXRfQ_m.js} +0 -0
  173. /package/dist/web/assets/{math-DwgHI-Cu.js → math-y9zN1W-N.js} +0 -0
  174. /package/dist/web/assets/{path-DZF-JdEe.js → path-DIKpVbHL.js} +0 -0
  175. /package/dist/web/assets/{preload-helper-qlgyTAkD.js → preload-helper-Bf_JiD2A.js} +0 -0
  176. /package/dist/web/assets/{react-BGf7KNLk.js → react-SKk5z-bm.js} +0 -0
  177. /package/dist/web/assets/{rough.esm-VLpapkIG.js → rough.esm-nHaDi0Kw.js} +0 -0
  178. /package/dist/web/assets/{src-BoSBNdA_.js → src-Dw4QhedI.js} +0 -0
  179. /package/dist/web/assets/{table-Yo02WRH-.js → table-CQVQM2SB.js} +0 -0
  180. /package/dist/web/assets/{tag-CaC1ng2E.js → tag-Q2dZiSPX.js} +0 -0
  181. /package/dist/web/assets/{utils-btZ8C8-R.js → utils-DMiycH3O.js} +0 -0
@@ -0,0 +1,228 @@
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
+ }
@@ -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
+ }