@hellcoder/companion 0.124.1 → 0.126.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/dist/assets/{AgentsPage-Byq8wvdn.js → AgentsPage-DuPncpk1.js} +1 -1
- package/dist/assets/{CronManager-Bq2lOMbZ.js → CronManager-DCmtUDzd.js} +1 -1
- package/dist/assets/{DashboardPage-V77HIt8S.js → DashboardPage-CYELiWmp.js} +1 -1
- package/dist/assets/{IntegrationsPage-C3aW9cDI.js → IntegrationsPage-BR2Z-pCf.js} +1 -1
- package/dist/assets/{LinearOAuthSettingsPage-_eAbciJN.js → LinearOAuthSettingsPage-InO6EwYe.js} +1 -1
- package/dist/assets/{LinearSettingsPage-DDHFHt7j.js → LinearSettingsPage-D8G4AAY3.js} +1 -1
- package/dist/assets/{MagicUIDashboard-Cl18AKjV.js → MagicUIDashboard-F0yTxNmY.js} +1 -1
- package/dist/assets/{MagicUIView-C0aiMTet.js → MagicUIView-1wNBb6FU.js} +1 -1
- package/dist/assets/{Playground-CvoHRMBd.js → Playground-CZoI80Za.js} +1 -1
- package/dist/assets/{PromptsPage-CzTs1_Se.js → PromptsPage-COKY3vTB.js} +1 -1
- package/dist/assets/{RunsPage-B7xru-Xy.js → RunsPage-uiW4-c8p.js} +1 -1
- package/dist/assets/{SandboxManager-C3tFkiWz.js → SandboxManager-Doc50lAB.js} +1 -1
- package/dist/assets/{SettingsPage-BZ2fBrHD.js → SettingsPage-B8tXQ_Xt.js} +1 -1
- package/dist/assets/{TailscalePage-DFRiFKcb.js → TailscalePage-TH3Seutc.js} +1 -1
- package/dist/assets/index-DduNX1Gc.js +142 -0
- package/dist/assets/{sw-register-Dc72vh_Y.js → sw-register-C-YDyat6.js} +1 -1
- package/dist/index.html +1 -1
- package/dist/sw.js +1 -1
- package/package.json +1 -1
- package/server/cli-launcher.ts +2 -0
- package/server/routes.test.ts +23 -0
- package/server/routes.ts +1 -0
- package/server/session-store.ts +2 -0
- package/server/settings-manager.ts +1 -0
- package/server/ws-bridge-persist.test.ts +41 -0
- package/server/ws-bridge-persist.ts +17 -0
- package/server/ws-bridge-types.ts +6 -0
- package/server/ws-bridge.test.ts +122 -0
- package/server/ws-bridge.ts +11 -0
- package/dist/assets/index-C7nK12tB.js +0 -142
|
@@ -1 +1 @@
|
|
|
1
|
-
import{_ as u}from"./index-
|
|
1
|
+
import{_ as u}from"./index-DduNX1Gc.js";function w(r={}){const{immediate:i=!1,onNeedReload:d,onNeedRefresh:_,onOfflineReady:s,onRegistered:n,onRegisteredSW:o,onRegisterError:t}=r;let a,c;const l=async(e=!0)=>{await c};async function f(){if("serviceWorker"in navigator){if(a=await u(async()=>{const{Workbox:e}=await import("./workbox-window.prod.es5-BBnX5xw4.js");return{Workbox:e}},[]).then(({Workbox:e})=>new e("/sw.js",{scope:"/",type:"classic"})).catch(e=>{t==null||t(e)}),!a)return;a.addEventListener("activated",e=>{(e.isUpdate||e.isExternal)&&(d?d():window.location.reload())}),a.addEventListener("installed",e=>{e.isUpdate||s==null||s()}),a.register({immediate:i}).then(e=>{o?o("/sw.js",e):n==null||n(e)}).catch(e=>{t==null||t(e)})}}return c=f(),l}w({onRegisteredSW(r,i){i&&setInterval(()=>{i.update()},3600*1e3)},onOfflineReady(){console.log("[SW] Offline-ready: all assets precached")}});
|
package/dist/index.html
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<meta name="apple-mobile-web-app-title" content="Companion ME" />
|
|
12
12
|
<meta name="theme-color" content="#d97757" />
|
|
13
13
|
<title>The Companion — Moritz Edition</title>
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-DduNX1Gc.js"></script>
|
|
15
15
|
<link rel="stylesheet" crossorigin href="/assets/index-OAVDFR0O.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body class="bg-cc-bg text-cc-fg">
|
package/dist/sw.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
try{self["workbox:core:7.4.0"]&&_()}catch{}const x=(a,...e)=>{let t=a;return e.length>0&&(t+=` :: ${JSON.stringify(e)}`),t},N=x;class l extends Error{constructor(e,t){const s=N(e,t);super(s),this.name=e,this.details=t}}const E=new Set,f={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:typeof registration<"u"?registration.scope:""},b=a=>[f.prefix,a,f.suffix].filter(e=>e&&e.length>0).join("-"),O=a=>{for(const e of Object.keys(f))a(e)},C={updateDetails:a=>{O(e=>{typeof a[e]=="string"&&(f[e]=a[e])})},getGoogleAnalyticsName:a=>a||b(f.googleAnalytics),getPrecacheName:a=>a||b(f.precache),getPrefix:()=>f.prefix,getRuntimeName:a=>a||b(f.runtime),getSuffix:()=>f.suffix};function P(a,e){const t=new URL(a);for(const s of e)t.searchParams.delete(s);return t.href}async function I(a,e,t,s){const n=P(e.url,t);if(e.url===n)return a.match(e,s);const r=Object.assign(Object.assign({},s),{ignoreSearch:!0}),c=await a.keys(e,r);for(const i of c){const o=P(i.url,t);if(n===o)return a.match(i,s)}}let y;function M(){if(y===void 0){const a=new Response("");if("body"in a)try{new Response(a.body),y=!0}catch{y=!1}y=!1}return y}class D{constructor(){this.promise=new Promise((e,t)=>{this.resolve=e,this.reject=t})}}async function S(){for(const a of E)await a()}const W=a=>new URL(String(a),location.href).href.replace(new RegExp(`^${location.origin}`),"");function A(a){return new Promise(e=>setTimeout(e,a))}function K(a,e){const t=e();return a.waitUntil(t),t}async function q(a,e){let t=null;if(a.url&&(t=new URL(a.url).origin),t!==self.location.origin)throw new l("cross-origin-copy-response",{origin:t});const s=a.clone(),r={headers:new Headers(s.headers),status:s.status,statusText:s.statusText},c=M()?s.body:await s.blob();return new Response(c,r)}function j(){self.addEventListener("activate",()=>self.clients.claim())}try{self["workbox:precaching:7.4.0"]&&_()}catch{}const H="__WB_REVISION__";function F(a){if(!a)throw new l("add-to-cache-list-unexpected-type",{entry:a});if(typeof a=="string"){const r=new URL(a,location.href);return{cacheKey:r.href,url:r.href}}const{revision:e,url:t}=a;if(!t)throw new l("add-to-cache-list-unexpected-type",{entry:a});if(!e){const r=new URL(t,location.href);return{cacheKey:r.href,url:r.href}}const s=new URL(t,location.href),n=new URL(t,location.href);return s.searchParams.set(H,e),{cacheKey:s.href,url:n.href}}class B{constructor(){this.updatedURLs=[],this.notUpdatedURLs=[],this.handlerWillStart=async({request:e,state:t})=>{t&&(t.originalRequest=e)},this.cachedResponseWillBeUsed=async({event:e,state:t,cachedResponse:s})=>{if(e.type==="install"&&t&&t.originalRequest&&t.originalRequest instanceof Request){const n=t.originalRequest.url;s?this.notUpdatedURLs.push(n):this.updatedURLs.push(n)}return s}}}class ${constructor({precacheController:e}){this.cacheKeyWillBeUsed=async({request:t,params:s})=>{const n=(s==null?void 0:s.cacheKey)||this._precacheController.getCacheKeyForURL(t.url);return n?new Request(n,{headers:t.headers}):t},this._precacheController=e}}try{self["workbox:strategies:7.4.0"]&&_()}catch{}function m(a){return typeof a=="string"?new Request(a):a}class G{constructor(e,t){this._cacheKeys={},Object.assign(this,t),this.event=t.event,this._strategy=e,this._handlerDeferred=new D,this._extendLifetimePromises=[],this._plugins=[...e.plugins],this._pluginStateMap=new Map;for(const s of this._plugins)this._pluginStateMap.set(s,{});this.event.waitUntil(this._handlerDeferred.promise)}async fetch(e){const{event:t}=this;let s=m(e);if(s.mode==="navigate"&&t instanceof FetchEvent&&t.preloadResponse){const c=await t.preloadResponse;if(c)return c}const n=this.hasCallback("fetchDidFail")?s.clone():null;try{for(const c of this.iterateCallbacks("requestWillFetch"))s=await c({request:s.clone(),event:t})}catch(c){if(c instanceof Error)throw new l("plugin-error-request-will-fetch",{thrownErrorMessage:c.message})}const r=s.clone();try{let c;c=await fetch(s,s.mode==="navigate"?void 0:this._strategy.fetchOptions);for(const i of this.iterateCallbacks("fetchDidSucceed"))c=await i({event:t,request:r,response:c});return c}catch(c){throw n&&await this.runCallbacks("fetchDidFail",{error:c,event:t,originalRequest:n.clone(),request:r.clone()}),c}}async fetchAndCachePut(e){const t=await this.fetch(e),s=t.clone();return this.waitUntil(this.cachePut(e,s)),t}async cacheMatch(e){const t=m(e);let s;const{cacheName:n,matchOptions:r}=this._strategy,c=await this.getCacheKey(t,"read"),i=Object.assign(Object.assign({},r),{cacheName:n});s=await caches.match(c,i);for(const o of this.iterateCallbacks("cachedResponseWillBeUsed"))s=await o({cacheName:n,matchOptions:r,cachedResponse:s,request:c,event:this.event})||void 0;return s}async cachePut(e,t){const s=m(e);await A(0);const n=await this.getCacheKey(s,"write");if(!t)throw new l("cache-put-with-no-response",{url:W(n.url)});const r=await this._ensureResponseSafeToCache(t);if(!r)return!1;const{cacheName:c,matchOptions:i}=this._strategy,o=await self.caches.open(c),h=this.hasCallback("cacheDidUpdate"),p=h?await I(o,n.clone(),["__WB_REVISION__"],i):null;try{await o.put(n,h?r.clone():r)}catch(u){if(u instanceof Error)throw u.name==="QuotaExceededError"&&await S(),u}for(const u of this.iterateCallbacks("cacheDidUpdate"))await u({cacheName:c,oldResponse:p,newResponse:r.clone(),request:n,event:this.event});return!0}async getCacheKey(e,t){const s=`${e.url} | ${t}`;if(!this._cacheKeys[s]){let n=e;for(const r of this.iterateCallbacks("cacheKeyWillBeUsed"))n=m(await r({mode:t,request:n,event:this.event,params:this.params}));this._cacheKeys[s]=n}return this._cacheKeys[s]}hasCallback(e){for(const t of this._strategy.plugins)if(e in t)return!0;return!1}async runCallbacks(e,t){for(const s of this.iterateCallbacks(e))await s(t)}*iterateCallbacks(e){for(const t of this._strategy.plugins)if(typeof t[e]=="function"){const s=this._pluginStateMap.get(t);yield r=>{const c=Object.assign(Object.assign({},r),{state:s});return t[e](c)}}}waitUntil(e){return this._extendLifetimePromises.push(e),e}async doneWaiting(){for(;this._extendLifetimePromises.length;){const e=this._extendLifetimePromises.splice(0),s=(await Promise.allSettled(e)).find(n=>n.status==="rejected");if(s)throw s.reason}}destroy(){this._handlerDeferred.resolve(null)}async _ensureResponseSafeToCache(e){let t=e,s=!1;for(const n of this.iterateCallbacks("cacheWillUpdate"))if(t=await n({request:this.request,response:t,event:this.event})||void 0,s=!0,!t)break;return s||t&&t.status!==200&&(t=void 0),t}}class V{constructor(e={}){this.cacheName=C.getRuntimeName(e.cacheName),this.plugins=e.plugins||[],this.fetchOptions=e.fetchOptions,this.matchOptions=e.matchOptions}handle(e){const[t]=this.handleAll(e);return t}handleAll(e){e instanceof FetchEvent&&(e={event:e,request:e.request});const t=e.event,s=typeof e.request=="string"?new Request(e.request):e.request,n="params"in e?e.params:void 0,r=new G(this,{event:t,request:s,params:n}),c=this._getResponse(r,s,t),i=this._awaitComplete(c,r,s,t);return[c,i]}async _getResponse(e,t,s){await e.runCallbacks("handlerWillStart",{event:s,request:t});let n;try{if(n=await this._handle(t,e),!n||n.type==="error")throw new l("no-response",{url:t.url})}catch(r){if(r instanceof Error){for(const c of e.iterateCallbacks("handlerDidError"))if(n=await c({error:r,event:s,request:t}),n)break}if(!n)throw r}for(const r of e.iterateCallbacks("handlerWillRespond"))n=await r({event:s,request:t,response:n});return n}async _awaitComplete(e,t,s,n){let r,c;try{r=await e}catch{}try{await t.runCallbacks("handlerDidRespond",{event:n,request:s,response:r}),await t.doneWaiting()}catch(i){i instanceof Error&&(c=i)}if(await t.runCallbacks("handlerDidComplete",{event:n,request:s,response:r,error:c}),t.destroy(),c)throw c}}class d extends V{constructor(e={}){e.cacheName=C.getPrecacheName(e.cacheName),super(e),this._fallbackToNetwork=e.fallbackToNetwork!==!1,this.plugins.push(d.copyRedirectedCacheableResponsesPlugin)}async _handle(e,t){const s=await t.cacheMatch(e);return s||(t.event&&t.event.type==="install"?await this._handleInstall(e,t):await this._handleFetch(e,t))}async _handleFetch(e,t){let s;const n=t.params||{};if(this._fallbackToNetwork){const r=n.integrity,c=e.integrity,i=!c||c===r;s=await t.fetch(new Request(e,{integrity:e.mode!=="no-cors"?c||r:void 0})),r&&i&&e.mode!=="no-cors"&&(this._useDefaultCacheabilityPluginIfNeeded(),await t.cachePut(e,s.clone()))}else throw new l("missing-precache-entry",{cacheName:this.cacheName,url:e.url});return s}async _handleInstall(e,t){this._useDefaultCacheabilityPluginIfNeeded();const s=await t.fetch(e);if(!await t.cachePut(e,s.clone()))throw new l("bad-precaching-response",{url:e.url,status:s.status});return s}_useDefaultCacheabilityPluginIfNeeded(){let e=null,t=0;for(const[s,n]of this.plugins.entries())n!==d.copyRedirectedCacheableResponsesPlugin&&(n===d.defaultPrecacheCacheabilityPlugin&&(e=s),n.cacheWillUpdate&&t++);t===0?this.plugins.push(d.defaultPrecacheCacheabilityPlugin):t>1&&e!==null&&this.plugins.splice(e,1)}}d.defaultPrecacheCacheabilityPlugin={async cacheWillUpdate({response:a}){return!a||a.status>=400?null:a}};d.copyRedirectedCacheableResponsesPlugin={async cacheWillUpdate({response:a}){return a.redirected?await q(a):a}};class Q{constructor({cacheName:e,plugins:t=[],fallbackToNetwork:s=!0}={}){this._urlsToCacheKeys=new Map,this._urlsToCacheModes=new Map,this._cacheKeysToIntegrities=new Map,this._strategy=new d({cacheName:C.getPrecacheName(e),plugins:[...t,new $({precacheController:this})],fallbackToNetwork:s}),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),this._installAndActiveListenersAdded=!0)}addToCacheList(e){const t=[];for(const s of e){typeof s=="string"?t.push(s):s&&s.revision===void 0&&t.push(s.url);const{cacheKey:n,url:r}=F(s),c=typeof s!="string"&&s.revision?"reload":"default";if(this._urlsToCacheKeys.has(r)&&this._urlsToCacheKeys.get(r)!==n)throw new l("add-to-cache-list-conflicting-entries",{firstEntry:this._urlsToCacheKeys.get(r),secondEntry:n});if(typeof s!="string"&&s.integrity){if(this._cacheKeysToIntegrities.has(n)&&this._cacheKeysToIntegrities.get(n)!==s.integrity)throw new l("add-to-cache-list-conflicting-integrities",{url:r});this._cacheKeysToIntegrities.set(n,s.integrity)}if(this._urlsToCacheKeys.set(r,n),this._urlsToCacheModes.set(r,c),t.length>0){const i=`Workbox is precaching URLs without revision info: ${t.join(", ")}
|
|
2
|
-
This is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(i)}}}install(e){return K(e,async()=>{const t=new B;this.strategy.plugins.push(t);for(const[r,c]of this._urlsToCacheKeys){const i=this._cacheKeysToIntegrities.get(c),o=this._urlsToCacheModes.get(r),h=new Request(r,{integrity:i,cache:o,credentials:"same-origin"});await Promise.all(this.strategy.handleAll({params:{cacheKey:c},request:h,event:e}))}const{updatedURLs:s,notUpdatedURLs:n}=t;return{updatedURLs:s,notUpdatedURLs:n}})}activate(e){return K(e,async()=>{const t=await self.caches.open(this.strategy.cacheName),s=await t.keys(),n=new Set(this._urlsToCacheKeys.values()),r=[];for(const c of s)n.has(c.url)||(await t.delete(c),r.push(c.url));return{deletedURLs:r}})}getURLsToCacheKeys(){return this._urlsToCacheKeys}getCachedURLs(){return[...this._urlsToCacheKeys.keys()]}getCacheKeyForURL(e){const t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForCacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){const t=e instanceof Request?e.url:e,s=this.getCacheKeyForURL(t);if(s)return(await self.caches.open(this.strategy.cacheName)).match(s)}createHandlerBoundToURL(e){const t=this.getCacheKeyForURL(e);if(!t)throw new l("non-precached-url",{url:e});return s=>(s.request=new Request(e),s.params=Object.assign({cacheKey:t},s.params),this.strategy.handle(s))}}let U;const L=()=>(U||(U=new Q),U);try{self["workbox:routing:7.4.0"]&&_()}catch{}const v="GET",R=a=>a&&typeof a=="object"?a:{handle:a};class g{constructor(e,t,s=v){this.handler=R(t),this.match=e,this.method=s}setCatchHandler(e){this.catchHandler=R(e)}}class z extends g{constructor(e,t,s){const n=({url:r})=>{const c=e.exec(r.href);if(c&&!(r.origin!==location.origin&&c.index!==0))return c.slice(1)};super(n,t,s)}}class J{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener("fetch",(e=>{const{request:t}=e,s=this.handleRequest({request:t,event:e});s&&e.respondWith(s)}))}addCacheListener(){self.addEventListener("message",(e=>{if(e.data&&e.data.type==="CACHE_URLS"){const{payload:t}=e.data,s=Promise.all(t.urlsToCache.map(n=>{typeof n=="string"&&(n=[n]);const r=new Request(...n);return this.handleRequest({request:r,event:e})}));e.waitUntil(s),e.ports&&e.ports[0]&&s.then(()=>e.ports[0].postMessage(!0))}}))}handleRequest({request:e,event:t}){const s=new URL(e.url,location.href);if(!s.protocol.startsWith("http"))return;const n=s.origin===location.origin,{params:r,route:c}=this.findMatchingRoute({event:t,request:e,sameOrigin:n,url:s});let i=c&&c.handler;const o=e.method;if(!i&&this._defaultHandlerMap.has(o)&&(i=this._defaultHandlerMap.get(o)),!i)return;let h;try{h=i.handle({url:s,request:e,event:t,params:r})}catch(u){h=Promise.reject(u)}const p=c&&c.catchHandler;return h instanceof Promise&&(this._catchHandler||p)&&(h=h.catch(async u=>{if(p)try{return await p.handle({url:s,request:e,event:t,params:r})}catch(k){k instanceof Error&&(u=k)}if(this._catchHandler)return this._catchHandler.handle({url:s,request:e,event:t});throw u})),h}findMatchingRoute({url:e,sameOrigin:t,request:s,event:n}){const r=this._routes.get(s.method)||[];for(const c of r){let i;const o=c.match({url:e,sameOrigin:t,request:s,event:n});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:c,params:i}}return{}}setDefaultHandler(e,t=v){this._defaultHandlerMap.set(t,R(e))}setCatchHandler(e){this._catchHandler=R(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 l("unregister-route-but-not-found-with-method",{method:e.method});const t=this._routes.get(e.method).indexOf(e);if(t>-1)this._routes.get(e.method).splice(t,1);else throw new l("unregister-route-route-not-registered")}}let w;const X=()=>(w||(w=new J,w.addFetchListener(),w.addCacheListener()),w);function T(a,e,t){let s;if(typeof a=="string"){const r=new URL(a,location.href),c=({url:i})=>i.href===r.href;s=new g(c,e,t)}else if(a instanceof RegExp)s=new z(a,e,t);else if(typeof a=="function")s=new g(a,e,t);else if(a instanceof g)s=a;else throw new l("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});return X().registerRoute(s),s}function Y(a,e=[]){for(const t of[...a.searchParams.keys()])e.some(s=>s.test(t))&&a.searchParams.delete(t);return a}function*Z(a,{ignoreURLParametersMatching:e=[/^utm_/,/^fbclid$/],directoryIndex:t="index.html",cleanURLs:s=!0,urlManipulation:n}={}){const r=new URL(a,location.href);r.hash="",yield r.href;const c=Y(r,e);if(yield c.href,t&&c.pathname.endsWith("/")){const i=new URL(c.href);i.pathname+=t,yield i.href}if(s){const i=new URL(c.href);i.pathname+=".html",yield i.href}if(n){const i=n({url:r});for(const o of i)yield o.href}}class ee extends g{constructor(e,t){const s=({request:n})=>{const r=e.getURLsToCacheKeys();for(const c of Z(n.url,t)){const i=r.get(c);if(i){const o=e.getIntegrityForCacheKey(i);return{cacheKey:i,integrity:o}}}};super(s,e.strategy)}}function te(a){const e=L(),t=new ee(e,a);T(t)}const se="-precache-",ae=async(a,e=se)=>{const s=(await self.caches.keys()).filter(n=>n.includes(e)&&n.includes(self.registration.scope)&&n!==a);return await Promise.all(s.map(n=>self.caches.delete(n))),s};function ne(){self.addEventListener("activate",(a=>{const e=C.getPrecacheName();a.waitUntil(ae(e).then(t=>{}))}))}function re(a){return L().createHandlerBoundToURL(a)}function ce(a){L().precache(a)}function ie(a,e){ce(a),te(e)}class oe extends g{constructor(e,{allowlist:t=[/./],denylist:s=[]}={}){super(n=>this._match(n),e),this._allowlist=t,this._denylist=s}_match({url:e,request:t}){if(t&&t.mode!=="navigate")return!1;const s=e.pathname+e.search;for(const n of this._denylist)if(n.test(s))return!1;return!!this._allowlist.some(n=>n.test(s))}}self.skipWaiting();j();ie([{"revision":"fab346dacb0b9b9af12554e9a59f004d","url":"logo.svg"},{"revision":"623840baec64a3438663496bb1050d30","url":"logo-docker.svg"},{"revision":"c5c8611b432190ec28522f1a2dfcbb7f","url":"logo-codex.svg"},{"revision":"
|
|
2
|
+
This is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(i)}}}install(e){return K(e,async()=>{const t=new B;this.strategy.plugins.push(t);for(const[r,c]of this._urlsToCacheKeys){const i=this._cacheKeysToIntegrities.get(c),o=this._urlsToCacheModes.get(r),h=new Request(r,{integrity:i,cache:o,credentials:"same-origin"});await Promise.all(this.strategy.handleAll({params:{cacheKey:c},request:h,event:e}))}const{updatedURLs:s,notUpdatedURLs:n}=t;return{updatedURLs:s,notUpdatedURLs:n}})}activate(e){return K(e,async()=>{const t=await self.caches.open(this.strategy.cacheName),s=await t.keys(),n=new Set(this._urlsToCacheKeys.values()),r=[];for(const c of s)n.has(c.url)||(await t.delete(c),r.push(c.url));return{deletedURLs:r}})}getURLsToCacheKeys(){return this._urlsToCacheKeys}getCachedURLs(){return[...this._urlsToCacheKeys.keys()]}getCacheKeyForURL(e){const t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForCacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){const t=e instanceof Request?e.url:e,s=this.getCacheKeyForURL(t);if(s)return(await self.caches.open(this.strategy.cacheName)).match(s)}createHandlerBoundToURL(e){const t=this.getCacheKeyForURL(e);if(!t)throw new l("non-precached-url",{url:e});return s=>(s.request=new Request(e),s.params=Object.assign({cacheKey:t},s.params),this.strategy.handle(s))}}let U;const L=()=>(U||(U=new Q),U);try{self["workbox:routing:7.4.0"]&&_()}catch{}const v="GET",R=a=>a&&typeof a=="object"?a:{handle:a};class g{constructor(e,t,s=v){this.handler=R(t),this.match=e,this.method=s}setCatchHandler(e){this.catchHandler=R(e)}}class z extends g{constructor(e,t,s){const n=({url:r})=>{const c=e.exec(r.href);if(c&&!(r.origin!==location.origin&&c.index!==0))return c.slice(1)};super(n,t,s)}}class J{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener("fetch",(e=>{const{request:t}=e,s=this.handleRequest({request:t,event:e});s&&e.respondWith(s)}))}addCacheListener(){self.addEventListener("message",(e=>{if(e.data&&e.data.type==="CACHE_URLS"){const{payload:t}=e.data,s=Promise.all(t.urlsToCache.map(n=>{typeof n=="string"&&(n=[n]);const r=new Request(...n);return this.handleRequest({request:r,event:e})}));e.waitUntil(s),e.ports&&e.ports[0]&&s.then(()=>e.ports[0].postMessage(!0))}}))}handleRequest({request:e,event:t}){const s=new URL(e.url,location.href);if(!s.protocol.startsWith("http"))return;const n=s.origin===location.origin,{params:r,route:c}=this.findMatchingRoute({event:t,request:e,sameOrigin:n,url:s});let i=c&&c.handler;const o=e.method;if(!i&&this._defaultHandlerMap.has(o)&&(i=this._defaultHandlerMap.get(o)),!i)return;let h;try{h=i.handle({url:s,request:e,event:t,params:r})}catch(u){h=Promise.reject(u)}const p=c&&c.catchHandler;return h instanceof Promise&&(this._catchHandler||p)&&(h=h.catch(async u=>{if(p)try{return await p.handle({url:s,request:e,event:t,params:r})}catch(k){k instanceof Error&&(u=k)}if(this._catchHandler)return this._catchHandler.handle({url:s,request:e,event:t});throw u})),h}findMatchingRoute({url:e,sameOrigin:t,request:s,event:n}){const r=this._routes.get(s.method)||[];for(const c of r){let i;const o=c.match({url:e,sameOrigin:t,request:s,event:n});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:c,params:i}}return{}}setDefaultHandler(e,t=v){this._defaultHandlerMap.set(t,R(e))}setCatchHandler(e){this._catchHandler=R(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 l("unregister-route-but-not-found-with-method",{method:e.method});const t=this._routes.get(e.method).indexOf(e);if(t>-1)this._routes.get(e.method).splice(t,1);else throw new l("unregister-route-route-not-registered")}}let w;const X=()=>(w||(w=new J,w.addFetchListener(),w.addCacheListener()),w);function T(a,e,t){let s;if(typeof a=="string"){const r=new URL(a,location.href),c=({url:i})=>i.href===r.href;s=new g(c,e,t)}else if(a instanceof RegExp)s=new z(a,e,t);else if(typeof a=="function")s=new g(a,e,t);else if(a instanceof g)s=a;else throw new l("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});return X().registerRoute(s),s}function Y(a,e=[]){for(const t of[...a.searchParams.keys()])e.some(s=>s.test(t))&&a.searchParams.delete(t);return a}function*Z(a,{ignoreURLParametersMatching:e=[/^utm_/,/^fbclid$/],directoryIndex:t="index.html",cleanURLs:s=!0,urlManipulation:n}={}){const r=new URL(a,location.href);r.hash="",yield r.href;const c=Y(r,e);if(yield c.href,t&&c.pathname.endsWith("/")){const i=new URL(c.href);i.pathname+=t,yield i.href}if(s){const i=new URL(c.href);i.pathname+=".html",yield i.href}if(n){const i=n({url:r});for(const o of i)yield o.href}}class ee extends g{constructor(e,t){const s=({request:n})=>{const r=e.getURLsToCacheKeys();for(const c of Z(n.url,t)){const i=r.get(c);if(i){const o=e.getIntegrityForCacheKey(i);return{cacheKey:i,integrity:o}}}};super(s,e.strategy)}}function te(a){const e=L(),t=new ee(e,a);T(t)}const se="-precache-",ae=async(a,e=se)=>{const s=(await self.caches.keys()).filter(n=>n.includes(e)&&n.includes(self.registration.scope)&&n!==a);return await Promise.all(s.map(n=>self.caches.delete(n))),s};function ne(){self.addEventListener("activate",(a=>{const e=C.getPrecacheName();a.waitUntil(ae(e).then(t=>{}))}))}function re(a){return L().createHandlerBoundToURL(a)}function ce(a){L().precache(a)}function ie(a,e){ce(a),te(e)}class oe extends g{constructor(e,{allowlist:t=[/./],denylist:s=[]}={}){super(n=>this._match(n),e),this._allowlist=t,this._denylist=s}_match({url:e,request:t}){if(t&&t.mode!=="navigate")return!1;const s=e.pathname+e.search;for(const n of this._denylist)if(n.test(s))return!1;return!!this._allowlist.some(n=>n.test(s))}}self.skipWaiting();j();ie([{"revision":"fab346dacb0b9b9af12554e9a59f004d","url":"logo.svg"},{"revision":"623840baec64a3438663496bb1050d30","url":"logo-docker.svg"},{"revision":"c5c8611b432190ec28522f1a2dfcbb7f","url":"logo-codex.svg"},{"revision":"9af5baa1120cc76be1900b52951a904e","url":"index.html"},{"revision":"1421e9c6cad8218ef5689b85034d3a95","url":"icon-512.png"},{"revision":"b96da5a0b6636012517f54b0e69c304d","url":"icon-192.png"},{"revision":"c64ca8ae8596d4ffe13d0f591beb2274","url":"favicon.svg"},{"revision":"6481face04bd08e6f0bfc2a633840b57","url":"apple-touch-icon.png"},{"revision":"83304194a3c8be2b5b61242eeb1c1046","url":"fonts/MesloLGSNerdFontMono-Regular.woff2"},{"revision":"60fafe18cbcb717c51cdabf87f9490f0","url":"fonts/MesloLGSNerdFontMono-Bold.woff2"},{"revision":null,"url":"assets/workbox-window.prod.es5-BBnX5xw4.js"},{"revision":null,"url":"assets/sw-register-C-YDyat6.js"},{"revision":null,"url":"assets/index-OAVDFR0O.css"},{"revision":null,"url":"assets/index-DduNX1Gc.js"},{"revision":null,"url":"assets/TailscalePage-TH3Seutc.js"},{"revision":null,"url":"assets/SettingsPage-B8tXQ_Xt.js"},{"revision":null,"url":"assets/SandboxManager-Doc50lAB.js"},{"revision":null,"url":"assets/RunsPage-uiW4-c8p.js"},{"revision":null,"url":"assets/PromptsPage-COKY3vTB.js"},{"revision":null,"url":"assets/Playground-CZoI80Za.js"},{"revision":null,"url":"assets/MagicUIView-1wNBb6FU.js"},{"revision":null,"url":"assets/MagicUIDashboard-F0yTxNmY.js"},{"revision":null,"url":"assets/LinearSettingsPage-D8G4AAY3.js"},{"revision":null,"url":"assets/LinearOAuthSettingsPage-InO6EwYe.js"},{"revision":null,"url":"assets/IntegrationsPage-BR2Z-pCf.js"},{"revision":null,"url":"assets/DashboardPage-CYELiWmp.js"},{"revision":null,"url":"assets/CronManager-DCmtUDzd.js"},{"revision":null,"url":"assets/AgentsPage-DuPncpk1.js"}]);ne();T(new oe(re("index.html"),{denylist:[/^\/api/,/^\/ws/]}));
|
package/package.json
CHANGED
package/server/cli-launcher.ts
CHANGED
|
@@ -111,6 +111,8 @@ export interface SdkSessionInfo {
|
|
|
111
111
|
totalLinesAdded?: number;
|
|
112
112
|
/** Total lines removed (enriched by REST API) */
|
|
113
113
|
totalLinesRemoved?: number;
|
|
114
|
+
/** Epoch ms of the last user_message (enriched by REST API from the bridge) */
|
|
115
|
+
lastUserMessageAt?: number;
|
|
114
116
|
/** Whether internet/web search is enabled for Codex sessions */
|
|
115
117
|
codexInternetAccess?: boolean;
|
|
116
118
|
/** Sandbox mode selected for Codex sessions */
|
package/server/routes.test.ts
CHANGED
|
@@ -320,6 +320,7 @@ function createMockBridge() {
|
|
|
320
320
|
closeSession: vi.fn(),
|
|
321
321
|
getSession: vi.fn(() => null),
|
|
322
322
|
getAllSessions: vi.fn(() => []),
|
|
323
|
+
getLastUserMessageAt: vi.fn(() => undefined),
|
|
323
324
|
getCodexRateLimits: vi.fn(() => null),
|
|
324
325
|
markContainerized: vi.fn(),
|
|
325
326
|
prePopulateCommands: vi.fn(),
|
|
@@ -615,6 +616,28 @@ describe("GET /api/sessions", () => {
|
|
|
615
616
|
});
|
|
616
617
|
});
|
|
617
618
|
|
|
619
|
+
it("includes each session's lastUserMessageAt from the bridge", async () => {
|
|
620
|
+
// The sidebar sorts by the time of the last user message, which only the
|
|
621
|
+
// bridge knows. Sessions that never received a message have no value (the
|
|
622
|
+
// key is dropped from the JSON), so the client falls back to createdAt.
|
|
623
|
+
const sessions = [
|
|
624
|
+
{ sessionId: "s1", state: "running", cwd: "/a", createdAt: 1000 },
|
|
625
|
+
{ sessionId: "s2", state: "running", cwd: "/b", createdAt: 2000 },
|
|
626
|
+
];
|
|
627
|
+
launcher.listSessions.mockReturnValue(sessions);
|
|
628
|
+
vi.mocked(sessionNames.getAllNames).mockReturnValue({});
|
|
629
|
+
bridge.getLastUserMessageAt.mockImplementation((id: string) => (id === "s1" ? 5000 : undefined));
|
|
630
|
+
|
|
631
|
+
const res = await app.request("/api/sessions", { method: "GET" });
|
|
632
|
+
|
|
633
|
+
expect(res.status).toBe(200);
|
|
634
|
+
const json = await res.json();
|
|
635
|
+
expect(json[0].lastUserMessageAt).toBe(5000);
|
|
636
|
+
expect(json[1]).not.toHaveProperty("lastUserMessageAt");
|
|
637
|
+
expect(bridge.getLastUserMessageAt).toHaveBeenCalledWith("s1");
|
|
638
|
+
expect(bridge.getLastUserMessageAt).toHaveBeenCalledWith("s2");
|
|
639
|
+
});
|
|
640
|
+
|
|
618
641
|
it("prefers bridge cwd over launcher cwd when available", async () => {
|
|
619
642
|
const sessions = [
|
|
620
643
|
{ sessionId: "s1", state: "running", cwd: "/workspace" },
|
package/server/routes.ts
CHANGED
|
@@ -252,6 +252,7 @@ export function createRoutes(
|
|
|
252
252
|
gitBehind: bridge?.git_behind || 0,
|
|
253
253
|
totalLinesAdded: bridge?.total_lines_added || 0,
|
|
254
254
|
totalLinesRemoved: bridge?.total_lines_removed || 0,
|
|
255
|
+
lastUserMessageAt: wsBridge.getLastUserMessageAt(s.sessionId),
|
|
255
256
|
};
|
|
256
257
|
});
|
|
257
258
|
return c.json(enriched);
|
package/server/session-store.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface PersistedSession {
|
|
|
21
21
|
lastAckSeq?: number;
|
|
22
22
|
processedClientMessageIds?: string[];
|
|
23
23
|
archived?: boolean;
|
|
24
|
+
/** Epoch ms of the last user_message (see Session.lastUserMessageAt). */
|
|
25
|
+
lastUserMessageAt?: number;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
// ─── Store ──────────────────────────────────────────────────────────────────
|
|
@@ -16,6 +16,7 @@ export const DEFAULT_ANTHROPIC_MODEL = "claude-opus-5-5";
|
|
|
16
16
|
export const DEFAULT_DASHBOARD_MODEL = "claude-haiku-4-5";
|
|
17
17
|
export const DASHBOARD_MODEL_OPTIONS = [
|
|
18
18
|
"claude-haiku-4-5",
|
|
19
|
+
"claude-haiku-5-5",
|
|
19
20
|
"claude-sonnet-4-6",
|
|
20
21
|
"claude-sonnet-5",
|
|
21
22
|
] as const;
|
|
@@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
|
2
2
|
import {
|
|
3
3
|
appendAndPersist,
|
|
4
4
|
appendHistory,
|
|
5
|
+
findLastUserMessageAt,
|
|
5
6
|
persistSession,
|
|
6
7
|
serializeForStore,
|
|
7
8
|
MESSAGE_HISTORY_LIMIT,
|
|
@@ -216,6 +217,15 @@ describe("appendAndPersist", () => {
|
|
|
216
217
|
// ─── serializeForStore ────────────────────────────────────────────────────────
|
|
217
218
|
|
|
218
219
|
describe("serializeForStore", () => {
|
|
220
|
+
it("includes lastUserMessageAt so sidebar ordering survives restarts", () => {
|
|
221
|
+
const session = makeSession({ lastUserMessageAt: 12345 });
|
|
222
|
+
expect(serializeForStore(session).lastUserMessageAt).toBe(12345);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("leaves lastUserMessageAt undefined when no message was ever sent", () => {
|
|
226
|
+
expect(serializeForStore(makeSession()).lastUserMessageAt).toBeUndefined();
|
|
227
|
+
});
|
|
228
|
+
|
|
219
229
|
it("converts pendingPermissions Map to array of entries", () => {
|
|
220
230
|
const session = makeSession();
|
|
221
231
|
session.pendingPermissions.set("req-1", {
|
|
@@ -294,3 +304,34 @@ describe("serializeForStore", () => {
|
|
|
294
304
|
expect(first).toBe(second);
|
|
295
305
|
});
|
|
296
306
|
});
|
|
307
|
+
|
|
308
|
+
// ─── findLastUserMessageAt ───────────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
describe("findLastUserMessageAt", () => {
|
|
311
|
+
// Backfills Session.lastUserMessageAt for session files persisted before the
|
|
312
|
+
// field existed; it must pick the NEWEST user message, ignoring other types.
|
|
313
|
+
it("returns the timestamp of the newest user_message", () => {
|
|
314
|
+
const history: BrowserIncomingMessage[] = [
|
|
315
|
+
{ type: "user_message", content: "a", timestamp: 100 },
|
|
316
|
+
makeAssistantMsg("m1"),
|
|
317
|
+
{ type: "user_message", content: "b", timestamp: 300 },
|
|
318
|
+
makeAssistantMsg("m2"),
|
|
319
|
+
];
|
|
320
|
+
expect(findLastUserMessageAt(history)).toBe(300);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("returns undefined when the history has no user messages", () => {
|
|
324
|
+
expect(findLastUserMessageAt([])).toBeUndefined();
|
|
325
|
+
expect(findLastUserMessageAt([makeAssistantMsg("m1")])).toBeUndefined();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("skips user messages without a usable timestamp", () => {
|
|
329
|
+
// Defensive: malformed/legacy entries with a zero timestamp must not win
|
|
330
|
+
// over an older valid one.
|
|
331
|
+
const history: BrowserIncomingMessage[] = [
|
|
332
|
+
{ type: "user_message", content: "valid", timestamp: 500 },
|
|
333
|
+
{ type: "user_message", content: "broken", timestamp: 0 },
|
|
334
|
+
];
|
|
335
|
+
expect(findLastUserMessageAt(history)).toBe(500);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
@@ -62,5 +62,22 @@ export function serializeForStore(session: Session): PersistedSession {
|
|
|
62
62
|
nextEventSeq: session.nextEventSeq,
|
|
63
63
|
lastAckSeq: session.lastAckSeq,
|
|
64
64
|
processedClientMessageIds: session.processedClientMessageIds,
|
|
65
|
+
lastUserMessageAt: session.lastUserMessageAt,
|
|
65
66
|
};
|
|
66
67
|
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Timestamp of the newest user_message in a history, or undefined if there is
|
|
71
|
+
* none. Used to backfill Session.lastUserMessageAt for sessions persisted
|
|
72
|
+
* before that field existed (history is capped, so very old sessions whose
|
|
73
|
+
* last prompt scrolled out simply fall back to createdAt on the client).
|
|
74
|
+
*/
|
|
75
|
+
export function findLastUserMessageAt(history: BrowserIncomingMessage[]): number | undefined {
|
|
76
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
77
|
+
const msg = history[i];
|
|
78
|
+
if (msg.type === "user_message" && typeof msg.timestamp === "number" && msg.timestamp > 0) {
|
|
79
|
+
return msg.timestamp;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
@@ -57,6 +57,12 @@ export interface Session {
|
|
|
57
57
|
processedClientMessageIdSet: Set<string>;
|
|
58
58
|
/** Timestamp of last non-keepalive CLI message (for idle detection) */
|
|
59
59
|
lastCliActivityTs: number;
|
|
60
|
+
/**
|
|
61
|
+
* Epoch ms of the most recent user_message routed into this session (typed by
|
|
62
|
+
* a human or injected by cron/agents). Drives the sidebar's "recently used
|
|
63
|
+
* first" ordering. Persisted; undefined until the first message is sent.
|
|
64
|
+
*/
|
|
65
|
+
lastUserMessageAt?: number;
|
|
60
66
|
/** Formal session state machine tracking phase and validating transitions. */
|
|
61
67
|
stateMachine: SessionStateMachine;
|
|
62
68
|
/** Cleanup function for state machine transition listener — call on session teardown. */
|
package/server/ws-bridge.test.ts
CHANGED
|
@@ -2422,6 +2422,45 @@ describe("Browser message routing", () => {
|
|
|
2422
2422
|
}
|
|
2423
2423
|
});
|
|
2424
2424
|
|
|
2425
|
+
it("user_message: records lastUserMessageAt for sidebar ordering", () => {
|
|
2426
|
+
// The sidebar orders sessions by the last time a user sent a message, so
|
|
2427
|
+
// routing a user_message must stamp the session (and expose it via
|
|
2428
|
+
// getLastUserMessageAt, which GET /api/sessions reads).
|
|
2429
|
+
const session = bridge.getSession("s1")!;
|
|
2430
|
+
expect(bridge.getLastUserMessageAt("s1")).toBeUndefined();
|
|
2431
|
+
|
|
2432
|
+
const before = Date.now();
|
|
2433
|
+
bridge.handleBrowserMessage(browser, JSON.stringify({
|
|
2434
|
+
type: "user_message",
|
|
2435
|
+
content: "bump me to the top",
|
|
2436
|
+
}));
|
|
2437
|
+
const after = Date.now();
|
|
2438
|
+
|
|
2439
|
+
expect(session.lastUserMessageAt).toBeGreaterThanOrEqual(before);
|
|
2440
|
+
expect(session.lastUserMessageAt).toBeLessThanOrEqual(after);
|
|
2441
|
+
expect(bridge.getLastUserMessageAt("s1")).toBe(session.lastUserMessageAt);
|
|
2442
|
+
// Same timestamp as the history entry, so restore/backfill stays consistent
|
|
2443
|
+
const entry = session.messageHistory[session.messageHistory.length - 1];
|
|
2444
|
+
expect(entry.type === "user_message" && entry.timestamp).toBe(session.lastUserMessageAt);
|
|
2445
|
+
});
|
|
2446
|
+
|
|
2447
|
+
it("user_message: rejected messages do not bump lastUserMessageAt", () => {
|
|
2448
|
+
// An attachment that fails validation is dropped before reaching the
|
|
2449
|
+
// agent, so it must not reorder the sidebar either.
|
|
2450
|
+
bridge.handleBrowserMessage(browser, JSON.stringify({
|
|
2451
|
+
type: "user_message",
|
|
2452
|
+
content: "bad attachment",
|
|
2453
|
+
// Invalid base64 payload → validateAttachments rejects the whole message
|
|
2454
|
+
attachments: [{ name: "x.bin", media_type: "application/octet-stream", data: "not base64!", size: 8 }],
|
|
2455
|
+
}));
|
|
2456
|
+
expect(cli.send).not.toHaveBeenCalled();
|
|
2457
|
+
expect(bridge.getLastUserMessageAt("s1")).toBeUndefined();
|
|
2458
|
+
});
|
|
2459
|
+
|
|
2460
|
+
it("getLastUserMessageAt: returns undefined for unknown sessions", () => {
|
|
2461
|
+
expect(bridge.getLastUserMessageAt("does-not-exist")).toBeUndefined();
|
|
2462
|
+
});
|
|
2463
|
+
|
|
2425
2464
|
it("user_message: queues when CLI not connected", () => {
|
|
2426
2465
|
// Close CLI
|
|
2427
2466
|
bridge.handleCLIClose(cli);
|
|
@@ -3018,6 +3057,89 @@ describe("Persistence", () => {
|
|
|
3018
3057
|
expect(session!.processedClientMessageIdSet.has("restored-client-1")).toBe(true);
|
|
3019
3058
|
});
|
|
3020
3059
|
|
|
3060
|
+
it("restoreFromDisk: keeps the persisted lastUserMessageAt", () => {
|
|
3061
|
+
// The persisted field wins over the history scan: history is capped, so the
|
|
3062
|
+
// stored value is the only reliable record once old prompts scroll out.
|
|
3063
|
+
store.saveSync({
|
|
3064
|
+
id: "persisted-lum",
|
|
3065
|
+
state: {
|
|
3066
|
+
session_id: "persisted-lum",
|
|
3067
|
+
model: "claude-sonnet-4-6",
|
|
3068
|
+
cwd: "/saved",
|
|
3069
|
+
tools: [],
|
|
3070
|
+
permissionMode: "default",
|
|
3071
|
+
claude_code_version: "1.0",
|
|
3072
|
+
mcp_servers: [],
|
|
3073
|
+
agents: [],
|
|
3074
|
+
slash_commands: [],
|
|
3075
|
+
skills: [],
|
|
3076
|
+
total_cost_usd: 0,
|
|
3077
|
+
num_turns: 2,
|
|
3078
|
+
context_used_percent: 0,
|
|
3079
|
+
is_compacting: false,
|
|
3080
|
+
git_branch: "",
|
|
3081
|
+
is_worktree: false,
|
|
3082
|
+
is_containerized: false,
|
|
3083
|
+
repo_root: "",
|
|
3084
|
+
git_ahead: 0,
|
|
3085
|
+
git_behind: 0,
|
|
3086
|
+
total_lines_added: 0,
|
|
3087
|
+
total_lines_removed: 0,
|
|
3088
|
+
},
|
|
3089
|
+
messageHistory: [
|
|
3090
|
+
{ type: "user_message", content: "older", timestamp: 1000 },
|
|
3091
|
+
],
|
|
3092
|
+
pendingMessages: [],
|
|
3093
|
+
pendingPermissions: [],
|
|
3094
|
+
lastUserMessageAt: 9000,
|
|
3095
|
+
});
|
|
3096
|
+
|
|
3097
|
+
bridge.restoreFromDisk();
|
|
3098
|
+
expect(bridge.getLastUserMessageAt("persisted-lum")).toBe(9000);
|
|
3099
|
+
});
|
|
3100
|
+
|
|
3101
|
+
it("restoreFromDisk: backfills lastUserMessageAt from history for older session files", () => {
|
|
3102
|
+
// Session files written before the field existed have no lastUserMessageAt;
|
|
3103
|
+
// the newest user_message in the history is the best available answer.
|
|
3104
|
+
store.saveSync({
|
|
3105
|
+
id: "persisted-legacy",
|
|
3106
|
+
state: {
|
|
3107
|
+
session_id: "persisted-legacy",
|
|
3108
|
+
model: "claude-sonnet-4-6",
|
|
3109
|
+
cwd: "/saved",
|
|
3110
|
+
tools: [],
|
|
3111
|
+
permissionMode: "default",
|
|
3112
|
+
claude_code_version: "1.0",
|
|
3113
|
+
mcp_servers: [],
|
|
3114
|
+
agents: [],
|
|
3115
|
+
slash_commands: [],
|
|
3116
|
+
skills: [],
|
|
3117
|
+
total_cost_usd: 0,
|
|
3118
|
+
num_turns: 2,
|
|
3119
|
+
context_used_percent: 0,
|
|
3120
|
+
is_compacting: false,
|
|
3121
|
+
git_branch: "",
|
|
3122
|
+
is_worktree: false,
|
|
3123
|
+
is_containerized: false,
|
|
3124
|
+
repo_root: "",
|
|
3125
|
+
git_ahead: 0,
|
|
3126
|
+
git_behind: 0,
|
|
3127
|
+
total_lines_added: 0,
|
|
3128
|
+
total_lines_removed: 0,
|
|
3129
|
+
},
|
|
3130
|
+
messageHistory: [
|
|
3131
|
+
{ type: "user_message", content: "first", timestamp: 1000 },
|
|
3132
|
+
{ type: "user_message", content: "second", timestamp: 2000 },
|
|
3133
|
+
{ type: "result", data: { type: "result", subtype: "success" } } as any,
|
|
3134
|
+
],
|
|
3135
|
+
pendingMessages: [],
|
|
3136
|
+
pendingPermissions: [],
|
|
3137
|
+
});
|
|
3138
|
+
|
|
3139
|
+
bridge.restoreFromDisk();
|
|
3140
|
+
expect(bridge.getLastUserMessageAt("persisted-legacy")).toBe(2000);
|
|
3141
|
+
});
|
|
3142
|
+
|
|
3021
3143
|
it("restoreFromDisk: does not overwrite live sessions", () => {
|
|
3022
3144
|
// Create a live session first
|
|
3023
3145
|
const liveSession = bridge.getOrCreateSession("live-1");
|
package/server/ws-bridge.ts
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
} from "./ws-bridge-browser-ingest.js";
|
|
34
34
|
import {
|
|
35
35
|
appendHistory as appendHistoryFn,
|
|
36
|
+
findLastUserMessageAt,
|
|
36
37
|
persistSession as persistSessionFn,
|
|
37
38
|
} from "./ws-bridge-persist.js";
|
|
38
39
|
import {
|
|
@@ -216,6 +217,10 @@ export class WsBridge {
|
|
|
216
217
|
Array.isArray(p.processedClientMessageIds) ? p.processedClientMessageIds : [],
|
|
217
218
|
),
|
|
218
219
|
lastCliActivityTs: Date.now(),
|
|
220
|
+
lastUserMessageAt:
|
|
221
|
+
typeof p.lastUserMessageAt === "number"
|
|
222
|
+
? p.lastUserMessageAt
|
|
223
|
+
: findLastUserMessageAt(p.messageHistory || []),
|
|
219
224
|
stateMachine: new SessionStateMachine(p.id, "terminated"),
|
|
220
225
|
};
|
|
221
226
|
session.state.backend_type = session.backendType;
|
|
@@ -374,6 +379,11 @@ export class WsBridge {
|
|
|
374
379
|
return Array.from(this.sessions.values()).map((s) => s.state);
|
|
375
380
|
}
|
|
376
381
|
|
|
382
|
+
/** Epoch ms of the last user_message sent to a session (undefined if none yet). */
|
|
383
|
+
getLastUserMessageAt(sessionId: string): number | undefined {
|
|
384
|
+
return this.sessions.get(sessionId)?.lastUserMessageAt;
|
|
385
|
+
}
|
|
386
|
+
|
|
377
387
|
/** Return per-session memory stats for diagnostics. */
|
|
378
388
|
getSessionMemoryStats(): { id: string; browsers: number; historyLen: number; eventBufferLen: number; pendingMsgs: number }[] {
|
|
379
389
|
return Array.from(this.sessions.values()).map((s) => ({
|
|
@@ -1426,6 +1436,7 @@ export class WsBridge {
|
|
|
1426
1436
|
attachments: msg.attachments,
|
|
1427
1437
|
};
|
|
1428
1438
|
this.appendHistory(session, userMessage);
|
|
1439
|
+
session.lastUserMessageAt = ts;
|
|
1429
1440
|
const transitioned = session.stateMachine.transition("streaming", "user_message");
|
|
1430
1441
|
if (!transitioned) {
|
|
1431
1442
|
// Session not ready yet (e.g. still initializing). Log a warning so
|