fix(chromium-headful): sync clipboard to viewers under implicit hosting#311
fix(chromium-headful): sync clipboard to viewers under implicit hosting#311breteo wants to merge 1 commit into
Conversation
The neko server only delivers remote-clipboard updates to the session registered as host. Under implicit hosting the client never sends control/request (the request() action bails because hosting is always true), so no session ever registers and clipboard updates are dropped — copying inside the remote browser never reaches the viewer's local clipboard. Port upstream m1k1o/neko#540: request control on the first interaction with the screen (buffering the triggering mouse event and replaying it once control is granted), and gate request() on an explicit-host 'controlling' getter instead of 'hosting'.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 0388493. Configure here.
|
|
||
| this.reqMouseDown = null | ||
| this.reqMouseUp = null | ||
| } |
There was a problem hiding this comment.
Incomplete click replay stuck button
Medium Severity
Under implicit hosting, a mousedown on the overlay stores reqMouseDown and sends control/request, but reqMouseUp is only set when onMouseUp runs on the same overlay. If the pointer is released outside the overlay before control is granted, onControlChange replays only onMouseDown, sending a remote mousedown with no matching mouseup.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 0388493. Configure here.
| } | ||
|
|
||
| if (controlling && this.reqMouseUp) { | ||
| this.onMouseUp(this.reqMouseUp) |
There was a problem hiding this comment.
Buffered mouseup replay skipped
Medium Severity
If control is lost after a forwarded mousedown but before mouseup, the physical mouseup clears isMouseDown and only stores reqMouseUp because controlling is false. When controlling becomes true again, @Watch('controlling') replays via onMouseUp, which returns immediately when isMouseDown is false, so the remote never gets mouseup. A later implicit mousedown clears reqMouseUp without flushing that state, leaving a stuck pressed button on the remote.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 0388493. Configure here.
## Problem Fixes #310. In an embedded Live View, copying text inside the remote browser never updates the viewer's local clipboard. Root cause is client-side control registration, not browser clipboard permissions: - Kernel runs neko with `session.implicit_hosting: true`. - The client's `remote.hosting` getter is `true` whenever implicit hosting is on, even though the viewer was never registered as a host server-side. - `remote.request()` short-circuits when `hosting` is true, so the client never sends `control/request`. - neko delivers remote clipboard updates only to the registered host (`sessions.GetHost()`); with no host registered, `control/clipboard` is never sent, so `navigator.clipboard.writeText()` never runs. Sending a manual `control/request` in a live session produces `control/locked`, after which the very next copy delivers `control/clipboard` and the paste works — isolating the defect to the missing client request. This matches upstream `m1k1o/neko#540`. ## Change - **`store/remote.ts`** — add a `controlling` getter (true only when the viewer is the actual registered host) and guard `request()` on it instead of the broad `hosting`. The `id !== ''` check prevents the pre-connection empty-id state from reading as controlling. - **`components/video.vue`** — on the first interaction under implicit hosting, call `remote.request()` so the session registers as host, while still forwarding the original click immediately. The pinned neko server accepts input via `LegacyIsHost()` under implicit hosting, so no mousedown/mouseup buffering + replay is required. - **`neko/index.ts`** — stop surfacing the "you took the controls" toast to the viewer. Control now registers automatically on the first click, so the toast would otherwise fire on essentially every session's first interaction. Notifications for *other* members taking/stealing control are left intact. ## Relationship to #311 Same diagnosis and the same store-level fix as #311 (both port upstream `m1k1o/neko#540`). This PR intentionally diverges in `video.vue`: instead of buffering the first mouse down/up and replaying them via a `@Watch('controlling')`, it forwards the first click immediately and relies on the pinned server accepting pre-registration input. This avoids the stuck-remote-button edge cases raised in #311's review (replaying a `mousedown` with no matching `mouseup`; a single `isMouseDown` boolean mishandling multi-button chords). It also adds the "you took the controls" toast suppression, which #311 does not. ## Verification Built the patched `chromium-headful` image and embedded its Live View in a cross-origin iframe (`allow="clipboard-read; clipboard-write"`), driven with Playwright while capturing `/ws` control frames: - **Baseline (main):** first click sends no `control/request`; remote Ctrl+C yields no `control/clipboard`; paste stays stale. - **Fixed:** first click → exactly one `control/request` → `control/locked` → remote Ctrl+C → `control/clipboard` with the exact text → paste matches. Second click sends no duplicate request; the first click is still delivered once (no dropped/stuck button); no "you took the controls" toast appears. macOS Cmd+C remains a separate, pre-existing gap noted in #310 (the client special-cases paste, not copy) and is out of scope here. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes WebSocket control registration and clipboard delivery for embedded sessions; behavior is localized to the neko client but affects first-interaction control state. > > **Overview** > Fixes embedded Live View clipboard sync when **implicit hosting** is enabled: the client treated the viewer as “hosting” without ever sending `control/request`, so the server never delivered `control/clipboard` to the viewer. > > **`store/remote.ts`** adds a **`controlling`** getter (viewer is the registered host) and a **`requesting`** flag so **`request()`** is no longer blocked by broad **`hosting`** and rapid clicks cannot spam duplicate requests before **`control/locked`**. > > **`video.vue`** calls **`remote.request()`** on the first mouse down when implicit hosting is on and the viewer is not yet controlling, while still forwarding that click to the remote (no buffered replay). > > **`neko/index.ts`** removes the self-only “you took the controls” toast on **`control/locked`**, since auto-registration on first click would show it on every session. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit c15d5d5. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude <noreply@anthropic.com>


Description
Copying inside a remote browser session never reaches the viewer's local clipboard. The neko server only sends remote-clipboard updates to the registered host session, and under implicit hosting the vendored client never sends
control/request(remote.tsrequest()bails becausehostingis always true), so no host is ever registered and the updates are silently dropped. Full analysis and a live repro in #310.This is a port of upstream m1k1o/neko#540 (which fixed the same bug, m1k1o/neko#499) onto the client vendored here:
remote.ts: add an explicit-hostcontrollinggetter; gaterequest()on it instead ofhosting.video.vue: on mouse interaction while not controlling, buffer the triggering event, send the control request, and replay the event oncecontrollingflips true (@Watch('controlling')). Non-implicit behavior (control-attemptemit) is preserved.Verification
npm run buildinimages/chromium-headful/clientpasses.control/requestthis patch adds made the server replycontrol/locked, after which a Ctrl+C in the remote browser produced thecontrol/clipboardpush,navigator.clipboard.writeTextsucceeded, and the local clipboard held the remote selection — end-to-end copy working (details in Copy in the live view never reaches the viewer's clipboard (implicit hosting drops clipboard sync) #310).Note
Medium Risk
Changes remote session control and input routing in the headful client; behavior is localized but affects who receives server clipboard and when mouse events reach the remote.
Overview
Under implicit hosting, the client treated every viewer as “hosting” without ever sending
control/request, so the neko server never registered a host and remote clipboard updates were dropped.This adds a
controllinggetter (registered host only) and gatesremote.request()on it instead ofhosting. Invideo.vue, the first click while not controlling sends a control request, buffers the mouse down/up, and replays them whencontrollingbecomes true; non-implicit mode still emitscontrol-attempton mousedown.Reviewed by Cursor Bugbot for commit 0388493. Bugbot is set up for automated code reviews on this repo. Configure here.