@hellcoder/companion 0.116.1-preview.20260814042234.2d329a2 → 0.117.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.
Files changed (34) hide show
  1. package/dist/assets/{AgentsPage-BAalV1v6.js → AgentsPage-DJIGGU86.js} +1 -1
  2. package/dist/assets/{CronManager-B2Vo1YmB.js → CronManager-D7aAMVgs.js} +1 -1
  3. package/dist/assets/{DashboardPage-BOvDiP1W.js → DashboardPage-Bporj7Vr.js} +1 -1
  4. package/dist/assets/{IntegrationsPage-kOXLH5KU.js → IntegrationsPage-BSY0-fsO.js} +1 -1
  5. package/dist/assets/{LinearOAuthSettingsPage-H-uUJ-ub.js → LinearOAuthSettingsPage-Cl5dP05g.js} +1 -1
  6. package/dist/assets/{LinearSettingsPage-CD_x4AGr.js → LinearSettingsPage-dwMpnpxL.js} +1 -1
  7. package/dist/assets/{MagicUIDashboard-9Jgiv4rb.js → MagicUIDashboard-Bpa4AupX.js} +90 -4
  8. package/dist/assets/{MagicUIView-BLEcS9XO.js → MagicUIView-BoHMBELi.js} +1 -1
  9. package/dist/assets/{Playground-QLrd9q16.js → Playground-By2aXWQF.js} +5 -5
  10. package/dist/assets/{PromptsPage-11eJ6ak7.js → PromptsPage-DVv0Qn3d.js} +1 -1
  11. package/dist/assets/{RunsPage-Bz3udIL2.js → RunsPage-bu3TZ6ai.js} +1 -1
  12. package/dist/assets/{SandboxManager-Dv47pbC6.js → SandboxManager-CDjGbD5N.js} +1 -1
  13. package/dist/assets/{SettingsPage-C_2ftG-C.js → SettingsPage-D0v_BZTU.js} +1 -1
  14. package/dist/assets/{TailscalePage-DzMX_7XA.js → TailscalePage-B8SJQpMF.js} +1 -1
  15. package/dist/assets/index-BqOdTpct.css +1 -0
  16. package/dist/assets/index-DqIZ_Ol5.js +140 -0
  17. package/dist/assets/{sw-register-CarYQEeI.js → sw-register-CvyRJNug.js} +1 -1
  18. package/dist/index.html +2 -2
  19. package/dist/sw.js +1 -1
  20. package/package.json +1 -1
  21. package/server/linear-agent-bridge.ts +0 -1
  22. package/server/magic-ui-ops.test.ts +59 -0
  23. package/server/magic-ui-ops.ts +29 -0
  24. package/server/magic-ui-types.ts +26 -2
  25. package/server/magic-ui-watcher.test.ts +85 -0
  26. package/server/magic-ui-watcher.ts +10 -2
  27. package/server/routes/system-routes.test.ts +401 -0
  28. package/server/routes/system-routes.ts +10 -0
  29. package/server/system-disk.test.ts +48 -0
  30. package/server/system-disk.ts +55 -0
  31. package/server/system-memory.test.ts +32 -0
  32. package/server/system-memory.ts +32 -0
  33. package/dist/assets/index-DSCRqqpK.css +0 -1
  34. package/dist/assets/index-DspbwgSL.js +0 -140
@@ -1 +1 @@
1
- import{_ as u}from"./index-DspbwgSL.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")}});
1
+ import{_ as u}from"./index-DqIZ_Ol5.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,8 +11,8 @@
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-DspbwgSL.js"></script>
15
- <link rel="stylesheet" crossorigin href="/assets/index-DSCRqqpK.css">
14
+ <script type="module" crossorigin src="/assets/index-DqIZ_Ol5.js"></script>
15
+ <link rel="stylesheet" crossorigin href="/assets/index-BqOdTpct.css">
16
16
  </head>
17
17
  <body class="bg-cc-bg text-cc-fg">
18
18
  <div id="root"></div>
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":"435beaa5ccfe53f5052e5e930e8e8f3f","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-CarYQEeI.js"},{"revision":null,"url":"assets/index-DspbwgSL.js"},{"revision":null,"url":"assets/index-DSCRqqpK.css"},{"revision":null,"url":"assets/TailscalePage-DzMX_7XA.js"},{"revision":null,"url":"assets/SettingsPage-C_2ftG-C.js"},{"revision":null,"url":"assets/SandboxManager-Dv47pbC6.js"},{"revision":null,"url":"assets/RunsPage-Bz3udIL2.js"},{"revision":null,"url":"assets/PromptsPage-11eJ6ak7.js"},{"revision":null,"url":"assets/Playground-QLrd9q16.js"},{"revision":null,"url":"assets/MagicUIView-BLEcS9XO.js"},{"revision":null,"url":"assets/MagicUIDashboard-9Jgiv4rb.js"},{"revision":null,"url":"assets/LinearSettingsPage-CD_x4AGr.js"},{"revision":null,"url":"assets/LinearOAuthSettingsPage-H-uUJ-ub.js"},{"revision":null,"url":"assets/IntegrationsPage-kOXLH5KU.js"},{"revision":null,"url":"assets/DashboardPage-BOvDiP1W.js"},{"revision":null,"url":"assets/CronManager-B2Vo1YmB.js"},{"revision":null,"url":"assets/AgentsPage-BAalV1v6.js"}]);ne();T(new oe(re("index.html"),{denylist:[/^\/api/,/^\/ws/]}));
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":"e1fd399a6dc88dfc55dc2d27a6bc45fc","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-CvyRJNug.js"},{"revision":null,"url":"assets/index-DqIZ_Ol5.js"},{"revision":null,"url":"assets/index-BqOdTpct.css"},{"revision":null,"url":"assets/TailscalePage-B8SJQpMF.js"},{"revision":null,"url":"assets/SettingsPage-D0v_BZTU.js"},{"revision":null,"url":"assets/SandboxManager-CDjGbD5N.js"},{"revision":null,"url":"assets/RunsPage-bu3TZ6ai.js"},{"revision":null,"url":"assets/PromptsPage-DVv0Qn3d.js"},{"revision":null,"url":"assets/Playground-By2aXWQF.js"},{"revision":null,"url":"assets/MagicUIView-BoHMBELi.js"},{"revision":null,"url":"assets/MagicUIDashboard-Bpa4AupX.js"},{"revision":null,"url":"assets/LinearSettingsPage-dwMpnpxL.js"},{"revision":null,"url":"assets/LinearOAuthSettingsPage-Cl5dP05g.js"},{"revision":null,"url":"assets/IntegrationsPage-BSY0-fsO.js"},{"revision":null,"url":"assets/DashboardPage-Bporj7Vr.js"},{"revision":null,"url":"assets/CronManager-D7aAMVgs.js"},{"revision":null,"url":"assets/AgentsPage-DJIGGU86.js"}]);ne();T(new oe(re("index.html"),{denylist:[/^\/api/,/^\/ws/]}));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hellcoder/companion",
3
- "version": "0.116.1-preview.20260814042234.2d329a2",
3
+ "version": "0.117.0",
4
4
  "type": "module",
5
5
  "description": "Web UI for launching and interacting with Claude Code agents — Moritz Edition (fork of the-companion)",
6
6
  "license": "MIT",
@@ -10,7 +10,6 @@
10
10
 
11
11
  import type { AgentExecutor } from "./agent-executor.js";
12
12
  import type { WsBridge } from "./ws-bridge.js";
13
- import type { BrowserIncomingMessage } from "./session-types.js";
14
13
  import type { AgentConfig } from "./agent-types.js";
15
14
  import * as agentStore from "./agent-store.js";
16
15
  import * as linearAgent from "./linear-agent.js";
@@ -223,6 +223,65 @@ describe("applyOps", () => {
223
223
  });
224
224
  });
225
225
 
226
+ describe("new_topic (subject change)", () => {
227
+ it("archives the whole current board and clears it for the new subject", () => {
228
+ let state = applyOps(
229
+ emptyMagicUiState(NOW),
230
+ validateOps([
231
+ { op: "set_slot", slot: "cpu", html: "<p>CPU 2%</p>", area: "hero" },
232
+ { op: "chart", slot: "load", spec: { kind: "line", series: [{ label: "load", data: [1, 2] }] } },
233
+ ]),
234
+ NOW,
235
+ );
236
+ state = { ...state, currentTopicTitle: "Server health" };
237
+
238
+ state = applyOps(state, validateOps([
239
+ { op: "new_topic", title: "Project overview" },
240
+ { op: "set_slot", slot: "projects", html: "<p>84 commits</p>", area: "hero" },
241
+ ]), NOW + 10);
242
+
243
+ // Old board is gone from the visible slots...
244
+ expect(state.slots.cpu).toBeUndefined();
245
+ expect(state.slots.load).toBeUndefined();
246
+ expect(state.slots.projects).toBeDefined();
247
+ expect(state.currentTopicTitle).toBe("Project overview");
248
+ // ...and preserved as an archived topic under the PREVIOUS title.
249
+ expect(state.topics).toHaveLength(1);
250
+ expect(state.topics[0].title).toBe("Server health");
251
+ expect(state.topics[0].slots.cpu?.html).toContain("CPU 2%");
252
+ expect(state.topics[0].slots.load?.chart?.kind).toBe("line");
253
+ expect(state.topics[0].layout.length).toBeGreaterThan(0);
254
+ });
255
+
256
+ it("does not archive an empty board, just retitles", () => {
257
+ const state = applyOps(
258
+ emptyMagicUiState(NOW),
259
+ validateOps([{ op: "new_topic", title: "First subject" }]),
260
+ NOW,
261
+ );
262
+ expect(state.topics).toHaveLength(0);
263
+ expect(state.currentTopicTitle).toBe("First subject");
264
+ });
265
+
266
+ it("caps archived topics at the retention limit", () => {
267
+ let state = emptyMagicUiState(NOW);
268
+ for (let i = 0; i < 12; i++) {
269
+ state = applyOps(state, validateOps([
270
+ { op: "set_slot", slot: `s${i}`, html: `<p>${i}</p>` },
271
+ { op: "new_topic", title: `topic-${i}` },
272
+ ]), NOW + i);
273
+ }
274
+ // set_slot runs before new_topic in each batch, so every round archives.
275
+ expect(state.topics.length).toBeLessThanOrEqual(8);
276
+ // Newest first
277
+ expect(state.topics[0].slots[`s11`]).toBeDefined();
278
+ });
279
+
280
+ it("rejects a new_topic without a title", () => {
281
+ expect(validateOps([{ op: "new_topic" }, { op: "new_topic", title: "" }])).toEqual([]);
282
+ });
283
+ });
284
+
226
285
  describe("appendDecision", () => {
227
286
  it("prepends server-generated entries and bumps the version", () => {
228
287
  const state = emptyMagicUiState(NOW);
@@ -19,6 +19,7 @@ import {
19
19
  MAGIC_UI_MAX_OPS_PER_TURN,
20
20
  MAGIC_UI_MAX_SLOTS,
21
21
  MAGIC_UI_MAX_SNIPPET_CHARS,
22
+ MAGIC_UI_MAX_TOPICS,
22
23
  type MagicChartSpec,
23
24
  type MagicUiArea,
24
25
  type MagicUiDashboardState,
@@ -207,6 +208,10 @@ export function validateOps(raw: unknown[]): MagicUiOp[] {
207
208
  });
208
209
  break;
209
210
  }
211
+ case "new_topic": {
212
+ if (isNonEmptyString(o.title, 80)) ops.push({ op: "new_topic", title: o.title });
213
+ break;
214
+ }
210
215
  case "open_item": {
211
216
  if (!isNonEmptyString(o.id, 80) || !isNonEmptyString(o.text, 500)) break;
212
217
  ops.push({
@@ -240,6 +245,7 @@ export function validateOps(raw: unknown[]): MagicUiOp[] {
240
245
  }
241
246
 
242
247
  let decisionCounter = 0;
248
+ let topicCounter = 0;
243
249
 
244
250
  /** Server-generated decision entry (user choices, AI auto-resolutions). */
245
251
  export function makeDecisionEntry(
@@ -286,10 +292,33 @@ export function applyOps(
286
292
  layout: [...state.layout],
287
293
  decisionLog: [...state.decisionLog],
288
294
  openItems: [...state.openItems],
295
+ topics: [...state.topics],
289
296
  };
290
297
 
291
298
  for (const op of ops) {
292
299
  switch (op.op) {
300
+ case "new_topic": {
301
+ // Subject change: archive the ENTIRE current board as a collapsed
302
+ // topic and hand the model a clean slate. Server-enforced so stale
303
+ // content always leaves the screen even if the model forgets to
304
+ // remove slots itself.
305
+ if (Object.keys(next.slots).length > 0) {
306
+ next.topics = [
307
+ {
308
+ id: `topic-${now}-${topicCounter++}`,
309
+ title: next.currentTopicTitle || "Earlier",
310
+ ts: now,
311
+ slots: next.slots,
312
+ layout: next.layout,
313
+ },
314
+ ...next.topics,
315
+ ].slice(0, MAGIC_UI_MAX_TOPICS);
316
+ }
317
+ next.slots = {};
318
+ next.layout = [];
319
+ next.currentTopicTitle = op.title;
320
+ break;
321
+ }
293
322
  case "set_slot":
294
323
  next.slots[op.slot] = { title: op.title, html: op.html, updatedAt: now };
295
324
  next.layout = upsertLayout(next.layout, op.slot, op.area, op.span);
@@ -78,6 +78,20 @@ export interface MagicUiLayoutEntry {
78
78
 
79
79
  export type MagicUiStatus = "live" | "degraded" | "stopped";
80
80
 
81
+ /**
82
+ * An archived board: when the session changes subject the watcher emits
83
+ * new_topic and the server moves the entire current board here. Topics are
84
+ * rendered as collapsed chips the user can reopen — the visible dashboard
85
+ * always shows only the current subject.
86
+ */
87
+ export interface MagicUiTopic {
88
+ id: string;
89
+ title: string;
90
+ ts: number;
91
+ slots: Record<string, MagicUiSlot>;
92
+ layout: MagicUiLayoutEntry[];
93
+ }
94
+
81
95
  /** Patch operations the watcher model may emit (one JSON array per turn). */
82
96
  export type MagicUiOp =
83
97
  | { op: "set_slot"; slot: string; html: string; title?: string; area?: MagicUiArea; span?: 1 | 2 | 3 }
@@ -86,6 +100,7 @@ export type MagicUiOp =
86
100
  | { op: "chart"; slot: string; spec: MagicChartSpec; title?: string; area?: MagicUiArea; span?: 1 | 2 | 3 }
87
101
  | { op: "stat"; slot: string; label: string; value: string; trend?: MagicUiTrend; area?: MagicUiArea; span?: 1 | 2 | 3 }
88
102
  | { op: "snippet"; slot: string; title: string; code: string; language?: string; area?: MagicUiArea; span?: 1 | 2 | 3 }
103
+ | { op: "new_topic"; title: string }
89
104
  | { op: "open_item"; id: string; text: string; kind?: "action" | "question" | "blocker" }
90
105
  | { op: "resolve_item"; id: string }
91
106
  | { op: "decision_log"; title: string; detail: string }
@@ -100,6 +115,10 @@ export interface MagicUiDashboardState {
100
115
  decisionLog: MagicUiDecisionEntry[];
101
116
  /** Unresolved user-facing points, newest first. */
102
117
  openItems: MagicUiOpenItem[];
118
+ /** Archived boards from earlier subjects, newest first. */
119
+ topics: MagicUiTopic[];
120
+ /** Title of the subject the current board covers. */
121
+ currentTopicTitle: string;
103
122
  /** Rolling watcher-maintained summary; the seed for watcher restarts. Not rendered. */
104
123
  sessionSummary: string;
105
124
  status: MagicUiStatus;
@@ -113,6 +132,8 @@ export function emptyMagicUiState(now: number): MagicUiDashboardState {
113
132
  layout: [],
114
133
  decisionLog: [],
115
134
  openItems: [],
135
+ topics: [],
136
+ currentTopicTitle: "Session",
116
137
  sessionSummary: "",
117
138
  status: "live",
118
139
  updatedAt: now,
@@ -121,8 +142,11 @@ export function emptyMagicUiState(now: number): MagicUiDashboardState {
121
142
 
122
143
  // ── Budgets (enforced server-side in magic-ui-ops.ts) ────────────────────────
123
144
 
124
- /** Hard cap on named slots; the prompt asks the model to stay well under. */
125
- export const MAGIC_UI_MAX_SLOTS = 24;
145
+ /** Hard cap on named slots; the prompt asks the model to stay well under.
146
+ * Kept close to the one-screen budget — beyond it the oldest slots evict. */
147
+ export const MAGIC_UI_MAX_SLOTS = 12;
148
+ /** Archived-topic retention (newest first). */
149
+ export const MAGIC_UI_MAX_TOPICS = 8;
126
150
  /** Per-slot sanitized HTML budget (chars). */
127
151
  export const MAGIC_UI_MAX_HTML_CHARS = 8_192;
128
152
  /** Per-snippet code budget (chars). */
@@ -286,3 +286,88 @@ describe("MagicUiWatcherManager", () => {
286
286
  manager.shutdown();
287
287
  });
288
288
  });
289
+
290
+ // The watcher's query can die at any time (SDK error, auth failure, killed
291
+ // subprocess). handleCrash retries it with exponential backoff and gives up
292
+ // into a "degraded" status after MAX_CRASH_RETRIES, so a permanently broken
293
+ // watcher stops burning tokens instead of hot-looping forever.
294
+ describe("MagicUiWatcher crash handling", () => {
295
+ /** A query that always throws, counting how many times it was started. */
296
+ function makeCrashingQuery() {
297
+ let calls = 0;
298
+ const fn = (() => {
299
+ calls += 1;
300
+ // eslint-disable-next-line require-yield
301
+ async function* gen(): AsyncGenerator<never> {
302
+ throw new Error("query exploded");
303
+ }
304
+ return gen();
305
+ }) as unknown as typeof import("@anthropic-ai/claude-agent-sdk").query;
306
+ return { fn, calls: () => calls };
307
+ }
308
+
309
+ function makeCrashWatcher() {
310
+ const { fn, calls } = makeCrashingQuery();
311
+ const watcher = new MagicUiWatcher("sess-1", {
312
+ broadcast: (sessionId, msg) => broadcasts.push({ sessionId, msg }),
313
+ store: store as unknown as MagicUiStore,
314
+ model: "claude-haiku-4-5",
315
+ queryFn: fn,
316
+ });
317
+ return { watcher, calls };
318
+ }
319
+
320
+ it("retries a crashed query with exponential backoff, then degrades", async () => {
321
+ vi.useFakeTimers();
322
+ try {
323
+ const { watcher, calls } = makeCrashWatcher();
324
+ watcher.start();
325
+
326
+ // First attempt crashes almost immediately.
327
+ await vi.advanceTimersByTimeAsync(0);
328
+ expect(calls()).toBe(1);
329
+
330
+ // Backoff is 2s, then 4s, then 8s — capped at 30s. Nothing should
331
+ // restart before the delay elapses.
332
+ await vi.advanceTimersByTimeAsync(1_999);
333
+ expect(calls()).toBe(1);
334
+
335
+ await vi.advanceTimersByTimeAsync(1);
336
+ expect(calls()).toBe(2);
337
+
338
+ await vi.advanceTimersByTimeAsync(4_000);
339
+ expect(calls()).toBe(3);
340
+
341
+ await vi.advanceTimersByTimeAsync(8_000);
342
+ expect(calls()).toBe(4);
343
+
344
+ // The 4th crash exceeds MAX_CRASH_RETRIES (3): give up rather than
345
+ // retry a 5th time, and surface the failure to the browser.
346
+ await vi.advanceTimersByTimeAsync(60_000);
347
+ expect(calls()).toBe(4);
348
+
349
+ const last = broadcasts.at(-1);
350
+ expect((last?.msg as { state: MagicUiDashboardState }).state.status).toBe("degraded");
351
+ } finally {
352
+ vi.useRealTimers();
353
+ }
354
+ });
355
+
356
+ it("does not retry after the watcher has been stopped", async () => {
357
+ vi.useFakeTimers();
358
+ try {
359
+ const { watcher, calls } = makeCrashWatcher();
360
+ watcher.start();
361
+ await vi.advanceTimersByTimeAsync(0);
362
+ expect(calls()).toBe(1);
363
+
364
+ // A stop() racing a pending backoff must win — otherwise a stopped
365
+ // session keeps spawning Haiku queries in the background.
366
+ watcher.stop();
367
+ await vi.advanceTimersByTimeAsync(30_000);
368
+ expect(calls()).toBe(1);
369
+ } finally {
370
+ vi.useRealTimers();
371
+ }
372
+ });
373
+ });
@@ -33,7 +33,6 @@ import {
33
33
  validateOps,
34
34
  } from "./magic-ui-ops.js";
35
35
  import {
36
- emptyMagicUiState,
37
36
  type MagicUiDashboardState,
38
37
  type MagicUiStatus,
39
38
  } from "./magic-ui-types.js";
@@ -75,12 +74,15 @@ OPS:
75
74
  - {"op":"chart","slot":"...","spec":{"kind":"bar|line|donut|sparkline","labels":[...],"series":[{"label":"...","data":[...]}]},"title":"...","area":"...","span":...}
76
75
  - {"op":"stat","slot":"...","label":"...","value":"...","trend":"up|down|flat","area":"...","span":...}
77
76
  - {"op":"snippet","slot":"...","title":"...","code":"...","language":"bash"} — for any script/command the USER must run themselves; rendered copyable.
77
+ - {"op":"new_topic","title":"<short title for the NEW subject>"} — TOPIC CHANGE. Archives the entire current board into a collapsed, reopenable "topic" chip and gives you a clean board. Emit this FIRST (before any set_slot) whenever the session moves to a clearly different subject.
78
78
  - {"op":"open_item","id":"<stable-id>","text":"...","kind":"action|question|blocker"} — OPEN points where the agent waits on the user (questions asked, manual steps requested, blockers). Keep them current; emit {"op":"resolve_item","id":"..."} the moment a point is addressed.
79
79
  - {"op":"decision_log","title":"...","detail":"..."} — ONLY for notable choices the agent itself made (approach picked, tradeoff taken). User choices are logged automatically — never duplicate them.
80
80
  - {"op":"session_summary","text":"..."} — maintain a rolling ~5 sentence summary of the WHOLE session. Refresh it every few turns. It is not rendered; it is your own memory across restarts.
81
81
 
82
82
  DASHBOARD RULES:
83
83
  - Fixed viewport, NO scrolling: keep at most 8 visible slots. When new content arrives, CONDENSE — merge older work into a compact summary slot or remove it — never just add.
84
+ - TOPIC DISCIPLINE: the visible board covers ONE subject at a time. If the digest is about something clearly different from the current board, emit new_topic first — do NOT leave slots from the previous subject on screen. Small follow-ups to the same subject update existing slots instead.
85
+ - NEVER create slots about yourself, the watcher, the dashboard, releases of this app, or "collecting/scanning/working…" progress placeholders. Only paint what the session actually produced — wait for real results instead of announcing that they are coming.
84
86
  - Newest/most important activity belongs in area "hero". Important points of the discussion must stay visible in a condensed form.
85
87
  - Charts and stats ONLY for genuinely numeric data (tests passed, files changed, durations, counts). Never invent numbers.
86
88
  - Decisions and open points are first-class: reference pending decisions in content, but NEVER build buttons or forms — the runtime renders the real controls.
@@ -193,7 +195,13 @@ export class MagicUiWatcher {
193
195
  /** First message of a (re)started watcher session: current dashboard + summary. */
194
196
  private seedMessage(): string | null {
195
197
  if (this.state.version === 0) return null;
196
- const seedState = { slots: this.state.slots, layout: this.state.layout, openItems: this.state.openItems };
198
+ const seedState = {
199
+ currentTopic: this.state.currentTopicTitle,
200
+ slots: this.state.slots,
201
+ layout: this.state.layout,
202
+ openItems: this.state.openItems,
203
+ archivedTopics: this.state.topics.map((t) => t.title),
204
+ };
197
205
  return [
198
206
  "You are resuming a dashboard you maintain. CURRENT DASHBOARD STATE:",
199
207
  JSON.stringify(seedState).slice(0, 12_000),