From fc7d52d26efd4e21951fad4b590410b3f806d56e Mon Sep 17 00:00:00 2001 From: Philip Zeyliger Date: Fri, 30 Jan 2026 21:07:39 +0000 Subject: [PATCH] shelley/ui: switch patch tool diffs to @pierre/diffs library Prompt: Can you use the @pierre/diffs library to render the diffs displayed in the patch tool? [...] Can you squash the "use diffs library" thing into one commit rebased on origin/main for me? Replace the custom diff rendering in PatchTool with the @pierre/diffs library which provides syntax-highlighted, split/unified diff views. Changes: - Add @pierre/diffs dependency - Update PatchTool to use MultiFileDiff component - Add language detection from file extension for syntax highlighting - Support both split (side-by-side) and unified diff views with toggle - Persist user's diff view preference in localStorage - Auto-switch to unified view on mobile - Add CSS containment properties for Safari rendering performance (content-visibility, contain) to optimize rendering of many diffs Co-authored-by: Shelley --- ui/package.json | 1 + ui/pnpm-lock.yaml | 356 +++++++++++++++++++ ui/src/components/PatchTool.tsx | 593 ++++++-------------------------- ui/src/styles.css | 210 +---------- 4 files changed, 471 insertions(+), 689 deletions(-) diff --git a/ui/package.json b/ui/package.json index 0caaed05961e45e9347aa5221def503894212fa0..04a80b3fc4f5188f76cfe9aed68c2f4757adac33 100644 --- a/ui/package.json +++ b/ui/package.json @@ -20,6 +20,7 @@ "test:e2e:debug": "pnpm run build && playwright test --debug" }, "dependencies": { + "@pierre/diffs": "^1.0.9", "@xterm/addon-fit": "^0.11.0", "@xterm/addon-web-links": "^0.12.0", "@xterm/xterm": "^6.0.0", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index a59f8bb3550b98339b18a53f7e2e0f4c37f4aef1..10f590921c9713afeccd7777a9fee5bc0f95d9bf 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@pierre/diffs': + specifier: ^1.0.9 + version: 1.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@xterm/addon-fit': specifier: ^0.11.0 version: 0.11.0 @@ -426,17 +429,53 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@pierre/diffs@1.0.9': + resolution: {integrity: sha512-PiRhcAzz0yuifRTe2DmTsKOHQgGf1qgh5W9OVO3/c+DONrjo1PM5tquOxndCAZDTdPxwzGuWc6jPUaDrfu6nDg==} + peerDependencies: + react: ^18.3.1 || ^19.0.0 + react-dom: ^18.3.1 || ^19.0.0 + '@playwright/test@1.57.0': resolution: {integrity: sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==} engines: {node: '>=18'} hasBin: true + '@shikijs/core@3.21.0': + resolution: {integrity: sha512-AXSQu/2n1UIQekY8euBJlvFYZIw0PHY63jUzGbrOma4wPxzznJXTXkri+QcHeBNaFxiiOljKxxJkVSoB3PjbyA==} + + '@shikijs/engine-javascript@3.21.0': + resolution: {integrity: sha512-ATwv86xlbmfD9n9gKRiwuPpWgPENAWCLwYCGz9ugTJlsO2kOzhOkvoyV/UD+tJ0uT7YRyD530x6ugNSffmvIiQ==} + + '@shikijs/engine-oniguruma@3.21.0': + resolution: {integrity: sha512-OYknTCct6qiwpQDqDdf3iedRdzj6hFlOPv5hMvI+hkWfCKs5mlJ4TXziBG9nyabLwGulrUjHiCq3xCspSzErYQ==} + + '@shikijs/langs@3.21.0': + resolution: {integrity: sha512-g6mn5m+Y6GBJ4wxmBYqalK9Sp0CFkUqfNzUy2pJglUginz6ZpWbaWjDB4fbQ/8SHzFjYbtU6Ddlp1pc+PPNDVA==} + + '@shikijs/themes@3.21.0': + resolution: {integrity: sha512-BAE4cr9EDiZyYzwIHEk7JTBJ9CzlPuM4PchfcA5ao1dWXb25nv6hYsoDiBq2aZK9E3dlt3WB78uI96UESD+8Mw==} + + '@shikijs/transformers@3.21.0': + resolution: {integrity: sha512-CZwvCWWIiRRiFk9/JKzdEooakAP8mQDtBOQ1TKiCaS2E1bYtyBCOkUzS8akO34/7ufICQ29oeSfkb3tT5KtrhA==} + + '@shikijs/types@3.21.0': + resolution: {integrity: sha512-zGrWOxZ0/+0ovPY7PvBU2gIS9tmhSUUt30jAcNV0Bq0gb2S98gwfjIs1vxlmH5zM7/4YxLamT6ChlqqAJmPPjA==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/node@22.19.5': resolution: {integrity: sha512-HfF8+mYcHPcPypui3w3mvzuIErlNOh2OAG+BCeBZCEwyiD5ls2SiCwEyT47OELtf7M3nHxBdu0FsmzdKxkN52Q==} @@ -451,6 +490,9 @@ packages: '@types/react@18.3.27': resolution: {integrity: sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==} + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@typescript-eslint/eslint-plugin@8.52.0': resolution: {integrity: sha512-okqtOgqu2qmZJ5iN4TWlgfF171dZmx2FzdOv2K/ixL2LZWDStL8+JgQerI2sa8eAEfoydG9+0V96m7V+P8yE1Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -510,6 +552,9 @@ packages: resolution: {integrity: sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@xterm/addon-fit@0.11.0': resolution: {integrity: sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==} @@ -600,10 +645,19 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -611,6 +665,9 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -656,6 +713,17 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + diff@8.0.2: + resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==} + engines: {node: '>=0.3.1'} + doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -883,6 +951,15 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1062,10 +1139,31 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + lru_map@0.4.1: + resolution: {integrity: sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg==} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -1110,6 +1208,12 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + oniguruma-parser@0.12.1: + resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} + + oniguruma-to-es@4.3.4: + resolution: {integrity: sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -1177,6 +1281,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1200,6 +1307,15 @@ packages: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + regexp.prototype.flags@1.5.4: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} @@ -1265,6 +1381,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shiki@3.21.0: + resolution: {integrity: sha512-N65B/3bqL/TI2crrXr+4UivctrAGEjmsib5rPMMPpFp1xAx/w03v8WZ9RDDFYteXoEgY7qZ4HGgl5KBIu1153w==} + side-channel-list@1.0.0: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} engines: {node: '>= 0.4'} @@ -1281,6 +1400,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -1307,6 +1429,9 @@ packages: string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -1323,6 +1448,9 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + ts-api-utils@2.4.0: resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} engines: {node: '>=18.12'} @@ -1373,12 +1501,33 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -1408,6 +1557,9 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@esbuild/aix-ppc64@0.19.12': @@ -1614,14 +1766,72 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@pierre/diffs@1.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@shikijs/core': 3.21.0 + '@shikijs/engine-javascript': 3.21.0 + '@shikijs/transformers': 3.21.0 + diff: 8.0.2 + hast-util-to-html: 9.0.5 + lru_map: 0.4.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + shiki: 3.21.0 + '@playwright/test@1.57.0': dependencies: playwright: 1.57.0 + '@shikijs/core@3.21.0': + dependencies: + '@shikijs/types': 3.21.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@3.21.0': + dependencies: + '@shikijs/types': 3.21.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.4 + + '@shikijs/engine-oniguruma@3.21.0': + dependencies: + '@shikijs/types': 3.21.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@3.21.0': + dependencies: + '@shikijs/types': 3.21.0 + + '@shikijs/themes@3.21.0': + dependencies: + '@shikijs/types': 3.21.0 + + '@shikijs/transformers@3.21.0': + dependencies: + '@shikijs/core': 3.21.0 + '@shikijs/types': 3.21.0 + + '@shikijs/types@3.21.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + '@types/estree@1.0.8': {} + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/json-schema@7.0.15': {} + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/node@22.19.5': dependencies: undici-types: 6.21.0 @@ -1637,6 +1847,8 @@ snapshots: '@types/prop-types': 15.7.15 csstype: 3.2.3 + '@types/unist@3.0.3': {} + '@typescript-eslint/eslint-plugin@8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -1728,6 +1940,8 @@ snapshots: '@typescript-eslint/types': 8.52.0 eslint-visitor-keys: 4.2.1 + '@ungap/structured-clone@1.3.0': {} + '@xterm/addon-fit@0.11.0': {} '@xterm/addon-web-links@0.12.0': {} @@ -1846,17 +2060,25 @@ snapshots: callsites@3.1.0: {} + ccount@2.0.1: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 color-name@1.1.4: {} + comma-separated-tokens@2.0.3: {} + concat-map@0.0.1: {} core-util-is@1.0.3: {} @@ -1905,6 +2127,14 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + dequal@2.0.3: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + diff@8.0.2: {} + doctrine@2.1.0: dependencies: esutils: 2.0.3 @@ -2279,6 +2509,26 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + html-void-elements@3.0.0: {} + ignore@5.3.2: {} ignore@7.0.5: {} @@ -2470,8 +2720,39 @@ snapshots: dependencies: js-tokens: 4.0.0 + lru_map@0.4.1: {} + math-intrinsics@1.1.0: {} + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-encode@2.0.1: {} + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -2522,6 +2803,14 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + oniguruma-parser@0.12.1: {} + + oniguruma-to-es@4.3.4: + dependencies: + oniguruma-parser: 0.12.1 + regex: 6.1.0 + regex-recursion: 6.0.2 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -2581,6 +2870,8 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + property-information@7.1.0: {} + punycode@2.3.1: {} react-dom@18.3.1(react@18.3.1): @@ -2616,6 +2907,16 @@ snapshots: get-proto: 1.0.1 which-builtin-type: 1.2.1 + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + regexp.prototype.flags@1.5.4: dependencies: call-bind: 1.0.8 @@ -2694,6 +2995,17 @@ snapshots: shebang-regex@3.0.0: {} + shiki@3.21.0: + dependencies: + '@shikijs/core': 3.21.0 + '@shikijs/engine-javascript': 3.21.0 + '@shikijs/engine-oniguruma': 3.21.0 + '@shikijs/langs': 3.21.0 + '@shikijs/themes': 3.21.0 + '@shikijs/types': 3.21.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + side-channel-list@1.0.0: dependencies: es-errors: 1.3.0 @@ -2722,6 +3034,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + space-separated-tokens@2.0.2: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -2775,6 +3089,11 @@ snapshots: dependencies: safe-buffer: 5.1.2 + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-json-comments@3.1.1: {} supports-color@7.2.0: @@ -2788,6 +3107,8 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + trim-lines@3.0.1: {} + ts-api-utils@2.4.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -2858,12 +3179,45 @@ snapshots: undici-types@6.21.0: {} + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + uri-js@4.4.1: dependencies: punycode: 2.3.1 util-deprecate@1.0.2: {} + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -2912,3 +3266,5 @@ snapshots: word-wrap@1.2.5: {} yocto-queue@0.1.0: {} + + zwitch@2.0.4: {} diff --git a/ui/src/components/PatchTool.tsx b/ui/src/components/PatchTool.tsx index f9b849eadbcd4e43e990ff1878e7466fb46d460b..2c1f2c3488b53811d614ab0ebd5e29c009ea8eb9 100644 --- a/ui/src/components/PatchTool.tsx +++ b/ui/src/components/PatchTool.tsx @@ -1,21 +1,12 @@ -import React, { useState, useEffect, useRef, useCallback } from "react"; -import type * as Monaco from "monaco-editor"; +import React, { useState, useEffect, useCallback } from "react"; +import { MultiFileDiff } from "@pierre/diffs/react"; +import type { FileContents, SupportedLanguages, ThemeTypes, ThemesType } from "@pierre/diffs"; import { LLMContent } from "../types"; import { isDarkModeActive } from "../services/theme"; -// LocalStorage keys for preferences -const STORAGE_KEY_MONACO_ENABLED = "shelley-use-monaco-diff"; +// LocalStorage key for side-by-side preference const STORAGE_KEY_SIDE_BY_SIDE = "shelley-diff-side-by-side"; -// Feature flag for Monaco diff view -function useMonacoDiff(): boolean { - try { - return localStorage.getItem(STORAGE_KEY_MONACO_ENABLED) === "true"; - } catch { - return false; - } -} - // Get saved side-by-side preference (default: true for desktop) function getSideBySidePreference(): boolean { try { @@ -59,485 +50,121 @@ interface PatchToolProps { onCommentTextChange?: (text: string) => void; } -// Global Monaco instance - loaded lazily -let monacoInstance: typeof Monaco | null = null; -let monacoLoadPromise: Promise | null = null; - -function loadMonaco(): Promise { - if (monacoInstance) { - return Promise.resolve(monacoInstance); - } - if (monacoLoadPromise) { - return monacoLoadPromise; - } - - monacoLoadPromise = (async () => { - // Configure Monaco environment for web workers before importing - const monacoEnv: Monaco.Environment = { - getWorkerUrl: () => "/editor.worker.js", - }; - (self as Window).MonacoEnvironment = monacoEnv; - - // Load Monaco CSS if not already loaded - if (!document.querySelector('link[href="/monaco-editor.css"]')) { - const link = document.createElement("link"); - link.rel = "stylesheet"; - link.href = "/monaco-editor.css"; - document.head.appendChild(link); - } - - // Load Monaco from our local bundle (runtime URL, cast to proper types) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - dynamic runtime URL import - const monaco = (await import("/monaco-editor.js")) as typeof Monaco; - monacoInstance = monaco; - return monacoInstance; - })(); - - return monacoLoadPromise; -} - -// Simple diff view component (default) -function SimpleDiffView({ displayData }: { displayData: PatchDisplayData | null }) { - // Get diff text from displayData or fall back to empty - const diff = displayData?.diff || ""; - - // Parse unified diff to extract lines - const lines = diff ? diff.split("\n") : []; - - return ( -
-      {lines.map((line, idx) => {
-        // Determine line type for styling
-        let className = "patch-diff-line";
-        if (line.startsWith("+") && !line.startsWith("+++")) {
-          className += " patch-diff-addition";
-        } else if (line.startsWith("-") && !line.startsWith("---")) {
-          className += " patch-diff-deletion";
-        } else if (line.startsWith("@@")) {
-          className += " patch-diff-hunk";
-        } else if (line.startsWith("---") || line.startsWith("+++")) {
-          className += " patch-diff-header";
-        }
-
-        return (
-          
- {line || " "} -
- ); - })} -
- ); +// Map file extension to language for syntax highlighting +function getLanguageFromPath(path: string): SupportedLanguages { + const ext = path.split(".").pop()?.toLowerCase() || ""; + const langMap: Record = { + ts: "typescript", + tsx: "tsx", + js: "javascript", + jsx: "jsx", + py: "python", + rb: "ruby", + go: "go", + rs: "rust", + java: "java", + c: "c", + cpp: "cpp", + h: "c", + hpp: "cpp", + cs: "csharp", + php: "php", + swift: "swift", + kt: "kotlin", + scala: "scala", + sh: "bash", + bash: "bash", + zsh: "bash", + fish: "fish", + ps1: "powershell", + sql: "sql", + html: "html", + htm: "html", + css: "css", + scss: "scss", + sass: "sass", + less: "less", + json: "json", + xml: "xml", + yaml: "yaml", + yml: "yaml", + toml: "toml", + ini: "ini", + md: "markdown", + markdown: "markdown", + txt: "text", + dockerfile: "dockerfile", + makefile: "makefile", + cmake: "cmake", + lua: "lua", + perl: "perl", + r: "r", + vue: "vue", + svelte: "svelte", + astro: "astro", + }; + return langMap[ext] || "text"; } -// Monaco diff view component (feature-flagged) -function MonacoDiffView({ +// Diff view component using @pierre/diffs +function DiffView({ displayData, - isMobile, sideBySide, - onCommentTextChange, - filename, }: { displayData: PatchDisplayData; - isMobile: boolean; sideBySide: boolean; - onCommentTextChange?: (text: string) => void; - filename: string; }) { - const [monacoLoaded, setMonacoLoaded] = useState(false); - const [isVisible, setIsVisible] = useState(false); - const [editorHeight, setEditorHeight] = useState(100); - const [showCommentDialog, setShowCommentDialog] = useState<{ - line: number; - selectedText?: string; - } | null>(null); - const [commentText, setCommentText] = useState(""); - - const containerRef = useRef(null); - const editorContainerRef = useRef(null); - const editorRef = useRef(null); - const monacoRef = useRef(null); - const commentInputRef = useRef(null); - const hoverDecorationsRef = useRef([]); - const heightSetRef = useRef(false); - const modelsRef = useRef<{ - original: Monaco.editor.ITextModel | null; - modified: Monaco.editor.ITextModel | null; - }>({ - original: null, - modified: null, - }); - - // Intersection observer for lazy loading - useEffect(() => { - const container = containerRef.current; - if (!container) return; - - const observer = new IntersectionObserver( - (entries) => { - for (const entry of entries) { - if (entry.isIntersecting) { - setIsVisible(true); - // Once visible, we don't need to observe anymore - observer.disconnect(); - } - } - }, - { - rootMargin: "100px", // Start loading a bit before it's visible - threshold: 0, - }, - ); - - observer.observe(container); - - return () => observer.disconnect(); - }, []); - - // Load Monaco only when visible - useEffect(() => { - if (!isVisible || monacoLoaded) return; - - loadMonaco() - .then((monaco) => { - monacoRef.current = monaco; - setMonacoLoaded(true); - }) - .catch((err) => { - console.error("Failed to load Monaco:", err); - }); - }, [isVisible, monacoLoaded]); - - // Update side-by-side mode when prop changes - useEffect(() => { - if (editorRef.current) { - editorRef.current.updateOptions({ renderSideBySide: sideBySide }); - // Reset height flag to allow recalculation after mode change - heightSetRef.current = false; - } - }, [sideBySide]); - - // Create Monaco editor when data is ready and visible - useEffect(() => { - if (!monacoLoaded || !isVisible || !editorContainerRef.current || !monacoRef.current) { - return; - } - - const monaco = monacoRef.current; - - // Dispose previous editor and models - if (editorRef.current) { - editorRef.current.dispose(); - editorRef.current = null; - } - if (modelsRef.current.original) { - modelsRef.current.original.dispose(); - modelsRef.current.original = null; - } - if (modelsRef.current.modified) { - modelsRef.current.modified.dispose(); - modelsRef.current.modified = null; - } - - // Reset height tracking for new editor - heightSetRef.current = false; - - // Get language from file extension - const ext = "." + (displayData.path.split(".").pop()?.toLowerCase() || ""); - const languages = monaco.languages.getLanguages(); - let language = "plaintext"; - for (const lang of languages) { - if (lang.extensions?.includes(ext)) { - language = lang.id; - break; - } - } + const [themeType, setThemeType] = useState(isDarkModeActive() ? "dark" : "light"); - // Create models with unique URIs (include timestamp to avoid conflicts) - const timestamp = Date.now(); - const originalUri = monaco.Uri.file(`patch-original-${timestamp}-${displayData.path}`); - const modifiedUri = monaco.Uri.file(`patch-modified-${timestamp}-${displayData.path}`); - - // Check for and dispose any existing models with these URIs (defensive, shouldn't happen) - const existingOriginal = monaco.editor.getModel(originalUri); - if (existingOriginal) existingOriginal.dispose(); - const existingModified = monaco.editor.getModel(modifiedUri); - if (existingModified) existingModified.dispose(); - - const originalModel = monaco.editor.createModel(displayData.oldContent, language, originalUri); - const modifiedModel = monaco.editor.createModel(displayData.newContent, language, modifiedUri); - modelsRef.current = { original: originalModel, modified: modifiedModel }; - - // Create diff editor with collapsed unchanged regions - const diffEditor = monaco.editor.createDiffEditor(editorContainerRef.current, { - theme: isDarkModeActive() ? "vs-dark" : "vs", - readOnly: true, - originalEditable: false, - automaticLayout: true, - renderSideBySide: sideBySide, - enableSplitViewResizing: true, - renderIndicators: true, - renderMarginRevertIcon: false, - lineNumbers: isMobile ? "off" : "on", - minimap: { enabled: false }, - scrollBeyondLastLine: false, - wordWrap: "on", - glyphMargin: !isMobile, // Enable glyph margin for comment indicator - lineDecorationsWidth: isMobile ? 0 : 10, - lineNumbersMinChars: isMobile ? 0 : 3, - quickSuggestions: false, - suggestOnTriggerCharacters: false, - lightbulb: { enabled: false }, - codeLens: false, - contextmenu: false, - links: false, - folding: !isMobile, - // Hide unchanged regions to show only edited sections - hideUnchangedRegions: { - enabled: true, - revealLineCount: 2, // Show 2 lines of context around changes - minimumLineCount: 3, // Hide regions with 3+ unchanged lines - contextLineCount: 2, // Context lines to show when expanding - }, - // Disable scrollbar when content fits - scrollbar: { - vertical: "auto", - horizontal: "auto", - alwaysConsumeMouseWheel: false, - }, - }); - - diffEditor.setModel({ - original: originalModel, - modified: modifiedModel, - }); - - editorRef.current = diffEditor; - - // Function to update height - only do this once to avoid scroll disruption - const updateHeight = () => { - if (heightSetRef.current) return; - - const modifiedEditor = diffEditor.getModifiedEditor(); - const contentHeight = modifiedEditor.getContentHeight(); - - if (contentHeight > 0) { - // Add small buffer, no max height - let it expand fully - const newHeight = Math.max(60, contentHeight + 4); - heightSetRef.current = true; - setEditorHeight(newHeight); - } - }; - - // Update height after diff is computed - // Monaco needs time to compute the diff and layout - const heightUpdateTimer = setTimeout(updateHeight, 200); - - // Also listen for content size change (fires when diff is computed) - const modifiedEditor = diffEditor.getModifiedEditor(); - const contentSizeDisposable = modifiedEditor.onDidContentSizeChange(() => { - updateHeight(); - }); - - // Add click handler for commenting if callback is provided - if (onCommentTextChange) { - const openCommentDialog = (lineNumber: number) => { - const model = modifiedEditor.getModel(); - const selection = modifiedEditor.getSelection(); - let selectedText = ""; - - if (selection && !selection.isEmpty() && model) { - selectedText = model.getValueInRange(selection); - } else if (model) { - selectedText = model.getLineContent(lineNumber) || ""; - } - - setShowCommentDialog({ - line: lineNumber, - selectedText, - }); - }; - - modifiedEditor.onMouseDown((e: Monaco.editor.IEditorMouseEvent) => { - const isLineClick = - e.target.type === monaco.editor.MouseTargetType.CONTENT_TEXT || - e.target.type === monaco.editor.MouseTargetType.CONTENT_EMPTY; - - if (isLineClick) { - const position = e.target.position; - if (position) { - openCommentDialog(position.lineNumber); - } - } - }); - - // Add hover highlighting with comment indicator - let lastHoveredLine = -1; - modifiedEditor.onMouseMove((e: Monaco.editor.IEditorMouseEvent) => { - const position = e.target.position; - const lineNumber = position?.lineNumber ?? -1; - - if (lineNumber === lastHoveredLine) return; - lastHoveredLine = lineNumber; - - if (lineNumber > 0) { - hoverDecorationsRef.current = modifiedEditor.deltaDecorations( - hoverDecorationsRef.current, - [ - { - range: new monaco.Range(lineNumber, 1, lineNumber, 1), - options: { - isWholeLine: true, - className: "patch-line-hover", - glyphMarginClassName: "patch-comment-glyph", - }, - }, - ], - ); - } else { - // Clear decorations when not hovering a line - hoverDecorationsRef.current = modifiedEditor.deltaDecorations( - hoverDecorationsRef.current, - [], - ); - } - }); - - // Clear decorations when mouse leaves editor - modifiedEditor.onMouseLeave(() => { - lastHoveredLine = -1; - hoverDecorationsRef.current = modifiedEditor.deltaDecorations( - hoverDecorationsRef.current, - [], - ); - }); - } - - // Cleanup function - return () => { - clearTimeout(heightUpdateTimer); - contentSizeDisposable.dispose(); - if (editorRef.current) { - editorRef.current.dispose(); - editorRef.current = null; - } - if (modelsRef.current.original) { - modelsRef.current.original.dispose(); - modelsRef.current.original = null; - } - if (modelsRef.current.modified) { - modelsRef.current.modified.dispose(); - modelsRef.current.modified = null; - } - }; - }, [monacoLoaded, isVisible, displayData, isMobile, onCommentTextChange, sideBySide]); - - // Update Monaco theme when dark mode changes + // Listen for theme changes useEffect(() => { - if (!monacoRef.current) return; - - const updateMonacoTheme = () => { - const theme = isDarkModeActive() ? "vs-dark" : "vs"; - monacoRef.current?.editor.setTheme(theme); + const updateTheme = () => { + setThemeType(isDarkModeActive() ? "dark" : "light"); }; const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.attributeName === "class") { - updateMonacoTheme(); + updateTheme(); } } }); observer.observe(document.documentElement, { attributes: true }); - return () => observer.disconnect(); - }, [monacoLoaded]); - - // Focus comment input when dialog opens - useEffect(() => { - if (showCommentDialog && commentInputRef.current) { - setTimeout(() => { - commentInputRef.current?.focus(); - }, 50); - } - }, [showCommentDialog]); + }, []); - // Handle adding a comment - const handleAddComment = useCallback(() => { - if (!showCommentDialog || !commentText.trim() || !onCommentTextChange) return; + const lang = getLanguageFromPath(displayData.path); - const line = showCommentDialog.line; - const codeSnippet = showCommentDialog.selectedText?.split("\n")[0]?.trim() || ""; - const truncatedCode = - codeSnippet.length > 60 ? codeSnippet.substring(0, 57) + "..." : codeSnippet; + const oldFile: FileContents = { + name: displayData.path, + contents: displayData.oldContent, + lang, + }; - const commentBlock = `> ${filename}:${line}: ${truncatedCode}\n${commentText}\n\n`; + const newFile: FileContents = { + name: displayData.path, + contents: displayData.newContent, + lang, + }; - onCommentTextChange(commentBlock); - setShowCommentDialog(null); - setCommentText(""); - }, [showCommentDialog, commentText, onCommentTextChange, filename]); + const theme: ThemesType = { + dark: "github-dark", + light: "github-light", + }; return ( -
- {/* Monaco editor container */} - {!isVisible ? ( -
- Scroll to load diff... -
- ) : !monacoLoaded ? ( -
-
- Loading editor... -
- ) : ( -
- )} - - {/* Comment dialog */} - {showCommentDialog && onCommentTextChange && ( -
-

Add Comment (Line {showCommentDialog.line})

- {showCommentDialog.selectedText && ( -
{showCommentDialog.selectedText}
- )} -