beeos-claw 0.6.40-staging.2 → 0.6.40-staging.3

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.
@@ -1 +1 @@
1
- import{randomUUID}from"node:crypto";import WebSocket from"ws";import{agentAuthHeaders}from"../agent-auth.js";import{MSG_SYNC,MSG_AWARENESS}from"./constants.js";const RECONNECT_BASE_MS=1e3,RECONNECT_MAX_MS=3e4,MAX_PENDING_DATA=500;export class CanvasRelayClient{ws=null;opts;reconnectAttempts=0;reconnectTimer=null;closed=!1;hasConnectedOnce=!1;canvasHandlers=new Map;joinedCanvases=new Set;pendingJoins=new Map;pendingData=[];actionHandler=null;pendingRequests=new Map;constructor(e){this.opts=e}connect(){this.closed=!1,this.reconnectAttempts=0,this.doConnect()}doConnect(){this.closed||this.doConnectAsync().catch(e=>{this.opts.logger?.warn?.(`[canvas-relay] Connect failed: ${e}`),this.scheduleReconnect()})}async doConnectAsync(){if(this.closed)return;const e="/ws/agent";let t,n={};if(this.opts.tokenProvider){const s=await this.opts.tokenProvider();t=`${(s.relay_url||this.opts.canvasRelayUrl).replace(/^http/,"ws")}${e}`,this.opts.agentId&&(t+=`?agentId=${encodeURIComponent(this.opts.agentId)}`),t+=`${t.includes("?")?"&":"?"}token=${encodeURIComponent(s.token)}`,n=agentAuthHeaders("GET",e,this.opts.agentKeys)}else t=`${this.opts.canvasRelayUrl.replace(/^http/,"ws")}${e}`,this.opts.agentId&&(t+=`?agentId=${encodeURIComponent(this.opts.agentId)}`),n=agentAuthHeaders("GET",e,this.opts.agentKeys);this.opts.logger?.info?.(`[canvas-relay] Connecting to ${t}`);const s=new WebSocket(t,{headers:n});s.binaryType="arraybuffer",s.on("open",()=>{this.opts.logger?.info?.("[canvas-relay] Connected (multiplexed)"),this.reconnectAttempts=0,this.ws=s,(this.opts.displayName||this.opts.avatarUrl)&&this.sendControlMessage({type:"profile",displayName:this.opts.displayName,avatarUrl:this.opts.avatarUrl});for(const e of this.joinedCanvases)this.sendJoinCommand(e);for(const e of this.pendingData)this.sendTagged(e.canvasId,e.data);this.pendingData=[],this.hasConnectedOnce?(this.opts.logger?.info?.("[canvas-relay] Reconnected — notifying upper layer"),this.opts.onReconnect?.()):this.hasConnectedOnce=!0}),s.on("message",(e,t)=>{if(!t&&"string"!=typeof e){const t=e instanceof Buffer?e.toString("utf8"):(new TextDecoder).decode(e);return void this.handleTextMessage(t)}if("string"==typeof e)return void this.handleTextMessage(e);const n=new Uint8Array(e instanceof ArrayBuffer?e:e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength));this.handleBinaryMessage(n)}),s.on("close",()=>{this.opts.logger?.info?.("[canvas-relay] Disconnected"),this.ws=null;for(const[,e]of this.pendingJoins)e.reject(new Error("WebSocket disconnected"));this.pendingJoins.clear();for(const[,e]of this.pendingRequests)clearTimeout(e.timer),e.reject(new Error("WebSocket disconnected"));this.pendingRequests.clear(),this.scheduleReconnect()}),s.on("error",e=>{this.opts.logger?.warn?.("[canvas-relay] Error:",e.message),s.close()})}scheduleReconnect(){if(this.closed)return;const e=Math.min(1e3*Math.pow(2,this.reconnectAttempts),3e4);this.reconnectAttempts++,this.opts.logger?.debug?.(`[canvas-relay] Reconnecting in ${e}ms (attempt ${this.reconnectAttempts})`),this.reconnectTimer=setTimeout(()=>this.doConnect(),e),"object"==typeof this.reconnectTimer&&"unref"in this.reconnectTimer&&this.reconnectTimer.unref()}async joinCanvas(e,t){if(this.canvasHandlers.set(e,t),!this.joinedCanvases.has(e)&&(this.joinedCanvases.add(e),this.connected))return new Promise((t,n)=>{this.pendingJoins.set(e,{resolve:t,reject:n}),this.sendJoinCommand(e);const s=setTimeout(()=>{const t=this.pendingJoins.get(e);t&&(this.pendingJoins.delete(e),this.opts.logger?.warn?.(`[canvas-relay] joinCanvas timed out for canvas=${e}, resolving optimistically`),t.resolve())},5e3),o=this.pendingJoins.get(e);this.pendingJoins.set(e,{resolve:()=>{clearTimeout(s),o.resolve()},reject:e=>{clearTimeout(s),o.reject(e)}})})}leaveCanvas(e){this.canvasHandlers.delete(e),this.joinedCanvases.delete(e),this.connected&&this.sendControlMessage({type:"leave",canvasId:e})}sendUpdate(e,t){this.sendTagged(e,t)}sendAwareness(e,t){this.sendTagged(e,t)}async ensureCanvas(e,t){return(await this.sendRequest({type:"ensureCanvas",sessionId:e,agentId:t?.agentId,title:t?.title,linkSessionId:t?.linkSessionId})).canvasId}async deleteCanvas(e){await this.sendRequest({type:"deleteCanvas",canvasId:e})}async createSnapshot(e,t){return(await this.sendRequest({type:"createSnapshot",canvasId:e,description:t})).snapshotId??null}onAction(e){this.actionHandler=e}sendActionResult(e){this.sendControlMessage({type:"action:result",...e})}sendActionError(e){this.sendControlMessage({type:"action:error",...e})}sendRequest(e){if(!this.connected)return Promise.reject(new Error(`WebSocket not connected (request: ${e.type})`));const t=randomUUID();return e.reqId=t,new Promise((n,s)=>{const o=setTimeout(()=>{this.pendingRequests.delete(t),s(new Error(`WSS request ${e.type} timed out`))},1e4);this.pendingRequests.set(t,{resolve:n,reject:s,timer:o}),this.sendControlMessage(e)})}sendTagged(e,t){if(!this.connected)return this.pendingData.length>=500&&(this.opts.logger?.warn?.("[canvas-relay] Pending data buffer full (500), dropping oldest entry"),this.pendingData.shift()),void this.pendingData.push({canvasId:e,data:t});const n=(new TextEncoder).encode(e),s=new Uint8Array(1+n.length+t.length);s[0]=n.length,s.set(n,1),s.set(t,1+n.length),this.ws.send(s)}handleTextMessage(e){try{const t=JSON.parse(e),n=t.type;if(n?.endsWith(":result")&&t.reqId){const e=this.pendingRequests.get(t.reqId);return void(e&&(this.pendingRequests.delete(t.reqId),clearTimeout(e.timer),t.error?e.reject(new Error(t.error)):e.resolve(t)))}switch(n){case"joined":{const e=this.pendingJoins.get(t.canvasId??"");e&&(this.pendingJoins.delete(t.canvasId),e.resolve()),this.opts.logger?.info?.(`[canvas-relay] Joined canvas ${t.canvasId}`);break}case"left":this.opts.logger?.info?.(`[canvas-relay] Left canvas ${t.canvasId}`);break;case"error":this.opts.logger?.warn?.(`[canvas-relay] Error for canvas ${t.canvasId}: ${t.message}`);break;case"action":this.opts.logger?.info?.(`[canvas-relay] Received action: canvasId=${t.canvasId} surfaceId=${t.surfaceId} requestId=${t.requestId} senderClientId=${t.senderClientId}`),this.actionHandler?this.actionHandler(t):this.opts.logger?.warn?.("[canvas-relay] Action received but no handler registered");break;default:this.opts.logger?.debug?.(`[canvas-relay] Unknown control: ${n}`)}}catch{this.opts.logger?.warn?.("[canvas-relay] Failed to parse control message")}}handleBinaryMessage(e){if(e.length<2)return;const t=e[0];if(e.length<1+t+1)return;const n=(new TextDecoder).decode(e.slice(1,1+t)),s=e.slice(1+t),o=this.canvasHandlers.get(n);if(!o)return;const a=s[0];a===MSG_SYNC&&o.onUpdate?o.onUpdate(s):a===MSG_AWARENESS&&o.onAwareness&&o.onAwareness(s)}sendJoinCommand(e){this.sendControlMessage({type:"join",canvasId:e})}sendControlMessage(e){this.ws&&this.ws.readyState===WebSocket.OPEN&&this.ws.send(JSON.stringify(e))}disconnect(){this.closed=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.ws&&(this.ws.close(),this.ws=null),this.pendingData=[];for(const[,e]of this.pendingJoins)e.reject(new Error("Client disconnected"));this.pendingJoins.clear();for(const[,e]of this.pendingRequests)clearTimeout(e.timer),e.reject(new Error("Client disconnected"));this.pendingRequests.clear()}get connected(){return null!==this.ws&&this.ws.readyState===WebSocket.OPEN}}
1
+ import{randomUUID}from"node:crypto";import WebSocket from"ws";import{agentAuthHeaders}from"../agent-auth.js";import{MSG_SYNC,MSG_AWARENESS}from"./constants.js";const RECONNECT_BASE_MS=1e3,RECONNECT_MAX_MS=3e4,MAX_PENDING_DATA=500;export class CanvasRelayClient{ws=null;opts;reconnectAttempts=0;reconnectTimer=null;closed=!1;hasConnectedOnce=!1;canvasHandlers=new Map;joinedCanvases=new Set;pendingJoins=new Map;pendingData=[];actionHandler=null;controlHandler=null;pendingRequests=new Map;constructor(e){this.opts=e}connect(){this.closed=!1,this.reconnectAttempts=0,this.doConnect()}doConnect(){this.closed||this.doConnectAsync().catch(e=>{this.opts.logger?.warn?.(`[canvas-relay] Connect failed: ${e}`),this.scheduleReconnect()})}async doConnectAsync(){if(this.closed)return;const e="/ws/agent";let t,n={};if(this.opts.tokenProvider){const s=await this.opts.tokenProvider();t=`${(s.relay_url||this.opts.canvasRelayUrl).replace(/^http/,"ws")}${e}`,this.opts.agentId&&(t+=`?agentId=${encodeURIComponent(this.opts.agentId)}`),t+=`${t.includes("?")?"&":"?"}token=${encodeURIComponent(s.token)}`,n=agentAuthHeaders("GET",e,this.opts.agentKeys)}else t=`${this.opts.canvasRelayUrl.replace(/^http/,"ws")}${e}`,this.opts.agentId&&(t+=`?agentId=${encodeURIComponent(this.opts.agentId)}`),n=agentAuthHeaders("GET",e,this.opts.agentKeys);this.opts.logger?.info?.(`[canvas-relay] Connecting to ${t}`);const s=new WebSocket(t,{headers:n});s.binaryType="arraybuffer",s.on("open",()=>{this.opts.logger?.info?.("[canvas-relay] Connected (multiplexed)"),this.reconnectAttempts=0,this.ws=s,(this.opts.displayName||this.opts.avatarUrl)&&this.sendControlMessage({type:"profile",displayName:this.opts.displayName,avatarUrl:this.opts.avatarUrl});for(const e of this.joinedCanvases)this.sendJoinCommand(e);for(const e of this.pendingData)this.sendTagged(e.canvasId,e.data);this.pendingData=[],this.hasConnectedOnce?(this.opts.logger?.info?.("[canvas-relay] Reconnected — notifying upper layer"),this.opts.onReconnect?.()):this.hasConnectedOnce=!0}),s.on("message",(e,t)=>{if(!t&&"string"!=typeof e){const t=e instanceof Buffer?e.toString("utf8"):(new TextDecoder).decode(e);return void this.handleTextMessage(t)}if("string"==typeof e)return void this.handleTextMessage(e);const n=new Uint8Array(e instanceof ArrayBuffer?e:e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength));this.handleBinaryMessage(n)}),s.on("close",()=>{this.opts.logger?.info?.("[canvas-relay] Disconnected"),this.ws=null;for(const[,e]of this.pendingJoins)e.reject(new Error("WebSocket disconnected"));this.pendingJoins.clear();for(const[,e]of this.pendingRequests)clearTimeout(e.timer),e.reject(new Error("WebSocket disconnected"));this.pendingRequests.clear(),this.scheduleReconnect()}),s.on("error",e=>{this.opts.logger?.warn?.("[canvas-relay] Error:",e.message),s.close()})}scheduleReconnect(){if(this.closed)return;const e=Math.min(1e3*Math.pow(2,this.reconnectAttempts),3e4);this.reconnectAttempts++,this.opts.logger?.debug?.(`[canvas-relay] Reconnecting in ${e}ms (attempt ${this.reconnectAttempts})`),this.reconnectTimer=setTimeout(()=>this.doConnect(),e),"object"==typeof this.reconnectTimer&&"unref"in this.reconnectTimer&&this.reconnectTimer.unref()}async joinCanvas(e,t){if(this.canvasHandlers.set(e,t),!this.joinedCanvases.has(e)&&(this.joinedCanvases.add(e),this.connected))return new Promise((t,n)=>{this.pendingJoins.set(e,{resolve:t,reject:n}),this.sendJoinCommand(e);const s=setTimeout(()=>{const t=this.pendingJoins.get(e);t&&(this.pendingJoins.delete(e),this.opts.logger?.warn?.(`[canvas-relay] joinCanvas timed out for canvas=${e}, resolving optimistically`),t.resolve())},5e3),o=this.pendingJoins.get(e);this.pendingJoins.set(e,{resolve:()=>{clearTimeout(s),o.resolve()},reject:e=>{clearTimeout(s),o.reject(e)}})})}leaveCanvas(e){this.canvasHandlers.delete(e),this.joinedCanvases.delete(e),this.connected&&this.sendControlMessage({type:"leave",canvasId:e})}sendUpdate(e,t){this.sendTagged(e,t)}sendAwareness(e,t){this.sendTagged(e,t)}async ensureCanvas(e,t){return(await this.sendRequest({type:"ensureCanvas",sessionId:e,agentId:t?.agentId,title:t?.title,linkSessionId:t?.linkSessionId})).canvasId}async deleteCanvas(e){await this.sendRequest({type:"deleteCanvas",canvasId:e})}async createSnapshot(e,t){return(await this.sendRequest({type:"createSnapshot",canvasId:e,description:t})).snapshotId??null}onAction(e){this.actionHandler=e}onCanvasControl(e){this.controlHandler=e}sendActionResult(e){this.sendControlMessage({type:"action:result",...e})}sendActionError(e){this.sendControlMessage({type:"action:error",...e})}sendRequest(e){if(!this.connected)return Promise.reject(new Error(`WebSocket not connected (request: ${e.type})`));const t=randomUUID();return e.reqId=t,new Promise((n,s)=>{const o=setTimeout(()=>{this.pendingRequests.delete(t),s(new Error(`WSS request ${e.type} timed out`))},1e4);this.pendingRequests.set(t,{resolve:n,reject:s,timer:o}),this.sendControlMessage(e)})}sendTagged(e,t){if(!this.connected)return this.pendingData.length>=500&&(this.opts.logger?.warn?.("[canvas-relay] Pending data buffer full (500), dropping oldest entry"),this.pendingData.shift()),void this.pendingData.push({canvasId:e,data:t});const n=(new TextEncoder).encode(e),s=new Uint8Array(1+n.length+t.length);s[0]=n.length,s.set(n,1),s.set(t,1+n.length),this.ws.send(s)}handleTextMessage(e){try{const t=JSON.parse(e),n=t.type;if(n?.endsWith(":result")&&t.reqId){const e=this.pendingRequests.get(t.reqId);return void(e&&(this.pendingRequests.delete(t.reqId),clearTimeout(e.timer),t.error?e.reject(new Error(t.error)):e.resolve(t)))}switch(n){case"joined":{const e=this.pendingJoins.get(t.canvasId??"");e&&(this.pendingJoins.delete(t.canvasId),e.resolve()),this.opts.logger?.info?.(`[canvas-relay] Joined canvas ${t.canvasId}`);break}case"left":this.opts.logger?.info?.(`[canvas-relay] Left canvas ${t.canvasId}`);break;case"error":this.opts.logger?.warn?.(`[canvas-relay] Error for canvas ${t.canvasId}: ${t.message}`);break;case"action":this.opts.logger?.info?.(`[canvas-relay] Received action: canvasId=${t.canvasId} surfaceId=${t.surfaceId} requestId=${t.requestId} senderClientId=${t.senderClientId}`),this.actionHandler?this.actionHandler(t):this.opts.logger?.warn?.("[canvas-relay] Action received but no handler registered");break;case"canvas.toggle":case"canvas.dimensions":case"canvas.reference":case"canvas.clear":this.opts.logger?.info?.(`[canvas-relay] Received control frame type=${n} canvasId=${t.canvasId} sessionId=${t.sessionId??""}`),this.controlHandler?this.controlHandler(t):this.opts.logger?.warn?.(`[canvas-relay] Control frame ${n} received but no handler registered`);break;default:this.opts.logger?.debug?.(`[canvas-relay] Unknown control: ${n}`)}}catch(e){this.opts.logger?.warn?.(`[canvas-relay] Failed to parse control message: ${e instanceof Error?e.message:String(e)}`)}}handleBinaryMessage(e){if(e.length<2)return;const t=e[0];if(e.length<1+t+1)return;const n=(new TextDecoder).decode(e.slice(1,1+t)),s=e.slice(1+t),o=this.canvasHandlers.get(n);if(!o)return;const a=s[0];a===MSG_SYNC&&o.onUpdate?o.onUpdate(s):a===MSG_AWARENESS&&o.onAwareness&&o.onAwareness(s)}sendJoinCommand(e){this.sendControlMessage({type:"join",canvasId:e})}sendControlMessage(e){this.ws&&this.ws.readyState===WebSocket.OPEN&&this.ws.send(JSON.stringify(e))}disconnect(){this.closed=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.ws&&(this.ws.close(),this.ws=null),this.pendingData=[];for(const[,e]of this.pendingJoins)e.reject(new Error("Client disconnected"));this.pendingJoins.clear();for(const[,e]of this.pendingRequests)clearTimeout(e.timer),e.reject(new Error("Client disconnected"));this.pendingRequests.clear()}get connected(){return null!==this.ws&&this.ws.readyState===WebSocket.OPEN}}
@@ -1 +1 @@
1
- import{CanvasActionRouter,formatActionForAgent}from"./canvas/action-router.js";import{FormSchemaRegistry}from"./surface/form-schema-registry.js";import{FormSurfaceBuilder}from"./surface/form-surface-builder.js";import{ResourceSurfaceRenderer}from"./surface/resource-surface-renderer.js";import{CANVAS_TERM_MAPPING}from"./canvas/tools.js";let _oclSeq=0;function nextOclId(e){return`${e}-${Date.now()}-${++_oclSeq}`}const STRING_PROPS=new Set(["text","url","label","name","title","description","placeholder","icon","src","alt","value","hint","caption"]);function toPascalCase(e){return e?e[0]>="A"&&e[0]<="Z"?e:e.charAt(0).toUpperCase()+e.slice(1):"Text"}function sanitizeA2UIComponents(e){return Array.isArray(e)?e.map(e=>{if("object"!=typeof e||null===e)return e;const s=e;if("component"in s&&"object"==typeof s.component&&null!==s.component){const e=s.component;for(const[s,t]of Object.entries(e)){if("object"==typeof t&&null!==t){const n=t;for(const[e,s]of Object.entries(n))"string"==typeof s&&STRING_PROPS.has(e)&&(n[e]={literalString:s});e[s]=n}if(s[0]>="a"&&s[0]<="z"){e[toPascalCase(s)]=e[s],delete e[s]}}s.component=e}return s}):e}export class CanvasOrchestrator{canvasState=null;resourceRenderer=null;descriptorRegistry=null;actionRouter=null;formBuilder=null;schemaRegistry=null;pendingCanvasLinks=[];deps;constructor(e){this.deps=e}updateDeps(e){Object.assign(this.deps,e)}setCanvasState(e,s){this.canvasState=e;const t=async(e,s)=>{if(!this.deps.gateway?.isConnected)throw new Error("Gateway not connected");return this.deps.gateway.request(e,s)},n=()=>{const s=this.deps.getSessionIdForToolCall();if(!s)return;const t=this.deps.state.sessions.get(s),n=t?.gatewaySessionKey;return n?e.resolveCanvasForSession(n):void 0},a=s?.schemaRegistry??new FormSchemaRegistry,i=new FormSurfaceBuilder({canvasState:e,gatewayRequest:t,resolveCanvasId:n,logger:this.deps.logger});this.formBuilder=i,this.schemaRegistry=a,this.descriptorRegistry=s?.descriptorRegistry??null,this.resourceRenderer=new ResourceSurfaceRenderer({canvasState:e,gatewayRequest:t,schemaRegistry:a,formBuilder:i,resolveCanvasId:n,logger:this.deps.logger}),this.actionRouter=new CanvasActionRouter({stateManager:e,logger:this.deps.logger,writeObs:e=>this.deps.writeObs(e),forwardToAgent:e=>this.forwardActionToAgent(e),emitActionAsUserMessage:e=>{const s=this.deps.state.resolveSessionFromParams(e);s?this.emitCanvasActionAsUserMessage(s.sessionId,e):this.deps.logger.warn?.("[canvas-orch] emitActionAsUserMessage: session not found, pill skipped",{surfaceId:e.surfaceId,sessionId:e.sessionId})}})}wireRelayActionHandler(){const e=this.canvasState?.getRelayClient();e?(e.onAction(e=>{this.handleRelayAction(e)}),this.deps.logger.debug?.("[canvas-orch] Canvas Relay action handler wired")):this.deps.logger.warn?.("[canvas-orch] wireRelayActionHandler called but no relay client available")}isCanvasActive(e){if(!e)return!1;const s=this.deps.state.sessions.get(e);return!0===s?.canvasActive}isCanvasActiveForGatewayKey(e){const s=this.deps.state.sessionsByGatewayKey.get(e);return!0===s?.canvasActive}onSessionLoaded(e,s,t){if(!this.deps.config.canvasEnabled)return;const n=this.canvasState?.getRelayClient();if(!n)return this.pendingCanvasLinks.push({acpSessionId:e,gatewaySessionKey:s}),void this.deps.logger.info?.(`[canvas-orch] Relay not ready, queued canvas link: acpSid=${e} gwKey=${s}`);this.ensureCanvasForSession(e,s)}onSessionDeleted(e){this.canvasState?.removeSession(e)}onSessionCleared(e,s){const t=this.canvasState?.resolveCanvasForSession(s);t&&(this.canvasState.clear(t),this.actionRouter?.reset())}onPromptStart(){this.actionRouter?.clearPending()}async onPromptComplete(e){try{await(this.actionRouter?.drainPending())}catch(e){this.deps.logger.warn?.(`[canvas-orch] Drain pending canvas actions failed: ${e instanceof Error?e.message:e}`)}}onPromptFailed(){this.actionRouter?.clearPending()}flushPendingCanvasLinks(){const e=this.pendingCanvasLinks.splice(0);if(0===e.length)return;const s=new Set,t=e.filter(({gatewaySessionKey:e})=>!s.has(e)&&(s.add(e),!this.canvasState?.resolveCanvasForSession(e)));if(0!==t.length){this.deps.logger.info?.(`[canvas-orch] Flushing ${t.length} pending canvas link(s)`);for(const{acpSessionId:e,gatewaySessionKey:s}of t)this.ensureCanvasForSession(e,s)}}onRelayReconnected(){this.deps.logger.info?.("[canvas-orch] Relay reconnected — retrying pending canvas links"),this.flushPendingCanvasLinks()}async handleSlashCommand(e){const s=this.descriptorRegistry?.getBySlashCommand(e);return!(!s||!this.resourceRenderer)&&(await this.resourceRenderer.render(s,"inline"),!0)}async enrichMessageWithCanvasContext(e,s,t){if(!this.canvasState)return e;if(t.has(s.split(":").slice(0,2).join(":"))||t.has(s))return e;try{let t=this.canvasState.resolveCanvasForSession(s);if(!t){const e=this.canvasState.getRelayClient();if(e)try{t=await e.ensureCanvas(s),this.canvasState.registerSession(s,t)}catch{}}if(t){let s=this.canvasState.buildContextForCanvas(t);s||(await this.canvasState.lazyLoadCanvas(t),s=this.canvasState.buildContextForCanvas(t));return`${CANVAS_TERM_MAPPING}\n\n${s??"[Canvas] Canvas panel is available. Use render_ui for structured/visual content."}\n\n${e}`}}catch(e){this.deps.logger.warn?.(`[canvas-orch] Canvas context injection failed: ${e instanceof Error?e.message:String(e)}`)}return e}handleCanvasNotification(e,s){if(this.canvasState){if("canvas/toggle"===e){const e=s,t=!1!==e.enabled,n=this.deps.state.resolveSessionFromParams(e);if(n&&(n.canvasActive=t,t&&(n.canvasPromptInjected=!1,n.sessionId))){const e=n.sessionId,s=n.gatewaySessionKey,t=n.agentId,a=this.canvasState?.getRelayClient();if(a){const i=this.deps.resolvePlatformAgentId(t??"")??n.platformAgentId,o=s&&s!==e?e:void 0;a.ensureCanvas(s??e,{agentId:i,linkSessionId:o}).catch(e=>{this.deps.logger.warn?.(`[canvas-orch] Failed to pre-create canvas on toggle: ${e}`)})}}this.deps.logger.info?.(`[canvas-orch] Canvas toggled: ${t}`),this.deps.writeObs({component:"canvas",domain:"canvas",name:"canvas.toggle",severity:"info",payload:{enabled:t}})}else if("canvas/clear"===e){const e=s,t=this.deps.state.resolveSessionFromParams(e),n=t?this.canvasState.resolveCanvasForSession(t.gatewaySessionKey):void 0,a=n?this.canvasState.getAllActive(n).length:0;n&&this.canvasState.clear(n),this.actionRouter?.reset(),this.deps.logger.info?.(`[canvas-orch] Canvas cleared (${a} surfaces removed)`),this.deps.writeObs({component:"canvas",domain:"canvas",name:"canvas.cleared",severity:"info"})}}else this.deps.logger.warn?.(`[canvas-orch] Canvas notification ${e} ignored — canvasState not initialized`)}handleOpenClawCanvasEvent(e){if(!this.canvasState||!e)return;const s=e,t=s.action;if("a2ui_push"===t||"surface_update"===t){const e=String(s.surfaceId??nextOclId("openclaw")),t=s.components;if(t&&Array.isArray(t)){const n=sanitizeA2UIComponents(t),a=s.canvasId?String(s.canvasId):void 0;this.canvasState.has(e,a)?this.canvasState.update(e,n,a):this.canvasState.create(a??nextOclId("_openclaw"),e,"inline",n,{origin:{kind:"openclaw"}}),this.deps.logger.debug?.(`[canvas-orch] OpenClaw canvas → state + relay (surfaceId=${e})`)}}else if("navigate"===t||"url_open"===t){const e=String(s.url??"");if(e){const t=nextOclId("openclaw-nav"),n=String(s.canvasId??nextOclId("_openclaw_nav")),a=[{id:`${t}-embed`,component:{WebEmbed:{url:{literalString:e},title:{literalString:String(s.title??e)}}}}];this.canvasState.create(n,t,"inline",a,{origin:{kind:"openclaw"}})}}else this.deps.logger.debug?.(`[canvas-orch] Unhandled OpenClaw canvas action: ${t}`)}ensureCanvasForSession(e,s){const t=this.canvasState?.getRelayClient();if(!t)return;const n=e!==s?e:void 0;t.ensureCanvas(s,{linkSessionId:n}).then(async t=>{this.canvasState?.registerSession(s,t),await(this.canvasState?.joinCanvas(t,s)),this.deps.logger.info?.(`[canvas-orch] Canvas ensured: canvas=${t} acpSid=${e} gwKey=${s}`)}).catch(t=>{this.deps.logger.warn?.(`[canvas-orch] Failed to ensure canvas: acpSid=${e} gwKey=${s} err=${t}`),this.pendingCanvasLinks.push({acpSessionId:e,gatewaySessionKey:s})})}async forwardActionToAgent(e){const s=this.deps.state.resolveSessionFromParams(e);if(!s)return void this.deps.logger.warn?.("[canvas-orch] forwardActionToAgent dropped — no session resolved");if(this.emitCanvasActionAsUserMessage(s.sessionId,e),!this.deps.gateway?.isConnected)return void this.deps.logger.warn?.("[canvas-orch] forwardActionToAgent: gateway not connected, pill sent but agent forward skipped");if(s.currentRunId&&!s.promptDone)return this.actionRouter?.enqueue(e),void this.deps.logger.info?.(`[canvas-orch] Agent busy, canvas action queued (runId=${s.currentRunId})`);this.deps.state.resetForNewRun(s,s.sessionId,this.deps.transport);const t=this.canvasState?.summarize(e.surfaceId??""),n=formatActionForAgent(e,t??void 0);try{const e=await this.deps.gateway.agentSend(s.gatewaySessionKey,n,this.deps.config.gateway.agentId||this.deps.getDefaultAgentId());s.currentRunId=e.runId,s.assistantStreamStartedAt=Date.now(),this.deps.state.promptsByRunId.set(e.runId,s)}catch(t){this.deps.logger.warn?.(`[canvas-orch] Agent forward failed: ${t instanceof Error?t.message:t}`),this.deps.transport.sendSessionUpdate(s.sessionId,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`> _User interacted with "${e.userAction?.name??"unknown"}" on canvas._`}})}}emitCanvasActionAsUserMessage(e,s){const t=s.userAction?.name??"unknown",n=`Clicked "${t}"`;this.deps.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:{type:"text",text:n},metadata:{type:"canvas_action",surfaceId:s.surfaceId,actionName:t,sourceComponentId:s.userAction?.sourceComponentId,actionPayload:s.userAction?.context,actionTimestamp:s.userAction?.timestamp}}),this.deps.logger.info?.(`[canvas-orch] Emitted canvas action pill: session=${e} action=${t} surface=${s.surfaceId}`)}handleRelayAction(e){if(!this.canvasState||!this.actionRouter)return void this.deps.logger.warn?.("[canvas-orch] Relay action ignored — canvas not initialized");const{canvasId:s,surfaceId:t,userAction:n,requestId:a,senderClientId:i}=e,o=this.canvasState.resolveSessionForCanvas(s);if(!o)return this.deps.logger.warn?.(`[canvas-orch] Relay action ignored — no session for canvas=${s}`),void this.sendRelayActionError(s,a,i,"no_session");const r=this.deps.state.findSessionByKey(o);if(!r)return this.deps.logger.warn?.(`[canvas-orch] Relay action ignored — session not found for gatewayKey=${o}`),void this.sendRelayActionError(s,a,i,"no_session");const c={surfaceId:t,userAction:{name:n.name,sourceComponentId:n.sourceComponentId,timestamp:n.timestamp,context:n.context},sessionId:r.sessionId};this.actionRouter.route(c,s).then(()=>{this.deps.logger.info?.(`[canvas-orch] Relay action routed: surface=${t} request=${a}`),this.sendRelayActionResult(s,a,i,"ok")}).catch(e=>{this.deps.logger.warn?.(`[canvas-orch] Relay action routing failed: ${e instanceof Error?e.message:e}`),this.sendRelayActionError(s,a,i,"processing_failed")})}sendRelayActionResult(e,s,t,n){this.canvasState?.getRelayClient()?.sendActionResult({canvasId:e,requestId:s,targetClientId:t,status:n})}sendRelayActionError(e,s,t,n){this.canvasState?.getRelayClient()?.sendActionError({canvasId:e,requestId:s,targetClientId:t,error:n})}destroy(){this.canvasState?.destroy(),this.actionRouter?.reset(),this.pendingCanvasLinks.length=0,this.canvasState=null,this.actionRouter=null,this.resourceRenderer=null,this.descriptorRegistry=null,this.formBuilder=null,this.schemaRegistry=null}}
1
+ import{CanvasActionRouter,formatActionForAgent}from"./canvas/action-router.js";import{FormSchemaRegistry}from"./surface/form-schema-registry.js";import{FormSurfaceBuilder}from"./surface/form-surface-builder.js";import{ResourceSurfaceRenderer}from"./surface/resource-surface-renderer.js";import{CANVAS_TERM_MAPPING}from"./canvas/tools.js";let _oclSeq=0;function nextOclId(e){return`${e}-${Date.now()}-${++_oclSeq}`}const STRING_PROPS=new Set(["text","url","label","name","title","description","placeholder","icon","src","alt","value","hint","caption"]);function toPascalCase(e){return e?e[0]>="A"&&e[0]<="Z"?e:e.charAt(0).toUpperCase()+e.slice(1):"Text"}function sanitizeA2UIComponents(e){return Array.isArray(e)?e.map(e=>{if("object"!=typeof e||null===e)return e;const s=e;if("component"in s&&"object"==typeof s.component&&null!==s.component){const e=s.component;for(const[s,a]of Object.entries(e)){if("object"==typeof a&&null!==a){const n=a;for(const[e,s]of Object.entries(n))"string"==typeof s&&STRING_PROPS.has(e)&&(n[e]={literalString:s});e[s]=n}if(s[0]>="a"&&s[0]<="z"){e[toPascalCase(s)]=e[s],delete e[s]}}s.component=e}return s}):e}export class CanvasOrchestrator{canvasState=null;resourceRenderer=null;descriptorRegistry=null;actionRouter=null;formBuilder=null;schemaRegistry=null;pendingCanvasLinks=[];deps;constructor(e){this.deps=e}updateDeps(e){Object.assign(this.deps,e)}setCanvasState(e,s){this.canvasState=e;const a=async(e,s)=>{if(!this.deps.gateway?.isConnected)throw new Error("Gateway not connected");return this.deps.gateway.request(e,s)},n=()=>{const s=this.deps.getSessionIdForToolCall();if(!s)return;const a=this.deps.state.sessions.get(s),n=a?.gatewaySessionKey;return n?e.resolveCanvasForSession(n):void 0},t=s?.schemaRegistry??new FormSchemaRegistry,i=new FormSurfaceBuilder({canvasState:e,gatewayRequest:a,resolveCanvasId:n,logger:this.deps.logger});this.formBuilder=i,this.schemaRegistry=t,this.descriptorRegistry=s?.descriptorRegistry??null,this.resourceRenderer=new ResourceSurfaceRenderer({canvasState:e,gatewayRequest:a,schemaRegistry:t,formBuilder:i,resolveCanvasId:n,logger:this.deps.logger}),this.actionRouter=new CanvasActionRouter({stateManager:e,logger:this.deps.logger,writeObs:e=>this.deps.writeObs(e),forwardToAgent:e=>this.forwardActionToAgent(e),emitActionAsUserMessage:e=>{const s=this.deps.state.resolveSessionFromParams(e);s?this.emitCanvasActionAsUserMessage(s.sessionId,e):this.deps.logger.warn?.("[canvas-orch] emitActionAsUserMessage: session not found, pill skipped",{surfaceId:e.surfaceId,sessionId:e.sessionId})}})}wireRelayActionHandler(){const e=this.canvasState?.getRelayClient();e?(e.onAction(e=>{this.handleRelayAction(e)}),e.onCanvasControl(e=>{this.handleRelayCanvasControl(e)}),this.deps.logger.debug?.("[canvas-orch] Canvas Relay action+control handlers wired")):this.deps.logger.warn?.("[canvas-orch] wireRelayActionHandler called but no relay client available")}handleRelayCanvasControl(e){if("canvas.toggle"===e.type){const s=!0===e.visible||!0===e.enabled,a={enabled:s,canvasId:e.canvasId,...e.sessionId?{sessionId:e.sessionId}:{}};if(e.sessionId?this.deps.state.resolveSessionFromParams(a):void 0)return void this.handleCanvasNotification("canvas/toggle",a);let n=0;for(const e of this.deps.state.sessionsByGatewayKey.values())e.canvasActive=s,s&&(e.canvasPromptInjected=!1),n++;for(const e of this.deps.state.sessions.values())e.canvasActive=s,s&&(e.canvasPromptInjected=!1),n++;return void this.deps.logger.info?.(`[canvas-orch] Relay canvas.toggle enabled=${s} fan-out sessions≈${n} canvasId=${e.canvasId}`)}"canvas.dimensions"!==e.type?"canvas.reference"!==e.type?"canvas.clear"===e.type&&this.handleCanvasNotification("canvas/clear",{canvasId:e.canvasId,...e.sessionId?{sessionId:e.sessionId}:{}}):this.handleCanvasNotification("canvas/reference",{canvasId:e.canvasId,...e.sessionId?{sessionId:e.sessionId}:{},surfaceId:e.surfaceId??null}):this.handleCanvasNotification("canvas/dimensions",{canvasId:e.canvasId,...e.sessionId?{sessionId:e.sessionId}:{},width:e.width,height:e.height,...void 0===e.devicePixelRatio?{}:{devicePixelRatio:e.devicePixelRatio}})}isCanvasActive(e){if(!e)return!1;const s=this.deps.state.sessions.get(e);return!0===s?.canvasActive}isCanvasActiveForGatewayKey(e){const s=this.deps.state.sessionsByGatewayKey.get(e);return!0===s?.canvasActive}onSessionLoaded(e,s,a){if(!this.deps.config.canvasEnabled)return;const n=this.canvasState?.getRelayClient();if(!n)return this.pendingCanvasLinks.push({acpSessionId:e,gatewaySessionKey:s}),void this.deps.logger.info?.(`[canvas-orch] Relay not ready, queued canvas link: acpSid=${e} gwKey=${s}`);this.ensureCanvasForSession(e,s)}onSessionDeleted(e){this.canvasState?.removeSession(e)}onSessionCleared(e,s){const a=this.canvasState?.resolveCanvasForSession(s);a&&(this.canvasState.clear(a),this.actionRouter?.reset())}onPromptStart(){this.actionRouter?.clearPending()}async onPromptComplete(e){try{await(this.actionRouter?.drainPending())}catch(e){this.deps.logger.warn?.(`[canvas-orch] Drain pending canvas actions failed: ${e instanceof Error?e.message:e}`)}}onPromptFailed(){this.actionRouter?.clearPending()}flushPendingCanvasLinks(){const e=this.pendingCanvasLinks.splice(0);if(0===e.length)return;const s=new Set,a=e.filter(({gatewaySessionKey:e})=>!s.has(e)&&(s.add(e),!this.canvasState?.resolveCanvasForSession(e)));if(0!==a.length){this.deps.logger.info?.(`[canvas-orch] Flushing ${a.length} pending canvas link(s)`);for(const{acpSessionId:e,gatewaySessionKey:s}of a)this.ensureCanvasForSession(e,s)}}onRelayReconnected(){this.deps.logger.info?.("[canvas-orch] Relay reconnected — retrying pending canvas links"),this.flushPendingCanvasLinks()}async handleSlashCommand(e){const s=this.descriptorRegistry?.getBySlashCommand(e);return!(!s||!this.resourceRenderer)&&(await this.resourceRenderer.render(s,"inline"),!0)}async enrichMessageWithCanvasContext(e,s,a){if(!this.canvasState)return e;if(a.has(s.split(":").slice(0,2).join(":"))||a.has(s))return e;try{let a=this.canvasState.resolveCanvasForSession(s);if(!a){const e=this.canvasState.getRelayClient();if(e)try{a=await e.ensureCanvas(s),this.canvasState.registerSession(s,a)}catch(e){this.deps.logger.debug?.(`[canvas-orch] ensureCanvas for prompt inject failed (session may not be linked yet): ${e instanceof Error?e.message:String(e)}`)}}if(a){let s=this.canvasState.buildContextForCanvas(a);s||(await this.canvasState.lazyLoadCanvas(a),s=this.canvasState.buildContextForCanvas(a));return`${CANVAS_TERM_MAPPING}\n\n${s??"[Canvas] Canvas panel is available. Use render_ui for structured/visual content."}\n\n${e}`}}catch(e){this.deps.logger.warn?.(`[canvas-orch] Canvas context injection failed: ${e instanceof Error?e.message:String(e)}`)}return e}handleCanvasNotification(e,s){if(this.canvasState){if("canvas/toggle"===e){const e=s,a=!1!==e.enabled,n=this.deps.state.resolveSessionFromParams(e);if(n&&(n.canvasActive=a,a&&(n.canvasPromptInjected=!1,n.sessionId))){const e=n.sessionId,s=n.gatewaySessionKey,a=n.agentId,t=this.canvasState?.getRelayClient();if(t){const i=this.deps.resolvePlatformAgentId(a??"")??n.platformAgentId,o=s&&s!==e?e:void 0;t.ensureCanvas(s??e,{agentId:i,linkSessionId:o}).catch(e=>{this.deps.logger.warn?.(`[canvas-orch] Failed to pre-create canvas on toggle: ${e}`)})}}this.deps.logger.info?.(`[canvas-orch] Canvas toggled: ${a}`),this.deps.writeObs({component:"canvas",domain:"canvas",name:"canvas.toggle",severity:"info",payload:{enabled:a}})}else if("canvas/clear"===e){const e=s,a=this.deps.state.resolveSessionFromParams(e),n=a?this.canvasState.resolveCanvasForSession(a.gatewaySessionKey):void 0,t=n?this.canvasState.getAllActive(n).length:0;n&&this.canvasState.clear(n),this.actionRouter?.reset(),this.deps.logger.info?.(`[canvas-orch] Canvas cleared (${t} surfaces removed)`),this.deps.writeObs({component:"canvas",domain:"canvas",name:"canvas.cleared",severity:"info"})}}else this.deps.logger.warn?.(`[canvas-orch] Canvas notification ${e} ignored — canvasState not initialized`)}handleOpenClawCanvasEvent(e){if(!this.canvasState||!e)return;const s=e,a=s.action;if("a2ui_push"===a||"surface_update"===a){const e=String(s.surfaceId??nextOclId("openclaw")),a=s.components;if(a&&Array.isArray(a)){const n=sanitizeA2UIComponents(a),t=s.canvasId?String(s.canvasId):void 0;this.canvasState.has(e,t)?this.canvasState.update(e,n,t):this.canvasState.create(t??nextOclId("_openclaw"),e,"inline",n,{origin:{kind:"openclaw"}}),this.deps.logger.debug?.(`[canvas-orch] OpenClaw canvas → state + relay (surfaceId=${e})`)}}else if("navigate"===a||"url_open"===a){const e=String(s.url??"");if(e){const a=nextOclId("openclaw-nav"),n=String(s.canvasId??nextOclId("_openclaw_nav")),t=[{id:`${a}-embed`,component:{WebEmbed:{url:{literalString:e},title:{literalString:String(s.title??e)}}}}];this.canvasState.create(n,a,"inline",t,{origin:{kind:"openclaw"}})}}else this.deps.logger.debug?.(`[canvas-orch] Unhandled OpenClaw canvas action: ${a}`)}ensureCanvasForSession(e,s){const a=this.canvasState?.getRelayClient();if(!a)return;const n=e!==s?e:void 0;a.ensureCanvas(s,{linkSessionId:n}).then(async a=>{this.canvasState?.registerSession(s,a),await(this.canvasState?.joinCanvas(a,s)),this.deps.logger.info?.(`[canvas-orch] Canvas ensured: canvas=${a} acpSid=${e} gwKey=${s}`)}).catch(a=>{this.deps.logger.warn?.(`[canvas-orch] Failed to ensure canvas: acpSid=${e} gwKey=${s} err=${a}`),this.pendingCanvasLinks.push({acpSessionId:e,gatewaySessionKey:s})})}async forwardActionToAgent(e){const s=this.deps.state.resolveSessionFromParams(e);if(!s)return void this.deps.logger.warn?.("[canvas-orch] forwardActionToAgent dropped — no session resolved");if(this.emitCanvasActionAsUserMessage(s.sessionId,e),!this.deps.gateway?.isConnected)return void this.deps.logger.warn?.("[canvas-orch] forwardActionToAgent: gateway not connected, pill sent but agent forward skipped");if(s.currentRunId&&!s.promptDone)return this.actionRouter?.enqueue(e),void this.deps.logger.info?.(`[canvas-orch] Agent busy, canvas action queued (runId=${s.currentRunId})`);this.deps.state.resetForNewRun(s,s.sessionId,this.deps.transport);const a=this.canvasState?.summarize(e.surfaceId??""),n=formatActionForAgent(e,a??void 0);try{const e=await this.deps.gateway.agentSend(s.gatewaySessionKey,n,this.deps.config.gateway.agentId||this.deps.getDefaultAgentId());s.currentRunId=e.runId,s.assistantStreamStartedAt=Date.now(),this.deps.state.promptsByRunId.set(e.runId,s)}catch(a){this.deps.logger.warn?.(`[canvas-orch] Agent forward failed: ${a instanceof Error?a.message:a}`),this.deps.transport.sendSessionUpdate(s.sessionId,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`> _User interacted with "${e.userAction?.name??"unknown"}" on canvas._`}})}}emitCanvasActionAsUserMessage(e,s){const a=s.userAction?.name??"unknown",n=`Clicked "${a}"`;this.deps.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:{type:"text",text:n},metadata:{type:"canvas_action",surfaceId:s.surfaceId,actionName:a,sourceComponentId:s.userAction?.sourceComponentId,actionPayload:s.userAction?.context,actionTimestamp:s.userAction?.timestamp}}),this.deps.logger.info?.(`[canvas-orch] Emitted canvas action pill: session=${e} action=${a} surface=${s.surfaceId}`)}handleRelayAction(e){if(!this.canvasState||!this.actionRouter)return void this.deps.logger.warn?.("[canvas-orch] Relay action ignored — canvas not initialized");const{canvasId:s,surfaceId:a,userAction:n,requestId:t,senderClientId:i}=e,o=this.canvasState.resolveSessionForCanvas(s);if(!o)return this.deps.logger.warn?.(`[canvas-orch] Relay action ignored — no session for canvas=${s}`),void this.sendRelayActionError(s,t,i,"no_session");const r=this.deps.state.findSessionByKey(o);if(!r)return this.deps.logger.warn?.(`[canvas-orch] Relay action ignored — session not found for gatewayKey=${o}`),void this.sendRelayActionError(s,t,i,"no_session");const c={surfaceId:a,userAction:{name:n.name,sourceComponentId:n.sourceComponentId,timestamp:n.timestamp,context:n.context},sessionId:r.sessionId};this.actionRouter.route(c,s).then(()=>{this.deps.logger.info?.(`[canvas-orch] Relay action routed: surface=${a} request=${t}`),this.sendRelayActionResult(s,t,i,"ok")}).catch(e=>{this.deps.logger.warn?.(`[canvas-orch] Relay action routing failed: ${e instanceof Error?e.message:e}`),this.sendRelayActionError(s,t,i,"processing_failed")})}sendRelayActionResult(e,s,a,n){this.canvasState?.getRelayClient()?.sendActionResult({canvasId:e,requestId:s,targetClientId:a,status:n})}sendRelayActionError(e,s,a,n){this.canvasState?.getRelayClient()?.sendActionError({canvasId:e,requestId:s,targetClientId:a,error:n})}destroy(){this.canvasState?.destroy(),this.actionRouter?.reset(),this.pendingCanvasLinks.length=0,this.canvasState=null,this.actionRouter=null,this.resourceRenderer=null,this.descriptorRegistry=null,this.formBuilder=null,this.schemaRegistry=null}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beeos-claw",
3
- "version": "0.6.40-staging.2",
3
+ "version": "0.6.40-staging.3",
4
4
  "description": "OpenClaw plugin that bridges the BeeOS platform with the local OpenClaw Gateway, providing file transfer, ACP bridge, and observability.",
5
5
  "license": "MIT",
6
6
  "author": "BeeOS <dev@beeos.ai>",