@hienlh/ppm 0.4.4 → 0.5.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 +26 -2
- package/bun.lock +3 -0
- package/dist/web/assets/api-client-ANLU-Irq.js +1 -0
- package/dist/web/assets/chat-tab-C6iTYbRI.js +7 -0
- package/dist/web/assets/code-editor-hnDDc8JZ.js +1 -0
- package/dist/web/assets/{diff-viewer-B9oX4DDx.js → diff-viewer-BWeMVAvK.js} +1 -1
- package/dist/web/assets/git-graph-D6oftHHC.js +1 -0
- package/dist/web/assets/index-CWwJBtaO.js +21 -0
- package/dist/web/assets/index-jmj5f_bQ.css +2 -0
- package/dist/web/assets/{input-AESbQWjx.js → input-D-F4ITU0.js} +1 -1
- package/dist/web/assets/jsx-runtime-B4BJKQ1u.js +1 -0
- package/dist/web/assets/{markdown-renderer-DdDDhQDx.js → markdown-renderer-PHBaNQ3l.js} +2 -2
- package/dist/web/assets/react-WvgCEYPV.js +1 -0
- package/dist/web/assets/rotate-ccw-BesidNnx.js +1 -0
- package/dist/web/assets/settings-store-CGtTcr8r.js +1 -0
- package/dist/web/assets/settings-tab-BpETyigv.js +1 -0
- package/dist/web/assets/tab-store-Dq1kMOkJ.js +1 -0
- package/dist/web/assets/{terminal-tab-BeFf07MH.js → terminal-tab-BTumEYyO.js} +2 -2
- package/dist/web/assets/{use-monaco-theme-Bb9W0CI2.js → use-monaco-theme-CsNwoeyj.js} +1 -1
- package/dist/web/index.html +8 -7
- package/dist/web/sw.js +1 -1
- package/package.json +2 -1
- package/src/cli/commands/stop.ts +8 -0
- package/src/providers/claude-agent-sdk.ts +36 -13
- package/src/server/index.ts +15 -0
- package/src/server/routes/chat.ts +31 -3
- package/src/server/ws/chat.ts +40 -0
- package/src/services/claude-usage.service.ts +51 -23
- package/src/services/tunnel.service.ts +4 -0
- package/src/types/api.ts +1 -0
- package/src/types/chat.ts +1 -0
- package/src/web/app.tsx +5 -0
- package/src/web/components/chat/chat-history-bar.tsx +15 -3
- package/src/web/components/chat/chat-tab.tsx +45 -50
- package/src/web/components/chat/message-input.tsx +116 -55
- package/src/web/components/chat/message-list.tsx +156 -69
- package/src/web/components/chat/usage-badge.tsx +4 -4
- package/src/web/components/layout/command-palette.tsx +37 -8
- package/src/web/components/layout/draggable-tab.tsx +4 -4
- package/src/web/components/layout/mobile-drawer.tsx +2 -2
- package/src/web/components/layout/mobile-nav.tsx +3 -2
- package/src/web/components/layout/project-bar.tsx +5 -3
- package/src/web/components/layout/project-bottom-sheet.tsx +3 -1
- package/src/web/components/layout/tab-bar.tsx +4 -4
- package/src/web/components/shared/bug-report-popup.tsx +58 -0
- package/src/web/hooks/use-chat.ts +63 -7
- package/src/web/hooks/use-usage.ts +15 -17
- package/src/web/lib/report-bug.ts +12 -3
- package/src/web/stores/project-store.ts +7 -1
- package/vite.config.ts +2 -0
- package/dist/web/assets/api-client-BsHoRDAn.js +0 -1
- package/dist/web/assets/chat-tab-Bj1hZQ4x.js +0 -6
- package/dist/web/assets/code-editor-Bj9jdnLm.js +0 -1
- package/dist/web/assets/copy-BNk4Z75P.js +0 -1
- package/dist/web/assets/external-link-CrtbmtJ6.js +0 -1
- package/dist/web/assets/git-graph-DoLRBTMk.js +0 -1
- package/dist/web/assets/index-C_yeSRZ0.css +0 -2
- package/dist/web/assets/index-D27GI6gs.js +0 -21
- package/dist/web/assets/jsx-runtime-BFALxl05.js +0 -1
- package/dist/web/assets/settings-store-DWYkr_a3.js +0 -1
- package/dist/web/assets/settings-tab-BLoiK6Nc.js +0 -1
- package/dist/web/assets/tab-store-B1wzyDLQ.js +0 -1
package/dist/web/sw.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
try{self[`workbox:core:7.3.0`]&&_()}catch{}var e=(e,...t)=>{let n=e;return t.length>0&&(n+=` :: ${JSON.stringify(t)}`),n},t=class extends Error{constructor(t,n){let r=e(t,n);super(r),this.name=t,this.details=n}},n={googleAnalytics:`googleAnalytics`,precache:`precache-v2`,prefix:`workbox`,runtime:`runtime`,suffix:typeof registration<`u`?registration.scope:``},r=e=>[n.prefix,e,n.suffix].filter(e=>e&&e.length>0).join(`-`),i=e=>{for(let t of Object.keys(n))e(t)},a={updateDetails:e=>{i(t=>{typeof e[t]==`string`&&(n[t]=e[t])})},getGoogleAnalyticsName:e=>e||r(n.googleAnalytics),getPrecacheName:e=>e||r(n.precache),getPrefix:()=>n.prefix,getRuntimeName:e=>e||r(n.runtime),getSuffix:()=>n.suffix};function o(e,t){let n=t();return e.waitUntil(n),n}try{self[`workbox:precaching:7.3.0`]&&_()}catch{}var s=`__WB_REVISION__`;function c(e){if(!e)throw new t(`add-to-cache-list-unexpected-type`,{entry:e});if(typeof e==`string`){let t=new URL(e,location.href);return{cacheKey:t.href,url:t.href}}let{revision:n,url:r}=e;if(!r)throw new t(`add-to-cache-list-unexpected-type`,{entry:e});if(!n){let e=new URL(r,location.href);return{cacheKey:e.href,url:e.href}}let i=new URL(r,location.href),a=new URL(r,location.href);return i.searchParams.set(s,n),{cacheKey:i.href,url:a.href}}var l=class{constructor(){this.updatedURLs=[],this.notUpdatedURLs=[],this.handlerWillStart=async({request:e,state:t})=>{t&&(t.originalRequest=e)},this.cachedResponseWillBeUsed=async({event:e,state:t,cachedResponse:n})=>{if(e.type===`install`&&t&&t.originalRequest&&t.originalRequest instanceof Request){let e=t.originalRequest.url;n?this.notUpdatedURLs.push(e):this.updatedURLs.push(e)}return n}}},u=class{constructor({precacheController:e}){this.cacheKeyWillBeUsed=async({request:e,params:t})=>{let n=t?.cacheKey||this._precacheController.getCacheKeyForURL(e.url);return n?new Request(n,{headers:e.headers}):e},this._precacheController=e}},d;function f(){if(d===void 0){let e=new Response(``);if(`body`in e)try{new Response(e.body),d=!0}catch{d=!1}d=!1}return d}async function p(e,n){let r=null;if(e.url&&(r=new URL(e.url).origin),r!==self.location.origin)throw new t(`cross-origin-copy-response`,{origin:r});let i=e.clone(),a={headers:new Headers(i.headers),status:i.status,statusText:i.statusText},o=n?n(a):a,s=f()?i.body:await i.blob();return new Response(s,o)}var m=e=>new URL(String(e),location.href).href.replace(RegExp(`^${location.origin}`),``);function h(e,t){let n=new URL(e);for(let e of t)n.searchParams.delete(e);return n.href}async function g(e,t,n,r){let i=h(t.url,n);if(t.url===i)return e.match(t,r);let a=Object.assign(Object.assign({},r),{ignoreSearch:!0}),o=await e.keys(t,a);for(let t of o)if(i===h(t.url,n))return e.match(t,r)}var v=class{constructor(){this.promise=new Promise((e,t)=>{this.resolve=e,this.reject=t})}},y=new Set;async function b(){for(let e of y)await e()}function x(e){return new Promise(t=>setTimeout(t,e))}try{self[`workbox:strategies:7.3.0`]&&_()}catch{}function S(e){return typeof e==`string`?new Request(e):e}var C=class{constructor(e,t){this._cacheKeys={},Object.assign(this,t),this.event=t.event,this._strategy=e,this._handlerDeferred=new v,this._extendLifetimePromises=[],this._plugins=[...e.plugins],this._pluginStateMap=new Map;for(let e of this._plugins)this._pluginStateMap.set(e,{});this.event.waitUntil(this._handlerDeferred.promise)}async fetch(e){let{event:n}=this,r=S(e);if(r.mode===`navigate`&&n instanceof FetchEvent&&n.preloadResponse){let e=await n.preloadResponse;if(e)return e}let i=this.hasCallback(`fetchDidFail`)?r.clone():null;try{for(let e of this.iterateCallbacks(`requestWillFetch`))r=await e({request:r.clone(),event:n})}catch(e){if(e instanceof Error)throw new t(`plugin-error-request-will-fetch`,{thrownErrorMessage:e.message})}let a=r.clone();try{let e;e=await fetch(r,r.mode===`navigate`?void 0:this._strategy.fetchOptions);for(let t of this.iterateCallbacks(`fetchDidSucceed`))e=await t({event:n,request:a,response:e});return e}catch(e){throw i&&await this.runCallbacks(`fetchDidFail`,{error:e,event:n,originalRequest:i.clone(),request:a.clone()}),e}}async fetchAndCachePut(e){let t=await this.fetch(e),n=t.clone();return this.waitUntil(this.cachePut(e,n)),t}async cacheMatch(e){let t=S(e),n,{cacheName:r,matchOptions:i}=this._strategy,a=await this.getCacheKey(t,`read`),o=Object.assign(Object.assign({},i),{cacheName:r});n=await caches.match(a,o);for(let e of this.iterateCallbacks(`cachedResponseWillBeUsed`))n=await e({cacheName:r,matchOptions:i,cachedResponse:n,request:a,event:this.event})||void 0;return n}async cachePut(e,n){let r=S(e);await x(0);let i=await this.getCacheKey(r,`write`);if(!n)throw new t(`cache-put-with-no-response`,{url:m(i.url)});let a=await this._ensureResponseSafeToCache(n);if(!a)return!1;let{cacheName:o,matchOptions:s}=this._strategy,c=await self.caches.open(o),l=this.hasCallback(`cacheDidUpdate`),u=l?await g(c,i.clone(),[`__WB_REVISION__`],s):null;try{await c.put(i,l?a.clone():a)}catch(e){if(e instanceof Error)throw e.name===`QuotaExceededError`&&await b(),e}for(let e of this.iterateCallbacks(`cacheDidUpdate`))await e({cacheName:o,oldResponse:u,newResponse:a.clone(),request:i,event:this.event});return!0}async getCacheKey(e,t){let n=`${e.url} | ${t}`;if(!this._cacheKeys[n]){let r=e;for(let e of this.iterateCallbacks(`cacheKeyWillBeUsed`))r=S(await e({mode:t,request:r,event:this.event,params:this.params}));this._cacheKeys[n]=r}return this._cacheKeys[n]}hasCallback(e){for(let t of this._strategy.plugins)if(e in t)return!0;return!1}async runCallbacks(e,t){for(let n of this.iterateCallbacks(e))await n(t)}*iterateCallbacks(e){for(let t of this._strategy.plugins)if(typeof t[e]==`function`){let n=this._pluginStateMap.get(t);yield r=>{let i=Object.assign(Object.assign({},r),{state:n});return t[e](i)}}}waitUntil(e){return this._extendLifetimePromises.push(e),e}async doneWaiting(){for(;this._extendLifetimePromises.length;){let e=this._extendLifetimePromises.splice(0),t=(await Promise.allSettled(e)).find(e=>e.status===`rejected`);if(t)throw t.reason}}destroy(){this._handlerDeferred.resolve(null)}async _ensureResponseSafeToCache(e){let t=e,n=!1;for(let e of this.iterateCallbacks(`cacheWillUpdate`))if(t=await e({request:this.request,response:t,event:this.event})||void 0,n=!0,!t)break;return n||t&&t.status!==200&&(t=void 0),t}},w=class{constructor(e={}){this.cacheName=a.getRuntimeName(e.cacheName),this.plugins=e.plugins||[],this.fetchOptions=e.fetchOptions,this.matchOptions=e.matchOptions}handle(e){let[t]=this.handleAll(e);return t}handleAll(e){e instanceof FetchEvent&&(e={event:e,request:e.request});let t=e.event,n=typeof e.request==`string`?new Request(e.request):e.request,r=`params`in e?e.params:void 0,i=new C(this,{event:t,request:n,params:r}),a=this._getResponse(i,n,t);return[a,this._awaitComplete(a,i,n,t)]}async _getResponse(e,n,r){await e.runCallbacks(`handlerWillStart`,{event:r,request:n});let i;try{if(i=await this._handle(n,e),!i||i.type===`error`)throw new t(`no-response`,{url:n.url})}catch(t){if(t instanceof Error){for(let a of e.iterateCallbacks(`handlerDidError`))if(i=await a({error:t,event:r,request:n}),i)break}if(!i)throw t}for(let t of e.iterateCallbacks(`handlerWillRespond`))i=await t({event:r,request:n,response:i});return i}async _awaitComplete(e,t,n,r){let i,a;try{i=await e}catch{}try{await t.runCallbacks(`handlerDidRespond`,{event:r,request:n,response:i}),await t.doneWaiting()}catch(e){e instanceof Error&&(a=e)}if(await t.runCallbacks(`handlerDidComplete`,{event:r,request:n,response:i,error:a}),t.destroy(),a)throw a}},T=class e extends w{constructor(t={}){t.cacheName=a.getPrecacheName(t.cacheName),super(t),this._fallbackToNetwork=t.fallbackToNetwork!==!1,this.plugins.push(e.copyRedirectedCacheableResponsesPlugin)}async _handle(e,t){return await t.cacheMatch(e)||(t.event&&t.event.type===`install`?await this._handleInstall(e,t):await this._handleFetch(e,t))}async _handleFetch(e,n){let r,i=n.params||{};if(this._fallbackToNetwork){let t=i.integrity,a=e.integrity,o=!a||a===t;r=await n.fetch(new Request(e,{integrity:e.mode===`no-cors`?void 0:a||t})),t&&o&&e.mode!==`no-cors`&&(this._useDefaultCacheabilityPluginIfNeeded(),await n.cachePut(e,r.clone()))}else throw new t(`missing-precache-entry`,{cacheName:this.cacheName,url:e.url});return r}async _handleInstall(e,n){this._useDefaultCacheabilityPluginIfNeeded();let r=await n.fetch(e);if(!await n.cachePut(e,r.clone()))throw new t(`bad-precaching-response`,{url:e.url,status:r.status});return r}_useDefaultCacheabilityPluginIfNeeded(){let t=null,n=0;for(let[r,i]of this.plugins.entries())i!==e.copyRedirectedCacheableResponsesPlugin&&(i===e.defaultPrecacheCacheabilityPlugin&&(t=r),i.cacheWillUpdate&&n++);n===0?this.plugins.push(e.defaultPrecacheCacheabilityPlugin):n>1&&t!==null&&this.plugins.splice(t,1)}};T.defaultPrecacheCacheabilityPlugin={async cacheWillUpdate({response:e}){return!e||e.status>=400?null:e}},T.copyRedirectedCacheableResponsesPlugin={async cacheWillUpdate({response:e}){return e.redirected?await p(e):e}};var E=class{constructor({cacheName:e,plugins:t=[],fallbackToNetwork:n=!0}={}){this._urlsToCacheKeys=new Map,this._urlsToCacheModes=new Map,this._cacheKeysToIntegrities=new Map,this._strategy=new T({cacheName:a.getPrecacheName(e),plugins:[...t,new u({precacheController:this})],fallbackToNetwork:n}),this.install=this.install.bind(this),this.activate=this.activate.bind(this)}get strategy(){return this._strategy}precache(e){this.addToCacheList(e),this._installAndActiveListenersAdded||=(self.addEventListener(`install`,this.install),self.addEventListener(`activate`,this.activate),!0)}addToCacheList(e){let n=[];for(let r of e){typeof r==`string`?n.push(r):r&&r.revision===void 0&&n.push(r.url);let{cacheKey:e,url:i}=c(r),a=typeof r!=`string`&&r.revision?`reload`:`default`;if(this._urlsToCacheKeys.has(i)&&this._urlsToCacheKeys.get(i)!==e)throw new t(`add-to-cache-list-conflicting-entries`,{firstEntry:this._urlsToCacheKeys.get(i),secondEntry:e});if(typeof r!=`string`&&r.integrity){if(this._cacheKeysToIntegrities.has(e)&&this._cacheKeysToIntegrities.get(e)!==r.integrity)throw new t(`add-to-cache-list-conflicting-integrities`,{url:i});this._cacheKeysToIntegrities.set(e,r.integrity)}if(this._urlsToCacheKeys.set(i,e),this._urlsToCacheModes.set(i,a),n.length>0){let e=`Workbox is precaching URLs without revision info: ${n.join(`, `)}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(e)}}}install(e){return o(e,async()=>{let t=new l;this.strategy.plugins.push(t);for(let[t,n]of this._urlsToCacheKeys){let r=this._cacheKeysToIntegrities.get(n),i=this._urlsToCacheModes.get(t),a=new Request(t,{integrity:r,cache:i,credentials:`same-origin`});await Promise.all(this.strategy.handleAll({params:{cacheKey:n},request:a,event:e}))}let{updatedURLs:n,notUpdatedURLs:r}=t;return{updatedURLs:n,notUpdatedURLs:r}})}activate(e){return o(e,async()=>{let e=await self.caches.open(this.strategy.cacheName),t=await e.keys(),n=new Set(this._urlsToCacheKeys.values()),r=[];for(let i of t)n.has(i.url)||(await e.delete(i),r.push(i.url));return{deletedURLs:r}})}getURLsToCacheKeys(){return this._urlsToCacheKeys}getCachedURLs(){return[...this._urlsToCacheKeys.keys()]}getCacheKeyForURL(e){let t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForCacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){let t=e instanceof Request?e.url:e,n=this.getCacheKeyForURL(t);if(n)return(await self.caches.open(this.strategy.cacheName)).match(n)}createHandlerBoundToURL(e){let n=this.getCacheKeyForURL(e);if(!n)throw new t(`non-precached-url`,{url:e});return t=>(t.request=new Request(e),t.params=Object.assign({cacheKey:n},t.params),this.strategy.handle(t))}},D,O=()=>(D||=new E,D);try{self[`workbox:routing:7.3.0`]&&_()}catch{}var k=e=>e&&typeof e==`object`?e:{handle:e},A=class{constructor(e,t,n=`GET`){this.handler=k(t),this.match=e,this.method=n}setCatchHandler(e){this.catchHandler=k(e)}},j=class extends A{constructor(e,t,n){super(({url:t})=>{let n=e.exec(t.href);if(n&&!(t.origin!==location.origin&&n.index!==0))return n.slice(1)},t,n)}},M=class{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener(`fetch`,(e=>{let{request:t}=e,n=this.handleRequest({request:t,event:e});n&&e.respondWith(n)}))}addCacheListener(){self.addEventListener(`message`,(e=>{if(e.data&&e.data.type===`CACHE_URLS`){let{payload:t}=e.data,n=Promise.all(t.urlsToCache.map(t=>{typeof t==`string`&&(t=[t]);let n=new Request(...t);return this.handleRequest({request:n,event:e})}));e.waitUntil(n),e.ports&&e.ports[0]&&n.then(()=>e.ports[0].postMessage(!0))}}))}handleRequest({request:e,event:t}){let n=new URL(e.url,location.href);if(!n.protocol.startsWith(`http`))return;let r=n.origin===location.origin,{params:i,route:a}=this.findMatchingRoute({event:t,request:e,sameOrigin:r,url:n}),o=a&&a.handler,s=e.method;if(!o&&this._defaultHandlerMap.has(s)&&(o=this._defaultHandlerMap.get(s)),!o)return;let c;try{c=o.handle({url:n,request:e,event:t,params:i})}catch(e){c=Promise.reject(e)}let l=a&&a.catchHandler;return c instanceof Promise&&(this._catchHandler||l)&&(c=c.catch(async r=>{if(l)try{return await l.handle({url:n,request:e,event:t,params:i})}catch(e){e instanceof Error&&(r=e)}if(this._catchHandler)return this._catchHandler.handle({url:n,request:e,event:t});throw r})),c}findMatchingRoute({url:e,sameOrigin:t,request:n,event:r}){let i=this._routes.get(n.method)||[];for(let a of i){let i,o=a.match({url:e,sameOrigin:t,request:n,event:r});if(o)return i=o,(Array.isArray(i)&&i.length===0||o.constructor===Object&&Object.keys(o).length===0||typeof o==`boolean`)&&(i=void 0),{route:a,params:i}}return{}}setDefaultHandler(e,t=`GET`){this._defaultHandlerMap.set(t,k(e))}setCatchHandler(e){this._catchHandler=k(e)}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new t(`unregister-route-but-not-found-with-method`,{method:e.method});let n=this._routes.get(e.method).indexOf(e);if(n>-1)this._routes.get(e.method).splice(n,1);else throw new t(`unregister-route-route-not-registered`)}},N,P=()=>(N||(N=new M,N.addFetchListener(),N.addCacheListener()),N);function F(e,n,r){let i;if(typeof e==`string`){let t=new URL(e,location.href);i=new A(({url:e})=>e.href===t.href,n,r)}else if(e instanceof RegExp)i=new j(e,n,r);else if(typeof e==`function`)i=new A(e,n,r);else if(e instanceof A)i=e;else throw new t(`unsupported-route-type`,{moduleName:`workbox-routing`,funcName:`registerRoute`,paramName:`capture`});return P().registerRoute(i),i}function I(e,t=[]){for(let n of[...e.searchParams.keys()])t.some(e=>e.test(n))&&e.searchParams.delete(n);return e}function*L(e,{ignoreURLParametersMatching:t=[/^utm_/,/^fbclid$/],directoryIndex:n=`index.html`,cleanURLs:r=!0,urlManipulation:i}={}){let a=new URL(e,location.href);a.hash=``,yield a.href;let o=I(a,t);if(yield o.href,n&&o.pathname.endsWith(`/`)){let e=new URL(o.href);e.pathname+=n,yield e.href}if(r){let e=new URL(o.href);e.pathname+=`.html`,yield e.href}if(i){let e=i({url:a});for(let t of e)yield t.href}}var R=class extends A{constructor(e,t){super(({request:n})=>{let r=e.getURLsToCacheKeys();for(let i of L(n.url,t)){let t=r.get(i);if(t)return{cacheKey:t,integrity:e.getIntegrityForCacheKey(t)}}},e.strategy)}};function z(e){F(new R(O(),e))}function B(e){O().precache(e)}function V(e,t){B(e),z(t)}V([{"revision":"1872c500de691dce40960bb85481de07","url":"registerSW.js"},{"revision":"b4776ca319ebb6d86f9ea3fe28420e73","url":"index.html"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-512.svg"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-192.svg"},{"revision":null,"url":"assets/utils-bntUtdc7.js"},{"revision":null,"url":"assets/use-monaco-theme-Bb9W0CI2.js"},{"revision":null,"url":"assets/terminal-tab-BrP-ENHg.css"},{"revision":null,"url":"assets/terminal-tab-BeFf07MH.js"},{"revision":null,"url":"assets/tab-store-B1wzyDLQ.js"},{"revision":null,"url":"assets/settings-tab-BLoiK6Nc.js"},{"revision":null,"url":"assets/settings-store-DWYkr_a3.js"},{"revision":null,"url":"assets/markdown-renderer-DdDDhQDx.js"},{"revision":null,"url":"assets/jsx-runtime-BFALxl05.js"},{"revision":null,"url":"assets/input-AESbQWjx.js"},{"revision":null,"url":"assets/index-D27GI6gs.js"},{"revision":null,"url":"assets/index-C_yeSRZ0.css"},{"revision":null,"url":"assets/git-graph-DoLRBTMk.js"},{"revision":null,"url":"assets/external-link-CrtbmtJ6.js"},{"revision":null,"url":"assets/diff-viewer-B9oX4DDx.js"},{"revision":null,"url":"assets/copy-BNk4Z75P.js"},{"revision":null,"url":"assets/code-editor-Bj9jdnLm.js"},{"revision":null,"url":"assets/chat-tab-Bj1hZQ4x.js"},{"revision":null,"url":"assets/api-client-BsHoRDAn.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":"1612fbe0df1759c9a202723adbeeb469","url":"index.html"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-512.svg"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-192.svg"},{"revision":null,"url":"assets/utils-bntUtdc7.js"},{"revision":null,"url":"assets/use-monaco-theme-CsNwoeyj.js"},{"revision":null,"url":"assets/terminal-tab-BrP-ENHg.css"},{"revision":null,"url":"assets/terminal-tab-BTumEYyO.js"},{"revision":null,"url":"assets/tab-store-Dq1kMOkJ.js"},{"revision":null,"url":"assets/settings-tab-BpETyigv.js"},{"revision":null,"url":"assets/settings-store-CGtTcr8r.js"},{"revision":null,"url":"assets/rotate-ccw-BesidNnx.js"},{"revision":null,"url":"assets/react-WvgCEYPV.js"},{"revision":null,"url":"assets/markdown-renderer-PHBaNQ3l.js"},{"revision":null,"url":"assets/jsx-runtime-B4BJKQ1u.js"},{"revision":null,"url":"assets/input-D-F4ITU0.js"},{"revision":null,"url":"assets/index-jmj5f_bQ.css"},{"revision":null,"url":"assets/index-CWwJBtaO.js"},{"revision":null,"url":"assets/git-graph-D6oftHHC.js"},{"revision":null,"url":"assets/diff-viewer-BWeMVAvK.js"},{"revision":null,"url":"assets/code-editor-hnDDc8JZ.js"},{"revision":null,"url":"assets/chat-tab-C6iTYbRI.js"},{"revision":null,"url":"assets/api-client-ANLU-Irq.js"},{"revision":"79c8870653c8f419f2e3323085e1f4be","url":"manifest.webmanifest"}]),self.addEventListener(`push`,e=>{e.waitUntil(self.clients.matchAll({type:`window`,includeUncontrolled:!0}).then(t=>{if(t.some(e=>e.visibilityState===`visible`))return;let n=e.data?.json()??{title:`PPM`,body:`Chat completed`};return self.registration.showNotification(n.title,{body:n.body,icon:`/icon-192.png`,badge:`/icon-192.png`,tag:`ppm-chat-done`,silent:!1,data:{url:self.location.origin}})}))}),self.addEventListener(`notificationclick`,e=>{e.notification.close(),e.waitUntil(self.clients.matchAll({type:`window`,includeUncontrolled:!0}).then(t=>{for(let e of t)if(e.url.includes(self.location.origin)&&`focus`in e)return e.focus();return self.clients.openWindow(e.notification.data?.url||`/`)}))});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hienlh/ppm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Personal Project Manager — mobile-first web IDE with AI assistance",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"simple-git": "^3.33.0",
|
|
60
60
|
"sonner": "^2.0.7",
|
|
61
61
|
"tailwind-merge": "^3.5.0",
|
|
62
|
+
"use-stick-to-bottom": "^1.1.3",
|
|
62
63
|
"vite-plugin-monaco-editor": "^1.1.0",
|
|
63
64
|
"web-push": "^3.6.7",
|
|
64
65
|
"zustand": "^5.0.11"
|
package/src/cli/commands/stop.ts
CHANGED
|
@@ -45,6 +45,14 @@ export async function stopServer() {
|
|
|
45
45
|
// Kill tunnel process (independent from server)
|
|
46
46
|
if (tunnelPid) killPid(tunnelPid, "tunnel");
|
|
47
47
|
|
|
48
|
+
// Windows fallback: kill orphan cloudflared processes spawned by PPM
|
|
49
|
+
if (process.platform === "win32") {
|
|
50
|
+
try {
|
|
51
|
+
const cfPath = resolve(homedir(), ".ppm", "bin", "cloudflared.exe");
|
|
52
|
+
Bun.spawnSync(["taskkill", "/F", "/IM", "cloudflared.exe"], { stdout: "ignore", stderr: "ignore" });
|
|
53
|
+
} catch {}
|
|
54
|
+
}
|
|
55
|
+
|
|
48
56
|
cleanup();
|
|
49
57
|
console.log("PPM stopped.");
|
|
50
58
|
}
|
|
@@ -65,6 +65,8 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
65
65
|
private pendingApprovals = new Map<string, PendingApproval>();
|
|
66
66
|
/** Active query objects for abort support */
|
|
67
67
|
private activeQueries = new Map<string, { close: () => void }>();
|
|
68
|
+
/** Fork source: ppmSessionId → sourceSessionId (used on first message to fork) */
|
|
69
|
+
private forkSources = new Map<string, string>();
|
|
68
70
|
/** Latest known usage/rate-limit info (shared across all sessions) */
|
|
69
71
|
private latestUsage: UsageInfo = {};
|
|
70
72
|
|
|
@@ -164,6 +166,11 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
164
166
|
}
|
|
165
167
|
}
|
|
166
168
|
|
|
169
|
+
/** Register a fork source — when this session sends its first message, it will fork from sourceId */
|
|
170
|
+
setForkSource(sessionId: string, sourceSessionId: string): void {
|
|
171
|
+
this.forkSources.set(sessionId, sourceSessionId);
|
|
172
|
+
}
|
|
173
|
+
|
|
167
174
|
/**
|
|
168
175
|
* Resolve a pending approval from FE (tool approval or AskUserQuestion answer).
|
|
169
176
|
* Called by WS handler when client sends approval_response.
|
|
@@ -179,6 +186,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
179
186
|
async *sendMessage(
|
|
180
187
|
sessionId: string,
|
|
181
188
|
message: string,
|
|
189
|
+
opts?: { forkSession?: boolean },
|
|
182
190
|
): AsyncIterable<ChatEvent> {
|
|
183
191
|
if (!this.activeSessions.has(sessionId)) {
|
|
184
192
|
await this.resumeSession(sessionId);
|
|
@@ -193,6 +201,11 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
193
201
|
const isFirstMessage = count === 0;
|
|
194
202
|
this.messageCount.set(sessionId, count + 1);
|
|
195
203
|
|
|
204
|
+
// Check if this session should fork from another
|
|
205
|
+
const forkSourceId = this.forkSources.get(sessionId);
|
|
206
|
+
const shouldFork = !!forkSourceId && isFirstMessage;
|
|
207
|
+
if (forkSourceId) this.forkSources.delete(sessionId);
|
|
208
|
+
|
|
196
209
|
/**
|
|
197
210
|
* Approval events to yield from the generator.
|
|
198
211
|
* canUseTool pushes events here; the main loop yields them.
|
|
@@ -254,14 +267,16 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
254
267
|
try {
|
|
255
268
|
const providerConfig = this.getProviderConfig();
|
|
256
269
|
// Resolve SDK's actual session ID for resume (may differ from PPM's UUID)
|
|
257
|
-
|
|
258
|
-
|
|
270
|
+
// For fork: use the source session's SDK id
|
|
271
|
+
const sdkId = shouldFork ? getSdkSessionId(forkSourceId!) : getSdkSessionId(sessionId);
|
|
272
|
+
console.log(`[sdk] query: session=${sessionId} sdkId=${sdkId} isFirst=${isFirstMessage} fork=${shouldFork} cwd=${meta.projectPath ?? "(none)"}`);
|
|
259
273
|
|
|
260
274
|
const q = query({
|
|
261
275
|
prompt: message,
|
|
262
276
|
options: {
|
|
263
|
-
sessionId: isFirstMessage ? sessionId : undefined,
|
|
264
|
-
resume: isFirstMessage ? undefined : sdkId,
|
|
277
|
+
sessionId: isFirstMessage && !shouldFork ? sessionId : undefined,
|
|
278
|
+
resume: (isFirstMessage && !shouldFork) ? undefined : sdkId,
|
|
279
|
+
...(shouldFork && { forkSession: true }),
|
|
265
280
|
cwd: meta.projectPath,
|
|
266
281
|
// Use full Claude Code system prompt (coding guidelines, security, response style)
|
|
267
282
|
systemPrompt: { type: "preset", preset: "claude_code" },
|
|
@@ -328,9 +343,9 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
328
343
|
continue;
|
|
329
344
|
}
|
|
330
345
|
|
|
331
|
-
// Handle `user` messages
|
|
332
|
-
//
|
|
333
|
-
if ((msg as any).type === "user"
|
|
346
|
+
// Handle `user` messages — they contain tool_result blocks.
|
|
347
|
+
// Top-level: e.g. after Agent finishes. Child: subagent internal tool results.
|
|
348
|
+
if ((msg as any).type === "user") {
|
|
334
349
|
const userContent = (msg as any).message?.content;
|
|
335
350
|
if (Array.isArray(userContent)) {
|
|
336
351
|
for (const block of userContent) {
|
|
@@ -341,8 +356,9 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
341
356
|
output: typeof output === "string" ? output : JSON.stringify(output),
|
|
342
357
|
isError: !!block.is_error,
|
|
343
358
|
toolUseId: block.tool_use_id as string | undefined,
|
|
359
|
+
...(parentId && { parentToolUseId: parentId }),
|
|
344
360
|
};
|
|
345
|
-
if (pendingToolCount > 0) pendingToolCount--;
|
|
361
|
+
if (!parentId && pendingToolCount > 0) pendingToolCount--;
|
|
346
362
|
}
|
|
347
363
|
}
|
|
348
364
|
}
|
|
@@ -385,11 +401,18 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
385
401
|
// Handle stream_event (raw API events) for text deltas
|
|
386
402
|
if ((msg as any).type === "stream_event") {
|
|
387
403
|
const event = partial.event;
|
|
388
|
-
if (event?.type === "content_block_delta"
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
404
|
+
if (event?.type === "content_block_delta") {
|
|
405
|
+
if (event.delta?.type === "text_delta") {
|
|
406
|
+
const text = event.delta.text ?? "";
|
|
407
|
+
if (text) {
|
|
408
|
+
lastPartialText += text;
|
|
409
|
+
yield { type: "text", content: text, ...(parentId && { parentToolUseId: parentId }) };
|
|
410
|
+
}
|
|
411
|
+
} else if (event.delta?.type === "thinking_delta") {
|
|
412
|
+
const thinking = event.delta.thinking ?? "";
|
|
413
|
+
if (thinking) {
|
|
414
|
+
yield { type: "thinking", content: thinking, ...(parentId && { parentToolUseId: parentId }) } as any;
|
|
415
|
+
}
|
|
393
416
|
}
|
|
394
417
|
}
|
|
395
418
|
continue;
|
package/src/server/index.ts
CHANGED
|
@@ -377,6 +377,9 @@ export async function startServer(options: {
|
|
|
377
377
|
} as Parameters<typeof Bun.serve>[0] extends { websocket?: infer W } ? W : never,
|
|
378
378
|
});
|
|
379
379
|
|
|
380
|
+
// Start background usage polling
|
|
381
|
+
import("../services/claude-usage.service.ts").then(({ startUsagePolling }) => startUsagePolling()).catch(() => {});
|
|
382
|
+
|
|
380
383
|
console.log(`\n PPM v${VERSION} ready\n`);
|
|
381
384
|
console.log(` ➜ Local: http://localhost:${server.port}/`);
|
|
382
385
|
|
|
@@ -415,6 +418,18 @@ export async function startServer(options: {
|
|
|
415
418
|
console.log(` Token: ${configService.get("auth").token}`);
|
|
416
419
|
}
|
|
417
420
|
console.log();
|
|
421
|
+
|
|
422
|
+
// Graceful shutdown — stop server + tunnel on exit (especially important on Windows)
|
|
423
|
+
const shutdown = () => {
|
|
424
|
+
try { server.stop(true); } catch {}
|
|
425
|
+
try {
|
|
426
|
+
// Dynamic import to avoid circular — tunnel may not be loaded
|
|
427
|
+
import("../services/tunnel.service.ts").then(({ tunnelService }) => tunnelService.stopTunnel()).catch(() => {});
|
|
428
|
+
} catch {}
|
|
429
|
+
};
|
|
430
|
+
process.on("SIGINT", () => { shutdown(); process.exit(0); });
|
|
431
|
+
process.on("SIGTERM", () => { shutdown(); process.exit(0); });
|
|
432
|
+
process.on("exit", shutdown);
|
|
418
433
|
}
|
|
419
434
|
|
|
420
435
|
// Internal entry point for daemon child process
|
|
@@ -5,7 +5,7 @@ import { tmpdir } from "node:os";
|
|
|
5
5
|
import { chatService } from "../../services/chat.service.ts";
|
|
6
6
|
import { providerRegistry } from "../../providers/registry.ts";
|
|
7
7
|
import { listSlashItems } from "../../services/slash-items.service.ts";
|
|
8
|
-
import {
|
|
8
|
+
import { getCachedUsage, refreshUsageNow } from "../../services/claude-usage.service.ts";
|
|
9
9
|
import { getSessionLog } from "../../services/session-log.service.ts";
|
|
10
10
|
import { ok, err } from "../../types/api.ts";
|
|
11
11
|
|
|
@@ -24,10 +24,14 @@ chatRoutes.get("/slash-items", (c) => {
|
|
|
24
24
|
}
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
/** GET /chat/usage —
|
|
27
|
+
/** GET /chat/usage — return cached usage. ?refresh=1 forces fresh fetch first. */
|
|
28
28
|
chatRoutes.get("/usage", async (c) => {
|
|
29
|
-
|
|
29
|
+
if (c.req.query("refresh")) {
|
|
30
|
+
try { await refreshUsageNow(); } catch { /* use stale cache */ }
|
|
31
|
+
}
|
|
32
|
+
const usage = getCachedUsage();
|
|
30
33
|
return c.json(ok({
|
|
34
|
+
lastFetchedAt: usage.lastFetchedAt,
|
|
31
35
|
fiveHour: usage.session?.utilization,
|
|
32
36
|
sevenDay: usage.weekly?.utilization,
|
|
33
37
|
fiveHourResetsAt: usage.session?.resetsAt,
|
|
@@ -101,6 +105,30 @@ chatRoutes.delete("/sessions/:id", async (c) => {
|
|
|
101
105
|
}
|
|
102
106
|
});
|
|
103
107
|
|
|
108
|
+
/** POST /chat/sessions/:id/fork — fork session into a new one (for rewind/branch) */
|
|
109
|
+
chatRoutes.post("/sessions/:id/fork", async (c) => {
|
|
110
|
+
try {
|
|
111
|
+
const sourceId = c.req.param("id");
|
|
112
|
+
const projectName = c.get("projectName");
|
|
113
|
+
const projectPath = c.get("projectPath");
|
|
114
|
+
const providerId = c.req.query("providerId") ?? "claude";
|
|
115
|
+
// Create a new PPM session that will fork from sourceId on first message
|
|
116
|
+
const session = await chatService.createSession(providerId, {
|
|
117
|
+
projectName,
|
|
118
|
+
projectPath,
|
|
119
|
+
title: "Forked Chat",
|
|
120
|
+
});
|
|
121
|
+
// Store fork source so WS handler knows to use forkSession on first message
|
|
122
|
+
const provider = providerRegistry.get(providerId);
|
|
123
|
+
if (provider && "setForkSource" in provider) {
|
|
124
|
+
(provider as any).setForkSource(session.id, sourceId);
|
|
125
|
+
}
|
|
126
|
+
return c.json(ok({ ...session, forkedFrom: sourceId }), 201);
|
|
127
|
+
} catch (e) {
|
|
128
|
+
return c.json(err((e as Error).message), 500);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
104
132
|
/** GET /chat/sessions/:id/logs — get session-level debug logs */
|
|
105
133
|
chatRoutes.get("/sessions/:id/logs", (c) => {
|
|
106
134
|
try {
|
package/src/server/ws/chat.ts
CHANGED
|
@@ -73,12 +73,36 @@ async function runStreamLoop(sessionId: string, providerId: string, content: str
|
|
|
73
73
|
entry.needsCatchUp = false;
|
|
74
74
|
entry.catchUpText = "";
|
|
75
75
|
|
|
76
|
+
// Heartbeat interval — declared outside try so finally can clear it
|
|
77
|
+
let heartbeat: ReturnType<typeof setInterval> | undefined;
|
|
78
|
+
|
|
76
79
|
try {
|
|
77
80
|
const userPreview = content.slice(0, 200);
|
|
78
81
|
logSessionEvent(sessionId, "USER", userPreview);
|
|
79
82
|
console.log(`[chat] session=${sessionId} sending message to provider=${providerId}`);
|
|
83
|
+
|
|
84
|
+
// Send "connecting" status with thinking config so FE can set appropriate warning threshold
|
|
85
|
+
const { configService } = await import("../../services/config.service.ts");
|
|
86
|
+
const ai = configService.get("ai");
|
|
87
|
+
const pCfg = ai.providers[ai.default_provider ?? "claude"] ?? {};
|
|
88
|
+
const effort = (pCfg as Record<string, unknown>).effort as string | undefined;
|
|
89
|
+
const thinkingBudget = (pCfg as Record<string, unknown>).thinking_budget_tokens as number | undefined;
|
|
90
|
+
safeSend(sessionId, { type: "streaming_status", status: "connecting", effort, thinkingBudget });
|
|
91
|
+
|
|
80
92
|
let eventCount = 0;
|
|
93
|
+
let firstEventReceived = false;
|
|
94
|
+
const startTime = Date.now();
|
|
81
95
|
|
|
96
|
+
// Heartbeat: while waiting for first response, send elapsed time every 5s
|
|
97
|
+
// so FE can show "Connecting... (15s)" and warn if it takes too long
|
|
98
|
+
heartbeat = setInterval(() => {
|
|
99
|
+
if (firstEventReceived || abortController.signal.aborted) {
|
|
100
|
+
clearInterval(heartbeat);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
104
|
+
safeSend(sessionId, { type: "streaming_status", status: "connecting", elapsed });
|
|
105
|
+
}, 5_000);
|
|
82
106
|
|
|
83
107
|
for await (const event of chatService.sendMessage(providerId, sessionId, content)) {
|
|
84
108
|
if (abortController.signal.aborted) break;
|
|
@@ -86,6 +110,13 @@ async function runStreamLoop(sessionId: string, providerId: string, content: str
|
|
|
86
110
|
const ev = event as any;
|
|
87
111
|
const evType = ev.type ?? "unknown";
|
|
88
112
|
|
|
113
|
+
// First event received — stop heartbeat, switch to streaming status
|
|
114
|
+
if (!firstEventReceived) {
|
|
115
|
+
firstEventReceived = true;
|
|
116
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
117
|
+
safeSend(sessionId, { type: "streaming_status", status: "streaming" });
|
|
118
|
+
}
|
|
119
|
+
|
|
89
120
|
// Log every event
|
|
90
121
|
if (evType === "text") {
|
|
91
122
|
logSessionEvent(sessionId, "TEXT", ev.content?.slice(0, 500) ?? "");
|
|
@@ -137,6 +168,9 @@ async function runStreamLoop(sessionId: string, providerId: string, content: str
|
|
|
137
168
|
safeSend(sessionId, { type: "error", message: errMsg });
|
|
138
169
|
}
|
|
139
170
|
} finally {
|
|
171
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
172
|
+
// Always send done — guarantees FE resets isStreaming even if provider didn't yield done
|
|
173
|
+
safeSend(sessionId, { type: "done", sessionId });
|
|
140
174
|
entry.abort = undefined;
|
|
141
175
|
entry.isStreaming = false;
|
|
142
176
|
entry.pendingApprovalEvent = undefined;
|
|
@@ -237,6 +271,12 @@ export const chatWebSocket = {
|
|
|
237
271
|
return;
|
|
238
272
|
}
|
|
239
273
|
|
|
274
|
+
// Ensure entry.ws is current — may be stale if open/close race during reconnect
|
|
275
|
+
const entry0 = activeSessions.get(sessionId);
|
|
276
|
+
if (entry0 && entry0.ws !== ws) {
|
|
277
|
+
entry0.ws = ws;
|
|
278
|
+
}
|
|
279
|
+
|
|
240
280
|
const entry = activeSessions.get(sessionId);
|
|
241
281
|
const providerId = entry?.providerId ?? providerRegistry.getDefault().id;
|
|
242
282
|
|
|
@@ -11,7 +11,8 @@ export interface LimitBucket {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export interface ClaudeUsage {
|
|
14
|
-
timestamp
|
|
14
|
+
/** ISO timestamp of last successful fetch */
|
|
15
|
+
lastFetchedAt?: string;
|
|
15
16
|
session?: LimitBucket;
|
|
16
17
|
weekly?: LimitBucket;
|
|
17
18
|
weeklyOpus?: LimitBucket;
|
|
@@ -21,16 +22,21 @@ export interface ClaudeUsage {
|
|
|
21
22
|
const API_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
22
23
|
const API_BETA = "oauth-2025-04-20";
|
|
23
24
|
const USER_AGENT = "claude-code/1.0";
|
|
24
|
-
const CACHE_TTL = 30_000; // 30s
|
|
25
25
|
const FETCH_TIMEOUT = 10_000; // 10s
|
|
26
|
+
const POLL_INTERVAL = 60_000; // auto-fetch every 60s
|
|
27
|
+
const RETRY_DELAY = 5_000; // 5s between retries
|
|
28
|
+
const MAX_RETRIES = 3;
|
|
26
29
|
|
|
27
|
-
/** Cached data
|
|
28
|
-
let cache:
|
|
30
|
+
/** Cached usage data */
|
|
31
|
+
let cache: ClaudeUsage = {};
|
|
29
32
|
|
|
30
33
|
/** Cached OAuth token (read once from Keychain/file) */
|
|
31
34
|
let tokenCache: { token: string; timestamp: number } | null = null;
|
|
32
35
|
const TOKEN_TTL = 300_000; // re-read token every 5min
|
|
33
36
|
|
|
37
|
+
/** Auto-poll timer */
|
|
38
|
+
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
39
|
+
|
|
34
40
|
/**
|
|
35
41
|
* Read OAuth access token from macOS Keychain, fallback to credentials file.
|
|
36
42
|
*/
|
|
@@ -66,9 +72,7 @@ function getAccessToken(): string {
|
|
|
66
72
|
return token;
|
|
67
73
|
}
|
|
68
74
|
|
|
69
|
-
/**
|
|
70
|
-
* Fetch usage from Anthropic OAuth API — native async, zero process spawn.
|
|
71
|
-
*/
|
|
75
|
+
/** Fetch usage from Anthropic OAuth API */
|
|
72
76
|
async function fetchUsageFromApi(): Promise<ClaudeUsage> {
|
|
73
77
|
const token = getAccessToken();
|
|
74
78
|
const res = await fetch(API_URL, {
|
|
@@ -86,8 +90,7 @@ async function fetchUsageFromApi(): Promise<ClaudeUsage> {
|
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
const raw = (await res.json()) as Record<string, any>;
|
|
89
|
-
const
|
|
90
|
-
const data: ClaudeUsage = { timestamp: now };
|
|
93
|
+
const data: ClaudeUsage = { lastFetchedAt: new Date().toISOString() };
|
|
91
94
|
|
|
92
95
|
if (raw.five_hour) data.session = parseApiBucket(raw.five_hour, 5);
|
|
93
96
|
if (raw.seven_day) data.weekly = parseApiBucket(raw.seven_day, 168);
|
|
@@ -113,20 +116,45 @@ function parseApiBucket(raw: Record<string, any>, windowHours: number): LimitBuc
|
|
|
113
116
|
};
|
|
114
117
|
}
|
|
115
118
|
|
|
116
|
-
/**
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
/** Fetch with retry logic */
|
|
120
|
+
async function fetchWithRetry(): Promise<void> {
|
|
121
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
122
|
+
try {
|
|
123
|
+
const data = await fetchUsageFromApi();
|
|
124
|
+
cache = data;
|
|
125
|
+
return;
|
|
126
|
+
} catch (e) {
|
|
127
|
+
const msg = (e as Error).message ?? "";
|
|
128
|
+
// Don't retry on 429 — just use stale cache
|
|
129
|
+
if (msg.includes("429")) return;
|
|
130
|
+
if (attempt < MAX_RETRIES) {
|
|
131
|
+
await new Promise((r) => setTimeout(r, RETRY_DELAY));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
123
134
|
}
|
|
135
|
+
}
|
|
124
136
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
137
|
+
/** Start background auto-polling (called once on server start) */
|
|
138
|
+
export function startUsagePolling(): void {
|
|
139
|
+
if (pollTimer) return;
|
|
140
|
+
// Initial fetch
|
|
141
|
+
fetchWithRetry();
|
|
142
|
+
// Poll every POLL_INTERVAL
|
|
143
|
+
pollTimer = setInterval(() => fetchWithRetry(), POLL_INTERVAL);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Stop background polling */
|
|
147
|
+
export function stopUsagePolling(): void {
|
|
148
|
+
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Get cached usage (fast, synchronous read — FE just reads this) */
|
|
152
|
+
export function getCachedUsage(): ClaudeUsage {
|
|
153
|
+
return cache;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Force immediate refresh (e.g. after a chat completes) */
|
|
157
|
+
export async function refreshUsageNow(): Promise<ClaudeUsage> {
|
|
158
|
+
await fetchWithRetry();
|
|
159
|
+
return cache;
|
|
132
160
|
}
|
|
@@ -29,10 +29,13 @@ class TunnelService {
|
|
|
29
29
|
if (this.cleanupHandler) {
|
|
30
30
|
process.off("SIGINT", this.cleanupHandler);
|
|
31
31
|
process.off("SIGTERM", this.cleanupHandler);
|
|
32
|
+
process.off("exit", this.cleanupHandler);
|
|
32
33
|
}
|
|
33
34
|
this.cleanupHandler = () => this.stopTunnel();
|
|
34
35
|
process.on("SIGINT", this.cleanupHandler);
|
|
35
36
|
process.on("SIGTERM", this.cleanupHandler);
|
|
37
|
+
// Windows: SIGINT/SIGTERM may not fire on Ctrl+C — use 'exit' as fallback
|
|
38
|
+
process.on("exit", this.cleanupHandler);
|
|
36
39
|
|
|
37
40
|
// Read stderr to find tunnel URL, then keep draining to avoid SIGPIPE
|
|
38
41
|
const reader = proc.stderr.getReader();
|
|
@@ -81,6 +84,7 @@ class TunnelService {
|
|
|
81
84
|
if (this.cleanupHandler) {
|
|
82
85
|
process.off("SIGINT", this.cleanupHandler);
|
|
83
86
|
process.off("SIGTERM", this.cleanupHandler);
|
|
87
|
+
process.off("exit", this.cleanupHandler);
|
|
84
88
|
this.cleanupHandler = null;
|
|
85
89
|
}
|
|
86
90
|
if (this.childProcess) {
|
package/src/types/api.ts
CHANGED
|
@@ -29,6 +29,7 @@ export type ChatWsClientMessage =
|
|
|
29
29
|
|
|
30
30
|
export type ChatWsServerMessage =
|
|
31
31
|
| { type: "text"; content: string; parentToolUseId?: string }
|
|
32
|
+
| { type: "thinking"; content: string; parentToolUseId?: string }
|
|
32
33
|
| { type: "tool_use"; tool: string; input: unknown; toolUseId?: string; parentToolUseId?: string }
|
|
33
34
|
| { type: "tool_result"; output: string; isError?: boolean; toolUseId?: string; parentToolUseId?: string }
|
|
34
35
|
| { type: "approval_request"; requestId: string; tool: string; input: unknown }
|
package/src/types/chat.ts
CHANGED
|
@@ -76,6 +76,7 @@ export type ResultSubtype =
|
|
|
76
76
|
|
|
77
77
|
export type ChatEvent =
|
|
78
78
|
| { type: "text"; content: string; parentToolUseId?: string }
|
|
79
|
+
| { type: "thinking"; content: string; parentToolUseId?: string }
|
|
79
80
|
| { type: "tool_use"; tool: string; input: unknown; toolUseId?: string; parentToolUseId?: string; children?: ChatEvent[] }
|
|
80
81
|
| { type: "tool_result"; output: string; isError?: boolean; toolUseId?: string; parentToolUseId?: string }
|
|
81
82
|
| { type: "approval_request"; requestId: string; tool: string; input: unknown }
|
package/src/web/app.tsx
CHANGED
|
@@ -19,6 +19,7 @@ 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
21
|
import { CommandPalette } from "@/components/layout/command-palette";
|
|
22
|
+
import { BugReportPopup } from "@/components/shared/bug-report-popup";
|
|
22
23
|
import { cn } from "@/lib/utils";
|
|
23
24
|
|
|
24
25
|
type AuthState = "checking" | "authenticated" | "unauthenticated";
|
|
@@ -210,9 +211,13 @@ export function App() {
|
|
|
210
211
|
{/* Command palette (Shift+Shift) */}
|
|
211
212
|
<CommandPalette open={paletteOpen} onClose={closePalette} initialQuery={paletteInitialQuery} />
|
|
212
213
|
|
|
214
|
+
{/* Global bug report popup */}
|
|
215
|
+
<BugReportPopup />
|
|
216
|
+
|
|
213
217
|
{/* Toast notifications */}
|
|
214
218
|
<Toaster
|
|
215
219
|
position="bottom-left"
|
|
220
|
+
closeButton
|
|
216
221
|
toastOptions={{
|
|
217
222
|
className: "bg-surface border-border text-foreground",
|
|
218
223
|
}}
|
|
@@ -15,7 +15,7 @@ interface ChatHistoryBarProps {
|
|
|
15
15
|
usageInfo: UsageInfo;
|
|
16
16
|
usageLoading?: boolean;
|
|
17
17
|
refreshUsage?: () => void;
|
|
18
|
-
|
|
18
|
+
lastFetchedAt?: string | null;
|
|
19
19
|
sessionId?: string | null;
|
|
20
20
|
onSelectSession?: (session: SessionInfo) => void;
|
|
21
21
|
onBugReport?: () => void;
|
|
@@ -31,6 +31,15 @@ function formatDate(iso: string): string {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
function relativeTime(iso: string): string {
|
|
35
|
+
const secs = Math.round((Date.now() - new Date(iso).getTime()) / 1000);
|
|
36
|
+
if (secs < 5) return "now";
|
|
37
|
+
if (secs < 60) return `${secs}s`;
|
|
38
|
+
const mins = Math.floor(secs / 60);
|
|
39
|
+
if (mins < 60) return `${mins}m`;
|
|
40
|
+
return `${Math.floor(mins / 60)}h`;
|
|
41
|
+
}
|
|
42
|
+
|
|
34
43
|
function pctColor(pct: number): string {
|
|
35
44
|
if (pct >= 90) return "text-red-500";
|
|
36
45
|
if (pct >= 70) return "text-amber-500";
|
|
@@ -38,7 +47,7 @@ function pctColor(pct: number): string {
|
|
|
38
47
|
}
|
|
39
48
|
|
|
40
49
|
export function ChatHistoryBar({
|
|
41
|
-
projectName, usageInfo, usageLoading, refreshUsage,
|
|
50
|
+
projectName, usageInfo, usageLoading, refreshUsage, lastFetchedAt,
|
|
42
51
|
sessionId, onSelectSession, onBugReport, isConnected, onReconnect,
|
|
43
52
|
}: ChatHistoryBarProps) {
|
|
44
53
|
const [activePanel, setActivePanel] = useState<PanelType>(null);
|
|
@@ -133,6 +142,9 @@ export function ChatHistoryBar({
|
|
|
133
142
|
<span>5h:{fiveHourPct != null ? `${fiveHourPct}%` : "--%"}</span>
|
|
134
143
|
<span className="text-text-subtle">·</span>
|
|
135
144
|
<span>Wk:{sevenDayPct != null ? `${sevenDayPct}%` : "--%"}</span>
|
|
145
|
+
{lastFetchedAt && (
|
|
146
|
+
<span className="text-text-subtle/50 font-normal text-[9px] ml-0.5">{relativeTime(lastFetchedAt)}</span>
|
|
147
|
+
)}
|
|
136
148
|
</button>
|
|
137
149
|
|
|
138
150
|
{/* Spacer */}
|
|
@@ -229,7 +241,7 @@ export function ChatHistoryBar({
|
|
|
229
241
|
onClose={() => setActivePanel(null)}
|
|
230
242
|
onReload={refreshUsage}
|
|
231
243
|
loading={usageLoading}
|
|
232
|
-
|
|
244
|
+
lastFetchedAt={lastFetchedAt}
|
|
233
245
|
/>
|
|
234
246
|
)}
|
|
235
247
|
</div>
|