@hienlh/ppm 0.8.86 → 0.8.87

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 (219) hide show
  1. package/CHANGELOG.md +193 -5
  2. package/bun.lock +5 -0
  3. package/dist/web/assets/{_basePickBy-5eBmZ_lt.js → _basePickBy-5PGDJbfF.js} +1 -1
  4. package/dist/web/assets/{_baseUniq-DimLlN0y.js → _baseUniq-BT4Ow4Kk.js} +1 -1
  5. package/dist/web/assets/api-settings-Bx1GaNmQ.js +1 -0
  6. package/dist/web/assets/{arc-D4SasZrA.js → arc-BAOivWpI.js} +1 -1
  7. package/dist/web/assets/architecture-PBZL5I3N-DEO2f3VD.js +1 -0
  8. package/dist/web/assets/{architectureDiagram-2XIMDMQ5-nv0WbM7d.js → architectureDiagram-2XIMDMQ5-DWBCPMLF.js} +1 -1
  9. package/dist/web/assets/arrow-up--LjUXLEt.js +1 -0
  10. package/dist/web/assets/{blockDiagram-WCTKOSBZ-C1XvYrb8.js → blockDiagram-WCTKOSBZ-TEF8Ally.js} +1 -1
  11. package/dist/web/assets/browser-tab-DaHGm_0i.js +1 -0
  12. package/dist/web/assets/{c4Diagram-IC4MRINW-CygDrbWJ.js → c4Diagram-IC4MRINW-dV22iAsY.js} +1 -1
  13. package/dist/web/assets/channel-wrd-NHWf.js +1 -0
  14. package/dist/web/assets/chat-tab-BDYE0KHF.js +8 -0
  15. package/dist/web/assets/chevron-right-DeV0ehiG.js +1 -0
  16. package/dist/web/assets/{chunk-4BX2VUAB-C2FDgsgT.js → chunk-4BX2VUAB-D4tOov49.js} +1 -1
  17. package/dist/web/assets/{chunk-55IACEB6-jF4w6cat.js → chunk-55IACEB6-DJ6BynZ4.js} +1 -1
  18. package/dist/web/assets/{chunk-7E7YKBS2-BVCECZFi.js → chunk-7E7YKBS2-CiyUJxNI.js} +1 -1
  19. package/dist/web/assets/{chunk-7R4GIKGN-DXTbeu5d.js → chunk-7R4GIKGN-BbIFzsIv.js} +2 -2
  20. package/dist/web/assets/{chunk-C72U2L5F-BaZqOsTs.js → chunk-C72U2L5F-D21mS_6G.js} +1 -1
  21. package/dist/web/assets/{chunk-EGIJ26TM-Bky2tcH7.js → chunk-EGIJ26TM-DzqmU2Z7.js} +1 -1
  22. package/dist/web/assets/{chunk-FMBD7UC4-Cp4BK9A8.js → chunk-FMBD7UC4-DXncblvW.js} +1 -1
  23. package/dist/web/assets/{chunk-GEFDOKGD-BosFEH7G.js → chunk-GEFDOKGD-BbQkJu8C.js} +1 -1
  24. package/dist/web/assets/chunk-GLR3WWYH-CzYx4w-r.js +2 -0
  25. package/dist/web/assets/chunk-HHEYEP7N-HRhYy3kG.js +1 -0
  26. package/dist/web/assets/{chunk-JSJVCQXG-H5Gbjsbr.js → chunk-JSJVCQXG-23tyvw8k.js} +1 -1
  27. package/dist/web/assets/{chunk-KX2RTZJC-CWerSUwS.js → chunk-KX2RTZJC-sQ0o-39C.js} +1 -1
  28. package/dist/web/assets/{chunk-KYZI473N-FvwP7jUy.js → chunk-KYZI473N-BcUZNnwd.js} +1 -1
  29. package/dist/web/assets/{chunk-L3YUKLVL-D1PI_ORP.js → chunk-L3YUKLVL-C7qGJrfV.js} +1 -1
  30. package/dist/web/assets/{chunk-MX3YWQON-C7Vzk_AI.js → chunk-MX3YWQON-BpS_PtKp.js} +1 -1
  31. package/dist/web/assets/{chunk-NQ4KR5QH-BceYBGYX.js → chunk-NQ4KR5QH-wMgTlP7f.js} +1 -1
  32. package/dist/web/assets/{chunk-O4XLMI2P-WPtzgxql.js → chunk-O4XLMI2P-JC6EGoUz.js} +1 -1
  33. package/dist/web/assets/{chunk-OZEHJAEY-DlHXDeLY.js → chunk-OZEHJAEY-BXhYx3nO.js} +1 -1
  34. package/dist/web/assets/{chunk-PQ6SQG4A-Ci_Prygb.js → chunk-PQ6SQG4A-D6BTbCQw.js} +1 -1
  35. package/dist/web/assets/{chunk-PU5JKC2W-CO0zMN-z.js → chunk-PU5JKC2W-Dw8ClWch.js} +1 -1
  36. package/dist/web/assets/chunk-QZHKN3VN-CYaTbeZf.js +1 -0
  37. package/dist/web/assets/{chunk-R5LLSJPH-IAEEzfpM.js → chunk-R5LLSJPH-CFwSJijQ.js} +1 -1
  38. package/dist/web/assets/{chunk-WL4C6EOR-BLXalOgc.js → chunk-WL4C6EOR-DfofndiH.js} +1 -1
  39. package/dist/web/assets/{chunk-XIRO2GV7-Dx1Ri_p2.js → chunk-XIRO2GV7-Djlmrely.js} +1 -1
  40. package/dist/web/assets/{chunk-XPW4576I-m9pPGKn7.js → chunk-XPW4576I-BPQQBakK.js} +1 -1
  41. package/dist/web/assets/{chunk-XZSTWKYB-B_08ExbI.js → chunk-XZSTWKYB-DxAOx4hG.js} +1 -1
  42. package/dist/web/assets/{chunk-YBOYWFTD-DqSOVcYe.js → chunk-YBOYWFTD-CeU4Q-xC.js} +1 -1
  43. package/dist/web/assets/classDiagram-VBA2DB6C-lse8oZoJ.js +1 -0
  44. package/dist/web/assets/classDiagram-v2-RAHNMMFH-CxkwuInd.js +1 -0
  45. package/dist/web/assets/clone-LRxlvnMj.js +1 -0
  46. package/dist/web/assets/code-editor-DTA3c9Y8.js +2 -0
  47. package/dist/web/assets/{cose-bilkent-S5V4N54A-DlL82QHu.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-BmVoh2At.js → dagre-Dbb5k38K.js} +1 -1
  50. package/dist/web/assets/{dagre-KLK3FWXG-sDrRW9MQ.js → dagre-KLK3FWXG-BH7aWGRP.js} +1 -1
  51. package/dist/web/assets/database-viewer-DXk79Nel.js +1 -0
  52. package/dist/web/assets/{diagram-E7M64L7V-ChnAhgni.js → diagram-E7M64L7V-B1Qz70Do.js} +1 -1
  53. package/dist/web/assets/{diagram-IFDJBPK2-DW1J1uJd.js → diagram-IFDJBPK2-k55eVqVU.js} +1 -1
  54. package/dist/web/assets/{diagram-P4PSJMXO-CQ32hyG_.js → diagram-P4PSJMXO-BkfNRc9U.js} +1 -1
  55. package/dist/web/assets/diff-viewer-HhIcsOQE.js +4 -0
  56. package/dist/web/assets/dist-DylI9XxN.js +13 -0
  57. package/dist/web/assets/dist-lF8CoYII.js +41 -0
  58. package/dist/web/assets/{erDiagram-INFDFZHY-6CHo6nOw.js → erDiagram-INFDFZHY-CKzVujYI.js} +1 -1
  59. package/dist/web/assets/{flowDiagram-PKNHOUZH-DroDiNT0.js → flowDiagram-PKNHOUZH-DIqcTrDV.js} +1 -1
  60. package/dist/web/assets/{ganttDiagram-A5KZAMGK-DP0QBh8w.js → ganttDiagram-A5KZAMGK-D4v7ZbVE.js} +1 -1
  61. package/dist/web/assets/git-graph-CQtWu8yE.js +1 -0
  62. package/dist/web/assets/gitGraph-HDMCJU4V-Bwna3and.js +1 -0
  63. package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-DvU3JGZn.js → gitGraphDiagram-K3NZZRJ6-BTXo57mF.js} +1 -1
  64. package/dist/web/assets/{graphlib-CQBb2thr.js → graphlib-BcsNnGcW.js} +1 -1
  65. package/dist/web/assets/index-CgQXpBb_.css +2 -0
  66. package/dist/web/assets/index-DEeeRoka.js +37 -0
  67. package/dist/web/assets/info-3K5VOQVL-_vRxVNUm.js +1 -0
  68. package/dist/web/assets/infoDiagram-LFFYTUFH-B1CX0pbC.js +2 -0
  69. package/dist/web/assets/input-BglMT33g.js +1 -0
  70. package/dist/web/assets/{isEmpty-B4kqZBtn.js → isEmpty-bnrF3Qbc.js} +1 -1
  71. package/dist/web/assets/{ishikawaDiagram-PHBUUO56-46yibrV5.js → ishikawaDiagram-PHBUUO56-BOyvKMmB.js} +1 -1
  72. package/dist/web/assets/{journeyDiagram-4ABVD52K-BcmRwjK-.js → journeyDiagram-4ABVD52K-ufoasAy6.js} +1 -1
  73. package/dist/web/assets/{kanban-definition-K7BYSVSG-B619K53y.js → kanban-definition-K7BYSVSG-Bi0UTUeN.js} +1 -1
  74. package/dist/web/assets/keybindings-store-1CJ7VX57.js +1 -0
  75. package/dist/web/assets/lib-BQ34Db2e.js +4 -0
  76. package/dist/web/assets/{line-1gcO63_w.js → line-B78g-52T.js} +1 -1
  77. package/dist/web/assets/{linear-DfRqDoVd.js → linear-DP4mkX3m.js} +1 -1
  78. package/dist/web/assets/markdown-renderer-Brj8_LQM.js +69 -0
  79. package/dist/web/assets/{mermaid-parser.core-XtjZQOeM.js → mermaid-parser.core-DMIWdgEW.js} +2 -2
  80. package/dist/web/assets/{mindmap-definition-YRQLILUH-CifOFo_q.js → mindmap-definition-YRQLILUH-BsfWvIoO.js} +1 -1
  81. package/dist/web/assets/{ordinal-BJYw-iDX.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-BuHUh_fO.js → pieDiagram-SKSYHLDU-WP0XXw51.js} +1 -1
  85. package/dist/web/assets/postgres-viewer-CwkTGmqy.js +1 -0
  86. package/dist/web/assets/{quadrantDiagram-337W2JSQ-Bau_hj6Z.js → quadrantDiagram-337W2JSQ-FHMogtsh.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-Cq2b-uwp.js → requirementDiagram-Z7DCOOCP-BatTxyWb.js} +1 -1
  90. package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-DrdGQxWQ.js → sankeyDiagram-WA2Y5GQK-ClJuW3Hv.js} +1 -1
  91. package/dist/web/assets/{sequenceDiagram-2WXFIKYE-qPxiTUcS.js → sequenceDiagram-2WXFIKYE-ByxQqGgs.js} +1 -1
  92. package/dist/web/assets/settings-tab-BDE1MsIh.js +1 -0
  93. package/dist/web/assets/sqlite-viewer-CFYTwgA8.js +1 -0
  94. package/dist/web/assets/{stateDiagram-RAJIS63D-Dulj2oa8.js → stateDiagram-RAJIS63D-f8opcZNY.js} +1 -1
  95. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-DrxVDY9q.js +1 -0
  96. package/dist/web/assets/tab-store-BJw7OCmy.js +1 -0
  97. package/dist/web/assets/{terminal-tab-wKgpSPAT.js → terminal-tab-CCDLZA5Y.js} +2 -2
  98. package/dist/web/assets/{timeline-definition-YZTLITO2-BWyDnCYq.js → timeline-definition-YZTLITO2-58BlOSf9.js} +1 -1
  99. package/dist/web/assets/treemap-KZPCXAKY-B2Xkyv-K.js +1 -0
  100. package/dist/web/assets/use-monaco-theme-CNzekTN3.js +11 -0
  101. package/dist/web/assets/{vennDiagram-LZ73GAT5-B9Iv2bNV.js → vennDiagram-LZ73GAT5-BOSy9ma9.js} +1 -1
  102. package/dist/web/assets/{xychartDiagram-JWTSCODW-ChXcMzBQ.js → xychartDiagram-JWTSCODW-z5MVJauZ.js} +1 -1
  103. package/dist/web/index.html +12 -11
  104. package/dist/web/sw.js +1 -1
  105. package/docs/code-standards.md +232 -7
  106. package/docs/codebase-summary.md +9 -3
  107. package/docs/design-guidelines.md +21 -0
  108. package/docs/project-changelog.md +115 -1
  109. package/docs/project-roadmap.md +41 -19
  110. package/docs/system-architecture.md +212 -15
  111. package/package.json +3 -2
  112. package/src/cli/commands/autostart.ts +1 -1
  113. package/src/cli/commands/restart.ts +9 -1
  114. package/src/cli/commands/status.ts +19 -0
  115. package/src/index.ts +2 -3
  116. package/src/providers/claude-agent-sdk.ts +92 -25
  117. package/src/providers/mock-provider.ts +6 -1
  118. package/src/server/index.ts +38 -166
  119. package/src/server/routes/browser-preview.ts +159 -0
  120. package/src/server/routes/chat.ts +52 -3
  121. package/src/server/routes/project-scoped.ts +2 -0
  122. package/src/server/routes/proxy.ts +46 -53
  123. package/src/server/routes/tunnel.ts +0 -32
  124. package/src/server/routes/workspace.ts +35 -0
  125. package/src/server/ws/chat.ts +207 -146
  126. package/src/services/account-selector.service.ts +16 -8
  127. package/src/services/account.service.ts +19 -13
  128. package/src/services/claude-usage.service.ts +48 -11
  129. package/src/services/cloud-ws.service.ts +227 -0
  130. package/src/services/cloud.service.ts +10 -6
  131. package/src/services/db.service.ts +111 -6
  132. package/src/services/proxy.service.ts +4 -19
  133. package/src/services/supervisor.ts +285 -25
  134. package/src/types/api.ts +9 -1
  135. package/src/types/chat.ts +3 -1
  136. package/src/web/app.tsx +41 -35
  137. package/src/web/components/browser/browser-tab.tsx +106 -97
  138. package/src/web/components/chat/account-rotation-settings.tsx +163 -0
  139. package/src/web/components/chat/chat-history-bar.tsx +72 -6
  140. package/src/web/components/chat/chat-tab.tsx +32 -16
  141. package/src/web/components/chat/chat-welcome.tsx +148 -0
  142. package/src/web/components/chat/message-input.tsx +107 -13
  143. package/src/web/components/chat/message-list.tsx +27 -15
  144. package/src/web/components/chat/session-picker.tsx +78 -31
  145. package/src/web/components/chat/usage-badge.tsx +11 -1
  146. package/src/web/components/editor/code-editor.tsx +36 -26
  147. package/src/web/components/editor/csv-preview.tsx +228 -0
  148. package/src/web/components/editor/editor-breadcrumb.tsx +216 -0
  149. package/src/web/components/editor/editor-toolbar.tsx +74 -0
  150. package/src/web/components/layout/command-palette.tsx +3 -1
  151. package/src/web/components/layout/editor-panel.tsx +162 -18
  152. package/src/web/components/layout/panel-layout.tsx +17 -1
  153. package/src/web/components/settings/proxy-settings-section.tsx +40 -42
  154. package/src/web/components/shared/connection-lost-overlay.tsx +89 -0
  155. package/src/web/hooks/use-chat.ts +211 -201
  156. package/src/web/hooks/use-global-keybindings.ts +25 -2
  157. package/src/web/hooks/use-server-reload.ts +9 -0
  158. package/src/web/hooks/use-url-sync.ts +173 -21
  159. package/src/web/hooks/use-voice-input.ts +111 -0
  160. package/src/web/lib/csv-parser.ts +134 -0
  161. package/src/web/stores/connection-store.ts +39 -0
  162. package/src/web/stores/keybindings-store.ts +1 -0
  163. package/src/web/stores/panel-store.ts +73 -19
  164. package/src/web/stores/panel-utils.ts +145 -3
  165. package/dist/web/assets/api-settings-CFw-lh5k.js +0 -1
  166. package/dist/web/assets/architecture-PBZL5I3N-CJupe6q_.js +0 -1
  167. package/dist/web/assets/browser-tab-CmsL5eny.js +0 -1
  168. package/dist/web/assets/channel-DmKoFTd_.js +0 -1
  169. package/dist/web/assets/chat-tab-CFWsf13Z.js +0 -7
  170. package/dist/web/assets/chunk-GLR3WWYH-BnP-hOp6.js +0 -2
  171. package/dist/web/assets/chunk-HHEYEP7N-DKDPTPEZ.js +0 -1
  172. package/dist/web/assets/chunk-QZHKN3VN-C_wpI9wz.js +0 -1
  173. package/dist/web/assets/classDiagram-VBA2DB6C-B1T5uY-F.js +0 -1
  174. package/dist/web/assets/classDiagram-v2-RAHNMMFH-xs5vI3xC.js +0 -1
  175. package/dist/web/assets/clone-CijCFRT5.js +0 -1
  176. package/dist/web/assets/code-editor-H_dAh_fJ.js +0 -1
  177. package/dist/web/assets/database-viewer-DBzsgEJ8.js +0 -1
  178. package/dist/web/assets/diff-viewer-DzS-OnAR.js +0 -4
  179. package/dist/web/assets/dist-0Va_2L7G.js +0 -16
  180. package/dist/web/assets/dist-D9irYETY.js +0 -41
  181. package/dist/web/assets/git-graph-D3C7F8o3.js +0 -1
  182. package/dist/web/assets/gitGraph-HDMCJU4V-B0KvGQG8.js +0 -1
  183. package/dist/web/assets/index-CIkjfera.js +0 -31
  184. package/dist/web/assets/index-WKLuYsBY.css +0 -2
  185. package/dist/web/assets/info-3K5VOQVL-1uJ6_hCm.js +0 -1
  186. package/dist/web/assets/infoDiagram-LFFYTUFH-DLA5Q-3y.js +0 -2
  187. package/dist/web/assets/input-CGp1nFIg.js +0 -1
  188. package/dist/web/assets/keybindings-store-BdaoLwSo.js +0 -1
  189. package/dist/web/assets/markdown-renderer-DH49Zag7.js +0 -69
  190. package/dist/web/assets/packet-RMMSAZCW-34C4o9yj.js +0 -1
  191. package/dist/web/assets/pie-UPGHQEXC-D9ekKlh9.js +0 -1
  192. package/dist/web/assets/postgres-viewer-B9FYk8sD.js +0 -1
  193. package/dist/web/assets/radar-KQ55EAFF-DEuXOXSD.js +0 -1
  194. package/dist/web/assets/settings-store-DWXGVHsE.js +0 -2
  195. package/dist/web/assets/settings-tab-D-q8pd-5.js +0 -1
  196. package/dist/web/assets/sqlite-viewer-CDqcTePw.js +0 -1
  197. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-CAkzLlhk.js +0 -1
  198. package/dist/web/assets/tab-store-BPeiymiH.js +0 -1
  199. package/dist/web/assets/treemap-KZPCXAKY-nc7a1Ia1.js +0 -1
  200. package/dist/web/assets/use-monaco-theme-CCBTQ0S3.js +0 -11
  201. package/src/services/port-tunnel.service.ts +0 -97
  202. /package/dist/web/assets/{api-client-DOElml5u.js → api-client-BfBM3I7n.js} +0 -0
  203. /package/dist/web/assets/{array-CYkMkqnU.js → array-B9UHiPd-.js} +0 -0
  204. /package/dist/web/assets/{columns-2-ChOTgl3e.js → columns-2-DpsNbZOc.js} +0 -0
  205. /package/dist/web/assets/{cytoscape.esm-HeHO0VhB.js → cytoscape.esm-BW-DbntU.js} +0 -0
  206. /package/dist/web/assets/{defaultLocale-Beh6XjaL.js → defaultLocale-5eAKkKJC.js} +0 -0
  207. /package/dist/web/assets/{dist-BUYzeuKe.js → dist-CSJdAyA9.js} +0 -0
  208. /package/dist/web/assets/{init-Rr1s_RiX.js → init-DlZdxViB.js} +0 -0
  209. /package/dist/web/assets/{isArrayLikeObject-BB-mzMLb.js → isArrayLikeObject-B_v2FtYn.js} +0 -0
  210. /package/dist/web/assets/{katex-CKoArbIw.js → katex-Bqvo_ZG0.js} +0 -0
  211. /package/dist/web/assets/{math-B7b0HgJF.js → math-069Z4SuC.js} +0 -0
  212. /package/dist/web/assets/{path-BAQ3hXlG.js → path-6uRLdFF7.js} +0 -0
  213. /package/dist/web/assets/{preload-helper-DeiOTZKJ.js → preload-helper-uTix4PVD.js} +0 -0
  214. /package/dist/web/assets/{react-Dev-wu-s.js → react-ER-4DN55.js} +0 -0
  215. /package/dist/web/assets/{rough.esm-Dwml_la6.js → rough.esm-JX0wREDd.js} +0 -0
  216. /package/dist/web/assets/{src-B_cC68fH.js → src-BqX54PbV.js} +0 -0
  217. /package/dist/web/assets/{table-COiJDPRA.js → table-C7X5UAEI.js} +0 -0
  218. /package/dist/web/assets/{tag-LMq02LfE.js → tag-CCtdV063.js} +0 -0
  219. /package/dist/web/assets/{utils-btZ8C8-R.js → utils-BNytJOb1.js} +0 -0
@@ -7,7 +7,12 @@ import { useTabStore } from "@/stores/tab-store";
7
7
  import { useSettingsStore } from "@/stores/settings-store";
8
8
  import { basename } from "@/lib/utils";
9
9
  import { useMonacoTheme } from "@/lib/use-monaco-theme";
10
- import { Loader2, FileWarning, ExternalLink, Code, Eye, WrapText } from "lucide-react";
10
+ import { Loader2, FileWarning, ExternalLink } from "lucide-react";
11
+ import { EditorBreadcrumb } from "./editor-breadcrumb";
12
+ import { EditorToolbar } from "./editor-toolbar";
13
+ import { lazy, Suspense } from "react";
14
+
15
+ const CsvPreview = lazy(() => import("./csv-preview").then((m) => ({ default: m.CsvPreview })));
11
16
 
12
17
  /** Image extensions renderable inline */
13
18
  const IMAGE_EXTS = new Set(["png", "jpg", "jpeg", "gif", "webp", "svg", "ico"]);
@@ -58,7 +63,9 @@ export function CodeEditor({ metadata, tabId }: CodeEditorProps) {
58
63
  const isPdf = ext === "pdf";
59
64
  const isSqlite = SQLITE_EXTS.has(ext);
60
65
  const isMarkdown = ext === "md" || ext === "mdx";
66
+ const isCsv = ext === "csv";
61
67
  const [mdMode, setMdMode] = useState<"edit" | "preview">("preview");
68
+ const [csvMode, setCsvMode] = useState<"table" | "raw">("table");
62
69
 
63
70
  // Redirect .db files to sqlite viewer by changing tab type
64
71
  useEffect(() => {
@@ -196,33 +203,36 @@ export function CodeEditor({ metadata, tabId }: CodeEditorProps) {
196
203
  );
197
204
  }
198
205
 
199
- const mdModeButtons = isMarkdown ? (
200
- <>
201
- <button type="button" onClick={() => setMdMode("edit")}
202
- className={`flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors ${mdMode === "edit" ? "bg-muted text-foreground" : "text-muted-foreground hover:text-foreground"}`}
203
- >
204
- <Code className="size-3" /> Edit
205
- </button>
206
- <button type="button" onClick={() => setMdMode("preview")}
207
- className={`flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors ${mdMode === "preview" ? "bg-muted text-foreground" : "text-muted-foreground hover:text-foreground"}`}
208
- >
209
- <Eye className="size-3" /> Preview
210
- </button>
211
- </>
212
- ) : null;
213
-
214
- const wrapBtn = (
215
- <button type="button" onClick={toggleWordWrap} title="Toggle word wrap (Alt+Z)"
216
- className={`flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors ${wordWrap ? "bg-muted text-foreground" : "text-muted-foreground hover:text-foreground"}`}
217
- >
218
- <WrapText className="size-3" />
219
- <span className="hidden sm:inline">Wrap</span>
220
- </button>
221
- );
222
-
223
206
  return (
224
207
  <div className="flex flex-col h-full w-full overflow-hidden">
225
- {isMarkdown && mdMode === "preview" ? (
208
+ {/* Breadcrumb + Toolbar bar desktop only */}
209
+ {filePath && projectName && tabId && (
210
+ <div className="hidden md:flex items-center h-7 border-b border-border bg-background shrink-0">
211
+ <EditorBreadcrumb
212
+ filePath={filePath}
213
+ projectName={projectName}
214
+ tabId={tabId}
215
+ className="flex items-center flex-1 min-w-0 overflow-x-auto scrollbar-none px-2 gap-0.5"
216
+ />
217
+ <EditorToolbar
218
+ ext={ext}
219
+ mdMode={mdMode}
220
+ onMdModeChange={setMdMode}
221
+ csvMode={csvMode}
222
+ onCsvModeChange={setCsvMode}
223
+ wordWrap={wordWrap}
224
+ onToggleWordWrap={toggleWordWrap}
225
+ className="shrink-0 flex items-center gap-1 px-2"
226
+ />
227
+ </div>
228
+ )}
229
+
230
+ {/* Content area */}
231
+ {isCsv && csvMode === "table" ? (
232
+ <Suspense fallback={<div className="flex items-center justify-center h-full"><Loader2 className="size-5 animate-spin text-text-subtle" /></div>}>
233
+ <CsvPreview content={content ?? ""} onContentChange={handleChange} wordWrap={wordWrap} />
234
+ </Suspense>
235
+ ) : isMarkdown && mdMode === "preview" ? (
226
236
  <MarkdownPreview content={content ?? ""} />
227
237
  ) : (
228
238
  <div className="flex-1 overflow-hidden">
@@ -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
+ }
@@ -11,6 +11,7 @@ import {
11
11
  FolderOpen,
12
12
  Loader2,
13
13
  Globe,
14
+ Mic,
14
15
  } from "lucide-react";
15
16
  import { useTabStore, type TabType } from "@/stores/tab-store";
16
17
  import { useProjectStore } from "@/stores/project-store";
@@ -157,8 +158,9 @@ export function CommandPalette({ open, onClose, initialQuery = "" }: { open: boo
157
158
  { id: "chat", label: "New AI Chat", icon: MessageSquare, action: openNewTab("chat", "AI Chat"), keywords: "ai assistant claude", group: "action", shortcut: formatShortcut(getBinding("open-chat")) },
158
159
  { id: "terminal", label: "New Terminal", icon: Terminal, action: openNewTab("terminal", "Terminal"), keywords: "bash shell console", group: "action", shortcut: formatShortcut(getBinding("open-terminal")) },
159
160
  { id: "git-graph", label: "Git Graph", icon: GitBranch, action: openNewTab("git-graph", "Git Graph"), keywords: "branch history log", group: "action", shortcut: formatShortcut(getBinding("open-git-graph")) },
161
+ { id: "browser", label: "Open Browser", icon: Globe, action: openNewTab("browser", "Browser"), keywords: "web preview localhost iframe url", group: "action" },
160
162
  { id: "postgres", label: "PostgreSQL", icon: Database, action: openNewTab("postgres", "PostgreSQL"), keywords: "database pg sql query", group: "action" },
161
- { id: "browser", label: "Browser (Port Tunnel)", icon: Globe, action: openNewTab("browser", "Browser"), keywords: "preview tunnel port iframe web", group: "action" },
163
+ { id: "voice-input", label: "Voice Input", icon: Mic, action: () => { window.dispatchEvent(new CustomEvent("toggle-voice-input")); onClose(); }, keywords: "speech microphone dictate voice", group: "action", shortcut: formatShortcut(getBinding("voice-input")) },
162
164
  { id: "git-status", label: "Git Status", icon: GitCommitHorizontal, action: () => { setSidebarActiveTab("git"); onClose(); }, keywords: "changes diff staged", group: "action", shortcut: formatShortcut(getBinding("open-git-status")) },
163
165
  {
164
166
  id: "settings", label: "Settings", icon: Settings,