@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.
- package/CHANGELOG.md +24 -0
- package/README.md +86 -313
- package/dist/web/assets/chat-tab-CbNbBMGw.js +7 -0
- package/dist/web/assets/{code-editor-BUg7alP6.js → code-editor-D6OuzcC-.js} +1 -1
- package/dist/web/assets/{database-viewer-CAgZOkZc.js → database-viewer-BxUpM_uA.js} +1 -1
- package/dist/web/assets/{diff-viewer-DVvY1aFb.js → diff-viewer-DAhrHpNM.js} +1 -1
- package/dist/web/assets/{dist-Jb3Tnkpc.js → dist-CNRrBoQi.js} +14 -14
- package/dist/web/assets/git-graph-BpTt5iOd.js +1 -0
- package/dist/web/assets/index-BU_07_oW.js +29 -0
- package/dist/web/assets/index-CBQhXXeV.css +2 -0
- package/dist/web/assets/keybindings-store-C0m8_V9X.js +1 -0
- package/dist/web/assets/{markdown-renderer-z99RjIxZ.js → markdown-renderer-CvGYO9sH.js} +2 -2
- package/dist/web/assets/postgres-viewer-BL99auSm.js +1 -0
- package/dist/web/assets/{settings-tab-BnDkeQWk.js → settings-tab-Bwsxb41F.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-EwHWc37J.js → sqlite-viewer-DfgaCbWT.js} +1 -1
- package/dist/web/assets/{terminal-tab-CTN18lb6.js → terminal-tab-D27e4ZTD.js} +2 -2
- package/dist/web/index.html +4 -3
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/src/lib/network-utils.ts +12 -0
- package/src/providers/claude-agent-sdk.ts +61 -3
- package/src/server/routes/chat.ts +5 -1
- package/src/server/routes/settings.ts +52 -0
- package/src/server/routes/tunnel.ts +1 -12
- package/src/server/ws/chat.ts +42 -12
- package/src/services/config.service.ts +1 -1
- package/src/services/notification.service.ts +42 -0
- package/src/services/telegram-notification.service.ts +106 -0
- package/src/types/config.ts +6 -0
- package/src/web/app.tsx +40 -1
- package/src/web/components/layout/draggable-tab.tsx +10 -2
- package/src/web/components/layout/mobile-nav.tsx +42 -3
- package/src/web/components/layout/project-bar.tsx +16 -8
- package/src/web/components/layout/tab-bar.tsx +55 -4
- package/src/web/components/settings/settings-tab.tsx +135 -94
- package/src/web/components/settings/telegram-settings-section.tsx +113 -0
- package/src/web/components/ui/accordion.tsx +64 -0
- package/src/web/hooks/use-chat.ts +29 -0
- package/src/web/hooks/use-notification-badge.ts +20 -0
- package/src/web/hooks/use-tab-overflow.ts +91 -0
- package/src/web/hooks/use-url-sync.ts +5 -2
- package/src/web/index.html +1 -0
- package/src/web/lib/favicon.ts +21 -0
- package/src/web/lib/notification-sounds.ts +61 -0
- package/src/web/stores/notification-store.ts +83 -0
- package/dist/web/assets/chat-tab-CWhxhPKH.js +0 -7
- package/dist/web/assets/git-graph-xD6TLRVv.js +0 -1
- package/dist/web/assets/index-CigdXBuQ.css +0 -2
- package/dist/web/assets/index-DBdw8tN_.js +0 -22
- package/dist/web/assets/keybindings-store-kHLASnRb.js +0 -1
- package/dist/web/assets/postgres-viewer-CaMySHpD.js +0 -1
- /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
|
@@ -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 (
|
|
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(
|
|
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 */
|
package/src/server/ws/chat.ts
CHANGED
|
@@ -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
|
-
|
|
156
|
-
|
|
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 =
|
|
168
|
+
if (session) session.title = title;
|
|
160
169
|
}
|
|
161
170
|
}).catch(() => {});
|
|
162
|
-
// Fire-and-forget push
|
|
163
|
-
import("../../services/
|
|
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
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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();
|
package/src/types/config.ts
CHANGED
|
@@ -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
|
|