@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.
- package/dist/assets/{AgentsPage-BAalV1v6.js → AgentsPage-DJIGGU86.js} +1 -1
- package/dist/assets/{CronManager-B2Vo1YmB.js → CronManager-D7aAMVgs.js} +1 -1
- package/dist/assets/{DashboardPage-BOvDiP1W.js → DashboardPage-Bporj7Vr.js} +1 -1
- package/dist/assets/{IntegrationsPage-kOXLH5KU.js → IntegrationsPage-BSY0-fsO.js} +1 -1
- package/dist/assets/{LinearOAuthSettingsPage-H-uUJ-ub.js → LinearOAuthSettingsPage-Cl5dP05g.js} +1 -1
- package/dist/assets/{LinearSettingsPage-CD_x4AGr.js → LinearSettingsPage-dwMpnpxL.js} +1 -1
- package/dist/assets/{MagicUIDashboard-9Jgiv4rb.js → MagicUIDashboard-Bpa4AupX.js} +90 -4
- package/dist/assets/{MagicUIView-BLEcS9XO.js → MagicUIView-BoHMBELi.js} +1 -1
- package/dist/assets/{Playground-QLrd9q16.js → Playground-By2aXWQF.js} +5 -5
- package/dist/assets/{PromptsPage-11eJ6ak7.js → PromptsPage-DVv0Qn3d.js} +1 -1
- package/dist/assets/{RunsPage-Bz3udIL2.js → RunsPage-bu3TZ6ai.js} +1 -1
- package/dist/assets/{SandboxManager-Dv47pbC6.js → SandboxManager-CDjGbD5N.js} +1 -1
- package/dist/assets/{SettingsPage-C_2ftG-C.js → SettingsPage-D0v_BZTU.js} +1 -1
- package/dist/assets/{TailscalePage-DzMX_7XA.js → TailscalePage-B8SJQpMF.js} +1 -1
- package/dist/assets/index-BqOdTpct.css +1 -0
- package/dist/assets/index-DqIZ_Ol5.js +140 -0
- package/dist/assets/{sw-register-CarYQEeI.js → sw-register-CvyRJNug.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/sw.js +1 -1
- package/package.json +1 -1
- package/server/linear-agent-bridge.ts +0 -1
- package/server/magic-ui-ops.test.ts +59 -0
- package/server/magic-ui-ops.ts +29 -0
- package/server/magic-ui-types.ts +26 -2
- package/server/magic-ui-watcher.test.ts +85 -0
- package/server/magic-ui-watcher.ts +10 -2
- package/server/routes/system-routes.test.ts +401 -0
- package/server/routes/system-routes.ts +10 -0
- package/server/system-disk.test.ts +48 -0
- package/server/system-disk.ts +55 -0
- package/server/system-memory.test.ts +32 -0
- package/server/system-memory.ts +32 -0
- package/dist/assets/index-DSCRqqpK.css +0 -1
- package/dist/assets/index-DspbwgSL.js +0 -140
|
@@ -1 +1 @@
|
|
|
1
|
-
import{_ as u}from"./index-
|
|
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-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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":"
|
|
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.
|
|
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);
|
package/server/magic-ui-ops.ts
CHANGED
|
@@ -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);
|
package/server/magic-ui-types.ts
CHANGED
|
@@ -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
|
-
|
|
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 = {
|
|
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),
|