@hellcoder/companion 0.116.0 → 0.116.1-preview.20260814041931.d191f06

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 (49) hide show
  1. package/dist/assets/{AgentsPage-BBi92OuO.js → AgentsPage-BAalV1v6.js} +5 -5
  2. package/dist/assets/CronManager-B2Vo1YmB.js +1 -0
  3. package/dist/assets/{DashboardPage-BvuDoRlp.js → DashboardPage-BOvDiP1W.js} +1 -1
  4. package/dist/assets/{IntegrationsPage-CK_ThzWg.js → IntegrationsPage-kOXLH5KU.js} +1 -1
  5. package/dist/assets/{LinearOAuthSettingsPage-InTf-E7u.js → LinearOAuthSettingsPage-H-uUJ-ub.js} +1 -1
  6. package/dist/assets/{LinearSettingsPage-BwjC9Vd-.js → LinearSettingsPage-CD_x4AGr.js} +1 -1
  7. package/dist/assets/MagicUIDashboard-9Jgiv4rb.js +1119 -0
  8. package/dist/assets/MagicUIView-BLEcS9XO.js +1 -0
  9. package/dist/assets/{Playground-DlzomEOu.js → Playground-QLrd9q16.js} +10 -9
  10. package/dist/assets/{PromptsPage-hRZhLUYR.js → PromptsPage-11eJ6ak7.js} +1 -1
  11. package/dist/assets/{RunsPage-zsVmbFoi.js → RunsPage-Bz3udIL2.js} +1 -1
  12. package/dist/assets/{SandboxManager-Bq_aXA1z.js → SandboxManager-Dv47pbC6.js} +1 -1
  13. package/dist/assets/SettingsPage-C_2ftG-C.js +1 -0
  14. package/dist/assets/{TailscalePage-fE7Z9uiF.js → TailscalePage-DzMX_7XA.js} +1 -1
  15. package/dist/assets/index-DSCRqqpK.css +1 -0
  16. package/dist/assets/index-DspbwgSL.js +140 -0
  17. package/dist/assets/{sw-register-Cjt2BQTW.js → sw-register-CarYQEeI.js} +1 -1
  18. package/dist/index.html +2 -2
  19. package/dist/sw.js +1 -1
  20. package/package.json +4 -1
  21. package/server/async-message-queue.ts +38 -0
  22. package/server/auto-namer.test.ts +8 -0
  23. package/server/claude-sdk-adapter.ts +1 -36
  24. package/server/event-bus-types.ts +32 -1
  25. package/server/index.ts +7 -0
  26. package/server/linear-agent-bridge.ts +6 -72
  27. package/server/linear-connections.test.ts +8 -0
  28. package/server/magic-ui-ops.test.ts +239 -0
  29. package/server/magic-ui-ops.ts +365 -0
  30. package/server/magic-ui-settings.test.ts +56 -0
  31. package/server/magic-ui-settings.ts +38 -0
  32. package/server/magic-ui-store.ts +94 -0
  33. package/server/magic-ui-types.ts +138 -0
  34. package/server/magic-ui-watcher.test.ts +288 -0
  35. package/server/magic-ui-watcher.ts +623 -0
  36. package/server/message-extract.ts +79 -0
  37. package/server/routes/settings-routes.ts +22 -0
  38. package/server/routes.test.ts +59 -0
  39. package/server/session-types.ts +15 -1
  40. package/server/settings-manager.test.ts +6 -0
  41. package/server/settings-manager.ts +24 -1
  42. package/server/ws-bridge-browser-ingest.ts +1 -0
  43. package/server/ws-bridge-codex.test.ts +12 -0
  44. package/server/ws-bridge-controls.ts +9 -0
  45. package/server/ws-bridge.ts +65 -0
  46. package/dist/assets/CronManager-DxLaNh9T.js +0 -1
  47. package/dist/assets/SettingsPage-BvrrlEKj.js +0 -1
  48. package/dist/assets/index--SnXduR0.js +0 -139
  49. package/dist/assets/index-DcQxHfE7.css +0 -1
@@ -1 +1 @@
1
- import{_ as u}from"./index--SnXduR0.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-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")}});
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--SnXduR0.js"></script>
15
- <link rel="stylesheet" crossorigin href="/assets/index-DcQxHfE7.css">
14
+ <script type="module" crossorigin src="/assets/index-DspbwgSL.js"></script>
15
+ <link rel="stylesheet" crossorigin href="/assets/index-DSCRqqpK.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":"2d1a1c2de7bd1760861e5c80edf8c4ea","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-Cjt2BQTW.js"},{"revision":null,"url":"assets/index-DcQxHfE7.css"},{"revision":null,"url":"assets/index--SnXduR0.js"},{"revision":null,"url":"assets/TailscalePage-fE7Z9uiF.js"},{"revision":null,"url":"assets/SettingsPage-BvrrlEKj.js"},{"revision":null,"url":"assets/SandboxManager-Bq_aXA1z.js"},{"revision":null,"url":"assets/RunsPage-zsVmbFoi.js"},{"revision":null,"url":"assets/PromptsPage-hRZhLUYR.js"},{"revision":null,"url":"assets/Playground-DlzomEOu.js"},{"revision":null,"url":"assets/LinearSettingsPage-BwjC9Vd-.js"},{"revision":null,"url":"assets/LinearOAuthSettingsPage-InTf-E7u.js"},{"revision":null,"url":"assets/IntegrationsPage-CK_ThzWg.js"},{"revision":null,"url":"assets/DashboardPage-BvuDoRlp.js"},{"revision":null,"url":"assets/CronManager-DxLaNh9T.js"},{"revision":null,"url":"assets/AgentsPage-BBi92OuO.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":"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/]}));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hellcoder/companion",
3
- "version": "0.116.0",
3
+ "version": "0.116.1-preview.20260814041931.d191f06",
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",
@@ -67,12 +67,14 @@
67
67
  "@codemirror/view": "6.39.15",
68
68
  "@uiw/react-codemirror": "4.25.4",
69
69
  "axe-core": "^4.11.1",
70
+ "chart.js": "^4.5.1",
70
71
  "croner": "^10.0.1",
71
72
  "diff": "^8.0.3",
72
73
  "fzf": "^0.5.2",
73
74
  "hono": "^4.7.0",
74
75
  "posthog-js": "^1.347.2",
75
76
  "qrcode": "^1.5.4",
77
+ "sanitize-html": "^2.17.7",
76
78
  "ws": "^8.19.0"
77
79
  },
78
80
  "devDependencies": {
@@ -84,6 +86,7 @@
84
86
  "@types/qrcode": "^1.5.6",
85
87
  "@types/react": "^19.0.0",
86
88
  "@types/react-dom": "^19.0.0",
89
+ "@types/sanitize-html": "^2.16.1",
87
90
  "@types/ws": "^8.18.1",
88
91
  "@vitejs/plugin-react": "^4.4.0",
89
92
  "@vitest/coverage-v8": "^4.0.18",
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Minimal async queue that adapts push-style sends to a pull-style
3
+ * AsyncIterable — the input primitive for the Agent SDK's `query()`
4
+ * streaming prompt. Shared by SdkClaudeAdapter and the MagicUI watcher.
5
+ */
6
+ export class AsyncMessageQueue<T> implements AsyncIterable<T> {
7
+ private buffer: T[] = [];
8
+ private waiters: Array<(r: IteratorResult<T>) => void> = [];
9
+ private closed = false;
10
+
11
+ push(item: T): void {
12
+ if (this.closed) return;
13
+ const waiter = this.waiters.shift();
14
+ if (waiter) waiter({ value: item, done: false });
15
+ else this.buffer.push(item);
16
+ }
17
+
18
+ close(): void {
19
+ this.closed = true;
20
+ for (const waiter of this.waiters.splice(0)) {
21
+ waiter({ value: undefined as never, done: true });
22
+ }
23
+ }
24
+
25
+ [Symbol.asyncIterator](): AsyncIterator<T> {
26
+ return {
27
+ next: (): Promise<IteratorResult<T>> => {
28
+ if (this.buffer.length > 0) {
29
+ return Promise.resolve({ value: this.buffer.shift() as T, done: false });
30
+ }
31
+ if (this.closed) {
32
+ return Promise.resolve({ value: undefined as never, done: true });
33
+ }
34
+ return new Promise((resolve) => this.waiters.push(resolve));
35
+ },
36
+ };
37
+ }
38
+ }
@@ -38,6 +38,8 @@ beforeEach(() => {
38
38
  dashboardModel: "claude-haiku-4-5",
39
39
  dashboardRunHour: 3,
40
40
  dashboardMaxSessionsPerRun: 30,
41
+ magicUiEnabled: false,
42
+ magicUiModel: "claude-haiku-4-5",
41
43
  publicUrl: "",
42
44
  updateChannel: "stable",
43
45
  dockerAutoUpdate: false,
@@ -89,6 +91,8 @@ describe("generateSessionTitle", () => {
89
91
  dashboardModel: "claude-haiku-4-5",
90
92
  dashboardRunHour: 3,
91
93
  dashboardMaxSessionsPerRun: 30,
94
+ magicUiEnabled: false,
95
+ magicUiModel: "claude-haiku-4-5",
92
96
  publicUrl: "",
93
97
  updateChannel: "stable",
94
98
  dockerAutoUpdate: false,
@@ -147,6 +151,8 @@ describe("generateSessionTitle", () => {
147
151
  dashboardModel: "claude-haiku-4-5",
148
152
  dashboardRunHour: 3,
149
153
  dashboardMaxSessionsPerRun: 30,
154
+ magicUiEnabled: false,
155
+ magicUiModel: "claude-haiku-4-5",
150
156
  publicUrl: "",
151
157
  updateChannel: "stable",
152
158
  dockerAutoUpdate: false,
@@ -234,6 +240,8 @@ describe("generateSessionTitle", () => {
234
240
  dashboardModel: "claude-haiku-4-5",
235
241
  dashboardRunHour: 3,
236
242
  dashboardMaxSessionsPerRun: 30,
243
+ magicUiEnabled: false,
244
+ magicUiModel: "claude-haiku-4-5",
237
245
  publicUrl: "",
238
246
  updateChannel: "stable",
239
247
  dockerAutoUpdate: false,
@@ -38,6 +38,7 @@ import {
38
38
  } from "@anthropic-ai/claude-agent-sdk";
39
39
  import { ClaudeAdapter } from "./claude-adapter.js";
40
40
  import { log } from "./logger.js";
41
+ import { AsyncMessageQueue } from "./async-message-queue.js";
41
42
 
42
43
  export interface SdkAttachOptions {
43
44
  model?: string;
@@ -57,42 +58,6 @@ export interface SdkAttachOptions {
57
58
  claudeBinary?: string;
58
59
  }
59
60
 
60
- /** Minimal async queue that adapts push-style sends to the SDK's
61
- * pull-style AsyncIterable input. */
62
- class AsyncMessageQueue<T> implements AsyncIterable<T> {
63
- private buffer: T[] = [];
64
- private waiters: Array<(r: IteratorResult<T>) => void> = [];
65
- private closed = false;
66
-
67
- push(item: T): void {
68
- if (this.closed) return;
69
- const waiter = this.waiters.shift();
70
- if (waiter) waiter({ value: item, done: false });
71
- else this.buffer.push(item);
72
- }
73
-
74
- close(): void {
75
- this.closed = true;
76
- for (const waiter of this.waiters.splice(0)) {
77
- waiter({ value: undefined as never, done: true });
78
- }
79
- }
80
-
81
- [Symbol.asyncIterator](): AsyncIterator<T> {
82
- return {
83
- next: (): Promise<IteratorResult<T>> => {
84
- if (this.buffer.length > 0) {
85
- return Promise.resolve({ value: this.buffer.shift() as T, done: false });
86
- }
87
- if (this.closed) {
88
- return Promise.resolve({ value: undefined as never, done: true });
89
- }
90
- return new Promise((resolve) => this.waiters.push(resolve));
91
- },
92
- };
93
- }
94
- }
95
-
96
61
  export class SdkClaudeAdapter extends ClaudeAdapter {
97
62
  private sdkQuery: Query | null = null;
98
63
  private inputQueue = new AsyncMessageQueue<SDKUserMessage>();
@@ -1,7 +1,7 @@
1
1
  // Typed event map for the Companion internal event bus.
2
2
  // Each key is a namespaced event name; values are the payload passed to handlers.
3
3
 
4
- import type { BrowserIncomingMessage } from "./session-types.js";
4
+ import type { BrowserIncomingMessage, PermissionRequest } from "./session-types.js";
5
5
  import type { CodexAdapter } from "./codex-adapter.js";
6
6
  import type { ClaudeAdapter } from "./claude-adapter.js";
7
7
  import type { SessionPhase } from "./session-state-machine.js";
@@ -76,4 +76,35 @@ export interface CompanionEventMap {
76
76
 
77
77
  /** A result (turn completion) was processed and broadcast to browsers. */
78
78
  "message:result": { sessionId: string; message: BrowserIncomingMessage };
79
+
80
+ // ── Permission flow (transport-agnostic; emitted at bridge level) ───
81
+
82
+ /** A permission request was stored and broadcast to browsers. */
83
+ "permission:requested": { sessionId: string; request: PermissionRequest };
84
+
85
+ /** A permission request was resolved — by the user or by AI validation. */
86
+ "permission:resolved": {
87
+ sessionId: string;
88
+ requestId: string;
89
+ behavior: "allow" | "deny";
90
+ resolvedBy: "user" | "ai";
91
+ toolName?: string;
92
+ /** For AskUserQuestion allows: the answers keyed by question text. */
93
+ answers?: Record<string, string>;
94
+ reason?: string;
95
+ };
96
+
97
+ /** A permission request was cancelled by the CLI (e.g. interrupt). */
98
+ "permission:cancelled": { sessionId: string; requestId: string };
99
+
100
+ // ── MagicUI ────────────────────────────────────────────────────────
101
+
102
+ /** Per-session MagicUI opt-in changed via set_magic_ui. */
103
+ "magic-ui:setting-changed": {
104
+ sessionId: string;
105
+ magicUiActive: boolean | null | undefined;
106
+ };
107
+
108
+ /** A browser asked for the current dashboard snapshot (join/resync). */
109
+ "magic-ui:sync-requested": { sessionId: string };
79
110
  }
package/server/index.ts CHANGED
@@ -32,6 +32,7 @@ import { migrateCronJobsToAgents } from "./agent-cron-migrator.js";
32
32
  import { migrateLinearCredentialsToAgents } from "./linear-credential-migration.js";
33
33
  import { authenticateManagedWebSocket } from "./ws-auth.js";
34
34
  import { LinearAgentBridge } from "./linear-agent-bridge.js";
35
+ import { MagicUiWatcherManager } from "./magic-ui-watcher.js";
35
36
  import { NoVncProxy } from "./novnc-proxy.js";
36
37
 
37
38
  import { startPeriodicCheck, setServiceMode, getCurrentVersion } from "./update-checker.js";
@@ -72,6 +73,12 @@ const recorder = new RecorderManager();
72
73
  const cronScheduler = new CronScheduler(launcher, wsBridge);
73
74
  const agentExecutor = new AgentExecutor(launcher, wsBridge);
74
75
  const linearAgentBridge = new LinearAgentBridge(agentExecutor, wsBridge);
76
+ // MagicUI watcher fleet: one Haiku session per opted-in companion session.
77
+ const magicUiManager = new MagicUiWatcherManager({
78
+ broadcast: (sessionId, msg) => wsBridge.broadcastToSession(sessionId, msg),
79
+ getSessionState: (sessionId) => wsBridge.getSession(sessionId)?.state,
80
+ });
81
+ void magicUiManager; // owned for process lifetime; bus-driven
75
82
 
76
83
  const orchestrator = new SessionOrchestrator({
77
84
  launcher, wsBridge, sessionStore, worktreeTracker,
@@ -19,82 +19,16 @@ import { buildLinearOAuthSystemPrompt } from "./linear-prompt-builder.js";
19
19
  import { getSettings } from "./settings-manager.js";
20
20
  import { companionBus } from "./event-bus.js";
21
21
  import { findOAuthConnectionByClientId, getOAuthConnection, updateOAuthConnection } from "./linear-oauth-connections.js";
22
+ import {
23
+ extractTextDeltaFromStreamEvent,
24
+ extractTextFromAssistant,
25
+ extractToolResults,
26
+ extractToolUses,
27
+ } from "./message-extract.js";
22
28
 
23
29
  /** Interval (ms) for flushing intermediate progress as ephemeral thoughts. */
24
30
  const PROGRESS_FLUSH_INTERVAL_MS = 30_000;
25
31
 
26
- /** Safely extract the content array from an assistant-type message. */
27
- function getAssistantContent(msg: BrowserIncomingMessage): unknown[] | null {
28
- if (msg.type !== "assistant") return null;
29
- // Assistant messages carry content blocks at msg.message.content
30
- const raw = msg as Record<string, unknown>;
31
- const message = raw.message;
32
- if (!message || typeof message !== "object") return null;
33
- const content = (message as Record<string, unknown>).content;
34
- return Array.isArray(content) ? content : null;
35
- }
36
-
37
- /** Extract text from assistant message content blocks */
38
- function extractTextFromAssistant(msg: BrowserIncomingMessage): string {
39
- const content = getAssistantContent(msg);
40
- if (!content) return "";
41
- return content
42
- .filter((b): b is { type: string; text: string } =>
43
- typeof b === "object" && b !== null && (b as Record<string, unknown>).type === "text" && typeof (b as Record<string, unknown>).text === "string")
44
- .map((b) => b.text)
45
- .join("\n");
46
- }
47
-
48
- /** Extract text deltas from stream events. */
49
- function extractTextDeltaFromStreamEvent(msg: BrowserIncomingMessage): string {
50
- if (msg.type !== "stream_event") return "";
51
- const event = msg.event as Record<string, unknown> | undefined;
52
- if (!event || event.type !== "content_block_delta") return "";
53
- const delta = event.delta as Record<string, unknown> | undefined;
54
- if (!delta || delta.type !== "text_delta" || typeof delta.text !== "string") return "";
55
- return delta.text;
56
- }
57
-
58
- /** Extract all tool use blocks from assistant message content (with raw input for plan extraction) */
59
- function extractToolUses(msg: BrowserIncomingMessage): Array<{ id?: string; name: string; input: string; rawInput?: Record<string, unknown> }> {
60
- const content = getAssistantContent(msg);
61
- if (!content) return [];
62
- return content
63
- .filter((b): b is { type: string; id?: string; name: string; input?: Record<string, unknown> } =>
64
- typeof b === "object" && b !== null
65
- && (b as Record<string, unknown>).type === "tool_use"
66
- && typeof (b as Record<string, unknown>).name === "string")
67
- .map((toolBlock) => ({
68
- id: typeof toolBlock.id === "string" ? toolBlock.id : undefined,
69
- name: toolBlock.name,
70
- input: toolBlock.input ? JSON.stringify(toolBlock.input).slice(0, 200) : "",
71
- rawInput: toolBlock.input,
72
- }));
73
- }
74
-
75
- /** Extract tool_result blocks from assistant message content. */
76
- function extractToolResults(msg: BrowserIncomingMessage): Array<{ tool_use_id: string; content: string }> {
77
- const content = getAssistantContent(msg);
78
- if (!content) return [];
79
- return content
80
- .filter((b): b is { type: string; tool_use_id: string; content?: unknown } =>
81
- typeof b === "object" && b !== null
82
- && (b as Record<string, unknown>).type === "tool_result"
83
- && typeof (b as Record<string, unknown>).tool_use_id === "string")
84
- .map((block) => ({
85
- tool_use_id: block.tool_use_id,
86
- content: typeof block.content === "string"
87
- ? block.content.slice(0, 500)
88
- : Array.isArray(block.content)
89
- ? (block.content as Array<{ type?: string; text?: string }>)
90
- .filter((c) => c.type === "text" && typeof c.text === "string")
91
- .map((c) => c.text)
92
- .join("\n")
93
- .slice(0, 500)
94
- : "",
95
- }));
96
- }
97
-
98
32
  /** Map TodoWrite status values to Linear plan item status values. */
99
33
  function mapTodoStatus(status: string): AgentPlanItem["status"] {
100
34
  if (status === "in_progress") return "inProgress";
@@ -64,6 +64,8 @@ beforeEach(() => {
64
64
  dashboardModel: "claude-haiku-4-5",
65
65
  dashboardRunHour: 3,
66
66
  dashboardMaxSessionsPerRun: 30,
67
+ magicUiEnabled: false,
68
+ magicUiModel: "claude-haiku-4-5",
67
69
  publicUrl: "",
68
70
  updateChannel: "stable",
69
71
  dockerAutoUpdate: false,
@@ -203,6 +205,8 @@ describe("linear-connections", () => {
203
205
  dashboardModel: "claude-haiku-4-5",
204
206
  dashboardRunHour: 3,
205
207
  dashboardMaxSessionsPerRun: 30,
208
+ magicUiEnabled: false,
209
+ magicUiModel: "claude-haiku-4-5",
206
210
  publicUrl: "",
207
211
  updateChannel: "stable",
208
212
  dockerAutoUpdate: false,
@@ -252,6 +256,8 @@ describe("linear-connections", () => {
252
256
  dashboardModel: "claude-haiku-4-5",
253
257
  dashboardRunHour: 3,
254
258
  dashboardMaxSessionsPerRun: 30,
259
+ magicUiEnabled: false,
260
+ magicUiModel: "claude-haiku-4-5",
255
261
  publicUrl: "",
256
262
  updateChannel: "stable",
257
263
  dockerAutoUpdate: false,
@@ -320,6 +326,8 @@ describe("linear-connections", () => {
320
326
  dashboardModel: "claude-haiku-4-5",
321
327
  dashboardRunHour: 3,
322
328
  dashboardMaxSessionsPerRun: 30,
329
+ magicUiEnabled: false,
330
+ magicUiModel: "claude-haiku-4-5",
323
331
  publicUrl: "",
324
332
  updateChannel: "stable",
325
333
  dockerAutoUpdate: false,