@hienlh/ppm 0.13.3 → 0.13.4

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 (96) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/assets/skills/ppm/SKILL.md +1 -1
  3. package/assets/skills/ppm/references/http-api.md +1 -1
  4. package/dist/web/assets/{ai-settings-section-QE6nBNgN.js → ai-settings-section-DeW4WN43.js} +1 -1
  5. package/dist/web/assets/{api-settings-DAk7D-NP.js → api-settings-t7Leca7J.js} +1 -1
  6. package/dist/web/assets/architecture-PBZL5I3N-Dy3PgD6O.js +1 -0
  7. package/dist/web/assets/{audio-preview-R7cq1uhJ.js → audio-preview-BSAe2WQB.js} +1 -1
  8. package/dist/web/assets/chat-tab-UFEFOnpl.js +12 -0
  9. package/dist/web/assets/code-editor-BJ1tSNWA.js +8 -0
  10. package/dist/web/assets/{conflict-editor-dzofjxab.js → conflict-editor-CrgrMZ2F.js} +1 -1
  11. package/dist/web/assets/{csv-preview-HMSavgBb.js → csv-preview-C9qGhDlb.js} +1 -1
  12. package/dist/web/assets/{database-viewer-5Uf8Rrls.js → database-viewer-e_NAkIL_.js} +2 -2
  13. package/dist/web/assets/diff-viewer-C2eOczTs.js +4 -0
  14. package/dist/web/assets/{esm-K1XIK4vc.js → esm-B3je8j5P.js} +1 -1
  15. package/dist/web/assets/{extension-webview-HILvTnnn.js → extension-webview-B95nOfj-.js} +2 -2
  16. package/dist/web/assets/{file-store-BrbCNyLm.js → file-store-BgZggznw.js} +1 -1
  17. package/dist/web/assets/gitGraph-HDMCJU4V-Bu1SIFFq.js +1 -0
  18. package/dist/web/assets/{image-preview-0cJMnFZK.js → image-preview-DAuPOzYl.js} +1 -1
  19. package/dist/web/assets/index-DJOjXTcq.js +27 -0
  20. package/dist/web/assets/index-DSOP0R0s.css +2 -0
  21. package/dist/web/assets/info-3K5VOQVL-DzfAxmVd.js +1 -0
  22. package/dist/web/assets/{input-Dk49gO8E.js → input-bGJExpJZ.js} +1 -1
  23. package/dist/web/assets/keybindings-store-V12kZZHO.js +1 -0
  24. package/dist/web/assets/{markdown-renderer-D0MrsVJB.js → markdown-renderer-DwINRWo4.js} +3 -3
  25. package/dist/web/assets/packet-RMMSAZCW-DpzHf4xp.js +1 -0
  26. package/dist/web/assets/{pdf-preview-BBVDS-z5.js → pdf-preview-CqoQE09t.js} +1 -1
  27. package/dist/web/assets/pie-UPGHQEXC-BpzFCKJ8.js +1 -0
  28. package/dist/web/assets/{port-forwarding-tab-ByKzBs-R.js → port-forwarding-tab-De7qxkjp.js} +1 -1
  29. package/dist/web/assets/{postgres-viewer-BnCbdR7g.js → postgres-viewer-Dd6rLb8b.js} +3 -3
  30. package/dist/web/assets/radar-KQ55EAFF-DAxWKxM4.js +1 -0
  31. package/dist/web/assets/{scroll-area-BEllam7_.js → scroll-area-D0EQpAH2.js} +1 -1
  32. package/dist/web/assets/{settings-store-BLLR7ed8.js → settings-store-CdcSAgEZ.js} +2 -2
  33. package/dist/web/assets/settings-tab-BdTEumwU.js +1 -0
  34. package/dist/web/assets/{sql-query-editor-CVAnRFbi.js → sql-query-editor-vpD0I0KG.js} +1 -1
  35. package/dist/web/assets/sqlite-viewer-Ccz2crvN.js +1 -0
  36. package/dist/web/assets/{tab-store-B3M9hjho.js → tab-store-Jvy1eZGM.js} +1 -1
  37. package/dist/web/assets/terminal-tab-D7u7wsyb.js +1 -0
  38. package/dist/web/assets/treemap-KZPCXAKY-D6dgXbAe.js +1 -0
  39. package/dist/web/assets/{use-blob-url-e9uTXjv5.js → use-blob-url-BgxxT-n_.js} +1 -1
  40. package/dist/web/assets/{use-monaco-theme-BkZDwoVd.js → use-monaco-theme-dtPsv6sh.js} +1 -1
  41. package/dist/web/assets/{vendor-mermaid-Dx86tuVP.js → vendor-mermaid-DCxaaPi4.js} +2 -2
  42. package/dist/web/assets/{video-preview-CKaht6nI.js → video-preview-BSDzqlzk.js} +1 -1
  43. package/dist/web/index.html +17 -19
  44. package/dist/web/sw.js +1 -1
  45. package/docs/codebase-summary.md +2 -0
  46. package/docs/journals/2026-04-22-compare-files-feature-ship.md +53 -0
  47. package/docs/project-changelog.md +9 -1
  48. package/package.json +1 -1
  49. package/src/services/file-filter.service.ts +17 -4
  50. package/src/services/file-list-index.service.ts +7 -3
  51. package/src/types/project.ts +2 -0
  52. package/src/web/app.tsx +4 -0
  53. package/src/web/components/editor/compare-picker.tsx +245 -0
  54. package/src/web/components/explorer/file-tree.tsx +42 -1
  55. package/src/web/components/layout/command-palette.tsx +31 -1
  56. package/src/web/components/layout/draggable-tab.tsx +8 -0
  57. package/src/web/components/layout/tab-bar.tsx +101 -27
  58. package/src/web/hooks/use-global-keybindings.ts +20 -0
  59. package/src/web/lib/open-compare-tab.ts +76 -0
  60. package/src/web/stores/compare-store.ts +57 -0
  61. package/src/web/stores/keybindings-store.ts +1 -0
  62. package/dist/web/assets/architecture-PBZL5I3N-DvZbltvY.js +0 -1
  63. package/dist/web/assets/chat-tab-umei1UkV.js +0 -12
  64. package/dist/web/assets/code-editor-BTosKXkr.js +0 -8
  65. package/dist/web/assets/columns-2-4fQcE4PF.js +0 -1
  66. package/dist/web/assets/diff-viewer-DKLeIBkK.js +0 -4
  67. package/dist/web/assets/extension-store-3yZYn07W.js +0 -1
  68. package/dist/web/assets/gitGraph-HDMCJU4V-BxhdxFgj.js +0 -1
  69. package/dist/web/assets/index-Bce0weeW.css +0 -2
  70. package/dist/web/assets/index-DDBvHVVr.js +0 -27
  71. package/dist/web/assets/info-3K5VOQVL-BwAZ2zd8.js +0 -1
  72. package/dist/web/assets/keybindings-store-B-zET-0o.js +0 -1
  73. package/dist/web/assets/keybindings-store-DaBV6qhz.js +0 -1
  74. package/dist/web/assets/packet-RMMSAZCW-tx2n5Qry.js +0 -1
  75. package/dist/web/assets/pie-UPGHQEXC-D6S2MqVT.js +0 -1
  76. package/dist/web/assets/radar-KQ55EAFF-BviZcL-b.js +0 -1
  77. package/dist/web/assets/settings-tab-BPdzUw3v.js +0 -1
  78. package/dist/web/assets/sqlite-viewer-D6mSIIx2.js +0 -1
  79. package/dist/web/assets/terminal-tab-BLIA53mt.js +0 -1
  80. package/dist/web/assets/treemap-KZPCXAKY-CM54VdaB.js +0 -1
  81. /package/dist/web/assets/{api-client-Dvzcc_EO.js → api-client-r4nyVy7H.js} +0 -0
  82. /package/dist/web/assets/{csv-parser--2WJNgS7.js → csv-parser-DxVplKKB.js} +0 -0
  83. /package/dist/web/assets/{database-D4DIhgi-.js → database-DCT0OjgQ.js} +0 -0
  84. /package/dist/web/assets/{dist-im4ynINo.js → dist-BqoEabX7.js} +0 -0
  85. /package/dist/web/assets/{file-exclamation-point-BwzaQ50n.js → file-exclamation-point-Baz81y5z.js} +0 -0
  86. /package/dist/web/assets/{katex-CKoArbIw.js → katex-bpagxk3Z.js} +0 -0
  87. /package/dist/web/assets/{lib-DQHnkzGy.js → lib-BqkcKGFq.js} +0 -0
  88. /package/dist/web/assets/{react-GqWghJ-L.js → react-BkWDCPD7.js} +0 -0
  89. /package/dist/web/assets/{refresh-cw-LlbZDJpO.js → refresh-cw-CSFrDtiu.js} +0 -0
  90. /package/dist/web/assets/{sql-completion-provider-C3cq9j99.js → sql-completion-provider-EzHOQLfo.js} +0 -0
  91. /package/dist/web/assets/{table-Dq575bPF.js → table-DbSviOmw.js} +0 -0
  92. /package/dist/web/assets/{text-wrap-Cn6BNQfq.js → text-wrap-DzvCTq_i.js} +0 -0
  93. /package/dist/web/assets/{trash-2-CJYoLw7Q.js → trash-2-BgDIBl6f.js} +0 -0
  94. /package/dist/web/assets/{utils-CTg5uAYR.js → utils-ChWX7pZv.js} +0 -0
  95. /package/dist/web/assets/{vendor-xterm-CU2c3f0A.js → vendor-xterm-D7SePDJp.js} +0 -0
  96. /package/dist/web/assets/{x-DlFGzN8d.js → x-BtqbfkR7.js} +0 -0
@@ -1 +1 @@
1
- import{b as e}from"./vendor-markdown-0Mxgxy0L.js";import{t}from"./file-exclamation-point-BwzaQ50n.js";import"./api-client-Dvzcc_EO.js";import{I as n}from"./index-DDBvHVVr.js";import{t as r}from"./use-blob-url-e9uTXjv5.js";var i=e();function a({filePath:e,projectName:a}){let{blobUrl:o,error:s}=r(e,a);return s?(0,i.jsxs)(`div`,{className:`flex flex-col items-center justify-center h-full gap-3 text-text-secondary`,children:[(0,i.jsx)(t,{className:`size-10 text-text-subtle`}),(0,i.jsx)(`p`,{className:`text-sm`,children:`Failed to load video.`})]}):o?(0,i.jsx)(`div`,{className:`flex items-center justify-center h-full p-4 bg-surface overflow-auto`,children:(0,i.jsx)(`video`,{src:o,controls:!0,className:`max-w-full max-h-full`})}):(0,i.jsx)(`div`,{className:`flex items-center justify-center h-full`,children:(0,i.jsx)(n,{className:`size-5 animate-spin text-text-subtle`})})}export{a as VideoPreview};
1
+ import{b as e}from"./vendor-markdown-0Mxgxy0L.js";import{t}from"./file-exclamation-point-Baz81y5z.js";import"./api-client-r4nyVy7H.js";import{U as n}from"./index-DJOjXTcq.js";import{t as r}from"./use-blob-url-BgxxT-n_.js";var i=e();function a({filePath:e,projectName:a}){let{blobUrl:o,error:s}=r(e,a);return s?(0,i.jsxs)(`div`,{className:`flex flex-col items-center justify-center h-full gap-3 text-text-secondary`,children:[(0,i.jsx)(t,{className:`size-10 text-text-subtle`}),(0,i.jsx)(`p`,{className:`text-sm`,children:`Failed to load video.`})]}):o?(0,i.jsx)(`div`,{className:`flex items-center justify-center h-full p-4 bg-surface overflow-auto`,children:(0,i.jsx)(`video`,{src:o,controls:!0,className:`max-w-full max-h-full`})}):(0,i.jsx)(`div`,{className:`flex items-center justify-center h-full`,children:(0,i.jsx)(n,{className:`size-5 animate-spin text-text-subtle`})})}export{a as VideoPreview};
@@ -39,32 +39,30 @@
39
39
  <link rel="preconnect" href="https://fonts.googleapis.com" />
40
40
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
41
41
  <link href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400;500;600;700&family=Geist:wght@400;500;600;700&display=swap" rel="stylesheet" />
42
- <script type="module" crossorigin src="/assets/index-DDBvHVVr.js"></script>
42
+ <script type="module" crossorigin src="/assets/index-DJOjXTcq.js"></script>
43
43
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-FhOqtrmT.js">
44
- <link rel="modulepreload" crossorigin href="/assets/vendor-mermaid-Dx86tuVP.js">
44
+ <link rel="modulepreload" crossorigin href="/assets/vendor-mermaid-DCxaaPi4.js">
45
45
  <link rel="modulepreload" crossorigin href="/assets/vendor-markdown-0Mxgxy0L.js">
46
46
  <link rel="modulepreload" crossorigin href="/assets/vendor-ui-B-89Uj8i.js">
47
- <link rel="modulepreload" crossorigin href="/assets/utils-CTg5uAYR.js">
47
+ <link rel="modulepreload" crossorigin href="/assets/utils-ChWX7pZv.js">
48
48
  <link rel="modulepreload" crossorigin href="/assets/createLucideIcon-BjHrJDVb.js">
49
- <link rel="modulepreload" crossorigin href="/assets/x-DlFGzN8d.js">
50
- <link rel="modulepreload" crossorigin href="/assets/input-Dk49gO8E.js">
51
- <link rel="modulepreload" crossorigin href="/assets/react-GqWghJ-L.js">
52
- <link rel="modulepreload" crossorigin href="/assets/api-client-Dvzcc_EO.js">
53
- <link rel="modulepreload" crossorigin href="/assets/settings-store-BLLR7ed8.js">
54
- <link rel="modulepreload" crossorigin href="/assets/scroll-area-BEllam7_.js">
49
+ <link rel="modulepreload" crossorigin href="/assets/x-BtqbfkR7.js">
50
+ <link rel="modulepreload" crossorigin href="/assets/input-bGJExpJZ.js">
51
+ <link rel="modulepreload" crossorigin href="/assets/react-BkWDCPD7.js">
52
+ <link rel="modulepreload" crossorigin href="/assets/api-client-r4nyVy7H.js">
53
+ <link rel="modulepreload" crossorigin href="/assets/settings-store-CdcSAgEZ.js">
54
+ <link rel="modulepreload" crossorigin href="/assets/scroll-area-D0EQpAH2.js">
55
55
  <link rel="modulepreload" crossorigin href="/assets/dist-D7KGU7Vl.js">
56
56
  <link rel="modulepreload" crossorigin href="/assets/plus-51UQ45rf.js">
57
- <link rel="modulepreload" crossorigin href="/assets/refresh-cw-LlbZDJpO.js">
58
- <link rel="modulepreload" crossorigin href="/assets/trash-2-CJYoLw7Q.js">
59
- <link rel="modulepreload" crossorigin href="/assets/api-settings-DAk7D-NP.js">
60
- <link rel="modulepreload" crossorigin href="/assets/ai-settings-section-QE6nBNgN.js">
57
+ <link rel="modulepreload" crossorigin href="/assets/refresh-cw-CSFrDtiu.js">
58
+ <link rel="modulepreload" crossorigin href="/assets/trash-2-BgDIBl6f.js">
59
+ <link rel="modulepreload" crossorigin href="/assets/api-settings-t7Leca7J.js">
60
+ <link rel="modulepreload" crossorigin href="/assets/ai-settings-section-DeW4WN43.js">
61
61
  <link rel="modulepreload" crossorigin href="/assets/chevron-right-BzAdxJRG.js">
62
- <link rel="modulepreload" crossorigin href="/assets/database-D4DIhgi-.js">
63
- <link rel="modulepreload" crossorigin href="/assets/extension-store-3yZYn07W.js">
64
- <link rel="modulepreload" crossorigin href="/assets/file-store-BrbCNyLm.js">
65
- <link rel="modulepreload" crossorigin href="/assets/keybindings-store-B-zET-0o.js">
66
- <link rel="modulepreload" crossorigin href="/assets/tab-store-B3M9hjho.js">
67
- <link rel="stylesheet" crossorigin href="/assets/index-Bce0weeW.css">
62
+ <link rel="modulepreload" crossorigin href="/assets/database-DCT0OjgQ.js">
63
+ <link rel="modulepreload" crossorigin href="/assets/file-store-BgZggznw.js">
64
+ <link rel="modulepreload" crossorigin href="/assets/tab-store-Jvy1eZGM.js">
65
+ <link rel="stylesheet" crossorigin href="/assets/index-DSOP0R0s.css">
68
66
  <link rel="manifest" href="/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script></head>
69
67
  <body class="bg-[#0f1419] text-[#e5e7eb] font-sans antialiased">
70
68
  <div id="root"></div>
package/dist/web/sw.js CHANGED
@@ -1 +1 @@
1
- try{self[`workbox:core:7.3.0`]&&_()}catch{}var e=(e,...t)=>{let n=e;return t.length>0&&(n+=` :: ${JSON.stringify(t)}`),n},t=class extends Error{constructor(t,n){let r=e(t,n);super(r),this.name=t,this.details=n}},n={googleAnalytics:`googleAnalytics`,precache:`precache-v2`,prefix:`workbox`,runtime:`runtime`,suffix:typeof registration<`u`?registration.scope:``},r=e=>[n.prefix,e,n.suffix].filter(e=>e&&e.length>0).join(`-`),i=e=>{for(let t of Object.keys(n))e(t)},a={updateDetails:e=>{i(t=>{typeof e[t]==`string`&&(n[t]=e[t])})},getGoogleAnalyticsName:e=>e||r(n.googleAnalytics),getPrecacheName:e=>e||r(n.precache),getPrefix:()=>n.prefix,getRuntimeName:e=>e||r(n.runtime),getSuffix:()=>n.suffix};function o(e,t){let n=t();return e.waitUntil(n),n}try{self[`workbox:precaching:7.3.0`]&&_()}catch{}var s=`__WB_REVISION__`;function c(e){if(!e)throw new t(`add-to-cache-list-unexpected-type`,{entry:e});if(typeof e==`string`){let t=new URL(e,location.href);return{cacheKey:t.href,url:t.href}}let{revision:n,url:r}=e;if(!r)throw new t(`add-to-cache-list-unexpected-type`,{entry:e});if(!n){let e=new URL(r,location.href);return{cacheKey:e.href,url:e.href}}let i=new URL(r,location.href),a=new URL(r,location.href);return i.searchParams.set(s,n),{cacheKey:i.href,url:a.href}}var l=class{constructor(){this.updatedURLs=[],this.notUpdatedURLs=[],this.handlerWillStart=async({request:e,state:t})=>{t&&(t.originalRequest=e)},this.cachedResponseWillBeUsed=async({event:e,state:t,cachedResponse:n})=>{if(e.type===`install`&&t&&t.originalRequest&&t.originalRequest instanceof Request){let e=t.originalRequest.url;n?this.notUpdatedURLs.push(e):this.updatedURLs.push(e)}return n}}},u=class{constructor({precacheController:e}){this.cacheKeyWillBeUsed=async({request:e,params:t})=>{let n=t?.cacheKey||this._precacheController.getCacheKeyForURL(e.url);return n?new Request(n,{headers:e.headers}):e},this._precacheController=e}},d;function f(){if(d===void 0){let e=new Response(``);if(`body`in e)try{new Response(e.body),d=!0}catch{d=!1}d=!1}return d}async function p(e,n){let r=null;if(e.url&&(r=new URL(e.url).origin),r!==self.location.origin)throw new t(`cross-origin-copy-response`,{origin:r});let i=e.clone(),a={headers:new Headers(i.headers),status:i.status,statusText:i.statusText},o=n?n(a):a,s=f()?i.body:await i.blob();return new Response(s,o)}var m=e=>new URL(String(e),location.href).href.replace(RegExp(`^${location.origin}`),``);function h(e,t){let n=new URL(e);for(let e of t)n.searchParams.delete(e);return n.href}async function g(e,t,n,r){let i=h(t.url,n);if(t.url===i)return e.match(t,r);let a=Object.assign(Object.assign({},r),{ignoreSearch:!0}),o=await e.keys(t,a);for(let t of o)if(i===h(t.url,n))return e.match(t,r)}var v=class{constructor(){this.promise=new Promise((e,t)=>{this.resolve=e,this.reject=t})}},y=new Set;async function b(){for(let e of y)await e()}function x(e){return new Promise(t=>setTimeout(t,e))}try{self[`workbox:strategies:7.3.0`]&&_()}catch{}function S(e){return typeof e==`string`?new Request(e):e}var C=class{constructor(e,t){this._cacheKeys={},Object.assign(this,t),this.event=t.event,this._strategy=e,this._handlerDeferred=new v,this._extendLifetimePromises=[],this._plugins=[...e.plugins],this._pluginStateMap=new Map;for(let e of this._plugins)this._pluginStateMap.set(e,{});this.event.waitUntil(this._handlerDeferred.promise)}async fetch(e){let{event:n}=this,r=S(e);if(r.mode===`navigate`&&n instanceof FetchEvent&&n.preloadResponse){let e=await n.preloadResponse;if(e)return e}let i=this.hasCallback(`fetchDidFail`)?r.clone():null;try{for(let e of this.iterateCallbacks(`requestWillFetch`))r=await e({request:r.clone(),event:n})}catch(e){if(e instanceof Error)throw new t(`plugin-error-request-will-fetch`,{thrownErrorMessage:e.message})}let a=r.clone();try{let e;e=await fetch(r,r.mode===`navigate`?void 0:this._strategy.fetchOptions);for(let t of this.iterateCallbacks(`fetchDidSucceed`))e=await t({event:n,request:a,response:e});return e}catch(e){throw i&&await this.runCallbacks(`fetchDidFail`,{error:e,event:n,originalRequest:i.clone(),request:a.clone()}),e}}async fetchAndCachePut(e){let t=await this.fetch(e),n=t.clone();return this.waitUntil(this.cachePut(e,n)),t}async cacheMatch(e){let t=S(e),n,{cacheName:r,matchOptions:i}=this._strategy,a=await this.getCacheKey(t,`read`),o=Object.assign(Object.assign({},i),{cacheName:r});n=await caches.match(a,o);for(let e of this.iterateCallbacks(`cachedResponseWillBeUsed`))n=await e({cacheName:r,matchOptions:i,cachedResponse:n,request:a,event:this.event})||void 0;return n}async cachePut(e,n){let r=S(e);await x(0);let i=await this.getCacheKey(r,`write`);if(!n)throw new t(`cache-put-with-no-response`,{url:m(i.url)});let a=await this._ensureResponseSafeToCache(n);if(!a)return!1;let{cacheName:o,matchOptions:s}=this._strategy,c=await self.caches.open(o),l=this.hasCallback(`cacheDidUpdate`),u=l?await g(c,i.clone(),[`__WB_REVISION__`],s):null;try{await c.put(i,l?a.clone():a)}catch(e){if(e instanceof Error)throw e.name===`QuotaExceededError`&&await b(),e}for(let e of this.iterateCallbacks(`cacheDidUpdate`))await e({cacheName:o,oldResponse:u,newResponse:a.clone(),request:i,event:this.event});return!0}async getCacheKey(e,t){let n=`${e.url} | ${t}`;if(!this._cacheKeys[n]){let r=e;for(let e of this.iterateCallbacks(`cacheKeyWillBeUsed`))r=S(await e({mode:t,request:r,event:this.event,params:this.params}));this._cacheKeys[n]=r}return this._cacheKeys[n]}hasCallback(e){for(let t of this._strategy.plugins)if(e in t)return!0;return!1}async runCallbacks(e,t){for(let n of this.iterateCallbacks(e))await n(t)}*iterateCallbacks(e){for(let t of this._strategy.plugins)if(typeof t[e]==`function`){let n=this._pluginStateMap.get(t);yield r=>{let i=Object.assign(Object.assign({},r),{state:n});return t[e](i)}}}waitUntil(e){return this._extendLifetimePromises.push(e),e}async doneWaiting(){for(;this._extendLifetimePromises.length;){let e=this._extendLifetimePromises.splice(0),t=(await Promise.allSettled(e)).find(e=>e.status===`rejected`);if(t)throw t.reason}}destroy(){this._handlerDeferred.resolve(null)}async _ensureResponseSafeToCache(e){let t=e,n=!1;for(let e of this.iterateCallbacks(`cacheWillUpdate`))if(t=await e({request:this.request,response:t,event:this.event})||void 0,n=!0,!t)break;return n||t&&t.status!==200&&(t=void 0),t}},w=class{constructor(e={}){this.cacheName=a.getRuntimeName(e.cacheName),this.plugins=e.plugins||[],this.fetchOptions=e.fetchOptions,this.matchOptions=e.matchOptions}handle(e){let[t]=this.handleAll(e);return t}handleAll(e){e instanceof FetchEvent&&(e={event:e,request:e.request});let t=e.event,n=typeof e.request==`string`?new Request(e.request):e.request,r=`params`in e?e.params:void 0,i=new C(this,{event:t,request:n,params:r}),a=this._getResponse(i,n,t);return[a,this._awaitComplete(a,i,n,t)]}async _getResponse(e,n,r){await e.runCallbacks(`handlerWillStart`,{event:r,request:n});let i;try{if(i=await this._handle(n,e),!i||i.type===`error`)throw new t(`no-response`,{url:n.url})}catch(t){if(t instanceof Error){for(let a of e.iterateCallbacks(`handlerDidError`))if(i=await a({error:t,event:r,request:n}),i)break}if(!i)throw t}for(let t of e.iterateCallbacks(`handlerWillRespond`))i=await t({event:r,request:n,response:i});return i}async _awaitComplete(e,t,n,r){let i,a;try{i=await e}catch{}try{await t.runCallbacks(`handlerDidRespond`,{event:r,request:n,response:i}),await t.doneWaiting()}catch(e){e instanceof Error&&(a=e)}if(await t.runCallbacks(`handlerDidComplete`,{event:r,request:n,response:i,error:a}),t.destroy(),a)throw a}},T=class e extends w{constructor(t={}){t.cacheName=a.getPrecacheName(t.cacheName),super(t),this._fallbackToNetwork=t.fallbackToNetwork!==!1,this.plugins.push(e.copyRedirectedCacheableResponsesPlugin)}async _handle(e,t){return await t.cacheMatch(e)||(t.event&&t.event.type===`install`?await this._handleInstall(e,t):await this._handleFetch(e,t))}async _handleFetch(e,n){let r,i=n.params||{};if(this._fallbackToNetwork){let t=i.integrity,a=e.integrity,o=!a||a===t;r=await n.fetch(new Request(e,{integrity:e.mode===`no-cors`?void 0:a||t})),t&&o&&e.mode!==`no-cors`&&(this._useDefaultCacheabilityPluginIfNeeded(),await n.cachePut(e,r.clone()))}else throw new t(`missing-precache-entry`,{cacheName:this.cacheName,url:e.url});return r}async _handleInstall(e,n){this._useDefaultCacheabilityPluginIfNeeded();let r=await n.fetch(e);if(!await n.cachePut(e,r.clone()))throw new t(`bad-precaching-response`,{url:e.url,status:r.status});return r}_useDefaultCacheabilityPluginIfNeeded(){let t=null,n=0;for(let[r,i]of this.plugins.entries())i!==e.copyRedirectedCacheableResponsesPlugin&&(i===e.defaultPrecacheCacheabilityPlugin&&(t=r),i.cacheWillUpdate&&n++);n===0?this.plugins.push(e.defaultPrecacheCacheabilityPlugin):n>1&&t!==null&&this.plugins.splice(t,1)}};T.defaultPrecacheCacheabilityPlugin={async cacheWillUpdate({response:e}){return!e||e.status>=400?null:e}},T.copyRedirectedCacheableResponsesPlugin={async cacheWillUpdate({response:e}){return e.redirected?await p(e):e}};var E=class{constructor({cacheName:e,plugins:t=[],fallbackToNetwork:n=!0}={}){this._urlsToCacheKeys=new Map,this._urlsToCacheModes=new Map,this._cacheKeysToIntegrities=new Map,this._strategy=new T({cacheName:a.getPrecacheName(e),plugins:[...t,new u({precacheController:this})],fallbackToNetwork:n}),this.install=this.install.bind(this),this.activate=this.activate.bind(this)}get strategy(){return this._strategy}precache(e){this.addToCacheList(e),this._installAndActiveListenersAdded||=(self.addEventListener(`install`,this.install),self.addEventListener(`activate`,this.activate),!0)}addToCacheList(e){let n=[];for(let r of e){typeof r==`string`?n.push(r):r&&r.revision===void 0&&n.push(r.url);let{cacheKey:e,url:i}=c(r),a=typeof r!=`string`&&r.revision?`reload`:`default`;if(this._urlsToCacheKeys.has(i)&&this._urlsToCacheKeys.get(i)!==e)throw new t(`add-to-cache-list-conflicting-entries`,{firstEntry:this._urlsToCacheKeys.get(i),secondEntry:e});if(typeof r!=`string`&&r.integrity){if(this._cacheKeysToIntegrities.has(e)&&this._cacheKeysToIntegrities.get(e)!==r.integrity)throw new t(`add-to-cache-list-conflicting-integrities`,{url:i});this._cacheKeysToIntegrities.set(e,r.integrity)}if(this._urlsToCacheKeys.set(i,e),this._urlsToCacheModes.set(i,a),n.length>0){let e=`Workbox is precaching URLs without revision info: ${n.join(`, `)}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(e)}}}install(e){return o(e,async()=>{let t=new l;this.strategy.plugins.push(t);for(let[t,n]of this._urlsToCacheKeys){let r=this._cacheKeysToIntegrities.get(n),i=this._urlsToCacheModes.get(t),a=new Request(t,{integrity:r,cache:i,credentials:`same-origin`});await Promise.all(this.strategy.handleAll({params:{cacheKey:n},request:a,event:e}))}let{updatedURLs:n,notUpdatedURLs:r}=t;return{updatedURLs:n,notUpdatedURLs:r}})}activate(e){return o(e,async()=>{let e=await self.caches.open(this.strategy.cacheName),t=await e.keys(),n=new Set(this._urlsToCacheKeys.values()),r=[];for(let i of t)n.has(i.url)||(await e.delete(i),r.push(i.url));return{deletedURLs:r}})}getURLsToCacheKeys(){return this._urlsToCacheKeys}getCachedURLs(){return[...this._urlsToCacheKeys.keys()]}getCacheKeyForURL(e){let t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForCacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){let t=e instanceof Request?e.url:e,n=this.getCacheKeyForURL(t);if(n)return(await self.caches.open(this.strategy.cacheName)).match(n)}createHandlerBoundToURL(e){let n=this.getCacheKeyForURL(e);if(!n)throw new t(`non-precached-url`,{url:e});return t=>(t.request=new Request(e),t.params=Object.assign({cacheKey:n},t.params),this.strategy.handle(t))}},D,O=()=>(D||=new E,D);try{self[`workbox:routing:7.3.0`]&&_()}catch{}var k=e=>e&&typeof e==`object`?e:{handle:e},A=class{constructor(e,t,n=`GET`){this.handler=k(t),this.match=e,this.method=n}setCatchHandler(e){this.catchHandler=k(e)}},j=class extends A{constructor(e,t,n){super(({url:t})=>{let n=e.exec(t.href);if(n&&!(t.origin!==location.origin&&n.index!==0))return n.slice(1)},t,n)}},M=class{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener(`fetch`,(e=>{let{request:t}=e,n=this.handleRequest({request:t,event:e});n&&e.respondWith(n)}))}addCacheListener(){self.addEventListener(`message`,(e=>{if(e.data&&e.data.type===`CACHE_URLS`){let{payload:t}=e.data,n=Promise.all(t.urlsToCache.map(t=>{typeof t==`string`&&(t=[t]);let n=new Request(...t);return this.handleRequest({request:n,event:e})}));e.waitUntil(n),e.ports&&e.ports[0]&&n.then(()=>e.ports[0].postMessage(!0))}}))}handleRequest({request:e,event:t}){let n=new URL(e.url,location.href);if(!n.protocol.startsWith(`http`))return;let r=n.origin===location.origin,{params:i,route:a}=this.findMatchingRoute({event:t,request:e,sameOrigin:r,url:n}),o=a&&a.handler,s=e.method;if(!o&&this._defaultHandlerMap.has(s)&&(o=this._defaultHandlerMap.get(s)),!o)return;let c;try{c=o.handle({url:n,request:e,event:t,params:i})}catch(e){c=Promise.reject(e)}let l=a&&a.catchHandler;return c instanceof Promise&&(this._catchHandler||l)&&(c=c.catch(async r=>{if(l)try{return await l.handle({url:n,request:e,event:t,params:i})}catch(e){e instanceof Error&&(r=e)}if(this._catchHandler)return this._catchHandler.handle({url:n,request:e,event:t});throw r})),c}findMatchingRoute({url:e,sameOrigin:t,request:n,event:r}){let i=this._routes.get(n.method)||[];for(let a of i){let i,o=a.match({url:e,sameOrigin:t,request:n,event:r});if(o)return i=o,(Array.isArray(i)&&i.length===0||o.constructor===Object&&Object.keys(o).length===0||typeof o==`boolean`)&&(i=void 0),{route:a,params:i}}return{}}setDefaultHandler(e,t=`GET`){this._defaultHandlerMap.set(t,k(e))}setCatchHandler(e){this._catchHandler=k(e)}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new t(`unregister-route-but-not-found-with-method`,{method:e.method});let n=this._routes.get(e.method).indexOf(e);if(n>-1)this._routes.get(e.method).splice(n,1);else throw new t(`unregister-route-route-not-registered`)}},N,P=()=>(N||(N=new M,N.addFetchListener(),N.addCacheListener()),N);function F(e,n,r){let i;if(typeof e==`string`){let t=new URL(e,location.href);i=new A(({url:e})=>e.href===t.href,n,r)}else if(e instanceof RegExp)i=new j(e,n,r);else if(typeof e==`function`)i=new A(e,n,r);else if(e instanceof A)i=e;else throw new t(`unsupported-route-type`,{moduleName:`workbox-routing`,funcName:`registerRoute`,paramName:`capture`});return P().registerRoute(i),i}function I(e,t=[]){for(let n of[...e.searchParams.keys()])t.some(e=>e.test(n))&&e.searchParams.delete(n);return e}function*L(e,{ignoreURLParametersMatching:t=[/^utm_/,/^fbclid$/],directoryIndex:n=`index.html`,cleanURLs:r=!0,urlManipulation:i}={}){let a=new URL(e,location.href);a.hash=``,yield a.href;let o=I(a,t);if(yield o.href,n&&o.pathname.endsWith(`/`)){let e=new URL(o.href);e.pathname+=n,yield e.href}if(r){let e=new URL(o.href);e.pathname+=`.html`,yield e.href}if(i){let e=i({url:a});for(let t of e)yield t.href}}var R=class extends A{constructor(e,t){super(({request:n})=>{let r=e.getURLsToCacheKeys();for(let i of L(n.url,t)){let t=r.get(i);if(t)return{cacheKey:t,integrity:e.getIntegrityForCacheKey(t)}}},e.strategy)}};function z(e){F(new R(O(),e))}function B(e){O().precache(e)}function V(e,t){B(e),z(t)}V([{"revision":"1872c500de691dce40960bb85481de07","url":"registerSW.js"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-192.svg"},{"revision":"e2e61f88ee712a28cc9a55adecc0812c","url":"index.html"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-512.svg"},{"revision":null,"url":"assets/extension-webview-HILvTnnn.js"},{"revision":null,"url":"assets/dist-D7KGU7Vl.js"},{"revision":null,"url":"assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2"},{"revision":null,"url":"assets/pdf-preview-BBVDS-z5.js"},{"revision":null,"url":"assets/KaTeX_AMS-Regular-BQhdFMY1.woff2"},{"revision":null,"url":"assets/scroll-area-BEllam7_.js"},{"revision":null,"url":"assets/database-D4DIhgi-.js"},{"revision":null,"url":"assets/csv-parser--2WJNgS7.js"},{"revision":null,"url":"assets/x-DlFGzN8d.js"},{"revision":null,"url":"assets/keybindings-store-B-zET-0o.js"},{"revision":null,"url":"assets/use-blob-url-e9uTXjv5.js"},{"revision":null,"url":"assets/index-Bce0weeW.css"},{"revision":null,"url":"assets/terminal-tab-BLIA53mt.js"},{"revision":null,"url":"assets/radar-KQ55EAFF-BviZcL-b.js"},{"revision":null,"url":"assets/vendor-xterm-CU2c3f0A.js"},{"revision":null,"url":"assets/code-editor-BTosKXkr.js"},{"revision":null,"url":"assets/input-Dk49gO8E.js"},{"revision":null,"url":"assets/video-preview-CKaht6nI.js"},{"revision":null,"url":"assets/KaTeX_Main-Regular-B22Nviop.woff2"},{"revision":null,"url":"assets/refresh-cw-LlbZDJpO.js"},{"revision":null,"url":"assets/pie-UPGHQEXC-D6S2MqVT.js"},{"revision":null,"url":"assets/treemap-KZPCXAKY-CM54VdaB.js"},{"revision":null,"url":"assets/react-GqWghJ-L.js"},{"revision":null,"url":"assets/github.min-D2BCvnWf.css"},{"revision":null,"url":"assets/vendor-mermaid-Dx86tuVP.js"},{"revision":null,"url":"assets/ai-settings-section-QE6nBNgN.js"},{"revision":null,"url":"assets/csv-preview-HMSavgBb.js"},{"revision":null,"url":"assets/katex-CKoArbIw.js"},{"revision":null,"url":"assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2"},{"revision":null,"url":"assets/info-3K5VOQVL-BwAZ2zd8.js"},{"revision":null,"url":"assets/chevron-right-BzAdxJRG.js"},{"revision":null,"url":"assets/extension-store-3yZYn07W.js"},{"revision":null,"url":"assets/markdown-renderer-D0MrsVJB.js"},{"revision":null,"url":"assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2"},{"revision":null,"url":"assets/vendor-markdown-0Mxgxy0L.js"},{"revision":null,"url":"assets/image-preview-0cJMnFZK.js"},{"revision":null,"url":"assets/api-client-Dvzcc_EO.js"},{"revision":null,"url":"assets/dist-im4ynINo.js"},{"revision":null,"url":"assets/KaTeX_Main-Italic-NWA7e6Wa.woff2"},{"revision":null,"url":"assets/table-Dq575bPF.js"},{"revision":null,"url":"assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2"},{"revision":null,"url":"assets/sqlite-viewer-D6mSIIx2.js"},{"revision":null,"url":"assets/packet-RMMSAZCW-tx2n5Qry.js"},{"revision":null,"url":"assets/port-forwarding-tab-ByKzBs-R.js"},{"revision":null,"url":"assets/plus-51UQ45rf.js"},{"revision":null,"url":"assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2"},{"revision":null,"url":"assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2"},{"revision":null,"url":"assets/arrow-up-Dtrfv490.js"},{"revision":null,"url":"assets/api-settings-DAk7D-NP.js"},{"revision":null,"url":"assets/architecture-PBZL5I3N-DvZbltvY.js"},{"revision":null,"url":"assets/rolldown-runtime-FhOqtrmT.js"},{"revision":null,"url":"assets/settings-store-BLLR7ed8.js"},{"revision":null,"url":"assets/use-monaco-theme-BkZDwoVd.js"},{"revision":null,"url":"assets/KaTeX_Math-Italic-t53AETM-.woff2"},{"revision":null,"url":"assets/github-dark-dimmed.min-BrpRStFV.css"},{"revision":null,"url":"assets/text-wrap-Cn6BNQfq.js"},{"revision":null,"url":"assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2"},{"revision":null,"url":"assets/KaTeX_Script-Regular-D3wIWfF6.woff2"},{"revision":null,"url":"assets/trash-2-CJYoLw7Q.js"},{"revision":null,"url":"assets/audio-preview-R7cq1uhJ.js"},{"revision":null,"url":"assets/file-exclamation-point-BwzaQ50n.js"},{"revision":null,"url":"assets/file-store-BrbCNyLm.js"},{"revision":null,"url":"assets/postgres-viewer-BnCbdR7g.js"},{"revision":null,"url":"assets/utils-CTg5uAYR.js"},{"revision":null,"url":"assets/columns-2-4fQcE4PF.js"},{"revision":null,"url":"assets/KaTeX_Main-Bold-Cx986IdX.woff2"},{"revision":null,"url":"assets/diff-viewer-DKLeIBkK.js"},{"revision":null,"url":"assets/settings-tab-BPdzUw3v.js"},{"revision":null,"url":"assets/KaTeX_Size2-Regular-Dy4dx90m.woff2"},{"revision":null,"url":"assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2"},{"revision":null,"url":"assets/KaTeX_Size1-Regular-mCD8mA8B.woff2"},{"revision":null,"url":"assets/tab-store-B3M9hjho.js"},{"revision":null,"url":"assets/keybindings-store-DaBV6qhz.js"},{"revision":null,"url":"assets/createLucideIcon-BjHrJDVb.js"},{"revision":null,"url":"assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2"},{"revision":null,"url":"assets/code-CuravVys.js"},{"revision":null,"url":"assets/chat-tab-umei1UkV.js"},{"revision":null,"url":"assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2"},{"revision":null,"url":"assets/sql-completion-provider-C3cq9j99.js"},{"revision":null,"url":"assets/vendor-ui-B-89Uj8i.js"},{"revision":null,"url":"assets/esm-K1XIK4vc.js"},{"revision":null,"url":"assets/gitGraph-HDMCJU4V-BxhdxFgj.js"},{"revision":null,"url":"assets/conflict-editor-dzofjxab.js"},{"revision":null,"url":"assets/index-DDBvHVVr.js"},{"revision":null,"url":"assets/vendor-xterm-BrP-ENHg.css"},{"revision":null,"url":"assets/database-viewer-5Uf8Rrls.js"},{"revision":null,"url":"assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2"},{"revision":null,"url":"assets/lib-DQHnkzGy.js"},{"revision":null,"url":"assets/sql-query-editor-CVAnRFbi.js"},{"revision":"d0f94ce046cf8cf09605ee7664dac557","url":"monacoeditorwork/html.worker.bundle.js"},{"revision":"a424156a79b9c1b907db93aa3180585a","url":"monacoeditorwork/editor.worker.bundle.js"},{"revision":"b3a7f967560c9816492a1567b3f7f0dc","url":"monacoeditorwork/css.worker.bundle.js"},{"revision":"a5d8a1acfc29c2a4c882a54ffc93def3","url":"monacoeditorwork/json.worker.bundle.js"},{"revision":"948e060affb598c339be40d69e1f6f9c","url":"monacoeditorwork/ts.worker.bundle.js"},{"revision":"79c8870653c8f419f2e3323085e1f4be","url":"manifest.webmanifest"}]),self.addEventListener(`push`,e=>{e.waitUntil(self.clients.matchAll({type:`window`,includeUncontrolled:!0}).then(t=>{if(t.some(e=>e.visibilityState===`visible`))return;let n=e.data?.json()??{title:`PPM`,body:`Chat completed`};return self.registration.showNotification(n.title,{body:n.body,icon:`/icon-192.png`,badge:`/icon-192.png`,tag:`ppm-chat-done`,silent:!1,data:{url:self.location.origin}})}))}),self.addEventListener(`notificationclick`,e=>{e.notification.close(),e.waitUntil(self.clients.matchAll({type:`window`,includeUncontrolled:!0}).then(t=>{for(let e of t)if(e.url.includes(self.location.origin)&&`focus`in e)return e.focus();return self.clients.openWindow(e.notification.data?.url||`/`)}))});
1
+ try{self[`workbox:core:7.3.0`]&&_()}catch{}var e=(e,...t)=>{let n=e;return t.length>0&&(n+=` :: ${JSON.stringify(t)}`),n},t=class extends Error{constructor(t,n){let r=e(t,n);super(r),this.name=t,this.details=n}},n={googleAnalytics:`googleAnalytics`,precache:`precache-v2`,prefix:`workbox`,runtime:`runtime`,suffix:typeof registration<`u`?registration.scope:``},r=e=>[n.prefix,e,n.suffix].filter(e=>e&&e.length>0).join(`-`),i=e=>{for(let t of Object.keys(n))e(t)},a={updateDetails:e=>{i(t=>{typeof e[t]==`string`&&(n[t]=e[t])})},getGoogleAnalyticsName:e=>e||r(n.googleAnalytics),getPrecacheName:e=>e||r(n.precache),getPrefix:()=>n.prefix,getRuntimeName:e=>e||r(n.runtime),getSuffix:()=>n.suffix};function o(e,t){let n=t();return e.waitUntil(n),n}try{self[`workbox:precaching:7.3.0`]&&_()}catch{}var s=`__WB_REVISION__`;function c(e){if(!e)throw new t(`add-to-cache-list-unexpected-type`,{entry:e});if(typeof e==`string`){let t=new URL(e,location.href);return{cacheKey:t.href,url:t.href}}let{revision:n,url:r}=e;if(!r)throw new t(`add-to-cache-list-unexpected-type`,{entry:e});if(!n){let e=new URL(r,location.href);return{cacheKey:e.href,url:e.href}}let i=new URL(r,location.href),a=new URL(r,location.href);return i.searchParams.set(s,n),{cacheKey:i.href,url:a.href}}var l=class{constructor(){this.updatedURLs=[],this.notUpdatedURLs=[],this.handlerWillStart=async({request:e,state:t})=>{t&&(t.originalRequest=e)},this.cachedResponseWillBeUsed=async({event:e,state:t,cachedResponse:n})=>{if(e.type===`install`&&t&&t.originalRequest&&t.originalRequest instanceof Request){let e=t.originalRequest.url;n?this.notUpdatedURLs.push(e):this.updatedURLs.push(e)}return n}}},u=class{constructor({precacheController:e}){this.cacheKeyWillBeUsed=async({request:e,params:t})=>{let n=t?.cacheKey||this._precacheController.getCacheKeyForURL(e.url);return n?new Request(n,{headers:e.headers}):e},this._precacheController=e}},d;function f(){if(d===void 0){let e=new Response(``);if(`body`in e)try{new Response(e.body),d=!0}catch{d=!1}d=!1}return d}async function p(e,n){let r=null;if(e.url&&(r=new URL(e.url).origin),r!==self.location.origin)throw new t(`cross-origin-copy-response`,{origin:r});let i=e.clone(),a={headers:new Headers(i.headers),status:i.status,statusText:i.statusText},o=n?n(a):a,s=f()?i.body:await i.blob();return new Response(s,o)}var m=e=>new URL(String(e),location.href).href.replace(RegExp(`^${location.origin}`),``);function h(e,t){let n=new URL(e);for(let e of t)n.searchParams.delete(e);return n.href}async function g(e,t,n,r){let i=h(t.url,n);if(t.url===i)return e.match(t,r);let a=Object.assign(Object.assign({},r),{ignoreSearch:!0}),o=await e.keys(t,a);for(let t of o)if(i===h(t.url,n))return e.match(t,r)}var v=class{constructor(){this.promise=new Promise((e,t)=>{this.resolve=e,this.reject=t})}},y=new Set;async function b(){for(let e of y)await e()}function x(e){return new Promise(t=>setTimeout(t,e))}try{self[`workbox:strategies:7.3.0`]&&_()}catch{}function S(e){return typeof e==`string`?new Request(e):e}var C=class{constructor(e,t){this._cacheKeys={},Object.assign(this,t),this.event=t.event,this._strategy=e,this._handlerDeferred=new v,this._extendLifetimePromises=[],this._plugins=[...e.plugins],this._pluginStateMap=new Map;for(let e of this._plugins)this._pluginStateMap.set(e,{});this.event.waitUntil(this._handlerDeferred.promise)}async fetch(e){let{event:n}=this,r=S(e);if(r.mode===`navigate`&&n instanceof FetchEvent&&n.preloadResponse){let e=await n.preloadResponse;if(e)return e}let i=this.hasCallback(`fetchDidFail`)?r.clone():null;try{for(let e of this.iterateCallbacks(`requestWillFetch`))r=await e({request:r.clone(),event:n})}catch(e){if(e instanceof Error)throw new t(`plugin-error-request-will-fetch`,{thrownErrorMessage:e.message})}let a=r.clone();try{let e;e=await fetch(r,r.mode===`navigate`?void 0:this._strategy.fetchOptions);for(let t of this.iterateCallbacks(`fetchDidSucceed`))e=await t({event:n,request:a,response:e});return e}catch(e){throw i&&await this.runCallbacks(`fetchDidFail`,{error:e,event:n,originalRequest:i.clone(),request:a.clone()}),e}}async fetchAndCachePut(e){let t=await this.fetch(e),n=t.clone();return this.waitUntil(this.cachePut(e,n)),t}async cacheMatch(e){let t=S(e),n,{cacheName:r,matchOptions:i}=this._strategy,a=await this.getCacheKey(t,`read`),o=Object.assign(Object.assign({},i),{cacheName:r});n=await caches.match(a,o);for(let e of this.iterateCallbacks(`cachedResponseWillBeUsed`))n=await e({cacheName:r,matchOptions:i,cachedResponse:n,request:a,event:this.event})||void 0;return n}async cachePut(e,n){let r=S(e);await x(0);let i=await this.getCacheKey(r,`write`);if(!n)throw new t(`cache-put-with-no-response`,{url:m(i.url)});let a=await this._ensureResponseSafeToCache(n);if(!a)return!1;let{cacheName:o,matchOptions:s}=this._strategy,c=await self.caches.open(o),l=this.hasCallback(`cacheDidUpdate`),u=l?await g(c,i.clone(),[`__WB_REVISION__`],s):null;try{await c.put(i,l?a.clone():a)}catch(e){if(e instanceof Error)throw e.name===`QuotaExceededError`&&await b(),e}for(let e of this.iterateCallbacks(`cacheDidUpdate`))await e({cacheName:o,oldResponse:u,newResponse:a.clone(),request:i,event:this.event});return!0}async getCacheKey(e,t){let n=`${e.url} | ${t}`;if(!this._cacheKeys[n]){let r=e;for(let e of this.iterateCallbacks(`cacheKeyWillBeUsed`))r=S(await e({mode:t,request:r,event:this.event,params:this.params}));this._cacheKeys[n]=r}return this._cacheKeys[n]}hasCallback(e){for(let t of this._strategy.plugins)if(e in t)return!0;return!1}async runCallbacks(e,t){for(let n of this.iterateCallbacks(e))await n(t)}*iterateCallbacks(e){for(let t of this._strategy.plugins)if(typeof t[e]==`function`){let n=this._pluginStateMap.get(t);yield r=>{let i=Object.assign(Object.assign({},r),{state:n});return t[e](i)}}}waitUntil(e){return this._extendLifetimePromises.push(e),e}async doneWaiting(){for(;this._extendLifetimePromises.length;){let e=this._extendLifetimePromises.splice(0),t=(await Promise.allSettled(e)).find(e=>e.status===`rejected`);if(t)throw t.reason}}destroy(){this._handlerDeferred.resolve(null)}async _ensureResponseSafeToCache(e){let t=e,n=!1;for(let e of this.iterateCallbacks(`cacheWillUpdate`))if(t=await e({request:this.request,response:t,event:this.event})||void 0,n=!0,!t)break;return n||t&&t.status!==200&&(t=void 0),t}},w=class{constructor(e={}){this.cacheName=a.getRuntimeName(e.cacheName),this.plugins=e.plugins||[],this.fetchOptions=e.fetchOptions,this.matchOptions=e.matchOptions}handle(e){let[t]=this.handleAll(e);return t}handleAll(e){e instanceof FetchEvent&&(e={event:e,request:e.request});let t=e.event,n=typeof e.request==`string`?new Request(e.request):e.request,r=`params`in e?e.params:void 0,i=new C(this,{event:t,request:n,params:r}),a=this._getResponse(i,n,t);return[a,this._awaitComplete(a,i,n,t)]}async _getResponse(e,n,r){await e.runCallbacks(`handlerWillStart`,{event:r,request:n});let i;try{if(i=await this._handle(n,e),!i||i.type===`error`)throw new t(`no-response`,{url:n.url})}catch(t){if(t instanceof Error){for(let a of e.iterateCallbacks(`handlerDidError`))if(i=await a({error:t,event:r,request:n}),i)break}if(!i)throw t}for(let t of e.iterateCallbacks(`handlerWillRespond`))i=await t({event:r,request:n,response:i});return i}async _awaitComplete(e,t,n,r){let i,a;try{i=await e}catch{}try{await t.runCallbacks(`handlerDidRespond`,{event:r,request:n,response:i}),await t.doneWaiting()}catch(e){e instanceof Error&&(a=e)}if(await t.runCallbacks(`handlerDidComplete`,{event:r,request:n,response:i,error:a}),t.destroy(),a)throw a}},T=class e extends w{constructor(t={}){t.cacheName=a.getPrecacheName(t.cacheName),super(t),this._fallbackToNetwork=t.fallbackToNetwork!==!1,this.plugins.push(e.copyRedirectedCacheableResponsesPlugin)}async _handle(e,t){return await t.cacheMatch(e)||(t.event&&t.event.type===`install`?await this._handleInstall(e,t):await this._handleFetch(e,t))}async _handleFetch(e,n){let r,i=n.params||{};if(this._fallbackToNetwork){let t=i.integrity,a=e.integrity,o=!a||a===t;r=await n.fetch(new Request(e,{integrity:e.mode===`no-cors`?void 0:a||t})),t&&o&&e.mode!==`no-cors`&&(this._useDefaultCacheabilityPluginIfNeeded(),await n.cachePut(e,r.clone()))}else throw new t(`missing-precache-entry`,{cacheName:this.cacheName,url:e.url});return r}async _handleInstall(e,n){this._useDefaultCacheabilityPluginIfNeeded();let r=await n.fetch(e);if(!await n.cachePut(e,r.clone()))throw new t(`bad-precaching-response`,{url:e.url,status:r.status});return r}_useDefaultCacheabilityPluginIfNeeded(){let t=null,n=0;for(let[r,i]of this.plugins.entries())i!==e.copyRedirectedCacheableResponsesPlugin&&(i===e.defaultPrecacheCacheabilityPlugin&&(t=r),i.cacheWillUpdate&&n++);n===0?this.plugins.push(e.defaultPrecacheCacheabilityPlugin):n>1&&t!==null&&this.plugins.splice(t,1)}};T.defaultPrecacheCacheabilityPlugin={async cacheWillUpdate({response:e}){return!e||e.status>=400?null:e}},T.copyRedirectedCacheableResponsesPlugin={async cacheWillUpdate({response:e}){return e.redirected?await p(e):e}};var E=class{constructor({cacheName:e,plugins:t=[],fallbackToNetwork:n=!0}={}){this._urlsToCacheKeys=new Map,this._urlsToCacheModes=new Map,this._cacheKeysToIntegrities=new Map,this._strategy=new T({cacheName:a.getPrecacheName(e),plugins:[...t,new u({precacheController:this})],fallbackToNetwork:n}),this.install=this.install.bind(this),this.activate=this.activate.bind(this)}get strategy(){return this._strategy}precache(e){this.addToCacheList(e),this._installAndActiveListenersAdded||=(self.addEventListener(`install`,this.install),self.addEventListener(`activate`,this.activate),!0)}addToCacheList(e){let n=[];for(let r of e){typeof r==`string`?n.push(r):r&&r.revision===void 0&&n.push(r.url);let{cacheKey:e,url:i}=c(r),a=typeof r!=`string`&&r.revision?`reload`:`default`;if(this._urlsToCacheKeys.has(i)&&this._urlsToCacheKeys.get(i)!==e)throw new t(`add-to-cache-list-conflicting-entries`,{firstEntry:this._urlsToCacheKeys.get(i),secondEntry:e});if(typeof r!=`string`&&r.integrity){if(this._cacheKeysToIntegrities.has(e)&&this._cacheKeysToIntegrities.get(e)!==r.integrity)throw new t(`add-to-cache-list-conflicting-integrities`,{url:i});this._cacheKeysToIntegrities.set(e,r.integrity)}if(this._urlsToCacheKeys.set(i,e),this._urlsToCacheModes.set(i,a),n.length>0){let e=`Workbox is precaching URLs without revision info: ${n.join(`, `)}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(e)}}}install(e){return o(e,async()=>{let t=new l;this.strategy.plugins.push(t);for(let[t,n]of this._urlsToCacheKeys){let r=this._cacheKeysToIntegrities.get(n),i=this._urlsToCacheModes.get(t),a=new Request(t,{integrity:r,cache:i,credentials:`same-origin`});await Promise.all(this.strategy.handleAll({params:{cacheKey:n},request:a,event:e}))}let{updatedURLs:n,notUpdatedURLs:r}=t;return{updatedURLs:n,notUpdatedURLs:r}})}activate(e){return o(e,async()=>{let e=await self.caches.open(this.strategy.cacheName),t=await e.keys(),n=new Set(this._urlsToCacheKeys.values()),r=[];for(let i of t)n.has(i.url)||(await e.delete(i),r.push(i.url));return{deletedURLs:r}})}getURLsToCacheKeys(){return this._urlsToCacheKeys}getCachedURLs(){return[...this._urlsToCacheKeys.keys()]}getCacheKeyForURL(e){let t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForCacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){let t=e instanceof Request?e.url:e,n=this.getCacheKeyForURL(t);if(n)return(await self.caches.open(this.strategy.cacheName)).match(n)}createHandlerBoundToURL(e){let n=this.getCacheKeyForURL(e);if(!n)throw new t(`non-precached-url`,{url:e});return t=>(t.request=new Request(e),t.params=Object.assign({cacheKey:n},t.params),this.strategy.handle(t))}},D,O=()=>(D||=new E,D);try{self[`workbox:routing:7.3.0`]&&_()}catch{}var k=e=>e&&typeof e==`object`?e:{handle:e},A=class{constructor(e,t,n=`GET`){this.handler=k(t),this.match=e,this.method=n}setCatchHandler(e){this.catchHandler=k(e)}},j=class extends A{constructor(e,t,n){super(({url:t})=>{let n=e.exec(t.href);if(n&&!(t.origin!==location.origin&&n.index!==0))return n.slice(1)},t,n)}},M=class{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener(`fetch`,(e=>{let{request:t}=e,n=this.handleRequest({request:t,event:e});n&&e.respondWith(n)}))}addCacheListener(){self.addEventListener(`message`,(e=>{if(e.data&&e.data.type===`CACHE_URLS`){let{payload:t}=e.data,n=Promise.all(t.urlsToCache.map(t=>{typeof t==`string`&&(t=[t]);let n=new Request(...t);return this.handleRequest({request:n,event:e})}));e.waitUntil(n),e.ports&&e.ports[0]&&n.then(()=>e.ports[0].postMessage(!0))}}))}handleRequest({request:e,event:t}){let n=new URL(e.url,location.href);if(!n.protocol.startsWith(`http`))return;let r=n.origin===location.origin,{params:i,route:a}=this.findMatchingRoute({event:t,request:e,sameOrigin:r,url:n}),o=a&&a.handler,s=e.method;if(!o&&this._defaultHandlerMap.has(s)&&(o=this._defaultHandlerMap.get(s)),!o)return;let c;try{c=o.handle({url:n,request:e,event:t,params:i})}catch(e){c=Promise.reject(e)}let l=a&&a.catchHandler;return c instanceof Promise&&(this._catchHandler||l)&&(c=c.catch(async r=>{if(l)try{return await l.handle({url:n,request:e,event:t,params:i})}catch(e){e instanceof Error&&(r=e)}if(this._catchHandler)return this._catchHandler.handle({url:n,request:e,event:t});throw r})),c}findMatchingRoute({url:e,sameOrigin:t,request:n,event:r}){let i=this._routes.get(n.method)||[];for(let a of i){let i,o=a.match({url:e,sameOrigin:t,request:n,event:r});if(o)return i=o,(Array.isArray(i)&&i.length===0||o.constructor===Object&&Object.keys(o).length===0||typeof o==`boolean`)&&(i=void 0),{route:a,params:i}}return{}}setDefaultHandler(e,t=`GET`){this._defaultHandlerMap.set(t,k(e))}setCatchHandler(e){this._catchHandler=k(e)}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new t(`unregister-route-but-not-found-with-method`,{method:e.method});let n=this._routes.get(e.method).indexOf(e);if(n>-1)this._routes.get(e.method).splice(n,1);else throw new t(`unregister-route-route-not-registered`)}},N,P=()=>(N||(N=new M,N.addFetchListener(),N.addCacheListener()),N);function F(e,n,r){let i;if(typeof e==`string`){let t=new URL(e,location.href);i=new A(({url:e})=>e.href===t.href,n,r)}else if(e instanceof RegExp)i=new j(e,n,r);else if(typeof e==`function`)i=new A(e,n,r);else if(e instanceof A)i=e;else throw new t(`unsupported-route-type`,{moduleName:`workbox-routing`,funcName:`registerRoute`,paramName:`capture`});return P().registerRoute(i),i}function I(e,t=[]){for(let n of[...e.searchParams.keys()])t.some(e=>e.test(n))&&e.searchParams.delete(n);return e}function*L(e,{ignoreURLParametersMatching:t=[/^utm_/,/^fbclid$/],directoryIndex:n=`index.html`,cleanURLs:r=!0,urlManipulation:i}={}){let a=new URL(e,location.href);a.hash=``,yield a.href;let o=I(a,t);if(yield o.href,n&&o.pathname.endsWith(`/`)){let e=new URL(o.href);e.pathname+=n,yield e.href}if(r){let e=new URL(o.href);e.pathname+=`.html`,yield e.href}if(i){let e=i({url:a});for(let t of e)yield t.href}}var R=class extends A{constructor(e,t){super(({request:n})=>{let r=e.getURLsToCacheKeys();for(let i of L(n.url,t)){let t=r.get(i);if(t)return{cacheKey:t,integrity:e.getIntegrityForCacheKey(t)}}},e.strategy)}};function z(e){F(new R(O(),e))}function B(e){O().precache(e)}function V(e,t){B(e),z(t)}V([{"revision":"1872c500de691dce40960bb85481de07","url":"registerSW.js"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-192.svg"},{"revision":"7bb21b98483de3a88d92d2f3edb2e8fa","url":"index.html"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-512.svg"},{"revision":null,"url":"assets/lib-BqkcKGFq.js"},{"revision":null,"url":"assets/dist-D7KGU7Vl.js"},{"revision":null,"url":"assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2"},{"revision":null,"url":"assets/KaTeX_AMS-Regular-BQhdFMY1.woff2"},{"revision":null,"url":"assets/tab-store-Jvy1eZGM.js"},{"revision":null,"url":"assets/architecture-PBZL5I3N-Dy3PgD6O.js"},{"revision":null,"url":"assets/sql-query-editor-vpD0I0KG.js"},{"revision":null,"url":"assets/pdf-preview-CqoQE09t.js"},{"revision":null,"url":"assets/image-preview-DAuPOzYl.js"},{"revision":null,"url":"assets/packet-RMMSAZCW-DpzHf4xp.js"},{"revision":null,"url":"assets/terminal-tab-D7u7wsyb.js"},{"revision":null,"url":"assets/KaTeX_Main-Regular-B22Nviop.woff2"},{"revision":null,"url":"assets/code-editor-BJ1tSNWA.js"},{"revision":null,"url":"assets/scroll-area-D0EQpAH2.js"},{"revision":null,"url":"assets/vendor-xterm-D7SePDJp.js"},{"revision":null,"url":"assets/keybindings-store-V12kZZHO.js"},{"revision":null,"url":"assets/file-exclamation-point-Baz81y5z.js"},{"revision":null,"url":"assets/github.min-D2BCvnWf.css"},{"revision":null,"url":"assets/csv-preview-C9qGhDlb.js"},{"revision":null,"url":"assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2"},{"revision":null,"url":"assets/chevron-right-BzAdxJRG.js"},{"revision":null,"url":"assets/file-store-BgZggznw.js"},{"revision":null,"url":"assets/x-BtqbfkR7.js"},{"revision":null,"url":"assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2"},{"revision":null,"url":"assets/ai-settings-section-DeW4WN43.js"},{"revision":null,"url":"assets/pie-UPGHQEXC-BpzFCKJ8.js"},{"revision":null,"url":"assets/vendor-mermaid-DCxaaPi4.js"},{"revision":null,"url":"assets/vendor-markdown-0Mxgxy0L.js"},{"revision":null,"url":"assets/conflict-editor-CrgrMZ2F.js"},{"revision":null,"url":"assets/KaTeX_Main-Italic-NWA7e6Wa.woff2"},{"revision":null,"url":"assets/chat-tab-UFEFOnpl.js"},{"revision":null,"url":"assets/trash-2-BgDIBl6f.js"},{"revision":null,"url":"assets/use-blob-url-BgxxT-n_.js"},{"revision":null,"url":"assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2"},{"revision":null,"url":"assets/video-preview-BSDzqlzk.js"},{"revision":null,"url":"assets/dist-BqoEabX7.js"},{"revision":null,"url":"assets/port-forwarding-tab-De7qxkjp.js"},{"revision":null,"url":"assets/input-bGJExpJZ.js"},{"revision":null,"url":"assets/plus-51UQ45rf.js"},{"revision":null,"url":"assets/api-settings-t7Leca7J.js"},{"revision":null,"url":"assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2"},{"revision":null,"url":"assets/sqlite-viewer-Ccz2crvN.js"},{"revision":null,"url":"assets/extension-webview-B95nOfj-.js"},{"revision":null,"url":"assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2"},{"revision":null,"url":"assets/arrow-up-Dtrfv490.js"},{"revision":null,"url":"assets/refresh-cw-CSFrDtiu.js"},{"revision":null,"url":"assets/rolldown-runtime-FhOqtrmT.js"},{"revision":null,"url":"assets/radar-KQ55EAFF-DAxWKxM4.js"},{"revision":null,"url":"assets/markdown-renderer-DwINRWo4.js"},{"revision":null,"url":"assets/KaTeX_Math-Italic-t53AETM-.woff2"},{"revision":null,"url":"assets/github-dark-dimmed.min-BrpRStFV.css"},{"revision":null,"url":"assets/index-DJOjXTcq.js"},{"revision":null,"url":"assets/diff-viewer-C2eOczTs.js"},{"revision":null,"url":"assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2"},{"revision":null,"url":"assets/KaTeX_Script-Regular-D3wIWfF6.woff2"},{"revision":null,"url":"assets/sql-completion-provider-EzHOQLfo.js"},{"revision":null,"url":"assets/database-viewer-e_NAkIL_.js"},{"revision":null,"url":"assets/esm-B3je8j5P.js"},{"revision":null,"url":"assets/index-DSOP0R0s.css"},{"revision":null,"url":"assets/treemap-KZPCXAKY-D6dgXbAe.js"},{"revision":null,"url":"assets/KaTeX_Main-Bold-Cx986IdX.woff2"},{"revision":null,"url":"assets/react-BkWDCPD7.js"},{"revision":null,"url":"assets/database-DCT0OjgQ.js"},{"revision":null,"url":"assets/settings-tab-BdTEumwU.js"},{"revision":null,"url":"assets/KaTeX_Size2-Regular-Dy4dx90m.woff2"},{"revision":null,"url":"assets/audio-preview-BSAe2WQB.js"},{"revision":null,"url":"assets/gitGraph-HDMCJU4V-Bu1SIFFq.js"},{"revision":null,"url":"assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2"},{"revision":null,"url":"assets/KaTeX_Size1-Regular-mCD8mA8B.woff2"},{"revision":null,"url":"assets/api-client-r4nyVy7H.js"},{"revision":null,"url":"assets/createLucideIcon-BjHrJDVb.js"},{"revision":null,"url":"assets/info-3K5VOQVL-DzfAxmVd.js"},{"revision":null,"url":"assets/settings-store-CdcSAgEZ.js"},{"revision":null,"url":"assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2"},{"revision":null,"url":"assets/code-CuravVys.js"},{"revision":null,"url":"assets/katex-bpagxk3Z.js"},{"revision":null,"url":"assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2"},{"revision":null,"url":"assets/vendor-ui-B-89Uj8i.js"},{"revision":null,"url":"assets/utils-ChWX7pZv.js"},{"revision":null,"url":"assets/text-wrap-DzvCTq_i.js"},{"revision":null,"url":"assets/postgres-viewer-Dd6rLb8b.js"},{"revision":null,"url":"assets/csv-parser-DxVplKKB.js"},{"revision":null,"url":"assets/vendor-xterm-BrP-ENHg.css"},{"revision":null,"url":"assets/use-monaco-theme-dtPsv6sh.js"},{"revision":null,"url":"assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2"},{"revision":null,"url":"assets/table-DbSviOmw.js"},{"revision":"d0f94ce046cf8cf09605ee7664dac557","url":"monacoeditorwork/html.worker.bundle.js"},{"revision":"a424156a79b9c1b907db93aa3180585a","url":"monacoeditorwork/editor.worker.bundle.js"},{"revision":"b3a7f967560c9816492a1567b3f7f0dc","url":"monacoeditorwork/css.worker.bundle.js"},{"revision":"a5d8a1acfc29c2a4c882a54ffc93def3","url":"monacoeditorwork/json.worker.bundle.js"},{"revision":"948e060affb598c339be40d69e1f6f9c","url":"monacoeditorwork/ts.worker.bundle.js"},{"revision":"79c8870653c8f419f2e3323085e1f4be","url":"manifest.webmanifest"}]),self.addEventListener(`push`,e=>{e.waitUntil(self.clients.matchAll({type:`window`,includeUncontrolled:!0}).then(t=>{if(t.some(e=>e.visibilityState===`visible`))return;let n=e.data?.json()??{title:`PPM`,body:`Chat completed`};return self.registration.showNotification(n.title,{body:n.body,icon:`/icon-192.png`,badge:`/icon-192.png`,tag:`ppm-chat-done`,silent:!1,data:{url:self.location.origin}})}))}),self.addEventListener(`notificationclick`,e=>{e.notification.close(),e.waitUntil(self.clients.matchAll({type:`window`,includeUncontrolled:!0}).then(t=>{for(let e of t)if(e.url.includes(self.location.origin)&&`focus`in e)return e.focus();return self.clients.openWindow(e.notification.data?.url||`/`)}))});
@@ -347,6 +347,8 @@ src/
347
347
  - **PanelStore** — Grid layout, panel creation, keep-alive snapshots
348
348
  - **FileStore** — File cache
349
349
  - **SettingsStore** — Theme, sidebar, git view, device name
350
+ - **CompareStore** — File compare selection (path, project, dirty content); persists to localStorage with >500KB guard; auto-clears on project switch
351
+ - **KeybindingsStore** — Custom keybinding overrides (includes `compare-files` action with default `Mod+Alt+D`)
350
352
  - **Pattern:** Zustand for state, React.lazy() for tab content splitting
351
353
 
352
354
  ## Data Flow Diagrams
@@ -0,0 +1,53 @@
1
+ # Compare-Files Feature Ship
2
+
3
+ **Date**: 2026-04-22 14:35
4
+ **Severity**: Low
5
+ **Component**: Web IDE / Diff Viewer
6
+ **Status**: Resolved
7
+
8
+ ## What Happened
9
+
10
+ Shipped compare-files feature (file diff picker) for PPM web IDE. Four trigger paths — tab context menu, file-tree context menu, command palette, keyboard shortcut `Mod+Alt+D`. Reused existing DiffViewer + git-diff tab type; zero backend changes. Added zustand store with persist middleware, ComparePicker singleton at App root, and in-flight guard to prevent double-invoke race.
11
+
12
+ ## Technical Details
13
+
14
+ - **370 new lines of code** across 3 new files + 6 edited files
15
+ - **Zustand store** (`useCompareStore`) with `persist` middleware that strips dirty content >500KB
16
+ - **Dirty-buffer semantic**: snapshot at select-time, not compare-time (closing the tab doesn't invalidate selection)
17
+ - **Custom event** (`open-compare-picker`) dispatched from 4 trigger points, singleton listener mounted at App root
18
+ - **Keyboard shortcut**: `Mod+Alt+D` single-stroke (chord parser doesn't support `Cmd+K D` yet)
19
+ - **Module-scope subscription**: `useProjectStore.subscribe` auto-clears selection on project switch
20
+ - **In-flight guard**: ref-based tracking in `handlePick` to block concurrent invocations (reviewer comment M5)
21
+ - **Test coverage**: 1698 pass, 48 pre-existing unrelated failures, 0 new regressions
22
+ - **Code review**: 7.5/10, 0 critical, 6 major (1 addressed: M5; 3 deferred: M3/M4/M6)
23
+
24
+ ## Why This Matters
25
+
26
+ Snapshot-at-select semantics felt risky initially but aligns with user mental model: "I'm picking a version to diff." Closing the tab doesn't invalidate the selection — this is more intuitive than invalidating on every interaction. Deferring byte-accurate persist limit (M4), clear-on-error (M3), and legacy multi-select-compare unification (M6) kept scope tight; all are low-risk YAGNI.
27
+
28
+ ## Key Decisions
29
+
30
+ 1. **Same-project only**: Cross-project selection auto-cleared rather than supported. Reduces state complexity; users can switch projects and re-pick.
31
+ 2. **Single-stroke `Mod+Alt+D`**: Chord parser limitation means `Cmd+K D` unsupported. Single-stroke is good enough; avoiding yak-shave on chord parsing.
32
+ 3. **Snapshot timing**: Capturing dirty buffer at select-time (not compare-time) prevents stale diffs after file edits. User closes tab; selection still valid.
33
+ 4. **Defer judgment calls**: M3/M4/M6 are correctness edge-cases, not showstoppers. Shipping first; addressing in follow-up if telemetry justifies.
34
+
35
+ ## Lessons Learned
36
+
37
+ - **Persist middleware footprint**: 500KB strip limit is conservative but safe; no observed bloat in dev.
38
+ - **Custom events + singleton**: Avoids prop-drilling 4 trigger points through component tree. Event-driven pattern cleaner than callback props.
39
+ - **In-flight guard as ref**: Simpler than promise-based debounce; prevents race without async complexity.
40
+ - **Deferred ≠ broken**: 6 major review comments sound high, but only 1 was blocking (M5). Others are judgment-calls; shipping with known trade-offs is valid.
41
+
42
+ ## Next Steps
43
+
44
+ - Monitor telemetry for M3 (clear-on-error) if users report stale diffs
45
+ - Consider byte-accurate persist limit (M4) if DB bloat observed
46
+ - Unify legacy multi-select-compare path (M6) in v0.14 if feature gains adoption
47
+
48
+ Commit: `cc09bb5b9e652a2feefde4e533934a6f53301895`
49
+
50
+ ---
51
+
52
+ **Status:** Resolved
53
+ **Summary:** Shipped compare-files feature with 4 trigger paths, zustand persist store, and event-driven ComparePicker. 370 new LOC, 0 new test regressions, 1 review blocker addressed (M5), 3 judgment-calls deferred (M3/M4/M6). Snapshot-at-select semantics and same-project-only scope kept complexity tight.
@@ -20,9 +20,17 @@ All notable changes to PPM are documented here. Format follows [Keep a Changelog
20
20
 
21
21
  ---
22
22
 
23
- ## [Unreleased] — Lazy-Load File Tree + Palette Index, Session Tagging, Jira Debug Session Redesign, Frontend Memory Optimization, Git-Graph Enhancements
23
+ ## [Unreleased] — Lazy-Load File Tree + Palette Index, Session Tagging, File Compare, Jira Debug Session Redesign, Frontend Memory Optimization, Git-Graph Enhancements
24
24
 
25
25
  ### Added
26
+ - **File Compare** — Side-by-side diff viewer for comparing two files or file versions
27
+ - Four triggers: (1) tab right-click "Select for Compare" / "Compare with Selected", (2) file tree right-click (same), (3) command palette "Compare Files...", (4) keyboard shortcut `Mod+Alt+D`
28
+ - Reuses existing `DiffViewer` component + `git-diff` tab type + `/files/compare` API — no new backend endpoints
29
+ - Supports dirty buffer content: unsaved editor changes captured at select-time
30
+ - New zustand store `useCompareStore` persists selection across reload (strips dirty content >500KB to keep localStorage fast)
31
+ - Auto-clears selection on project switch via store subscription
32
+ - New keybinding action `compare-files` with customizable default `Mod+Alt+D` in Settings > Keybindings
33
+
26
34
  - **Lazy-Load File Tree + Palette Index** — Instant project opening on large codebases
27
35
  - Backend: `GET /api/project/:name/files/list?path=<rel>` for 1-level directory listing with gitignore decoration
28
36
  - Backend: `GET /api/project/:name/files/index` for flat full-project index (cached, watcher-invalidated)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.13.3",
3
+ "version": "0.13.4",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "author": "hienlh",
6
6
  "license": "MIT",
@@ -104,6 +104,12 @@ function globPatternToRegex(pattern: string): RegExp {
104
104
  // Strip leading ./
105
105
  if (p.startsWith("./")) p = p.slice(2);
106
106
 
107
+ // Leading `**/` should match zero-or-more path segments, INCLUDING root.
108
+ // So `**/.git` matches both `.git` (at root) and `src/.git` (nested).
109
+ // Strip `**/` prefix here so it doesn't force a leading path; we handle it via optional group below.
110
+ const hasStarstarPrefix = p.startsWith("**/");
111
+ if (hasStarstarPrefix) p = p.slice(3);
112
+
107
113
  const escaped = p
108
114
  .replace(/[.+^${}()|[\]]/g, "\\$&") // escape regex special chars (not * ?)
109
115
  .replace(/\*\*/g, "\x00") // temp: ** placeholder
@@ -111,10 +117,17 @@ function globPatternToRegex(pattern: string): RegExp {
111
117
  .replace(/\x00/g, ".*") // ** = any path
112
118
  .replace(/\?/g, "[^/]"); // ? = single non-slash char
113
119
 
114
- // Pattern with no slash (e.g. *.log) → match at any depth
115
- const re = p.includes("/")
116
- ? new RegExp(`^${escaped}(/|$)`)
117
- : new RegExp(`(^|/)${escaped}(/|$)`);
120
+ let re: RegExp;
121
+ if (hasStarstarPrefix) {
122
+ // `**/X` → match X at any depth including root: `(^|.*/)X(/|$)`
123
+ re = new RegExp(`(^|.*/)${escaped}(/|$)`);
124
+ } else if (p.includes("/")) {
125
+ // Anchored pattern with explicit path
126
+ re = new RegExp(`^${escaped}(/|$)`);
127
+ } else {
128
+ // Pattern with no slash (e.g. *.log) → match at any depth
129
+ re = new RegExp(`(^|/)${escaped}(/|$)`);
130
+ }
118
131
 
119
132
  regexCache.set(pattern, re);
120
133
  return re;
@@ -151,20 +151,24 @@ function walkForIndex(
151
151
  const relPosix = relPath.split("\\").join("/");
152
152
 
153
153
  // Apply glob exclusion (check full relative path and bare entry name)
154
+ // These are HARD excludes — .git, node_modules, dist, etc.
154
155
  if (matchesGlob(relPosix, allExclude)) continue;
155
156
  if (matchesGlob(entry.name, allExclude)) continue;
156
157
 
157
- // Apply gitignore rules
158
+ // Apply gitignore rules — SOFT exclude for files (include with isIgnored flag),
159
+ // HARD exclude for directories (skip recursion to avoid walking huge gitignored dirs).
160
+ let isIgnored = false;
158
161
  if (ig) {
159
162
  const checkPath = entry.isDirectory() ? `${relPosix}/` : relPosix;
160
- if (ig.ignores(checkPath) || ig.ignores(relPosix)) continue;
163
+ isIgnored = ig.ignores(checkPath) || ig.ignores(relPosix);
164
+ if (isIgnored && entry.isDirectory()) continue;
161
165
  }
162
166
 
163
167
  if (entry.isDirectory()) {
164
168
  results.push({ path: relPosix, name: entry.name, type: "directory" });
165
169
  walkForIndex(rootPath, fullPath, allExclude, ig, results);
166
170
  } else {
167
- results.push({ path: relPosix, name: entry.name, type: "file" });
171
+ results.push({ path: relPosix, name: entry.name, type: "file", ...(isIgnored && { isIgnored: true }) });
168
172
  }
169
173
  }
170
174
  }
@@ -25,6 +25,8 @@ export interface FileEntry {
25
25
  path: string;
26
26
  name: string;
27
27
  type: "file" | "directory";
28
+ /** True if file is excluded by .gitignore but still surfaced in palette for discoverability (e.g. .env) */
29
+ isIgnored?: boolean;
28
30
  }
29
31
 
30
32
  /** Entry returned by /files/list (single directory level) */
package/src/web/app.tsx CHANGED
@@ -24,6 +24,7 @@ import { useGlobalKeybindings } from "@/hooks/use-global-keybindings";
24
24
  import { useNotificationBadge } from "@/hooks/use-notification-badge";
25
25
  import { useServerReload } from "@/hooks/use-server-reload";
26
26
  import { CommandPalette } from "@/components/layout/command-palette";
27
+ import { ComparePicker } from "@/components/editor/compare-picker";
27
28
  import { BugReportPopup } from "@/components/shared/bug-report-popup";
28
29
  import { UpgradeBanner } from "@/components/layout/upgrade-banner";
29
30
  import { ImageOverlay } from "@/components/shared/image-overlay";
@@ -299,6 +300,9 @@ export function App() {
299
300
  {/* Command palette (Shift+Shift) */}
300
301
  <CommandPalette open={paletteOpen} onClose={closePalette} initialQuery={paletteInitialQuery} />
301
302
 
303
+ {/* Compare Files picker (Mod+Alt+D, palette, context menus) — singleton */}
304
+ <ComparePicker />
305
+
302
306
  {/* Global bug report popup */}
303
307
  <BugReportPopup />
304
308
 
@@ -0,0 +1,245 @@
1
+ import { useState, useMemo, useEffect, useRef } from "react";
2
+ import { Columns2, FileCode, X } from "lucide-react";
3
+ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
4
+ import { useTabStore } from "@/stores/tab-store";
5
+ import { useFileStore } from "@/stores/file-store";
6
+ import { useProjectStore } from "@/stores/project-store";
7
+ import { useCompareStore, type CompareSelection } from "@/stores/compare-store";
8
+ import { openCompareTab } from "@/lib/open-compare-tab";
9
+ import { basename, cn } from "@/lib/utils";
10
+ import { scoreFileSearch, compareScores } from "@/lib/score-file-search";
11
+
12
+ interface Candidate {
13
+ id: string;
14
+ path: string;
15
+ label: string;
16
+ source: "tab" | "file";
17
+ dirtyContent?: string;
18
+ }
19
+
20
+ interface ComparePickerProps {
21
+ /** Controlled mode: parent manages open state. Omit both to use singleton/event mode. */
22
+ open?: boolean;
23
+ onOpenChange?: (o: boolean) => void;
24
+ /** If provided, dialog pre-seeds Side A. Ignored in singleton mode (reads from store on open). */
25
+ initialA?: CompareSelection | null;
26
+ }
27
+
28
+ const MAX_RESULTS = 50;
29
+
30
+ /**
31
+ * File-compare picker.
32
+ *
33
+ * Two modes:
34
+ * - Controlled: pass `open`+`onOpenChange` (for tests / programmatic callers).
35
+ * - Singleton: mount once at app root with no props — listens for
36
+ * `window` event `open-compare-picker` and seeds Side A from `useCompareStore`.
37
+ */
38
+ export function ComparePicker({ open: openProp, onOpenChange, initialA }: ComparePickerProps = {}) {
39
+ const controlled = openProp !== undefined;
40
+ const [internalOpen, setInternalOpen] = useState(false);
41
+ const open = controlled ? openProp : internalOpen;
42
+ const setOpen = (o: boolean) => {
43
+ if (controlled) onOpenChange?.(o);
44
+ else setInternalOpen(o);
45
+ };
46
+
47
+ const [localA, setLocalA] = useState<CompareSelection | null>(initialA ?? null);
48
+
49
+ // Singleton mode: listen for global event, seed A from store
50
+ useEffect(() => {
51
+ if (controlled) return;
52
+ function onEvent() {
53
+ setLocalA(useCompareStore.getState().selection);
54
+ setInternalOpen(true);
55
+ }
56
+ window.addEventListener("open-compare-picker", onEvent);
57
+ return () => window.removeEventListener("open-compare-picker", onEvent);
58
+ }, [controlled]);
59
+ const [query, setQuery] = useState("");
60
+ const [activeIndex, setActiveIndex] = useState(0);
61
+ const [error, setError] = useState<string | null>(null);
62
+ const inputRef = useRef<HTMLInputElement>(null);
63
+
64
+ const tabs = useTabStore((s) => s.tabs);
65
+ const fileIndex = useFileStore((s) => s.fileIndex);
66
+ const activeProject = useProjectStore((s) => s.activeProject);
67
+
68
+ useEffect(() => {
69
+ if (!open) return;
70
+ // In controlled mode, sync A from prop. In singleton mode, event handler
71
+ // already populated localA — don't clobber it here.
72
+ if (controlled) setLocalA(initialA ?? null);
73
+ setQuery("");
74
+ setActiveIndex(0);
75
+ setError(null);
76
+ // Focus input after dialog mounts
77
+ setTimeout(() => inputRef.current?.focus(), 50);
78
+ }, [open, initialA, controlled]);
79
+
80
+ const candidates = useMemo<Candidate[]>(() => {
81
+ const tabCands: Candidate[] = tabs
82
+ .filter((t) => t.type === "editor" && t.metadata?.filePath)
83
+ .map((t) => ({
84
+ id: `tab:${t.id}`,
85
+ path: t.metadata!.filePath as string,
86
+ label: basename(t.metadata!.filePath as string),
87
+ source: "tab",
88
+ dirtyContent: t.metadata!.unsavedContent as string | undefined,
89
+ }));
90
+ const seenPaths = new Set(tabCands.map((c) => c.path));
91
+ const fileCands: Candidate[] = fileIndex
92
+ .filter((f) => f.type === "file" && !seenPaths.has(f.path))
93
+ .map((f) => ({
94
+ id: `file:${f.path}`,
95
+ path: f.path,
96
+ label: f.name,
97
+ source: "file",
98
+ }));
99
+ return [...tabCands, ...fileCands];
100
+ }, [tabs, fileIndex]);
101
+
102
+ const filtered = useMemo<Candidate[]>(() => {
103
+ if (!query.trim()) return candidates.slice(0, MAX_RESULTS);
104
+ const scored = candidates
105
+ .map((c) => {
106
+ const score = scoreFileSearch(query, c.label, c.path);
107
+ return score ? { c, score } : null;
108
+ })
109
+ .filter((x): x is { c: Candidate; score: ReturnType<typeof scoreFileSearch> & {} } => x !== null)
110
+ .sort((a, b) => compareScores(a.score, b.score))
111
+ .slice(0, MAX_RESULTS)
112
+ .map((x) => x.c);
113
+ return scored;
114
+ }, [candidates, query]);
115
+
116
+ useEffect(() => {
117
+ if (activeIndex >= filtered.length) setActiveIndex(Math.max(0, filtered.length - 1));
118
+ }, [filtered, activeIndex]);
119
+
120
+ // Guards against rapid double-invoke (Enter spam, double-click) while the
121
+ // openCompareTab promise is in flight — ref so a second sync call sees it.
122
+ const pickingRef = useRef(false);
123
+
124
+ async function handlePick(c: Candidate) {
125
+ if (!activeProject) return;
126
+ if (!localA) {
127
+ setLocalA({
128
+ filePath: c.path,
129
+ projectName: activeProject.name,
130
+ dirtyContent: c.dirtyContent,
131
+ label: c.label,
132
+ });
133
+ setQuery("");
134
+ setActiveIndex(0);
135
+ inputRef.current?.focus();
136
+ return;
137
+ }
138
+ if (pickingRef.current) return;
139
+ pickingRef.current = true;
140
+ try {
141
+ await openCompareTab(
142
+ { path: localA.filePath, dirtyContent: localA.dirtyContent },
143
+ { path: c.path, dirtyContent: c.dirtyContent },
144
+ activeProject.name,
145
+ );
146
+ useCompareStore.getState().clearSelection();
147
+ setOpen(false);
148
+ } catch (err) {
149
+ setError(err instanceof Error ? err.message : "Compare failed");
150
+ } finally {
151
+ pickingRef.current = false;
152
+ }
153
+ }
154
+
155
+ function handleKeyDown(e: React.KeyboardEvent) {
156
+ if (e.key === "ArrowDown") {
157
+ e.preventDefault();
158
+ setActiveIndex((i) => Math.min(filtered.length - 1, i + 1));
159
+ } else if (e.key === "ArrowUp") {
160
+ e.preventDefault();
161
+ setActiveIndex((i) => Math.max(0, i - 1));
162
+ } else if (e.key === "Enter") {
163
+ e.preventDefault();
164
+ const pick = filtered[activeIndex];
165
+ if (pick) handlePick(pick);
166
+ }
167
+ }
168
+
169
+ return (
170
+ <Dialog open={open} onOpenChange={setOpen}>
171
+ <DialogContent className="max-w-lg p-0 gap-0 overflow-hidden">
172
+ <DialogHeader className="px-4 pt-4 pb-2">
173
+ <DialogTitle className="flex items-center gap-2 text-sm">
174
+ <Columns2 className="size-4" />
175
+ Compare Files
176
+ </DialogTitle>
177
+ </DialogHeader>
178
+
179
+ {/* Side A chip */}
180
+ <div className="px-4 pb-2">
181
+ {localA ? (
182
+ <div className="flex items-center gap-2 text-xs bg-muted rounded px-2 py-1 w-fit max-w-full">
183
+ <FileCode className="size-3.5 shrink-0" />
184
+ <span className="truncate" title={localA.filePath}>{localA.label}</span>
185
+ <button
186
+ type="button"
187
+ onClick={() => setLocalA(null)}
188
+ className="hover:bg-surface-elevated rounded p-0.5"
189
+ aria-label="Clear first file"
190
+ >
191
+ <X className="size-3" />
192
+ </button>
193
+ </div>
194
+ ) : (
195
+ <p className="text-xs text-muted-foreground">Pick first file, then second.</p>
196
+ )}
197
+ </div>
198
+
199
+ {/* Search input */}
200
+ <input
201
+ ref={inputRef}
202
+ type="text"
203
+ value={query}
204
+ onChange={(e) => { setQuery(e.target.value); setActiveIndex(0); }}
205
+ onKeyDown={handleKeyDown}
206
+ placeholder={localA ? "Search for file B..." : "Search for file A..."}
207
+ className="w-full px-4 py-2 bg-transparent border-y border-border text-sm outline-none"
208
+ />
209
+
210
+ {error && (
211
+ <div className="px-4 py-2 text-xs text-destructive border-b border-border">{error}</div>
212
+ )}
213
+
214
+ {/* Results list */}
215
+ <div className="max-h-[50vh] md:max-h-80 overflow-y-auto">
216
+ {filtered.length === 0 ? (
217
+ <div className="px-4 py-6 text-center text-xs text-muted-foreground">
218
+ {candidates.length === 0 ? "No files available" : "No matches"}
219
+ </div>
220
+ ) : (
221
+ filtered.map((c, i) => (
222
+ <button
223
+ key={c.id}
224
+ type="button"
225
+ onClick={() => handlePick(c)}
226
+ onMouseEnter={() => setActiveIndex(i)}
227
+ className={cn(
228
+ "w-full flex items-center gap-2 px-4 py-1.5 text-left text-sm",
229
+ "hover:bg-surface-elevated transition-colors",
230
+ i === activeIndex && "bg-surface-elevated",
231
+ )}
232
+ >
233
+ <FileCode className="size-3.5 shrink-0 text-text-secondary" />
234
+ <span className="truncate">{c.label}</span>
235
+ <span className="text-xs text-muted-foreground truncate ml-auto" title={c.path}>
236
+ {c.source === "tab" ? "open" : c.path}
237
+ </span>
238
+ </button>
239
+ ))
240
+ )}
241
+ </div>
242
+ </DialogContent>
243
+ </Dialog>
244
+ );
245
+ }