@hienlh/ppm 0.9.85 → 0.9.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.
- package/260415-0932-git-graph-stash-rebase-conflicts/reports/code-reviewer-260415-1020-stash-rebase-conflicts.md +288 -0
- package/260415-0932-git-graph-stash-rebase-conflicts/reports/tester-260415-1020-build-check.md +117 -0
- package/260415-1150-ext-silent-failure-debugging/reports/code-reviewer-260415-1159-ext-error-reporting-review.md +205 -0
- package/260415-1150-ext-silent-failure-debugging/reports/docs-manager-260415-1206-ext-error-reporting.md +99 -0
- package/260415-1150-ext-silent-failure-debugging/reports/tester-260415-1159-extension-error-reporting.md +174 -0
- package/CHANGELOG.md +24 -0
- package/dist/web/assets/{_basePickBy-D-bUmjma.js → _basePickBy-Bj0dI1ei.js} +1 -1
- package/dist/web/assets/{_baseUniq-BnXXIfRB.js → _baseUniq-CyzdZeQH.js} +1 -1
- package/dist/web/assets/ai-settings-section-Bo9lCaTd.js +1 -0
- package/dist/web/assets/{api-settings-Qi2xRiHa.js → api-settings-CUxg9RE5.js} +1 -1
- package/dist/web/assets/{arc-DB9vXGzd.js → arc-CxgHJ7Z4.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-DDFO_NKq.js +1 -0
- package/dist/web/assets/{architectureDiagram-2XIMDMQ5-BBV25747.js → architectureDiagram-2XIMDMQ5-D16OotsC.js} +1 -1
- package/dist/web/assets/arrow-up-I9-21gkR.js +1 -0
- package/dist/web/assets/{blockDiagram-WCTKOSBZ-BOTnY2Lq.js → blockDiagram-WCTKOSBZ-Ct57Wtfk.js} +1 -1
- package/dist/web/assets/{c4Diagram-IC4MRINW-D7QAUdHD.js → c4Diagram-IC4MRINW-BIymcNsg.js} +1 -1
- package/dist/web/assets/channel-wumTB1if.js +1 -0
- package/dist/web/assets/chat-tab-R4gKsnxD.js +10 -0
- package/dist/web/assets/chevron-right-DY_wImxB.js +1 -0
- package/dist/web/assets/{chunk-4BX2VUAB-BnOVw77D.js → chunk-4BX2VUAB-CENmY7Kw.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-BftA8DxR.js → chunk-55IACEB6-DhZGI1l3.js} +1 -1
- package/dist/web/assets/{chunk-7E7YKBS2-B0vnP8v3.js → chunk-7E7YKBS2-DZcnC7Ow.js} +1 -1
- package/dist/web/assets/{chunk-7R4GIKGN-Czlaj26D.js → chunk-7R4GIKGN-y8bfHEy-.js} +2 -2
- package/dist/web/assets/{chunk-C72U2L5F-DpEbDtMo.js → chunk-C72U2L5F-BHPkfQj2.js} +1 -1
- package/dist/web/assets/{chunk-EGIJ26TM-BWXe6lkx.js → chunk-EGIJ26TM-nant2LXl.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-DspqhPfk.js → chunk-FMBD7UC4-Bog4cpN-.js} +1 -1
- package/dist/web/assets/{chunk-GEFDOKGD-D6HHRbYk.js → chunk-GEFDOKGD-86LFbsAC.js} +1 -1
- package/dist/web/assets/chunk-GLR3WWYH-Re-5eSlQ.js +2 -0
- package/dist/web/assets/chunk-HHEYEP7N-C45i5G_3.js +1 -0
- package/dist/web/assets/{chunk-JSJVCQXG-BC8wnMwf.js → chunk-JSJVCQXG-23eG9mgt.js} +1 -1
- package/dist/web/assets/{chunk-KX2RTZJC-D3VDtyvX.js → chunk-KX2RTZJC-CHj8TnTB.js} +1 -1
- package/dist/web/assets/{chunk-KYZI473N-Z-NBw_HS.js → chunk-KYZI473N-gqRLpJ4w.js} +1 -1
- package/dist/web/assets/{chunk-L3YUKLVL--RGkEh__.js → chunk-L3YUKLVL-DnSMmNFC.js} +1 -1
- package/dist/web/assets/{chunk-MX3YWQON-2B76t_Kx.js → chunk-MX3YWQON-B6g1ZH9X.js} +1 -1
- package/dist/web/assets/{chunk-NQ4KR5QH-BekY3tEi.js → chunk-NQ4KR5QH-DX32345Y.js} +1 -1
- package/dist/web/assets/{chunk-O4XLMI2P-2CJLfx_1.js → chunk-O4XLMI2P-Vp_V4P-b.js} +1 -1
- package/dist/web/assets/{chunk-OZEHJAEY-sug_L09P.js → chunk-OZEHJAEY-lKq2SWjA.js} +1 -1
- package/dist/web/assets/{chunk-PQ6SQG4A-_fwPRLQy.js → chunk-PQ6SQG4A-Bik13fTV.js} +1 -1
- package/dist/web/assets/{chunk-PU5JKC2W-BUaTFJVQ.js → chunk-PU5JKC2W-DD95Rx35.js} +1 -1
- package/dist/web/assets/chunk-QZHKN3VN-N3VXx1VH.js +1 -0
- package/dist/web/assets/{chunk-R5LLSJPH-C37xW0vj.js → chunk-R5LLSJPH-dRhXRnrb.js} +1 -1
- package/dist/web/assets/{chunk-WL4C6EOR-CCkt_MT6.js → chunk-WL4C6EOR-B1iIvLOG.js} +1 -1
- package/dist/web/assets/{chunk-XIRO2GV7-Dz2LBq7Y.js → chunk-XIRO2GV7-DZBoNl1_.js} +1 -1
- package/dist/web/assets/{chunk-XPW4576I-DenTbBuj.js → chunk-XPW4576I-CgLyyW03.js} +1 -1
- package/dist/web/assets/{chunk-XZSTWKYB-Dbp1nUSQ.js → chunk-XZSTWKYB-DjV8xl5A.js} +1 -1
- package/dist/web/assets/{chunk-YBOYWFTD-3OTKowjE.js → chunk-YBOYWFTD-D_ILLe6_.js} +1 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-mr-Cb1me.js +1 -0
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-BKe8_uda.js +1 -0
- package/dist/web/assets/clone--z5KLAuR.js +1 -0
- package/dist/web/assets/code-editor-Br0vzTOy.js +8 -0
- package/dist/web/assets/columns-2-IeETSfON.js +1 -0
- package/dist/web/assets/conflict-editor-BPgCjnNz.js +19 -0
- package/dist/web/assets/{cose-bilkent-S5V4N54A-MbmGZnt0.js → cose-bilkent-S5V4N54A-BGNPFv3x.js} +1 -1
- package/dist/web/assets/{csv-preview-uZ_7b8I7.js → csv-preview-BZRICDP0.js} +2 -2
- package/dist/web/assets/{dagre-CPhI6v-K.js → dagre-CkhlMHnx.js} +1 -1
- package/dist/web/assets/{dagre-KLK3FWXG-CmSE-oNj.js → dagre-KLK3FWXG-Cnp996VG.js} +1 -1
- package/dist/web/assets/database-CgTomMxt.js +1 -0
- package/dist/web/assets/{database-viewer-5xljX0JI.js → database-viewer-DaUoQ-oR.js} +2 -2
- package/dist/web/assets/{diagram-E7M64L7V-B5XG3ZT7.js → diagram-E7M64L7V-BZF0tSOr.js} +1 -1
- package/dist/web/assets/{diagram-IFDJBPK2-BsP248aX.js → diagram-IFDJBPK2-nUcO8sN8.js} +1 -1
- package/dist/web/assets/{diagram-P4PSJMXO-Cna3408N.js → diagram-P4PSJMXO-CW0eCkwC.js} +1 -1
- package/dist/web/assets/diff-viewer-BzvK3gAE.js +4 -0
- package/dist/web/assets/dist-CM0oD8tQ.js +1 -0
- package/dist/web/assets/{erDiagram-INFDFZHY-B7SgktiR.js → erDiagram-INFDFZHY-DSkriYZ9.js} +1 -1
- package/dist/web/assets/extension-webview-CGepEw-b.js +3 -0
- package/dist/web/assets/{flowDiagram-PKNHOUZH-FOYZZ1OB.js → flowDiagram-PKNHOUZH-CFYAfZBx.js} +1 -1
- package/dist/web/assets/{ganttDiagram-A5KZAMGK-CnHVYh9v.js → ganttDiagram-A5KZAMGK-KSn4XAU4.js} +1 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-OkvBPi6H.js +1 -0
- package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-0G9XxZay.js → gitGraphDiagram-K3NZZRJ6-BMgjjVys.js} +1 -1
- package/dist/web/assets/{graphlib-CNiBwlg_.js → graphlib-BWe1iK_s.js} +1 -1
- package/dist/web/assets/index-CKsEzQ4f.js +26 -0
- package/dist/web/assets/index-Chf0otez.css +2 -0
- package/dist/web/assets/info-3K5VOQVL-BDU2_bYD.js +1 -0
- package/dist/web/assets/infoDiagram-LFFYTUFH-Diq4Cyc3.js +2 -0
- package/dist/web/assets/input-BHj0veau.js +45 -0
- package/dist/web/assets/{isEmpty-CcCb5n2-.js → isEmpty-BfLnxq-B.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-PHBUUO56-D4QCzh5J.js → ishikawaDiagram-PHBUUO56-CiVEvp8o.js} +1 -1
- package/dist/web/assets/{journeyDiagram-4ABVD52K-CnHYNfKW.js → journeyDiagram-4ABVD52K-CG_v5Aho.js} +1 -1
- package/dist/web/assets/jsx-runtime-BRW_vwa9.js +1 -0
- package/dist/web/assets/{kanban-definition-K7BYSVSG-Bh_g3EVu.js → kanban-definition-K7BYSVSG-miB0-_Zq.js} +1 -1
- package/dist/web/assets/keybindings-store-D5zgHod8.js +1 -0
- package/dist/web/assets/{line-6d3eBADm.js → line-CSuSrJ9J.js} +1 -1
- package/dist/web/assets/{linear-cA_2lQy7.js → linear-DFN_MPsw.js} +1 -1
- package/dist/web/assets/{markdown-renderer-CZ07F7T6.js → markdown-renderer-DSYnGywb.js} +6 -6
- package/dist/web/assets/{mermaid-parser.core-C3kd7JXM.js → mermaid-parser.core-CFdP1Z5_.js} +2 -2
- package/dist/web/assets/{mindmap-definition-YRQLILUH-CYiUwhr_.js → mindmap-definition-YRQLILUH-pYPWwASE.js} +1 -1
- package/dist/web/assets/{ordinal-XHK5vIzZ.js → ordinal-DpFn432U.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-BwpIpYB3.js +1 -0
- package/dist/web/assets/pie-UPGHQEXC-BPgAfmes.js +1 -0
- package/dist/web/assets/{pieDiagram-SKSYHLDU-D0S7jeZA.js → pieDiagram-SKSYHLDU-Dovdlvhu.js} +1 -1
- package/dist/web/assets/plus-DQGIb4mQ.js +1 -0
- package/dist/web/assets/port-forwarding-tab-vmqDKmk2.js +1 -0
- package/dist/web/assets/{postgres-viewer-RldlAO_m.js → postgres-viewer-0lIAosrr.js} +3 -3
- package/dist/web/assets/{quadrantDiagram-337W2JSQ-0hNP63hW.js → quadrantDiagram-337W2JSQ-TXe6cU_F.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-TqxBkWx-.js +1 -0
- package/dist/web/assets/refresh-cw-Clk8fdUD.js +1 -0
- package/dist/web/assets/{requirementDiagram-Z7DCOOCP-BVnmqFbL.js → requirementDiagram-Z7DCOOCP-CuiiuGS9.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-DVkYdCJb.js → sankeyDiagram-WA2Y5GQK-BbRmhv0t.js} +1 -1
- package/dist/web/assets/scroll-area-BpXCNme3.js +1 -0
- package/dist/web/assets/{sequenceDiagram-2WXFIKYE-B80s7sOg.js → sequenceDiagram-2WXFIKYE-B2D8IQDb.js} +1 -1
- package/dist/web/assets/settings-tab-CMnv1fce.js +1 -0
- package/dist/web/assets/{sql-query-editor-CjZ7Z6XL.js → sql-query-editor-Bc2hAwqT.js} +1 -1
- package/dist/web/assets/sqlite-viewer-B60MS2Dy.js +1 -0
- package/dist/web/assets/square-vBdqj0bF.js +1 -0
- package/dist/web/assets/{stateDiagram-RAJIS63D-BPLXgXRR.js → stateDiagram-RAJIS63D-ylr4HxPu.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-D6zvxf3M.js +1 -0
- package/dist/web/assets/table-Bi27fEaN.js +1 -0
- package/dist/web/assets/{terminal-tab-DjzD8GLn.js → terminal-tab-CCJoLstH.js} +2 -2
- package/dist/web/assets/text-wrap-D_OmSzhp.js +1 -0
- package/dist/web/assets/{timeline-definition-YZTLITO2-fa_51u1X.js → timeline-definition-YZTLITO2-pMv1grvM.js} +1 -1
- package/dist/web/assets/trash-2-CNuB-htI.js +1 -0
- package/dist/web/assets/treemap-KZPCXAKY-Kck06FKU.js +1 -0
- package/dist/web/assets/{use-monaco-theme-D9XFxQuU.js → use-monaco-theme-BJK48EmK.js} +1 -1
- package/dist/web/assets/{vennDiagram-LZ73GAT5-kX4jJn6W.js → vennDiagram-LZ73GAT5-C-rkIUbo.js} +1 -1
- package/dist/web/assets/x-Dw3TjeY_.js +1 -0
- package/dist/web/assets/{xychartDiagram-JWTSCODW-Bzm5lZBs.js → xychartDiagram-JWTSCODW-CtpjAakO.js} +1 -1
- package/dist/web/index.html +18 -22
- package/dist/web/sw.js +1 -1
- package/docs/codebase-summary.md +169 -13
- package/docs/extension-development-guide.md +98 -1
- package/docs/journals/260414-1400-ext-git-graph-port-complete.md +147 -0
- package/docs/journals/260414-1452-git-graph-faithful-port.md +144 -0
- package/docs/journals/260414-1810-git-graph-ui-improvements-complete.md +261 -0
- package/docs/journals/260414-2001-bundled-extensions.md +219 -0
- package/docs/project-changelog.md +123 -21
- package/docs/project-roadmap.md +4 -2
- package/docs/system-architecture.md +77 -6
- package/package.json +1 -1
- package/packages/ext-git-graph/package.json +30 -0
- package/packages/ext-git-graph/src/extension-integration.test.ts +230 -0
- package/packages/ext-git-graph/src/extension-parsers.test.ts +193 -0
- package/packages/ext-git-graph/src/extension.ts +921 -0
- package/packages/ext-git-graph/src/git-log-parser.test.ts +271 -0
- package/packages/ext-git-graph/src/git-log-parser.ts +38 -0
- package/packages/ext-git-graph/src/types.ts +192 -0
- package/packages/ext-git-graph/src/webview-html.test.ts +142 -0
- package/packages/ext-git-graph/src/webview-html.ts +2417 -0
- package/packages/vscode-compat/src/index.ts +4 -0
- package/packages/vscode-compat/src/process.ts +25 -0
- package/packages/vscode-compat/src/window.ts +10 -0
- package/src/cli/commands/ext-cmd.ts +3 -1
- package/src/server/ws/extensions.ts +34 -4
- package/src/services/contribution-registry.ts +14 -1
- package/src/services/extension-host-worker.ts +12 -3
- package/src/services/extension-manifest.ts +18 -1
- package/src/services/extension-rpc-handlers.ts +68 -2
- package/src/services/extension.service.ts +63 -9
- package/src/types/extension-messages.ts +3 -1
- package/src/types/extension.ts +8 -0
- package/src/web/components/editor/code-editor.tsx +16 -4
- package/src/web/components/editor/conflict-editor.tsx +368 -0
- package/src/web/components/extensions/extension-webview.tsx +153 -12
- package/src/web/components/layout/command-palette.tsx +41 -17
- package/src/web/components/layout/editor-panel.tsx +16 -4
- package/src/web/components/layout/mobile-nav.tsx +6 -5
- package/src/web/components/layout/tab-bar.tsx +3 -3
- package/src/web/components/layout/tab-content.tsx +17 -5
- package/src/web/components/settings/keyboard-shortcuts-section.tsx +46 -1
- package/src/web/hooks/use-extension-ws.ts +30 -4
- package/src/web/hooks/use-global-keybindings.ts +24 -2
- package/src/web/hooks/use-url-sync.ts +8 -3
- package/src/web/stores/extension-store.ts +8 -0
- package/src/web/stores/keybindings-store.ts +2 -3
- package/src/web/stores/panel-store.ts +2 -2
- package/src/web/stores/panel-utils.ts +6 -2
- package/src/web/stores/tab-store.ts +3 -2
- package/dist/web/assets/ai-settings-section-D6d-RmR6.js +0 -1
- package/dist/web/assets/architecture-PBZL5I3N-DpVzOETR.js +0 -1
- package/dist/web/assets/arrow-up-BigIMx-e.js +0 -1
- package/dist/web/assets/channel-Cgy1thYT.js +0 -1
- package/dist/web/assets/chat-tab-DXBb9Y3U.js +0 -10
- package/dist/web/assets/check-ePA3ZvK4.js +0 -1
- package/dist/web/assets/chevron-down-EQA06nR-.js +0 -1
- package/dist/web/assets/chevron-right-CXzzT44u.js +0 -1
- package/dist/web/assets/chunk-GLR3WWYH-CxUl1sdz.js +0 -2
- package/dist/web/assets/chunk-HHEYEP7N-DN7ebS2Y.js +0 -1
- package/dist/web/assets/chunk-QZHKN3VN-C4La7oLj.js +0 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-C3IyfqG-.js +0 -1
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-Dcvhz2pb.js +0 -1
- package/dist/web/assets/clone--C7Tby8z.js +0 -1
- package/dist/web/assets/code-editor-Cr7JrBKC.js +0 -8
- package/dist/web/assets/columns-2-BZ9uqssV.js +0 -1
- package/dist/web/assets/createLucideIcon-PuMiQgHl.js +0 -1
- package/dist/web/assets/database-D1ToEV9d.js +0 -1
- package/dist/web/assets/diff-viewer-BBr6e_gb.js +0 -4
- package/dist/web/assets/dist-KUoHa6tg.js +0 -1
- package/dist/web/assets/extension-webview-B0klBip8.js +0 -3
- package/dist/web/assets/eye-CNcBU6Tx.js +0 -1
- package/dist/web/assets/git-graph-CDiwGa0g.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-DcPyMEIJ.js +0 -1
- package/dist/web/assets/index-CkaCzNgO.css +0 -2
- package/dist/web/assets/index-Ic5uTu20.js +0 -26
- package/dist/web/assets/info-3K5VOQVL-Dw4O15cw.js +0 -1
- package/dist/web/assets/infoDiagram-LFFYTUFH-DFhmsucr.js +0 -2
- package/dist/web/assets/input-CcbTF6ih.js +0 -45
- package/dist/web/assets/jsx-runtime-R_NjdZtX.js +0 -1
- package/dist/web/assets/keybindings-store-CxE6BlG2.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-o3LmdL8H.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-BjNP0M3B.js +0 -1
- package/dist/web/assets/plus-Iso5r9vD.js +0 -1
- package/dist/web/assets/port-forwarding-tab-BPuSc6pI.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-gDgOiaME.js +0 -1
- package/dist/web/assets/refresh-cw-BgQzFNaG.js +0 -1
- package/dist/web/assets/scroll-area-i4EZlOl_.js +0 -1
- package/dist/web/assets/settings-tab-BzSSN2BQ.js +0 -1
- package/dist/web/assets/sqlite-viewer-CoyZOM_Y.js +0 -1
- package/dist/web/assets/square-pfn_LYYy.js +0 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-DksQJ7es.js +0 -1
- package/dist/web/assets/table-CHv2x_qg.js +0 -1
- package/dist/web/assets/tag-Bb_UFXt0.js +0 -1
- package/dist/web/assets/text-wrap-D8BbQYTx.js +0 -1
- package/dist/web/assets/trash-2-DYCa06CV.js +0 -1
- package/dist/web/assets/treemap-KZPCXAKY-DwFqAvnj.js +0 -1
- package/dist/web/assets/x-BXecj-16.js +0 -1
- package/src/web/components/git/git-graph-branch-label.tsx +0 -124
- package/src/web/components/git/git-graph-constants.ts +0 -185
- package/src/web/components/git/git-graph-detail.tsx +0 -107
- package/src/web/components/git/git-graph-dialog.tsx +0 -72
- package/src/web/components/git/git-graph-row.tsx +0 -167
- package/src/web/components/git/git-graph-settings-dialog.tsx +0 -104
- package/src/web/components/git/git-graph-svg.tsx +0 -54
- package/src/web/components/git/git-graph-toolbar.tsx +0 -195
- package/src/web/components/git/git-graph.tsx +0 -193
- package/src/web/components/git/use-column-resize.ts +0 -33
- package/src/web/components/git/use-git-graph.ts +0 -201
- /package/dist/web/assets/{api-client-wQbeUyeh.js → api-client-BvxmRZUi.js} +0 -0
- /package/dist/web/assets/{array-X0JlPOfd.js → array-BFDiaBgf.js} +0 -0
- /package/dist/web/assets/{csv-parser-CElqio6o.js → csv-parser-i7fjqP2H.js} +0 -0
- /package/dist/web/assets/{cytoscape.esm-BfIOPvwt.js → cytoscape.esm-C8i2jUzT.js} +0 -0
- /package/dist/web/assets/{defaultLocale-B6RGN4id.js → defaultLocale-ZeknFqNe.js} +0 -0
- /package/dist/web/assets/{dist-CK1enexV.js → dist-DZmJeHOA.js} +0 -0
- /package/dist/web/assets/{init-BmUWJJHz.js → init-0VJVrkRJ.js} +0 -0
- /package/dist/web/assets/{isArrayLikeObject-BrCM-iA1.js → isArrayLikeObject-ClzWCpcm.js} +0 -0
- /package/dist/web/assets/{katex-xQS_6bNb.js → katex-DR0kdMDv.js} +0 -0
- /package/dist/web/assets/{lib-CfWBrYll.js → lib-DSLzfeW0.js} +0 -0
- /package/dist/web/assets/{math-CpLFzrfV.js → math-CRc16Nj6.js} +0 -0
- /package/dist/web/assets/{path-CoPyR7c2.js → path-INs8XTPH.js} +0 -0
- /package/dist/web/assets/{preload-helper-CH6UZRzu.js → preload-helper-mr3rCizq.js} +0 -0
- /package/dist/web/assets/{react-j5zqhEum.js → react-0tkk-ztn.js} +0 -0
- /package/dist/web/assets/{rough.esm-D5NinLFK.js → rough.esm-eLccZ4OJ.js} +0 -0
- /package/dist/web/assets/{sql-completion-provider-D0xutVaK.js → sql-completion-provider-B8uUWWej.js} +0 -0
- /package/dist/web/assets/{src-j04igtQ5.js → src-CqyWLlNZ.js} +0 -0
- /package/dist/web/assets/{utils-CSCvNZxE.js → utils-DX8jb5qv.js} +0 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# Code Review: Git Graph — Stash, Rebase, Conflicts
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-04-15
|
|
4
|
+
**Reviewer:** code-reviewer
|
|
5
|
+
**Scope:** 10 files changed, ~600 LOC net new
|
|
6
|
+
|
|
7
|
+
## Scope
|
|
8
|
+
|
|
9
|
+
| File | Role |
|
|
10
|
+
|------|------|
|
|
11
|
+
| `packages/ext-git-graph/src/types.ts` | Type definitions |
|
|
12
|
+
| `packages/ext-git-graph/src/extension.ts` | Backend: stash/merge-state/conflict handlers |
|
|
13
|
+
| `packages/ext-git-graph/src/webview-html.ts` | Webview: stash popover, merge banner, conflict UI |
|
|
14
|
+
| `src/web/components/editor/conflict-editor.tsx` | NEW: Monaco conflict resolution editor |
|
|
15
|
+
| `src/web/stores/tab-store.ts` | TabType union |
|
|
16
|
+
| `src/web/stores/panel-utils.ts` | `deriveTabId` for conflict-editor |
|
|
17
|
+
| `src/web/components/layout/editor-panel.tsx` | TAB_COMPONENTS registration |
|
|
18
|
+
| `src/web/components/layout/tab-content.tsx` | TAB_COMPONENTS registration |
|
|
19
|
+
| `src/web/components/layout/mobile-nav.tsx` | Icon mapping |
|
|
20
|
+
| `src/web/components/layout/tab-bar.tsx` | Icon mapping |
|
|
21
|
+
|
|
22
|
+
## Overall Assessment
|
|
23
|
+
|
|
24
|
+
Well-structured feature addition. Security practices (assertSafeFilePaths, assertValidRef, escHtml) are consistently applied to the new surface area. Three blocking issues, four informational.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Critical Issues
|
|
29
|
+
|
|
30
|
+
### C1 — `detectMergeState` hardcodes `.git` path, breaks for worktrees
|
|
31
|
+
|
|
32
|
+
**File:** `extension.ts:531-562`
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
const rebaseMergeDir = `${projectPath}/.git/rebase-merge`;
|
|
36
|
+
const checkMerge = await vscode.process.spawn("test", ["-f", `${projectPath}/.git/MERGE_HEAD`], ...);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
For git worktrees, `.git` inside the worktree directory is a **file** (containing `gitdir: /path/to/main/.git/worktrees/<name>`), not a directory. The rebase/merge state is stored under `$GIT_DIR/rebase-merge`, where `$GIT_DIR` for a worktree is `<main>/.git/worktrees/<name>`. The current hardcoded path will always fail `test -d` / `test -f` for worktrees, silently returning `undefined` for `mergeState`.
|
|
40
|
+
|
|
41
|
+
The PPM app already heavily uses worktrees (worktree CRUD is a major feature of this same extension). Merge conflicts inside a worktree will show files as conflicted but the banner will never appear.
|
|
42
|
+
|
|
43
|
+
**Fix:** Use `git rev-parse --git-dir` to get the actual `GIT_DIR`, then construct paths from that:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
async function detectMergeState(vscode, projectPath) {
|
|
47
|
+
const gitDirResult = await spawnGit(vscode, ["rev-parse", "--git-dir"], projectPath, { timeout: 2000 });
|
|
48
|
+
if (gitDirResult.exitCode !== 0) return undefined;
|
|
49
|
+
const gitDir = gitDirResult.stdout.trim();
|
|
50
|
+
const absGitDir = gitDir.startsWith("/") ? gitDir : `${projectPath}/${gitDir}`;
|
|
51
|
+
|
|
52
|
+
const checkRebase = await vscode.process.spawn("test", ["-d", `${absGitDir}/rebase-merge`], projectPath, { timeout: 2000 });
|
|
53
|
+
// ... rest of checks use absGitDir instead of `${projectPath}/.git`
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
### C2 — `getDisplayCommits` excludes virtual row for conflict-only state
|
|
60
|
+
|
|
61
|
+
**File:** `webview-html.ts:1382-1383`
|
|
62
|
+
|
|
63
|
+
```javascript
|
|
64
|
+
function getDisplayCommits() {
|
|
65
|
+
const u = state.uncommitted;
|
|
66
|
+
if (!u || (u.staged.length === 0 && u.unstaged.length === 0)) return state.commits;
|
|
67
|
+
// virtual row only added if staged or unstaged files exist
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
When a merge conflict exists but no staged/unstaged changes (pure merge conflict during `git merge`), `conflicted.length > 0` but `staged.length === 0 && unstaged.length === 0`. The virtual uncommitted row is **not added to the commit list**. The user sees the merge banner but cannot click into the uncommitted detail panel to reach the conflict file list and the "open conflict editor" button.
|
|
71
|
+
|
|
72
|
+
The conflict panel is properly rendered in `renderUncommittedDetail` (checks `u.conflicted`) but is unreachable because `selectCommit('uncommitted')` is only triggered by clicking the virtual row.
|
|
73
|
+
|
|
74
|
+
**Fix:**
|
|
75
|
+
|
|
76
|
+
```javascript
|
|
77
|
+
if (!u || (u.staged.length === 0 && u.unstaged.length === 0 && (!u.conflicted || u.conflicted.length === 0)))
|
|
78
|
+
return state.commits;
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
And update the virtual commit message:
|
|
82
|
+
```javascript
|
|
83
|
+
const totalFiles = u.staged.length + u.unstaged.length + (u.conflicted?.length || 0);
|
|
84
|
+
message: `Uncommitted Changes (${totalFiles} files)${u.conflicted?.length ? ' ⚠ conflicts' : ''}`,
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
### C3 — Style injection in `ConflictEditor.handleMount` is not idempotent
|
|
90
|
+
|
|
91
|
+
**File:** `conflict-editor.tsx:286-301`
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
const handleMount: OnMount = (editor, monaco) => {
|
|
95
|
+
// ...
|
|
96
|
+
const styleEl = document.createElement("style");
|
|
97
|
+
styleEl.textContent = `...conflict styles...`;
|
|
98
|
+
editor.getDomNode()?.ownerDocument?.head?.appendChild(styleEl);
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`handleMount` fires each time the Monaco editor mounts. If the conflict editor tab is opened, closed, and reopened, the style tag accumulates in `<head>`. This doesn't cause broken behavior (CSS rules are idempotent) but leaks DOM nodes.
|
|
102
|
+
|
|
103
|
+
**Fix:** Add an ID guard:
|
|
104
|
+
```typescript
|
|
105
|
+
const DOC_STYLE_ID = "conflict-editor-styles";
|
|
106
|
+
if (!editor.getDomNode()?.ownerDocument?.getElementById(DOC_STYLE_ID)) {
|
|
107
|
+
const styleEl = document.createElement("style");
|
|
108
|
+
styleEl.id = DOC_STYLE_ID;
|
|
109
|
+
styleEl.textContent = `...`;
|
|
110
|
+
editor.getDomNode()?.ownerDocument?.head?.appendChild(styleEl);
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## High Priority
|
|
117
|
+
|
|
118
|
+
### H1 — Content widget removal uses an invalid fake object
|
|
119
|
+
|
|
120
|
+
**File:** `conflict-editor.tsx:145-148`
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
for (const wid of widgetIdsRef.current) {
|
|
124
|
+
const w = editor.getLayoutInfo() && { getId: () => wid } as MonacoType.editor.IContentWidget;
|
|
125
|
+
try { editor.removeContentWidget(w); } catch { /* ignore */ }
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`editor.getLayoutInfo()` always returns a truthy object when the editor is alive; `&&` effectively just evaluates to `{ getId: () => wid }`. Monaco's `removeContentWidget` accepts `IContentWidget`, which requires `getDomNode()` and `getPosition()` methods. Monaco's implementation internally looks up widgets by their ID string, so this happens to work — but it is type-unsafe and relies on Monaco's internal implementation detail.
|
|
130
|
+
|
|
131
|
+
The silent `try/catch` hides any future Monaco version breaking this. The real widgets are never stored, so they cannot be properly removed.
|
|
132
|
+
|
|
133
|
+
**Fix:** Store actual widget references:
|
|
134
|
+
```typescript
|
|
135
|
+
const widgetsRef = useRef<MonacoType.editor.IContentWidget[]>([]);
|
|
136
|
+
|
|
137
|
+
// In refreshConflicts — removal:
|
|
138
|
+
for (const w of widgetsRef.current) {
|
|
139
|
+
editor.removeContentWidget(w);
|
|
140
|
+
}
|
|
141
|
+
widgetsRef.current = [];
|
|
142
|
+
|
|
143
|
+
// When adding:
|
|
144
|
+
editor.addContentWidget(widget);
|
|
145
|
+
widgetsRef.current.push(widget);
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
### H2 — `ResizeObserver` re-subscribes unnecessarily on `loading`/`error` changes
|
|
151
|
+
|
|
152
|
+
**File:** `conflict-editor.tsx:109-117`
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
const el = containerRef.current;
|
|
157
|
+
if (!el) return;
|
|
158
|
+
const ro = new ResizeObserver(...);
|
|
159
|
+
ro.observe(el);
|
|
160
|
+
return () => ro.disconnect();
|
|
161
|
+
}, [loading, error]); // ← problem
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The dependency array `[loading, error]` causes the `ResizeObserver` to be torn down and recreated every time loading/error state changes. The intent is to observe after the container renders (post-loading). However, the container `<div ref={containerRef}>` is always mounted (it wraps the Monaco editor), so `containerRef.current` is stable. The correct approach is `[]` (observe once on mount):
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
}, []); // containerRef is stable; re-observe not needed on state changes
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Medium Priority
|
|
173
|
+
|
|
174
|
+
### M1 — `acceptConflict` resolves by region ID but re-parses the full file
|
|
175
|
+
|
|
176
|
+
**File:** `conflict-editor.tsx:224-263`
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
const acceptConflict = useCallback((regionId: number, ...) => {
|
|
180
|
+
const value = model.getValue();
|
|
181
|
+
const regions = parseConflicts(value);
|
|
182
|
+
const region = regions.find((r) => r.id === regionId);
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Region IDs are assigned sequentially (0, 1, 2, ...) in `parseConflicts`. After resolving region `id=0`, the model is updated, then `refreshConflicts` re-parses — and the remaining regions are now renumbered starting from 0. If the user clicks "Accept Current" on region 1 (now rendered as `conflict-widget-1`), then resolves region 0 without refreshing, the ID lookup will still find the correct region because `acceptConflict` re-parses the current model state. This is actually correct.
|
|
186
|
+
|
|
187
|
+
However, the `setTimeout(() => refreshConflicts(), 50)` is a fragile way to wait for model stabilization. Monaco's `pushEditOperations` is synchronous; the model is updated before the call returns. `refreshConflicts` can be called directly:
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
saveFile(model.getValue());
|
|
191
|
+
refreshConflicts(); // synchronous - no timeout needed
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### M2 — Merge banner `action.includes('Abort')` is fragile string matching
|
|
195
|
+
|
|
196
|
+
**File:** `webview-html.ts:927`
|
|
197
|
+
|
|
198
|
+
```javascript
|
|
199
|
+
const action = btn.dataset.mergeAction;
|
|
200
|
+
if (action.includes('Abort')) {
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
This works for the current action names (`rebaseAbort`, `mergeAbort`, `cherryPickAbort`) but is fragile if new actions are added. Prefer an explicit set:
|
|
204
|
+
|
|
205
|
+
```javascript
|
|
206
|
+
const ABORT_ACTIONS = new Set(['rebaseAbort', 'mergeAbort', 'cherryPickAbort']);
|
|
207
|
+
if (ABORT_ACTIONS.has(action)) {
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### M3 — Stash popover and worktree popover do not close each other
|
|
211
|
+
|
|
212
|
+
**File:** `webview-html.ts:763-764, 953-954`
|
|
213
|
+
|
|
214
|
+
Two separate `document.addEventListener('click', ...)` handlers: one closes the worktree popover when clicking outside `.worktree-dropdown`, another closes the stash popover when clicking outside `.stash-dropdown`. But clicking the stash button while the worktree popover is open does NOT close the worktree popover (the stash button is inside `.stash-dropdown`, not `.worktree-dropdown`, so the worktree handler runs and closes it). Actually this works correctly by accident — the worktree click handler fires on every click and checks `closest('.worktree-dropdown')`.
|
|
215
|
+
|
|
216
|
+
Testing shows this is fine, but it's worth adding a mutual close for clarity:
|
|
217
|
+
```javascript
|
|
218
|
+
btnStash.addEventListener('click', (e) => {
|
|
219
|
+
wtPopover.classList.add('hidden'); // close sibling
|
|
220
|
+
// ...
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Low Priority
|
|
227
|
+
|
|
228
|
+
### L1 — `conflict-editor.tsx`: `filePath.split("/")` is not cross-platform
|
|
229
|
+
|
|
230
|
+
**File:** `conflict-editor.tsx:306`
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
const fileName = filePath?.split("/").pop() ?? "unknown";
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
On Windows paths use `\`. Prefer a regex: `.split(/[\\/]/).pop()`. (Consistent with the extension's existing pattern.)
|
|
237
|
+
|
|
238
|
+
### L2 — Empty conflict resolution leaves a blank line
|
|
239
|
+
|
|
240
|
+
When "Accept Current" or "Accept Incoming" is used on a conflict where one side is empty (e.g., `<<<<<<< HEAD\n=======\n>>>>>>> branch`), `replacement` = `""` and `text: "" + "\n"` inserts a single blank line. Minor but can leave stray newlines in the file. Low impact.
|
|
241
|
+
|
|
242
|
+
### L3 — `parseConflicts` doesn't handle diff3-style conflicts
|
|
243
|
+
|
|
244
|
+
`git` can be configured to use `diff3` style which adds a base section between `<<<` and `===`. The parser looks for `=======` but diff3 adds `||||||| base` between `<<<<` and `=====`. In diff3 mode, the parser would put the base content into `currentContent` and miss the real separator. Not broken but results in "Accept Current" keeping the diff3 base section too.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Edge Cases Found
|
|
249
|
+
|
|
250
|
+
1. **Conflict-only state (C2 above):** No virtual uncommitted row = no access to conflict file list. Blocking.
|
|
251
|
+
2. **Worktree rebase detection (C1 above):** Hardcoded `.git/rebase-merge` fails for all worktrees.
|
|
252
|
+
3. **Empty conflict sides:** Resolving an empty-vs-content conflict inserts a blank line (L2).
|
|
253
|
+
4. **diff3 style conflicts:** Parser misidentifies `||||||| base` as current content (L3).
|
|
254
|
+
5. **stash popover and worktree popover open simultaneously:** Both can be visible together until a click. Minor UX.
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Positive Observations
|
|
259
|
+
|
|
260
|
+
- `assertSafeFilePaths` is applied to `openConflictFile` consistently.
|
|
261
|
+
- `escHtml` is applied to all user-visible git data in the webview (stash messages, branch names, file paths).
|
|
262
|
+
- `assertValidRef` correctly blocks the commit hash passed to `rebase` from the context menu (no injection possible).
|
|
263
|
+
- Stash message parsing correctly handles pipe characters in stash messages via `.slice(2).join("|")`.
|
|
264
|
+
- The 500-file limit in `handleUncommittedStatus` applies globally, preventing unbounded memory use.
|
|
265
|
+
- ResizeObserver cleanup (`return () => ro.disconnect()`) is present.
|
|
266
|
+
- Git stash index is always a safe integer (enforced by both parsing paths).
|
|
267
|
+
- Stash drop/abort operations correctly use confirmation dialogs.
|
|
268
|
+
- `rebaseContinue`/`rebaseAbort`/`mergeAbort` correctly added to `buildGitActionArgs` with no extra args needed (safe).
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Recommended Actions
|
|
273
|
+
|
|
274
|
+
1. **[C1 — BLOCK]** Fix `detectMergeState` to use `git rev-parse --git-dir` before constructing paths.
|
|
275
|
+
2. **[C2 — BLOCK]** Fix `getDisplayCommits` to include virtual row when `conflicted.length > 0`.
|
|
276
|
+
3. **[C3 — BLOCK]** Add ID guard to style injection in `handleMount`.
|
|
277
|
+
4. **[H1]** Store actual widget references instead of fake objects for `removeContentWidget`.
|
|
278
|
+
5. **[H2]** Change ResizeObserver dependency to `[]`.
|
|
279
|
+
6. **[M1]** Remove `setTimeout` before `refreshConflicts` call in `acceptConflict`.
|
|
280
|
+
7. **[M2]** Replace `action.includes('Abort')` with a `Set` check.
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Unresolved Questions
|
|
285
|
+
|
|
286
|
+
1. Is the git graph webview ever shown for a worktree project path? If not, C1 is lower priority, but the worktree feature exists so it should be assumed yes.
|
|
287
|
+
2. Is diff3 conflict style expected to be supported? If users have `merge.conflictstyle=diff3` in their gitconfig, the parser will silently produce wrong results.
|
|
288
|
+
3. Should "Accept Both" include a separator between current and incoming content? Some editors insert `--- current ---` / `--- incoming ---` comments when accepting both.
|
package/260415-0932-git-graph-stash-rebase-conflicts/reports/tester-260415-1020-build-check.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Tester Report — Build & Test Check
|
|
2
|
+
**Date:** 2026-04-15
|
|
3
|
+
**Scope:** git-graph extension + conflict editor component changes
|
|
4
|
+
**Mode:** Diff-aware
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Diff-Aware Mode
|
|
9
|
+
|
|
10
|
+
Analyzed 10 changed files:
|
|
11
|
+
|
|
12
|
+
**Changed:**
|
|
13
|
+
- `packages/ext-git-graph/src/types.ts`
|
|
14
|
+
- `packages/ext-git-graph/src/extension.ts`
|
|
15
|
+
- `packages/ext-git-graph/src/webview-html.ts`
|
|
16
|
+
- `src/web/components/editor/conflict-editor.tsx` (NEW)
|
|
17
|
+
- `src/web/stores/tab-store.ts`
|
|
18
|
+
- `src/web/stores/panel-utils.ts`
|
|
19
|
+
- `src/web/components/layout/editor-panel.tsx`
|
|
20
|
+
- `src/web/components/layout/tab-content.tsx`
|
|
21
|
+
- `src/web/components/layout/mobile-nav.tsx`
|
|
22
|
+
- `src/web/components/layout/tab-bar.tsx`
|
|
23
|
+
|
|
24
|
+
**Mapped → Tests (Strategy A/Co-located):**
|
|
25
|
+
- `packages/ext-git-graph/src/extension-parsers.test.ts`
|
|
26
|
+
- `packages/ext-git-graph/src/git-log-parser.test.ts`
|
|
27
|
+
- `packages/ext-git-graph/src/extension-integration.test.ts`
|
|
28
|
+
- `packages/ext-git-graph/src/webview-html.test.ts`
|
|
29
|
+
|
|
30
|
+
**Unmapped (no test files found):**
|
|
31
|
+
- `src/web/components/editor/conflict-editor.tsx`
|
|
32
|
+
- `src/web/stores/tab-store.ts`
|
|
33
|
+
- `src/web/stores/panel-utils.ts`
|
|
34
|
+
- `src/web/components/layout/editor-panel.tsx`
|
|
35
|
+
- `src/web/components/layout/tab-content.tsx`
|
|
36
|
+
- `src/web/components/layout/mobile-nav.tsx`
|
|
37
|
+
- `src/web/components/layout/tab-bar.tsx`
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## TypeScript Check
|
|
42
|
+
|
|
43
|
+
**Result: PASS** — only 3 known pre-existing errors in:
|
|
44
|
+
- `src/providers/claude-agent-sdk.ts` (TS2322 `"session_migrated"` type mismatch)
|
|
45
|
+
- `src/services/upgrade.service.ts` (TS2532 x2, possibly undefined)
|
|
46
|
+
|
|
47
|
+
**No new TypeScript errors introduced by the changes.**
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Build
|
|
52
|
+
|
|
53
|
+
**Result: PASS** — `bun run build:web` succeeded in 851ms, 4314 modules transformed.
|
|
54
|
+
|
|
55
|
+
Warnings (pre-existing, not new):
|
|
56
|
+
- `[INEFFECTIVE_DYNAMIC_IMPORT]` for `keybindings-store.ts` and `settings-tab.tsx` — dynamic imports ineffective due to static imports elsewhere
|
|
57
|
+
- Large chunks (>500kB): `index-D4xwwuhE.js` (544kB), `markdown-renderer` (794kB) — pre-existing
|
|
58
|
+
|
|
59
|
+
Confirms `conflict-editor.tsx` was successfully bundled: `conflict-editor-BIAHaSMw.js` (6.89kB / 2.83kB gzip).
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Test Results
|
|
64
|
+
|
|
65
|
+
**Diff-targeted (ext-git-graph):**
|
|
66
|
+
```
|
|
67
|
+
Ran 62 tests across 4 files — 62 pass, 0 fail [2.43s]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Full suite:**
|
|
71
|
+
```
|
|
72
|
+
Ran 1587 tests across 99 files — 1569 pass, 5 fail [204.80s]
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Failing Tests (all pre-existing, unrelated to changed files)
|
|
76
|
+
|
|
77
|
+
| Test | File | Duration | Root Cause |
|
|
78
|
+
|------|------|----------|------------|
|
|
79
|
+
| Cloud WS Client > queues messages when disconnected... | `tests/integration/cloud-ws-client.test.ts` | 3098ms | Timing/reconnect flakiness |
|
|
80
|
+
| Cloud WS Client > invokes command handler on inbound command | `tests/integration/cloud-ws-client.test.ts` | 3071ms | Timing/reconnect flakiness |
|
|
81
|
+
| Logs endpoint > GET /api/logs/recent returns last log lines | `tests/integration/api/server-health-logs.test.ts` | 1ms | Log file path issue |
|
|
82
|
+
| Logs endpoint > GET /api/logs/recent redacts sensitive data | `tests/integration/api/server-health-logs.test.ts` | 0.25ms | Log file path issue |
|
|
83
|
+
| Logs endpoint > GET /api/logs/recent returns empty when no log file | `tests/integration/api/server-health-logs.test.ts` | 0.3ms | Log file path issue |
|
|
84
|
+
|
|
85
|
+
All 5 failures exist on `main` before these changes — none of those test files were modified by the current changeset.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Coverage Gaps (Unmapped Files)
|
|
90
|
+
|
|
91
|
+
[!] No tests found for `src/web/components/editor/conflict-editor.tsx` — NEW component with no test coverage. Consider adding tests for:
|
|
92
|
+
- Conflict block parsing and rendering (ours/theirs/base sections)
|
|
93
|
+
- "Accept ours / Accept theirs" action handlers
|
|
94
|
+
- Keyboard navigation between conflict markers
|
|
95
|
+
|
|
96
|
+
[!] No tests found for `src/web/stores/tab-store.ts` — critical state management with no unit tests. Consider:
|
|
97
|
+
- Tab open/close/switch transitions
|
|
98
|
+
- Conflict editor tab type handling
|
|
99
|
+
|
|
100
|
+
[!] No tests found for `src/web/stores/panel-utils.ts`, `editor-panel.tsx`, `tab-content.tsx`, `mobile-nav.tsx`, `tab-bar.tsx` — consistent with project pattern (no frontend component tests exist).
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Summary
|
|
105
|
+
|
|
106
|
+
| Check | Result |
|
|
107
|
+
|-------|--------|
|
|
108
|
+
| TypeScript (`bunx tsc --noEmit`) | PASS — no new errors |
|
|
109
|
+
| Vite build (`bun run build:web`) | PASS |
|
|
110
|
+
| ext-git-graph unit tests | 62/62 pass |
|
|
111
|
+
| Full test suite | 1569/1574 pass (5 pre-existing failures) |
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
**Unresolved Questions:**
|
|
116
|
+
1. The 2 `cloud-ws-client` failures are timing-sensitive — are they known flaky tests scheduled for fix?
|
|
117
|
+
2. The 3 `server-health-logs` failures suggest log file path misconfiguration in test env — is this being tracked?
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Code Review: Extension Error Reporting & Logging
|
|
2
|
+
|
|
3
|
+
## Scope
|
|
4
|
+
- **Files**: 7 (extension.service.ts, ws/extensions.ts, extension-store.ts, use-extension-ws.ts, extension-webview.tsx, extension-host-worker.ts, ext-git-graph/extension.ts)
|
|
5
|
+
- **LOC changed**: ~270 additions across error tracking, toast notifications, breadcrumb logs, stash/conflict features
|
|
6
|
+
- **Focus**: Error propagation, type safety, memory, race conditions, toast spam
|
|
7
|
+
|
|
8
|
+
## Overall Assessment
|
|
9
|
+
|
|
10
|
+
Solid improvement to extension debugging. The silent failure path is now surfaced end-to-end: activation errors stored server-side, sent to clients on connect and broadcast, displayed in webview UI with retry. Breadcrumb logs added at each layer. A few issues need attention.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Critical Issues
|
|
15
|
+
|
|
16
|
+
None found.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## High Priority
|
|
21
|
+
|
|
22
|
+
### H1. Type bypass: `activationErrors` piggybacked outside `ExtServerMsg` union
|
|
23
|
+
|
|
24
|
+
**Files**: `extensions.ts:78-79`, `use-extension-ws.ts:43-44`, `extension.service.ts:269-272`
|
|
25
|
+
|
|
26
|
+
The `activationErrors` field is added to `contributions:update` messages via `Record<string, unknown>` or `(msg as any).activationErrors`, bypassing the `ExtServerMsg` discriminated union type. The type definition at `extension-messages.ts:54` does not include this field.
|
|
27
|
+
|
|
28
|
+
**Impact**: Any future refactor touching `ExtServerMsg` won't catch this field. TypeScript provides zero safety on the consumer side. If the field name changes server-side, the client silently receives nothing.
|
|
29
|
+
|
|
30
|
+
**Fix**: Extend the union member:
|
|
31
|
+
```ts
|
|
32
|
+
// extension-messages.ts
|
|
33
|
+
| { type: "contributions:update"; contributions: ExtensionContributes; activationErrors?: Record<string, string> }
|
|
34
|
+
```
|
|
35
|
+
Then remove all `(msg as any).activationErrors` casts and `Record<string, unknown>` intermediate types.
|
|
36
|
+
|
|
37
|
+
### H2. Toast spam on every `contributions:update` broadcast
|
|
38
|
+
|
|
39
|
+
**File**: `use-extension-ws.ts:43-49`
|
|
40
|
+
|
|
41
|
+
Every `contributions:update` with `activationErrors` fires `toast.error()` for each error entry. This message is broadcast on every `activate()` AND `deactivate()` call (both call `broadcastContributions()`). If 3 extensions fail on startup and then any extension activates/deactivates later, all 3 error toasts fire again.
|
|
42
|
+
|
|
43
|
+
**Impact**: Users see repeated error toasts for errors they already acknowledged. With N failing extensions and M subsequent operations, toast count = N * M.
|
|
44
|
+
|
|
45
|
+
**Fix**: Track which errors have already been toasted (e.g., compare previous `activationErrors` keys before showing new toasts):
|
|
46
|
+
```ts
|
|
47
|
+
case "contributions:update":
|
|
48
|
+
store.setContributions(msg.contributions);
|
|
49
|
+
if (msg.activationErrors) {
|
|
50
|
+
const prev = store.activationErrors;
|
|
51
|
+
const errors = msg.activationErrors;
|
|
52
|
+
store.setActivationErrors(errors);
|
|
53
|
+
// Only toast NEW errors
|
|
54
|
+
for (const [extId, error] of Object.entries(errors)) {
|
|
55
|
+
if (!prev[extId]) {
|
|
56
|
+
toast.error(`Extension "${extId}" failed to activate: ${error}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### H3. `activationErrors` Map never cleared on `terminateWorker()` / `shutdown()`
|
|
64
|
+
|
|
65
|
+
**File**: `extension.service.ts:55-63`
|
|
66
|
+
|
|
67
|
+
`terminateWorker()` clears `activatedIds`, `extensionPaths`, `bundledIds` but not `activationErrors`. After a shutdown + restart cycle, stale errors from the previous session persist and get broadcast to new clients.
|
|
68
|
+
|
|
69
|
+
**Fix**: Add `this.activationErrors.clear()` inside `terminateWorker()`.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Medium Priority
|
|
74
|
+
|
|
75
|
+
### M1. Retry loop fires commands indefinitely until panel appears
|
|
76
|
+
|
|
77
|
+
**File**: `extension-webview.tsx:86-94`
|
|
78
|
+
|
|
79
|
+
The `setInterval` fires `ext:command:execute` every 2 seconds with no upper bound (except the 10s timeout that just shows an error message, but does NOT stop the interval). The cleanup function only runs on unmount or dependency change (`[panel, viewType, projectName]`).
|
|
80
|
+
|
|
81
|
+
If the extension is broken (e.g., activation failed), this dispatches a command every 2s forever while the tab is open.
|
|
82
|
+
|
|
83
|
+
**Impact**: Wasted network/CPU; if the command triggers server-side side effects (e.g., webview creation), could cause resource leaks.
|
|
84
|
+
|
|
85
|
+
**Fix**: Cap retries (e.g., 5 attempts) or clear the interval when `timedOut` becomes true:
|
|
86
|
+
```ts
|
|
87
|
+
let attempts = 0;
|
|
88
|
+
const retryTimer = setInterval(() => {
|
|
89
|
+
if (!cancelled && attempts++ < 5) attempt();
|
|
90
|
+
else clearInterval(retryTimer);
|
|
91
|
+
}, 2_000);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### M2. `activationError` matching in extension-webview.tsx is fragile
|
|
95
|
+
|
|
96
|
+
**File**: `extension-webview.tsx:122-128`
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
if (extId.includes(viewType) || viewType.includes(extId.replace(/^ext-/, "")))
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
This substring matching can produce false positives. Example: viewType `"graph"` would match extId `"ext-git-graph"` and also hypothetically `"ext-graph-viz"`, `"ext-photography"`. Also, an extId like `"ext-editor"` stripped to `"editor"` would match a viewType of `"editor-settings"`.
|
|
103
|
+
|
|
104
|
+
**Fix**: Use exact matching or a registry-based lookup:
|
|
105
|
+
```ts
|
|
106
|
+
const extensionId = metadata?.extensionId as string | undefined;
|
|
107
|
+
const activationError = useExtensionStore((s) =>
|
|
108
|
+
extensionId ? s.activationErrors[extensionId] : undefined
|
|
109
|
+
);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### M3. `detectMergeState` path construction not Windows-safe
|
|
113
|
+
|
|
114
|
+
**File**: `ext-git-graph/extension.ts:543`
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
if (!gitDir.startsWith("/")) gitDir = `${projectPath}/${gitDir}`;
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
On Windows, git returns paths with backslashes or drive letters (e.g., `C:\...`). The `startsWith("/")` check fails. String concatenation with `/` produces mixed separators.
|
|
121
|
+
|
|
122
|
+
Per project memory (`feedback_cross_platform_paths.md`): file/path features must work on Windows too.
|
|
123
|
+
|
|
124
|
+
**Fix**: Use `path.resolve()` or `path.isAbsolute()`:
|
|
125
|
+
```ts
|
|
126
|
+
const path = require("path");
|
|
127
|
+
if (!path.isAbsolute(gitDir)) gitDir = path.resolve(projectPath, gitDir);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### M4. `getActivationErrors()` returns mutable reference
|
|
131
|
+
|
|
132
|
+
**File**: `extension.service.ts:263`
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
getActivationErrors(): Map<string, string> { return this.activationErrors; }
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Callers (ws/extensions.ts) get a direct reference to the internal Map. If any consumer mutates it, the service state is corrupted silently.
|
|
139
|
+
|
|
140
|
+
**Fix**: Return a snapshot:
|
|
141
|
+
```ts
|
|
142
|
+
getActivationErrors(): Map<string, string> { return new Map(this.activationErrors); }
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Low Priority
|
|
148
|
+
|
|
149
|
+
### L1. `broadcastExtMsg` type mismatch with `Record<string, unknown>`
|
|
150
|
+
|
|
151
|
+
**File**: `extensions.ts:78-80`
|
|
152
|
+
|
|
153
|
+
`readyMsg` is typed as `Record<string, unknown>` but passed to `ws.send(JSON.stringify(readyMsg))` directly, bypassing the `broadcastExtMsg(msg: ExtServerMsg)` type check. This is consistent with the H1 issue -- fixing H1 resolves this.
|
|
154
|
+
|
|
155
|
+
### L2. Error messages in command:execute notifications could leak internal paths
|
|
156
|
+
|
|
157
|
+
**File**: `extensions.ts:98, 112-116`
|
|
158
|
+
|
|
159
|
+
`result?.error` and `e.message` are sent directly to browser clients. These can contain stack traces with server file paths (e.g., `/Users/hienlh/Projects/ppm/...`).
|
|
160
|
+
|
|
161
|
+
**Fix**: Truncate or sanitize error messages before broadcasting:
|
|
162
|
+
```ts
|
|
163
|
+
const safeMsg = (msg: string) => msg.split('\n')[0].slice(0, 200);
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### L3. `broadcastExtMsg` used for command errors sends to ALL clients
|
|
167
|
+
|
|
168
|
+
**File**: `extensions.ts:94-116`
|
|
169
|
+
|
|
170
|
+
When one client's command fails, the error notification is broadcast to all connected clients via `broadcastExtMsg`. Only the requesting client should see the error.
|
|
171
|
+
|
|
172
|
+
**Fix**: Send error only to the requesting socket:
|
|
173
|
+
```ts
|
|
174
|
+
ws.send(JSON.stringify({ type: "notification", ... }));
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Positive Observations
|
|
180
|
+
|
|
181
|
+
1. **Breadcrumb logging** at each layer (ExtService, ExtHost, ExtWS, extension) creates clear trail for debugging
|
|
182
|
+
2. **Activation error storage** with Map allows per-extension tracking and clearing on successful retry
|
|
183
|
+
3. **Error display in webview** with retry button is good UX -- users get actionable feedback
|
|
184
|
+
4. **Existing security**: `assertSafeFilePaths` properly validates all user-supplied paths in git-graph
|
|
185
|
+
5. **Timeout handling** in activation (10s in worker) and webview loading (10s in UI) prevents indefinite hangs
|
|
186
|
+
6. **Stash and conflict detection** implementations are clean and well-structured
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Recommended Actions (Priority Order)
|
|
191
|
+
|
|
192
|
+
1. **[H1]** Add `activationErrors?` to `ExtServerMsg` type union, remove `as any` casts
|
|
193
|
+
2. **[H2]** Deduplicate toasts by tracking previously shown errors
|
|
194
|
+
3. **[H3]** Clear `activationErrors` in `terminateWorker()`
|
|
195
|
+
4. **[M1]** Cap retry attempts in extension-webview reload loop
|
|
196
|
+
5. **[M2]** Use exact extension ID matching instead of substring
|
|
197
|
+
6. **[M3]** Fix Windows path handling in `detectMergeState`
|
|
198
|
+
7. **[M4]** Return Map copy from `getActivationErrors()`
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Unresolved Questions
|
|
203
|
+
|
|
204
|
+
- Is there an intentional reason `activationErrors` is not part of the `ExtServerMsg` type? If so, document the rationale.
|
|
205
|
+
- Should `activationErrors` be cleared when an extension is uninstalled/removed? Currently `remove()` calls `deactivate()` but doesn't touch `activationErrors`.
|