@hienlh/ppm 0.7.2 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/web/assets/ai-settings-section-CBpXqM-I.js +1 -0
  3. package/dist/web/assets/chat-tab-DXtCcWg2.js +7 -0
  4. package/dist/web/assets/code-editor-CxMuSJrX.js +1 -0
  5. package/dist/web/assets/{database-viewer-BxUpM_uA.js → database-viewer-C6riNG1v.js} +1 -1
  6. package/dist/web/assets/{diff-viewer-DAhrHpNM.js → diff-viewer-CQIockZr.js} +1 -1
  7. package/dist/web/assets/git-graph-D2BR2rfP.js +1 -0
  8. package/dist/web/assets/index-4pPCbWJp.css +2 -0
  9. package/dist/web/assets/index-B9ZHWVob.js +29 -0
  10. package/dist/web/assets/{input-nI4xe1Y9.js → input-BpFvhpT8.js} +1 -1
  11. package/dist/web/assets/keybindings-store-fcYIcK0C.js +1 -0
  12. package/dist/web/assets/{markdown-renderer-CvGYO9sH.js → markdown-renderer-DKVNZXEw.js} +2 -2
  13. package/dist/web/assets/{postgres-viewer-BL99auSm.js → postgres-viewer-DHvwHGEL.js} +1 -1
  14. package/dist/web/assets/settings-store-DS-ifJ7c.js +1 -0
  15. package/dist/web/assets/settings-tab-D_quOlcC.js +1 -0
  16. package/dist/web/assets/{sqlite-viewer-DfgaCbWT.js → sqlite-viewer-C5Vj_kSU.js} +1 -1
  17. package/dist/web/assets/{tab-store-Bm1Hw8OR.js → tab-store-D7tRt0VT.js} +1 -1
  18. package/dist/web/assets/{terminal-tab-D27e4ZTD.js → terminal-tab-B95lVSty.js} +2 -2
  19. package/dist/web/assets/{use-monaco-theme-Dexl3s3E.js → use-monaco-theme-M04jkKDM.js} +1 -1
  20. package/dist/web/index.html +9 -8
  21. package/dist/web/sw.js +1 -1
  22. package/package.json +1 -1
  23. package/src/server/routes/files.ts +81 -0
  24. package/src/web/components/chat/chat-history-bar.tsx +15 -1
  25. package/src/web/components/chat/chat-tab.tsx +23 -0
  26. package/src/web/components/editor/code-editor.tsx +10 -0
  27. package/src/web/components/explorer/search-panel.tsx +310 -0
  28. package/src/web/components/layout/sidebar.tsx +6 -1
  29. package/src/web/hooks/use-global-keybindings.ts +9 -0
  30. package/src/web/hooks/use-notification-badge.ts +20 -11
  31. package/src/web/hooks/use-tab-overflow.ts +8 -2
  32. package/src/web/stores/keybindings-store.ts +1 -0
  33. package/src/web/stores/settings-store.ts +2 -2
  34. package/dist/web/assets/chat-tab-CbNbBMGw.js +0 -7
  35. package/dist/web/assets/code-editor-D6OuzcC-.js +0 -1
  36. package/dist/web/assets/git-graph-BpTt5iOd.js +0 -1
  37. package/dist/web/assets/index-BU_07_oW.js +0 -29
  38. package/dist/web/assets/index-CBQhXXeV.css +0 -2
  39. package/dist/web/assets/keybindings-store-C0m8_V9X.js +0 -1
  40. package/dist/web/assets/settings-store-CfB0vCtQ.js +0 -1
  41. package/dist/web/assets/settings-tab-Bwsxb41F.js +0 -1
  42. /package/dist/web/assets/{api-client-4Ni0i4Hl.js → api-client-B0aMOJxF.js} +0 -0
  43. /package/dist/web/assets/{dist-CNRrBoQi.js → dist-QgqOdSYG.js} +0 -0
  44. /package/dist/web/assets/{react-DHSo28we.js → react-Dk7fkoaB.js} +0 -0
  45. /package/dist/web/assets/{table-DCVKGOr2.js → table-B6neW6Hr.js} +0 -0
  46. /package/dist/web/assets/{utils-siJJ3uG0.js → utils-DBpa1UZX.js} +0 -0
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":"4276cff85869e97ffbe37bc4b5426eda","url":"index.html"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-512.svg"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-192.svg"},{"revision":"eb9818b9094675c0c5d303168f273345","url":"monacoeditorwork/ts.worker.bundle.js"},{"revision":"9af0be92dcefdc1f1290441cb5ff5d9b","url":"monacoeditorwork/json.worker.bundle.js"},{"revision":"a261b429c39dbb75ae97972d7d005e6d","url":"monacoeditorwork/html.worker.bundle.js"},{"revision":"79953d804e1bbacecfd79b85fd679016","url":"monacoeditorwork/editor.worker.bundle.js"},{"revision":"fdcba0d09aac31df7a0bc652f6e739bd","url":"monacoeditorwork/css.worker.bundle.js"},{"revision":null,"url":"assets/utils-siJJ3uG0.js"},{"revision":null,"url":"assets/use-monaco-theme-Dexl3s3E.js"},{"revision":null,"url":"assets/terminal-tab-D27e4ZTD.js"},{"revision":null,"url":"assets/terminal-tab-BrP-ENHg.css"},{"revision":null,"url":"assets/table-DCVKGOr2.js"},{"revision":null,"url":"assets/tab-store-Bm1Hw8OR.js"},{"revision":null,"url":"assets/sqlite-viewer-DfgaCbWT.js"},{"revision":null,"url":"assets/settings-tab-Bwsxb41F.js"},{"revision":null,"url":"assets/settings-store-CfB0vCtQ.js"},{"revision":null,"url":"assets/react-DHSo28we.js"},{"revision":null,"url":"assets/react-CYzKIDNi.js"},{"revision":null,"url":"assets/postgres-viewer-BL99auSm.js"},{"revision":null,"url":"assets/markdown-renderer-CvGYO9sH.js"},{"revision":null,"url":"assets/keybindings-store-C0m8_V9X.js"},{"revision":null,"url":"assets/jsx-runtime-wQxeESYQ.js"},{"revision":null,"url":"assets/input-nI4xe1Y9.js"},{"revision":null,"url":"assets/index-CBQhXXeV.css"},{"revision":null,"url":"assets/index-BU_07_oW.js"},{"revision":null,"url":"assets/git-graph-BpTt5iOd.js"},{"revision":null,"url":"assets/dist-CNRrBoQi.js"},{"revision":null,"url":"assets/diff-viewer-DAhrHpNM.js"},{"revision":null,"url":"assets/database-viewer-BxUpM_uA.js"},{"revision":null,"url":"assets/code-editor-D6OuzcC-.js"},{"revision":null,"url":"assets/chat-tab-CbNbBMGw.js"},{"revision":null,"url":"assets/api-client-4Ni0i4Hl.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":"2d6f942c9da6283860d6fd34d33a4539","url":"index.html"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-512.svg"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-192.svg"},{"revision":"eb9818b9094675c0c5d303168f273345","url":"monacoeditorwork/ts.worker.bundle.js"},{"revision":"9af0be92dcefdc1f1290441cb5ff5d9b","url":"monacoeditorwork/json.worker.bundle.js"},{"revision":"a261b429c39dbb75ae97972d7d005e6d","url":"monacoeditorwork/html.worker.bundle.js"},{"revision":"79953d804e1bbacecfd79b85fd679016","url":"monacoeditorwork/editor.worker.bundle.js"},{"revision":"fdcba0d09aac31df7a0bc652f6e739bd","url":"monacoeditorwork/css.worker.bundle.js"},{"revision":null,"url":"assets/utils-DBpa1UZX.js"},{"revision":null,"url":"assets/use-monaco-theme-M04jkKDM.js"},{"revision":null,"url":"assets/terminal-tab-BrP-ENHg.css"},{"revision":null,"url":"assets/terminal-tab-B95lVSty.js"},{"revision":null,"url":"assets/table-B6neW6Hr.js"},{"revision":null,"url":"assets/tab-store-D7tRt0VT.js"},{"revision":null,"url":"assets/sqlite-viewer-C5Vj_kSU.js"},{"revision":null,"url":"assets/settings-tab-D_quOlcC.js"},{"revision":null,"url":"assets/settings-store-DS-ifJ7c.js"},{"revision":null,"url":"assets/react-Dk7fkoaB.js"},{"revision":null,"url":"assets/react-CYzKIDNi.js"},{"revision":null,"url":"assets/postgres-viewer-DHvwHGEL.js"},{"revision":null,"url":"assets/markdown-renderer-DKVNZXEw.js"},{"revision":null,"url":"assets/keybindings-store-fcYIcK0C.js"},{"revision":null,"url":"assets/jsx-runtime-wQxeESYQ.js"},{"revision":null,"url":"assets/input-BpFvhpT8.js"},{"revision":null,"url":"assets/index-B9ZHWVob.js"},{"revision":null,"url":"assets/index-4pPCbWJp.css"},{"revision":null,"url":"assets/git-graph-D2BR2rfP.js"},{"revision":null,"url":"assets/dist-QgqOdSYG.js"},{"revision":null,"url":"assets/diff-viewer-CQIockZr.js"},{"revision":null,"url":"assets/database-viewer-C6riNG1v.js"},{"revision":null,"url":"assets/code-editor-CxMuSJrX.js"},{"revision":null,"url":"assets/chat-tab-DXtCcWg2.js"},{"revision":null,"url":"assets/api-client-B0aMOJxF.js"},{"revision":null,"url":"assets/ai-settings-section-CBpXqM-I.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||`/`)}))});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -152,6 +152,87 @@ fileRoutes.post("/rename", async (c) => {
152
152
  }
153
153
  });
154
154
 
155
+ /** Convert glob pattern (VSCode-style) to RegExp for path filtering.
156
+ * - `*.ts` → matches any .ts file in any directory
157
+ * - `src/**` → matches any file under src/
158
+ * - `src/**\/*.ts` → matches .ts files under src/
159
+ */
160
+ function globToPathRegex(glob: string): RegExp {
161
+ const escaped = glob
162
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&") // escape regex special chars
163
+ .replace(/\*\*/g, "\x00") // temp placeholder for **
164
+ .replace(/\*/g, "[^/]*") // * = within one segment
165
+ .replace(/\x00/g, ".*") // ** = across segments
166
+ .replace(/\?/g, "[^/]"); // ? = single non-slash char
167
+ // No slash in pattern → match at any depth (like **/<pattern>)
168
+ const re = glob.includes("/") ? `^${escaped}($|/)` : `(^|/)${escaped}($|/)`;
169
+ return new RegExp(re);
170
+ }
171
+
172
+ /** GET /files/search?q=...&caseSensitive=false — search file content with grep */
173
+ fileRoutes.get("/search", async (c) => {
174
+ const projectPath = c.get("projectPath");
175
+ const q = (c.req.query("q") ?? "").trim();
176
+ const caseSensitive = c.req.query("caseSensitive") === "true";
177
+ const wholeWord = c.req.query("wholeWord") === "true";
178
+ const useRegex = c.req.query("regex") === "true";
179
+ const include = (c.req.query("include") ?? "").trim();
180
+
181
+ if (!useRegex && q.length < 2) return c.json(ok({ results: [], total: 0 }));
182
+ if (useRegex && q.length < 1) return c.json(ok({ results: [], total: 0 }));
183
+
184
+ // Build include path filter regexes (post-filter, supports paths + globs)
185
+ const includeFilters = include
186
+ ? include.split(",").map((s) => s.trim()).filter(Boolean).map(globToPathRegex)
187
+ : [];
188
+
189
+ // Build the grep pattern
190
+ let pattern = useRegex ? q : q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
191
+ if (wholeWord) pattern = `\\b${pattern}\\b`;
192
+
193
+ try {
194
+ const EXCLUDE_DIRS = ["node_modules", ".git", "dist", ".next", "build", ".turbo", "coverage", "__pycache__"];
195
+ const excludeDirArgs = EXCLUDE_DIRS.flatMap((d) => ["--exclude-dir", d]);
196
+ const excludeArgs = ["--exclude=*.min.js", "--exclude=*.map", "--exclude=*.lock", "--exclude=bun.lock"];
197
+ const flags = ["-rn", "--max-count=5", "-I", "-E", ...(caseSensitive ? [] : ["-i"])];
198
+
199
+ const proc = Bun.spawnSync({
200
+ cmd: ["grep", ...flags, ...excludeDirArgs, ...excludeArgs, "--", pattern, projectPath],
201
+ stdout: "pipe",
202
+ stderr: "pipe",
203
+ });
204
+
205
+ const raw = proc.stdout.toString();
206
+ if (!raw.trim()) return c.json(ok({ results: [], total: 0 }));
207
+
208
+ // Parse grep output: /abs/path/file.ts:42:content
209
+ const fileMap = new Map<string, { lineNum: number; content: string }[]>();
210
+ for (const line of raw.split("\n")) {
211
+ if (!line) continue;
212
+ // Strip projectPath prefix, then split on first two colons
213
+ const rel = line.startsWith(projectPath) ? line.slice(projectPath.length + 1) : line;
214
+ const firstColon = rel.indexOf(":");
215
+ if (firstColon < 0) continue;
216
+ const secondColon = rel.indexOf(":", firstColon + 1);
217
+ if (secondColon < 0) continue;
218
+ const filePath = rel.slice(0, firstColon);
219
+ const lineNum = parseInt(rel.slice(firstColon + 1, secondColon), 10);
220
+ const content = rel.slice(secondColon + 1).trimEnd();
221
+ if (!filePath || isNaN(lineNum)) continue;
222
+ // Apply include path filter if specified
223
+ if (includeFilters.length > 0 && !includeFilters.some((re) => re.test(filePath))) continue;
224
+ if (!fileMap.has(filePath)) fileMap.set(filePath, []);
225
+ fileMap.get(filePath)!.push({ lineNum, content });
226
+ }
227
+
228
+ const results = Array.from(fileMap.entries()).map(([file, matches]) => ({ file, matches }));
229
+ const total = results.reduce((sum, r) => sum + r.matches.length, 0);
230
+ return c.json(ok({ results, total }));
231
+ } catch (e) {
232
+ return c.json(err((e as Error).message), 500);
233
+ }
234
+ });
235
+
155
236
  /** POST /files/move — body: { source, destination } */
156
237
  fileRoutes.post("/move", async (c) => {
157
238
  try {
@@ -1,8 +1,9 @@
1
1
  import { useState, useEffect, useCallback, useRef } from "react";
2
- import { History, Settings2, Loader2, MessageSquare, RefreshCw, Search, Pencil, Check, X } from "lucide-react";
2
+ import { History, Settings2, Loader2, MessageSquare, RefreshCw, Search, Pencil, Check, X, BellOff } from "lucide-react";
3
3
  import { Activity } from "lucide-react";
4
4
  import { api, projectUrl } from "@/lib/api-client";
5
5
  import { useTabStore } from "@/stores/tab-store";
6
+ import { useNotificationStore } from "@/stores/notification-store";
6
7
  import { AISettingsSection } from "@/components/settings/ai-settings-section";
7
8
  import { UsageDetailPanel } from "./usage-badge";
8
9
  import type { SessionInfo } from "../../../types/chat";
@@ -54,6 +55,8 @@ export function ChatHistoryBar({
54
55
  const [activePanel, setActivePanel] = useState<PanelType>(null);
55
56
  const [sessions, setSessions] = useState<SessionInfo[]>([]);
56
57
  const [loading, setLoading] = useState(false);
58
+ const hasUnread = useNotificationStore((s) => sessionId ? s.notifications.has(sessionId) : false);
59
+ const clearForSession = useNotificationStore((s) => s.clearForSession);
57
60
  const [searchQuery, setSearchQuery] = useState("");
58
61
  const [editingId, setEditingId] = useState<string | null>(null);
59
62
  const [editingTitle, setEditingTitle] = useState("");
@@ -178,6 +181,17 @@ export function ChatHistoryBar({
178
181
  {/* Spacer */}
179
182
  <div className="flex-1" />
180
183
 
184
+ {/* Mark as read */}
185
+ {hasUnread && sessionId && (
186
+ <button
187
+ onClick={() => clearForSession(sessionId)}
188
+ className="p-1 rounded text-amber-500 hover:text-amber-400 hover:bg-surface-elevated transition-colors"
189
+ title="Mark as read"
190
+ >
191
+ <BellOff className="size-3" />
192
+ </button>
193
+ )}
194
+
181
195
  {/* Connection indicator */}
182
196
  {onReconnect && (
183
197
  <button
@@ -5,6 +5,8 @@ import { useChat } from "@/hooks/use-chat";
5
5
  import { useUsage } from "@/hooks/use-usage";
6
6
  import { useTabStore } from "@/stores/tab-store";
7
7
  import { useSettingsStore } from "@/stores/settings-store";
8
+ import { usePanelStore } from "@/stores/panel-store";
9
+ import { useNotificationStore } from "@/stores/notification-store";
8
10
  import { openBugReportPopup } from "@/lib/report-bug";
9
11
  import { MessageList } from "./message-list";
10
12
  import { MessageInput, type ChatAttachment } from "./message-input";
@@ -79,6 +81,27 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
79
81
  isConnected,
80
82
  } = useChat(sessionId, providerId, projectName);
81
83
 
84
+ // Auto-clear notification badge when this tab is active and document is visible.
85
+ // Handles the case where notification arrived while browser tab was hidden.
86
+ useEffect(() => {
87
+ if (!sessionId || !tabId) return;
88
+ const maybeClear = () => {
89
+ if (document.hidden) return;
90
+ const { panels, focusedPanelId } = usePanelStore.getState();
91
+ const panel = panels[focusedPanelId];
92
+ if (panel?.activeTabId === tabId) {
93
+ useNotificationStore.getState().clearForSession(sessionId);
94
+ }
95
+ };
96
+ maybeClear();
97
+ document.addEventListener("visibilitychange", maybeClear);
98
+ const unsub = usePanelStore.subscribe(maybeClear);
99
+ return () => {
100
+ document.removeEventListener("visibilitychange", maybeClear);
101
+ unsub();
102
+ };
103
+ }, [sessionId, tabId]);
104
+
82
105
  // Update tab title when SDK summary arrives
83
106
  useEffect(() => {
84
107
  if (tabId && sessionTitle) {
@@ -130,8 +130,18 @@ export function CodeEditor({ metadata, tabId }: CodeEditorProps) {
130
130
  saveTimerRef.current = setTimeout(() => saveFile(latestContentRef.current), 1000);
131
131
  }
132
132
 
133
+ // Jump to line when metadata.lineNumber is set (e.g. from search panel)
134
+ const lineNumber = metadata?.lineNumber as number | undefined;
133
135
  const handleEditorMount: OnMount = useCallback((editor, monaco) => {
134
136
  editorRef.current = editor;
137
+ if (lineNumber && lineNumber > 0) {
138
+ // Defer until content is rendered
139
+ setTimeout(() => {
140
+ editor.revealLineInCenter(lineNumber);
141
+ editor.setPosition({ lineNumber, column: 1 });
142
+ editor.focus();
143
+ }, 100);
144
+ }
135
145
  // Alt+Z → toggle word wrap
136
146
  editor.addCommand(
137
147
  monaco.KeyMod.Alt | monaco.KeyCode.KeyZ,
@@ -0,0 +1,310 @@
1
+ import { useState, useRef, useCallback, useEffect } from "react";
2
+ import { Search, CaseSensitive, ChevronRight, ChevronDown, FileText, X, Loader2, WholeWord, Regex, ReplaceAll } from "lucide-react";
3
+ import { useProjectStore } from "@/stores/project-store";
4
+ import { useTabStore } from "@/stores/tab-store";
5
+ import { projectUrl, api } from "@/lib/api-client";
6
+ import { cn } from "@/lib/utils";
7
+
8
+ interface SearchMatch {
9
+ lineNum: number;
10
+ content: string;
11
+ }
12
+
13
+ interface SearchResult {
14
+ file: string;
15
+ matches: SearchMatch[];
16
+ }
17
+
18
+ /** Build highlight regex from query + options */
19
+ function buildHighlightRegex(query: string, caseSensitive: boolean, wholeWord: boolean, useRegex: boolean): RegExp | null {
20
+ if (!query) return null;
21
+ try {
22
+ let pattern = useRegex ? query : query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
23
+ if (wholeWord) pattern = `\\b${pattern}\\b`;
24
+ return new RegExp(`(${pattern})`, caseSensitive ? "g" : "gi");
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ function HighlightMatch({ text, re }: { text: string; re: RegExp | null }) {
31
+ if (!re) return <span>{text}</span>;
32
+ try {
33
+ re.lastIndex = 0;
34
+ const parts = text.split(re);
35
+ re.lastIndex = 0;
36
+ return (
37
+ <span>
38
+ {parts.map((p, i) => {
39
+ re.lastIndex = 0;
40
+ return re.test(p) ? <mark key={i} className="bg-yellow-300/40 text-foreground rounded-sm">{p}</mark> : p;
41
+ })}
42
+ </span>
43
+ );
44
+ } catch {
45
+ return <span>{text}</span>;
46
+ }
47
+ }
48
+
49
+ function OptionButton({ active, onClick, title, children }: { active: boolean; onClick: () => void; title: string; children: React.ReactNode }) {
50
+ return (
51
+ <button
52
+ onClick={onClick}
53
+ title={title}
54
+ className={cn(
55
+ "flex items-center justify-center w-6 h-6 rounded border shrink-0",
56
+ active ? "border-primary text-primary bg-primary/10" : "border-border text-text-subtle hover:text-foreground hover:border-border/80"
57
+ )}
58
+ >
59
+ {children}
60
+ </button>
61
+ );
62
+ }
63
+
64
+ export function SearchPanel() {
65
+ const { activeProject } = useProjectStore();
66
+ const openTab = useTabStore((s) => s.openTab);
67
+
68
+ const [query, setQuery] = useState("");
69
+ const [caseSensitive, setCaseSensitive] = useState(false);
70
+ const [wholeWord, setWholeWord] = useState(false);
71
+ const [useRegex, setUseRegex] = useState(false);
72
+ const [regexError, setRegexError] = useState(false);
73
+ const [filesFilter, setFilesFilter] = useState("");
74
+ const [replace, setReplace] = useState("");
75
+ const [replacing, setReplacing] = useState(false);
76
+ const [replaceCount, setReplaceCount] = useState<number | null>(null);
77
+ const [results, setResults] = useState<SearchResult[]>([]);
78
+ const [total, setTotal] = useState(0);
79
+ const [loading, setLoading] = useState(false);
80
+ const [collapsed, setCollapsed] = useState<Set<string>>(new Set());
81
+ const inputRef = useRef<HTMLInputElement>(null);
82
+ const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
83
+
84
+ const doSearch = useCallback(async (q: string, cs: boolean, ww: boolean, rx: boolean, ff: string) => {
85
+ setRegexError(false);
86
+ if (!activeProject || (!rx && q.length < 2) || (rx && q.length < 1)) {
87
+ setResults([]);
88
+ setTotal(0);
89
+ return;
90
+ }
91
+ if (rx) {
92
+ try { new RegExp(q); } catch { setRegexError(true); setResults([]); return; }
93
+ }
94
+ setLoading(true);
95
+ try {
96
+ const params = new URLSearchParams({ q, caseSensitive: String(cs), wholeWord: String(ww), regex: String(rx) });
97
+ if (ff) params.set("include", ff);
98
+ const data = await api.get<{ results: SearchResult[]; total: number }>(
99
+ `${projectUrl(activeProject.name)}/files/search?${params}`
100
+ );
101
+ setResults(data.results);
102
+ setTotal(data.total);
103
+ } catch {
104
+ setResults([]);
105
+ } finally {
106
+ setLoading(false);
107
+ }
108
+ }, [activeProject]);
109
+
110
+ useEffect(() => {
111
+ if (debounceRef.current) clearTimeout(debounceRef.current);
112
+ debounceRef.current = setTimeout(() => doSearch(query, caseSensitive, wholeWord, useRegex, filesFilter), 300);
113
+ return () => { if (debounceRef.current) clearTimeout(debounceRef.current); };
114
+ }, [query, caseSensitive, wholeWord, useRegex, filesFilter, doSearch]);
115
+
116
+ useEffect(() => { inputRef.current?.focus(); }, []);
117
+
118
+ const highlightRe = buildHighlightRegex(query, caseSensitive, wholeWord, useRegex);
119
+
120
+ function openFile(file: string, lineNum?: number) {
121
+ if (!activeProject) return;
122
+ const name = file.split("/").pop() ?? file;
123
+ openTab({
124
+ type: "editor",
125
+ title: name,
126
+ metadata: { filePath: file, projectName: activeProject.name, lineNumber: lineNum },
127
+ projectId: activeProject.name,
128
+ closable: true,
129
+ });
130
+ }
131
+
132
+ function toggleCollapse(file: string) {
133
+ setCollapsed((prev) => {
134
+ const next = new Set(prev);
135
+ next.has(file) ? next.delete(file) : next.add(file);
136
+ return next;
137
+ });
138
+ }
139
+
140
+ async function doReplaceAll() {
141
+ if (!activeProject || !query || results.length === 0 || replacing) return;
142
+ setReplacing(true);
143
+ setReplaceCount(null);
144
+ let count = 0;
145
+ try {
146
+ for (const r of results) {
147
+ const fileData = await api.get<{ content: string }>(
148
+ `${projectUrl(activeProject.name)}/files/read?path=${encodeURIComponent(r.file)}`
149
+ );
150
+ const re = buildHighlightRegex(query, caseSensitive, wholeWord, useRegex);
151
+ if (!re) continue;
152
+ re.lastIndex = 0;
153
+ const matches = fileData.content.match(re) ?? [];
154
+ if (!matches.length) continue;
155
+ count += matches.length;
156
+ re.lastIndex = 0;
157
+ const newContent = fileData.content.replace(re, replace);
158
+ await api.put(`${projectUrl(activeProject.name)}/files/write`, { path: r.file, content: newContent });
159
+ }
160
+ setReplaceCount(count);
161
+ doSearch(query, caseSensitive, wholeWord, useRegex, filesFilter);
162
+ } catch {
163
+ // ignore
164
+ } finally {
165
+ setReplacing(false);
166
+ }
167
+ }
168
+
169
+ return (
170
+ <div className="flex flex-col h-full">
171
+ {/* Search input + options */}
172
+ <div className="p-2 border-b border-border space-y-1.5">
173
+ {/* Search row */}
174
+ <div className="relative flex items-center gap-1">
175
+ <div className="relative flex-1">
176
+ <Search className="absolute left-2 top-1/2 -translate-y-1/2 size-3.5 text-text-subtle pointer-events-none" />
177
+ <input
178
+ ref={inputRef}
179
+ value={query}
180
+ onChange={(e) => { setQuery(e.target.value); setReplaceCount(null); }}
181
+ placeholder={activeProject ? "Search files…" : "Select a project first"}
182
+ disabled={!activeProject}
183
+ className={cn(
184
+ "w-full pl-7 pr-6 py-1 text-xs bg-input border rounded focus:outline-none focus:ring-1 focus:ring-primary/50 disabled:opacity-50",
185
+ regexError ? "border-destructive" : "border-border"
186
+ )}
187
+ />
188
+ {query && (
189
+ <button
190
+ onClick={() => { setQuery(""); setResults([]); setTotal(0); setRegexError(false); setReplaceCount(null); }}
191
+ className="absolute right-1.5 top-1/2 -translate-y-1/2 text-text-subtle hover:text-foreground"
192
+ >
193
+ <X className="size-3" />
194
+ </button>
195
+ )}
196
+ </div>
197
+ </div>
198
+
199
+ {/* Option toggles */}
200
+ <div className="flex items-center gap-1">
201
+ <OptionButton active={caseSensitive} onClick={() => setCaseSensitive((v) => !v)} title="Match Case (Alt+C)">
202
+ <CaseSensitive className="size-3.5" />
203
+ </OptionButton>
204
+ <OptionButton active={wholeWord} onClick={() => { setWholeWord((v) => !v); if (useRegex) setUseRegex(false); }} title="Match Whole Word (Alt+W)">
205
+ <WholeWord className="size-3.5" />
206
+ </OptionButton>
207
+ <OptionButton active={useRegex} onClick={() => { setUseRegex((v) => !v); if (wholeWord) setWholeWord(false); }} title="Use Regular Expression (Alt+R)">
208
+ <Regex className="size-3.5" />
209
+ </OptionButton>
210
+ {regexError && <span className="text-[10px] text-destructive ml-1">Invalid regex</span>}
211
+ </div>
212
+
213
+ {/* Replace row */}
214
+ <div className="flex items-center gap-1">
215
+ <div className="relative flex-1">
216
+ <input
217
+ value={replace}
218
+ onChange={(e) => setReplace(e.target.value)}
219
+ placeholder="Replace…"
220
+ disabled={!activeProject}
221
+ className="w-full pl-2 pr-6 py-1 text-xs bg-input border border-border rounded focus:outline-none focus:ring-1 focus:ring-primary/50 disabled:opacity-50"
222
+ />
223
+ {replace && (
224
+ <button onClick={() => setReplace("")} className="absolute right-1.5 top-1/2 -translate-y-1/2 text-text-subtle hover:text-foreground">
225
+ <X className="size-3" />
226
+ </button>
227
+ )}
228
+ </div>
229
+ <button
230
+ onClick={doReplaceAll}
231
+ disabled={!query || results.length === 0 || replacing}
232
+ title="Replace All"
233
+ className="flex items-center justify-center w-6 h-6 rounded border border-border text-text-subtle hover:text-foreground hover:border-border/80 disabled:opacity-40 shrink-0"
234
+ >
235
+ {replacing ? <Loader2 className="size-3.5 animate-spin" /> : <ReplaceAll className="size-3.5" />}
236
+ </button>
237
+ </div>
238
+
239
+ {/* Files filter row */}
240
+ <div className="relative">
241
+ <input
242
+ value={filesFilter}
243
+ onChange={(e) => setFilesFilter(e.target.value)}
244
+ placeholder="Files to include (e.g. *.ts, src/**)"
245
+ disabled={!activeProject}
246
+ className="w-full pl-2 pr-6 py-1 text-xs bg-input border border-border rounded focus:outline-none focus:ring-1 focus:ring-primary/50 disabled:opacity-50"
247
+ />
248
+ {filesFilter && (
249
+ <button onClick={() => setFilesFilter("")} className="absolute right-1.5 top-1/2 -translate-y-1/2 text-text-subtle hover:text-foreground">
250
+ <X className="size-3" />
251
+ </button>
252
+ )}
253
+ </div>
254
+
255
+ {/* Status line */}
256
+ <div className="text-[10px] text-text-subtle h-3">
257
+ {(loading || replacing) && (
258
+ <span className="flex items-center gap-1">
259
+ <Loader2 className="size-2.5 animate-spin" />
260
+ {replacing ? "Replacing…" : "Searching…"}
261
+ </span>
262
+ )}
263
+ {!loading && !replacing && replaceCount !== null && (
264
+ <span className="text-green-500">{replaceCount} replacement{replaceCount !== 1 ? "s" : ""} made</span>
265
+ )}
266
+ {!loading && !replacing && replaceCount === null && !regexError && query.length >= 2 && results.length === 0 && <span>No results</span>}
267
+ {!loading && !replacing && replaceCount === null && total > 0 && (
268
+ <span>{total} result{total !== 1 ? "s" : ""} in {results.length} file{results.length !== 1 ? "s" : ""}</span>
269
+ )}
270
+ </div>
271
+ </div>
272
+
273
+ {/* Results */}
274
+ <div className="flex-1 overflow-y-auto min-h-0">
275
+ {results.map((r) => {
276
+ const isCollapsed = collapsed.has(r.file);
277
+ const fileName = r.file.split("/").pop() ?? r.file;
278
+ const dirPath = r.file.includes("/") ? r.file.slice(0, r.file.lastIndexOf("/")) : "";
279
+ return (
280
+ <div key={r.file}>
281
+ <button
282
+ onClick={() => toggleCollapse(r.file)}
283
+ className="w-full flex items-center gap-1 px-2 py-1 hover:bg-muted/50 text-left"
284
+ >
285
+ {isCollapsed ? <ChevronRight className="size-3 shrink-0 text-text-subtle" /> : <ChevronDown className="size-3 shrink-0 text-text-subtle" />}
286
+ <FileText className="size-3 shrink-0 text-text-subtle" />
287
+ <span className="text-xs font-medium text-foreground truncate">{fileName}</span>
288
+ <span className="text-[10px] text-text-subtle truncate flex-1 min-w-0 ml-1">{dirPath}</span>
289
+ <span className="text-[10px] text-text-subtle shrink-0 ml-1 bg-muted px-1 rounded">{r.matches.length}</span>
290
+ </button>
291
+
292
+ {!isCollapsed && r.matches.map((m) => (
293
+ <button
294
+ key={`${r.file}-${m.lineNum}`}
295
+ onClick={() => openFile(r.file, m.lineNum)}
296
+ className="w-full flex items-start gap-2 pl-7 pr-2 py-0.5 hover:bg-primary/10 text-left"
297
+ >
298
+ <span className="text-[10px] text-text-subtle shrink-0 w-7 text-right pt-px">{m.lineNum}</span>
299
+ <span className="text-xs text-text-secondary truncate font-mono leading-4">
300
+ <HighlightMatch text={m.content.trimStart()} re={highlightRe} />
301
+ </span>
302
+ </button>
303
+ ))}
304
+ </div>
305
+ );
306
+ })}
307
+ </div>
308
+ </div>
309
+ );
310
+ }
@@ -1,15 +1,17 @@
1
1
  import { useCallback, useRef } from "react";
2
- import { PanelLeftClose, PanelLeftOpen, FolderOpen, GitBranch, Settings, Database } from "lucide-react";
2
+ import { PanelLeftClose, PanelLeftOpen, FolderOpen, GitBranch, Settings, Database, Search } from "lucide-react";
3
3
  import { useProjectStore } from "@/stores/project-store";
4
4
  import { useSettingsStore, type SidebarActiveTab } from "@/stores/settings-store";
5
5
  import { FileTree } from "@/components/explorer/file-tree";
6
6
  import { GitStatusPanel } from "@/components/git/git-status-panel";
7
7
  import { SettingsTab } from "@/components/settings/settings-tab";
8
8
  import { DatabaseSidebar } from "@/components/database/database-sidebar";
9
+ import { SearchPanel } from "@/components/explorer/search-panel";
9
10
  import { cn } from "@/lib/utils";
10
11
 
11
12
  const TABS: { id: SidebarActiveTab; label: string; icon: React.ElementType }[] = [
12
13
  { id: "explorer", label: "Explorer", icon: FolderOpen },
14
+ { id: "search", label: "Search", icon: Search },
13
15
  { id: "git", label: "Git", icon: GitBranch },
14
16
  { id: "database", label: "Database", icon: Database },
15
17
  { id: "settings", label: "Settings", icon: Settings },
@@ -124,6 +126,9 @@ export function Sidebar() {
124
126
  {sidebarActiveTab === "git" && (
125
127
  <GitStatusPanel metadata={{ projectName: activeProject?.name }} />
126
128
  )}
129
+ {sidebarActiveTab === "search" && (
130
+ <SearchPanel />
131
+ )}
127
132
  {sidebarActiveTab === "database" && (
128
133
  <DatabaseSidebar />
129
134
  )}
@@ -116,6 +116,15 @@ export function useGlobalKeybindings() {
116
116
  return;
117
117
  }
118
118
 
119
+ // Open search (sidebar)
120
+ if (match(e, "open-search")) {
121
+ e.preventDefault();
122
+ const settings = useSettingsStore.getState();
123
+ if (settings.sidebarCollapsed) settings.toggleSidebar();
124
+ settings.setSidebarActiveTab("search");
125
+ return;
126
+ }
127
+
119
128
  // Switch project 1-9
120
129
  for (let i = 1; i <= 9; i++) {
121
130
  if (match(e, `switch-project-${i}`)) {