Repository navigation
feat(mcp): take mouse and keyboard in the task view when Sai isn't working - #50
Conversation
…rking The in-chat task view (MCP Apps) and the local watch_url page streamed the screen view-only. They now forward the person's mouse, touch and keyboard while the task waits for their approval, and after it ends through a new "Use this computer" button. While Sai works, input is paused, the same rule as the hosted live_url page. The keyboard binds to the screen element only, so the host's chat box keeps its keys; Control+Alt+Escape gives them back. Turning input off, losing focus or closing the stream releases any held key or button. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lihautan
left a comment
There was a problem hiding this comment.
I read the diff at 1742f1b against the full task-view.ts (poll loop, decide, followScreen) and input.ts. Checked: when input is turned on and off around approval and task end; releasing held keys and buttons on off, blur, close and dispose; keyboard scope (bound to the screen element, never the document); and whether the view can now see more of the tunnel or its token. input.ts is clean: release() runs before on flips, and dispose() runs before the client disconnects, so key-ups still go out. The in-card Approve path also turns input off before the approve call, because decide sets status: 'running' and then calls render().
Blocking
task-view.ts:76+:228: after the task ends, "Use this computer" can stay on while Sai works on that computer again.inputAllowed()isisDone(view), andkeepLive()isusing. Both are about this task only.poll()stops once the task is done (:181), so the card never learns that a newer task started on the same machine. The person keeps mouse and keyboard on the screen while that task runs. Thearchivedsignal at:197would catch it, but only while polling. The bad case: use the computer from an old card, the model sends the next task, and both drive the screen. A related path: after a takeover,usestays visible (:118ignorestakenOver).followScreenresetstakenOver = false(:259), so this old card takes the screen back from the new task's card, with input on.
Fix: whileusing, keep polling (onesai_view_pollevery 4 s already fits the rate limit). Onarchived, or on a status that isn't done, callstopUsing()with a note. Also gateuseon the machine not running another task. If the server can't tell us that, at least say in the PR that this case isn't covered.
Non-blocking
task-view.ts:76: an approval made outside this card leaves input on for up to one poll (about 4 s) while Sai resumes. That covers the link-onlyapprovalUrlsign-in case and approvals in the Playground orlive_url. Today the card can't detect it sooner. Worth a line in the PR, or a quicker poll whileneeds_approval.task-view.tsstop-use.onclick: Done setsusing = false, butrender()still callssetInput(liveConnected && isDone(view)), which is still true. Input stays on untiluntilEnd's next 1 s tick closes the stream. Calllive?.setInput(false)in the handler, or makeinputAllowed()requireusingonce the task is done.- No tests cover the gating in
task-view.ts: on duringneeds_approval, off afterdecide,use→stopUsingwhensai_view_livehas no URL or the stream isunreachable, and Done.input.test.tscovers the binding only. - The PR says the hand test in Claude web and desktop, and on the
watch_urlpage, hasn't run yet. Check Control+Alt+Escape inside the Claude iframe: hosts may take that chord before the iframe sees it. - Tunnel and token: no change.
websocket_urlwas already in the view. Input gating is client-side only, as it is onlive_url. Anyone who has the tunnel URL could always send input, so this PR adds no server-side exposure.
Review on #50: after a task ended, the view stopped polling, so input stayed on while a newer task drove the same computer. - While the person uses the computer, the view keeps polling (one sai_view_poll per 4 s). It stops using, with a note, when the task is archived or expired, or the session's status is no longer done. "Use this computer" runs that check before it connects. - The task log also archives a task when a newer one starts on the same machine, so a new_session task counts too. - Input after the task requires "using": Done turns it off at once instead of on the screen's next close tick. - "Use this computer" is hidden after another viewer took the screen over, so an old card can't take it back from a newer task's card. - The gating rules move to view-model (inputAllowed, useEnd, useStopsOnLoss) with unit tests; a TaskLog test covers the machine case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@lihautan thanks, addressed in 9f8ebb1. Blocking: input stays on while a newer task drives the computer
Non-blocking
|
lihautan
left a comment
There was a problem hiding this comment.
Re-review of 1742f1b..9f8ebb1. The blocking issue is fixed, so I'm approving.
Checked:
- Newer task on the same computer. After the task, the poll loop keeps running while
using((!isDone(view) || using)). It stops onarchived, onexpired, or on a status that isn't done, and it doesn't fold the new turn's events into the old card.sai_view_pollalways returnsstatusfromdeps.wait, souseEndcan't stop use by mistake whenstatusis missing. - Use this computer now polls once before
followScreen().render()runs first, butliveConnectedis still false, so input stays off until the check passes. TaskLog.state()now returnsarchivedwhen a newer task starts on the samemachineIdfrom another session (new_session).app-view.test.tscovers this.- After a takeover,
useis hidden, so an old card can't take the screen back. - Done turns input off at once:
inputAllowed(view, using)needsusingafter the task. stopUsing()turns input off in the same tick (render()). It shows the note now, or when the screen closes.gating.test.tscovers the rules that moved toview-model.ts. CI passes on ubuntu, macOS and Windows.- The PR now states the cases it doesn't cover: a task this sai-mcp can't see, and an approval made outside the card (up to one 4 s poll).
Non-blocking:
useStopsOnLoss(view, loss: 'no_tunnel' | string): the union becomes plainstring.'no_tunnel' | 'unreachable'(or the stream's kind type) would catch a typo at the call site.- The hand test in Claude web/desktop and on
watch_url, including Control+Alt+Escape inside the iframe, is still to do before merge, as you said.
Why
Ang asked for the in-chat live view to take input ("if it's a small change we should make it controllable", #sai-api, 10-31). The task view, shown inside Claude (MCP Apps) and as the local
watch_urlpage for Claude Code / Codex CLI, streamed the screen view-only. The Playground and the hostedlive_urlpage already take input over the same Guacamole session.What
live_urlpage:sai_view_pollper 4 s). Input goes off, with a note, once Sai starts another task on that computer: the task is archived (a newer task from this sai-mcp on the same session or the same machine, includingnew_session), or the session starts a new turn. Use this computer runs this check before it connects, and is hidden after another viewer took the screen over.ui/input.tsmirrors the Playground'sbindComputerInput(sai-platformsrc/playground/guacamole/input.ts).Not covered:
live_url) leaves input on for up to one poll (about 4 s) while Sai resumes. 4 s is the/eventsrate-limit floor, so the poll can't go faster.Not included: a free machine's control lease. The Playground takes one on input to keep the network on; the
live_urlpage doesn't, and this matcheslive_url.Tests
ui/__tests__/input.test.ts:ui/__tests__/gating.test.ts: input on duringneeds_approval, off once running (afterdecide), after the task only while using (Done turns it off); using stops on archived / expired / a non-done status, and when the computer has no tunnel or is unreachable after the task.app-view.test.ts: a newer task on the same machine, in another session, archives the older one.npm run typecheck,lint,test(122) andbuildpass.watch_urlpage; that test runs before merge.Docs: sai-platform
coding-agents/live-view.mdstill says the task view is view-only; a docs PR follows once this is tested.🤖 Generated with Claude Code