Skip to content

Fix Live View clipboard sync under implicit hosting (#310)#318

Merged
rgarcia merged 2 commits into
mainfrom
fix/live-view-clipboard-host
Jul 17, 2026
Merged

Fix Live View clipboard sync under implicit hosting (#310)#318
rgarcia merged 2 commits into
mainfrom
fix/live-view-clipboard-host

Conversation

@rgarcia

@rgarcia rgarcia commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

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/requestcontrol/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


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.

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

Copying inside the remote browser never updated the viewer's local
clipboard in an embedded Live View. Under neko's implicit_hosting, the
client treated the viewer as a host (`remote.hosting` true) but never
registered one server-side, so `remote.request()` short-circuited and no
`control/request` was ever sent. neko delivers clipboard updates only to
the registered host (`sessions.GetHost()`), so with no host they were
dropped.

- store/remote.ts: add a `controlling` getter that is true only when the
  viewer is the actual registered host, and guard `request()` on it
  instead of the broad `hosting`. The `id !== ''` check avoids treating
  the pre-connection empty-id state 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 (the pinned neko server accepts input via
  LegacyIsHost() under implicit hosting, so no buffer/replay is needed).
- neko/index.ts: stop surfacing the "you took the controls" toast to the
  viewer. Control is now registered automatically on first interaction,
  so it would otherwise fire on essentially every session's first click.

Co-Authored-By: Claude <noreply@anthropic.com>
@rgarcia
rgarcia requested a review from masnwilliams July 17, 2026 19:36

@masnwilliams masnwilliams left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

reviewed — approve. clean, well-scoped fix that correctly isolates the root cause (client-side control registration under implicit hosting, not clipboard perms). the controlling getter + re-gating request() on it is the right mechanism, the empty-id guard is a nice touch, and the first-click path forwards the click immediately with no buffering. all notes below are minor — nothing blocking.

Nits / minor

  • components/video.vue:814 — no in-flight guard on request(). controlling only flips after the server's control/locked round-trips, so rapid clicks before that response can send multiple control/request frames. benign (server re-locks to the same host id, idempotent), but the "second click sends no duplicate" property only holds once the response has landed. a local "request sent" flag would make it strictly one.
  • neko/index.ts:242 — the comment says notifications for other members are left intact, but the removed $notify was self-only (this.id === id); the intact path for others is the chat event message, not a toast. wording nit.
  • neko/index.ts:233 — suppressing the self-toast also drops the confirmation for explicit control-takes in the full (non-embedded) neko UI. intended for the embed use case, just noting the shared-component side effect.

Question (non-blocking)

  • store/remote.ts:82toggle() still gates on hosting, so under implicit hosting it only ever sends RELEASE; any UI still wired to it (e.g. controls.vue:311) can't acquire control. pre-existing, not introduced here — flagging since you're in the area.

- remote.ts: add a `requesting` flag so rapid clicks before control/locked
  round-trips can't emit multiple control/request frames. Cleared on
  setHost (host resolved) and reset (disconnect/release).
- neko/index.ts: clarify the toast-suppression comment — the removed
  $notify was self-only; the surviving other-member path is the chat
  event, and the suppression is a shared-component side effect intended
  for the embedded live view.

Co-Authored-By: Claude <noreply@anthropic.com>
@rgarcia

rgarcia commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @masnwilliams! Addressed the two actionable notes in c15d5d5:

  • request() in-flight guard — added a requesting flag in store/remote.ts, set when the request is sent and cleared on setHost (host resolved) / reset (disconnect/release). Now strictly one control/request regardless of how fast you click. Verified: 5 rapid clicks before control/locked lands → exactly 1 request frame sent, and the clipboard round-trip still works.
  • Comment wording (neko/index.ts) — corrected: the removed $notify was self-only (this.id === id); the surviving other-member path is the chat event, not a toast. Also called out that this is a shared-component side effect (explicit control-takes in the full neko UI lose the confirmation too), which is fine since Kernel only ships the embedded live view.

On the non-blocking toggle() question: agreed it's pre-existing — under implicit hosting toggle() gates on hosting so it only ever sends RELEASE, leaving any UI wired to it (e.g. controls.vue) unable to acquire control. I left it out of this PR to keep scope tight to the clipboard fix, but happy to follow up with a separate change re-gating toggle() on controlling if you'd like.

@rgarcia
rgarcia merged commit 452f342 into main Jul 17, 2026
10 of 11 checks passed
@rgarcia
rgarcia deleted the fix/live-view-clipboard-host branch July 17, 2026 20:32

@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 1 potential issue.

Fix All in Cursor

Bugbot Autofix is ON, but it could not run because the branch was deleted or merged before autofix could start.

Reviewed by Cursor Bugbot for commit c15d5d5. Configure here.

return
}

accessor.remote.setRequesting(true)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stuck requesting blocks host registration

Medium Severity

The request action sets requesting to true before confirming the message can be sent. If $client.connected is false (e.g., during network reconnects), sendMessage no-ops, leaving requesting latched true. This incorrectly blocks subsequent control requests, breaking host registration and clipboard sync until a full reset.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c15d5d5. Configure here.

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.

Copy in the live view never reaches the viewer's clipboard (implicit hosting drops clipboard sync)

2 participants