@hienlh/ppm 0.6.7 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -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/server/routes/settings.ts +52 -0
- package/src/server/routes/tunnel.ts +1 -12
- package/src/server/ws/chat.ts +30 -3
- 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
|
+
}
|
|
@@ -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);
|
|
@@ -159,15 +167,34 @@ async function runStreamLoop(sessionId: string, providerId: string, content: str
|
|
|
159
167
|
if (session) session.title = found.summary;
|
|
160
168
|
}
|
|
161
169
|
}).catch(() => {});
|
|
162
|
-
// Fire-and-forget push
|
|
163
|
-
import("../../services/
|
|
170
|
+
// Fire-and-forget notification broadcast (push + telegram)
|
|
171
|
+
import("../../services/notification.service.ts").then(({ notificationService }) => {
|
|
164
172
|
const project = entry.projectName || "Project";
|
|
165
173
|
const session = chatService.getSession(sessionId);
|
|
166
174
|
const sessionTitle = session?.title || `Session ${sessionId.slice(0, 8)}`;
|
|
167
|
-
|
|
175
|
+
notificationService.broadcast("done", {
|
|
176
|
+
title: "Chat completed",
|
|
177
|
+
body: `${project} — ${sessionTitle}`,
|
|
178
|
+
project,
|
|
179
|
+
sessionId,
|
|
180
|
+
sessionTitle,
|
|
181
|
+
});
|
|
168
182
|
}).catch(() => {});
|
|
169
183
|
} else if (evType === "approval_request") {
|
|
170
184
|
entry.pendingApprovalEvent = ev;
|
|
185
|
+
// Fire-and-forget notification for approval/question
|
|
186
|
+
import("../../services/notification.service.ts").then(({ notificationService }) => {
|
|
187
|
+
const project = entry.projectName || "Project";
|
|
188
|
+
const session = chatService.getSession(sessionId);
|
|
189
|
+
const sTitle = session?.title || `Session ${sessionId.slice(0, 8)}`;
|
|
190
|
+
const isQuestion = ev.tool === "AskUserQuestion";
|
|
191
|
+
const nType = isQuestion ? "question" : "approval_request";
|
|
192
|
+
const title = isQuestion ? "AI has a question" : "Waiting for approval";
|
|
193
|
+
const body = isQuestion
|
|
194
|
+
? `${project} — ${sTitle}`
|
|
195
|
+
: `${project} — ${ev.tool} needs permission`;
|
|
196
|
+
notificationService.broadcast(nType as any, { title, body, project, sessionId, sessionTitle: sTitle, tool: ev.tool });
|
|
197
|
+
}).catch(() => {});
|
|
171
198
|
} else {
|
|
172
199
|
logSessionEvent(sessionId, evType.toUpperCase(), JSON.stringify(ev).slice(0, 200));
|
|
173
200
|
}
|
|
@@ -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
|
|
|
@@ -3,12 +3,15 @@ import { X } from "lucide-react";
|
|
|
3
3
|
import type { Tab, TabType } from "@/stores/tab-store";
|
|
4
4
|
import { cn } from "@/lib/utils";
|
|
5
5
|
import { isDarkColor } from "@/lib/color-utils";
|
|
6
|
+
import { notificationColor } from "@/stores/notification-store";
|
|
6
7
|
|
|
7
8
|
interface DraggableTabProps {
|
|
8
9
|
tab: Tab;
|
|
9
10
|
isActive: boolean;
|
|
10
11
|
icon: React.ElementType;
|
|
11
12
|
showDropBefore: boolean;
|
|
13
|
+
/** Notification type if unread (null = no unread). Controls badge color. */
|
|
14
|
+
notificationType?: string | null;
|
|
12
15
|
onSelect: () => void;
|
|
13
16
|
onClose: () => void;
|
|
14
17
|
onDragStart: (e: React.DragEvent) => void;
|
|
@@ -20,7 +23,7 @@ interface DraggableTabProps {
|
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
export function DraggableTab({
|
|
23
|
-
tab, isActive, icon: Icon, showDropBefore, onSelect, onClose,
|
|
26
|
+
tab, isActive, icon: Icon, showDropBefore, notificationType, onSelect, onClose,
|
|
24
27
|
onDragStart, onDragOver, onDragEnd, tabRef, onRename,
|
|
25
28
|
}: DraggableTabProps) {
|
|
26
29
|
const [editing, setEditing] = useState(false);
|
|
@@ -74,7 +77,12 @@ export function DraggableTab({
|
|
|
74
77
|
colorStyle && "border-transparent",
|
|
75
78
|
)}
|
|
76
79
|
>
|
|
77
|
-
<
|
|
80
|
+
<span className="relative">
|
|
81
|
+
<Icon className="size-4" />
|
|
82
|
+
{notificationType && !isActive && (
|
|
83
|
+
<span className={cn("absolute -top-1 -right-1 size-2 rounded-full", notificationColor(notificationType))} />
|
|
84
|
+
)}
|
|
85
|
+
</span>
|
|
78
86
|
{editing ? (
|
|
79
87
|
<input
|
|
80
88
|
ref={inputRef}
|
|
@@ -2,6 +2,7 @@ import { useState, useEffect, useRef, useCallback } from "react";
|
|
|
2
2
|
import {
|
|
3
3
|
Terminal, MessageSquare, GitBranch, Database,
|
|
4
4
|
FileDiff, FileCode, Settings, Menu, X, ArrowLeft, ArrowRight, SplitSquareVertical, MoveVertical, Layers, Plus,
|
|
5
|
+
ChevronLeft, ChevronRight,
|
|
5
6
|
} from "lucide-react";
|
|
6
7
|
import { usePanelStore } from "@/stores/panel-store";
|
|
7
8
|
import { useProjectStore, resolveOrder } from "@/stores/project-store";
|
|
@@ -11,6 +12,8 @@ import { getProjectInitials } from "@/lib/project-avatar";
|
|
|
11
12
|
import type { TabType } from "@/stores/tab-store";
|
|
12
13
|
import { cn } from "@/lib/utils";
|
|
13
14
|
import { openCommandPalette } from "@/hooks/use-global-keybindings";
|
|
15
|
+
import { useNotificationStore, notificationColor } from "@/stores/notification-store";
|
|
16
|
+
import { useTabOverflow, getHiddenUnreadDirection } from "@/hooks/use-tab-overflow";
|
|
14
17
|
|
|
15
18
|
const NEW_TAB_OPTIONS: { type: TabType; label: string }[] = [
|
|
16
19
|
{ type: "terminal", label: "Terminal" },
|
|
@@ -35,7 +38,12 @@ export function MobileNav({ onMenuPress, onProjectsPress }: MobileNavProps) {
|
|
|
35
38
|
const tabs = panel?.tabs ?? [];
|
|
36
39
|
const activeTabId = panel?.activeTabId ?? null;
|
|
37
40
|
const tabRefs = useRef<Map<string, HTMLButtonElement>>(new Map());
|
|
41
|
+
const mobileScrollRef = useRef<HTMLDivElement>(null);
|
|
38
42
|
const prevTabCount = useRef(tabs.length);
|
|
43
|
+
const notifications = useNotificationStore((s) => s.notifications);
|
|
44
|
+
const { canScrollLeft, canScrollRight, scrollLeft: doScrollLeft, scrollRight: doScrollRight } =
|
|
45
|
+
useTabOverflow(mobileScrollRef);
|
|
46
|
+
const hiddenUnread = getHiddenUnreadDirection(mobileScrollRef.current, tabRefs.current as Map<string, HTMLElement>, tabs, notifications);
|
|
39
47
|
|
|
40
48
|
const [menuTabId, setMenuTabId] = useState<string | null>(null);
|
|
41
49
|
const [newTabSheetOpen, setNewTabSheetOpen] = useState(false);
|
|
@@ -109,15 +117,31 @@ export function MobileNav({ onMenuPress, onProjectsPress }: MobileNavProps) {
|
|
|
109
117
|
<Menu className="size-5" />
|
|
110
118
|
</button>
|
|
111
119
|
|
|
112
|
-
<div className="flex-1 flex items-center h-12
|
|
120
|
+
<div className="flex-1 relative flex items-center h-12">
|
|
121
|
+
{/* Left scroll arrow */}
|
|
122
|
+
{canScrollLeft && (
|
|
123
|
+
<button onClick={doScrollLeft} className="absolute left-0 z-10 flex items-center justify-center size-8 bg-gradient-to-r from-background via-background to-transparent">
|
|
124
|
+
<span className="relative">
|
|
125
|
+
<ChevronLeft className="size-3.5 text-text-secondary" />
|
|
126
|
+
{hiddenUnread.left && <span className={cn("absolute -top-1 -right-0.5 size-1.5 rounded-full", notificationColor(hiddenUnread.left))} />}
|
|
127
|
+
</span>
|
|
128
|
+
</button>
|
|
129
|
+
)}
|
|
130
|
+
<div ref={mobileScrollRef} className="flex-1 flex items-center h-12 overflow-x-auto scrollbar-none">
|
|
113
131
|
{tabs.map((tab) => {
|
|
114
132
|
const Icon = TAB_ICONS[tab.type];
|
|
115
133
|
const isActive = tab.id === activeTabId;
|
|
134
|
+
const sessionId = tab.type === "chat" ? (tab.metadata?.sessionId as string) : undefined;
|
|
135
|
+
const entry = sessionId ? notifications.get(sessionId) : undefined;
|
|
136
|
+
const notiType = entry && entry.count > 0 ? entry.type : null;
|
|
116
137
|
return (
|
|
117
138
|
<button
|
|
118
139
|
key={tab.id}
|
|
119
140
|
ref={(el) => { if (el) tabRefs.current.set(tab.id, el); else tabRefs.current.delete(tab.id); }}
|
|
120
|
-
onClick={() =>
|
|
141
|
+
onClick={() => {
|
|
142
|
+
usePanelStore.getState().setActiveTab(tab.id);
|
|
143
|
+
if (sessionId) useNotificationStore.getState().clearForSession(sessionId);
|
|
144
|
+
}}
|
|
121
145
|
onTouchStart={() => startLongPress(tab.id)}
|
|
122
146
|
onTouchEnd={cancelLongPress}
|
|
123
147
|
onTouchMove={cancelLongPress}
|
|
@@ -127,7 +151,12 @@ export function MobileNav({ onMenuPress, onProjectsPress }: MobileNavProps) {
|
|
|
127
151
|
isActive ? "border-primary bg-surface text-primary" : "border-transparent text-text-secondary",
|
|
128
152
|
)}
|
|
129
153
|
>
|
|
130
|
-
<
|
|
154
|
+
<span className="relative">
|
|
155
|
+
<Icon className="size-4" />
|
|
156
|
+
{notiType && !isActive && (
|
|
157
|
+
<span className={cn("absolute -top-1 -right-1 size-2 rounded-full", notificationColor(notiType))} />
|
|
158
|
+
)}
|
|
159
|
+
</span>
|
|
131
160
|
<span className="max-w-[80px] truncate">{tab.title}</span>
|
|
132
161
|
{tab.closable && (
|
|
133
162
|
<span role="button" tabIndex={0} onClick={(e) => { e.stopPropagation(); usePanelStore.getState().closeTab(tab.id); }}
|
|
@@ -138,6 +167,16 @@ export function MobileNav({ onMenuPress, onProjectsPress }: MobileNavProps) {
|
|
|
138
167
|
</button>
|
|
139
168
|
);
|
|
140
169
|
})}
|
|
170
|
+
</div>
|
|
171
|
+
{/* Right scroll arrow */}
|
|
172
|
+
{canScrollRight && (
|
|
173
|
+
<button onClick={doScrollRight} className="absolute right-0 z-10 flex items-center justify-center size-8 bg-gradient-to-l from-background via-background to-transparent">
|
|
174
|
+
<span className="relative">
|
|
175
|
+
<ChevronRight className="size-3.5 text-text-secondary" />
|
|
176
|
+
{hiddenUnread.right && <span className={cn("absolute -top-1 -left-0.5 size-1.5 rounded-full", notificationColor(hiddenUnread.right))} />}
|
|
177
|
+
</span>
|
|
178
|
+
</button>
|
|
179
|
+
)}
|
|
141
180
|
</div>
|
|
142
181
|
|
|
143
182
|
{/* Add tab — opens command palette */}
|