@hienlh/ppm 0.11.17 → 0.12.0

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 (56) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/web/assets/{ai-settings-section-L6XAmZEP.js → ai-settings-section-BHdBBJtS.js} +1 -1
  3. package/dist/web/assets/{audio-preview-VMboGrIH.js → audio-preview-D4AxF10w.js} +1 -1
  4. package/dist/web/assets/chat-tab-Bq2hmJ-B.js +12 -0
  5. package/dist/web/assets/code-editor-CMcDjype.js +8 -0
  6. package/dist/web/assets/{conflict-editor-943WUefe.js → conflict-editor-Br-ugFiK.js} +1 -1
  7. package/dist/web/assets/{csv-preview-BEBJD4a_.js → csv-preview-HMSavgBb.js} +1 -1
  8. package/dist/web/assets/{database-viewer-BV0Ebp0z.js → database-viewer-DxP0GmQK.js} +2 -2
  9. package/dist/web/assets/{diff-viewer-B3gAWXgA.js → diff-viewer-oEyE9UwV.js} +1 -1
  10. package/dist/web/assets/dist-D7KGU7Vl.js +1 -0
  11. package/dist/web/assets/extension-webview-CVqfQGjg.js +3 -0
  12. package/dist/web/assets/{image-preview-BEiYtg6_.js → image-preview-CY3sVd25.js} +1 -1
  13. package/dist/web/assets/index-BDRoldC9.js +23 -0
  14. package/dist/web/assets/index-CDSox8V2.css +2 -0
  15. package/dist/web/assets/{input-ClhO__YM.js → input-Dk49gO8E.js} +1 -1
  16. package/dist/web/assets/{markdown-renderer-t1ZBKbXZ.js → markdown-renderer-DwqWhkri.js} +1 -1
  17. package/dist/web/assets/{pdf-preview-CjfQxXE5.js → pdf-preview-Cl95qWE_.js} +1 -1
  18. package/dist/web/assets/{port-forwarding-tab-BZmfg410.js → port-forwarding-tab-iJ3MAjXa.js} +1 -1
  19. package/dist/web/assets/{postgres-viewer-CSTO0jc2.js → postgres-viewer-Do_w0Cji.js} +2 -2
  20. package/dist/web/assets/{scroll-area-DW7L4Gnc.js → scroll-area-BEllam7_.js} +1 -1
  21. package/dist/web/assets/settings-tab-DyBeLmUh.js +1 -0
  22. package/dist/web/assets/{sqlite-viewer-D0oWgepE.js → sqlite-viewer-oZkGJfW2.js} +1 -1
  23. package/dist/web/assets/{terminal-tab-WBPZXu12.js → terminal-tab-UoDiWvzG.js} +1 -1
  24. package/dist/web/assets/{vendor-ui-B-T_damt.js → vendor-ui-B-89Uj8i.js} +1 -1
  25. package/dist/web/assets/{video-preview-BcMa4tim.js → video-preview-3MbkDYcA.js} +1 -1
  26. package/dist/web/index.html +7 -7
  27. package/dist/web/sw.js +1 -1
  28. package/docs/project-changelog.md +56 -0
  29. package/docs/system-architecture.md +10 -1
  30. package/package.json +1 -1
  31. package/src/server/routes/chat.ts +57 -2
  32. package/src/server/routes/project-scoped.ts +2 -0
  33. package/src/server/routes/tag-routes.ts +93 -0
  34. package/src/services/db.service.ts +35 -1
  35. package/src/services/project.service.ts +2 -0
  36. package/src/services/supervisor.ts +7 -2
  37. package/src/services/tag.service.ts +114 -0
  38. package/src/types/chat.ts +9 -0
  39. package/src/web/components/chat/chat-history-bar.tsx +106 -7
  40. package/src/web/components/chat/chat-welcome.tsx +54 -27
  41. package/src/web/components/chat/session-context-menu.tsx +101 -0
  42. package/src/web/components/chat/session-picker.tsx +3 -0
  43. package/src/web/components/chat/tag-filter-chips.tsx +58 -0
  44. package/src/web/components/extensions/extension-webview.tsx +5 -33
  45. package/src/web/components/layout/editor-panel.tsx +53 -26
  46. package/src/web/components/layout/upgrade-banner.tsx +47 -37
  47. package/src/web/components/settings/tag-settings-section.tsx +167 -0
  48. package/src/web/hooks/use-extension-ws.ts +7 -2
  49. package/src/web/styles/globals.css +14 -0
  50. package/dist/web/assets/chat-tab-DfO2rHO8.js +0 -12
  51. package/dist/web/assets/code-editor-BU7NX_SZ.js +0 -8
  52. package/dist/web/assets/dist-C5IgeqrV.js +0 -1
  53. package/dist/web/assets/extension-webview-C8rdBYLl.js +0 -3
  54. package/dist/web/assets/index-B0V_IYbX.css +0 -2
  55. package/dist/web/assets/index-CBsOxcqb.js +0 -23
  56. package/dist/web/assets/settings-tab-b3AbZg6I.js +0 -1
package/dist/web/sw.js CHANGED
@@ -1 +1 @@
1
- try{self[`workbox:core:7.3.0`]&&_()}catch{}var e=(e,...t)=>{let n=e;return t.length>0&&(n+=` :: ${JSON.stringify(t)}`),n},t=class extends Error{constructor(t,n){let r=e(t,n);super(r),this.name=t,this.details=n}},n={googleAnalytics:`googleAnalytics`,precache:`precache-v2`,prefix:`workbox`,runtime:`runtime`,suffix:typeof registration<`u`?registration.scope:``},r=e=>[n.prefix,e,n.suffix].filter(e=>e&&e.length>0).join(`-`),i=e=>{for(let t of Object.keys(n))e(t)},a={updateDetails:e=>{i(t=>{typeof e[t]==`string`&&(n[t]=e[t])})},getGoogleAnalyticsName:e=>e||r(n.googleAnalytics),getPrecacheName:e=>e||r(n.precache),getPrefix:()=>n.prefix,getRuntimeName:e=>e||r(n.runtime),getSuffix:()=>n.suffix};function o(e,t){let n=t();return e.waitUntil(n),n}try{self[`workbox:precaching:7.3.0`]&&_()}catch{}var s=`__WB_REVISION__`;function c(e){if(!e)throw new t(`add-to-cache-list-unexpected-type`,{entry:e});if(typeof e==`string`){let t=new URL(e,location.href);return{cacheKey:t.href,url:t.href}}let{revision:n,url:r}=e;if(!r)throw new t(`add-to-cache-list-unexpected-type`,{entry:e});if(!n){let e=new URL(r,location.href);return{cacheKey:e.href,url:e.href}}let i=new URL(r,location.href),a=new URL(r,location.href);return i.searchParams.set(s,n),{cacheKey:i.href,url:a.href}}var l=class{constructor(){this.updatedURLs=[],this.notUpdatedURLs=[],this.handlerWillStart=async({request:e,state:t})=>{t&&(t.originalRequest=e)},this.cachedResponseWillBeUsed=async({event:e,state:t,cachedResponse:n})=>{if(e.type===`install`&&t&&t.originalRequest&&t.originalRequest instanceof Request){let e=t.originalRequest.url;n?this.notUpdatedURLs.push(e):this.updatedURLs.push(e)}return n}}},u=class{constructor({precacheController:e}){this.cacheKeyWillBeUsed=async({request:e,params:t})=>{let n=t?.cacheKey||this._precacheController.getCacheKeyForURL(e.url);return n?new Request(n,{headers:e.headers}):e},this._precacheController=e}},d;function f(){if(d===void 0){let e=new Response(``);if(`body`in e)try{new Response(e.body),d=!0}catch{d=!1}d=!1}return d}async function p(e,n){let r=null;if(e.url&&(r=new URL(e.url).origin),r!==self.location.origin)throw new t(`cross-origin-copy-response`,{origin:r});let i=e.clone(),a={headers:new Headers(i.headers),status:i.status,statusText:i.statusText},o=n?n(a):a,s=f()?i.body:await i.blob();return new Response(s,o)}var m=e=>new URL(String(e),location.href).href.replace(RegExp(`^${location.origin}`),``);function h(e,t){let n=new URL(e);for(let e of t)n.searchParams.delete(e);return n.href}async function g(e,t,n,r){let i=h(t.url,n);if(t.url===i)return e.match(t,r);let a=Object.assign(Object.assign({},r),{ignoreSearch:!0}),o=await e.keys(t,a);for(let t of o)if(i===h(t.url,n))return e.match(t,r)}var v=class{constructor(){this.promise=new Promise((e,t)=>{this.resolve=e,this.reject=t})}},y=new Set;async function b(){for(let e of y)await e()}function x(e){return new Promise(t=>setTimeout(t,e))}try{self[`workbox:strategies:7.3.0`]&&_()}catch{}function S(e){return typeof e==`string`?new Request(e):e}var C=class{constructor(e,t){this._cacheKeys={},Object.assign(this,t),this.event=t.event,this._strategy=e,this._handlerDeferred=new v,this._extendLifetimePromises=[],this._plugins=[...e.plugins],this._pluginStateMap=new Map;for(let e of this._plugins)this._pluginStateMap.set(e,{});this.event.waitUntil(this._handlerDeferred.promise)}async fetch(e){let{event:n}=this,r=S(e);if(r.mode===`navigate`&&n instanceof FetchEvent&&n.preloadResponse){let e=await n.preloadResponse;if(e)return e}let i=this.hasCallback(`fetchDidFail`)?r.clone():null;try{for(let e of this.iterateCallbacks(`requestWillFetch`))r=await e({request:r.clone(),event:n})}catch(e){if(e instanceof Error)throw new t(`plugin-error-request-will-fetch`,{thrownErrorMessage:e.message})}let a=r.clone();try{let e;e=await fetch(r,r.mode===`navigate`?void 0:this._strategy.fetchOptions);for(let t of this.iterateCallbacks(`fetchDidSucceed`))e=await t({event:n,request:a,response:e});return e}catch(e){throw i&&await this.runCallbacks(`fetchDidFail`,{error:e,event:n,originalRequest:i.clone(),request:a.clone()}),e}}async fetchAndCachePut(e){let t=await this.fetch(e),n=t.clone();return this.waitUntil(this.cachePut(e,n)),t}async cacheMatch(e){let t=S(e),n,{cacheName:r,matchOptions:i}=this._strategy,a=await this.getCacheKey(t,`read`),o=Object.assign(Object.assign({},i),{cacheName:r});n=await caches.match(a,o);for(let e of this.iterateCallbacks(`cachedResponseWillBeUsed`))n=await e({cacheName:r,matchOptions:i,cachedResponse:n,request:a,event:this.event})||void 0;return n}async cachePut(e,n){let r=S(e);await x(0);let i=await this.getCacheKey(r,`write`);if(!n)throw new t(`cache-put-with-no-response`,{url:m(i.url)});let a=await this._ensureResponseSafeToCache(n);if(!a)return!1;let{cacheName:o,matchOptions:s}=this._strategy,c=await self.caches.open(o),l=this.hasCallback(`cacheDidUpdate`),u=l?await g(c,i.clone(),[`__WB_REVISION__`],s):null;try{await c.put(i,l?a.clone():a)}catch(e){if(e instanceof Error)throw e.name===`QuotaExceededError`&&await b(),e}for(let e of this.iterateCallbacks(`cacheDidUpdate`))await e({cacheName:o,oldResponse:u,newResponse:a.clone(),request:i,event:this.event});return!0}async getCacheKey(e,t){let n=`${e.url} | ${t}`;if(!this._cacheKeys[n]){let r=e;for(let e of this.iterateCallbacks(`cacheKeyWillBeUsed`))r=S(await e({mode:t,request:r,event:this.event,params:this.params}));this._cacheKeys[n]=r}return this._cacheKeys[n]}hasCallback(e){for(let t of this._strategy.plugins)if(e in t)return!0;return!1}async runCallbacks(e,t){for(let n of this.iterateCallbacks(e))await n(t)}*iterateCallbacks(e){for(let t of this._strategy.plugins)if(typeof t[e]==`function`){let n=this._pluginStateMap.get(t);yield r=>{let i=Object.assign(Object.assign({},r),{state:n});return t[e](i)}}}waitUntil(e){return this._extendLifetimePromises.push(e),e}async doneWaiting(){for(;this._extendLifetimePromises.length;){let e=this._extendLifetimePromises.splice(0),t=(await Promise.allSettled(e)).find(e=>e.status===`rejected`);if(t)throw t.reason}}destroy(){this._handlerDeferred.resolve(null)}async _ensureResponseSafeToCache(e){let t=e,n=!1;for(let e of this.iterateCallbacks(`cacheWillUpdate`))if(t=await e({request:this.request,response:t,event:this.event})||void 0,n=!0,!t)break;return n||t&&t.status!==200&&(t=void 0),t}},w=class{constructor(e={}){this.cacheName=a.getRuntimeName(e.cacheName),this.plugins=e.plugins||[],this.fetchOptions=e.fetchOptions,this.matchOptions=e.matchOptions}handle(e){let[t]=this.handleAll(e);return t}handleAll(e){e instanceof FetchEvent&&(e={event:e,request:e.request});let t=e.event,n=typeof e.request==`string`?new Request(e.request):e.request,r=`params`in e?e.params:void 0,i=new C(this,{event:t,request:n,params:r}),a=this._getResponse(i,n,t);return[a,this._awaitComplete(a,i,n,t)]}async _getResponse(e,n,r){await e.runCallbacks(`handlerWillStart`,{event:r,request:n});let i;try{if(i=await this._handle(n,e),!i||i.type===`error`)throw new t(`no-response`,{url:n.url})}catch(t){if(t instanceof Error){for(let a of e.iterateCallbacks(`handlerDidError`))if(i=await a({error:t,event:r,request:n}),i)break}if(!i)throw t}for(let t of e.iterateCallbacks(`handlerWillRespond`))i=await t({event:r,request:n,response:i});return i}async _awaitComplete(e,t,n,r){let i,a;try{i=await e}catch{}try{await t.runCallbacks(`handlerDidRespond`,{event:r,request:n,response:i}),await t.doneWaiting()}catch(e){e instanceof Error&&(a=e)}if(await t.runCallbacks(`handlerDidComplete`,{event:r,request:n,response:i,error:a}),t.destroy(),a)throw a}},T=class e extends w{constructor(t={}){t.cacheName=a.getPrecacheName(t.cacheName),super(t),this._fallbackToNetwork=t.fallbackToNetwork!==!1,this.plugins.push(e.copyRedirectedCacheableResponsesPlugin)}async _handle(e,t){return await t.cacheMatch(e)||(t.event&&t.event.type===`install`?await this._handleInstall(e,t):await this._handleFetch(e,t))}async _handleFetch(e,n){let r,i=n.params||{};if(this._fallbackToNetwork){let t=i.integrity,a=e.integrity,o=!a||a===t;r=await n.fetch(new Request(e,{integrity:e.mode===`no-cors`?void 0:a||t})),t&&o&&e.mode!==`no-cors`&&(this._useDefaultCacheabilityPluginIfNeeded(),await n.cachePut(e,r.clone()))}else throw new t(`missing-precache-entry`,{cacheName:this.cacheName,url:e.url});return r}async _handleInstall(e,n){this._useDefaultCacheabilityPluginIfNeeded();let r=await n.fetch(e);if(!await n.cachePut(e,r.clone()))throw new t(`bad-precaching-response`,{url:e.url,status:r.status});return r}_useDefaultCacheabilityPluginIfNeeded(){let t=null,n=0;for(let[r,i]of this.plugins.entries())i!==e.copyRedirectedCacheableResponsesPlugin&&(i===e.defaultPrecacheCacheabilityPlugin&&(t=r),i.cacheWillUpdate&&n++);n===0?this.plugins.push(e.defaultPrecacheCacheabilityPlugin):n>1&&t!==null&&this.plugins.splice(t,1)}};T.defaultPrecacheCacheabilityPlugin={async cacheWillUpdate({response:e}){return!e||e.status>=400?null:e}},T.copyRedirectedCacheableResponsesPlugin={async cacheWillUpdate({response:e}){return e.redirected?await p(e):e}};var E=class{constructor({cacheName:e,plugins:t=[],fallbackToNetwork:n=!0}={}){this._urlsToCacheKeys=new Map,this._urlsToCacheModes=new Map,this._cacheKeysToIntegrities=new Map,this._strategy=new T({cacheName:a.getPrecacheName(e),plugins:[...t,new u({precacheController:this})],fallbackToNetwork:n}),this.install=this.install.bind(this),this.activate=this.activate.bind(this)}get strategy(){return this._strategy}precache(e){this.addToCacheList(e),this._installAndActiveListenersAdded||=(self.addEventListener(`install`,this.install),self.addEventListener(`activate`,this.activate),!0)}addToCacheList(e){let n=[];for(let r of e){typeof r==`string`?n.push(r):r&&r.revision===void 0&&n.push(r.url);let{cacheKey:e,url:i}=c(r),a=typeof r!=`string`&&r.revision?`reload`:`default`;if(this._urlsToCacheKeys.has(i)&&this._urlsToCacheKeys.get(i)!==e)throw new t(`add-to-cache-list-conflicting-entries`,{firstEntry:this._urlsToCacheKeys.get(i),secondEntry:e});if(typeof r!=`string`&&r.integrity){if(this._cacheKeysToIntegrities.has(e)&&this._cacheKeysToIntegrities.get(e)!==r.integrity)throw new t(`add-to-cache-list-conflicting-integrities`,{url:i});this._cacheKeysToIntegrities.set(e,r.integrity)}if(this._urlsToCacheKeys.set(i,e),this._urlsToCacheModes.set(i,a),n.length>0){let e=`Workbox is precaching URLs without revision info: ${n.join(`, `)}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(e)}}}install(e){return o(e,async()=>{let t=new l;this.strategy.plugins.push(t);for(let[t,n]of this._urlsToCacheKeys){let r=this._cacheKeysToIntegrities.get(n),i=this._urlsToCacheModes.get(t),a=new Request(t,{integrity:r,cache:i,credentials:`same-origin`});await Promise.all(this.strategy.handleAll({params:{cacheKey:n},request:a,event:e}))}let{updatedURLs:n,notUpdatedURLs:r}=t;return{updatedURLs:n,notUpdatedURLs:r}})}activate(e){return o(e,async()=>{let e=await self.caches.open(this.strategy.cacheName),t=await e.keys(),n=new Set(this._urlsToCacheKeys.values()),r=[];for(let i of t)n.has(i.url)||(await e.delete(i),r.push(i.url));return{deletedURLs:r}})}getURLsToCacheKeys(){return this._urlsToCacheKeys}getCachedURLs(){return[...this._urlsToCacheKeys.keys()]}getCacheKeyForURL(e){let t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForCacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){let t=e instanceof Request?e.url:e,n=this.getCacheKeyForURL(t);if(n)return(await self.caches.open(this.strategy.cacheName)).match(n)}createHandlerBoundToURL(e){let n=this.getCacheKeyForURL(e);if(!n)throw new t(`non-precached-url`,{url:e});return t=>(t.request=new Request(e),t.params=Object.assign({cacheKey:n},t.params),this.strategy.handle(t))}},D,O=()=>(D||=new E,D);try{self[`workbox:routing:7.3.0`]&&_()}catch{}var k=e=>e&&typeof e==`object`?e:{handle:e},A=class{constructor(e,t,n=`GET`){this.handler=k(t),this.match=e,this.method=n}setCatchHandler(e){this.catchHandler=k(e)}},j=class extends A{constructor(e,t,n){super(({url:t})=>{let n=e.exec(t.href);if(n&&!(t.origin!==location.origin&&n.index!==0))return n.slice(1)},t,n)}},M=class{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener(`fetch`,(e=>{let{request:t}=e,n=this.handleRequest({request:t,event:e});n&&e.respondWith(n)}))}addCacheListener(){self.addEventListener(`message`,(e=>{if(e.data&&e.data.type===`CACHE_URLS`){let{payload:t}=e.data,n=Promise.all(t.urlsToCache.map(t=>{typeof t==`string`&&(t=[t]);let n=new Request(...t);return this.handleRequest({request:n,event:e})}));e.waitUntil(n),e.ports&&e.ports[0]&&n.then(()=>e.ports[0].postMessage(!0))}}))}handleRequest({request:e,event:t}){let n=new URL(e.url,location.href);if(!n.protocol.startsWith(`http`))return;let r=n.origin===location.origin,{params:i,route:a}=this.findMatchingRoute({event:t,request:e,sameOrigin:r,url:n}),o=a&&a.handler,s=e.method;if(!o&&this._defaultHandlerMap.has(s)&&(o=this._defaultHandlerMap.get(s)),!o)return;let c;try{c=o.handle({url:n,request:e,event:t,params:i})}catch(e){c=Promise.reject(e)}let l=a&&a.catchHandler;return c instanceof Promise&&(this._catchHandler||l)&&(c=c.catch(async r=>{if(l)try{return await l.handle({url:n,request:e,event:t,params:i})}catch(e){e instanceof Error&&(r=e)}if(this._catchHandler)return this._catchHandler.handle({url:n,request:e,event:t});throw r})),c}findMatchingRoute({url:e,sameOrigin:t,request:n,event:r}){let i=this._routes.get(n.method)||[];for(let a of i){let i,o=a.match({url:e,sameOrigin:t,request:n,event:r});if(o)return i=o,(Array.isArray(i)&&i.length===0||o.constructor===Object&&Object.keys(o).length===0||typeof o==`boolean`)&&(i=void 0),{route:a,params:i}}return{}}setDefaultHandler(e,t=`GET`){this._defaultHandlerMap.set(t,k(e))}setCatchHandler(e){this._catchHandler=k(e)}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new t(`unregister-route-but-not-found-with-method`,{method:e.method});let n=this._routes.get(e.method).indexOf(e);if(n>-1)this._routes.get(e.method).splice(n,1);else throw new t(`unregister-route-route-not-registered`)}},N,P=()=>(N||(N=new M,N.addFetchListener(),N.addCacheListener()),N);function F(e,n,r){let i;if(typeof e==`string`){let t=new URL(e,location.href);i=new A(({url:e})=>e.href===t.href,n,r)}else if(e instanceof RegExp)i=new j(e,n,r);else if(typeof e==`function`)i=new A(e,n,r);else if(e instanceof A)i=e;else throw new t(`unsupported-route-type`,{moduleName:`workbox-routing`,funcName:`registerRoute`,paramName:`capture`});return P().registerRoute(i),i}function I(e,t=[]){for(let n of[...e.searchParams.keys()])t.some(e=>e.test(n))&&e.searchParams.delete(n);return e}function*L(e,{ignoreURLParametersMatching:t=[/^utm_/,/^fbclid$/],directoryIndex:n=`index.html`,cleanURLs:r=!0,urlManipulation:i}={}){let a=new URL(e,location.href);a.hash=``,yield a.href;let o=I(a,t);if(yield o.href,n&&o.pathname.endsWith(`/`)){let e=new URL(o.href);e.pathname+=n,yield e.href}if(r){let e=new URL(o.href);e.pathname+=`.html`,yield e.href}if(i){let e=i({url:a});for(let t of e)yield t.href}}var R=class extends A{constructor(e,t){super(({request:n})=>{let r=e.getURLsToCacheKeys();for(let i of L(n.url,t)){let t=r.get(i);if(t)return{cacheKey:t,integrity:e.getIntegrityForCacheKey(t)}}},e.strategy)}};function z(e){F(new R(O(),e))}function B(e){O().precache(e)}function V(e,t){B(e),z(t)}V([{"revision":"1872c500de691dce40960bb85481de07","url":"registerSW.js"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-192.svg"},{"revision":"7a3450b859ce133cbce54fcb1b190b47","url":"index.html"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-512.svg"},{"revision":null,"url":"assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2"},{"revision":null,"url":"assets/input-ClhO__YM.js"},{"revision":null,"url":"assets/KaTeX_AMS-Regular-BQhdFMY1.woff2"},{"revision":null,"url":"assets/database-D4DIhgi-.js"},{"revision":null,"url":"assets/csv-parser--2WJNgS7.js"},{"revision":null,"url":"assets/x-DlFGzN8d.js"},{"revision":null,"url":"assets/audio-preview-VMboGrIH.js"},{"revision":null,"url":"assets/csv-preview-BEBJD4a_.js"},{"revision":null,"url":"assets/vendor-xterm-CU2c3f0A.js"},{"revision":null,"url":"assets/use-blob-url-BU9hYOj9.js"},{"revision":null,"url":"assets/sql-query-editor-JwymAmuK.js"},{"revision":null,"url":"assets/KaTeX_Main-Regular-B22Nviop.woff2"},{"revision":null,"url":"assets/gitGraph-HDMCJU4V-BhjTKsbg.js"},{"revision":null,"url":"assets/refresh-cw-LlbZDJpO.js"},{"revision":null,"url":"assets/diff-viewer-B3gAWXgA.js"},{"revision":null,"url":"assets/index-B0V_IYbX.css"},{"revision":null,"url":"assets/architecture-PBZL5I3N-XX6_EZsC.js"},{"revision":null,"url":"assets/react-GqWghJ-L.js"},{"revision":null,"url":"assets/markdown-renderer-t1ZBKbXZ.js"},{"revision":null,"url":"assets/settings-tab-b3AbZg6I.js"},{"revision":null,"url":"assets/vendor-ui-B-T_damt.js"},{"revision":null,"url":"assets/katex-CKoArbIw.js"},{"revision":null,"url":"assets/chat-tab-DfO2rHO8.js"},{"revision":null,"url":"assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2"},{"revision":null,"url":"assets/chevron-right-BzAdxJRG.js"},{"revision":null,"url":"assets/extension-store-3yZYn07W.js"},{"revision":null,"url":"assets/code-editor-BU7NX_SZ.js"},{"revision":null,"url":"assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2"},{"revision":null,"url":"assets/scroll-area-DW7L4Gnc.js"},{"revision":null,"url":"assets/vendor-markdown-0Mxgxy0L.js"},{"revision":null,"url":"assets/vendor-mermaid-BlWh9BJO.js"},{"revision":null,"url":"assets/dist-im4ynINo.js"},{"revision":null,"url":"assets/KaTeX_Main-Italic-NWA7e6Wa.woff2"},{"revision":null,"url":"assets/database-viewer-BV0Ebp0z.js"},{"revision":null,"url":"assets/table-Dq575bPF.js"},{"revision":null,"url":"assets/packet-RMMSAZCW-C7agXrtd.js"},{"revision":null,"url":"assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2"},{"revision":null,"url":"assets/api-client-CwbMRXYl.js"},{"revision":null,"url":"assets/dist-C5IgeqrV.js"},{"revision":null,"url":"assets/plus-51UQ45rf.js"},{"revision":null,"url":"assets/keybindings-store-CThBg3hS.js"},{"revision":null,"url":"assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2"},{"revision":null,"url":"assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2"},{"revision":null,"url":"assets/arrow-up-Dtrfv490.js"},{"revision":null,"url":"assets/use-monaco-theme-o7Ip-BDL.js"},{"revision":null,"url":"assets/radar-KQ55EAFF-DSn_ekR5.js"},{"revision":null,"url":"assets/rolldown-runtime-FhOqtrmT.js"},{"revision":null,"url":"assets/info-3K5VOQVL-CzgVqYTx.js"},{"revision":null,"url":"assets/KaTeX_Math-Italic-t53AETM-.woff2"},{"revision":null,"url":"assets/extension-webview-C8rdBYLl.js"},{"revision":null,"url":"assets/text-wrap-Cn6BNQfq.js"},{"revision":null,"url":"assets/sqlite-viewer-D0oWgepE.js"},{"revision":null,"url":"assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2"},{"revision":null,"url":"assets/KaTeX_Script-Regular-D3wIWfF6.woff2"},{"revision":null,"url":"assets/trash-2-CJYoLw7Q.js"},{"revision":null,"url":"assets/file-exclamation-point-BwzaQ50n.js"},{"revision":null,"url":"assets/project-store-IB6pAGQh.js"},{"revision":null,"url":"assets/utils-CTg5uAYR.js"},{"revision":null,"url":"assets/columns-2-4fQcE4PF.js"},{"revision":null,"url":"assets/KaTeX_Main-Bold-Cx986IdX.woff2"},{"revision":null,"url":"assets/image-preview-BEiYtg6_.js"},{"revision":null,"url":"assets/KaTeX_Size2-Regular-Dy4dx90m.woff2"},{"revision":null,"url":"assets/port-forwarding-tab-BZmfg410.js"},{"revision":null,"url":"assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2"},{"revision":null,"url":"assets/index-CBsOxcqb.js"},{"revision":null,"url":"assets/KaTeX_Size1-Regular-mCD8mA8B.woff2"},{"revision":null,"url":"assets/tab-store-B3M9hjho.js"},{"revision":null,"url":"assets/treemap-KZPCXAKY-C8puYVyN.js"},{"revision":null,"url":"assets/createLucideIcon-BjHrJDVb.js"},{"revision":null,"url":"assets/keybindings-store-BIQHClUy.js"},{"revision":null,"url":"assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2"},{"revision":null,"url":"assets/code-CuravVys.js"},{"revision":null,"url":"assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2"},{"revision":null,"url":"assets/sql-completion-provider-C3cq9j99.js"},{"revision":null,"url":"assets/postgres-viewer-CSTO0jc2.js"},{"revision":null,"url":"assets/esm-K1XIK4vc.js"},{"revision":null,"url":"assets/video-preview-BcMa4tim.js"},{"revision":null,"url":"assets/pdf-preview-CjfQxXE5.js"},{"revision":null,"url":"assets/settings-store-fDOEursg.js"},{"revision":null,"url":"assets/conflict-editor-943WUefe.js"},{"revision":null,"url":"assets/terminal-tab-WBPZXu12.js"},{"revision":null,"url":"assets/vendor-xterm-BrP-ENHg.css"},{"revision":null,"url":"assets/api-settings-ByUGHhTB.js"},{"revision":null,"url":"assets/ai-settings-section-L6XAmZEP.js"},{"revision":null,"url":"assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2"},{"revision":null,"url":"assets/lib-DQHnkzGy.js"},{"revision":null,"url":"assets/pie-UPGHQEXC-BRZ7alnf.js"},{"revision":"d0f94ce046cf8cf09605ee7664dac557","url":"monacoeditorwork/html.worker.bundle.js"},{"revision":"a424156a79b9c1b907db93aa3180585a","url":"monacoeditorwork/editor.worker.bundle.js"},{"revision":"b3a7f967560c9816492a1567b3f7f0dc","url":"monacoeditorwork/css.worker.bundle.js"},{"revision":"a5d8a1acfc29c2a4c882a54ffc93def3","url":"monacoeditorwork/json.worker.bundle.js"},{"revision":"948e060affb598c339be40d69e1f6f9c","url":"monacoeditorwork/ts.worker.bundle.js"},{"revision":"79c8870653c8f419f2e3323085e1f4be","url":"manifest.webmanifest"}]),self.addEventListener(`push`,e=>{e.waitUntil(self.clients.matchAll({type:`window`,includeUncontrolled:!0}).then(t=>{if(t.some(e=>e.visibilityState===`visible`))return;let n=e.data?.json()??{title:`PPM`,body:`Chat completed`};return self.registration.showNotification(n.title,{body:n.body,icon:`/icon-192.png`,badge:`/icon-192.png`,tag:`ppm-chat-done`,silent:!1,data:{url:self.location.origin}})}))}),self.addEventListener(`notificationclick`,e=>{e.notification.close(),e.waitUntil(self.clients.matchAll({type:`window`,includeUncontrolled:!0}).then(t=>{for(let e of t)if(e.url.includes(self.location.origin)&&`focus`in e)return e.focus();return self.clients.openWindow(e.notification.data?.url||`/`)}))});
1
+ try{self[`workbox:core:7.3.0`]&&_()}catch{}var e=(e,...t)=>{let n=e;return t.length>0&&(n+=` :: ${JSON.stringify(t)}`),n},t=class extends Error{constructor(t,n){let r=e(t,n);super(r),this.name=t,this.details=n}},n={googleAnalytics:`googleAnalytics`,precache:`precache-v2`,prefix:`workbox`,runtime:`runtime`,suffix:typeof registration<`u`?registration.scope:``},r=e=>[n.prefix,e,n.suffix].filter(e=>e&&e.length>0).join(`-`),i=e=>{for(let t of Object.keys(n))e(t)},a={updateDetails:e=>{i(t=>{typeof e[t]==`string`&&(n[t]=e[t])})},getGoogleAnalyticsName:e=>e||r(n.googleAnalytics),getPrecacheName:e=>e||r(n.precache),getPrefix:()=>n.prefix,getRuntimeName:e=>e||r(n.runtime),getSuffix:()=>n.suffix};function o(e,t){let n=t();return e.waitUntil(n),n}try{self[`workbox:precaching:7.3.0`]&&_()}catch{}var s=`__WB_REVISION__`;function c(e){if(!e)throw new t(`add-to-cache-list-unexpected-type`,{entry:e});if(typeof e==`string`){let t=new URL(e,location.href);return{cacheKey:t.href,url:t.href}}let{revision:n,url:r}=e;if(!r)throw new t(`add-to-cache-list-unexpected-type`,{entry:e});if(!n){let e=new URL(r,location.href);return{cacheKey:e.href,url:e.href}}let i=new URL(r,location.href),a=new URL(r,location.href);return i.searchParams.set(s,n),{cacheKey:i.href,url:a.href}}var l=class{constructor(){this.updatedURLs=[],this.notUpdatedURLs=[],this.handlerWillStart=async({request:e,state:t})=>{t&&(t.originalRequest=e)},this.cachedResponseWillBeUsed=async({event:e,state:t,cachedResponse:n})=>{if(e.type===`install`&&t&&t.originalRequest&&t.originalRequest instanceof Request){let e=t.originalRequest.url;n?this.notUpdatedURLs.push(e):this.updatedURLs.push(e)}return n}}},u=class{constructor({precacheController:e}){this.cacheKeyWillBeUsed=async({request:e,params:t})=>{let n=t?.cacheKey||this._precacheController.getCacheKeyForURL(e.url);return n?new Request(n,{headers:e.headers}):e},this._precacheController=e}},d;function f(){if(d===void 0){let e=new Response(``);if(`body`in e)try{new Response(e.body),d=!0}catch{d=!1}d=!1}return d}async function p(e,n){let r=null;if(e.url&&(r=new URL(e.url).origin),r!==self.location.origin)throw new t(`cross-origin-copy-response`,{origin:r});let i=e.clone(),a={headers:new Headers(i.headers),status:i.status,statusText:i.statusText},o=n?n(a):a,s=f()?i.body:await i.blob();return new Response(s,o)}var m=e=>new URL(String(e),location.href).href.replace(RegExp(`^${location.origin}`),``);function h(e,t){let n=new URL(e);for(let e of t)n.searchParams.delete(e);return n.href}async function g(e,t,n,r){let i=h(t.url,n);if(t.url===i)return e.match(t,r);let a=Object.assign(Object.assign({},r),{ignoreSearch:!0}),o=await e.keys(t,a);for(let t of o)if(i===h(t.url,n))return e.match(t,r)}var v=class{constructor(){this.promise=new Promise((e,t)=>{this.resolve=e,this.reject=t})}},y=new Set;async function b(){for(let e of y)await e()}function x(e){return new Promise(t=>setTimeout(t,e))}try{self[`workbox:strategies:7.3.0`]&&_()}catch{}function S(e){return typeof e==`string`?new Request(e):e}var C=class{constructor(e,t){this._cacheKeys={},Object.assign(this,t),this.event=t.event,this._strategy=e,this._handlerDeferred=new v,this._extendLifetimePromises=[],this._plugins=[...e.plugins],this._pluginStateMap=new Map;for(let e of this._plugins)this._pluginStateMap.set(e,{});this.event.waitUntil(this._handlerDeferred.promise)}async fetch(e){let{event:n}=this,r=S(e);if(r.mode===`navigate`&&n instanceof FetchEvent&&n.preloadResponse){let e=await n.preloadResponse;if(e)return e}let i=this.hasCallback(`fetchDidFail`)?r.clone():null;try{for(let e of this.iterateCallbacks(`requestWillFetch`))r=await e({request:r.clone(),event:n})}catch(e){if(e instanceof Error)throw new t(`plugin-error-request-will-fetch`,{thrownErrorMessage:e.message})}let a=r.clone();try{let e;e=await fetch(r,r.mode===`navigate`?void 0:this._strategy.fetchOptions);for(let t of this.iterateCallbacks(`fetchDidSucceed`))e=await t({event:n,request:a,response:e});return e}catch(e){throw i&&await this.runCallbacks(`fetchDidFail`,{error:e,event:n,originalRequest:i.clone(),request:a.clone()}),e}}async fetchAndCachePut(e){let t=await this.fetch(e),n=t.clone();return this.waitUntil(this.cachePut(e,n)),t}async cacheMatch(e){let t=S(e),n,{cacheName:r,matchOptions:i}=this._strategy,a=await this.getCacheKey(t,`read`),o=Object.assign(Object.assign({},i),{cacheName:r});n=await caches.match(a,o);for(let e of this.iterateCallbacks(`cachedResponseWillBeUsed`))n=await e({cacheName:r,matchOptions:i,cachedResponse:n,request:a,event:this.event})||void 0;return n}async cachePut(e,n){let r=S(e);await x(0);let i=await this.getCacheKey(r,`write`);if(!n)throw new t(`cache-put-with-no-response`,{url:m(i.url)});let a=await this._ensureResponseSafeToCache(n);if(!a)return!1;let{cacheName:o,matchOptions:s}=this._strategy,c=await self.caches.open(o),l=this.hasCallback(`cacheDidUpdate`),u=l?await g(c,i.clone(),[`__WB_REVISION__`],s):null;try{await c.put(i,l?a.clone():a)}catch(e){if(e instanceof Error)throw e.name===`QuotaExceededError`&&await b(),e}for(let e of this.iterateCallbacks(`cacheDidUpdate`))await e({cacheName:o,oldResponse:u,newResponse:a.clone(),request:i,event:this.event});return!0}async getCacheKey(e,t){let n=`${e.url} | ${t}`;if(!this._cacheKeys[n]){let r=e;for(let e of this.iterateCallbacks(`cacheKeyWillBeUsed`))r=S(await e({mode:t,request:r,event:this.event,params:this.params}));this._cacheKeys[n]=r}return this._cacheKeys[n]}hasCallback(e){for(let t of this._strategy.plugins)if(e in t)return!0;return!1}async runCallbacks(e,t){for(let n of this.iterateCallbacks(e))await n(t)}*iterateCallbacks(e){for(let t of this._strategy.plugins)if(typeof t[e]==`function`){let n=this._pluginStateMap.get(t);yield r=>{let i=Object.assign(Object.assign({},r),{state:n});return t[e](i)}}}waitUntil(e){return this._extendLifetimePromises.push(e),e}async doneWaiting(){for(;this._extendLifetimePromises.length;){let e=this._extendLifetimePromises.splice(0),t=(await Promise.allSettled(e)).find(e=>e.status===`rejected`);if(t)throw t.reason}}destroy(){this._handlerDeferred.resolve(null)}async _ensureResponseSafeToCache(e){let t=e,n=!1;for(let e of this.iterateCallbacks(`cacheWillUpdate`))if(t=await e({request:this.request,response:t,event:this.event})||void 0,n=!0,!t)break;return n||t&&t.status!==200&&(t=void 0),t}},w=class{constructor(e={}){this.cacheName=a.getRuntimeName(e.cacheName),this.plugins=e.plugins||[],this.fetchOptions=e.fetchOptions,this.matchOptions=e.matchOptions}handle(e){let[t]=this.handleAll(e);return t}handleAll(e){e instanceof FetchEvent&&(e={event:e,request:e.request});let t=e.event,n=typeof e.request==`string`?new Request(e.request):e.request,r=`params`in e?e.params:void 0,i=new C(this,{event:t,request:n,params:r}),a=this._getResponse(i,n,t);return[a,this._awaitComplete(a,i,n,t)]}async _getResponse(e,n,r){await e.runCallbacks(`handlerWillStart`,{event:r,request:n});let i;try{if(i=await this._handle(n,e),!i||i.type===`error`)throw new t(`no-response`,{url:n.url})}catch(t){if(t instanceof Error){for(let a of e.iterateCallbacks(`handlerDidError`))if(i=await a({error:t,event:r,request:n}),i)break}if(!i)throw t}for(let t of e.iterateCallbacks(`handlerWillRespond`))i=await t({event:r,request:n,response:i});return i}async _awaitComplete(e,t,n,r){let i,a;try{i=await e}catch{}try{await t.runCallbacks(`handlerDidRespond`,{event:r,request:n,response:i}),await t.doneWaiting()}catch(e){e instanceof Error&&(a=e)}if(await t.runCallbacks(`handlerDidComplete`,{event:r,request:n,response:i,error:a}),t.destroy(),a)throw a}},T=class e extends w{constructor(t={}){t.cacheName=a.getPrecacheName(t.cacheName),super(t),this._fallbackToNetwork=t.fallbackToNetwork!==!1,this.plugins.push(e.copyRedirectedCacheableResponsesPlugin)}async _handle(e,t){return await t.cacheMatch(e)||(t.event&&t.event.type===`install`?await this._handleInstall(e,t):await this._handleFetch(e,t))}async _handleFetch(e,n){let r,i=n.params||{};if(this._fallbackToNetwork){let t=i.integrity,a=e.integrity,o=!a||a===t;r=await n.fetch(new Request(e,{integrity:e.mode===`no-cors`?void 0:a||t})),t&&o&&e.mode!==`no-cors`&&(this._useDefaultCacheabilityPluginIfNeeded(),await n.cachePut(e,r.clone()))}else throw new t(`missing-precache-entry`,{cacheName:this.cacheName,url:e.url});return r}async _handleInstall(e,n){this._useDefaultCacheabilityPluginIfNeeded();let r=await n.fetch(e);if(!await n.cachePut(e,r.clone()))throw new t(`bad-precaching-response`,{url:e.url,status:r.status});return r}_useDefaultCacheabilityPluginIfNeeded(){let t=null,n=0;for(let[r,i]of this.plugins.entries())i!==e.copyRedirectedCacheableResponsesPlugin&&(i===e.defaultPrecacheCacheabilityPlugin&&(t=r),i.cacheWillUpdate&&n++);n===0?this.plugins.push(e.defaultPrecacheCacheabilityPlugin):n>1&&t!==null&&this.plugins.splice(t,1)}};T.defaultPrecacheCacheabilityPlugin={async cacheWillUpdate({response:e}){return!e||e.status>=400?null:e}},T.copyRedirectedCacheableResponsesPlugin={async cacheWillUpdate({response:e}){return e.redirected?await p(e):e}};var E=class{constructor({cacheName:e,plugins:t=[],fallbackToNetwork:n=!0}={}){this._urlsToCacheKeys=new Map,this._urlsToCacheModes=new Map,this._cacheKeysToIntegrities=new Map,this._strategy=new T({cacheName:a.getPrecacheName(e),plugins:[...t,new u({precacheController:this})],fallbackToNetwork:n}),this.install=this.install.bind(this),this.activate=this.activate.bind(this)}get strategy(){return this._strategy}precache(e){this.addToCacheList(e),this._installAndActiveListenersAdded||=(self.addEventListener(`install`,this.install),self.addEventListener(`activate`,this.activate),!0)}addToCacheList(e){let n=[];for(let r of e){typeof r==`string`?n.push(r):r&&r.revision===void 0&&n.push(r.url);let{cacheKey:e,url:i}=c(r),a=typeof r!=`string`&&r.revision?`reload`:`default`;if(this._urlsToCacheKeys.has(i)&&this._urlsToCacheKeys.get(i)!==e)throw new t(`add-to-cache-list-conflicting-entries`,{firstEntry:this._urlsToCacheKeys.get(i),secondEntry:e});if(typeof r!=`string`&&r.integrity){if(this._cacheKeysToIntegrities.has(e)&&this._cacheKeysToIntegrities.get(e)!==r.integrity)throw new t(`add-to-cache-list-conflicting-integrities`,{url:i});this._cacheKeysToIntegrities.set(e,r.integrity)}if(this._urlsToCacheKeys.set(i,e),this._urlsToCacheModes.set(i,a),n.length>0){let e=`Workbox is precaching URLs without revision info: ${n.join(`, `)}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(e)}}}install(e){return o(e,async()=>{let t=new l;this.strategy.plugins.push(t);for(let[t,n]of this._urlsToCacheKeys){let r=this._cacheKeysToIntegrities.get(n),i=this._urlsToCacheModes.get(t),a=new Request(t,{integrity:r,cache:i,credentials:`same-origin`});await Promise.all(this.strategy.handleAll({params:{cacheKey:n},request:a,event:e}))}let{updatedURLs:n,notUpdatedURLs:r}=t;return{updatedURLs:n,notUpdatedURLs:r}})}activate(e){return o(e,async()=>{let e=await self.caches.open(this.strategy.cacheName),t=await e.keys(),n=new Set(this._urlsToCacheKeys.values()),r=[];for(let i of t)n.has(i.url)||(await e.delete(i),r.push(i.url));return{deletedURLs:r}})}getURLsToCacheKeys(){return this._urlsToCacheKeys}getCachedURLs(){return[...this._urlsToCacheKeys.keys()]}getCacheKeyForURL(e){let t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForCacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){let t=e instanceof Request?e.url:e,n=this.getCacheKeyForURL(t);if(n)return(await self.caches.open(this.strategy.cacheName)).match(n)}createHandlerBoundToURL(e){let n=this.getCacheKeyForURL(e);if(!n)throw new t(`non-precached-url`,{url:e});return t=>(t.request=new Request(e),t.params=Object.assign({cacheKey:n},t.params),this.strategy.handle(t))}},D,O=()=>(D||=new E,D);try{self[`workbox:routing:7.3.0`]&&_()}catch{}var k=e=>e&&typeof e==`object`?e:{handle:e},A=class{constructor(e,t,n=`GET`){this.handler=k(t),this.match=e,this.method=n}setCatchHandler(e){this.catchHandler=k(e)}},j=class extends A{constructor(e,t,n){super(({url:t})=>{let n=e.exec(t.href);if(n&&!(t.origin!==location.origin&&n.index!==0))return n.slice(1)},t,n)}},M=class{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener(`fetch`,(e=>{let{request:t}=e,n=this.handleRequest({request:t,event:e});n&&e.respondWith(n)}))}addCacheListener(){self.addEventListener(`message`,(e=>{if(e.data&&e.data.type===`CACHE_URLS`){let{payload:t}=e.data,n=Promise.all(t.urlsToCache.map(t=>{typeof t==`string`&&(t=[t]);let n=new Request(...t);return this.handleRequest({request:n,event:e})}));e.waitUntil(n),e.ports&&e.ports[0]&&n.then(()=>e.ports[0].postMessage(!0))}}))}handleRequest({request:e,event:t}){let n=new URL(e.url,location.href);if(!n.protocol.startsWith(`http`))return;let r=n.origin===location.origin,{params:i,route:a}=this.findMatchingRoute({event:t,request:e,sameOrigin:r,url:n}),o=a&&a.handler,s=e.method;if(!o&&this._defaultHandlerMap.has(s)&&(o=this._defaultHandlerMap.get(s)),!o)return;let c;try{c=o.handle({url:n,request:e,event:t,params:i})}catch(e){c=Promise.reject(e)}let l=a&&a.catchHandler;return c instanceof Promise&&(this._catchHandler||l)&&(c=c.catch(async r=>{if(l)try{return await l.handle({url:n,request:e,event:t,params:i})}catch(e){e instanceof Error&&(r=e)}if(this._catchHandler)return this._catchHandler.handle({url:n,request:e,event:t});throw r})),c}findMatchingRoute({url:e,sameOrigin:t,request:n,event:r}){let i=this._routes.get(n.method)||[];for(let a of i){let i,o=a.match({url:e,sameOrigin:t,request:n,event:r});if(o)return i=o,(Array.isArray(i)&&i.length===0||o.constructor===Object&&Object.keys(o).length===0||typeof o==`boolean`)&&(i=void 0),{route:a,params:i}}return{}}setDefaultHandler(e,t=`GET`){this._defaultHandlerMap.set(t,k(e))}setCatchHandler(e){this._catchHandler=k(e)}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new t(`unregister-route-but-not-found-with-method`,{method:e.method});let n=this._routes.get(e.method).indexOf(e);if(n>-1)this._routes.get(e.method).splice(n,1);else throw new t(`unregister-route-route-not-registered`)}},N,P=()=>(N||(N=new M,N.addFetchListener(),N.addCacheListener()),N);function F(e,n,r){let i;if(typeof e==`string`){let t=new URL(e,location.href);i=new A(({url:e})=>e.href===t.href,n,r)}else if(e instanceof RegExp)i=new j(e,n,r);else if(typeof e==`function`)i=new A(e,n,r);else if(e instanceof A)i=e;else throw new t(`unsupported-route-type`,{moduleName:`workbox-routing`,funcName:`registerRoute`,paramName:`capture`});return P().registerRoute(i),i}function I(e,t=[]){for(let n of[...e.searchParams.keys()])t.some(e=>e.test(n))&&e.searchParams.delete(n);return e}function*L(e,{ignoreURLParametersMatching:t=[/^utm_/,/^fbclid$/],directoryIndex:n=`index.html`,cleanURLs:r=!0,urlManipulation:i}={}){let a=new URL(e,location.href);a.hash=``,yield a.href;let o=I(a,t);if(yield o.href,n&&o.pathname.endsWith(`/`)){let e=new URL(o.href);e.pathname+=n,yield e.href}if(r){let e=new URL(o.href);e.pathname+=`.html`,yield e.href}if(i){let e=i({url:a});for(let t of e)yield t.href}}var R=class extends A{constructor(e,t){super(({request:n})=>{let r=e.getURLsToCacheKeys();for(let i of L(n.url,t)){let t=r.get(i);if(t)return{cacheKey:t,integrity:e.getIntegrityForCacheKey(t)}}},e.strategy)}};function z(e){F(new R(O(),e))}function B(e){O().precache(e)}function V(e,t){B(e),z(t)}V([{"revision":"1872c500de691dce40960bb85481de07","url":"registerSW.js"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-192.svg"},{"revision":"2b34e7ac3ebf2d63aca84b8e289f8c7c","url":"index.html"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-512.svg"},{"revision":null,"url":"assets/index-BDRoldC9.js"},{"revision":null,"url":"assets/dist-D7KGU7Vl.js"},{"revision":null,"url":"assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2"},{"revision":null,"url":"assets/KaTeX_AMS-Regular-BQhdFMY1.woff2"},{"revision":null,"url":"assets/scroll-area-BEllam7_.js"},{"revision":null,"url":"assets/database-D4DIhgi-.js"},{"revision":null,"url":"assets/csv-parser--2WJNgS7.js"},{"revision":null,"url":"assets/code-editor-CMcDjype.js"},{"revision":null,"url":"assets/x-DlFGzN8d.js"},{"revision":null,"url":"assets/terminal-tab-UoDiWvzG.js"},{"revision":null,"url":"assets/pdf-preview-Cl95qWE_.js"},{"revision":null,"url":"assets/settings-tab-DyBeLmUh.js"},{"revision":null,"url":"assets/ai-settings-section-BHdBBJtS.js"},{"revision":null,"url":"assets/postgres-viewer-Do_w0Cji.js"},{"revision":null,"url":"assets/vendor-xterm-CU2c3f0A.js"},{"revision":null,"url":"assets/use-blob-url-BU9hYOj9.js"},{"revision":null,"url":"assets/markdown-renderer-DwqWhkri.js"},{"revision":null,"url":"assets/input-Dk49gO8E.js"},{"revision":null,"url":"assets/diff-viewer-oEyE9UwV.js"},{"revision":null,"url":"assets/sql-query-editor-JwymAmuK.js"},{"revision":null,"url":"assets/KaTeX_Main-Regular-B22Nviop.woff2"},{"revision":null,"url":"assets/gitGraph-HDMCJU4V-BhjTKsbg.js"},{"revision":null,"url":"assets/refresh-cw-LlbZDJpO.js"},{"revision":null,"url":"assets/image-preview-CY3sVd25.js"},{"revision":null,"url":"assets/architecture-PBZL5I3N-XX6_EZsC.js"},{"revision":null,"url":"assets/react-GqWghJ-L.js"},{"revision":null,"url":"assets/csv-preview-HMSavgBb.js"},{"revision":null,"url":"assets/katex-CKoArbIw.js"},{"revision":null,"url":"assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2"},{"revision":null,"url":"assets/chevron-right-BzAdxJRG.js"},{"revision":null,"url":"assets/extension-store-3yZYn07W.js"},{"revision":null,"url":"assets/extension-webview-CVqfQGjg.js"},{"revision":null,"url":"assets/conflict-editor-Br-ugFiK.js"},{"revision":null,"url":"assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2"},{"revision":null,"url":"assets/video-preview-3MbkDYcA.js"},{"revision":null,"url":"assets/vendor-markdown-0Mxgxy0L.js"},{"revision":null,"url":"assets/vendor-mermaid-BlWh9BJO.js"},{"revision":null,"url":"assets/dist-im4ynINo.js"},{"revision":null,"url":"assets/KaTeX_Main-Italic-NWA7e6Wa.woff2"},{"revision":null,"url":"assets/table-Dq575bPF.js"},{"revision":null,"url":"assets/packet-RMMSAZCW-C7agXrtd.js"},{"revision":null,"url":"assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2"},{"revision":null,"url":"assets/api-client-CwbMRXYl.js"},{"revision":null,"url":"assets/sqlite-viewer-oZkGJfW2.js"},{"revision":null,"url":"assets/plus-51UQ45rf.js"},{"revision":null,"url":"assets/keybindings-store-CThBg3hS.js"},{"revision":null,"url":"assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2"},{"revision":null,"url":"assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2"},{"revision":null,"url":"assets/arrow-up-Dtrfv490.js"},{"revision":null,"url":"assets/use-monaco-theme-o7Ip-BDL.js"},{"revision":null,"url":"assets/radar-KQ55EAFF-DSn_ekR5.js"},{"revision":null,"url":"assets/rolldown-runtime-FhOqtrmT.js"},{"revision":null,"url":"assets/info-3K5VOQVL-CzgVqYTx.js"},{"revision":null,"url":"assets/KaTeX_Math-Italic-t53AETM-.woff2"},{"revision":null,"url":"assets/text-wrap-Cn6BNQfq.js"},{"revision":null,"url":"assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2"},{"revision":null,"url":"assets/KaTeX_Script-Regular-D3wIWfF6.woff2"},{"revision":null,"url":"assets/trash-2-CJYoLw7Q.js"},{"revision":null,"url":"assets/file-exclamation-point-BwzaQ50n.js"},{"revision":null,"url":"assets/database-viewer-DxP0GmQK.js"},{"revision":null,"url":"assets/project-store-IB6pAGQh.js"},{"revision":null,"url":"assets/utils-CTg5uAYR.js"},{"revision":null,"url":"assets/columns-2-4fQcE4PF.js"},{"revision":null,"url":"assets/KaTeX_Main-Bold-Cx986IdX.woff2"},{"revision":null,"url":"assets/KaTeX_Size2-Regular-Dy4dx90m.woff2"},{"revision":null,"url":"assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2"},{"revision":null,"url":"assets/KaTeX_Size1-Regular-mCD8mA8B.woff2"},{"revision":null,"url":"assets/tab-store-B3M9hjho.js"},{"revision":null,"url":"assets/treemap-KZPCXAKY-C8puYVyN.js"},{"revision":null,"url":"assets/createLucideIcon-BjHrJDVb.js"},{"revision":null,"url":"assets/keybindings-store-BIQHClUy.js"},{"revision":null,"url":"assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2"},{"revision":null,"url":"assets/code-CuravVys.js"},{"revision":null,"url":"assets/index-CDSox8V2.css"},{"revision":null,"url":"assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2"},{"revision":null,"url":"assets/sql-completion-provider-C3cq9j99.js"},{"revision":null,"url":"assets/port-forwarding-tab-iJ3MAjXa.js"},{"revision":null,"url":"assets/vendor-ui-B-89Uj8i.js"},{"revision":null,"url":"assets/esm-K1XIK4vc.js"},{"revision":null,"url":"assets/settings-store-fDOEursg.js"},{"revision":null,"url":"assets/audio-preview-D4AxF10w.js"},{"revision":null,"url":"assets/vendor-xterm-BrP-ENHg.css"},{"revision":null,"url":"assets/api-settings-ByUGHhTB.js"},{"revision":null,"url":"assets/chat-tab-Bq2hmJ-B.js"},{"revision":null,"url":"assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2"},{"revision":null,"url":"assets/lib-DQHnkzGy.js"},{"revision":null,"url":"assets/pie-UPGHQEXC-BRZ7alnf.js"},{"revision":"d0f94ce046cf8cf09605ee7664dac557","url":"monacoeditorwork/html.worker.bundle.js"},{"revision":"a424156a79b9c1b907db93aa3180585a","url":"monacoeditorwork/editor.worker.bundle.js"},{"revision":"b3a7f967560c9816492a1567b3f7f0dc","url":"monacoeditorwork/css.worker.bundle.js"},{"revision":"a5d8a1acfc29c2a4c882a54ffc93def3","url":"monacoeditorwork/json.worker.bundle.js"},{"revision":"948e060affb598c339be40d69e1f6f9c","url":"monacoeditorwork/ts.worker.bundle.js"},{"revision":"79c8870653c8f419f2e3323085e1f4be","url":"manifest.webmanifest"}]),self.addEventListener(`push`,e=>{e.waitUntil(self.clients.matchAll({type:`window`,includeUncontrolled:!0}).then(t=>{if(t.some(e=>e.visibilityState===`visible`))return;let n=e.data?.json()??{title:`PPM`,body:`Chat completed`};return self.registration.showNotification(n.title,{body:n.body,icon:`/icon-192.png`,badge:`/icon-192.png`,tag:`ppm-chat-done`,silent:!1,data:{url:self.location.origin}})}))}),self.addEventListener(`notificationclick`,e=>{e.notification.close(),e.waitUntil(self.clients.matchAll({type:`window`,includeUncontrolled:!0}).then(t=>{for(let e of t)if(e.url.includes(self.location.origin)&&`focus`in e)return e.focus();return self.clients.openWindow(e.notification.data?.url||`/`)}))});
@@ -6,6 +6,62 @@ All notable changes to PPM are documented here. Format follows [Keep a Changelog
6
6
 
7
7
  ---
8
8
 
9
+ ## [Unreleased] — Session Tagging, Jira Debug Session Redesign, Frontend Memory Optimization, Git-Graph Enhancements
10
+
11
+ ### Added
12
+ - **Session Tagging** — Per-project tags for organizing chat sessions
13
+ - Database: schema v20 migration creates `project_tags` table with id, project_path, name, color, sort_order; adds `tag_id` FK to `session_metadata`
14
+ - Tag service: `tag.service.ts` with CRUD helpers (create, read, update, delete, bulk assign), session tag enrichment, tag counting
15
+ - API routes: `tag-routes.ts` with GET/POST/PATCH/DELETE endpoints for tag CRUD, default tag management
16
+ - Session tag assignment: PATCH/DELETE endpoints on chat routes, bulk assign endpoint for multi-session tagging
17
+ - Auto-tag new sessions: New sessions auto-assigned to project's default tag if configured
18
+ - UI: Color dots on session rows (8x8px circles showing tag color), tag filter chip bar above session list with count badges
19
+ - Filter bar: "All" chip plus one per tag, client-side filtering integrated with search (AND logic)
20
+ - Tag management: Settings panel (tag-settings-section.tsx) with create/edit/delete/reorder UI, drag-to-reorder on desktop, up/down arrows on mobile
21
+ - Context menu: Right-click session → "Set Tag" submenu with tag list, current tag has checkmark, "Remove tag" option
22
+ - Keyboard shortcuts: Keys 1-4 quickly assign top 4 project tags when history panel open and session focused
23
+ - Bulk operations: Select multiple sessions → floating action bar with tag assignment and bulk endpoint
24
+ - Responsive: Mobile-first design (44x44px touch targets), horizontal chip scroll, bottom sheet context menu, touch-friendly tag management
25
+
26
+ ### Technical Details
27
+ - **Database:**
28
+ - Migration v20: Creates `project_tags(id, project_path, name, color, sort_order, created_at)` with UNIQUE(project_path, name)
29
+ - Alters `session_metadata` to add `tag_id INTEGER REFERENCES project_tags(id) ON DELETE SET NULL`
30
+ - Alters `projects` to add `default_tag_id INTEGER REFERENCES project_tags(id) ON DELETE SET NULL`
31
+ - Seeds 4 default tags per project on migration and on new project creation
32
+ - **Files Created:**
33
+ - `src/services/tag.service.ts` — Tag CRUD helpers, session tag enrichment, bulk operations (~150 lines)
34
+ - `src/server/routes/tag-routes.ts` — Tag API endpoints (GET/POST/PATCH/DELETE project tags, default tag) (~100 lines)
35
+ - `src/web/components/settings/tag-settings-section.tsx` — Tag management UI with CRUD, reorder, default toggle (~170 lines)
36
+ - `src/web/components/chat/session-context-menu.tsx` — Extracted context menu content (optional, ~80 lines)
37
+ - `src/web/components/chat/session-bulk-actions.tsx` — Bulk action bar (optional, ~60 lines)
38
+ - **Files Modified:**
39
+ - `src/services/db.service.ts` — Schema v20 migration, bump CURRENT_SCHEMA_VERSION
40
+ - `src/services/project.service.ts` — Call seedDefaultTags() on project add
41
+ - `src/types/chat.ts` — Add ProjectTag interface, tag field to SessionInfo
42
+ - `src/server/routes/chat.ts` — Session tag endpoints (PATCH/DELETE single, PATCH bulk), tag enrichment in GET /sessions, auto-tag on POST /sessions
43
+ - `src/web/components/chat/chat-history-bar.tsx` — Tag filter chip bar, color dots per session, context menu wrapper, keyboard shortcuts, bulk select mode
44
+ - `src/web/components/chat/session-picker.tsx` — Color dots per session
45
+ - `src/web/components/chat/chat-welcome.tsx` — Color dots per recent session
46
+ - `src/web/components/settings/settings-tab.tsx` — Register TagSettingsSection
47
+ - **Type Changes:**
48
+ - New: `ProjectTag` = { id, projectPath, name, color, sortOrder }
49
+ - New: `ChatWsServerMessage` variants for tag updates (future)
50
+ - Updated: `SessionInfo` includes `tag?: { id, name, color } | null`
51
+ - **API Changes:**
52
+ - GET `/projects/:path/tags` → `{ tags: ProjectTag[], counts: Record<number, number> }`
53
+ - POST `/projects/:path/tags` → create tag
54
+ - PATCH `/projects/:path/tags/:id` → update tag (name, color, sortOrder)
55
+ - DELETE `/projects/:path/tags/:id` → delete tag
56
+ - PATCH `/projects/:path/default-tag` → set project default tag
57
+ - PATCH `/chat/sessions/:id/tag` → assign tag to session
58
+ - DELETE `/chat/sessions/:id/tag` → remove tag from session
59
+ - PATCH `/chat/sessions/bulk-tag` → bulk assign tag to multiple sessions (limit 100 per request)
60
+ - **Breaking Changes:** None (additive feature, backward compatible)
61
+ - **Test Coverage:** Integration tests for tag CRUD, session enrichment, API validation (100+ tests)
62
+
63
+ ---
64
+
9
65
  ## [0.11.11] — 2026-04-19
10
66
 
11
67
  ### Added
@@ -143,6 +143,14 @@ POST /api/upgrade/apply → Install new version, trigg
143
143
  GET /api/project/:name/workspace → Get saved workspace layout + metadata
144
144
  PUT /api/project/:name/workspace → Save workspace layout (layout JSON)
145
145
  GET /api/project/:name/chat/slash-items → List slash commands/skills (optional ?q=<query> for fuzzy search)
146
+ GET /api/projects/:path/tags → List project tags with session counts
147
+ POST /api/projects/:path/tags → Create tag
148
+ PATCH /api/projects/:path/tags/:id → Update tag (name, color, sortOrder)
149
+ DELETE /api/projects/:path/tags/:id → Delete tag
150
+ PATCH /api/projects/:path/default-tag → Set project default tag
151
+ PATCH /api/project/:name/chat/sessions/:id/tag → Assign tag to session
152
+ DELETE /api/project/:name/chat/sessions/:id/tag → Remove tag from session
153
+ PATCH /api/project/:name/chat/sessions/bulk-tag → Bulk assign tag to multiple sessions
146
154
  WS /ws/project/:name/chat/:sessionId → Chat streaming
147
155
  WS /ws/project/:name/terminal/:id → Terminal I/O
148
156
  ```
@@ -214,8 +222,9 @@ Tab IDs are deterministic: `{type}:{identifier}` (e.g., `editor:src/index.ts`, `
214
222
  | **ClawBotFormatterService** | LEGACY formatter | (deprecated v0.9.11) |
215
223
  | **ClawBotStreamerService** | LEGACY streamer | (deprecated v0.9.11) |
216
224
  | **BashOutputSpy** | Monitor bash tool output in real-time via /proc/PID/fd (Linux/WSL2) or lsof (macOS) | startSpy, stopSpy, stopAllForSession |
225
+ | **TagService** | Session tagging CRUD, bulk operations, tag-session enrichment | seedDefaultTags, getTagsByProject, createTag, updateTag, deleteTag, setSessionTag, bulkSetSessionTag, getSessionTags, getTagSessionCounts |
217
226
 
218
- **Key Files:** `src/services/*.service.ts`, `src/services/ppmbot/*.ts`, `src/services/bash-output-spy.ts`, `src/cli/commands/bot-cmd.ts`
227
+ **Key Files:** `src/services/*.service.ts`, `src/services/tag.service.ts`, `src/services/ppmbot/*.ts`, `src/services/bash-output-spy.ts`, `src/cli/commands/bot-cmd.ts`
219
228
 
220
229
  ---
221
230
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.11.17",
3
+ "version": "0.12.0",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "author": "hienlh",
6
6
  "license": "MIT",
@@ -10,6 +10,7 @@ import { upsertSlashRecent, getSlashRecents } from "../../services/db.service.ts
10
10
  import { getCachedUsage, refreshUsageNow } from "../../services/claude-usage.service.ts";
11
11
  import { getSessionLog } from "../../services/session-log.service.ts";
12
12
  import { getSessionProjectPath, setSessionMetadata, setSessionTitle, getPinnedSessionIds, pinSession, unpinSession, deleteSessionMapping, deleteSessionMetadata, deleteSessionTitle } from "../../services/db.service.ts";
13
+ import { setSessionTag, bulkSetSessionTag, getTagById, getSessionTags, getProjectDefaultTagId } from "../../services/tag.service.ts";
13
14
  import { ok, err } from "../../types/api.ts";
14
15
 
15
16
  type Env = { Variables: { projectPath: string; projectName: string } };
@@ -102,6 +103,8 @@ chatRoutes.get("/sessions", async (c) => {
102
103
  try {
103
104
  const projectPath = c.get("projectPath");
104
105
  const providerId = c.req.query("providerId");
106
+ const tagIdParam = c.req.query("tag_id");
107
+ const filterTagId = tagIdParam ? parseInt(tagIdParam, 10) : null;
105
108
  const limit = Math.min(parseInt(c.req.query("limit") ?? "50", 10) || 50, 200);
106
109
  const offset = parseInt(c.req.query("offset") ?? "0", 10) || 0;
107
110
 
@@ -129,7 +132,8 @@ chatRoutes.get("/sessions", async (c) => {
129
132
  const merged = [...pinnedSessions, ...sessions];
130
133
  const seen = new Set<string>();
131
134
  const deduped = merged.filter((s) => { if (seen.has(s.id)) return false; seen.add(s.id); return true; });
132
- const enriched = deduped.map((s) => ({ ...s, pinned: pinnedIds.has(s.id) }));
135
+ const tagMap = getSessionTags(deduped.map((s) => s.id));
136
+ const enriched = deduped.map((s) => ({ ...s, pinned: pinnedIds.has(s.id), tag: tagMap[s.id] ?? null }));
133
137
 
134
138
  // Sort: pinned first, then by createdAt desc
135
139
  enriched.sort((a, b) => {
@@ -138,8 +142,10 @@ chatRoutes.get("/sessions", async (c) => {
138
142
  return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
139
143
  });
140
144
 
145
+ // Server-side tag filter
146
+ const filtered = filterTagId !== null ? enriched.filter((s) => s.tag?.id === filterTagId) : enriched;
141
147
  const hasMore = sessions.length >= limit;
142
- return c.json(ok({ sessions: enriched, hasMore }));
148
+ return c.json(ok({ sessions: filtered, hasMore }));
143
149
  } catch (e) {
144
150
  return c.json(err((e as Error).message), 500);
145
151
  }
@@ -168,6 +174,9 @@ chatRoutes.post("/sessions", async (c) => {
168
174
  projectPath,
169
175
  title: body.title,
170
176
  });
177
+ // Auto-assign default tag if project has one
178
+ const defaultTagId = getProjectDefaultTagId(projectPath);
179
+ if (defaultTagId) setSessionTag(session.id, defaultTagId, projectPath);
171
180
  return c.json(ok(session), 201);
172
181
  } catch (e) {
173
182
  return c.json(err((e as Error).message), 400);
@@ -183,6 +192,7 @@ chatRoutes.delete("/sessions/:id", async (c) => {
183
192
  await chatService.deleteSession(providerId, id);
184
193
  // Shared DB cleanup
185
194
  deleteSessionMapping(id); // legacy cleanup
195
+ setSessionTag(id, null, c.get("projectPath"));
186
196
  deleteSessionMetadata(id);
187
197
  deleteSessionTitle(id);
188
198
  unpinSession(id);
@@ -235,6 +245,51 @@ chatRoutes.delete("/sessions/:id/pin", (c) => {
235
245
  }
236
246
  });
237
247
 
248
+ /** PATCH /chat/sessions/bulk-tag — assign tag to multiple sessions (MUST be before /sessions/:id) */
249
+ chatRoutes.patch("/sessions/bulk-tag", async (c) => {
250
+ try {
251
+ const projectPath = c.get("projectPath");
252
+ const { sessionIds, tagId } = await c.req.json<{ sessionIds: string[]; tagId: number | null }>();
253
+ if (!Array.isArray(sessionIds) || sessionIds.length === 0) return c.json(err("sessionIds array required"), 400);
254
+ if (sessionIds.length > 100) return c.json(err("Max 100 sessions per bulk operation"), 400);
255
+ if (tagId !== null) {
256
+ const tag = getTagById(tagId);
257
+ if (!tag || tag.projectPath !== projectPath) return c.json(err("Tag not found"), 404);
258
+ }
259
+ bulkSetSessionTag(sessionIds, tagId, projectPath);
260
+ return c.json(ok({ updated: sessionIds.length }));
261
+ } catch (e) {
262
+ return c.json(err((e as Error).message), 500);
263
+ }
264
+ });
265
+
266
+ /** PATCH /chat/sessions/:id/tag — assign a tag to a session */
267
+ chatRoutes.patch("/sessions/:id/tag", async (c) => {
268
+ try {
269
+ const id = c.req.param("id");
270
+ const projectPath = c.get("projectPath");
271
+ const { tagId } = await c.req.json<{ tagId: number }>();
272
+ if (tagId == null || typeof tagId !== "number") return c.json(err("tagId is required"), 400);
273
+ const tag = getTagById(tagId);
274
+ if (!tag || tag.projectPath !== projectPath) return c.json(err("Tag not found"), 404);
275
+ setSessionTag(id, tagId, projectPath);
276
+ return c.json(ok({ id, tagId }));
277
+ } catch (e) {
278
+ return c.json(err((e as Error).message), 500);
279
+ }
280
+ });
281
+
282
+ /** DELETE /chat/sessions/:id/tag — remove tag from a session */
283
+ chatRoutes.delete("/sessions/:id/tag", (c) => {
284
+ try {
285
+ const id = c.req.param("id");
286
+ setSessionTag(id, null, c.get("projectPath"));
287
+ return c.json(ok({ id, tagId: null }));
288
+ } catch (e) {
289
+ return c.json(err((e as Error).message), 500);
290
+ }
291
+ });
292
+
238
293
  /** POST /chat/sessions/:id/fork — fork session into a new one (for rewind/branch) */
239
294
  chatRoutes.post("/sessions/:id/fork", async (c) => {
240
295
  try {
@@ -1,6 +1,7 @@
1
1
  import { Hono } from "hono";
2
2
  import { resolveProjectPath } from "../helpers/resolve-project.ts";
3
3
  import { chatRoutes } from "./chat.ts";
4
+ import { tagRoutes } from "./tag-routes.ts";
4
5
  import { gitRoutes } from "./git.ts";
5
6
  import { fileRoutes } from "./files.ts";
6
7
  import { sqliteRoutes } from "./sqlite.ts";
@@ -26,6 +27,7 @@ projectScopedRouter.use("*", async (c, next) => {
26
27
  });
27
28
 
28
29
  projectScopedRouter.route("/chat", chatRoutes);
30
+ projectScopedRouter.route("/tags", tagRoutes);
29
31
  projectScopedRouter.route("/git", gitRoutes);
30
32
  projectScopedRouter.route("/files", fileRoutes);
31
33
  projectScopedRouter.route("/sqlite", sqliteRoutes);
@@ -0,0 +1,93 @@
1
+ import { Hono } from "hono";
2
+ import { ok, err } from "../../types/api.ts";
3
+ import {
4
+ getTagsByProject, createTag, updateTag, deleteTag, getTagById,
5
+ setProjectDefaultTag, getProjectDefaultTagId, getTagSessionCounts,
6
+ seedDefaultTags,
7
+ } from "../../services/tag.service.ts";
8
+
9
+ type Env = { Variables: { projectPath: string; projectName: string } };
10
+
11
+ export const tagRoutes = new Hono<Env>();
12
+
13
+ /** GET /tags — list all tags for the project with session counts */
14
+ tagRoutes.get("/", (c) => {
15
+ try {
16
+ const projectPath = c.get("projectPath");
17
+ const tags = getTagsByProject(projectPath);
18
+ const counts = getTagSessionCounts(projectPath);
19
+ const defaultTagId = getProjectDefaultTagId(projectPath);
20
+ return c.json(ok({ tags, counts, defaultTagId }));
21
+ } catch (e) {
22
+ return c.json(err((e as Error).message), 500);
23
+ }
24
+ });
25
+
26
+ /** POST /tags — create a new tag */
27
+ tagRoutes.post("/", async (c) => {
28
+ try {
29
+ const projectPath = c.get("projectPath");
30
+ const { name, color } = await c.req.json<{ name: string; color: string }>();
31
+ if (!name?.trim()) return c.json(err("name is required"), 400);
32
+ if (!color?.trim()) return c.json(err("color is required"), 400);
33
+ const tag = createTag(projectPath, name.trim(), color.trim());
34
+ return c.json(ok(tag), 201);
35
+ } catch (e) {
36
+ return c.json(err((e as Error).message), 400);
37
+ }
38
+ });
39
+
40
+ /** PATCH /default-tag — set the project's default tag for new sessions (MUST be before /:id) */
41
+ tagRoutes.patch("/default-tag", async (c) => {
42
+ try {
43
+ const projectPath = c.get("projectPath");
44
+ const { tagId } = await c.req.json<{ tagId: number | null }>();
45
+ if (tagId !== null) {
46
+ const tag = getTagById(tagId);
47
+ if (!tag || tag.projectPath !== projectPath) return c.json(err("Tag not found"), 404);
48
+ }
49
+ setProjectDefaultTag(projectPath, tagId);
50
+ return c.json(ok({ defaultTagId: tagId }));
51
+ } catch (e) {
52
+ return c.json(err((e as Error).message), 500);
53
+ }
54
+ });
55
+
56
+ /** POST /reset — re-seed default tags (MUST be before /:id) */
57
+ tagRoutes.post("/reset", (c) => {
58
+ try {
59
+ seedDefaultTags(c.get("projectPath"));
60
+ return c.json(ok({ reset: true }));
61
+ } catch (e) {
62
+ return c.json(err((e as Error).message), 500);
63
+ }
64
+ });
65
+
66
+ /** PATCH /tags/:id — update a tag (after literal routes) */
67
+ tagRoutes.patch("/:id", async (c) => {
68
+ try {
69
+ const id = parseInt(c.req.param("id"), 10);
70
+ const tag = getTagById(id);
71
+ if (!tag) return c.json(err("Tag not found"), 404);
72
+ if (tag.projectPath !== c.get("projectPath")) return c.json(err("Tag does not belong to this project"), 403);
73
+ const body = await c.req.json<{ name?: string; color?: string; sortOrder?: number }>();
74
+ updateTag(id, body);
75
+ return c.json(ok({ updated: true }));
76
+ } catch (e) {
77
+ return c.json(err((e as Error).message), 500);
78
+ }
79
+ });
80
+
81
+ /** DELETE /tags/:id — delete a tag (after literal routes) */
82
+ tagRoutes.delete("/:id", (c) => {
83
+ try {
84
+ const id = parseInt(c.req.param("id"), 10);
85
+ const tag = getTagById(id);
86
+ if (!tag) return c.json(err("Tag not found"), 404);
87
+ if (tag.projectPath !== c.get("projectPath")) return c.json(err("Tag does not belong to this project"), 403);
88
+ deleteTag(id);
89
+ return c.json(ok({ deleted: true }));
90
+ } catch (e) {
91
+ return c.json(err((e as Error).message), 500);
92
+ }
93
+ });
@@ -3,7 +3,7 @@ import { resolve } from "node:path";
3
3
  import { mkdirSync, existsSync } from "node:fs";
4
4
  import { encrypt, decrypt } from "../lib/account-crypto.ts";
5
5
  import { getPpmDir } from "./ppm-dir.ts";
6
- const CURRENT_SCHEMA_VERSION = 18;
6
+ const CURRENT_SCHEMA_VERSION = 20;
7
7
 
8
8
  let db: Database | null = null;
9
9
  let dbProfile: string | null = null;
@@ -555,6 +555,40 @@ function runMigrations(database: Database): void {
555
555
  PRAGMA user_version = 19;
556
556
  `);
557
557
  }
558
+
559
+ if (current < 20) {
560
+ database.exec(`
561
+ CREATE TABLE IF NOT EXISTS project_tags (
562
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
563
+ project_path TEXT NOT NULL,
564
+ name TEXT NOT NULL,
565
+ color TEXT NOT NULL,
566
+ sort_order INTEGER NOT NULL DEFAULT 0,
567
+ created_at TEXT DEFAULT (datetime('now')),
568
+ UNIQUE(project_path, name)
569
+ );
570
+ CREATE INDEX IF NOT EXISTS idx_project_tags_path ON project_tags(project_path);
571
+ `);
572
+ // ALTER columns wrapped individually for idempotence
573
+ try { database.exec("ALTER TABLE session_metadata ADD COLUMN tag_id INTEGER REFERENCES project_tags(id) ON DELETE SET NULL"); } catch { /* column exists */ }
574
+ try { database.exec("ALTER TABLE projects ADD COLUMN default_tag_id INTEGER REFERENCES project_tags(id) ON DELETE SET NULL"); } catch { /* column exists */ }
575
+ // Seed default tags for all existing projects
576
+ const projects = database.query("SELECT path FROM projects").all() as { path: string }[];
577
+ const defaultTags = [
578
+ { name: "Todo", color: "#22c55e", sort: 0 },
579
+ { name: "In Progress", color: "#3b82f6", sort: 1 },
580
+ { name: "Review", color: "#f59e0b", sort: 2 },
581
+ { name: "Done", color: "#8b5cf6", sort: 3 },
582
+ ];
583
+ for (const p of projects) {
584
+ for (const t of defaultTags) {
585
+ database.query(
586
+ "INSERT OR IGNORE INTO project_tags (project_path, name, color, sort_order) VALUES (?, ?, ?, ?)",
587
+ ).run(p.path, t.name, t.color, t.sort);
588
+ }
589
+ }
590
+ database.exec("PRAGMA user_version = 20");
591
+ }
558
592
  }
559
593
 
560
594
  // ---------------------------------------------------------------------------
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readdirSync, statSync } from "node:fs";
2
2
  import { resolve, basename, join } from "node:path";
3
3
  import { configService } from "./config.service.ts";
4
+ import { seedDefaultTags } from "./tag.service.ts";
4
5
  import type { ProjectConfig } from "../types/config.ts";
5
6
  import type { ProjectInfo } from "../types/project.ts";
6
7
 
@@ -38,6 +39,7 @@ class ProjectService {
38
39
  const entry: ProjectConfig = { path: abs, name: projectName };
39
40
  configService.set("projects", [...projects, entry]);
40
41
  configService.save();
42
+ try { seedDefaultTags(abs); } catch { /* non-critical */ }
41
43
  return entry;
42
44
  }
43
45
 
@@ -806,12 +806,17 @@ export async function runSupervisor(opts: {
806
806
  log("ERROR", `Unhandled rejection: ${reason}`);
807
807
  });
808
808
 
809
- // Full write to clear any stale data from previous runs (different port, dead PIDs, etc.)
809
+ // Full write to clear stale data but preserve tunnel info during self-replace upgrade
810
+ // so the new supervisor can adopt the existing tunnel and keep the domain.
810
811
  writeFileSync(PID_FILE(), String(process.pid));
812
+ const prevStatus = readStatus();
813
+ const isUpgrade = prevStatus.state === "upgrading";
811
814
  writeStatus({
812
815
  supervisorPid: process.pid, port: opts.port, host: opts.host, availableVersion: null,
813
816
  state: "running", pausedAt: null, pauseReason: null, lastCrashError: null,
814
- pid: null, tunnelPid: null, shareUrl: null,
817
+ pid: null,
818
+ tunnelPid: isUpgrade ? (prevStatus.tunnelPid ?? null) : null,
819
+ shareUrl: isUpgrade ? (prevStatus.shareUrl ?? null) : null,
815
820
  });
816
821
 
817
822
  // Build __serve__ args
@@ -0,0 +1,114 @@
1
+ import { getDb } from "./db.service.ts";
2
+ import type { ProjectTag } from "../types/chat.ts";
3
+
4
+ const DEFAULT_TAGS = [
5
+ { name: "Todo", color: "#22c55e", sort: 0 },
6
+ { name: "In Progress", color: "#3b82f6", sort: 1 },
7
+ { name: "Review", color: "#f59e0b", sort: 2 },
8
+ { name: "Done", color: "#8b5cf6", sort: 3 },
9
+ ];
10
+
11
+ /** Seed default tags for a project (idempotent via INSERT OR IGNORE) */
12
+ export function seedDefaultTags(projectPath: string): void {
13
+ for (const t of DEFAULT_TAGS) {
14
+ getDb().query(
15
+ "INSERT OR IGNORE INTO project_tags (project_path, name, color, sort_order) VALUES (?, ?, ?, ?)",
16
+ ).run(projectPath, t.name, t.color, t.sort);
17
+ }
18
+ }
19
+
20
+ export function getTagsByProject(projectPath: string): ProjectTag[] {
21
+ return getDb().query(
22
+ "SELECT id, project_path AS projectPath, name, color, sort_order AS sortOrder FROM project_tags WHERE project_path = ? ORDER BY sort_order, id",
23
+ ).all(projectPath) as ProjectTag[];
24
+ }
25
+
26
+ export function getTagById(id: number): ProjectTag | null {
27
+ return getDb().query(
28
+ "SELECT id, project_path AS projectPath, name, color, sort_order AS sortOrder FROM project_tags WHERE id = ?",
29
+ ).get(id) as ProjectTag | null;
30
+ }
31
+
32
+ export function createTag(projectPath: string, name: string, color: string): ProjectTag {
33
+ const maxOrder = (getDb().query(
34
+ "SELECT COALESCE(MAX(sort_order), -1) AS m FROM project_tags WHERE project_path = ?",
35
+ ).get(projectPath) as { m: number }).m;
36
+ getDb().query(
37
+ "INSERT INTO project_tags (project_path, name, color, sort_order) VALUES (?, ?, ?, ?)",
38
+ ).run(projectPath, name, color, maxOrder + 1);
39
+ const row = getDb().query(
40
+ "SELECT id, project_path AS projectPath, name, color, sort_order AS sortOrder FROM project_tags WHERE project_path = ? AND name = ?",
41
+ ).get(projectPath, name) as ProjectTag;
42
+ return row;
43
+ }
44
+
45
+ export function updateTag(id: number, updates: { name?: string; color?: string; sortOrder?: number }): void {
46
+ const parts: string[] = [];
47
+ const values: (string | number)[] = [];
48
+ if (updates.name !== undefined) { parts.push("name = ?"); values.push(updates.name); }
49
+ if (updates.color !== undefined) { parts.push("color = ?"); values.push(updates.color); }
50
+ if (updates.sortOrder !== undefined) { parts.push("sort_order = ?"); values.push(updates.sortOrder); }
51
+ if (parts.length === 0) return;
52
+ values.push(id);
53
+ getDb().query(`UPDATE project_tags SET ${parts.join(", ")} WHERE id = ?`).run(...values);
54
+ }
55
+
56
+ export function deleteTag(id: number): void {
57
+ getDb().query("DELETE FROM project_tags WHERE id = ?").run(id);
58
+ }
59
+
60
+ /** Bulk-fetch tag info for a list of session IDs (JOIN session_metadata + project_tags) */
61
+ export function getSessionTags(
62
+ sessionIds: string[],
63
+ ): Record<string, { id: number; name: string; color: string }> {
64
+ if (sessionIds.length === 0) return {};
65
+ const placeholders = sessionIds.map(() => "?").join(", ");
66
+ const rows = getDb().query(
67
+ `SELECT sm.session_id, pt.id, pt.name, pt.color
68
+ FROM session_metadata sm
69
+ JOIN project_tags pt ON sm.tag_id = pt.id
70
+ WHERE sm.session_id IN (${placeholders})`,
71
+ ).all(...sessionIds) as { session_id: string; id: number; name: string; color: string }[];
72
+ const result: Record<string, { id: number; name: string; color: string }> = {};
73
+ for (const r of rows) result[r.session_id] = { id: r.id, name: r.name, color: r.color };
74
+ return result;
75
+ }
76
+
77
+ export function setSessionTag(sessionId: string, tagId: number | null, projectPath?: string): void {
78
+ if (projectPath) {
79
+ // UPSERT: create session_metadata row if missing (e.g. sessions discovered from JSONL)
80
+ getDb().query(
81
+ `INSERT INTO session_metadata (session_id, tag_id, project_path)
82
+ VALUES (?, ?, ?)
83
+ ON CONFLICT(session_id) DO UPDATE SET tag_id = excluded.tag_id,
84
+ project_path = COALESCE(session_metadata.project_path, excluded.project_path)`,
85
+ ).run(sessionId, tagId, projectPath);
86
+ } else {
87
+ getDb().query("UPDATE session_metadata SET tag_id = ? WHERE session_id = ?").run(tagId, sessionId);
88
+ }
89
+ }
90
+
91
+ export function bulkSetSessionTag(sessionIds: string[], tagId: number | null, projectPath?: string): void {
92
+ for (const id of sessionIds) setSessionTag(id, tagId, projectPath);
93
+ }
94
+
95
+ export function getTagSessionCounts(projectPath: string): Record<number, number> {
96
+ const rows = getDb().query(
97
+ `SELECT sm.tag_id, COUNT(*) AS cnt
98
+ FROM session_metadata sm
99
+ WHERE sm.tag_id IS NOT NULL AND sm.project_path = ?
100
+ GROUP BY sm.tag_id`,
101
+ ).all(projectPath) as { tag_id: number; cnt: number }[];
102
+ const result: Record<number, number> = {};
103
+ for (const r of rows) result[r.tag_id] = r.cnt;
104
+ return result;
105
+ }
106
+
107
+ export function setProjectDefaultTag(projectPath: string, tagId: number | null): void {
108
+ getDb().query("UPDATE projects SET default_tag_id = ? WHERE path = ?").run(tagId, projectPath);
109
+ }
110
+
111
+ export function getProjectDefaultTagId(projectPath: string): number | null {
112
+ const row = getDb().query("SELECT default_tag_id FROM projects WHERE path = ?").get(projectPath) as { default_tag_id: number | null } | null;
113
+ return row?.default_tag_id ?? null;
114
+ }