@hienlh/ppm 0.9.0-beta.6 → 0.9.0-beta.8

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.

Potentially problematic release.


This version of @hienlh/ppm might be problematic. Click here for more details.

Files changed (182) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bun.lock +5 -0
  3. package/dist/web/assets/{_basePickBy-CZovQgWd.js → _basePickBy-5PGDJbfF.js} +1 -1
  4. package/dist/web/assets/{_baseUniq-ClnvscgW.js → _baseUniq-BT4Ow4Kk.js} +1 -1
  5. package/dist/web/assets/{api-settings--eVrUeZM.js → api-settings-D21InCnR.js} +1 -1
  6. package/dist/web/assets/{arc-C2Qaz-ch.js → arc-BAOivWpI.js} +1 -1
  7. package/dist/web/assets/architecture-PBZL5I3N-DEO2f3VD.js +1 -0
  8. package/dist/web/assets/{architectureDiagram-2XIMDMQ5-Jq91S_rs.js → architectureDiagram-2XIMDMQ5-Z-4eN4za.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-BCLqzhuZ.js} +1 -1
  11. package/dist/web/assets/browser-tab-BEe89aSD.js +1 -0
  12. package/dist/web/assets/{c4Diagram-IC4MRINW-BNP2L9r_.js → c4Diagram-IC4MRINW-0Vp0Jeas.js} +1 -1
  13. package/dist/web/assets/channel-By7bn0Yq.js +1 -0
  14. package/dist/web/assets/chat-tab-9lqvWozA.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-D4tOov49.js} +1 -1
  17. package/dist/web/assets/{chunk-55IACEB6-C4mUdyio.js → chunk-55IACEB6-DJ6BynZ4.js} +1 -1
  18. package/dist/web/assets/{chunk-7E7YKBS2-6xAQfBwa.js → chunk-7E7YKBS2-CiyUJxNI.js} +1 -1
  19. package/dist/web/assets/{chunk-7R4GIKGN-DXaGAn_K.js → chunk-7R4GIKGN-Dv-4cAYn.js} +2 -2
  20. package/dist/web/assets/{chunk-C72U2L5F-DOtEiN5f.js → chunk-C72U2L5F-D21mS_6G.js} +1 -1
  21. package/dist/web/assets/{chunk-EGIJ26TM-D0KJTa_T.js → chunk-EGIJ26TM-DzqmU2Z7.js} +1 -1
  22. package/dist/web/assets/{chunk-FMBD7UC4-C_1aG0eb.js → chunk-FMBD7UC4-DXncblvW.js} +1 -1
  23. package/dist/web/assets/{chunk-GEFDOKGD-DwVPiYfW.js → chunk-GEFDOKGD-D-pKjlVd.js} +1 -1
  24. package/dist/web/assets/chunk-GLR3WWYH-DKikpoJM.js +2 -0
  25. package/dist/web/assets/chunk-HHEYEP7N-C7vxA5i9.js +1 -0
  26. package/dist/web/assets/{chunk-JSJVCQXG-BSrqCL_3.js → chunk-JSJVCQXG-99JzIdPr.js} +1 -1
  27. package/dist/web/assets/{chunk-KX2RTZJC-BCxGmbzy.js → chunk-KX2RTZJC-CRq1OBZv.js} +1 -1
  28. package/dist/web/assets/{chunk-KYZI473N-BKO5gMeU.js → chunk-KYZI473N-Bb0MCaIO.js} +1 -1
  29. package/dist/web/assets/{chunk-L3YUKLVL-3wBgkSvL.js → chunk-L3YUKLVL-C7qGJrfV.js} +1 -1
  30. package/dist/web/assets/{chunk-MX3YWQON-BgjSEzus.js → chunk-MX3YWQON-BpS_PtKp.js} +1 -1
  31. package/dist/web/assets/{chunk-NQ4KR5QH-DLrZwBEm.js → chunk-NQ4KR5QH-z_blpjxi.js} +1 -1
  32. package/dist/web/assets/{chunk-O4XLMI2P-BurQy8tt.js → chunk-O4XLMI2P-nDhi_cVu.js} +1 -1
  33. package/dist/web/assets/{chunk-OZEHJAEY-YTn24bGg.js → chunk-OZEHJAEY-BXhYx3nO.js} +1 -1
  34. package/dist/web/assets/{chunk-PQ6SQG4A-BxtUGYhW.js → chunk-PQ6SQG4A-TF58UVMU.js} +1 -1
  35. package/dist/web/assets/{chunk-PU5JKC2W-B66ELkQm.js → chunk-PU5JKC2W-ek7k4QVB.js} +1 -1
  36. package/dist/web/assets/chunk-QZHKN3VN-CYaTbeZf.js +1 -0
  37. package/dist/web/assets/{chunk-R5LLSJPH-euR2RxLN.js → chunk-R5LLSJPH-CFwSJijQ.js} +1 -1
  38. package/dist/web/assets/{chunk-WL4C6EOR-_2CBOJdI.js → chunk-WL4C6EOR-ByUrSRin.js} +1 -1
  39. package/dist/web/assets/{chunk-XIRO2GV7-kqQ0g6wW.js → chunk-XIRO2GV7-Djlmrely.js} +1 -1
  40. package/dist/web/assets/{chunk-XPW4576I-CtcaMb09.js → chunk-XPW4576I-BPQQBakK.js} +1 -1
  41. package/dist/web/assets/{chunk-XZSTWKYB-BYxFzZwS.js → chunk-XZSTWKYB-DxAOx4hG.js} +1 -1
  42. package/dist/web/assets/{chunk-YBOYWFTD-Dx_fX35n.js → chunk-YBOYWFTD-rQG3QH5s.js} +1 -1
  43. package/dist/web/assets/classDiagram-VBA2DB6C-BA8Nj-_C.js +1 -0
  44. package/dist/web/assets/classDiagram-v2-RAHNMMFH-DjYu-6mn.js +1 -0
  45. package/dist/web/assets/clone-LRxlvnMj.js +1 -0
  46. package/dist/web/assets/code-editor-COAIZx-B.js +2 -0
  47. package/dist/web/assets/{cose-bilkent-S5V4N54A-CHHjH2dV.js → cose-bilkent-S5V4N54A-B_AWZsOP.js} +1 -1
  48. package/dist/web/assets/csv-preview-DLqYtXxt.js +10 -0
  49. package/dist/web/assets/{dagre-CNtSxiE_.js → dagre-DHq9bhnd.js} +1 -1
  50. package/dist/web/assets/{dagre-KLK3FWXG-ChenfPp1.js → dagre-KLK3FWXG-BdJr7Byp.js} +1 -1
  51. package/dist/web/assets/database-viewer-aRR9n_Ui.js +1 -0
  52. package/dist/web/assets/{diagram-E7M64L7V-CzKYZM0Y.js → diagram-E7M64L7V-_db4pBVA.js} +1 -1
  53. package/dist/web/assets/{diagram-IFDJBPK2-ChB_paPo.js → diagram-IFDJBPK2-xKoeuiJx.js} +1 -1
  54. package/dist/web/assets/{diagram-P4PSJMXO-D1eW1dkL.js → diagram-P4PSJMXO-C8tjJsev.js} +1 -1
  55. package/dist/web/assets/diff-viewer-C4KMvpHr.js +4 -0
  56. package/dist/web/assets/dist-CALwEtco.js +41 -0
  57. package/dist/web/assets/dist-CVTST7Gc.js +1 -0
  58. package/dist/web/assets/dist-DGDPTxs1.js +13 -0
  59. package/dist/web/assets/{erDiagram-INFDFZHY-mCvUFSn6.js → erDiagram-INFDFZHY-BSh2z9Df.js} +1 -1
  60. package/dist/web/assets/{flowDiagram-PKNHOUZH-14ohZ1M1.js → flowDiagram-PKNHOUZH-oYaovqyp.js} +1 -1
  61. package/dist/web/assets/{ganttDiagram-A5KZAMGK-DIX0pLbk.js → ganttDiagram-A5KZAMGK-DmL26q2P.js} +1 -1
  62. package/dist/web/assets/git-graph-CfJjl4E3.js +1 -0
  63. package/dist/web/assets/gitGraph-HDMCJU4V-Bwna3and.js +1 -0
  64. package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-yEWZbdf_.js → gitGraphDiagram-K3NZZRJ6-CMoukSrY.js} +1 -1
  65. package/dist/web/assets/{graphlib-DhOZxqsh.js → graphlib-BcsNnGcW.js} +1 -1
  66. package/dist/web/assets/index-Db8uky1a.css +2 -0
  67. package/dist/web/assets/index-DxZuwBDe.js +37 -0
  68. package/dist/web/assets/info-3K5VOQVL-_vRxVNUm.js +1 -0
  69. package/dist/web/assets/infoDiagram-LFFYTUFH-DWwumDkq.js +2 -0
  70. package/dist/web/assets/{isEmpty-C0YYdhYj.js → isEmpty-bnrF3Qbc.js} +1 -1
  71. package/dist/web/assets/{ishikawaDiagram-PHBUUO56-olazD6dZ.js → ishikawaDiagram-PHBUUO56-D05_LyL7.js} +1 -1
  72. package/dist/web/assets/{journeyDiagram-4ABVD52K-CttDH9bb.js → journeyDiagram-4ABVD52K-B_L20qMe.js} +1 -1
  73. package/dist/web/assets/{kanban-definition-K7BYSVSG-BBXbI37U.js → kanban-definition-K7BYSVSG-CZ535BbZ.js} +1 -1
  74. package/dist/web/assets/keybindings-store-_uWVCZMv.js +1 -0
  75. package/dist/web/assets/lib-BQ34Db2e.js +4 -0
  76. package/dist/web/assets/{line-DBLLF7lH.js → line-CVvo3dRu.js} +1 -1
  77. package/dist/web/assets/{linear-BLFWatDe.js → linear-DP4mkX3m.js} +1 -1
  78. package/dist/web/assets/markdown-renderer-DklUd_Gv.js +69 -0
  79. package/dist/web/assets/{mermaid-parser.core-BKiGOTjR.js → mermaid-parser.core-C7UwoIh6.js} +2 -2
  80. package/dist/web/assets/{mindmap-definition-YRQLILUH-DoT7m4Sz.js → mindmap-definition-YRQLILUH-x0MTutJp.js} +1 -1
  81. package/dist/web/assets/{ordinal-CCj7PWgZ.js → ordinal-_K3x1fkz.js} +1 -1
  82. package/dist/web/assets/packet-RMMSAZCW-DY5PNnZU.js +1 -0
  83. package/dist/web/assets/pie-UPGHQEXC-BHncZutv.js +1 -0
  84. package/dist/web/assets/{pieDiagram-SKSYHLDU-Bkh2E4zE.js → pieDiagram-SKSYHLDU-C1Gjrtzy.js} +1 -1
  85. package/dist/web/assets/postgres-viewer-DEAvAyaX.js +1 -0
  86. package/dist/web/assets/{quadrantDiagram-337W2JSQ-B7zgALOL.js → quadrantDiagram-337W2JSQ-C8bzJCjQ.js} +1 -1
  87. package/dist/web/assets/radar-KQ55EAFF-DH0AOkUy.js +1 -0
  88. package/dist/web/assets/react-dom-Bpkvzu3U.js +1 -0
  89. package/dist/web/assets/{requirementDiagram-Z7DCOOCP-D_5GXNRo.js → requirementDiagram-Z7DCOOCP-pQyah6WB.js} +1 -1
  90. package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-BA9EFAAe.js → sankeyDiagram-WA2Y5GQK-T6RgG-N8.js} +1 -1
  91. package/dist/web/assets/{sequenceDiagram-2WXFIKYE-fyWIrHiG.js → sequenceDiagram-2WXFIKYE-BQDJ4CVs.js} +1 -1
  92. package/dist/web/assets/settings-tab-BQedc-No.js +1 -0
  93. package/dist/web/assets/sqlite-viewer-BPA5idzT.js +1 -0
  94. package/dist/web/assets/{stateDiagram-RAJIS63D-DfRBcaBu.js → stateDiagram-RAJIS63D-66vhiIuk.js} +1 -1
  95. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-BGVqj_g9.js +1 -0
  96. package/dist/web/assets/{tab-store-DcIBZTD4.js → tab-store-DhK6EpBT.js} +1 -1
  97. package/dist/web/assets/{terminal-tab-CAZtLK6i.js → terminal-tab-CqRuiIFn.js} +2 -2
  98. package/dist/web/assets/{timeline-definition-YZTLITO2-DYfwJ1jM.js → timeline-definition-YZTLITO2-DwZqB3nn.js} +1 -1
  99. package/dist/web/assets/treemap-KZPCXAKY-B2Xkyv-K.js +1 -0
  100. package/dist/web/assets/use-monaco-theme-Dcz3aLAE.js +11 -0
  101. package/dist/web/assets/{vennDiagram-LZ73GAT5-DqbKNRD9.js → vennDiagram-LZ73GAT5-s9Z71fz-.js} +1 -1
  102. package/dist/web/assets/{xychartDiagram-JWTSCODW-DhUL86qT.js → xychartDiagram-JWTSCODW-DRa_TH4B.js} +1 -1
  103. package/dist/web/index.html +12 -10
  104. package/dist/web/sw.js +1 -1
  105. package/docs/codebase-summary.md +17 -5
  106. package/docs/design-guidelines.md +21 -0
  107. package/docs/project-changelog.md +28 -1
  108. package/docs/project-roadmap.md +41 -19
  109. package/docs/system-architecture.md +151 -0
  110. package/package.json +2 -1
  111. package/src/providers/claude-agent-sdk.ts +32 -10
  112. package/src/server/index.ts +6 -0
  113. package/src/server/routes/browser-preview.ts +2 -2
  114. package/src/server/routes/chat.ts +4 -2
  115. package/src/server/routes/mcp.ts +84 -0
  116. package/src/server/ws/chat.ts +18 -12
  117. package/src/services/account-selector.service.ts +8 -2
  118. package/src/services/claude-usage.service.ts +37 -11
  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 +225 -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/browser-tab-DAvH4mv0.js +0 -1
  134. package/dist/web/assets/channel-w7yboq56.js +0 -1
  135. package/dist/web/assets/chat-tab-WEBXxGgN.js +0 -7
  136. package/dist/web/assets/chunk-GLR3WWYH-D9pZakqr.js +0 -2
  137. package/dist/web/assets/chunk-HHEYEP7N-Dld5BpGB.js +0 -1
  138. package/dist/web/assets/chunk-QZHKN3VN-DwSXwtjH.js +0 -1
  139. package/dist/web/assets/classDiagram-VBA2DB6C-BpJ6Oog2.js +0 -1
  140. package/dist/web/assets/classDiagram-v2-RAHNMMFH-Bj8gIhkP.js +0 -1
  141. package/dist/web/assets/clone-BSi6cgDh.js +0 -1
  142. package/dist/web/assets/code-editor-B5sg_uJQ.js +0 -1
  143. package/dist/web/assets/database-viewer-CwtyWCkE.js +0 -1
  144. package/dist/web/assets/diff-viewer-CzE5M-Wd.js +0 -4
  145. package/dist/web/assets/dist-T0Vhi0Mh.js +0 -16
  146. package/dist/web/assets/git-graph-6yxCeeN9.js +0 -1
  147. package/dist/web/assets/gitGraph-HDMCJU4V-CEee2FCA.js +0 -1
  148. package/dist/web/assets/index-DE8b9u8F.css +0 -2
  149. package/dist/web/assets/index-wuWZBO9y.js +0 -37
  150. package/dist/web/assets/info-3K5VOQVL-ce_pi3En.js +0 -1
  151. package/dist/web/assets/infoDiagram-LFFYTUFH-BzqyoqXw.js +0 -2
  152. package/dist/web/assets/input-Brjz2Vv-.js +0 -41
  153. package/dist/web/assets/keybindings-store-mkBHnWN1.js +0 -1
  154. package/dist/web/assets/markdown-renderer-CxWxvrzT.js +0 -69
  155. package/dist/web/assets/packet-RMMSAZCW-CdYSLjRL.js +0 -1
  156. package/dist/web/assets/pie-UPGHQEXC-Bm5LiD-6.js +0 -1
  157. package/dist/web/assets/postgres-viewer-UP3yv9Yh.js +0 -1
  158. package/dist/web/assets/radar-KQ55EAFF-C4PnyG7_.js +0 -1
  159. package/dist/web/assets/settings-store-Bbhg_ptG.js +0 -2
  160. package/dist/web/assets/settings-tab-BoBXlVHe.js +0 -1
  161. package/dist/web/assets/sqlite-viewer-lzRVvM5j.js +0 -1
  162. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-CMN4M2Em.js +0 -1
  163. package/dist/web/assets/treemap-KZPCXAKY-2_y-mhkz.js +0 -1
  164. package/dist/web/assets/use-monaco-theme-vwto-Vlf.js +0 -11
  165. /package/dist/web/assets/{api-client-DpGMOZNf.js → api-client-BfBM3I7n.js} +0 -0
  166. /package/dist/web/assets/{array-BGFCBI0e.js → array-B9UHiPd-.js} +0 -0
  167. /package/dist/web/assets/{columns-2-ChOTgl3e.js → columns-2-DbesTfa7.js} +0 -0
  168. /package/dist/web/assets/{cytoscape.esm-Ccan6xou.js → cytoscape.esm-BW-DbntU.js} +0 -0
  169. /package/dist/web/assets/{defaultLocale-CRZydyG6.js → defaultLocale-5eAKkKJC.js} +0 -0
  170. /package/dist/web/assets/{dist-Cce3efmT.js → dist-CSJdAyA9.js} +0 -0
  171. /package/dist/web/assets/{init-B8gtcn7T.js → init-DlZdxViB.js} +0 -0
  172. /package/dist/web/assets/{isArrayLikeObject-B4pdpV8V.js → isArrayLikeObject-B_v2FtYn.js} +0 -0
  173. /package/dist/web/assets/{katex-Bbu770d9.js → katex-Bqvo_ZG0.js} +0 -0
  174. /package/dist/web/assets/{math-DwgHI-Cu.js → math-069Z4SuC.js} +0 -0
  175. /package/dist/web/assets/{path-DZF-JdEe.js → path-6uRLdFF7.js} +0 -0
  176. /package/dist/web/assets/{preload-helper-qlgyTAkD.js → preload-helper-Bf_JiD2A.js} +0 -0
  177. /package/dist/web/assets/{react-BGf7KNLk.js → react-SKk5z-bm.js} +0 -0
  178. /package/dist/web/assets/{rough.esm-VLpapkIG.js → rough.esm-JX0wREDd.js} +0 -0
  179. /package/dist/web/assets/{src-BoSBNdA_.js → src-BqX54PbV.js} +0 -0
  180. /package/dist/web/assets/{table-Yo02WRH-.js → table-CQVQM2SB.js} +0 -0
  181. /package/dist/web/assets/{tag-CaC1ng2E.js → tag-Q2dZiSPX.js} +0 -0
  182. /package/dist/web/assets/{utils-btZ8C8-R.js → utils-BNytJOb1.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,225 @@
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 { ScrollArea } from "@/components/ui/scroll-area";
13
+ import { useFileStore, type FileNode } from "@/stores/file-store";
14
+ import { useTabStore } from "@/stores/tab-store";
15
+ import { basename } from "@/lib/utils";
16
+
17
+ const ICON_MAP: Record<string, React.ComponentType<{ className?: string }>> = {
18
+ ts: FileCode, tsx: FileCode, js: FileCode, jsx: FileCode,
19
+ py: FileCode, rs: FileCode, go: FileCode, html: FileCode,
20
+ css: FileCode, scss: FileCode,
21
+ json: FileJson,
22
+ md: FileText, txt: FileText,
23
+ yaml: FileType, yml: FileType,
24
+ };
25
+
26
+ function getIcon(name: string, isDir: boolean) {
27
+ if (isDir) return Folder;
28
+ const ext = name.split(".").pop()?.toLowerCase() ?? "";
29
+ return ICON_MAP[ext] ?? File;
30
+ }
31
+
32
+ interface BreadcrumbSegment {
33
+ name: string;
34
+ fullPath: string;
35
+ node: FileNode | null;
36
+ siblings: FileNode[];
37
+ }
38
+
39
+ function walkTree(tree: FileNode[], segments: string[]): BreadcrumbSegment[] {
40
+ const result: BreadcrumbSegment[] = [];
41
+ let current: FileNode[] = tree;
42
+
43
+ for (let i = 0; i < segments.length; i++) {
44
+ const seg = segments[i]!;
45
+ const fullPath = segments.slice(0, i + 1).join("/");
46
+ const match = current.find((n) => n.name === seg);
47
+ result.push({
48
+ name: seg,
49
+ fullPath,
50
+ node: match ?? null,
51
+ siblings: current,
52
+ });
53
+ if (match?.children) {
54
+ current = match.children;
55
+ } else {
56
+ // Remaining segments have no tree data — add as plain
57
+ for (let j = i + 1; j < segments.length; j++) {
58
+ result.push({
59
+ name: segments[j]!,
60
+ fullPath: segments.slice(0, j + 1).join("/"),
61
+ node: null,
62
+ siblings: [],
63
+ });
64
+ }
65
+ break;
66
+ }
67
+ }
68
+ return result;
69
+ }
70
+
71
+ function sortNodes(nodes: FileNode[]): FileNode[] {
72
+ return [...nodes].sort((a, b) => {
73
+ if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
74
+ return a.name.localeCompare(b.name);
75
+ });
76
+ }
77
+
78
+ interface EditorBreadcrumbProps {
79
+ filePath: string;
80
+ projectName: string;
81
+ tabId: string;
82
+ className?: string;
83
+ }
84
+
85
+ export function EditorBreadcrumb({ filePath, projectName, tabId, className }: EditorBreadcrumbProps) {
86
+ const tree = useFileStore((s) => s.tree);
87
+ const { updateTab, openTab } = useTabStore();
88
+ const scrollRef = useRef<HTMLDivElement>(null);
89
+
90
+ const segments = useMemo(
91
+ () => walkTree(tree, filePath.split("/").filter(Boolean)),
92
+ [tree, filePath],
93
+ );
94
+
95
+ // Auto-scroll to rightmost segment
96
+ useEffect(() => {
97
+ if (scrollRef.current) {
98
+ scrollRef.current.scrollLeft = scrollRef.current.scrollWidth;
99
+ }
100
+ }, [segments]);
101
+
102
+ function handleFileClick(path: string, e: React.MouseEvent) {
103
+ const name = basename(path);
104
+ if (e.metaKey || e.ctrlKey) {
105
+ openTab({ type: "editor", title: name, metadata: { filePath: path, projectName }, projectId: projectName, closable: true });
106
+ } else {
107
+ updateTab(tabId, { title: name, metadata: { filePath: path, projectName } });
108
+ }
109
+ }
110
+
111
+ return (
112
+ <div ref={scrollRef} className={className}>
113
+ {segments.map((seg, i) => (
114
+ <div key={seg.fullPath} className="flex items-center shrink-0">
115
+ {i > 0 && <ChevronRight className="size-3 text-muted-foreground shrink-0 mx-0.5" />}
116
+ {seg.siblings.length > 0 ? (
117
+ <SegmentDropdown
118
+ segment={seg}
119
+ isLast={i === segments.length - 1}
120
+ projectName={projectName}
121
+ onFileClick={handleFileClick}
122
+ />
123
+ ) : (
124
+ <span className="text-xs text-muted-foreground px-1 py-0.5">{seg.name}</span>
125
+ )}
126
+ </div>
127
+ ))}
128
+ </div>
129
+ );
130
+ }
131
+
132
+ interface SegmentDropdownProps {
133
+ segment: BreadcrumbSegment;
134
+ isLast: boolean;
135
+ projectName: string;
136
+ onFileClick: (path: string, e: React.MouseEvent) => void;
137
+ }
138
+
139
+ function SegmentDropdown({ segment, isLast, projectName, onFileClick }: SegmentDropdownProps) {
140
+ const sorted = useMemo(() => sortNodes(segment.siblings), [segment.siblings]);
141
+
142
+ return (
143
+ <DropdownMenu>
144
+ <DropdownMenuTrigger asChild>
145
+ <button
146
+ type="button"
147
+ className={`text-xs px-1 py-0.5 rounded hover:bg-muted transition-colors truncate max-w-[120px] ${
148
+ isLast ? "text-foreground font-medium" : "text-muted-foreground"
149
+ }`}
150
+ >
151
+ {segment.name}
152
+ </button>
153
+ </DropdownMenuTrigger>
154
+ <DropdownMenuContent align="start" className="max-h-[300px] overflow-hidden p-0">
155
+ <ScrollArea className="max-h-[300px]">
156
+ <div className="p-1">
157
+ {sorted.map((node) => (
158
+ <NodeMenuItem
159
+ key={node.path}
160
+ node={node}
161
+ projectName={projectName}
162
+ activePath={segment.fullPath}
163
+ onFileClick={onFileClick}
164
+ />
165
+ ))}
166
+ </div>
167
+ </ScrollArea>
168
+ </DropdownMenuContent>
169
+ </DropdownMenu>
170
+ );
171
+ }
172
+
173
+ interface NodeMenuItemProps {
174
+ node: FileNode;
175
+ projectName: string;
176
+ activePath: string;
177
+ onFileClick: (path: string, e: React.MouseEvent) => void;
178
+ }
179
+
180
+ function NodeMenuItem({ node, projectName, activePath, onFileClick }: NodeMenuItemProps) {
181
+ const Icon = getIcon(node.name, node.type === "directory");
182
+ const isActive = node.path === activePath;
183
+
184
+ if (node.type === "directory" && node.children && node.children.length > 0) {
185
+ return (
186
+ <DropdownMenuSub>
187
+ <DropdownMenuSubTrigger className={`text-xs gap-1.5 ${isActive ? "bg-muted" : ""}`}>
188
+ <Icon className="size-3.5 shrink-0 text-muted-foreground" />
189
+ <span className="truncate">{node.name}</span>
190
+ </DropdownMenuSubTrigger>
191
+ <DropdownMenuSubContent className="max-h-[300px] overflow-hidden p-0">
192
+ <ScrollArea className="max-h-[300px]">
193
+ <div className="p-1">
194
+ {sortNodes(node.children).map((child) => (
195
+ <NodeMenuItem
196
+ key={child.path}
197
+ node={child}
198
+ projectName={projectName}
199
+ activePath={activePath}
200
+ onFileClick={onFileClick}
201
+ />
202
+ ))}
203
+ </div>
204
+ </ScrollArea>
205
+ </DropdownMenuSubContent>
206
+ </DropdownMenuSub>
207
+ );
208
+ }
209
+
210
+ return (
211
+ <DropdownMenuItem
212
+ className={`text-xs gap-1.5 cursor-pointer ${isActive ? "bg-muted" : ""}`}
213
+ onSelect={(e) => {
214
+ // onSelect doesn't give MouseEvent, use click handler for Ctrl detection
215
+ }}
216
+ onClick={(e) => {
217
+ if (node.type === "directory") return;
218
+ onFileClick(node.path, e);
219
+ }}
220
+ >
221
+ <Icon className="size-3.5 shrink-0 text-muted-foreground" />
222
+ <span className="truncate">{node.name}</span>
223
+ </DropdownMenuItem>
224
+ );
225
+ }
@@ -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
+ }