@hienlh/ppm 0.8.69 → 0.8.71

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 (30) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/web/assets/{browser-tab-BhnA1gi8.js → browser-tab-D5GfU4Ja.js} +1 -1
  3. package/dist/web/assets/chat-tab-BJeNwwUM.js +8 -0
  4. package/dist/web/assets/code-editor-CTjgdXh2.js +2 -0
  5. package/dist/web/assets/{database-viewer-ngGR7s3t.js → database-viewer-QzEuetE6.js} +1 -1
  6. package/dist/web/assets/{diff-viewer-C281t_Li.js → diff-viewer-CvZ06EAH.js} +1 -1
  7. package/dist/web/assets/{git-graph-DxbU6ljU.js → git-graph-BQqdvSjX.js} +1 -1
  8. package/dist/web/assets/index-5a-tMkk5.js +37 -0
  9. package/dist/web/assets/index-CzwYVupc.css +2 -0
  10. package/dist/web/assets/keybindings-store-zY8zbJ2c.js +1 -0
  11. package/dist/web/assets/{markdown-renderer-DaS9de_M.js → markdown-renderer-BVxlq4zO.js} +1 -1
  12. package/dist/web/assets/{postgres-viewer-ck2oHZND.js → postgres-viewer-DP0FOQOa.js} +1 -1
  13. package/dist/web/assets/{settings-tab-D1LdiVy8.js → settings-tab-CcmhnYpw.js} +1 -1
  14. package/dist/web/assets/{sqlite-viewer-l4eyjI6C.js → sqlite-viewer-4a4hHLZk.js} +1 -1
  15. package/dist/web/assets/{terminal-tab-BnA-xDvX.js → terminal-tab-CKsBIgnq.js} +1 -1
  16. package/dist/web/assets/{use-monaco-theme-DsGrkXnv.js → use-monaco-theme-BwIb9BHq.js} +1 -1
  17. package/dist/web/index.html +2 -2
  18. package/dist/web/sw.js +1 -1
  19. package/package.json +1 -1
  20. package/src/server/routes/chat.ts +33 -3
  21. package/src/services/db.service.ts +31 -1
  22. package/src/types/chat.ts +1 -0
  23. package/src/web/components/chat/chat-history-bar.tsx +35 -3
  24. package/src/web/components/chat/session-picker.tsx +78 -31
  25. package/src/web/components/layout/editor-panel.tsx +149 -18
  26. package/dist/web/assets/chat-tab-DqaEGCJ2.js +0 -8
  27. package/dist/web/assets/code-editor-MGlcP92U.js +0 -2
  28. package/dist/web/assets/index-DPI-YVJI.css +0 -2
  29. package/dist/web/assets/index-xBnF10pS.js +0 -37
  30. package/dist/web/assets/keybindings-store-lSdxs8NV.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":"cb207881f2411490f280101b05f4b3cb","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/xychartDiagram-JWTSCODW-z5MVJauZ.js"},{"revision":null,"url":"assets/vennDiagram-LZ73GAT5-BOSy9ma9.js"},{"revision":null,"url":"assets/utils-BNytJOb1.js"},{"revision":null,"url":"assets/use-monaco-theme-DsGrkXnv.js"},{"revision":null,"url":"assets/treemap-KZPCXAKY-B2Xkyv-K.js"},{"revision":null,"url":"assets/timeline-definition-YZTLITO2-58BlOSf9.js"},{"revision":null,"url":"assets/terminal-tab-BrP-ENHg.css"},{"revision":null,"url":"assets/terminal-tab-BnA-xDvX.js"},{"revision":null,"url":"assets/tag-CCtdV063.js"},{"revision":null,"url":"assets/table-C7X5UAEI.js"},{"revision":null,"url":"assets/tab-store-BCfMgMKM.js"},{"revision":null,"url":"assets/stateDiagram-v2-FVOUBMTO-DrxVDY9q.js"},{"revision":null,"url":"assets/stateDiagram-RAJIS63D-f8opcZNY.js"},{"revision":null,"url":"assets/src-BqX54PbV.js"},{"revision":null,"url":"assets/sqlite-viewer-l4eyjI6C.js"},{"revision":null,"url":"assets/settings-tab-D1LdiVy8.js"},{"revision":null,"url":"assets/sequenceDiagram-2WXFIKYE-ByxQqGgs.js"},{"revision":null,"url":"assets/sankeyDiagram-WA2Y5GQK-ClJuW3Hv.js"},{"revision":null,"url":"assets/rough.esm-JX0wREDd.js"},{"revision":null,"url":"assets/requirementDiagram-Z7DCOOCP-BatTxyWb.js"},{"revision":null,"url":"assets/react-nm2Ru1Pt.js"},{"revision":null,"url":"assets/react-dom-Bpkvzu3U.js"},{"revision":null,"url":"assets/react-ER-4DN55.js"},{"revision":null,"url":"assets/radar-KQ55EAFF-DH0AOkUy.js"},{"revision":null,"url":"assets/quadrantDiagram-337W2JSQ-FHMogtsh.js"},{"revision":null,"url":"assets/preload-helper-uTix4PVD.js"},{"revision":null,"url":"assets/postgres-viewer-ck2oHZND.js"},{"revision":null,"url":"assets/pieDiagram-SKSYHLDU-WP0XXw51.js"},{"revision":null,"url":"assets/pie-UPGHQEXC-BHncZutv.js"},{"revision":null,"url":"assets/path-6uRLdFF7.js"},{"revision":null,"url":"assets/packet-RMMSAZCW-DY5PNnZU.js"},{"revision":null,"url":"assets/ordinal-_K3x1fkz.js"},{"revision":null,"url":"assets/mindmap-definition-YRQLILUH-BsfWvIoO.js"},{"revision":null,"url":"assets/mermaid-parser.core-DMIWdgEW.js"},{"revision":null,"url":"assets/math-069Z4SuC.js"},{"revision":null,"url":"assets/markdown-renderer-DaS9de_M.js"},{"revision":null,"url":"assets/linear-DP4mkX3m.js"},{"revision":null,"url":"assets/line-B78g-52T.js"},{"revision":null,"url":"assets/lib-BQ34Db2e.js"},{"revision":null,"url":"assets/keybindings-store-lSdxs8NV.js"},{"revision":null,"url":"assets/katex-Bqvo_ZG0.js"},{"revision":null,"url":"assets/kanban-definition-K7BYSVSG-Bi0UTUeN.js"},{"revision":null,"url":"assets/jsx-runtime-BRW_vwa9.js"},{"revision":null,"url":"assets/journeyDiagram-4ABVD52K-ufoasAy6.js"},{"revision":null,"url":"assets/ishikawaDiagram-PHBUUO56-BOyvKMmB.js"},{"revision":null,"url":"assets/isEmpty-bnrF3Qbc.js"},{"revision":null,"url":"assets/isArrayLikeObject-B_v2FtYn.js"},{"revision":null,"url":"assets/input-BglMT33g.js"},{"revision":null,"url":"assets/init-DlZdxViB.js"},{"revision":null,"url":"assets/infoDiagram-LFFYTUFH-B1CX0pbC.js"},{"revision":null,"url":"assets/info-3K5VOQVL-_vRxVNUm.js"},{"revision":null,"url":"assets/index-xBnF10pS.js"},{"revision":null,"url":"assets/index-DPI-YVJI.css"},{"revision":null,"url":"assets/graphlib-BcsNnGcW.js"},{"revision":null,"url":"assets/gitGraphDiagram-K3NZZRJ6-BTXo57mF.js"},{"revision":null,"url":"assets/gitGraph-HDMCJU4V-Bwna3and.js"},{"revision":null,"url":"assets/git-graph-DxbU6ljU.js"},{"revision":null,"url":"assets/ganttDiagram-A5KZAMGK-D4v7ZbVE.js"},{"revision":null,"url":"assets/flowDiagram-PKNHOUZH-DIqcTrDV.js"},{"revision":null,"url":"assets/erDiagram-INFDFZHY-CKzVujYI.js"},{"revision":null,"url":"assets/dist-lF8CoYII.js"},{"revision":null,"url":"assets/dist-DylI9XxN.js"},{"revision":null,"url":"assets/dist-CSJdAyA9.js"},{"revision":null,"url":"assets/diff-viewer-C281t_Li.js"},{"revision":null,"url":"assets/diagram-P4PSJMXO-BkfNRc9U.js"},{"revision":null,"url":"assets/diagram-IFDJBPK2-k55eVqVU.js"},{"revision":null,"url":"assets/diagram-E7M64L7V-B1Qz70Do.js"},{"revision":null,"url":"assets/defaultLocale-5eAKkKJC.js"},{"revision":null,"url":"assets/database-viewer-ngGR7s3t.js"},{"revision":null,"url":"assets/dagre-KLK3FWXG-BH7aWGRP.js"},{"revision":null,"url":"assets/dagre-Dbb5k38K.js"},{"revision":null,"url":"assets/cytoscape.esm-BW-DbntU.js"},{"revision":null,"url":"assets/csv-preview-DLqYtXxt.js"},{"revision":null,"url":"assets/cose-bilkent-S5V4N54A-B_AWZsOP.js"},{"revision":null,"url":"assets/columns-2-DpsNbZOc.js"},{"revision":null,"url":"assets/code-editor-MGlcP92U.js"},{"revision":null,"url":"assets/clone-LRxlvnMj.js"},{"revision":null,"url":"assets/classDiagram-v2-RAHNMMFH-CxkwuInd.js"},{"revision":null,"url":"assets/classDiagram-VBA2DB6C-lse8oZoJ.js"},{"revision":null,"url":"assets/chunk-YBOYWFTD-CeU4Q-xC.js"},{"revision":null,"url":"assets/chunk-XZSTWKYB-DxAOx4hG.js"},{"revision":null,"url":"assets/chunk-XPW4576I-BPQQBakK.js"},{"revision":null,"url":"assets/chunk-XIRO2GV7-Djlmrely.js"},{"revision":null,"url":"assets/chunk-WL4C6EOR-DfofndiH.js"},{"revision":null,"url":"assets/chunk-R5LLSJPH-CFwSJijQ.js"},{"revision":null,"url":"assets/chunk-QZHKN3VN-CYaTbeZf.js"},{"revision":null,"url":"assets/chunk-PU5JKC2W-Dw8ClWch.js"},{"revision":null,"url":"assets/chunk-PQ6SQG4A-D6BTbCQw.js"},{"revision":null,"url":"assets/chunk-OZEHJAEY-BXhYx3nO.js"},{"revision":null,"url":"assets/chunk-O4XLMI2P-JC6EGoUz.js"},{"revision":null,"url":"assets/chunk-NQ4KR5QH-wMgTlP7f.js"},{"revision":null,"url":"assets/chunk-MX3YWQON-BpS_PtKp.js"},{"revision":null,"url":"assets/chunk-L3YUKLVL-C7qGJrfV.js"},{"revision":null,"url":"assets/chunk-KYZI473N-BcUZNnwd.js"},{"revision":null,"url":"assets/chunk-KX2RTZJC-sQ0o-39C.js"},{"revision":null,"url":"assets/chunk-JSJVCQXG-23tyvw8k.js"},{"revision":null,"url":"assets/chunk-HHEYEP7N-HRhYy3kG.js"},{"revision":null,"url":"assets/chunk-GLR3WWYH-CzYx4w-r.js"},{"revision":null,"url":"assets/chunk-GEFDOKGD-BbQkJu8C.js"},{"revision":null,"url":"assets/chunk-FMBD7UC4-DXncblvW.js"},{"revision":null,"url":"assets/chunk-EGIJ26TM-DzqmU2Z7.js"},{"revision":null,"url":"assets/chunk-CFjPhJqf.js"},{"revision":null,"url":"assets/chunk-C72U2L5F-D21mS_6G.js"},{"revision":null,"url":"assets/chunk-7R4GIKGN-BbIFzsIv.js"},{"revision":null,"url":"assets/chunk-7E7YKBS2-CiyUJxNI.js"},{"revision":null,"url":"assets/chunk-55IACEB6-DJ6BynZ4.js"},{"revision":null,"url":"assets/chunk-4BX2VUAB-D4tOov49.js"},{"revision":null,"url":"assets/chevron-right-DeV0ehiG.js"},{"revision":null,"url":"assets/chat-tab-DqaEGCJ2.js"},{"revision":null,"url":"assets/channel-wrd-NHWf.js"},{"revision":null,"url":"assets/c4Diagram-IC4MRINW-dV22iAsY.js"},{"revision":null,"url":"assets/browser-tab-BhnA1gi8.js"},{"revision":null,"url":"assets/blockDiagram-WCTKOSBZ-TEF8Ally.js"},{"revision":null,"url":"assets/arrow-up--LjUXLEt.js"},{"revision":null,"url":"assets/array-B9UHiPd-.js"},{"revision":null,"url":"assets/architectureDiagram-2XIMDMQ5-DWBCPMLF.js"},{"revision":null,"url":"assets/architecture-PBZL5I3N-DEO2f3VD.js"},{"revision":null,"url":"assets/arc-BAOivWpI.js"},{"revision":null,"url":"assets/api-settings-D21InCnR.js"},{"revision":null,"url":"assets/api-client-BfBM3I7n.js"},{"revision":null,"url":"assets/_baseUniq-BT4Ow4Kk.js"},{"revision":null,"url":"assets/_basePickBy-5PGDJbfF.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":"519dc61eb20563fe21c8146628078ccd","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/xychartDiagram-JWTSCODW-z5MVJauZ.js"},{"revision":null,"url":"assets/vennDiagram-LZ73GAT5-BOSy9ma9.js"},{"revision":null,"url":"assets/utils-BNytJOb1.js"},{"revision":null,"url":"assets/use-monaco-theme-BwIb9BHq.js"},{"revision":null,"url":"assets/treemap-KZPCXAKY-B2Xkyv-K.js"},{"revision":null,"url":"assets/timeline-definition-YZTLITO2-58BlOSf9.js"},{"revision":null,"url":"assets/terminal-tab-CKsBIgnq.js"},{"revision":null,"url":"assets/terminal-tab-BrP-ENHg.css"},{"revision":null,"url":"assets/tag-CCtdV063.js"},{"revision":null,"url":"assets/table-C7X5UAEI.js"},{"revision":null,"url":"assets/tab-store-BCfMgMKM.js"},{"revision":null,"url":"assets/stateDiagram-v2-FVOUBMTO-DrxVDY9q.js"},{"revision":null,"url":"assets/stateDiagram-RAJIS63D-f8opcZNY.js"},{"revision":null,"url":"assets/src-BqX54PbV.js"},{"revision":null,"url":"assets/sqlite-viewer-4a4hHLZk.js"},{"revision":null,"url":"assets/settings-tab-CcmhnYpw.js"},{"revision":null,"url":"assets/sequenceDiagram-2WXFIKYE-ByxQqGgs.js"},{"revision":null,"url":"assets/sankeyDiagram-WA2Y5GQK-ClJuW3Hv.js"},{"revision":null,"url":"assets/rough.esm-JX0wREDd.js"},{"revision":null,"url":"assets/requirementDiagram-Z7DCOOCP-BatTxyWb.js"},{"revision":null,"url":"assets/react-nm2Ru1Pt.js"},{"revision":null,"url":"assets/react-dom-Bpkvzu3U.js"},{"revision":null,"url":"assets/react-ER-4DN55.js"},{"revision":null,"url":"assets/radar-KQ55EAFF-DH0AOkUy.js"},{"revision":null,"url":"assets/quadrantDiagram-337W2JSQ-FHMogtsh.js"},{"revision":null,"url":"assets/preload-helper-uTix4PVD.js"},{"revision":null,"url":"assets/postgres-viewer-DP0FOQOa.js"},{"revision":null,"url":"assets/pieDiagram-SKSYHLDU-WP0XXw51.js"},{"revision":null,"url":"assets/pie-UPGHQEXC-BHncZutv.js"},{"revision":null,"url":"assets/path-6uRLdFF7.js"},{"revision":null,"url":"assets/packet-RMMSAZCW-DY5PNnZU.js"},{"revision":null,"url":"assets/ordinal-_K3x1fkz.js"},{"revision":null,"url":"assets/mindmap-definition-YRQLILUH-BsfWvIoO.js"},{"revision":null,"url":"assets/mermaid-parser.core-DMIWdgEW.js"},{"revision":null,"url":"assets/math-069Z4SuC.js"},{"revision":null,"url":"assets/markdown-renderer-BVxlq4zO.js"},{"revision":null,"url":"assets/linear-DP4mkX3m.js"},{"revision":null,"url":"assets/line-B78g-52T.js"},{"revision":null,"url":"assets/lib-BQ34Db2e.js"},{"revision":null,"url":"assets/keybindings-store-zY8zbJ2c.js"},{"revision":null,"url":"assets/katex-Bqvo_ZG0.js"},{"revision":null,"url":"assets/kanban-definition-K7BYSVSG-Bi0UTUeN.js"},{"revision":null,"url":"assets/jsx-runtime-BRW_vwa9.js"},{"revision":null,"url":"assets/journeyDiagram-4ABVD52K-ufoasAy6.js"},{"revision":null,"url":"assets/ishikawaDiagram-PHBUUO56-BOyvKMmB.js"},{"revision":null,"url":"assets/isEmpty-bnrF3Qbc.js"},{"revision":null,"url":"assets/isArrayLikeObject-B_v2FtYn.js"},{"revision":null,"url":"assets/input-BglMT33g.js"},{"revision":null,"url":"assets/init-DlZdxViB.js"},{"revision":null,"url":"assets/infoDiagram-LFFYTUFH-B1CX0pbC.js"},{"revision":null,"url":"assets/info-3K5VOQVL-_vRxVNUm.js"},{"revision":null,"url":"assets/index-CzwYVupc.css"},{"revision":null,"url":"assets/index-5a-tMkk5.js"},{"revision":null,"url":"assets/graphlib-BcsNnGcW.js"},{"revision":null,"url":"assets/gitGraphDiagram-K3NZZRJ6-BTXo57mF.js"},{"revision":null,"url":"assets/gitGraph-HDMCJU4V-Bwna3and.js"},{"revision":null,"url":"assets/git-graph-BQqdvSjX.js"},{"revision":null,"url":"assets/ganttDiagram-A5KZAMGK-D4v7ZbVE.js"},{"revision":null,"url":"assets/flowDiagram-PKNHOUZH-DIqcTrDV.js"},{"revision":null,"url":"assets/erDiagram-INFDFZHY-CKzVujYI.js"},{"revision":null,"url":"assets/dist-lF8CoYII.js"},{"revision":null,"url":"assets/dist-DylI9XxN.js"},{"revision":null,"url":"assets/dist-CSJdAyA9.js"},{"revision":null,"url":"assets/diff-viewer-CvZ06EAH.js"},{"revision":null,"url":"assets/diagram-P4PSJMXO-BkfNRc9U.js"},{"revision":null,"url":"assets/diagram-IFDJBPK2-k55eVqVU.js"},{"revision":null,"url":"assets/diagram-E7M64L7V-B1Qz70Do.js"},{"revision":null,"url":"assets/defaultLocale-5eAKkKJC.js"},{"revision":null,"url":"assets/database-viewer-QzEuetE6.js"},{"revision":null,"url":"assets/dagre-KLK3FWXG-BH7aWGRP.js"},{"revision":null,"url":"assets/dagre-Dbb5k38K.js"},{"revision":null,"url":"assets/cytoscape.esm-BW-DbntU.js"},{"revision":null,"url":"assets/csv-preview-DLqYtXxt.js"},{"revision":null,"url":"assets/cose-bilkent-S5V4N54A-B_AWZsOP.js"},{"revision":null,"url":"assets/columns-2-DpsNbZOc.js"},{"revision":null,"url":"assets/code-editor-CTjgdXh2.js"},{"revision":null,"url":"assets/clone-LRxlvnMj.js"},{"revision":null,"url":"assets/classDiagram-v2-RAHNMMFH-CxkwuInd.js"},{"revision":null,"url":"assets/classDiagram-VBA2DB6C-lse8oZoJ.js"},{"revision":null,"url":"assets/chunk-YBOYWFTD-CeU4Q-xC.js"},{"revision":null,"url":"assets/chunk-XZSTWKYB-DxAOx4hG.js"},{"revision":null,"url":"assets/chunk-XPW4576I-BPQQBakK.js"},{"revision":null,"url":"assets/chunk-XIRO2GV7-Djlmrely.js"},{"revision":null,"url":"assets/chunk-WL4C6EOR-DfofndiH.js"},{"revision":null,"url":"assets/chunk-R5LLSJPH-CFwSJijQ.js"},{"revision":null,"url":"assets/chunk-QZHKN3VN-CYaTbeZf.js"},{"revision":null,"url":"assets/chunk-PU5JKC2W-Dw8ClWch.js"},{"revision":null,"url":"assets/chunk-PQ6SQG4A-D6BTbCQw.js"},{"revision":null,"url":"assets/chunk-OZEHJAEY-BXhYx3nO.js"},{"revision":null,"url":"assets/chunk-O4XLMI2P-JC6EGoUz.js"},{"revision":null,"url":"assets/chunk-NQ4KR5QH-wMgTlP7f.js"},{"revision":null,"url":"assets/chunk-MX3YWQON-BpS_PtKp.js"},{"revision":null,"url":"assets/chunk-L3YUKLVL-C7qGJrfV.js"},{"revision":null,"url":"assets/chunk-KYZI473N-BcUZNnwd.js"},{"revision":null,"url":"assets/chunk-KX2RTZJC-sQ0o-39C.js"},{"revision":null,"url":"assets/chunk-JSJVCQXG-23tyvw8k.js"},{"revision":null,"url":"assets/chunk-HHEYEP7N-HRhYy3kG.js"},{"revision":null,"url":"assets/chunk-GLR3WWYH-CzYx4w-r.js"},{"revision":null,"url":"assets/chunk-GEFDOKGD-BbQkJu8C.js"},{"revision":null,"url":"assets/chunk-FMBD7UC4-DXncblvW.js"},{"revision":null,"url":"assets/chunk-EGIJ26TM-DzqmU2Z7.js"},{"revision":null,"url":"assets/chunk-CFjPhJqf.js"},{"revision":null,"url":"assets/chunk-C72U2L5F-D21mS_6G.js"},{"revision":null,"url":"assets/chunk-7R4GIKGN-BbIFzsIv.js"},{"revision":null,"url":"assets/chunk-7E7YKBS2-CiyUJxNI.js"},{"revision":null,"url":"assets/chunk-55IACEB6-DJ6BynZ4.js"},{"revision":null,"url":"assets/chunk-4BX2VUAB-D4tOov49.js"},{"revision":null,"url":"assets/chevron-right-DeV0ehiG.js"},{"revision":null,"url":"assets/chat-tab-BJeNwwUM.js"},{"revision":null,"url":"assets/channel-wrd-NHWf.js"},{"revision":null,"url":"assets/c4Diagram-IC4MRINW-dV22iAsY.js"},{"revision":null,"url":"assets/browser-tab-D5GfU4Ja.js"},{"revision":null,"url":"assets/blockDiagram-WCTKOSBZ-TEF8Ally.js"},{"revision":null,"url":"assets/arrow-up--LjUXLEt.js"},{"revision":null,"url":"assets/array-B9UHiPd-.js"},{"revision":null,"url":"assets/architectureDiagram-2XIMDMQ5-DWBCPMLF.js"},{"revision":null,"url":"assets/architecture-PBZL5I3N-DEO2f3VD.js"},{"revision":null,"url":"assets/arc-BAOivWpI.js"},{"revision":null,"url":"assets/api-settings-D21InCnR.js"},{"revision":null,"url":"assets/api-client-BfBM3I7n.js"},{"revision":null,"url":"assets/_baseUniq-BT4Ow4Kk.js"},{"revision":null,"url":"assets/_basePickBy-5PGDJbfF.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.8.69",
3
+ "version": "0.8.71",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "author": "hienlh",
6
6
  "license": "MIT",
@@ -8,8 +8,7 @@ import { renameSession as sdkRenameSession } from "@anthropic-ai/claude-agent-sd
8
8
  import { listSlashItems } from "../../services/slash-items.service.ts";
9
9
  import { getCachedUsage, refreshUsageNow } from "../../services/claude-usage.service.ts";
10
10
  import { getSessionLog } from "../../services/session-log.service.ts";
11
- import { getSessionMapping } from "../../services/db.service.ts";
12
- import { getSessionMapping, setSessionTitle } from "../../services/db.service.ts";
11
+ import { getSessionMapping, setSessionTitle, getPinnedSessionIds, pinSession, unpinSession } from "../../services/db.service.ts";
13
12
  import { ok, err } from "../../types/api.ts";
14
13
 
15
14
  type Env = { Variables: { projectPath: string; projectName: string } };
@@ -64,7 +63,16 @@ chatRoutes.get("/sessions", async (c) => {
64
63
  const projectPath = c.get("projectPath");
65
64
  const providerId = c.req.query("providerId");
66
65
  const sessions = await chatService.listSessions(providerId, projectPath);
67
- return c.json(ok(sessions));
66
+ // Enrich with pin status
67
+ const pinnedIds = getPinnedSessionIds();
68
+ const enriched = sessions.map((s) => ({ ...s, pinned: pinnedIds.has(s.id) }));
69
+ // Sort: pinned first (by pinned_at implicit via Set order), then unpinned by createdAt
70
+ enriched.sort((a, b) => {
71
+ if (a.pinned && !b.pinned) return -1;
72
+ if (!a.pinned && b.pinned) return 1;
73
+ return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
74
+ });
75
+ return c.json(ok(enriched));
68
76
  } catch (e) {
69
77
  return c.json(err((e as Error).message), 500);
70
78
  }
@@ -134,6 +142,28 @@ chatRoutes.patch("/sessions/:id", async (c) => {
134
142
  }
135
143
  });
136
144
 
145
+ /** PUT /chat/sessions/:id/pin — pin a session */
146
+ chatRoutes.put("/sessions/:id/pin", (c) => {
147
+ try {
148
+ const id = c.req.param("id");
149
+ pinSession(id);
150
+ return c.json(ok({ id, pinned: true }));
151
+ } catch (e) {
152
+ return c.json(err((e as Error).message), 500);
153
+ }
154
+ });
155
+
156
+ /** DELETE /chat/sessions/:id/pin — unpin a session */
157
+ chatRoutes.delete("/sessions/:id/pin", (c) => {
158
+ try {
159
+ const id = c.req.param("id");
160
+ unpinSession(id);
161
+ return c.json(ok({ id, pinned: false }));
162
+ } catch (e) {
163
+ return c.json(err((e as Error).message), 500);
164
+ }
165
+ });
166
+
137
167
  /** POST /chat/sessions/:id/fork — fork session into a new one (for rewind/branch) */
138
168
  chatRoutes.post("/sessions/:id/fork", async (c) => {
139
169
  try {
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
4
4
  import { mkdirSync, existsSync } from "node:fs";
5
5
 
6
6
  const PPM_DIR = process.env.PPM_HOME || resolve(homedir(), ".ppm");
7
- const CURRENT_SCHEMA_VERSION = 8;
7
+ const CURRENT_SCHEMA_VERSION = 9;
8
8
 
9
9
  let db: Database | null = null;
10
10
  let dbProfile: string | null = null;
@@ -240,6 +240,17 @@ function runMigrations(database: Database): void {
240
240
  PRAGMA user_version = 8;
241
241
  `);
242
242
  }
243
+
244
+ if (current < 9) {
245
+ database.exec(`
246
+ CREATE TABLE IF NOT EXISTS session_pins (
247
+ session_id TEXT PRIMARY KEY,
248
+ pinned_at TEXT DEFAULT (datetime('now'))
249
+ );
250
+
251
+ PRAGMA user_version = 9;
252
+ `);
253
+ }
243
254
  }
244
255
 
245
256
  // ---------------------------------------------------------------------------
@@ -350,6 +361,25 @@ export function getSessionTitles(sessionIds: string[]): Record<string, string> {
350
361
  return result;
351
362
  }
352
363
 
364
+ // ---------------------------------------------------------------------------
365
+ // Session pin helpers
366
+ // ---------------------------------------------------------------------------
367
+
368
+ export function pinSession(sessionId: string): void {
369
+ getDb().query(
370
+ "INSERT INTO session_pins (session_id, pinned_at) VALUES (?, datetime('now')) ON CONFLICT(session_id) DO UPDATE SET pinned_at = datetime('now')",
371
+ ).run(sessionId);
372
+ }
373
+
374
+ export function unpinSession(sessionId: string): void {
375
+ getDb().query("DELETE FROM session_pins WHERE session_id = ?").run(sessionId);
376
+ }
377
+
378
+ export function getPinnedSessionIds(): Set<string> {
379
+ const rows = getDb().query("SELECT session_id FROM session_pins ORDER BY pinned_at DESC").all() as { session_id: string }[];
380
+ return new Set(rows.map((r) => r.session_id));
381
+ }
382
+
353
383
  // ---------------------------------------------------------------------------
354
384
  // Push subscription helpers
355
385
  // ---------------------------------------------------------------------------
package/src/types/chat.ts CHANGED
@@ -42,6 +42,7 @@ export interface SessionInfo {
42
42
  projectName?: string;
43
43
  createdAt: string;
44
44
  updatedAt?: string;
45
+ pinned?: boolean;
45
46
  }
46
47
 
47
48
  export interface LimitBucket {
@@ -1,5 +1,5 @@
1
1
  import { useState, useEffect, useCallback, useRef } from "react";
2
- import { History, Settings2, Loader2, MessageSquare, RefreshCw, Search, Pencil, Check, X, BellOff, Bug, ClipboardCheck } from "lucide-react";
2
+ import { History, Settings2, Loader2, MessageSquare, RefreshCw, Search, Pencil, Check, X, BellOff, Bug, ClipboardCheck, Pin, PinOff } 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";
@@ -149,6 +149,27 @@ export function ChatHistoryBar({
149
149
 
150
150
  const cancelEditing = useCallback(() => setEditingId(null), []);
151
151
 
152
+ const togglePin = useCallback(async (e: React.MouseEvent, session: SessionInfo) => {
153
+ e.stopPropagation();
154
+ if (!projectName) return;
155
+ const url = `${projectUrl(projectName)}/chat/sessions/${session.id}/pin`;
156
+ try {
157
+ if (session.pinned) {
158
+ await api.del(url);
159
+ } else {
160
+ await api.put(url);
161
+ }
162
+ setSessions((prev) => {
163
+ const updated = prev.map((s) => s.id === session.id ? { ...s, pinned: !s.pinned } : s);
164
+ return updated.sort((a, b) => {
165
+ if (a.pinned && !b.pinned) return -1;
166
+ if (!a.pinned && b.pinned) return 1;
167
+ return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
168
+ });
169
+ });
170
+ } catch { /* silent */ }
171
+ }, [projectName]);
172
+
152
173
  // Filter sessions by search query
153
174
  const filteredSessions = searchQuery.trim()
154
175
  ? sessions.filter((s) => (s.title || "").toLowerCase().includes(searchQuery.toLowerCase()))
@@ -310,9 +331,20 @@ export function ChatHistoryBar({
310
331
  >
311
332
  {session.title || "Untitled"}
312
333
  </button>
334
+ <button
335
+ onClick={(e) => togglePin(e, session)}
336
+ className={`p-0.5 rounded transition-all ${
337
+ session.pinned
338
+ ? "text-primary hover:text-primary/70"
339
+ : "text-text-subtle hover:text-text-secondary md:opacity-0 md:group-hover:opacity-100"
340
+ }`}
341
+ title={session.pinned ? "Unpin session" : "Pin session"}
342
+ >
343
+ {session.pinned ? <PinOff className="size-3" /> : <Pin className="size-3" />}
344
+ </button>
313
345
  <button
314
346
  onClick={(e) => startEditing(session, e)}
315
- className="p-0.5 rounded text-text-subtle hover:text-text-secondary opacity-0 group-hover:opacity-100 transition-opacity"
347
+ className="p-0.5 rounded text-text-subtle hover:text-text-secondary md:opacity-0 md:group-hover:opacity-100 transition-opacity"
316
348
  title="Rename session"
317
349
  >
318
350
  <Pencil className="size-3" />
@@ -320,7 +352,7 @@ export function ChatHistoryBar({
320
352
  </>
321
353
  )}
322
354
  {editingId !== session.id && session.updatedAt && (
323
- <span className="text-[10px] text-text-subtle shrink-0">{formatDate(session.updatedAt)}</span>
355
+ <span className="text-[10px] text-text-subtle shrink-0 w-10 text-right">{formatDate(session.updatedAt)}</span>
324
356
  )}
325
357
  </div>
326
358
  ))
@@ -1,6 +1,6 @@
1
1
  import { useState, useEffect, useCallback } from "react";
2
2
  import { api, projectUrl } from "@/lib/api-client";
3
- import { Plus, Trash2, MessageSquare, ChevronDown } from "lucide-react";
3
+ import { Plus, Trash2, MessageSquare, ChevronDown, Pin, PinOff } from "lucide-react";
4
4
  import type { SessionInfo } from "../../../types/chat";
5
5
 
6
6
  interface SessionPickerProps {
@@ -57,6 +57,75 @@ export function SessionPicker({
57
57
  }
58
58
  };
59
59
 
60
+ const handleTogglePin = async (e: React.MouseEvent, session: SessionInfo) => {
61
+ e.stopPropagation();
62
+ if (!projectName) return;
63
+ const url = `${projectUrl(projectName)}/chat/sessions/${session.id}/pin`;
64
+ try {
65
+ if (session.pinned) {
66
+ await api.del(url);
67
+ } else {
68
+ await api.put(url);
69
+ }
70
+ setSessions((prev) => {
71
+ const updated = prev.map((s) => s.id === session.id ? { ...s, pinned: !s.pinned } : s);
72
+ return updated.sort((a, b) => {
73
+ if (a.pinned && !b.pinned) return -1;
74
+ if (!a.pinned && b.pinned) return 1;
75
+ return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
76
+ });
77
+ });
78
+ } catch {
79
+ // Silently fail
80
+ }
81
+ };
82
+
83
+ function renderSessionRow(session: SessionInfo) {
84
+ return (
85
+ <div
86
+ key={session.id}
87
+ onClick={() => {
88
+ onSelectSession(session);
89
+ setOpen(false);
90
+ }}
91
+ className={`group flex items-center justify-between px-3 py-2 text-sm cursor-pointer hover:bg-surface-elevated transition-colors ${
92
+ session.id === currentSessionId
93
+ ? "bg-surface-elevated text-text-primary"
94
+ : "text-text-secondary"
95
+ }`}
96
+ >
97
+ <div className="flex flex-col min-w-0 flex-1">
98
+ <span className="truncate text-xs font-medium">
99
+ {session.title}
100
+ </span>
101
+ <span className="text-xs text-text-subtle">
102
+ {new Date(session.createdAt).toLocaleDateString()}
103
+ </span>
104
+ </div>
105
+ <div className="flex items-center gap-0.5 shrink-0">
106
+ <button
107
+ onClick={(e) => handleTogglePin(e, session)}
108
+ className={`p-1 rounded transition-colors ${
109
+ session.pinned
110
+ ? "text-primary hover:text-primary/70"
111
+ : "text-text-subtle md:opacity-0 md:group-hover:opacity-100 hover:text-text-primary"
112
+ }`}
113
+ aria-label={session.pinned ? "Unpin session" : "Pin session"}
114
+ >
115
+ {session.pinned ? <PinOff className="size-3" /> : <Pin className="size-3" />}
116
+ </button>
117
+ <button
118
+ onClick={(e) => handleDelete(e, session)}
119
+ className="p-1 rounded hover:bg-red-500/20 text-text-subtle hover:text-red-400 transition-colors md:opacity-0 md:group-hover:opacity-100"
120
+ aria-label="Delete session"
121
+ >
122
+ <Trash2 className="size-3" />
123
+ </button>
124
+ </div>
125
+ </div>
126
+ );
127
+ }
128
+
60
129
  return (
61
130
  <div className="relative">
62
131
  <button
@@ -100,36 +169,14 @@ export function SessionPicker({
100
169
  No sessions yet
101
170
  </p>
102
171
  )}
103
- {sessions.map((session) => (
104
- <div
105
- key={session.id}
106
- onClick={() => {
107
- onSelectSession(session);
108
- setOpen(false);
109
- }}
110
- className={`flex items-center justify-between px-3 py-2 text-sm cursor-pointer hover:bg-surface-elevated transition-colors ${
111
- session.id === currentSessionId
112
- ? "bg-surface-elevated text-text-primary"
113
- : "text-text-secondary"
114
- }`}
115
- >
116
- <div className="flex flex-col min-w-0 flex-1">
117
- <span className="truncate text-xs font-medium">
118
- {session.title}
119
- </span>
120
- <span className="text-xs text-text-subtle">
121
- {new Date(session.createdAt).toLocaleDateString()}
122
- </span>
123
- </div>
124
- <button
125
- onClick={(e) => handleDelete(e, session)}
126
- className="p-1 rounded hover:bg-red-500/20 text-text-subtle hover:text-red-400 transition-colors shrink-0"
127
- aria-label="Delete session"
128
- >
129
- <Trash2 className="size-3" />
130
- </button>
131
- </div>
132
- ))}
172
+ {sessions.filter((s) => s.pinned).length > 0 && (
173
+ <p className="px-3 py-1 text-[10px] text-text-subtle uppercase tracking-wider bg-surface">Pinned</p>
174
+ )}
175
+ {sessions.filter((s) => s.pinned).map((session) => renderSessionRow(session))}
176
+ {sessions.filter((s) => s.pinned).length > 0 && sessions.filter((s) => !s.pinned).length > 0 && (
177
+ <div className="border-t border-border" />
178
+ )}
179
+ {sessions.filter((s) => !s.pinned).map((session) => renderSessionRow(session))}
133
180
  </div>
134
181
  </div>
135
182
  </>
@@ -1,8 +1,10 @@
1
- import { Suspense, lazy } from "react";
2
- import { Loader2, Terminal, MessageSquare, GitBranch } from "lucide-react";
1
+ import { Suspense, lazy, useEffect, useState, useCallback } from "react";
2
+ import { Loader2, Terminal, MessageSquare, GitBranch, Pin, PinOff } from "lucide-react";
3
3
  import { usePanelStore } from "@/stores/panel-store";
4
4
  import { useProjectStore } from "@/stores/project-store";
5
5
  import type { TabType } from "@/stores/tab-store";
6
+ import { api, projectUrl } from "@/lib/api-client";
7
+ import type { SessionInfo } from "../../../types/chat";
6
8
  import { TabBar } from "./tab-bar";
7
9
  import { SplitDropOverlay } from "./split-drop-overlay";
8
10
  import { cn } from "@/lib/utils";
@@ -74,8 +76,68 @@ export function EditorPanel({ panelId, projectName }: EditorPanelProps) {
74
76
  );
75
77
  }
76
78
 
79
+ function formatRelativeDate(iso: string): string {
80
+ try {
81
+ const date = new Date(iso);
82
+ const now = new Date();
83
+ const diffMs = now.getTime() - date.getTime();
84
+ const diffMin = Math.floor(diffMs / 60_000);
85
+ if (diffMin < 1) return "Just now";
86
+ if (diffMin < 60) return `${diffMin}m ago`;
87
+ const diffHr = Math.floor(diffMin / 60);
88
+ if (diffHr < 24) return `${diffHr}h ago`;
89
+ const diffDay = Math.floor(diffHr / 24);
90
+ if (diffDay < 7) return `${diffDay}d ago`;
91
+ return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
92
+ } catch {
93
+ return "";
94
+ }
95
+ }
96
+
97
+ const MAX_RECENT_SESSIONS = 5;
98
+
77
99
  function EmptyPanel({ panelId }: { panelId: string }) {
78
100
  const activeProject = useProjectStore((s) => s.activeProject);
101
+ const [sessions, setSessions] = useState<SessionInfo[]>([]);
102
+ const [loadingSessions, setLoadingSessions] = useState(false);
103
+
104
+ const loadSessions = useCallback(async () => {
105
+ if (!activeProject?.name) return;
106
+ setLoadingSessions(true);
107
+ try {
108
+ const data = await api.get<SessionInfo[]>(`${projectUrl(activeProject.name)}/chat/sessions`);
109
+ setSessions(data.slice(0, MAX_RECENT_SESSIONS));
110
+ } catch {
111
+ // silently ignore — empty state still functional without sessions
112
+ } finally {
113
+ setLoadingSessions(false);
114
+ }
115
+ }, [activeProject?.name]);
116
+
117
+ useEffect(() => { loadSessions(); }, [loadSessions]);
118
+
119
+ const togglePin = useCallback(async (e: React.MouseEvent, session: SessionInfo) => {
120
+ e.stopPropagation();
121
+ if (!activeProject?.name) return;
122
+ const url = `${projectUrl(activeProject.name)}/chat/sessions/${session.id}/pin`;
123
+ try {
124
+ if (session.pinned) {
125
+ await api.del(url);
126
+ } else {
127
+ await api.put(url);
128
+ }
129
+ setSessions((prev) => {
130
+ const updated = prev.map((s) => s.id === session.id ? { ...s, pinned: !s.pinned } : s);
131
+ return updated.sort((a, b) => {
132
+ if (a.pinned && !b.pinned) return -1;
133
+ if (!a.pinned && b.pinned) return 1;
134
+ return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
135
+ });
136
+ });
137
+ } catch {
138
+ // silently ignore
139
+ }
140
+ }, [activeProject?.name]);
79
141
 
80
142
  function openTab(type: TabType) {
81
143
  const needsProject = type !== "settings";
@@ -86,23 +148,92 @@ function EmptyPanel({ panelId }: { panelId: string }) {
86
148
  );
87
149
  }
88
150
 
151
+ function openSession(session: SessionInfo) {
152
+ usePanelStore.getState().openTab(
153
+ {
154
+ type: "chat",
155
+ title: session.title || "Chat",
156
+ projectId: activeProject?.name ?? null,
157
+ metadata: { projectName: activeProject?.name, sessionId: session.id, providerId: session.providerId },
158
+ closable: true,
159
+ },
160
+ panelId,
161
+ );
162
+ }
163
+
164
+ const pinnedSessions = sessions.filter((s) => s.pinned);
165
+ const recentSessions = sessions.filter((s) => !s.pinned).slice(0, MAX_RECENT_SESSIONS);
166
+
167
+ function renderSessionRow(session: SessionInfo) {
168
+ return (
169
+ <button
170
+ key={session.id}
171
+ onClick={() => openSession(session)}
172
+ className="group flex items-center gap-2.5 w-full px-3 py-2.5 text-left hover:bg-surface-elevated active:bg-surface-elevated transition-colors border-b border-border/50 last:border-0"
173
+ >
174
+ <MessageSquare className="size-3.5 shrink-0 text-text-subtle" />
175
+ <span className="flex-1 min-w-0 text-xs font-medium truncate text-text-primary">
176
+ {session.title || "Untitled"}
177
+ </span>
178
+ {session.updatedAt && (
179
+ <span className="text-[10px] text-text-subtle shrink-0">
180
+ {formatRelativeDate(session.updatedAt)}
181
+ </span>
182
+ )}
183
+ <span
184
+ role="button"
185
+ tabIndex={0}
186
+ onClick={(e) => togglePin(e, session)}
187
+ className={`p-1 rounded transition-colors shrink-0 ${
188
+ session.pinned
189
+ ? "text-primary hover:text-primary/70"
190
+ : "text-text-subtle md:opacity-0 md:group-hover:opacity-100 hover:text-text-primary"
191
+ }`}
192
+ aria-label={session.pinned ? "Unpin session" : "Pin session"}
193
+ >
194
+ {session.pinned ? <PinOff className="size-3" /> : <Pin className="size-3" />}
195
+ </span>
196
+ </button>
197
+ );
198
+ }
199
+
89
200
  return (
90
- <div className="flex flex-col items-center justify-center h-full gap-4 text-text-secondary">
91
- <p className="text-sm">Open a tab to get started</p>
92
- <div className="flex flex-col md:flex-row flex-wrap justify-center gap-2">
93
- {QUICK_OPEN_TABS.map((opt) => {
94
- const Icon = opt.icon;
95
- return (
96
- <button
97
- key={opt.type}
98
- onClick={() => openTab(opt.type)}
99
- className="flex items-center gap-2 px-4 py-2 rounded-md border border-border bg-surface hover:bg-surface-elevated text-sm text-foreground transition-colors"
100
- >
101
- <Icon className="size-4" />
102
- {opt.label}
103
- </button>
104
- );
105
- })}
201
+ <div className="flex flex-col h-full overflow-y-auto text-text-secondary">
202
+ <div className="flex flex-col items-center justify-center gap-6 px-4 flex-1">
203
+ <p className="text-sm">Open a tab to get started</p>
204
+ <div className="grid grid-cols-3 gap-2 w-full max-w-sm">
205
+ {QUICK_OPEN_TABS.map((opt) => {
206
+ const Icon = opt.icon;
207
+ return (
208
+ <button
209
+ key={opt.type}
210
+ onClick={() => openTab(opt.type)}
211
+ className="flex flex-col items-center justify-center gap-1.5 px-2 py-3 rounded-md border border-border bg-surface hover:bg-surface-elevated active:bg-surface-elevated text-xs text-foreground transition-colors"
212
+ >
213
+ <Icon className="size-5" />
214
+ {opt.label}
215
+ </button>
216
+ );
217
+ })}
218
+ </div>
219
+
220
+ {activeProject && !loadingSessions && pinnedSessions.length > 0 && (
221
+ <div className="flex flex-col gap-2 w-full max-w-sm">
222
+ <p className="text-xs text-text-subtle text-center">Pinned</p>
223
+ <div className="w-full rounded-md border border-border bg-surface overflow-hidden">
224
+ {pinnedSessions.map(renderSessionRow)}
225
+ </div>
226
+ </div>
227
+ )}
228
+
229
+ {activeProject && !loadingSessions && recentSessions.length > 0 && (
230
+ <div className="flex flex-col gap-2 w-full max-w-sm">
231
+ <p className="text-xs text-text-subtle text-center">Recent chats</p>
232
+ <div className="w-full rounded-md border border-border bg-surface overflow-hidden">
233
+ {recentSessions.map(renderSessionRow)}
234
+ </div>
235
+ </div>
236
+ )}
106
237
  </div>
107
238
  </div>
108
239
  );