Skip to content

fix(chromium-headful): sync clipboard to viewers under implicit hosting#311

Closed
breteo wants to merge 1 commit into
kernel:mainfrom
breteo:fix/implicit-hosting-clipboard-sync
Closed

fix(chromium-headful): sync clipboard to viewers under implicit hosting#311
breteo wants to merge 1 commit into
kernel:mainfrom
breteo:fix/implicit-hosting-clipboard-sync

Conversation

@breteo

@breteo breteo commented Jul 10, 2026

Copy link
Copy Markdown

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.ts request() bails because hosting is 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-host controlling getter; gate request() on it instead of hosting.
  • video.vue: on mouse interaction while not controlling, buffer the triggering event, send the control request, and replay the event once controlling flips true (@Watch('controlling')). Non-implicit behavior (control-attempt emit) is preserved.

Verification

  • npm run build in images/chromium-headful/client passes.
  • Protocol change validated against a live production Kernel session: manually issuing the exact control/request this patch adds made the server reply control/locked, after which a Ctrl+C in the remote browser produced the control/clipboard push, navigator.clipboard.writeText succeeded, 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 controlling getter (registered host only) and gates remote.request() on it instead of hosting. In video.vue, the first click while not controlling sends a control request, buffers the mouse down/up, and replays them when controlling becomes true; non-implicit mode still emits control-attempt on mousedown.

Reviewed by Cursor Bugbot for commit 0388493. Bugbot is set up for automated code reviews on this repo. Configure here.

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'.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ 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
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0388493. Configure here.

}

if (controlling && this.reqMouseUp) {
this.onMouseUp(this.reqMouseUp)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0388493. Configure here.

rgarcia added a commit that referenced this pull request Jul 17, 2026
## 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>
@rgarcia

rgarcia commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

@breteo thanks so much for this and the bug report! I just merged #318 which should address this. This will likely go out to actual Kernel production on Monday

@rgarcia rgarcia closed this Jul 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants