@hienlh/ppm 0.12.7 → 0.12.9

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 (70) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/web/assets/{ai-settings-section-BHdBBJtS.js → ai-settings-section-QE6nBNgN.js} +1 -1
  3. package/dist/web/assets/api-client-Dvzcc_EO.js +1 -0
  4. package/dist/web/assets/{api-settings-ByUGHhTB.js → api-settings-DAk7D-NP.js} +1 -1
  5. package/dist/web/assets/{audio-preview-A6ScJemm.js → audio-preview-DnQmf9fu.js} +1 -1
  6. package/dist/web/assets/chat-tab-Cf6T3mGO.js +12 -0
  7. package/dist/web/assets/code-editor-B-lU1fz3.js +8 -0
  8. package/dist/web/assets/{conflict-editor-DQt8Bap3.js → conflict-editor-BYzf3LuW.js} +1 -1
  9. package/dist/web/assets/{database-viewer-C1k-aq-e.js → database-viewer-DjvnIn8p.js} +2 -2
  10. package/dist/web/assets/{diff-viewer-TowzH722.js → diff-viewer-CP2jcR5J.js} +1 -1
  11. package/dist/web/assets/{extension-webview-Cn1x5C5F.js → extension-webview-4xMREn_x.js} +1 -1
  12. package/dist/web/assets/file-store-BrbCNyLm.js +1 -0
  13. package/dist/web/assets/{image-preview-MGnGKiYs.js → image-preview-CkS2PVdQ.js} +1 -1
  14. package/dist/web/assets/index-BTjuH4fn.css +2 -0
  15. package/dist/web/assets/index-FGlF8IWZ.js +23 -0
  16. package/dist/web/assets/{keybindings-store-CThBg3hS.js → keybindings-store-B-zET-0o.js} +1 -1
  17. package/dist/web/assets/keybindings-store-DaBV6qhz.js +1 -0
  18. package/dist/web/assets/{markdown-renderer-DSINJjCx.js → markdown-renderer-Bj2B05Km.js} +1 -1
  19. package/dist/web/assets/{pdf-preview-BiI5Qihn.js → pdf-preview-CCyw5cuH.js} +1 -1
  20. package/dist/web/assets/{port-forwarding-tab-jjdgxhoi.js → port-forwarding-tab-Cebb5Eix.js} +1 -1
  21. package/dist/web/assets/{postgres-viewer-BwXJ-fGk.js → postgres-viewer-BrOiliEv.js} +2 -2
  22. package/dist/web/assets/{settings-store-BMZgnYTp.js → settings-store-BLLR7ed8.js} +2 -2
  23. package/dist/web/assets/settings-tab-D0XjupJm.js +1 -0
  24. package/dist/web/assets/{sql-query-editor-BSHd21AE.js → sql-query-editor-CVAnRFbi.js} +1 -1
  25. package/dist/web/assets/{sqlite-viewer-BPywcOES.js → sqlite-viewer-OEVq_-Po.js} +1 -1
  26. package/dist/web/assets/{terminal-tab-Civ2Yhce.js → terminal-tab-MjmJaQyA.js} +1 -1
  27. package/dist/web/assets/{use-blob-url-BU9hYOj9.js → use-blob-url-e9uTXjv5.js} +1 -1
  28. package/dist/web/assets/{use-monaco-theme-CXs7t0_G.js → use-monaco-theme-BkZDwoVd.js} +1 -1
  29. package/dist/web/assets/{video-preview-Db5TkPSt.js → video-preview-B819qvlp.js} +1 -1
  30. package/dist/web/index.html +8 -8
  31. package/dist/web/sw.js +1 -1
  32. package/docs/journals/260421-lazy-load-file-tree-palette-index.md +125 -0
  33. package/docs/project-changelog.md +13 -1
  34. package/docs/system-architecture.md +79 -1
  35. package/package.json +1 -1
  36. package/src/providers/claude-agent-sdk.ts +23 -0
  37. package/src/server/index.ts +1 -1
  38. package/src/server/routes/files.ts +40 -2
  39. package/src/server/routes/projects.ts +53 -0
  40. package/src/server/routes/settings.ts +50 -1
  41. package/src/services/config.service.ts +41 -0
  42. package/src/services/db.service.ts +57 -1
  43. package/src/services/file-filter.service.ts +121 -0
  44. package/src/services/file-list-index.service.ts +170 -0
  45. package/src/services/file-watcher.service.ts +8 -4
  46. package/src/services/file.service.ts +55 -53
  47. package/src/services/upgrade.service.ts +2 -2
  48. package/src/types/chat.ts +2 -1
  49. package/src/types/project.ts +31 -0
  50. package/src/web/components/chat/file-picker.tsx +0 -13
  51. package/src/web/components/chat/message-input.tsx +11 -14
  52. package/src/web/components/chat/tool-cards.tsx +4 -2
  53. package/src/web/components/explorer/file-tree.tsx +91 -26
  54. package/src/web/components/layout/command-palette.tsx +26 -3
  55. package/src/web/components/settings/files-settings-section.tsx +230 -0
  56. package/src/web/components/settings/glob-list-editor.tsx +121 -0
  57. package/src/web/components/settings/settings-tab.tsx +5 -2
  58. package/src/web/lib/api-client.ts +2 -1
  59. package/src/web/lib/api-files-settings.ts +42 -0
  60. package/src/web/stores/file-store.ts +139 -14
  61. package/src/web/stores/file-tree-merge-helpers.ts +44 -0
  62. package/src/web/stores/jira-store.ts +1 -1
  63. package/dist/web/assets/api-client-CwbMRXYl.js +0 -1
  64. package/dist/web/assets/chat-tab--Rc7WIJp.js +0 -12
  65. package/dist/web/assets/code-editor-DZSUYMBx.js +0 -8
  66. package/dist/web/assets/index-BrAupjGV.css +0 -2
  67. package/dist/web/assets/index-gxtJiPiW.js +0 -23
  68. package/dist/web/assets/keybindings-store-BIQHClUy.js +0 -1
  69. package/dist/web/assets/project-store-IB6pAGQh.js +0 -1
  70. package/dist/web/assets/settings-tab-USIB-LOd.js +0 -1
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":"de3bdc9d760ff3a85e5d166122864395","url":"index.html"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-512.svg"},{"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/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/index-BrAupjGV.css"},{"revision":null,"url":"assets/ai-settings-section-BHdBBJtS.js"},{"revision":null,"url":"assets/terminal-tab-Civ2Yhce.js"},{"revision":null,"url":"assets/radar-KQ55EAFF-BviZcL-b.js"},{"revision":null,"url":"assets/vendor-xterm-CU2c3f0A.js"},{"revision":null,"url":"assets/use-blob-url-BU9hYOj9.js"},{"revision":null,"url":"assets/input-Dk49gO8E.js"},{"revision":null,"url":"assets/database-viewer-C1k-aq-e.js"},{"revision":null,"url":"assets/index-gxtJiPiW.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/chat-tab--Rc7WIJp.js"},{"revision":null,"url":"assets/csv-preview-HMSavgBb.js"},{"revision":null,"url":"assets/katex-CKoArbIw.js"},{"revision":null,"url":"assets/settings-store-BMZgnYTp.js"},{"revision":null,"url":"assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2"},{"revision":null,"url":"assets/pdf-preview-BiI5Qihn.js"},{"revision":null,"url":"assets/info-3K5VOQVL-BwAZ2zd8.js"},{"revision":null,"url":"assets/chevron-right-BzAdxJRG.js"},{"revision":null,"url":"assets/audio-preview-A6ScJemm.js"},{"revision":null,"url":"assets/extension-store-3yZYn07W.js"},{"revision":null,"url":"assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2"},{"revision":null,"url":"assets/vendor-markdown-0Mxgxy0L.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/api-client-CwbMRXYl.js"},{"revision":null,"url":"assets/packet-RMMSAZCW-tx2n5Qry.js"},{"revision":null,"url":"assets/use-monaco-theme-CXs7t0_G.js"},{"revision":null,"url":"assets/plus-51UQ45rf.js"},{"revision":null,"url":"assets/keybindings-store-CThBg3hS.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/sqlite-viewer-BPywcOES.js"},{"revision":null,"url":"assets/architecture-PBZL5I3N-DvZbltvY.js"},{"revision":null,"url":"assets/rolldown-runtime-FhOqtrmT.js"},{"revision":null,"url":"assets/extension-webview-Cn1x5C5F.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/file-exclamation-point-BwzaQ50n.js"},{"revision":null,"url":"assets/diff-viewer-TowzH722.js"},{"revision":null,"url":"assets/image-preview-MGnGKiYs.js"},{"revision":null,"url":"assets/port-forwarding-tab-jjdgxhoi.js"},{"revision":null,"url":"assets/settings-tab-USIB-LOd.js"},{"revision":null,"url":"assets/project-store-IB6pAGQh.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/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/sql-query-editor-BSHd21AE.js"},{"revision":null,"url":"assets/createLucideIcon-BjHrJDVb.js"},{"revision":null,"url":"assets/keybindings-store-BIQHClUy.js"},{"revision":null,"url":"assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2"},{"revision":null,"url":"assets/code-editor-DZSUYMBx.js"},{"revision":null,"url":"assets/code-CuravVys.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/markdown-renderer-DSINJjCx.js"},{"revision":null,"url":"assets/esm-K1XIK4vc.js"},{"revision":null,"url":"assets/gitGraph-HDMCJU4V-BxhdxFgj.js"},{"revision":null,"url":"assets/vendor-xterm-BrP-ENHg.css"},{"revision":null,"url":"assets/api-settings-ByUGHhTB.js"},{"revision":null,"url":"assets/postgres-viewer-BwXJ-fGk.js"},{"revision":null,"url":"assets/conflict-editor-DQt8Bap3.js"},{"revision":null,"url":"assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2"},{"revision":null,"url":"assets/lib-DQHnkzGy.js"},{"revision":null,"url":"assets/video-preview-Db5TkPSt.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":"ef266ba2bae293df7fe4f1c88e521a45","url":"index.html"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-512.svg"},{"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/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/radar-KQ55EAFF-BviZcL-b.js"},{"revision":null,"url":"assets/vendor-xterm-CU2c3f0A.js"},{"revision":null,"url":"assets/input-Dk49gO8E.js"},{"revision":null,"url":"assets/diff-viewer-CP2jcR5J.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/database-viewer-DjvnIn8p.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/conflict-editor-BYzf3LuW.js"},{"revision":null,"url":"assets/image-preview-CkS2PVdQ.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/index-BTjuH4fn.css"},{"revision":null,"url":"assets/chevron-right-BzAdxJRG.js"},{"revision":null,"url":"assets/extension-store-3yZYn07W.js"},{"revision":null,"url":"assets/terminal-tab-MjmJaQyA.js"},{"revision":null,"url":"assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2"},{"revision":null,"url":"assets/code-editor-B-lU1fz3.js"},{"revision":null,"url":"assets/audio-preview-DnQmf9fu.js"},{"revision":null,"url":"assets/vendor-markdown-0Mxgxy0L.js"},{"revision":null,"url":"assets/port-forwarding-tab-Cebb5Eix.js"},{"revision":null,"url":"assets/settings-tab-D0XjupJm.js"},{"revision":null,"url":"assets/api-client-Dvzcc_EO.js"},{"revision":null,"url":"assets/pdf-preview-CCyw5cuH.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/packet-RMMSAZCW-tx2n5Qry.js"},{"revision":null,"url":"assets/postgres-viewer-BrOiliEv.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/extension-webview-4xMREn_x.js"},{"revision":null,"url":"assets/settings-store-BLLR7ed8.js"},{"revision":null,"url":"assets/use-monaco-theme-BkZDwoVd.js"},{"revision":null,"url":"assets/video-preview-B819qvlp.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/file-exclamation-point-BwzaQ50n.js"},{"revision":null,"url":"assets/file-store-BrbCNyLm.js"},{"revision":null,"url":"assets/index-FGlF8IWZ.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/KaTeX_Size2-Regular-Dy4dx90m.woff2"},{"revision":null,"url":"assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2"},{"revision":null,"url":"assets/sqlite-viewer-OEVq_-Po.js"},{"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/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2"},{"revision":null,"url":"assets/sql-completion-provider-C3cq9j99.js"},{"revision":null,"url":"assets/markdown-renderer-Bj2B05Km.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/vendor-xterm-BrP-ENHg.css"},{"revision":null,"url":"assets/chat-tab-Cf6T3mGO.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||`/`)}))});
@@ -0,0 +1,125 @@
1
+ # Lazy-Load File Tree + Palette Index Feature Complete
2
+
3
+ **Date**: 2026-04-21 11:58
4
+ **Severity**: Medium
5
+ **Component**: File explorer, settings, API layer
6
+ **Status**: Resolved
7
+
8
+ ## What Happened
9
+
10
+ Completed a 14-hour, 5-phase feature adding VS Code-style lazy-loaded file tree + separate flat palette index with configurable per-project exclusion rules. Full stack: backend file filtering service + index cache, settings CRUD endpoints, frontend tree UI with incremental loading, settings dialog, and chat integration. Code review caught 3 critical bugs before merge. 2 schema migration tests needed updates for schema version bump.
11
+
12
+ ## The Brutal Truth
13
+
14
+ This feature works now, but it shipped with three bugs that passed unit tests. Code review was the only thing that caught them—neither the author nor 126 targeted Docker tests detected data loss, missing invalidation, or dead code. That's simultaneously reassuring (review process caught them) and horrifying (we're relying on humans, not coverage). The pre-existing test brittleness (hardcoded schema versions) exploded as soon as we bumped the schema, which means our migration tests are fundamentally fragile.
15
+
16
+ ## Technical Details
17
+
18
+ ### Critical Bugs Fixed
19
+
20
+ **C1: Shallow Merge in Global Settings PATCH**
21
+ - `file-filter.service.ts` excludes merged shallow into existing config
22
+ - Result: global excludes clobbered by PATCH request with only per-project rules
23
+ - Error: `Object.assign(existingConfig, newRules)` vs deep merge
24
+ - Impact: Silent data loss—no error thrown, just silently overwritten
25
+
26
+ **C2: Missing Cache Invalidation on Global PATCH**
27
+ - Global settings endpoint didn't trigger watcher-based cache invalidation
28
+ - Cache remained stale after PATCH, clients saw old excludes
29
+ - Fix: Added `invalidateIndexCache()` call in global PATCH handler
30
+
31
+ **C3: Dead AbortController Code**
32
+ - `AbortController` instantiated but never used in async tree expansion
33
+ - Removed unused variable—dead code that could confuse future devs
34
+
35
+ ### Bonus Fix (H1)
36
+ - Directory entries (not just files) now included in `/files/index` flat palette
37
+ - Improves chat file-picker usability when dirs need direct reference
38
+
39
+ ### Test Coverage
40
+ - 126 targeted tests pass in Docker (file-filter, file-list-index, API routes, UI components)
41
+ - Full suite has pre-existing env-related failures unrelated to this feature
42
+ - 2 migration tests updated: hardcoded schema version (19 → 21) needed manual bump
43
+
44
+ ### Files Created (9 total)
45
+ **Backend Services:**
46
+ - `src/services/file-filter.service.ts` — glob matching, per-project rule logic
47
+ - `src/services/file-list-index.service.ts` — flat index cache, watcher invalidation
48
+
49
+ **Frontend UI:**
50
+ - `src/web/components/settings/files-settings-section.tsx` — settings dialog
51
+ - `src/web/components/settings/glob-list-editor.tsx` — reusable exclude rule editor
52
+ - `src/web/stores/file-tree-merge-helpers.ts` — tree merge logic for incremental load
53
+
54
+ **API & Integration:**
55
+ - `src/web/lib/api-files-settings.ts` — settings CRUD client
56
+ - 3 API integration test files (files-index, files-list, files-settings)
57
+ - 1 unit test (file-filter-service)
58
+
59
+ ## What We Tried
60
+
61
+ 1. **Initial Code Review**: Caught all 3 bugs before merge. Author's follow-up fixes verified via unit tests.
62
+ 2. **Test Updates**: Bumped schema version in migration tests—initially hardcoded, now should be dynamic.
63
+ 3. **Cache Invalidation**: Switched to watcher-only (no TTL, no focus-based refresh) to avoid stale data without over-invalidating.
64
+
65
+ ## Root Cause Analysis
66
+
67
+ ### Why Tests Didn't Catch These
68
+
69
+ **C1 (Shallow Merge)**
70
+ - Unit tests for file-filter mocked config objects; didn't test PATCH endpoint's Object.assign behavior
71
+ - Integration tests existed but only tested happy path (all rules provided); didn't test partial updates
72
+ - Lesson: **Partial update tests must verify existing data survives the merge**
73
+
74
+ **C2 (Missing Invalidation)**
75
+ - Cache invalidation is implicit behavior, not a testable return value
76
+ - Tests mocked the cache and watcher separately; integration test didn't verify both together
77
+ - Lesson: **Cache tests must assert invalidation side effects, not just cache contents**
78
+
79
+ **C3 (Dead Code)**
80
+ - Linting doesn't catch unused parameters; static analysis missed the instantiated-but-unused controller
81
+ - Lesson: **Code review is catching what linters miss**
82
+
83
+ ### Why Schema Test Brittleness Surfaced
84
+
85
+ - Migration tests hardcoded expected schema version (19)
86
+ - Feature bumped to version 21 (2 migrations: file filters table + index rebuild)
87
+ - Tests broke because `CURRENT_SCHEMA_VERSION !== 19`
88
+ - Fix: Tests should assert `schema_version = CURRENT_SCHEMA_VERSION`, not hardcoded values
89
+ - Lesson: **Any value that changes frequently (schema, version, timestamp) should never be hardcoded in tests**
90
+
91
+ ## Lessons Learned
92
+
93
+ ### Process Wins
94
+ - **Code review process validated**: Caught real bugs that tests missed. This is not a failure of testing—it's confirmation that human review adds critical value.
95
+ - **Parallel execution worked**: Phase 2 (backend) + Phase 4 (chat integration) dispatched simultaneously with strict file ownership boundaries. Zero conflicts.
96
+ - **Scope flexibility paid off**: Phase 3 scope expanded mid-flight to include chat input migration and file-picker updates without cascading delays.
97
+
98
+ ### Code Quality Failures
99
+ - **Integrate merge tests with cache tests**: Next time, write integration tests that verify data persists through full request cycle (PATCH → merge → cache invalidation → client read).
100
+ - **Mock less, test more**: The file-filter unit tests mocked config; should have tested actual config objects with existing state.
101
+ - **Dynamic schema assertions**: Never hardcode version numbers in tests. Parameterize against `CURRENT_SCHEMA_VERSION` or use version-agnostic comparisons.
102
+
103
+ ### Architecture Patterns (Confirmed)
104
+ - Auto-expand root only, everything else lazy—minimal initial load, matches VS Code behavior
105
+ - Per-project settings scoped to active project (no dropdown)—simpler UI, avoids "which project?" confusion
106
+ - Watcher-only cache invalidation (no TTL)—eliminates background noise, piggybacks on existing FS monitoring
107
+
108
+ ## Next Steps
109
+
110
+ 1. **High priority**: Update all migration tests to use `CURRENT_SCHEMA_VERSION` dynamically. Prevents regression when schema changes again.
111
+ - File: `tests/integration/db/` (any migration test files)
112
+ - Owner: QA/testing team
113
+
114
+ 2. **Medium priority**: Add integration tests for partial config updates (PATCH with subset of fields)
115
+ - Verifies shallow merge doesn't clobber existing data
116
+ - File: `tests/integration/api/files-settings.test.ts`
117
+ - Estimated: 2h
118
+
119
+ 3. **Documentation**: System architecture already updated; no further docs needed.
120
+
121
+ ## Unresolved Questions
122
+
123
+ - Should `/files/tree` endpoint be officially deprecated, or keep it as a compatibility shim indefinitely? Currently marked deprecated but functional. No breaking change planned yet.
124
+ - Cache invalidation: Should we add a manual refresh button in UI if watcher fails, or accept data staleness as acceptable in degraded scenarios?
125
+ - File-picker: Should directory entries always be in palette, or should exclude rules filter them? Currently included unconditionally.
@@ -6,9 +6,21 @@ All notable changes to PPM are documented here. Format follows [Keep a Changelog
6
6
 
7
7
  ---
8
8
 
9
- ## [Unreleased] — Session Tagging, Jira Debug Session Redesign, Frontend Memory Optimization, Git-Graph Enhancements
9
+ ## [Unreleased] — Lazy-Load File Tree + Palette Index, Session Tagging, Jira Debug Session Redesign, Frontend Memory Optimization, Git-Graph Enhancements
10
10
 
11
11
  ### Added
12
+ - **Lazy-Load File Tree + Palette Index** — Instant project opening on large codebases
13
+ - Backend: `GET /api/project/:name/files/list?path=<rel>` for 1-level directory listing with gitignore decoration
14
+ - Backend: `GET /api/project/:name/files/index` for flat full-project index (cached, watcher-invalidated)
15
+ - Filter model: hardcoded defaults ⊂ global config ⊂ per-project override (VS Code style)
16
+ - Settings API: `GET/PATCH /api/settings/files` for global `files.exclude` / `files.searchExclude` / `files.useIgnoreFiles`
17
+ - Project settings: `GET/PATCH /api/project/:name/settings` for per-project override (schema v21: projects.settings JSON)
18
+ - Frontend: File store refactored to lazy-load with AbortController pool; tree auto-expands root only, children load on-demand
19
+ - Command palette + chat file-picker switched from tree-flattening to `fileIndex` for instant search
20
+ - New Settings section: **Files & Search** — global + active-project-scoped settings, glob list editor, `useIgnoreFiles` toggle
21
+ - Gitignored files decorated grey in tree (when `useIgnoreFiles=true`)
22
+ - `/api/project/:name/files/tree` marked @deprecated (still functional)
23
+
12
24
  - **Session Tagging** — Per-project tags for organizing chat sessions
13
25
  - Database: schema v20 migration creates `project_tags` table with id, project_path, name, color, sort_order; adds `tag_id` FK to `session_metadata`
14
26
  - Tag service: `tag.service.ts` with CRUD helpers (create, read, update, delete, bulk assign), session tag enrichment, tag counting
@@ -223,8 +223,86 @@ Tab IDs are deterministic: `{type}:{identifier}` (e.g., `editor:src/index.ts`, `
223
223
  | **ClawBotStreamerService** | LEGACY streamer | (deprecated v0.9.11) |
224
224
  | **BashOutputSpy** | Monitor bash tool output in real-time via /proc/PID/fd (Linux/WSL2) or lsof (macOS) | startSpy, stopSpy, stopAllForSession |
225
225
  | **TagService** | Session tagging CRUD, bulk operations, tag-session enrichment | seedDefaultTags, getTagsByProject, createTag, updateTag, deleteTag, setSessionTag, bulkSetSessionTag, getSessionTags, getTagSessionCounts |
226
+ | **FileFilterService** | Glob pattern matching + precedence-enforced filtering (hardcoded ⊂ global ⊂ project) | mergeFilters, isPathIgnored, matchesPattern |
226
227
 
227
- **Key Files:** `src/services/*.service.ts`, `src/services/tag.service.ts`, `src/services/ppmbot/*.ts`, `src/services/bash-output-spy.ts`, `src/cli/commands/bot-cmd.ts`
228
+ **Key Files:** `src/services/*.service.ts`, `src/services/tag.service.ts`, `src/services/ppmbot/*.ts`, `src/services/bash-output-spy.ts`, `src/services/file-filter.service.ts`, `src/cli/commands/bot-cmd.ts`
229
+
230
+ ---
231
+
232
+ ### File Service & Filtering (Lazy-Load Tree, Palette Index)
233
+
234
+ **Component:** FileFilterService + API endpoints `/files/list`, `/files/index`, settings endpoints
235
+
236
+ **Overview:** Provides efficient file discovery with VS Code-style glob filtering and gitignore support. Three-layer filter precedence enforces consistent exclude patterns across tree navigation and search indexing.
237
+
238
+ **Filter Precedence (evaluated low-to-high):**
239
+ 1. **Hardcoded defaults** — `node_modules/**`, `.git`, `.env*` (always excluded, cannot override)
240
+ 2. **Global config** — `files.exclude`, `files.searchExclude`, `files.useIgnoreFiles` (applies to all projects)
241
+ 3. **Per-project override** — Project-scoped settings (DB: `projects.settings` JSON, schema v21) override global
242
+
243
+ **API Endpoints:**
244
+ ```
245
+ GET /api/project/:name/files/list?path=<rel>
246
+ → 1-level directory children with gitignore decoration (isIgnored field)
247
+ → { items: [{ name, type, isDir, isIgnored }], ... }
248
+
249
+ GET /api/project/:name/files/index
250
+ → Flat full-project file list (cached in memory, watcher-invalidated)
251
+ → { files: [{ path, isIgnored }], ... }
252
+
253
+ GET /api/settings/files
254
+ → Global file filter config (all projects)
255
+ → { filesExclude: [], searchExclude: [], useIgnoreFiles: bool }
256
+
257
+ PATCH /api/settings/files
258
+ → Update global config (partial: only specified fields)
259
+ → Validates arrays ≤200 items, filters non-string patterns
260
+
261
+ GET /api/project/:name/settings
262
+ → Per-project settings (includes file filter overrides)
263
+ → { filesExclude?: [], searchExclude?: [], useIgnoreFiles?: bool, ... }
264
+
265
+ PATCH /api/project/:name/settings
266
+ → Per-project override (stored in projects.settings JSON, schema v21)
267
+ → Same validation as global, caches invalidation on write
268
+ ```
269
+
270
+ **Filtering Model:**
271
+
272
+ | Config | Applies To | Validation | Notes |
273
+ |--------|-----------|------------|-------|
274
+ | `filesExclude` | Tree navigation | Glob patterns (max 200) | Hides from tree explorer |
275
+ | `searchExclude` | Index + palette search | Glob patterns (max 200) | Hides from search results |
276
+ | `useIgnoreFiles` | Both (when true) | Boolean | Include `.gitignore` + `.git/info/exclude` in filtering |
277
+
278
+ **Frontend Integration:**
279
+ - `useFileStore()` hook manages lazy-loading: `loadRoot()`, `loadChildren()`, `loadIndex()`
280
+ - AbortController pool cancels pending requests on project switch
281
+ - File tree auto-expands root (1 level), children load on-demand with spinner
282
+ - Command palette + chat file-picker switched from tree-flattening to `fileIndex` from store
283
+ - "Indexing project…" hint shown when `loadIndex()` is pending
284
+
285
+ **Server-Side Implementation:**
286
+ - `FileFilterService.mergeFilters()` — Combine hardcoded + global + project overrides with precedence
287
+ - `FileFilterService.isPathIgnored()` — Check if path matches any exclude pattern (gitignore if enabled)
288
+ - `FileService.list()` — 1-level enumeration with `isIgnored` field computed per item
289
+ - In-memory `indexCache` (Map: projectName → FileIndex) invalidated by `fs.watch` (file changes) + manual `invalidateIndexCache()` calls
290
+ - WS `file:changed` events routed to `invalidateFolder()` or `invalidateIndex()` depending on scope
291
+
292
+ **Database Schema (v21+):**
293
+ ```typescript
294
+ // projects table gains:
295
+ settings: TEXT // JSON: { filesExclude?, searchExclude?, useIgnoreFiles? }
296
+
297
+ // Example:
298
+ projects.settings = JSON.stringify({
299
+ filesExclude: ["**/.venv", "**/*.pyc"],
300
+ searchExclude: ["**/node_modules"],
301
+ useIgnoreFiles: false
302
+ })
303
+ ```
304
+
305
+ **Deprecated:** `/api/project/:name/files/tree` (marked @deprecated, still functional for backward compat)
228
306
 
229
307
  ---
230
308
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.12.7",
3
+ "version": "0.12.9",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "author": "hienlh",
6
6
  "license": "MIT",
@@ -1576,6 +1576,29 @@ function parseSessionMessage(msg: { uuid: string; type: string; message: unknown
1576
1576
  const role = msg.type as "user" | "assistant";
1577
1577
  const parentId = (msg as any).parent_tool_use_id as string | undefined;
1578
1578
 
1579
+ // Filter synthetic SDK-generated error messages (auth failures, rate limits, etc.).
1580
+ // Structure: { isApiErrorMessage: true, error: "authentication_failed"|"rate_limit"|...,
1581
+ // message: { model: "<synthetic>", content: [{text: "Failed to authenticate..."}] } }
1582
+ // Our retry loop handles these; the raw text must not render in chat history.
1583
+ const isSdkErrorMessage =
1584
+ (msg as any).isApiErrorMessage === true ||
1585
+ typeof (msg as any).error === "string" ||
1586
+ (message && (message as any).model === "<synthetic>" &&
1587
+ Array.isArray(message.content) &&
1588
+ (message.content as Array<Record<string, unknown>>).some(
1589
+ (b) => b.type === "text" && typeof b.text === "string" &&
1590
+ /Failed to authenticate|API Error: 40[13]|hit your limit|rate.?limit/i.test(b.text as string),
1591
+ ));
1592
+ if (isSdkErrorMessage) {
1593
+ return {
1594
+ id: msg.uuid,
1595
+ role,
1596
+ content: "",
1597
+ timestamp: new Date().toISOString(),
1598
+ sdkUuid: msg.uuid,
1599
+ };
1600
+ }
1601
+
1579
1602
  // Parse content blocks for both user and assistant messages
1580
1603
  const events: ChatEvent[] = [];
1581
1604
  let textContent = "";
@@ -328,7 +328,7 @@ export async function startServer(options: {
328
328
  if (portInUse) {
329
329
  // Retry — port may still be releasing after supervisor self-replace
330
330
  for (let attempt = 1; attempt <= 4; attempt++) {
331
- writeLog("WARN", [`Port ${port} in use, retrying in 1s (${attempt}/4)`]);
331
+ console.warn(`Port ${port} in use, retrying in 1s (${attempt}/4)`);
332
332
  await Bun.sleep(1000);
333
333
  portInUse = await checkPort();
334
334
  if (!portInUse) break;
@@ -1,7 +1,7 @@
1
1
  import { Hono } from "hono";
2
2
  import { resolve } from "node:path";
3
3
  import { existsSync, mkdirSync } from "node:fs";
4
- import { fileService } from "../../services/file.service.ts";
4
+ import { fileService, SecurityError, NotFoundError, ValidationError } from "../../services/file.service.ts";
5
5
  import { ok, err } from "../../types/api.ts";
6
6
  import { errorStatus } from "../helpers/error-status.ts";
7
7
 
@@ -13,7 +13,45 @@ const MAX_UPLOAD_FILES = 20;
13
13
  export const fileRoutes = new Hono<Env>();
14
14
 
15
15
 
16
- /** GET /files/tree?depth=3 */
16
+ /**
17
+ * GET /files/list?path=<relPath>
18
+ * Returns one directory level of entries with type and gitignore flag.
19
+ * Applies filesExclude patterns. path defaults to "" (project root).
20
+ */
21
+ fileRoutes.get("/list", (c) => {
22
+ try {
23
+ const projectPath = c.get("projectPath");
24
+ const relPath = (c.req.query("path") ?? "").trim();
25
+ // Reject path traversal attempts early
26
+ if (relPath.includes("..")) return c.json(err("Invalid path: traversal not allowed"), 400);
27
+ const entries = fileService.listDir(projectPath, relPath);
28
+ return c.json(ok(entries));
29
+ } catch (e) {
30
+ if (e instanceof SecurityError) return c.json(err((e as Error).message), 403);
31
+ if (e instanceof NotFoundError) return c.json(err((e as Error).message), 404);
32
+ return c.json(err((e as Error).message), errorStatus(e));
33
+ }
34
+ });
35
+
36
+ /**
37
+ * GET /files/index
38
+ * Returns flat array of all project files {path, name} for palette/search.
39
+ * Result is cached; cache is invalidated on file change events.
40
+ */
41
+ fileRoutes.get("/index", (c) => {
42
+ try {
43
+ const projectPath = c.get("projectPath");
44
+ const entries = fileService.buildIndex(projectPath);
45
+ return c.json(ok(entries));
46
+ } catch (e) {
47
+ return c.json(err((e as Error).message), errorStatus(e));
48
+ }
49
+ });
50
+
51
+ /**
52
+ * @deprecated Use /files/list for lazy-load tree instead.
53
+ * GET /files/tree?depth=3
54
+ */
17
55
  fileRoutes.get("/tree", (c) => {
18
56
  try {
19
57
  const projectPath = c.get("projectPath");
@@ -2,7 +2,9 @@ import { Hono } from "hono";
2
2
  import { projectService } from "../../services/project.service.ts";
3
3
  import { configService } from "../../services/config.service.ts";
4
4
  import { searchGitDirs } from "../../services/git-dirs.service.ts";
5
+ import { invalidateIndexCache } from "../../services/file-list-index.service.ts";
5
6
  import { ok, err } from "../../types/api.ts";
7
+ import type { ProjectSettings } from "../../types/project.ts";
6
8
 
7
9
  export const projectRoutes = new Hono();
8
10
 
@@ -88,6 +90,57 @@ projectRoutes.patch("/:name/color", async (c) => {
88
90
  }
89
91
  });
90
92
 
93
+ /** GET /api/projects/:name/settings — return per-project settings JSON */
94
+ projectRoutes.get("/:name/settings", (c) => {
95
+ try {
96
+ const name = c.req.param("name");
97
+ const projects = configService.get("projects");
98
+ const project = projects.find((p) => p.name === name);
99
+ if (!project) return c.json(err(`Project not found: ${name}`), 404);
100
+ const settings = configService.getProjectSettings(project.path);
101
+ return c.json(ok(settings));
102
+ } catch (e) {
103
+ return c.json(err((e as Error).message), 500);
104
+ }
105
+ });
106
+
107
+ /** PATCH /api/projects/:name/settings — merge-patch per-project settings, invalidate index cache */
108
+ projectRoutes.patch("/:name/settings", async (c) => {
109
+ try {
110
+ const name = c.req.param("name");
111
+ const projects = configService.get("projects");
112
+ const project = projects.find((p) => p.name === name);
113
+ if (!project) return c.json(err(`Project not found: ${name}`), 404);
114
+
115
+ const body = await c.req.json<ProjectSettings>();
116
+
117
+ // Validate files.filesExclude / searchExclude are bounded arrays of strings
118
+ if (body.files) {
119
+ const f = body.files;
120
+ if (f.filesExclude !== undefined) {
121
+ if (!Array.isArray(f.filesExclude)) return c.json(err("files.filesExclude must be an array"), 400);
122
+ f.filesExclude = f.filesExclude.filter((p) => typeof p === "string").slice(0, 200);
123
+ }
124
+ if (f.searchExclude !== undefined) {
125
+ if (!Array.isArray(f.searchExclude)) return c.json(err("files.searchExclude must be an array"), 400);
126
+ f.searchExclude = f.searchExclude.filter((p) => typeof p === "string").slice(0, 200);
127
+ }
128
+ if (f.useIgnoreFiles !== undefined && typeof f.useIgnoreFiles !== "boolean") {
129
+ return c.json(err("files.useIgnoreFiles must be a boolean"), 400);
130
+ }
131
+ }
132
+
133
+ configService.setProjectSettings(project.path, body);
134
+ // Bust server-side index cache so next /files/index re-builds with new filters
135
+ invalidateIndexCache(project.path);
136
+
137
+ const updated = configService.getProjectSettings(project.path);
138
+ return c.json(ok(updated));
139
+ } catch (e) {
140
+ return c.json(err((e as Error).message), 400);
141
+ }
142
+ });
143
+
91
144
  /** PATCH /api/projects/:name — update a project's name/path */
92
145
  projectRoutes.patch("/:name", async (c) => {
93
146
  try {
@@ -1,5 +1,5 @@
1
1
  import { Hono } from "hono";
2
- import { configService } from "../../services/config.service.ts";
2
+ import { configService, FILE_CONFIG_KEYS } from "../../services/config.service.ts";
3
3
  import { getConfigValue, setConfigValue, listPairedChats, getPairingByCode, approvePairing, revokePairing, getPPMBotMemories, getDb } from "../../services/db.service.ts";
4
4
  import {
5
5
  validateAIProviderConfig,
@@ -13,6 +13,7 @@ import {
13
13
  } from "../../types/config.ts";
14
14
  import { ok, err } from "../../types/api.ts";
15
15
  import { proxyService } from "../../services/proxy.service.ts";
16
+ import { clearIndexCache } from "../../services/file-list-index.service.ts";
16
17
  import { providerRegistry } from "../../providers/registry.ts";
17
18
 
18
19
  export const settingsRoutes = new Hono();
@@ -405,6 +406,54 @@ settingsRoutes.delete("/clawbot/memories/:id", (c) => {
405
406
  }
406
407
  });
407
408
 
409
+ // ── File Filters ──────────────────────────────────────────────────────────────
410
+
411
+ /** GET /settings/files — return global file filter config */
412
+ settingsRoutes.get("/files", (c) => {
413
+ return c.json(ok({
414
+ filesExclude: configService.getFilesExclude(),
415
+ searchExclude: configService.getSearchExclude(),
416
+ useIgnoreFiles: configService.getUseIgnoreFiles(),
417
+ }));
418
+ });
419
+
420
+ /** PATCH /settings/files — partial update to global file filter config */
421
+ settingsRoutes.patch("/files", async (c) => {
422
+ try {
423
+ const body = await c.req.json<{
424
+ filesExclude?: string[];
425
+ searchExclude?: string[];
426
+ useIgnoreFiles?: boolean;
427
+ }>();
428
+
429
+ if (body.filesExclude !== undefined) {
430
+ if (!Array.isArray(body.filesExclude)) return c.json(err("filesExclude must be an array"), 400);
431
+ const patterns = body.filesExclude.filter((p) => typeof p === "string").slice(0, 200);
432
+ setConfigValue(FILE_CONFIG_KEYS.filesExclude, JSON.stringify(patterns));
433
+ }
434
+ if (body.searchExclude !== undefined) {
435
+ if (!Array.isArray(body.searchExclude)) return c.json(err("searchExclude must be an array"), 400);
436
+ const patterns = body.searchExclude.filter((p) => typeof p === "string").slice(0, 200);
437
+ setConfigValue(FILE_CONFIG_KEYS.searchExclude, JSON.stringify(patterns));
438
+ }
439
+ if (body.useIgnoreFiles !== undefined) {
440
+ if (typeof body.useIgnoreFiles !== "boolean") return c.json(err("useIgnoreFiles must be a boolean"), 400);
441
+ setConfigValue(FILE_CONFIG_KEYS.useIgnoreFiles, JSON.stringify(body.useIgnoreFiles));
442
+ }
443
+
444
+ // Invalidate all project index caches — global filter changes affect every project
445
+ clearIndexCache();
446
+
447
+ return c.json(ok({
448
+ filesExclude: configService.getFilesExclude(),
449
+ searchExclude: configService.getSearchExclude(),
450
+ useIgnoreFiles: configService.getUseIgnoreFiles(),
451
+ }));
452
+ } catch (e) {
453
+ return c.json(err((e as Error).message), 400);
454
+ }
455
+ });
456
+
408
457
  /** GET /settings/clawbot/tasks — list recent delegated tasks */
409
458
  settingsRoutes.get("/clawbot/tasks", (c) => {
410
459
  const limit = Number(c.req.query("limit")) || 20;