@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.
- package/CHANGELOG.md +13 -0
- package/dist/web/assets/{ai-settings-section-BHdBBJtS.js → ai-settings-section-QE6nBNgN.js} +1 -1
- package/dist/web/assets/api-client-Dvzcc_EO.js +1 -0
- package/dist/web/assets/{api-settings-ByUGHhTB.js → api-settings-DAk7D-NP.js} +1 -1
- package/dist/web/assets/{audio-preview-A6ScJemm.js → audio-preview-DnQmf9fu.js} +1 -1
- package/dist/web/assets/chat-tab-Cf6T3mGO.js +12 -0
- package/dist/web/assets/code-editor-B-lU1fz3.js +8 -0
- package/dist/web/assets/{conflict-editor-DQt8Bap3.js → conflict-editor-BYzf3LuW.js} +1 -1
- package/dist/web/assets/{database-viewer-C1k-aq-e.js → database-viewer-DjvnIn8p.js} +2 -2
- package/dist/web/assets/{diff-viewer-TowzH722.js → diff-viewer-CP2jcR5J.js} +1 -1
- package/dist/web/assets/{extension-webview-Cn1x5C5F.js → extension-webview-4xMREn_x.js} +1 -1
- package/dist/web/assets/file-store-BrbCNyLm.js +1 -0
- package/dist/web/assets/{image-preview-MGnGKiYs.js → image-preview-CkS2PVdQ.js} +1 -1
- package/dist/web/assets/index-BTjuH4fn.css +2 -0
- package/dist/web/assets/index-FGlF8IWZ.js +23 -0
- package/dist/web/assets/{keybindings-store-CThBg3hS.js → keybindings-store-B-zET-0o.js} +1 -1
- package/dist/web/assets/keybindings-store-DaBV6qhz.js +1 -0
- package/dist/web/assets/{markdown-renderer-DSINJjCx.js → markdown-renderer-Bj2B05Km.js} +1 -1
- package/dist/web/assets/{pdf-preview-BiI5Qihn.js → pdf-preview-CCyw5cuH.js} +1 -1
- package/dist/web/assets/{port-forwarding-tab-jjdgxhoi.js → port-forwarding-tab-Cebb5Eix.js} +1 -1
- package/dist/web/assets/{postgres-viewer-BwXJ-fGk.js → postgres-viewer-BrOiliEv.js} +2 -2
- package/dist/web/assets/{settings-store-BMZgnYTp.js → settings-store-BLLR7ed8.js} +2 -2
- package/dist/web/assets/settings-tab-D0XjupJm.js +1 -0
- package/dist/web/assets/{sql-query-editor-BSHd21AE.js → sql-query-editor-CVAnRFbi.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-BPywcOES.js → sqlite-viewer-OEVq_-Po.js} +1 -1
- package/dist/web/assets/{terminal-tab-Civ2Yhce.js → terminal-tab-MjmJaQyA.js} +1 -1
- package/dist/web/assets/{use-blob-url-BU9hYOj9.js → use-blob-url-e9uTXjv5.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-CXs7t0_G.js → use-monaco-theme-BkZDwoVd.js} +1 -1
- package/dist/web/assets/{video-preview-Db5TkPSt.js → video-preview-B819qvlp.js} +1 -1
- package/dist/web/index.html +8 -8
- package/dist/web/sw.js +1 -1
- package/docs/journals/260421-lazy-load-file-tree-palette-index.md +125 -0
- package/docs/project-changelog.md +13 -1
- package/docs/system-architecture.md +79 -1
- package/package.json +1 -1
- package/src/providers/claude-agent-sdk.ts +23 -0
- package/src/server/index.ts +1 -1
- package/src/server/routes/files.ts +40 -2
- package/src/server/routes/projects.ts +53 -0
- package/src/server/routes/settings.ts +50 -1
- package/src/services/config.service.ts +41 -0
- package/src/services/db.service.ts +57 -1
- package/src/services/file-filter.service.ts +121 -0
- package/src/services/file-list-index.service.ts +170 -0
- package/src/services/file-watcher.service.ts +8 -4
- package/src/services/file.service.ts +55 -53
- package/src/services/upgrade.service.ts +2 -2
- package/src/types/chat.ts +2 -1
- package/src/types/project.ts +31 -0
- package/src/web/components/chat/file-picker.tsx +0 -13
- package/src/web/components/chat/message-input.tsx +11 -14
- package/src/web/components/chat/tool-cards.tsx +4 -2
- package/src/web/components/explorer/file-tree.tsx +91 -26
- package/src/web/components/layout/command-palette.tsx +26 -3
- package/src/web/components/settings/files-settings-section.tsx +230 -0
- package/src/web/components/settings/glob-list-editor.tsx +121 -0
- package/src/web/components/settings/settings-tab.tsx +5 -2
- package/src/web/lib/api-client.ts +2 -1
- package/src/web/lib/api-files-settings.ts +42 -0
- package/src/web/stores/file-store.ts +139 -14
- package/src/web/stores/file-tree-merge-helpers.ts +44 -0
- package/src/web/stores/jira-store.ts +1 -1
- package/dist/web/assets/api-client-CwbMRXYl.js +0 -1
- package/dist/web/assets/chat-tab--Rc7WIJp.js +0 -12
- package/dist/web/assets/code-editor-DZSUYMBx.js +0 -8
- package/dist/web/assets/index-BrAupjGV.css +0 -2
- package/dist/web/assets/index-gxtJiPiW.js +0 -23
- package/dist/web/assets/keybindings-store-BIQHClUy.js +0 -1
- package/dist/web/assets/project-store-IB6pAGQh.js +0 -1
- 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
|
@@ -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 = "";
|
package/src/server/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
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;
|