@hienlh/ppm 0.6.7 → 0.7.1

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 (52) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +86 -313
  3. package/dist/web/assets/chat-tab-CbNbBMGw.js +7 -0
  4. package/dist/web/assets/{code-editor-BUg7alP6.js → code-editor-D6OuzcC-.js} +1 -1
  5. package/dist/web/assets/{database-viewer-CAgZOkZc.js → database-viewer-BxUpM_uA.js} +1 -1
  6. package/dist/web/assets/{diff-viewer-DVvY1aFb.js → diff-viewer-DAhrHpNM.js} +1 -1
  7. package/dist/web/assets/{dist-Jb3Tnkpc.js → dist-CNRrBoQi.js} +14 -14
  8. package/dist/web/assets/git-graph-BpTt5iOd.js +1 -0
  9. package/dist/web/assets/index-BU_07_oW.js +29 -0
  10. package/dist/web/assets/index-CBQhXXeV.css +2 -0
  11. package/dist/web/assets/keybindings-store-C0m8_V9X.js +1 -0
  12. package/dist/web/assets/{markdown-renderer-z99RjIxZ.js → markdown-renderer-CvGYO9sH.js} +2 -2
  13. package/dist/web/assets/postgres-viewer-BL99auSm.js +1 -0
  14. package/dist/web/assets/{settings-tab-BnDkeQWk.js → settings-tab-Bwsxb41F.js} +1 -1
  15. package/dist/web/assets/{sqlite-viewer-EwHWc37J.js → sqlite-viewer-DfgaCbWT.js} +1 -1
  16. package/dist/web/assets/{terminal-tab-CTN18lb6.js → terminal-tab-D27e4ZTD.js} +2 -2
  17. package/dist/web/index.html +4 -3
  18. package/dist/web/sw.js +1 -1
  19. package/package.json +1 -1
  20. package/src/lib/network-utils.ts +12 -0
  21. package/src/providers/claude-agent-sdk.ts +61 -3
  22. package/src/server/routes/chat.ts +5 -1
  23. package/src/server/routes/settings.ts +52 -0
  24. package/src/server/routes/tunnel.ts +1 -12
  25. package/src/server/ws/chat.ts +42 -12
  26. package/src/services/config.service.ts +1 -1
  27. package/src/services/notification.service.ts +42 -0
  28. package/src/services/telegram-notification.service.ts +106 -0
  29. package/src/types/config.ts +6 -0
  30. package/src/web/app.tsx +40 -1
  31. package/src/web/components/layout/draggable-tab.tsx +10 -2
  32. package/src/web/components/layout/mobile-nav.tsx +42 -3
  33. package/src/web/components/layout/project-bar.tsx +16 -8
  34. package/src/web/components/layout/tab-bar.tsx +55 -4
  35. package/src/web/components/settings/settings-tab.tsx +135 -94
  36. package/src/web/components/settings/telegram-settings-section.tsx +113 -0
  37. package/src/web/components/ui/accordion.tsx +64 -0
  38. package/src/web/hooks/use-chat.ts +29 -0
  39. package/src/web/hooks/use-notification-badge.ts +20 -0
  40. package/src/web/hooks/use-tab-overflow.ts +91 -0
  41. package/src/web/hooks/use-url-sync.ts +5 -2
  42. package/src/web/index.html +1 -0
  43. package/src/web/lib/favicon.ts +21 -0
  44. package/src/web/lib/notification-sounds.ts +61 -0
  45. package/src/web/stores/notification-store.ts +83 -0
  46. package/dist/web/assets/chat-tab-CWhxhPKH.js +0 -7
  47. package/dist/web/assets/git-graph-xD6TLRVv.js +0 -1
  48. package/dist/web/assets/index-CigdXBuQ.css +0 -2
  49. package/dist/web/assets/index-DBdw8tN_.js +0 -22
  50. package/dist/web/assets/keybindings-store-kHLASnRb.js +0 -1
  51. package/dist/web/assets/postgres-viewer-CaMySHpD.js +0 -1
  52. /package/dist/web/assets/{tab-store-DIyJSjtr.js → tab-store-Bm1Hw8OR.js} +0 -0
package/dist/web/sw.js CHANGED
@@ -1 +1 @@
1
- try{self[`workbox:core:7.3.0`]&&_()}catch{}var e=(e,...t)=>{let n=e;return t.length>0&&(n+=` :: ${JSON.stringify(t)}`),n},t=class extends Error{constructor(t,n){let r=e(t,n);super(r),this.name=t,this.details=n}},n={googleAnalytics:`googleAnalytics`,precache:`precache-v2`,prefix:`workbox`,runtime:`runtime`,suffix:typeof registration<`u`?registration.scope:``},r=e=>[n.prefix,e,n.suffix].filter(e=>e&&e.length>0).join(`-`),i=e=>{for(let t of Object.keys(n))e(t)},a={updateDetails:e=>{i(t=>{typeof e[t]==`string`&&(n[t]=e[t])})},getGoogleAnalyticsName:e=>e||r(n.googleAnalytics),getPrecacheName:e=>e||r(n.precache),getPrefix:()=>n.prefix,getRuntimeName:e=>e||r(n.runtime),getSuffix:()=>n.suffix};function o(e,t){let n=t();return e.waitUntil(n),n}try{self[`workbox:precaching:7.3.0`]&&_()}catch{}var s=`__WB_REVISION__`;function c(e){if(!e)throw new t(`add-to-cache-list-unexpected-type`,{entry:e});if(typeof e==`string`){let t=new URL(e,location.href);return{cacheKey:t.href,url:t.href}}let{revision:n,url:r}=e;if(!r)throw new t(`add-to-cache-list-unexpected-type`,{entry:e});if(!n){let e=new URL(r,location.href);return{cacheKey:e.href,url:e.href}}let i=new URL(r,location.href),a=new URL(r,location.href);return i.searchParams.set(s,n),{cacheKey:i.href,url:a.href}}var l=class{constructor(){this.updatedURLs=[],this.notUpdatedURLs=[],this.handlerWillStart=async({request:e,state:t})=>{t&&(t.originalRequest=e)},this.cachedResponseWillBeUsed=async({event:e,state:t,cachedResponse:n})=>{if(e.type===`install`&&t&&t.originalRequest&&t.originalRequest instanceof Request){let e=t.originalRequest.url;n?this.notUpdatedURLs.push(e):this.updatedURLs.push(e)}return n}}},u=class{constructor({precacheController:e}){this.cacheKeyWillBeUsed=async({request:e,params:t})=>{let n=t?.cacheKey||this._precacheController.getCacheKeyForURL(e.url);return n?new Request(n,{headers:e.headers}):e},this._precacheController=e}},d;function f(){if(d===void 0){let e=new Response(``);if(`body`in e)try{new Response(e.body),d=!0}catch{d=!1}d=!1}return d}async function p(e,n){let r=null;if(e.url&&(r=new URL(e.url).origin),r!==self.location.origin)throw new t(`cross-origin-copy-response`,{origin:r});let i=e.clone(),a={headers:new Headers(i.headers),status:i.status,statusText:i.statusText},o=n?n(a):a,s=f()?i.body:await i.blob();return new Response(s,o)}var m=e=>new URL(String(e),location.href).href.replace(RegExp(`^${location.origin}`),``);function h(e,t){let n=new URL(e);for(let e of t)n.searchParams.delete(e);return n.href}async function g(e,t,n,r){let i=h(t.url,n);if(t.url===i)return e.match(t,r);let a=Object.assign(Object.assign({},r),{ignoreSearch:!0}),o=await e.keys(t,a);for(let t of o)if(i===h(t.url,n))return e.match(t,r)}var v=class{constructor(){this.promise=new Promise((e,t)=>{this.resolve=e,this.reject=t})}},y=new Set;async function b(){for(let e of y)await e()}function x(e){return new Promise(t=>setTimeout(t,e))}try{self[`workbox:strategies:7.3.0`]&&_()}catch{}function S(e){return typeof e==`string`?new Request(e):e}var C=class{constructor(e,t){this._cacheKeys={},Object.assign(this,t),this.event=t.event,this._strategy=e,this._handlerDeferred=new v,this._extendLifetimePromises=[],this._plugins=[...e.plugins],this._pluginStateMap=new Map;for(let e of this._plugins)this._pluginStateMap.set(e,{});this.event.waitUntil(this._handlerDeferred.promise)}async fetch(e){let{event:n}=this,r=S(e);if(r.mode===`navigate`&&n instanceof FetchEvent&&n.preloadResponse){let e=await n.preloadResponse;if(e)return e}let i=this.hasCallback(`fetchDidFail`)?r.clone():null;try{for(let e of this.iterateCallbacks(`requestWillFetch`))r=await e({request:r.clone(),event:n})}catch(e){if(e instanceof Error)throw new t(`plugin-error-request-will-fetch`,{thrownErrorMessage:e.message})}let a=r.clone();try{let e;e=await fetch(r,r.mode===`navigate`?void 0:this._strategy.fetchOptions);for(let t of this.iterateCallbacks(`fetchDidSucceed`))e=await t({event:n,request:a,response:e});return e}catch(e){throw i&&await this.runCallbacks(`fetchDidFail`,{error:e,event:n,originalRequest:i.clone(),request:a.clone()}),e}}async fetchAndCachePut(e){let t=await this.fetch(e),n=t.clone();return this.waitUntil(this.cachePut(e,n)),t}async cacheMatch(e){let t=S(e),n,{cacheName:r,matchOptions:i}=this._strategy,a=await this.getCacheKey(t,`read`),o=Object.assign(Object.assign({},i),{cacheName:r});n=await caches.match(a,o);for(let e of this.iterateCallbacks(`cachedResponseWillBeUsed`))n=await e({cacheName:r,matchOptions:i,cachedResponse:n,request:a,event:this.event})||void 0;return n}async cachePut(e,n){let r=S(e);await x(0);let i=await this.getCacheKey(r,`write`);if(!n)throw new t(`cache-put-with-no-response`,{url:m(i.url)});let a=await this._ensureResponseSafeToCache(n);if(!a)return!1;let{cacheName:o,matchOptions:s}=this._strategy,c=await self.caches.open(o),l=this.hasCallback(`cacheDidUpdate`),u=l?await g(c,i.clone(),[`__WB_REVISION__`],s):null;try{await c.put(i,l?a.clone():a)}catch(e){if(e instanceof Error)throw e.name===`QuotaExceededError`&&await b(),e}for(let e of this.iterateCallbacks(`cacheDidUpdate`))await e({cacheName:o,oldResponse:u,newResponse:a.clone(),request:i,event:this.event});return!0}async getCacheKey(e,t){let n=`${e.url} | ${t}`;if(!this._cacheKeys[n]){let r=e;for(let e of this.iterateCallbacks(`cacheKeyWillBeUsed`))r=S(await e({mode:t,request:r,event:this.event,params:this.params}));this._cacheKeys[n]=r}return this._cacheKeys[n]}hasCallback(e){for(let t of this._strategy.plugins)if(e in t)return!0;return!1}async runCallbacks(e,t){for(let n of this.iterateCallbacks(e))await n(t)}*iterateCallbacks(e){for(let t of this._strategy.plugins)if(typeof t[e]==`function`){let n=this._pluginStateMap.get(t);yield r=>{let i=Object.assign(Object.assign({},r),{state:n});return t[e](i)}}}waitUntil(e){return this._extendLifetimePromises.push(e),e}async doneWaiting(){for(;this._extendLifetimePromises.length;){let e=this._extendLifetimePromises.splice(0),t=(await Promise.allSettled(e)).find(e=>e.status===`rejected`);if(t)throw t.reason}}destroy(){this._handlerDeferred.resolve(null)}async _ensureResponseSafeToCache(e){let t=e,n=!1;for(let e of this.iterateCallbacks(`cacheWillUpdate`))if(t=await e({request:this.request,response:t,event:this.event})||void 0,n=!0,!t)break;return n||t&&t.status!==200&&(t=void 0),t}},w=class{constructor(e={}){this.cacheName=a.getRuntimeName(e.cacheName),this.plugins=e.plugins||[],this.fetchOptions=e.fetchOptions,this.matchOptions=e.matchOptions}handle(e){let[t]=this.handleAll(e);return t}handleAll(e){e instanceof FetchEvent&&(e={event:e,request:e.request});let t=e.event,n=typeof e.request==`string`?new Request(e.request):e.request,r=`params`in e?e.params:void 0,i=new C(this,{event:t,request:n,params:r}),a=this._getResponse(i,n,t);return[a,this._awaitComplete(a,i,n,t)]}async _getResponse(e,n,r){await e.runCallbacks(`handlerWillStart`,{event:r,request:n});let i;try{if(i=await this._handle(n,e),!i||i.type===`error`)throw new t(`no-response`,{url:n.url})}catch(t){if(t instanceof Error){for(let a of e.iterateCallbacks(`handlerDidError`))if(i=await a({error:t,event:r,request:n}),i)break}if(!i)throw t}for(let t of e.iterateCallbacks(`handlerWillRespond`))i=await t({event:r,request:n,response:i});return i}async _awaitComplete(e,t,n,r){let i,a;try{i=await e}catch{}try{await t.runCallbacks(`handlerDidRespond`,{event:r,request:n,response:i}),await t.doneWaiting()}catch(e){e instanceof Error&&(a=e)}if(await t.runCallbacks(`handlerDidComplete`,{event:r,request:n,response:i,error:a}),t.destroy(),a)throw a}},T=class e extends w{constructor(t={}){t.cacheName=a.getPrecacheName(t.cacheName),super(t),this._fallbackToNetwork=t.fallbackToNetwork!==!1,this.plugins.push(e.copyRedirectedCacheableResponsesPlugin)}async _handle(e,t){return await t.cacheMatch(e)||(t.event&&t.event.type===`install`?await this._handleInstall(e,t):await this._handleFetch(e,t))}async _handleFetch(e,n){let r,i=n.params||{};if(this._fallbackToNetwork){let t=i.integrity,a=e.integrity,o=!a||a===t;r=await n.fetch(new Request(e,{integrity:e.mode===`no-cors`?void 0:a||t})),t&&o&&e.mode!==`no-cors`&&(this._useDefaultCacheabilityPluginIfNeeded(),await n.cachePut(e,r.clone()))}else throw new t(`missing-precache-entry`,{cacheName:this.cacheName,url:e.url});return r}async _handleInstall(e,n){this._useDefaultCacheabilityPluginIfNeeded();let r=await n.fetch(e);if(!await n.cachePut(e,r.clone()))throw new t(`bad-precaching-response`,{url:e.url,status:r.status});return r}_useDefaultCacheabilityPluginIfNeeded(){let t=null,n=0;for(let[r,i]of this.plugins.entries())i!==e.copyRedirectedCacheableResponsesPlugin&&(i===e.defaultPrecacheCacheabilityPlugin&&(t=r),i.cacheWillUpdate&&n++);n===0?this.plugins.push(e.defaultPrecacheCacheabilityPlugin):n>1&&t!==null&&this.plugins.splice(t,1)}};T.defaultPrecacheCacheabilityPlugin={async cacheWillUpdate({response:e}){return!e||e.status>=400?null:e}},T.copyRedirectedCacheableResponsesPlugin={async cacheWillUpdate({response:e}){return e.redirected?await p(e):e}};var E=class{constructor({cacheName:e,plugins:t=[],fallbackToNetwork:n=!0}={}){this._urlsToCacheKeys=new Map,this._urlsToCacheModes=new Map,this._cacheKeysToIntegrities=new Map,this._strategy=new T({cacheName:a.getPrecacheName(e),plugins:[...t,new u({precacheController:this})],fallbackToNetwork:n}),this.install=this.install.bind(this),this.activate=this.activate.bind(this)}get strategy(){return this._strategy}precache(e){this.addToCacheList(e),this._installAndActiveListenersAdded||=(self.addEventListener(`install`,this.install),self.addEventListener(`activate`,this.activate),!0)}addToCacheList(e){let n=[];for(let r of e){typeof r==`string`?n.push(r):r&&r.revision===void 0&&n.push(r.url);let{cacheKey:e,url:i}=c(r),a=typeof r!=`string`&&r.revision?`reload`:`default`;if(this._urlsToCacheKeys.has(i)&&this._urlsToCacheKeys.get(i)!==e)throw new t(`add-to-cache-list-conflicting-entries`,{firstEntry:this._urlsToCacheKeys.get(i),secondEntry:e});if(typeof r!=`string`&&r.integrity){if(this._cacheKeysToIntegrities.has(e)&&this._cacheKeysToIntegrities.get(e)!==r.integrity)throw new t(`add-to-cache-list-conflicting-integrities`,{url:i});this._cacheKeysToIntegrities.set(e,r.integrity)}if(this._urlsToCacheKeys.set(i,e),this._urlsToCacheModes.set(i,a),n.length>0){let e=`Workbox is precaching URLs without revision info: ${n.join(`, `)}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(e)}}}install(e){return o(e,async()=>{let t=new l;this.strategy.plugins.push(t);for(let[t,n]of this._urlsToCacheKeys){let r=this._cacheKeysToIntegrities.get(n),i=this._urlsToCacheModes.get(t),a=new Request(t,{integrity:r,cache:i,credentials:`same-origin`});await Promise.all(this.strategy.handleAll({params:{cacheKey:n},request:a,event:e}))}let{updatedURLs:n,notUpdatedURLs:r}=t;return{updatedURLs:n,notUpdatedURLs:r}})}activate(e){return o(e,async()=>{let e=await self.caches.open(this.strategy.cacheName),t=await e.keys(),n=new Set(this._urlsToCacheKeys.values()),r=[];for(let i of t)n.has(i.url)||(await e.delete(i),r.push(i.url));return{deletedURLs:r}})}getURLsToCacheKeys(){return this._urlsToCacheKeys}getCachedURLs(){return[...this._urlsToCacheKeys.keys()]}getCacheKeyForURL(e){let t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForCacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){let t=e instanceof Request?e.url:e,n=this.getCacheKeyForURL(t);if(n)return(await self.caches.open(this.strategy.cacheName)).match(n)}createHandlerBoundToURL(e){let n=this.getCacheKeyForURL(e);if(!n)throw new t(`non-precached-url`,{url:e});return t=>(t.request=new Request(e),t.params=Object.assign({cacheKey:n},t.params),this.strategy.handle(t))}},D,O=()=>(D||=new E,D);try{self[`workbox:routing:7.3.0`]&&_()}catch{}var k=e=>e&&typeof e==`object`?e:{handle:e},A=class{constructor(e,t,n=`GET`){this.handler=k(t),this.match=e,this.method=n}setCatchHandler(e){this.catchHandler=k(e)}},j=class extends A{constructor(e,t,n){super(({url:t})=>{let n=e.exec(t.href);if(n&&!(t.origin!==location.origin&&n.index!==0))return n.slice(1)},t,n)}},M=class{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener(`fetch`,(e=>{let{request:t}=e,n=this.handleRequest({request:t,event:e});n&&e.respondWith(n)}))}addCacheListener(){self.addEventListener(`message`,(e=>{if(e.data&&e.data.type===`CACHE_URLS`){let{payload:t}=e.data,n=Promise.all(t.urlsToCache.map(t=>{typeof t==`string`&&(t=[t]);let n=new Request(...t);return this.handleRequest({request:n,event:e})}));e.waitUntil(n),e.ports&&e.ports[0]&&n.then(()=>e.ports[0].postMessage(!0))}}))}handleRequest({request:e,event:t}){let n=new URL(e.url,location.href);if(!n.protocol.startsWith(`http`))return;let r=n.origin===location.origin,{params:i,route:a}=this.findMatchingRoute({event:t,request:e,sameOrigin:r,url:n}),o=a&&a.handler,s=e.method;if(!o&&this._defaultHandlerMap.has(s)&&(o=this._defaultHandlerMap.get(s)),!o)return;let c;try{c=o.handle({url:n,request:e,event:t,params:i})}catch(e){c=Promise.reject(e)}let l=a&&a.catchHandler;return c instanceof Promise&&(this._catchHandler||l)&&(c=c.catch(async r=>{if(l)try{return await l.handle({url:n,request:e,event:t,params:i})}catch(e){e instanceof Error&&(r=e)}if(this._catchHandler)return this._catchHandler.handle({url:n,request:e,event:t});throw r})),c}findMatchingRoute({url:e,sameOrigin:t,request:n,event:r}){let i=this._routes.get(n.method)||[];for(let a of i){let i,o=a.match({url:e,sameOrigin:t,request:n,event:r});if(o)return i=o,(Array.isArray(i)&&i.length===0||o.constructor===Object&&Object.keys(o).length===0||typeof o==`boolean`)&&(i=void 0),{route:a,params:i}}return{}}setDefaultHandler(e,t=`GET`){this._defaultHandlerMap.set(t,k(e))}setCatchHandler(e){this._catchHandler=k(e)}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new t(`unregister-route-but-not-found-with-method`,{method:e.method});let n=this._routes.get(e.method).indexOf(e);if(n>-1)this._routes.get(e.method).splice(n,1);else throw new t(`unregister-route-route-not-registered`)}},N,P=()=>(N||(N=new M,N.addFetchListener(),N.addCacheListener()),N);function F(e,n,r){let i;if(typeof e==`string`){let t=new URL(e,location.href);i=new A(({url:e})=>e.href===t.href,n,r)}else if(e instanceof RegExp)i=new j(e,n,r);else if(typeof e==`function`)i=new A(e,n,r);else if(e instanceof A)i=e;else throw new t(`unsupported-route-type`,{moduleName:`workbox-routing`,funcName:`registerRoute`,paramName:`capture`});return P().registerRoute(i),i}function I(e,t=[]){for(let n of[...e.searchParams.keys()])t.some(e=>e.test(n))&&e.searchParams.delete(n);return e}function*L(e,{ignoreURLParametersMatching:t=[/^utm_/,/^fbclid$/],directoryIndex:n=`index.html`,cleanURLs:r=!0,urlManipulation:i}={}){let a=new URL(e,location.href);a.hash=``,yield a.href;let o=I(a,t);if(yield o.href,n&&o.pathname.endsWith(`/`)){let e=new URL(o.href);e.pathname+=n,yield e.href}if(r){let e=new URL(o.href);e.pathname+=`.html`,yield e.href}if(i){let e=i({url:a});for(let t of e)yield t.href}}var R=class extends A{constructor(e,t){super(({request:n})=>{let r=e.getURLsToCacheKeys();for(let i of L(n.url,t)){let t=r.get(i);if(t)return{cacheKey:t,integrity:e.getIntegrityForCacheKey(t)}}},e.strategy)}};function z(e){F(new R(O(),e))}function B(e){O().precache(e)}function V(e,t){B(e),z(t)}V([{"revision":"1872c500de691dce40960bb85481de07","url":"registerSW.js"},{"revision":"ba24b0a24eb3a419f20bb0032733de86","url":"index.html"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-512.svg"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-192.svg"},{"revision":"eb9818b9094675c0c5d303168f273345","url":"monacoeditorwork/ts.worker.bundle.js"},{"revision":"9af0be92dcefdc1f1290441cb5ff5d9b","url":"monacoeditorwork/json.worker.bundle.js"},{"revision":"a261b429c39dbb75ae97972d7d005e6d","url":"monacoeditorwork/html.worker.bundle.js"},{"revision":"79953d804e1bbacecfd79b85fd679016","url":"monacoeditorwork/editor.worker.bundle.js"},{"revision":"fdcba0d09aac31df7a0bc652f6e739bd","url":"monacoeditorwork/css.worker.bundle.js"},{"revision":null,"url":"assets/utils-siJJ3uG0.js"},{"revision":null,"url":"assets/use-monaco-theme-Dexl3s3E.js"},{"revision":null,"url":"assets/terminal-tab-CTN18lb6.js"},{"revision":null,"url":"assets/terminal-tab-BrP-ENHg.css"},{"revision":null,"url":"assets/table-DCVKGOr2.js"},{"revision":null,"url":"assets/tab-store-DIyJSjtr.js"},{"revision":null,"url":"assets/sqlite-viewer-EwHWc37J.js"},{"revision":null,"url":"assets/settings-tab-BnDkeQWk.js"},{"revision":null,"url":"assets/settings-store-CfB0vCtQ.js"},{"revision":null,"url":"assets/react-DHSo28we.js"},{"revision":null,"url":"assets/react-CYzKIDNi.js"},{"revision":null,"url":"assets/postgres-viewer-CaMySHpD.js"},{"revision":null,"url":"assets/markdown-renderer-z99RjIxZ.js"},{"revision":null,"url":"assets/keybindings-store-kHLASnRb.js"},{"revision":null,"url":"assets/jsx-runtime-wQxeESYQ.js"},{"revision":null,"url":"assets/input-nI4xe1Y9.js"},{"revision":null,"url":"assets/index-DBdw8tN_.js"},{"revision":null,"url":"assets/index-CigdXBuQ.css"},{"revision":null,"url":"assets/git-graph-xD6TLRVv.js"},{"revision":null,"url":"assets/dist-Jb3Tnkpc.js"},{"revision":null,"url":"assets/diff-viewer-DVvY1aFb.js"},{"revision":null,"url":"assets/database-viewer-CAgZOkZc.js"},{"revision":null,"url":"assets/code-editor-BUg7alP6.js"},{"revision":null,"url":"assets/chat-tab-CWhxhPKH.js"},{"revision":null,"url":"assets/api-client-4Ni0i4Hl.js"},{"revision":"79c8870653c8f419f2e3323085e1f4be","url":"manifest.webmanifest"}]),self.addEventListener(`push`,e=>{e.waitUntil(self.clients.matchAll({type:`window`,includeUncontrolled:!0}).then(t=>{if(t.some(e=>e.visibilityState===`visible`))return;let n=e.data?.json()??{title:`PPM`,body:`Chat completed`};return self.registration.showNotification(n.title,{body:n.body,icon:`/icon-192.png`,badge:`/icon-192.png`,tag:`ppm-chat-done`,silent:!1,data:{url:self.location.origin}})}))}),self.addEventListener(`notificationclick`,e=>{e.notification.close(),e.waitUntil(self.clients.matchAll({type:`window`,includeUncontrolled:!0}).then(t=>{for(let e of t)if(e.url.includes(self.location.origin)&&`focus`in e)return e.focus();return self.clients.openWindow(e.notification.data?.url||`/`)}))});
1
+ try{self[`workbox:core:7.3.0`]&&_()}catch{}var e=(e,...t)=>{let n=e;return t.length>0&&(n+=` :: ${JSON.stringify(t)}`),n},t=class extends Error{constructor(t,n){let r=e(t,n);super(r),this.name=t,this.details=n}},n={googleAnalytics:`googleAnalytics`,precache:`precache-v2`,prefix:`workbox`,runtime:`runtime`,suffix:typeof registration<`u`?registration.scope:``},r=e=>[n.prefix,e,n.suffix].filter(e=>e&&e.length>0).join(`-`),i=e=>{for(let t of Object.keys(n))e(t)},a={updateDetails:e=>{i(t=>{typeof e[t]==`string`&&(n[t]=e[t])})},getGoogleAnalyticsName:e=>e||r(n.googleAnalytics),getPrecacheName:e=>e||r(n.precache),getPrefix:()=>n.prefix,getRuntimeName:e=>e||r(n.runtime),getSuffix:()=>n.suffix};function o(e,t){let n=t();return e.waitUntil(n),n}try{self[`workbox:precaching:7.3.0`]&&_()}catch{}var s=`__WB_REVISION__`;function c(e){if(!e)throw new t(`add-to-cache-list-unexpected-type`,{entry:e});if(typeof e==`string`){let t=new URL(e,location.href);return{cacheKey:t.href,url:t.href}}let{revision:n,url:r}=e;if(!r)throw new t(`add-to-cache-list-unexpected-type`,{entry:e});if(!n){let e=new URL(r,location.href);return{cacheKey:e.href,url:e.href}}let i=new URL(r,location.href),a=new URL(r,location.href);return i.searchParams.set(s,n),{cacheKey:i.href,url:a.href}}var l=class{constructor(){this.updatedURLs=[],this.notUpdatedURLs=[],this.handlerWillStart=async({request:e,state:t})=>{t&&(t.originalRequest=e)},this.cachedResponseWillBeUsed=async({event:e,state:t,cachedResponse:n})=>{if(e.type===`install`&&t&&t.originalRequest&&t.originalRequest instanceof Request){let e=t.originalRequest.url;n?this.notUpdatedURLs.push(e):this.updatedURLs.push(e)}return n}}},u=class{constructor({precacheController:e}){this.cacheKeyWillBeUsed=async({request:e,params:t})=>{let n=t?.cacheKey||this._precacheController.getCacheKeyForURL(e.url);return n?new Request(n,{headers:e.headers}):e},this._precacheController=e}},d;function f(){if(d===void 0){let e=new Response(``);if(`body`in e)try{new Response(e.body),d=!0}catch{d=!1}d=!1}return d}async function p(e,n){let r=null;if(e.url&&(r=new URL(e.url).origin),r!==self.location.origin)throw new t(`cross-origin-copy-response`,{origin:r});let i=e.clone(),a={headers:new Headers(i.headers),status:i.status,statusText:i.statusText},o=n?n(a):a,s=f()?i.body:await i.blob();return new Response(s,o)}var m=e=>new URL(String(e),location.href).href.replace(RegExp(`^${location.origin}`),``);function h(e,t){let n=new URL(e);for(let e of t)n.searchParams.delete(e);return n.href}async function g(e,t,n,r){let i=h(t.url,n);if(t.url===i)return e.match(t,r);let a=Object.assign(Object.assign({},r),{ignoreSearch:!0}),o=await e.keys(t,a);for(let t of o)if(i===h(t.url,n))return e.match(t,r)}var v=class{constructor(){this.promise=new Promise((e,t)=>{this.resolve=e,this.reject=t})}},y=new Set;async function b(){for(let e of y)await e()}function x(e){return new Promise(t=>setTimeout(t,e))}try{self[`workbox:strategies:7.3.0`]&&_()}catch{}function S(e){return typeof e==`string`?new Request(e):e}var C=class{constructor(e,t){this._cacheKeys={},Object.assign(this,t),this.event=t.event,this._strategy=e,this._handlerDeferred=new v,this._extendLifetimePromises=[],this._plugins=[...e.plugins],this._pluginStateMap=new Map;for(let e of this._plugins)this._pluginStateMap.set(e,{});this.event.waitUntil(this._handlerDeferred.promise)}async fetch(e){let{event:n}=this,r=S(e);if(r.mode===`navigate`&&n instanceof FetchEvent&&n.preloadResponse){let e=await n.preloadResponse;if(e)return e}let i=this.hasCallback(`fetchDidFail`)?r.clone():null;try{for(let e of this.iterateCallbacks(`requestWillFetch`))r=await e({request:r.clone(),event:n})}catch(e){if(e instanceof Error)throw new t(`plugin-error-request-will-fetch`,{thrownErrorMessage:e.message})}let a=r.clone();try{let e;e=await fetch(r,r.mode===`navigate`?void 0:this._strategy.fetchOptions);for(let t of this.iterateCallbacks(`fetchDidSucceed`))e=await t({event:n,request:a,response:e});return e}catch(e){throw i&&await this.runCallbacks(`fetchDidFail`,{error:e,event:n,originalRequest:i.clone(),request:a.clone()}),e}}async fetchAndCachePut(e){let t=await this.fetch(e),n=t.clone();return this.waitUntil(this.cachePut(e,n)),t}async cacheMatch(e){let t=S(e),n,{cacheName:r,matchOptions:i}=this._strategy,a=await this.getCacheKey(t,`read`),o=Object.assign(Object.assign({},i),{cacheName:r});n=await caches.match(a,o);for(let e of this.iterateCallbacks(`cachedResponseWillBeUsed`))n=await e({cacheName:r,matchOptions:i,cachedResponse:n,request:a,event:this.event})||void 0;return n}async cachePut(e,n){let r=S(e);await x(0);let i=await this.getCacheKey(r,`write`);if(!n)throw new t(`cache-put-with-no-response`,{url:m(i.url)});let a=await this._ensureResponseSafeToCache(n);if(!a)return!1;let{cacheName:o,matchOptions:s}=this._strategy,c=await self.caches.open(o),l=this.hasCallback(`cacheDidUpdate`),u=l?await g(c,i.clone(),[`__WB_REVISION__`],s):null;try{await c.put(i,l?a.clone():a)}catch(e){if(e instanceof Error)throw e.name===`QuotaExceededError`&&await b(),e}for(let e of this.iterateCallbacks(`cacheDidUpdate`))await e({cacheName:o,oldResponse:u,newResponse:a.clone(),request:i,event:this.event});return!0}async getCacheKey(e,t){let n=`${e.url} | ${t}`;if(!this._cacheKeys[n]){let r=e;for(let e of this.iterateCallbacks(`cacheKeyWillBeUsed`))r=S(await e({mode:t,request:r,event:this.event,params:this.params}));this._cacheKeys[n]=r}return this._cacheKeys[n]}hasCallback(e){for(let t of this._strategy.plugins)if(e in t)return!0;return!1}async runCallbacks(e,t){for(let n of this.iterateCallbacks(e))await n(t)}*iterateCallbacks(e){for(let t of this._strategy.plugins)if(typeof t[e]==`function`){let n=this._pluginStateMap.get(t);yield r=>{let i=Object.assign(Object.assign({},r),{state:n});return t[e](i)}}}waitUntil(e){return this._extendLifetimePromises.push(e),e}async doneWaiting(){for(;this._extendLifetimePromises.length;){let e=this._extendLifetimePromises.splice(0),t=(await Promise.allSettled(e)).find(e=>e.status===`rejected`);if(t)throw t.reason}}destroy(){this._handlerDeferred.resolve(null)}async _ensureResponseSafeToCache(e){let t=e,n=!1;for(let e of this.iterateCallbacks(`cacheWillUpdate`))if(t=await e({request:this.request,response:t,event:this.event})||void 0,n=!0,!t)break;return n||t&&t.status!==200&&(t=void 0),t}},w=class{constructor(e={}){this.cacheName=a.getRuntimeName(e.cacheName),this.plugins=e.plugins||[],this.fetchOptions=e.fetchOptions,this.matchOptions=e.matchOptions}handle(e){let[t]=this.handleAll(e);return t}handleAll(e){e instanceof FetchEvent&&(e={event:e,request:e.request});let t=e.event,n=typeof e.request==`string`?new Request(e.request):e.request,r=`params`in e?e.params:void 0,i=new C(this,{event:t,request:n,params:r}),a=this._getResponse(i,n,t);return[a,this._awaitComplete(a,i,n,t)]}async _getResponse(e,n,r){await e.runCallbacks(`handlerWillStart`,{event:r,request:n});let i;try{if(i=await this._handle(n,e),!i||i.type===`error`)throw new t(`no-response`,{url:n.url})}catch(t){if(t instanceof Error){for(let a of e.iterateCallbacks(`handlerDidError`))if(i=await a({error:t,event:r,request:n}),i)break}if(!i)throw t}for(let t of e.iterateCallbacks(`handlerWillRespond`))i=await t({event:r,request:n,response:i});return i}async _awaitComplete(e,t,n,r){let i,a;try{i=await e}catch{}try{await t.runCallbacks(`handlerDidRespond`,{event:r,request:n,response:i}),await t.doneWaiting()}catch(e){e instanceof Error&&(a=e)}if(await t.runCallbacks(`handlerDidComplete`,{event:r,request:n,response:i,error:a}),t.destroy(),a)throw a}},T=class e extends w{constructor(t={}){t.cacheName=a.getPrecacheName(t.cacheName),super(t),this._fallbackToNetwork=t.fallbackToNetwork!==!1,this.plugins.push(e.copyRedirectedCacheableResponsesPlugin)}async _handle(e,t){return await t.cacheMatch(e)||(t.event&&t.event.type===`install`?await this._handleInstall(e,t):await this._handleFetch(e,t))}async _handleFetch(e,n){let r,i=n.params||{};if(this._fallbackToNetwork){let t=i.integrity,a=e.integrity,o=!a||a===t;r=await n.fetch(new Request(e,{integrity:e.mode===`no-cors`?void 0:a||t})),t&&o&&e.mode!==`no-cors`&&(this._useDefaultCacheabilityPluginIfNeeded(),await n.cachePut(e,r.clone()))}else throw new t(`missing-precache-entry`,{cacheName:this.cacheName,url:e.url});return r}async _handleInstall(e,n){this._useDefaultCacheabilityPluginIfNeeded();let r=await n.fetch(e);if(!await n.cachePut(e,r.clone()))throw new t(`bad-precaching-response`,{url:e.url,status:r.status});return r}_useDefaultCacheabilityPluginIfNeeded(){let t=null,n=0;for(let[r,i]of this.plugins.entries())i!==e.copyRedirectedCacheableResponsesPlugin&&(i===e.defaultPrecacheCacheabilityPlugin&&(t=r),i.cacheWillUpdate&&n++);n===0?this.plugins.push(e.defaultPrecacheCacheabilityPlugin):n>1&&t!==null&&this.plugins.splice(t,1)}};T.defaultPrecacheCacheabilityPlugin={async cacheWillUpdate({response:e}){return!e||e.status>=400?null:e}},T.copyRedirectedCacheableResponsesPlugin={async cacheWillUpdate({response:e}){return e.redirected?await p(e):e}};var E=class{constructor({cacheName:e,plugins:t=[],fallbackToNetwork:n=!0}={}){this._urlsToCacheKeys=new Map,this._urlsToCacheModes=new Map,this._cacheKeysToIntegrities=new Map,this._strategy=new T({cacheName:a.getPrecacheName(e),plugins:[...t,new u({precacheController:this})],fallbackToNetwork:n}),this.install=this.install.bind(this),this.activate=this.activate.bind(this)}get strategy(){return this._strategy}precache(e){this.addToCacheList(e),this._installAndActiveListenersAdded||=(self.addEventListener(`install`,this.install),self.addEventListener(`activate`,this.activate),!0)}addToCacheList(e){let n=[];for(let r of e){typeof r==`string`?n.push(r):r&&r.revision===void 0&&n.push(r.url);let{cacheKey:e,url:i}=c(r),a=typeof r!=`string`&&r.revision?`reload`:`default`;if(this._urlsToCacheKeys.has(i)&&this._urlsToCacheKeys.get(i)!==e)throw new t(`add-to-cache-list-conflicting-entries`,{firstEntry:this._urlsToCacheKeys.get(i),secondEntry:e});if(typeof r!=`string`&&r.integrity){if(this._cacheKeysToIntegrities.has(e)&&this._cacheKeysToIntegrities.get(e)!==r.integrity)throw new t(`add-to-cache-list-conflicting-integrities`,{url:i});this._cacheKeysToIntegrities.set(e,r.integrity)}if(this._urlsToCacheKeys.set(i,e),this._urlsToCacheModes.set(i,a),n.length>0){let e=`Workbox is precaching URLs without revision info: ${n.join(`, `)}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(e)}}}install(e){return o(e,async()=>{let t=new l;this.strategy.plugins.push(t);for(let[t,n]of this._urlsToCacheKeys){let r=this._cacheKeysToIntegrities.get(n),i=this._urlsToCacheModes.get(t),a=new Request(t,{integrity:r,cache:i,credentials:`same-origin`});await Promise.all(this.strategy.handleAll({params:{cacheKey:n},request:a,event:e}))}let{updatedURLs:n,notUpdatedURLs:r}=t;return{updatedURLs:n,notUpdatedURLs:r}})}activate(e){return o(e,async()=>{let e=await self.caches.open(this.strategy.cacheName),t=await e.keys(),n=new Set(this._urlsToCacheKeys.values()),r=[];for(let i of t)n.has(i.url)||(await e.delete(i),r.push(i.url));return{deletedURLs:r}})}getURLsToCacheKeys(){return this._urlsToCacheKeys}getCachedURLs(){return[...this._urlsToCacheKeys.keys()]}getCacheKeyForURL(e){let t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForCacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){let t=e instanceof Request?e.url:e,n=this.getCacheKeyForURL(t);if(n)return(await self.caches.open(this.strategy.cacheName)).match(n)}createHandlerBoundToURL(e){let n=this.getCacheKeyForURL(e);if(!n)throw new t(`non-precached-url`,{url:e});return t=>(t.request=new Request(e),t.params=Object.assign({cacheKey:n},t.params),this.strategy.handle(t))}},D,O=()=>(D||=new E,D);try{self[`workbox:routing:7.3.0`]&&_()}catch{}var k=e=>e&&typeof e==`object`?e:{handle:e},A=class{constructor(e,t,n=`GET`){this.handler=k(t),this.match=e,this.method=n}setCatchHandler(e){this.catchHandler=k(e)}},j=class extends A{constructor(e,t,n){super(({url:t})=>{let n=e.exec(t.href);if(n&&!(t.origin!==location.origin&&n.index!==0))return n.slice(1)},t,n)}},M=class{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener(`fetch`,(e=>{let{request:t}=e,n=this.handleRequest({request:t,event:e});n&&e.respondWith(n)}))}addCacheListener(){self.addEventListener(`message`,(e=>{if(e.data&&e.data.type===`CACHE_URLS`){let{payload:t}=e.data,n=Promise.all(t.urlsToCache.map(t=>{typeof t==`string`&&(t=[t]);let n=new Request(...t);return this.handleRequest({request:n,event:e})}));e.waitUntil(n),e.ports&&e.ports[0]&&n.then(()=>e.ports[0].postMessage(!0))}}))}handleRequest({request:e,event:t}){let n=new URL(e.url,location.href);if(!n.protocol.startsWith(`http`))return;let r=n.origin===location.origin,{params:i,route:a}=this.findMatchingRoute({event:t,request:e,sameOrigin:r,url:n}),o=a&&a.handler,s=e.method;if(!o&&this._defaultHandlerMap.has(s)&&(o=this._defaultHandlerMap.get(s)),!o)return;let c;try{c=o.handle({url:n,request:e,event:t,params:i})}catch(e){c=Promise.reject(e)}let l=a&&a.catchHandler;return c instanceof Promise&&(this._catchHandler||l)&&(c=c.catch(async r=>{if(l)try{return await l.handle({url:n,request:e,event:t,params:i})}catch(e){e instanceof Error&&(r=e)}if(this._catchHandler)return this._catchHandler.handle({url:n,request:e,event:t});throw r})),c}findMatchingRoute({url:e,sameOrigin:t,request:n,event:r}){let i=this._routes.get(n.method)||[];for(let a of i){let i,o=a.match({url:e,sameOrigin:t,request:n,event:r});if(o)return i=o,(Array.isArray(i)&&i.length===0||o.constructor===Object&&Object.keys(o).length===0||typeof o==`boolean`)&&(i=void 0),{route:a,params:i}}return{}}setDefaultHandler(e,t=`GET`){this._defaultHandlerMap.set(t,k(e))}setCatchHandler(e){this._catchHandler=k(e)}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new t(`unregister-route-but-not-found-with-method`,{method:e.method});let n=this._routes.get(e.method).indexOf(e);if(n>-1)this._routes.get(e.method).splice(n,1);else throw new t(`unregister-route-route-not-registered`)}},N,P=()=>(N||(N=new M,N.addFetchListener(),N.addCacheListener()),N);function F(e,n,r){let i;if(typeof e==`string`){let t=new URL(e,location.href);i=new A(({url:e})=>e.href===t.href,n,r)}else if(e instanceof RegExp)i=new j(e,n,r);else if(typeof e==`function`)i=new A(e,n,r);else if(e instanceof A)i=e;else throw new t(`unsupported-route-type`,{moduleName:`workbox-routing`,funcName:`registerRoute`,paramName:`capture`});return P().registerRoute(i),i}function I(e,t=[]){for(let n of[...e.searchParams.keys()])t.some(e=>e.test(n))&&e.searchParams.delete(n);return e}function*L(e,{ignoreURLParametersMatching:t=[/^utm_/,/^fbclid$/],directoryIndex:n=`index.html`,cleanURLs:r=!0,urlManipulation:i}={}){let a=new URL(e,location.href);a.hash=``,yield a.href;let o=I(a,t);if(yield o.href,n&&o.pathname.endsWith(`/`)){let e=new URL(o.href);e.pathname+=n,yield e.href}if(r){let e=new URL(o.href);e.pathname+=`.html`,yield e.href}if(i){let e=i({url:a});for(let t of e)yield t.href}}var R=class extends A{constructor(e,t){super(({request:n})=>{let r=e.getURLsToCacheKeys();for(let i of L(n.url,t)){let t=r.get(i);if(t)return{cacheKey:t,integrity:e.getIntegrityForCacheKey(t)}}},e.strategy)}};function z(e){F(new R(O(),e))}function B(e){O().precache(e)}function V(e,t){B(e),z(t)}V([{"revision":"1872c500de691dce40960bb85481de07","url":"registerSW.js"},{"revision":"4276cff85869e97ffbe37bc4b5426eda","url":"index.html"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-512.svg"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-192.svg"},{"revision":"eb9818b9094675c0c5d303168f273345","url":"monacoeditorwork/ts.worker.bundle.js"},{"revision":"9af0be92dcefdc1f1290441cb5ff5d9b","url":"monacoeditorwork/json.worker.bundle.js"},{"revision":"a261b429c39dbb75ae97972d7d005e6d","url":"monacoeditorwork/html.worker.bundle.js"},{"revision":"79953d804e1bbacecfd79b85fd679016","url":"monacoeditorwork/editor.worker.bundle.js"},{"revision":"fdcba0d09aac31df7a0bc652f6e739bd","url":"monacoeditorwork/css.worker.bundle.js"},{"revision":null,"url":"assets/utils-siJJ3uG0.js"},{"revision":null,"url":"assets/use-monaco-theme-Dexl3s3E.js"},{"revision":null,"url":"assets/terminal-tab-D27e4ZTD.js"},{"revision":null,"url":"assets/terminal-tab-BrP-ENHg.css"},{"revision":null,"url":"assets/table-DCVKGOr2.js"},{"revision":null,"url":"assets/tab-store-Bm1Hw8OR.js"},{"revision":null,"url":"assets/sqlite-viewer-DfgaCbWT.js"},{"revision":null,"url":"assets/settings-tab-Bwsxb41F.js"},{"revision":null,"url":"assets/settings-store-CfB0vCtQ.js"},{"revision":null,"url":"assets/react-DHSo28we.js"},{"revision":null,"url":"assets/react-CYzKIDNi.js"},{"revision":null,"url":"assets/postgres-viewer-BL99auSm.js"},{"revision":null,"url":"assets/markdown-renderer-CvGYO9sH.js"},{"revision":null,"url":"assets/keybindings-store-C0m8_V9X.js"},{"revision":null,"url":"assets/jsx-runtime-wQxeESYQ.js"},{"revision":null,"url":"assets/input-nI4xe1Y9.js"},{"revision":null,"url":"assets/index-CBQhXXeV.css"},{"revision":null,"url":"assets/index-BU_07_oW.js"},{"revision":null,"url":"assets/git-graph-BpTt5iOd.js"},{"revision":null,"url":"assets/dist-CNRrBoQi.js"},{"revision":null,"url":"assets/diff-viewer-DAhrHpNM.js"},{"revision":null,"url":"assets/database-viewer-BxUpM_uA.js"},{"revision":null,"url":"assets/code-editor-D6OuzcC-.js"},{"revision":null,"url":"assets/chat-tab-CbNbBMGw.js"},{"revision":null,"url":"assets/api-client-4Ni0i4Hl.js"},{"revision":"79c8870653c8f419f2e3323085e1f4be","url":"manifest.webmanifest"}]),self.addEventListener(`push`,e=>{e.waitUntil(self.clients.matchAll({type:`window`,includeUncontrolled:!0}).then(t=>{if(t.some(e=>e.visibilityState===`visible`))return;let n=e.data?.json()??{title:`PPM`,body:`Chat completed`};return self.registration.showNotification(n.title,{body:n.body,icon:`/icon-192.png`,badge:`/icon-192.png`,tag:`ppm-chat-done`,silent:!1,data:{url:self.location.origin}})}))}),self.addEventListener(`notificationclick`,e=>{e.notification.close(),e.waitUntil(self.clients.matchAll({type:`window`,includeUncontrolled:!0}).then(t=>{for(let e of t)if(e.url.includes(self.location.origin)&&`focus`in e)return e.focus();return self.clients.openWindow(e.notification.data?.url||`/`)}))});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.6.7",
3
+ "version": "0.7.1",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -0,0 +1,12 @@
1
+ import { networkInterfaces } from "node:os";
2
+
3
+ /** Return first non-internal IPv4 address, or null if none found */
4
+ export function getLocalIp(): string | null {
5
+ const nets = networkInterfaces();
6
+ for (const name of Object.keys(nets)) {
7
+ for (const net of nets[name] ?? []) {
8
+ if (net.family === "IPv4" && !net.internal) return net.address;
9
+ }
10
+ }
11
+ return null;
12
+ }
@@ -261,7 +261,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
261
261
  const meta: Session = {
262
262
  id: sessionId,
263
263
  providerId: this.id,
264
- title: found.summary ?? "Resumed Chat",
264
+ title: found.customTitle ?? found.summary ?? "Resumed Chat",
265
265
  createdAt: new Date(found.lastModified).toISOString(),
266
266
  };
267
267
  this.activeSessions.set(sessionId, meta);
@@ -295,7 +295,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
295
295
  return sdkSessions.map((s) => ({
296
296
  id: s.sessionId,
297
297
  providerId: this.id,
298
- title: s.summary ?? s.firstPrompt ?? "Chat",
298
+ title: s.customTitle ?? s.summary ?? s.firstPrompt ?? "Chat",
299
299
  createdAt: new Date(s.lastModified).toISOString(),
300
300
  updatedAt: new Date(s.lastModified).toISOString(),
301
301
  }));
@@ -806,7 +806,65 @@ export class ClaudeAgentSdkProvider implements AIProvider {
806
806
  } catch (e) {
807
807
  const msg = (e as Error).message ?? String(e);
808
808
  console.error(`[sdk] error: ${msg}`);
809
- if (!msg.includes("abort") && !msg.includes("closed")) {
809
+ if (msg.includes("abort") || msg.includes("closed")) {
810
+ // User-initiated abort or WS closed — nothing to report
811
+ } else if (!isFirstMessage && msg.includes("exited with code")) {
812
+ // SDK subprocess crashed during session resume — retry as fresh session
813
+ console.warn(`[sdk] session resume failed, retrying as fresh session`);
814
+ try {
815
+ const providerConfig = this.getProviderConfig();
816
+ const effectiveCwd = meta.projectPath || homedir();
817
+ const queryEnv = { ...process.env, ...this.getProjectEnvOverrides(meta.projectPath) };
818
+ const retryQuery = query({
819
+ prompt: message,
820
+ options: {
821
+ cwd: effectiveCwd,
822
+ systemPrompt: { type: "preset", preset: "claude_code" },
823
+ settingSources: ["user", "project"],
824
+ env: queryEnv,
825
+ settings: { permissions: { allow: [], deny: [] } },
826
+ allowedTools: [
827
+ "Read", "Write", "Edit", "Bash", "Glob", "Grep",
828
+ "WebSearch", "WebFetch", "AskUserQuestion",
829
+ "Agent", "Skill", "TodoWrite", "ToolSearch",
830
+ ],
831
+ permissionMode: "bypassPermissions",
832
+ allowDangerouslySkipPermissions: true,
833
+ ...(providerConfig.model && { model: providerConfig.model }),
834
+ maxTurns: providerConfig.max_turns ?? 100,
835
+ canUseTool,
836
+ includePartialMessages: true,
837
+ } as any,
838
+ });
839
+ this.activeQueries.set(sessionId, retryQuery);
840
+ for await (const retryMsg of retryQuery) {
841
+ if (retryMsg.type === "system") continue;
842
+ if (retryMsg.type === "result") {
843
+ const r = retryMsg as any;
844
+ if (r.subtype && r.subtype !== "success") {
845
+ yield { type: "error", message: r.error ?? `Agent stopped: ${r.subtype}` };
846
+ }
847
+ resultSubtype = r.subtype;
848
+ resultNumTurns = r.num_turns;
849
+ break;
850
+ }
851
+ if ((retryMsg as any).type === "assistant") {
852
+ const content = (retryMsg as any).message?.content;
853
+ if (Array.isArray(content)) {
854
+ for (const block of content) {
855
+ if (block.type === "text" && typeof block.text === "string") {
856
+ yield { type: "text", content: block.text };
857
+ }
858
+ }
859
+ }
860
+ }
861
+ }
862
+ } catch (retryErr) {
863
+ const retryMsg = (retryErr as Error).message ?? String(retryErr);
864
+ console.error(`[sdk] retry also failed: ${retryMsg}`);
865
+ yield { type: "error", message: `SDK error: ${msg}` };
866
+ }
867
+ } else {
810
868
  yield { type: "error", message: `SDK error: ${msg}` };
811
869
  }
812
870
  } finally {
@@ -8,6 +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";
11
12
  import { ok, err } from "../../types/api.ts";
12
13
 
13
14
  type Env = { Variables: { projectPath: string; projectName: string } };
@@ -114,8 +115,11 @@ chatRoutes.patch("/sessions/:id", async (c) => {
114
115
  const body = await c.req.json<{ title?: string }>();
115
116
  if (!body.title?.trim()) return c.json(err("title is required"), 400);
116
117
  const title = body.title.trim();
118
+ // Resolve PPM UUID → SDK session ID if mapped
119
+ const sdkId = getSessionMapping(id) ?? id;
120
+ const projectPath = c.get("projectPath");
117
121
  // Persist to SDK so Claude Code CLI also sees the custom title
118
- await sdkRenameSession(id, title);
122
+ await sdkRenameSession(sdkId, title, { dir: projectPath });
119
123
  // Also update in-memory session
120
124
  const session = chatService.getSession(id);
121
125
  if (session) session.title = title;
@@ -6,6 +6,7 @@ import {
6
6
  validateDefaultProvider,
7
7
  VALID_PROVIDERS,
8
8
  type AIProviderConfig,
9
+ type TelegramConfig,
9
10
  type ThemeConfig,
10
11
  } from "../../types/config.ts";
11
12
  import { ok, err } from "../../types/api.ts";
@@ -135,3 +136,54 @@ settingsRoutes.put("/keybindings", async (c) => {
135
136
  return c.json(err((e as Error).message), 400);
136
137
  }
137
138
  });
139
+
140
+ // ── Telegram ──────────────────────────────────────────────────────────
141
+
142
+ /** GET /settings/telegram — return current telegram config (masks bot_token) */
143
+ settingsRoutes.get("/telegram", (c) => {
144
+ const tg = configService.get("telegram") as TelegramConfig | undefined;
145
+ if (!tg) return c.json(ok({ bot_token: "", chat_id: "" }));
146
+ return c.json(ok({
147
+ bot_token: tg.bot_token ? `${tg.bot_token.slice(0, 6)}...` : "",
148
+ chat_id: tg.chat_id,
149
+ }));
150
+ });
151
+
152
+ /** PUT /settings/telegram — save telegram bot_token + chat_id */
153
+ settingsRoutes.put("/telegram", async (c) => {
154
+ try {
155
+ const body = await c.req.json<{ bot_token?: string; chat_id?: string }>();
156
+ const current = (configService.get("telegram") as TelegramConfig | undefined) ?? { bot_token: "", chat_id: "" };
157
+ const updated: TelegramConfig = {
158
+ bot_token: body.bot_token ?? current.bot_token,
159
+ chat_id: body.chat_id ?? current.chat_id,
160
+ };
161
+ configService.set("telegram", updated);
162
+ configService.save();
163
+ return c.json(ok({
164
+ bot_token: updated.bot_token ? `${updated.bot_token.slice(0, 6)}...` : "",
165
+ chat_id: updated.chat_id,
166
+ }));
167
+ } catch (e) {
168
+ return c.json(err((e as Error).message), 400);
169
+ }
170
+ });
171
+
172
+ /** POST /settings/telegram/test — send a test message */
173
+ settingsRoutes.post("/telegram/test", async (c) => {
174
+ try {
175
+ const body = await c.req.json<{ bot_token?: string; chat_id?: string }>();
176
+ const current = (configService.get("telegram") as TelegramConfig | undefined) ?? { bot_token: "", chat_id: "" };
177
+ const token = body.bot_token || current.bot_token;
178
+ const chatId = body.chat_id || current.chat_id;
179
+ if (!token || !chatId) {
180
+ return c.json(err("bot_token and chat_id are required"), 400);
181
+ }
182
+ const { telegramService } = await import("../../services/telegram-notification.service.ts");
183
+ const result = await telegramService.sendTest(token, chatId);
184
+ if (!result.ok) return c.json(err(result.error ?? "Failed"), 500);
185
+ return c.json(ok({ sent: true }));
186
+ } catch (e) {
187
+ return c.json(err((e as Error).message), 500);
188
+ }
189
+ });
@@ -1,20 +1,9 @@
1
1
  import { Hono } from "hono";
2
- import { networkInterfaces } from "node:os";
3
2
  import { tunnelService } from "../../services/tunnel.service.ts";
4
3
  import { configService } from "../../services/config.service.ts";
4
+ import { getLocalIp } from "../../lib/network-utils.ts";
5
5
  import { ok, err } from "../../types/api.ts";
6
6
 
7
- /** Return first non-internal IPv4 address */
8
- function getLocalIp(): string | null {
9
- const nets = networkInterfaces();
10
- for (const name of Object.keys(nets)) {
11
- for (const net of nets[name] ?? []) {
12
- if (net.family === "IPv4" && !net.internal) return net.address;
13
- }
14
- }
15
- return null;
16
- }
17
-
18
7
  export const tunnelRoutes = new Hono();
19
8
 
20
9
  /** GET /api/tunnel — current tunnel status + local URL */
@@ -35,6 +35,14 @@ interface SessionEntry {
35
35
  /** Tracks active sessions — persists even when FE disconnects */
36
36
  const activeSessions = new Map<string, SessionEntry>();
37
37
 
38
+ /** Check if any frontend client is currently connected via WebSocket */
39
+ export function hasActiveClient(): boolean {
40
+ for (const entry of activeSessions.values()) {
41
+ if (entry.ws) return true;
42
+ }
43
+ return false;
44
+ }
45
+
38
46
  /** Send event to FE if connected, silently drop otherwise */
39
47
  function safeSend(sessionId: string, event: unknown): void {
40
48
  const entry = activeSessions.get(sessionId);
@@ -152,22 +160,42 @@ async function runStreamLoop(sessionId: string, providerId: string, content: str
152
160
  // Fire-and-forget: fetch updated session title from SDK summary
153
161
  sdkListSessions({ dir: entry.projectPath, limit: 50 }).then((sessions) => {
154
162
  const found = sessions.find((s) => s.sessionId === sessionId || s.sessionId === ev.sessionId);
155
- if (found?.summary) {
156
- safeSend(sessionId, { type: "title_updated", title: found.summary });
163
+ const title = found?.customTitle ?? found?.summary;
164
+ if (title) {
165
+ safeSend(sessionId, { type: "title_updated", title });
157
166
  // Also update in-memory session title
158
167
  const session = chatService.getSession(sessionId);
159
- if (session) session.title = found.summary;
168
+ if (session) session.title = title;
160
169
  }
161
170
  }).catch(() => {});
162
- // Fire-and-forget push notification
163
- import("../../services/push-notification.service.ts").then(({ pushService }) => {
171
+ // Fire-and-forget notification broadcast (push + telegram)
172
+ import("../../services/notification.service.ts").then(({ notificationService }) => {
164
173
  const project = entry.projectName || "Project";
165
174
  const session = chatService.getSession(sessionId);
166
175
  const sessionTitle = session?.title || `Session ${sessionId.slice(0, 8)}`;
167
- pushService.notifyAll("Chat completed", `${project} — ${sessionTitle}`).catch(() => {});
176
+ notificationService.broadcast("done", {
177
+ title: "Chat completed",
178
+ body: `${project} — ${sessionTitle}`,
179
+ project,
180
+ sessionId,
181
+ sessionTitle,
182
+ });
168
183
  }).catch(() => {});
169
184
  } else if (evType === "approval_request") {
170
185
  entry.pendingApprovalEvent = ev;
186
+ // Fire-and-forget notification for approval/question
187
+ import("../../services/notification.service.ts").then(({ notificationService }) => {
188
+ const project = entry.projectName || "Project";
189
+ const session = chatService.getSession(sessionId);
190
+ const sTitle = session?.title || `Session ${sessionId.slice(0, 8)}`;
191
+ const isQuestion = ev.tool === "AskUserQuestion";
192
+ const nType = isQuestion ? "question" : "approval_request";
193
+ const title = isQuestion ? "AI has a question" : "Waiting for approval";
194
+ const body = isQuestion
195
+ ? `${project} — ${sTitle}`
196
+ : `${project} — ${ev.tool} needs permission`;
197
+ notificationService.broadcast(nType as any, { title, body, project, sessionId, sessionTitle: sTitle, tool: ev.tool });
198
+ }).catch(() => {});
171
199
  } else {
172
200
  logSessionEvent(sessionId, evType.toUpperCase(), JSON.stringify(ev).slice(0, 200));
173
201
  }
@@ -271,9 +299,10 @@ export const chatWebSocket = {
271
299
  if (!session?.title || session.title === "Chat" || session.title === "Resumed Chat") {
272
300
  sdkListSessions({ dir: projectPath, limit: 50 }).then((sessions) => {
273
301
  const found = sessions.find((s) => s.sessionId === sessionId);
274
- if (found?.summary) {
275
- safeSend(sessionId, { type: "title_updated", title: found.summary });
276
- if (session) session.title = found.summary;
302
+ const title = found?.customTitle ?? found?.summary;
303
+ if (title) {
304
+ safeSend(sessionId, { type: "title_updated", title });
305
+ if (session) session.title = title;
277
306
  }
278
307
  }).catch(() => {});
279
308
  }
@@ -303,9 +332,10 @@ export const chatWebSocket = {
303
332
  if (!session?.title || session.title === "Chat" || session.title === "Resumed Chat") {
304
333
  sdkListSessions({ dir: projectPath, limit: 50 }).then((sessions) => {
305
334
  const found = sessions.find((s) => s.sessionId === sessionId);
306
- if (found?.summary) {
307
- safeSend(sessionId, { type: "title_updated", title: found.summary });
308
- if (session) session.title = found.summary;
335
+ const title = found?.customTitle ?? found?.summary;
336
+ if (title) {
337
+ safeSend(sessionId, { type: "title_updated", title });
338
+ if (session) session.title = title;
309
339
  }
310
340
  }).catch(() => {});
311
341
  }
@@ -19,7 +19,7 @@ const PPM_DIR = resolve(homedir(), ".ppm");
19
19
 
20
20
  /** Top-level config keys stored in the config table (not projects) */
21
21
  const CONFIG_TABLE_KEYS: (keyof PpmConfig)[] = [
22
- "device_name", "port", "host", "theme", "auth", "ai", "push",
22
+ "device_name", "port", "host", "theme", "auth", "ai", "push", "telegram",
23
23
  ];
24
24
 
25
25
  class ConfigService {
@@ -0,0 +1,42 @@
1
+ import { hasActiveClient } from "../server/ws/chat.ts";
2
+
3
+ export type NotificationType = "done" | "approval_request" | "question";
4
+
5
+ export interface NotificationPayload {
6
+ title: string;
7
+ body: string;
8
+ project: string;
9
+ sessionId: string;
10
+ sessionTitle?: string;
11
+ tool?: string;
12
+ deviceName?: string;
13
+ }
14
+
15
+ class NotificationService {
16
+ /** Broadcast notification to all channels (push, telegram). Fire-and-forget. */
17
+ async broadcast(_type: NotificationType, payload: NotificationPayload): Promise<void> {
18
+ const tasks: Promise<void>[] = [];
19
+ const userOnline = hasActiveClient();
20
+
21
+ // Push notifications — always send (works as ambient alert)
22
+ tasks.push(
23
+ import("./push-notification.service.ts")
24
+ .then(({ pushService }) => pushService.notifyAll(payload.title, payload.body))
25
+ .catch(() => {}),
26
+ );
27
+
28
+ // Telegram — only when user has no active browser session
29
+ if (!userOnline) {
30
+ tasks.push(
31
+ import("./telegram-notification.service.ts")
32
+ .then(({ telegramService }) => telegramService.send(payload))
33
+ .catch(() => {}),
34
+ );
35
+ }
36
+
37
+ await Promise.allSettled(tasks);
38
+ }
39
+ }
40
+
41
+ /** Singleton notification dispatcher */
42
+ export const notificationService = new NotificationService();
@@ -0,0 +1,106 @@
1
+ import { configService } from "./config.service.ts";
2
+ import { tunnelService } from "./tunnel.service.ts";
3
+ import { getLocalIp } from "../lib/network-utils.ts";
4
+ import type { TelegramConfig } from "../types/config.ts";
5
+ import type { NotificationPayload } from "./notification.service.ts";
6
+
7
+ const BOT_TOKEN_RE = /^\d+:[A-Za-z0-9_-]{30,50}$/;
8
+
9
+ /** Escape HTML special chars for Telegram HTML parse mode */
10
+ function escapeHtml(str: string): string {
11
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
12
+ }
13
+
14
+ class TelegramNotificationService {
15
+ /** Send notification to Telegram. No-op if not configured. */
16
+ async send(payload: NotificationPayload): Promise<void> {
17
+ const config = configService.get("telegram") as TelegramConfig | undefined;
18
+ if (!config?.bot_token || !config?.chat_id) return;
19
+ if (!BOT_TOKEN_RE.test(config.bot_token)) return;
20
+
21
+ const deviceName = (configService.get("device_name") as string) || "PPM";
22
+ const deepLink = this.buildDeepLink(payload);
23
+
24
+ let text = `<b>${escapeHtml(deviceName)} — ${escapeHtml(payload.title)}</b>\n`;
25
+ text += escapeHtml(payload.body);
26
+ if (deepLink) {
27
+ text += `\n\n<a href="${deepLink}">Open in PPM</a>`;
28
+ }
29
+
30
+ await this.callApi(config.bot_token, config.chat_id, text);
31
+ }
32
+
33
+ /** Send a test message. Returns { ok, error? } */
34
+ async sendTest(botToken: string, chatId: string): Promise<{ ok: boolean; error?: string }> {
35
+ if (!BOT_TOKEN_RE.test(botToken)) return { ok: false, error: "Invalid bot token format" };
36
+ const deviceName = (configService.get("device_name") as string) || "PPM";
37
+ const text = `<b>${escapeHtml(deviceName)} — Test</b>\nTelegram notifications are working!`;
38
+ const controller = new AbortController();
39
+ const timeout = setTimeout(() => controller.abort(), 10_000);
40
+ try {
41
+ const res = await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
42
+ method: "POST",
43
+ headers: { "Content-Type": "application/json" },
44
+ body: JSON.stringify({ chat_id: chatId, text, parse_mode: "HTML" }),
45
+ signal: controller.signal,
46
+ });
47
+ const json = (await res.json()) as { ok: boolean; description?: string };
48
+ if (!json.ok) return { ok: false, error: json.description || "Unknown error" };
49
+ return { ok: true };
50
+ } catch (e) {
51
+ return { ok: false, error: (e as Error).message };
52
+ } finally {
53
+ clearTimeout(timeout);
54
+ }
55
+ }
56
+
57
+ private buildDeepLink(payload: NotificationPayload): string | null {
58
+ // Prefer tunnel URL (globally accessible), fallback to local IP
59
+ let baseUrl = tunnelService.getTunnelUrl();
60
+ if (!baseUrl) {
61
+ const localIp = getLocalIp();
62
+ const port = configService.get("port") ?? 8080;
63
+ if (localIp) {
64
+ baseUrl = `http://${localIp}:${port}`;
65
+ }
66
+ }
67
+ if (!baseUrl) return null;
68
+
69
+ const projectPath = payload.project
70
+ ? `/project/${encodeURIComponent(payload.project)}`
71
+ : "";
72
+ const query = payload.sessionId ? `?openChat=${payload.sessionId}` : "";
73
+ return `${baseUrl}${projectPath}${query}`;
74
+ }
75
+
76
+ private async callApi(token: string, chatId: string, text: string): Promise<void> {
77
+ if (!BOT_TOKEN_RE.test(token)) return;
78
+ const controller = new AbortController();
79
+ const timeout = setTimeout(() => controller.abort(), 10_000);
80
+
81
+ try {
82
+ const res = await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
83
+ method: "POST",
84
+ headers: { "Content-Type": "application/json" },
85
+ body: JSON.stringify({
86
+ chat_id: chatId,
87
+ text,
88
+ parse_mode: "HTML",
89
+ disable_web_page_preview: true,
90
+ }),
91
+ signal: controller.signal,
92
+ });
93
+ if (!res.ok) {
94
+ const errBody = await res.text();
95
+ console.error(`[telegram] sendMessage failed: ${res.status} ${errBody}`);
96
+ }
97
+ } catch (e) {
98
+ console.error(`[telegram] send error: ${(e as Error).message}`);
99
+ } finally {
100
+ clearTimeout(timeout);
101
+ }
102
+ }
103
+ }
104
+
105
+ /** Singleton Telegram notification service */
106
+ export const telegramService = new TelegramNotificationService();
@@ -4,6 +4,11 @@ export interface PushConfig {
4
4
  vapid_subject: string;
5
5
  }
6
6
 
7
+ export interface TelegramConfig {
8
+ bot_token: string;
9
+ chat_id: string;
10
+ }
11
+
7
12
  export type ThemeConfig = "light" | "dark" | "system";
8
13
 
9
14
  export interface PpmConfig {
@@ -15,6 +20,7 @@ export interface PpmConfig {
15
20
  projects: ProjectConfig[];
16
21
  ai: AIConfig;
17
22
  push?: PushConfig;
23
+ telegram?: TelegramConfig;
18
24
  }
19
25
 
20
26
  export interface AuthConfig {
package/src/web/app.tsx CHANGED
@@ -18,6 +18,7 @@ import { getAuthToken } from "@/lib/api-client";
18
18
  import { useUrlSync, parseUrlState } from "@/hooks/use-url-sync";
19
19
  import { useGlobalKeybindings } from "@/hooks/use-global-keybindings";
20
20
  import { useHealthCheck } from "@/hooks/use-health-check";
21
+ import { useNotificationBadge } from "@/hooks/use-notification-badge";
21
22
  import { CommandPalette } from "@/components/layout/command-palette";
22
23
  import { BugReportPopup } from "@/components/shared/bug-report-popup";
23
24
  import { cn } from "@/lib/utils";
@@ -102,6 +103,19 @@ export function App() {
102
103
  // Health check — detects server crash/restart
103
104
  useHealthCheck();
104
105
 
106
+ // Notification badge — syncs document.title + favicon with unread count
107
+ useNotificationBadge();
108
+
109
+ // Warn before closing browser tab (prevents accidental Ctrl+W)
110
+ useEffect(() => {
111
+ if (authState !== "authenticated") return;
112
+ const handler = (e: BeforeUnloadEvent) => {
113
+ e.preventDefault();
114
+ };
115
+ window.addEventListener("beforeunload", handler);
116
+ return () => window.removeEventListener("beforeunload", handler);
117
+ }, [authState]);
118
+
105
119
  // Load keybindings after auth confirmed (must not call ApiClient before auth)
106
120
  useEffect(() => {
107
121
  if (authState !== "authenticated") return;
@@ -115,7 +129,7 @@ export function App() {
115
129
  if (authState !== "authenticated") return;
116
130
 
117
131
  fetchProjects().then(() => {
118
- const { projectName: urlProject, tabId: urlTab } = initialUrlRef.current;
132
+ const { projectName: urlProject, tabId: urlTab, openChat } = initialUrlRef.current;
119
133
  const { projects, customOrder } = useProjectStore.getState();
120
134
  if (projects.length === 0) return;
121
135
 
@@ -135,6 +149,31 @@ export function App() {
135
149
  });
136
150
  }
137
151
  }
152
+
153
+ // Deep link: ?openChat=sessionId — open/focus the chat tab
154
+ if (openChat) {
155
+ queueMicrotask(() => {
156
+ const { tabs, setActiveTab, openTab } = useTabStore.getState();
157
+ const existing = tabs.find(
158
+ (t) => t.type === "chat" && t.metadata?.sessionId === openChat,
159
+ );
160
+ if (existing) {
161
+ setActiveTab(existing.id);
162
+ } else {
163
+ openTab({
164
+ type: "chat",
165
+ title: "Chat",
166
+ projectId: target?.name ?? null,
167
+ closable: true,
168
+ metadata: { sessionId: openChat },
169
+ });
170
+ }
171
+ // Clean up query param
172
+ const url = new URL(window.location.href);
173
+ url.searchParams.delete("openChat");
174
+ window.history.replaceState(null, "", url.pathname);
175
+ });
176
+ }
138
177
  });
139
178
  }, [authState, fetchProjects]);
140
179