@hienlh/ppm 0.9.83 → 0.9.85

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 (189) hide show
  1. package/260413-1354-new-file-editor-tab/reports/code-reviewer-260413-1420-new-file-tab-review.md +210 -0
  2. package/CHANGELOG.md +20 -0
  3. package/bun.lock +259 -9
  4. package/dist/web/assets/{_basePickBy-5PGDJbfF.js → _basePickBy-D-bUmjma.js} +1 -1
  5. package/dist/web/assets/{_baseUniq-BT4Ow4Kk.js → _baseUniq-BnXXIfRB.js} +1 -1
  6. package/dist/web/assets/ai-settings-section-D6d-RmR6.js +1 -0
  7. package/dist/web/assets/{api-settings-Bn-bIxD1.js → api-settings-Qi2xRiHa.js} +1 -1
  8. package/dist/web/assets/{arc-BAOivWpI.js → arc-DB9vXGzd.js} +1 -1
  9. package/dist/web/assets/architecture-PBZL5I3N-DpVzOETR.js +1 -0
  10. package/dist/web/assets/{architectureDiagram-2XIMDMQ5-Z-4eN4za.js → architectureDiagram-2XIMDMQ5-BBV25747.js} +1 -1
  11. package/dist/web/assets/{blockDiagram-WCTKOSBZ-BCLqzhuZ.js → blockDiagram-WCTKOSBZ-BOTnY2Lq.js} +1 -1
  12. package/dist/web/assets/{c4Diagram-IC4MRINW-0Vp0Jeas.js → c4Diagram-IC4MRINW-D7QAUdHD.js} +1 -1
  13. package/dist/web/assets/channel-Cgy1thYT.js +1 -0
  14. package/dist/web/assets/chat-tab-DXBb9Y3U.js +10 -0
  15. package/dist/web/assets/check-ePA3ZvK4.js +1 -0
  16. package/dist/web/assets/chevron-down-EQA06nR-.js +1 -0
  17. package/dist/web/assets/{chunk-4BX2VUAB-D4tOov49.js → chunk-4BX2VUAB-BnOVw77D.js} +1 -1
  18. package/dist/web/assets/{chunk-55IACEB6-DJ6BynZ4.js → chunk-55IACEB6-BftA8DxR.js} +1 -1
  19. package/dist/web/assets/{chunk-7E7YKBS2-CiyUJxNI.js → chunk-7E7YKBS2-B0vnP8v3.js} +1 -1
  20. package/dist/web/assets/{chunk-7R4GIKGN-Dv-4cAYn.js → chunk-7R4GIKGN-Czlaj26D.js} +2 -2
  21. package/dist/web/assets/{chunk-C72U2L5F-D21mS_6G.js → chunk-C72U2L5F-DpEbDtMo.js} +1 -1
  22. package/dist/web/assets/{chunk-EGIJ26TM-DzqmU2Z7.js → chunk-EGIJ26TM-BWXe6lkx.js} +1 -1
  23. package/dist/web/assets/{chunk-FMBD7UC4-DXncblvW.js → chunk-FMBD7UC4-DspqhPfk.js} +1 -1
  24. package/dist/web/assets/{chunk-GEFDOKGD-D-pKjlVd.js → chunk-GEFDOKGD-D6HHRbYk.js} +1 -1
  25. package/dist/web/assets/chunk-GLR3WWYH-CxUl1sdz.js +2 -0
  26. package/dist/web/assets/chunk-HHEYEP7N-DN7ebS2Y.js +1 -0
  27. package/dist/web/assets/{chunk-JSJVCQXG-99JzIdPr.js → chunk-JSJVCQXG-BC8wnMwf.js} +1 -1
  28. package/dist/web/assets/{chunk-KX2RTZJC-CRq1OBZv.js → chunk-KX2RTZJC-D3VDtyvX.js} +1 -1
  29. package/dist/web/assets/{chunk-KYZI473N-Bb0MCaIO.js → chunk-KYZI473N-Z-NBw_HS.js} +1 -1
  30. package/dist/web/assets/{chunk-L3YUKLVL-C7qGJrfV.js → chunk-L3YUKLVL--RGkEh__.js} +1 -1
  31. package/dist/web/assets/{chunk-MX3YWQON-BpS_PtKp.js → chunk-MX3YWQON-2B76t_Kx.js} +1 -1
  32. package/dist/web/assets/{chunk-NQ4KR5QH-z_blpjxi.js → chunk-NQ4KR5QH-BekY3tEi.js} +1 -1
  33. package/dist/web/assets/{chunk-O4XLMI2P-nDhi_cVu.js → chunk-O4XLMI2P-2CJLfx_1.js} +1 -1
  34. package/dist/web/assets/{chunk-OZEHJAEY-BXhYx3nO.js → chunk-OZEHJAEY-sug_L09P.js} +1 -1
  35. package/dist/web/assets/{chunk-PQ6SQG4A-TF58UVMU.js → chunk-PQ6SQG4A-_fwPRLQy.js} +1 -1
  36. package/dist/web/assets/{chunk-PU5JKC2W-ek7k4QVB.js → chunk-PU5JKC2W-BUaTFJVQ.js} +1 -1
  37. package/dist/web/assets/chunk-QZHKN3VN-C4La7oLj.js +1 -0
  38. package/dist/web/assets/{chunk-R5LLSJPH-CFwSJijQ.js → chunk-R5LLSJPH-C37xW0vj.js} +1 -1
  39. package/dist/web/assets/{chunk-WL4C6EOR-ByUrSRin.js → chunk-WL4C6EOR-CCkt_MT6.js} +1 -1
  40. package/dist/web/assets/{chunk-XIRO2GV7-Djlmrely.js → chunk-XIRO2GV7-Dz2LBq7Y.js} +1 -1
  41. package/dist/web/assets/{chunk-XPW4576I-BPQQBakK.js → chunk-XPW4576I-DenTbBuj.js} +1 -1
  42. package/dist/web/assets/{chunk-XZSTWKYB-DxAOx4hG.js → chunk-XZSTWKYB-Dbp1nUSQ.js} +1 -1
  43. package/dist/web/assets/{chunk-YBOYWFTD-rQG3QH5s.js → chunk-YBOYWFTD-3OTKowjE.js} +1 -1
  44. package/dist/web/assets/classDiagram-VBA2DB6C-C3IyfqG-.js +1 -0
  45. package/dist/web/assets/classDiagram-v2-RAHNMMFH-Dcvhz2pb.js +1 -0
  46. package/dist/web/assets/clone--C7Tby8z.js +1 -0
  47. package/dist/web/assets/code-editor-Cr7JrBKC.js +8 -0
  48. package/dist/web/assets/{cose-bilkent-S5V4N54A-B_AWZsOP.js → cose-bilkent-S5V4N54A-MbmGZnt0.js} +1 -1
  49. package/dist/web/assets/{csv-preview-D2pJJj3K.js → csv-preview-uZ_7b8I7.js} +1 -1
  50. package/dist/web/assets/{dagre-DHq9bhnd.js → dagre-CPhI6v-K.js} +1 -1
  51. package/dist/web/assets/{dagre-KLK3FWXG-BdJr7Byp.js → dagre-KLK3FWXG-CmSE-oNj.js} +1 -1
  52. package/dist/web/assets/database-D1ToEV9d.js +1 -0
  53. package/dist/web/assets/{database-viewer-0P_zRC9w.js → database-viewer-5xljX0JI.js} +2 -2
  54. package/dist/web/assets/{diagram-E7M64L7V-_db4pBVA.js → diagram-E7M64L7V-B5XG3ZT7.js} +1 -1
  55. package/dist/web/assets/{diagram-IFDJBPK2-xKoeuiJx.js → diagram-IFDJBPK2-BsP248aX.js} +1 -1
  56. package/dist/web/assets/{diagram-P4PSJMXO-C8tjJsev.js → diagram-P4PSJMXO-Cna3408N.js} +1 -1
  57. package/dist/web/assets/diff-viewer-BBr6e_gb.js +4 -0
  58. package/dist/web/assets/dist-KUoHa6tg.js +1 -0
  59. package/dist/web/assets/{erDiagram-INFDFZHY-BSh2z9Df.js → erDiagram-INFDFZHY-B7SgktiR.js} +1 -1
  60. package/dist/web/assets/{extension-webview-Cx0GpRyC.js → extension-webview-B0klBip8.js} +1 -1
  61. package/dist/web/assets/eye-CNcBU6Tx.js +1 -0
  62. package/dist/web/assets/{flowDiagram-PKNHOUZH-oYaovqyp.js → flowDiagram-PKNHOUZH-FOYZZ1OB.js} +1 -1
  63. package/dist/web/assets/{ganttDiagram-A5KZAMGK-DmL26q2P.js → ganttDiagram-A5KZAMGK-CnHVYh9v.js} +1 -1
  64. package/dist/web/assets/git-graph-CDiwGa0g.js +1 -0
  65. package/dist/web/assets/gitGraph-HDMCJU4V-DcPyMEIJ.js +1 -0
  66. package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-CMoukSrY.js → gitGraphDiagram-K3NZZRJ6-0G9XxZay.js} +1 -1
  67. package/dist/web/assets/{graphlib-BcsNnGcW.js → graphlib-CNiBwlg_.js} +1 -1
  68. package/dist/web/assets/index-CkaCzNgO.css +2 -0
  69. package/dist/web/assets/index-Ic5uTu20.js +26 -0
  70. package/dist/web/assets/info-3K5VOQVL-Dw4O15cw.js +1 -0
  71. package/dist/web/assets/infoDiagram-LFFYTUFH-DFhmsucr.js +2 -0
  72. package/dist/web/assets/input-CcbTF6ih.js +45 -0
  73. package/dist/web/assets/{isEmpty-bnrF3Qbc.js → isEmpty-CcCb5n2-.js} +1 -1
  74. package/dist/web/assets/{ishikawaDiagram-PHBUUO56-D05_LyL7.js → ishikawaDiagram-PHBUUO56-D4QCzh5J.js} +1 -1
  75. package/dist/web/assets/{journeyDiagram-4ABVD52K-B_L20qMe.js → journeyDiagram-4ABVD52K-CnHYNfKW.js} +1 -1
  76. package/dist/web/assets/{kanban-definition-K7BYSVSG-CZ535BbZ.js → kanban-definition-K7BYSVSG-Bh_g3EVu.js} +1 -1
  77. package/dist/web/assets/keybindings-store-CxE6BlG2.js +1 -0
  78. package/dist/web/assets/{line-CVvo3dRu.js → line-6d3eBADm.js} +1 -1
  79. package/dist/web/assets/{linear-DP4mkX3m.js → linear-cA_2lQy7.js} +1 -1
  80. package/dist/web/assets/markdown-renderer-CZ07F7T6.js +306 -0
  81. package/dist/web/assets/{mermaid-parser.core-C7UwoIh6.js → mermaid-parser.core-C3kd7JXM.js} +2 -2
  82. package/dist/web/assets/{mindmap-definition-YRQLILUH-x0MTutJp.js → mindmap-definition-YRQLILUH-CYiUwhr_.js} +1 -1
  83. package/dist/web/assets/{ordinal-_K3x1fkz.js → ordinal-XHK5vIzZ.js} +1 -1
  84. package/dist/web/assets/packet-RMMSAZCW-o3LmdL8H.js +1 -0
  85. package/dist/web/assets/pie-UPGHQEXC-BjNP0M3B.js +1 -0
  86. package/dist/web/assets/{pieDiagram-SKSYHLDU-C1Gjrtzy.js → pieDiagram-SKSYHLDU-D0S7jeZA.js} +1 -1
  87. package/dist/web/assets/plus-Iso5r9vD.js +1 -0
  88. package/dist/web/assets/port-forwarding-tab-BPuSc6pI.js +1 -0
  89. package/dist/web/assets/{postgres-viewer-DlCLiEGU.js → postgres-viewer-RldlAO_m.js} +3 -3
  90. package/dist/web/assets/{quadrantDiagram-337W2JSQ-C8bzJCjQ.js → quadrantDiagram-337W2JSQ-0hNP63hW.js} +1 -1
  91. package/dist/web/assets/radar-KQ55EAFF-gDgOiaME.js +1 -0
  92. package/dist/web/assets/refresh-cw-BgQzFNaG.js +1 -0
  93. package/dist/web/assets/{requirementDiagram-Z7DCOOCP-pQyah6WB.js → requirementDiagram-Z7DCOOCP-BVnmqFbL.js} +1 -1
  94. package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-T6RgG-N8.js → sankeyDiagram-WA2Y5GQK-DVkYdCJb.js} +1 -1
  95. package/dist/web/assets/scroll-area-i4EZlOl_.js +1 -0
  96. package/dist/web/assets/{sequenceDiagram-2WXFIKYE-BQDJ4CVs.js → sequenceDiagram-2WXFIKYE-B80s7sOg.js} +1 -1
  97. package/dist/web/assets/settings-tab-BzSSN2BQ.js +1 -0
  98. package/dist/web/assets/{sql-query-editor-Dxx-QZaI.js → sql-query-editor-CjZ7Z6XL.js} +1 -1
  99. package/dist/web/assets/sqlite-viewer-CoyZOM_Y.js +1 -0
  100. package/dist/web/assets/{stateDiagram-RAJIS63D-66vhiIuk.js → stateDiagram-RAJIS63D-BPLXgXRR.js} +1 -1
  101. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-DksQJ7es.js +1 -0
  102. package/dist/web/assets/{terminal-tab-yfmxGQ5e.js → terminal-tab-DjzD8GLn.js} +2 -2
  103. package/dist/web/assets/{timeline-definition-YZTLITO2-DwZqB3nn.js → timeline-definition-YZTLITO2-fa_51u1X.js} +1 -1
  104. package/dist/web/assets/trash-2-DYCa06CV.js +1 -0
  105. package/dist/web/assets/treemap-KZPCXAKY-DwFqAvnj.js +1 -0
  106. package/dist/web/assets/{use-monaco-theme-7qyyJae5.js → use-monaco-theme-D9XFxQuU.js} +1 -1
  107. package/dist/web/assets/{vennDiagram-LZ73GAT5-s9Z71fz-.js → vennDiagram-LZ73GAT5-kX4jJn6W.js} +1 -1
  108. package/dist/web/assets/x-BXecj-16.js +1 -0
  109. package/dist/web/assets/{xychartDiagram-JWTSCODW-DRa_TH4B.js → xychartDiagram-JWTSCODW-Bzm5lZBs.js} +1 -1
  110. package/dist/web/index.html +22 -12
  111. package/dist/web/sw.js +1 -1
  112. package/package.json +9 -3
  113. package/src/server/index.ts +1 -1
  114. package/src/server/routes/settings.ts +6 -3
  115. package/src/services/cloud.service.ts +35 -4
  116. package/src/web/components/editor/code-editor.tsx +67 -4
  117. package/src/web/components/editor/save-as-dialog.tsx +75 -0
  118. package/src/web/components/git/git-status-panel.tsx +7 -1
  119. package/src/web/components/layout/command-palette.tsx +2 -0
  120. package/src/web/components/layout/draggable-tab.tsx +120 -67
  121. package/src/web/components/layout/mobile-nav.tsx +69 -2
  122. package/src/web/components/layout/sidebar.tsx +11 -1
  123. package/src/web/components/layout/tab-bar.tsx +74 -1
  124. package/src/web/components/layout/upgrade-banner.tsx +3 -0
  125. package/src/web/components/shared/markdown-code-block.tsx +142 -0
  126. package/src/web/components/shared/markdown-context.ts +20 -0
  127. package/src/web/components/shared/markdown-renderer.tsx +113 -288
  128. package/src/web/hooks/use-global-keybindings.ts +7 -0
  129. package/src/web/main.tsx +1 -0
  130. package/src/web/stores/git-status-store.ts +55 -0
  131. package/src/web/stores/keybindings-store.ts +1 -0
  132. package/src/web/stores/panel-utils.ts +13 -0
  133. package/src/web/stores/tab-store.ts +16 -0
  134. package/src/web/styles/globals.css +6 -0
  135. package/dist/web/assets/architecture-PBZL5I3N-DEO2f3VD.js +0 -1
  136. package/dist/web/assets/channel-By7bn0Yq.js +0 -1
  137. package/dist/web/assets/chat-tab-DBJJz0Dm.js +0 -10
  138. package/dist/web/assets/chunk-GLR3WWYH-DKikpoJM.js +0 -2
  139. package/dist/web/assets/chunk-HHEYEP7N-C7vxA5i9.js +0 -1
  140. package/dist/web/assets/chunk-QZHKN3VN-CYaTbeZf.js +0 -1
  141. package/dist/web/assets/classDiagram-VBA2DB6C-BA8Nj-_C.js +0 -1
  142. package/dist/web/assets/classDiagram-v2-RAHNMMFH-DjYu-6mn.js +0 -1
  143. package/dist/web/assets/clone-LRxlvnMj.js +0 -1
  144. package/dist/web/assets/code-editor-BvSFsrGo.js +0 -8
  145. package/dist/web/assets/diff-viewer-BmBJq4gO.js +0 -4
  146. package/dist/web/assets/dist-DIV6WgAG.js +0 -41
  147. package/dist/web/assets/git-graph-BAlhf058.js +0 -1
  148. package/dist/web/assets/gitGraph-HDMCJU4V-Bwna3and.js +0 -1
  149. package/dist/web/assets/index-BDAqXmpQ.js +0 -30
  150. package/dist/web/assets/index-BYXjCNlK.css +0 -2
  151. package/dist/web/assets/info-3K5VOQVL-_vRxVNUm.js +0 -1
  152. package/dist/web/assets/infoDiagram-LFFYTUFH-DWwumDkq.js +0 -2
  153. package/dist/web/assets/keybindings-store-L7UlPjK0.js +0 -1
  154. package/dist/web/assets/markdown-renderer-CYs_lrjt.js +0 -326
  155. package/dist/web/assets/packet-RMMSAZCW-DY5PNnZU.js +0 -1
  156. package/dist/web/assets/pie-UPGHQEXC-BHncZutv.js +0 -1
  157. package/dist/web/assets/port-forwarding-tab-CSHJ7gxM.js +0 -1
  158. package/dist/web/assets/radar-KQ55EAFF-DH0AOkUy.js +0 -1
  159. package/dist/web/assets/settings-tab-Dcr7lDcJ.js +0 -1
  160. package/dist/web/assets/sqlite-viewer-B0052fC-.js +0 -1
  161. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-BGVqj_g9.js +0 -1
  162. package/dist/web/assets/treemap-KZPCXAKY-B2Xkyv-K.js +0 -1
  163. package/dist/web/assets/x-D2_KzIET.js +0 -1
  164. /package/dist/web/assets/{api-client-BfBM3I7n.js → api-client-wQbeUyeh.js} +0 -0
  165. /package/dist/web/assets/{array-B9UHiPd-.js → array-X0JlPOfd.js} +0 -0
  166. /package/dist/web/assets/{arrow-up-BYhx9ckd.js → arrow-up-BigIMx-e.js} +0 -0
  167. /package/dist/web/assets/{chevron-right-4zq1jPv6.js → chevron-right-CXzzT44u.js} +0 -0
  168. /package/dist/web/assets/{columns-2-BoZAN-iw.js → columns-2-BZ9uqssV.js} +0 -0
  169. /package/dist/web/assets/{csv-parser-CNNw2RVA.js → csv-parser-CElqio6o.js} +0 -0
  170. /package/dist/web/assets/{cytoscape.esm-BW-DbntU.js → cytoscape.esm-BfIOPvwt.js} +0 -0
  171. /package/dist/web/assets/{defaultLocale-5eAKkKJC.js → defaultLocale-B6RGN4id.js} +0 -0
  172. /package/dist/web/assets/{dist-CSJdAyA9.js → dist-CK1enexV.js} +0 -0
  173. /package/dist/web/assets/{init-DlZdxViB.js → init-BmUWJJHz.js} +0 -0
  174. /package/dist/web/assets/{isArrayLikeObject-B_v2FtYn.js → isArrayLikeObject-BrCM-iA1.js} +0 -0
  175. /package/dist/web/assets/{jsx-runtime-kMwlnEGE.js → jsx-runtime-R_NjdZtX.js} +0 -0
  176. /package/dist/web/assets/{katex-Bqvo_ZG0.js → katex-xQS_6bNb.js} +0 -0
  177. /package/dist/web/assets/{lib-DurwGtQO.js → lib-CfWBrYll.js} +0 -0
  178. /package/dist/web/assets/{math-069Z4SuC.js → math-CpLFzrfV.js} +0 -0
  179. /package/dist/web/assets/{path-6uRLdFF7.js → path-CoPyR7c2.js} +0 -0
  180. /package/dist/web/assets/{preload-helper-Bf_JiD2A.js → preload-helper-CH6UZRzu.js} +0 -0
  181. /package/dist/web/assets/{react-SKk5z-bm.js → react-j5zqhEum.js} +0 -0
  182. /package/dist/web/assets/{rough.esm-JX0wREDd.js → rough.esm-D5NinLFK.js} +0 -0
  183. /package/dist/web/assets/{sql-completion-provider-DM9Qov6L.js → sql-completion-provider-D0xutVaK.js} +0 -0
  184. /package/dist/web/assets/{square-oPKIkJiw.js → square-pfn_LYYy.js} +0 -0
  185. /package/dist/web/assets/{src-BqX54PbV.js → src-j04igtQ5.js} +0 -0
  186. /package/dist/web/assets/{table-DFevCOMd.js → table-CHv2x_qg.js} +0 -0
  187. /package/dist/web/assets/{tag-CXMT0QB6.js → tag-Bb_UFXt0.js} +0 -0
  188. /package/dist/web/assets/{text-wrap-BWNOVswA.js → text-wrap-D8BbQYTx.js} +0 -0
  189. /package/dist/web/assets/{utils-BNytJOb1.js → utils-CSCvNZxE.js} +0 -0
@@ -0,0 +1,210 @@
1
+ ## Code Review: New File Editor Tab Feature
2
+
3
+ ### Scope
4
+ - Files: 8 (panel-utils.ts, tab-store.ts, code-editor.tsx, save-as-dialog.tsx, tab-bar.tsx, use-global-keybindings.ts, keybindings-store.ts, command-palette.tsx)
5
+ - Focus: New untitled tab lifecycle, Save As transition, entry points
6
+ - Scout findings: Race condition in metadata persistence, stale tab ID after Save As, Ctrl+S binding leak
7
+
8
+ ### Overall Assessment
9
+ Feature is well-structured with clean separation between store logic, editor behavior, and UI entry points. The untitled number allocation via `getNextUntitledNumber()` scanning all panels is correct. However, the Save As transition has two critical issues that will cause user-visible bugs in production.
10
+
11
+ ---
12
+
13
+ ### Critical Issues
14
+
15
+ #### 1. Race condition: debounced metadata persistence overwrites Save As transition
16
+
17
+ **File:** `src/web/components/editor/code-editor.tsx` lines 272-276
18
+
19
+ **Problem:** `handleChange` sets a 2-second debounced timer to persist `unsavedContent` to tab metadata. `handleSaveAs` does NOT clear `saveTimerRef`. If the user types, then triggers Save As within the 2s debounce window, the pending timeout fires after Save As and calls:
20
+
21
+ ```ts
22
+ updateTab(tabId, { metadata: { ...oldMetadata, unsavedContent: latestContent } })
23
+ ```
24
+
25
+ The `oldMetadata` closure captured at the time of the keystroke still has `isUntitled: true`. This **reverts** the Save As transition, restoring the tab to untitled state. The file is written to disk but the tab loses its file association.
26
+
27
+ **Fix:** Clear `saveTimerRef` at the start of `handleSaveAs`:
28
+
29
+ ```ts
30
+ const handleSaveAs = useCallback(async (targetPath: string, savedText: string) => {
31
+ // Cancel any pending metadata persistence to prevent race condition
32
+ if (saveTimerRef.current) {
33
+ clearTimeout(saveTimerRef.current);
34
+ saveTimerRef.current = null;
35
+ }
36
+ try {
37
+ await api.put("/api/fs/write", { path: targetPath, content: savedText });
38
+ // ... rest
39
+ ```
40
+
41
+ #### 2. Tab ID not updated after Save As -- stale ID causes duplicate tabs
42
+
43
+ **File:** `src/web/components/editor/code-editor.tsx` lines 283-296
44
+
45
+ **Problem:** After Save As, `updateTab` changes the tab's metadata (filePath, removes isUntitled) but the tab ID remains `editor:untitled-N`. The `updateTab` API signature `Partial<Omit<Tab, "id">>` prevents changing the ID. Consequences:
46
+
47
+ - Opening the same file from explorer or command palette creates a **second tab** with ID `editor:/path/to/file` (dedup logic in `openTab` checks by ID)
48
+ - The `getNextUntitledNumber()` scanner still sees the old `editor:untitled-N` ID, so it considers number N as "taken" even though the tab is no longer untitled
49
+ - Tab persistence to localStorage/server keeps the stale ID
50
+
51
+ **Fix:** Replace the `updateTab` call with a close-then-open sequence:
52
+
53
+ ```ts
54
+ const handleSaveAs = useCallback(async (targetPath: string, savedText: string) => {
55
+ if (saveTimerRef.current) { clearTimeout(saveTimerRef.current); saveTimerRef.current = null; }
56
+ try {
57
+ await api.put("/api/fs/write", { path: targetPath, content: savedText });
58
+ if (tabId) {
59
+ // Close the untitled tab and open as a proper file tab
60
+ const panelStore = usePanelStore.getState();
61
+ panelStore.closeTab(tabId);
62
+ panelStore.openTab({
63
+ type: "editor",
64
+ title: basename(targetPath),
65
+ projectId: null,
66
+ metadata: { filePath: targetPath },
67
+ closable: true,
68
+ });
69
+ }
70
+ setShowSaveAs(false);
71
+ } catch { /* silent */ }
72
+ }, [tabId]);
73
+ ```
74
+
75
+ **Alternative (simpler but less clean):** Add a `replaceTabId` method to panel-store that atomically swaps a tab's ID.
76
+
77
+ ---
78
+
79
+ ### High Priority
80
+
81
+ #### 3. Ctrl+S Monaco binding leaks after Save As
82
+
83
+ **File:** `src/web/components/editor/code-editor.tsx` lines 315-320
84
+
85
+ **Problem:** `handleEditorMount` registers `Ctrl+S -> setShowSaveAs(true)` once when `isUntitled` is true. After Save As transitions the tab, the Monaco command is never removed. Pressing Ctrl+S re-opens the Save As dialog instead of doing nothing (the global `save-prevent` handler only `preventDefault()`s the browser dialog, it doesn't trigger actual file save).
86
+
87
+ **Impact:** After saving an untitled file, every subsequent Ctrl+S opens Save As again instead of silently auto-saving.
88
+
89
+ **Fix (combined with Issue #2):** If using the close-then-open approach from Issue #2, this is automatically resolved since the new editor instance mounts without the untitled binding. If keeping `updateTab`, add a `useEffect` that removes and re-registers commands when `isUntitled` changes, or guard the callback:
90
+
91
+ ```ts
92
+ editor.addCommand(
93
+ monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS,
94
+ () => {
95
+ // Check current state at invocation time, not registration time
96
+ if (metadata?.isUntitled) setShowSaveAs(true);
97
+ },
98
+ );
99
+ ```
100
+
101
+ However, this still requires `metadata` to be current. A better approach uses a ref:
102
+
103
+ ```ts
104
+ const isUntitledRef = useRef(isUntitled);
105
+ isUntitledRef.current = isUntitled;
106
+
107
+ // In handleEditorMount:
108
+ editor.addCommand(
109
+ monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS,
110
+ () => { if (isUntitledRef.current) setShowSaveAs(true); },
111
+ );
112
+ ```
113
+
114
+ #### 4. Metadata replacement (not merge) loses context in `handleChange`
115
+
116
+ **File:** `src/web/components/editor/code-editor.tsx` line 275
117
+
118
+ `updateTab` in panel-store does shallow spread: `{ ...t, ...updates }`. When `updates` contains `{ metadata: {...} }`, the entire `metadata` object is replaced, not deep-merged. Currently this is fine because untitled tabs only have `isUntitled`, `untitledNumber`, and `unsavedContent`. But if any code adds additional metadata keys to the tab between creation and the debounced save, they would be silently dropped.
119
+
120
+ **Recommendation:** Use explicit merge:
121
+
122
+ ```ts
123
+ updateTab(tabId, { metadata: { ...(ownTab?.metadata ?? {}), unsavedContent: latestContentRef.current } });
124
+ ```
125
+
126
+ Or better yet, read current metadata from the store at timeout fire time rather than closing over the render-time value.
127
+
128
+ ---
129
+
130
+ ### Medium Priority
131
+
132
+ #### 5. `handleEditorMount` has incomplete dependency array
133
+
134
+ **File:** `src/web/components/editor/code-editor.tsx` line 388
135
+
136
+ ```ts
137
+ }, [sqlSchemaInfo]); // eslint-disable-line react-hooks/exhaustive-deps
138
+ ```
139
+
140
+ The `useCallback` for `handleEditorMount` lists only `[sqlSchemaInfo]` as a dependency but uses `isUntitled`, `lineNumber`, `isSql`, and several refs. The eslint suppression hides stale closure bugs. Specifically, if `isUntitled` changes (impossible in practice for mount, but the lint suppression is risky for future changes).
141
+
142
+ **Recommendation:** This was pre-existing and the new code (`isUntitled` check inside) works because `handleEditorMount` only fires once per editor instance. No action needed now, but note for future.
143
+
144
+ #### 6. New tab dropdown menu: custom implementation vs existing dropdown component
145
+
146
+ **File:** `src/web/components/layout/tab-bar.tsx` lines 98-107, 240-262
147
+
148
+ The dropdown uses a manual `showNewMenu` state + `mousedown` outside click handler. This:
149
+ - Doesn't handle Escape key to close
150
+ - Doesn't handle focus management / trap
151
+ - Doesn't handle mobile touch events properly (no long-press activation)
152
+
153
+ Per the design guidelines ("Context menus -> long-press on mobile"), the tab bar dropdown is `hidden md:flex` so it only shows on desktop. No mobile issue, but missing Escape handling is a usability gap.
154
+
155
+ **Recommendation:** Use `DropdownMenu` from shadcn/ui for built-in keyboard navigation and accessibility.
156
+
157
+ #### 7. Save As dialog: `filename` validation allows path traversal characters
158
+
159
+ **File:** `src/web/components/editor/save-as-dialog.tsx` line 27
160
+
161
+ ```ts
162
+ if (/[/\\]/.test(trimmed)) { setError("Filename cannot contain / or \\"); return; }
163
+ ```
164
+
165
+ This blocks slashes but allows other potentially problematic characters (`..`, null bytes, colons on Windows, etc.). The server-side `writeSystemFile` has `isAllowedPath` guard which mitigates exploitation, but client-side validation could be more robust.
166
+
167
+ **Recommendation:** Also reject filenames starting with `.` (hidden files), containing `..`, or with special chars like `:`, `*`, `?`, `"`, `<`, `>`, `|` (Windows-unsafe).
168
+
169
+ ---
170
+
171
+ ### Low Priority
172
+
173
+ #### 8. Tab bar dropdown shortcut labels are hardcoded
174
+
175
+ **File:** `src/web/components/layout/tab-bar.tsx` lines 251-256
176
+
177
+ Shortcuts are hardcoded as strings (`"⌘L"`, `"⌘N"`, etc.) instead of reading from the keybindings store. If a user customizes their keybindings, the dropdown would show wrong shortcuts.
178
+
179
+ **Recommendation:** Use `formatCombo(getBinding("new-file"))` etc., matching the command palette's approach.
180
+
181
+ #### 9. Minor: `getNextUntitledNumber` is O(all tabs) on every new file creation
182
+
183
+ **File:** `src/web/stores/panel-utils.ts` lines 77-86
184
+
185
+ Scans all tabs in all panels to find max untitled number. Negligible with normal tab counts (< 100), but worth noting.
186
+
187
+ ---
188
+
189
+ ### Positive Observations
190
+
191
+ - Clean separation: untitled logic in store, persistence in editor, UI in dialog
192
+ - `getNextUntitledNumber` correctly scans all panels (multi-panel aware)
193
+ - Server-side `isAllowedPath` guard protects Save As writes
194
+ - `deriveTabId` correctly handles the untitled pattern
195
+ - Multiple entry points (dropdown, Ctrl+N, command palette) all route through `openNewFile()` -- single source of truth
196
+ - Keyboard shortcut added to keybindings store (customizable)
197
+ - The `isUntitled` check in the file load effect correctly skips API call and restores from metadata
198
+
199
+ ### Recommended Actions (Priority Order)
200
+
201
+ 1. **[CRITICAL]** Clear `saveTimerRef` in `handleSaveAs` to prevent metadata race condition
202
+ 2. **[CRITICAL]** Fix tab ID persistence after Save As -- either close+reopen or add a `replaceTabId` store method
203
+ 3. **[HIGH]** Fix Ctrl+S Monaco binding to check current untitled state via ref, not mount-time closure
204
+ 4. **[MEDIUM]** Consider using shadcn DropdownMenu for the tab bar "+" menu
205
+ 5. **[LOW]** Read shortcut labels from keybindings store in tab bar dropdown
206
+
207
+ ### Unresolved Questions
208
+
209
+ - Should there be a "close without saving" confirmation for untitled tabs with content? Currently closing an untitled tab silently discards content (metadata is removed, localStorage entry is gone). This could surprise users.
210
+ - Should the Save As dialog support saving within a project (relative paths) or only absolute paths? Currently it always uses `/api/fs/write` which writes to absolute paths. After Save As, the file is treated as an "external file" even if it's inside the active project directory.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.85] - 2026-04-14
4
+
5
+ ### Added
6
+ - **New file editor tabs**: Create untitled editor tabs (Untitled-1, Untitled-2...) via Ctrl+N or Command Palette. Content persists in localStorage across sessions. Save As dialog on first Ctrl+S using file browser picker.
7
+ - **Right-click context menu on tabs**: Copy path, download, rename, delete files directly from tab context menu.
8
+
9
+ ### Changed
10
+ - **Markdown rendering migrated to react-markdown**: Replaced `marked` with `react-markdown` for better extensibility and React integration.
11
+
12
+ ### Fixed
13
+ - **Upgrade dismiss persisted to sessionStorage**: Dismissed upgrade banner no longer reappears during same session.
14
+ - **WebSocket project path decoded for Windows**: Paths with special characters now work on Windows.
15
+
16
+ ## [0.9.84] - 2026-04-13
17
+
18
+ ### Fixed
19
+ - **Device rename now syncs to cloud**: Implemented JWT token auto-refresh so cloud API calls succeed after token expiry. Previously `refreshAccessToken` was a stub returning null.
20
+ - **Cloud sync errors surfaced**: PUT /settings/device-name now returns `cloud_synced` and `cloud_error` fields instead of silently swallowing failures.
21
+ - **Heartbeat propagates device name**: Cloud server now persists device name from both WS and HTTP heartbeats, providing eventual consistency even if JWT-based rename fails.
22
+
3
23
  ## [0.9.83] - 2026-04-12
4
24
 
5
25
  ### Fixed