This tab, joined to its signed-in listener's own hub (worker/src/tab-hub.ts) so the listener's own AI, connected through the hosted MCP (worker/src/hosted-mcp.ts, /ai/), can play code here.
The tab joins only while someone is signed in (account.mjs), over a WebSocket to /jev/me/tabs that carries the session cookie; the Worker puts it in that account's hub and no other. It leaves when they sign out.
What the AI plays is not the page's code: it goes into the editor as
foreign code ({ ai: app }) and plays only in the sandbox (sandbox.mjs),
in an opaque origin, never in this page. So code an agent was talked into
writing cannot act as the listener here: it cannot read the page, post,
publish, or reach the session. Its Jev calls are charged to the
listener's own budget, made by this page (sandbox.mjs has why). Its logs
come back through the sandbox's protocol, rebuilt and bounded there.
Commands, from the hub: play { code, app }, stop, get-code, get-logs, status, and the tools both MCPs share (tabTools.mjs: the sounds, Jev's picks, settings, the console, a song by id, a reaction). Each is answered { type: 'reply', id, … }, bounded to what the hub takes (tab-hub-core.ts).
25export const TABS_PATH = '/jev/me/tabs';
as the hub bounds them (worker/src/tab-hub-core.ts)
What the sandbox has logged since the AI's last play.
35let logs = []; 36let logStart = typeof performance !== 'undefined' ? performance.now() : 0; 37export function recordLog(message, kind = '') { 38 const text = String(message).slice(0, MAX_LOG_CHARS); 39 const last = logs.at(-1); 40 if (last?.message === text && last.kind === kind) { 41 last.count++; 42 return; 43 } 44 logs.push({ at: (performance.now() - logStart) / 1000, message: text, kind: kind || '', count: 1 }); 45 if (logs.length > MAX_LOG_LINES) logs = logs.slice(-MAX_LOG_LINES); 46} 47export function clearLogs() { 48 logs = []; 49 logStart = performance.now(); 50}
What the editor holds, for get_status.
60const bytes = (s) => new TextEncoder().encode(s).length;
the shared tools' page modules load with the first such command
Whether the sandbox's logs are this MCP's: code its AI played, or a listener's song it played by id (tabTools.mjs).
67export const loggedForMcp = (foreign, played = playedForeign()) => Boolean(foreign?.ai || (foreign && foreign === played));
One command from the hub, answered. deps are the sandbox's, for tests.
70export async function runCommand(editor, command, deps = defaults) { 71 if (!editor) return { error: 'the editor is not ready' }; 72 switch (command?.type) { 73 case 'play': { 74 const { code } = command; 75 if (typeof code !== 'string' || !code.trim()) return { error: 'code is empty' }; 76 if (bytes(code) > MAX_CODE_BYTES) return { error: `code is at most ${MAX_CODE_BYTES / 1024} KiB` }; 77 const app = typeof command.app === 'string' && command.app.trim() ? command.app.trim().slice(0, 80) : 'your AI'; 78 // whatever played before stops: the page's own song and any sandbox 79 await editor.stop(); 80 clearLogs(); 81 // never into the page: foreign, so it plays only in the sandbox 82 deps.showForeign(editor, code, { ai: app }); 83 return deps.playForeign(code); 84 } 85 case 'stop': 86 await editor.stop(); 87 return { ok: true }; 88 case 'get-code': 89 return { code: editor.code ?? '' }; 90 case 'get-logs': 91 return { logs }; 92 case 'status': { 93 const inSandbox = deps.sandbox.playing(); 94 const started = Boolean(editor.repl?.scheduler?.started); 95 const cycle = inSandbox ? deps.sandboxCycle() : started ? editor.repl.scheduler.now() : null; 96 return { 97 status: { playing: inSandbox || started, cycle: Number.isFinite(cycle) ? cycle : null, code: codeKind(editor, deps.sandbox.foreign()) }, 98 }; 99 } 100 default: { 101 if (!TAB_TOOLS.has(command?.type)) return { error: `unknown command ${command?.type}` }; 102 // a song played by id starts the logs afresh, as a play does 103 if (command.type === 'play-song' || (command.type === 'ask-song' && command.play)) clearLogs(); 104 return deps.tabTool(editor, command); 105 } 106 } 107}
The reply as sent: within what the hub accepts, whatever the tab had.
110export function replyFrame(id, reply) { 111 let frame = JSON.stringify({ type: 'reply', id, ...reply }); 112 if (frame.length <= MAX_REPLY_BYTES) return frame; 113 if (reply.logs) { 114 let kept = reply.logs; 115 while (kept.length && frame.length > MAX_REPLY_BYTES) { 116 kept = kept.slice(Math.ceil(kept.length / 2)); 117 frame = JSON.stringify({ type: 'reply', id, ...reply, logs: kept }); 118 } 119 if (frame.length <= MAX_REPLY_BYTES) return frame; 120 } 121 return JSON.stringify({ type: 'reply', id, error: 'the answer is too large to send' }); 122}
Joins while signed in; onStatus hears 'off' (signed out), 'connecting',
'connected'. Returns a function that leaves.
126export function joinMyTabs(editorRef, sessionId, onStatus = () => {}) { 127 let socket = null; 128 let retry = null; 129 let ping = null; 130 let attempts = 0; 131 let userId = null; 132 let closed = false; 133 134 const unlog = onSandboxLog((message, kind) => { 135 if (loggedForMcp(sandbox.foreign())) recordLog(message, kind); 136 }); 137 138 const leave = () => { 139 clearTimeout(retry); 140 clearInterval(ping); 141 retry = ping = null; 142 const s = socket; 143 socket = null; 144 s?.close(1000, 'left'); 145 }; 146 147 const connect = () => { 148 if (closed || !userId || socket) return; 149 onStatus('connecting'); 150 const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; 151 const s = new WebSocket(`${protocol}//${location.host}${TABS_PATH}?session_id=${encodeURIComponent(sessionId)}`); 152 socket = s; 153 s.onopen = () => { 154 attempts = 0; 155 onStatus('connected'); 156 ping = setInterval(() => s.readyState === WebSocket.OPEN && s.send('ping'), PING_MS); 157 }; 158 s.onmessage = async (event) => { 159 if (event.data === 'pong') return; 160 let message; 161 try { 162 message = JSON.parse(event.data); 163 } catch { 164 return; 165 } 166 if (message?.type !== 'command' || typeof message.id !== 'string') return; 167 let reply; 168 try { 169 reply = await runCommand(editorRef.current, message.command); 170 } catch (e) { 171 reply = { error: e?.message ?? String(e) }; 172 } 173 if (s.readyState === WebSocket.OPEN) s.send(replyFrame(message.id, reply)); 174 }; 175 s.onclose = (event) => { 176 clearInterval(ping); 177 ping = null; 178 if (socket !== s) return; 179 socket = null; 180 if (closed || !userId) return; 181 onStatus('connecting'); 182 // replaced by this tab's newer connection, or pushed out by newer tabs: stay out 183 if (event.code === 4008) { 184 onStatus('off'); 185 return; 186 } 187 retry = setTimeout(connect, RETRY_MS[Math.min(attempts++, RETRY_MS.length - 1)]); 188 }; 189 }; 190 191 const follow = () => { 192 const id = account.get().user?.id ?? null; 193 if (id === userId) return; 194 userId = id; 195 leave(); 196 attempts = 0; 197 if (userId) connect(); 198 else onStatus('off'); 199 }; 200 const unsubscribe = account.subscribe(follow); 201 follow(); 202 203 return () => { 204 closed = true; 205 unsubscribe(); 206 unlog(); 207 leave(); 208 }; 209}