From 038849301103f3e6b9cedba693118dc48c58fc73 Mon Sep 17 00:00:00 2001 From: Brendan Teo <44599374+breteo@users.noreply.github.com> Date: Thu, 9 Jul 2026 18:14:26 -0700 Subject: [PATCH] fix(chromium-headful): sync clipboard to viewers under implicit hosting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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'. --- .../client/src/components/video.vue | 59 +++++++++++++++++-- .../client/src/store/remote.ts | 5 +- 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/images/chromium-headful/client/src/components/video.vue b/images/chromium-headful/client/src/components/video.vue index 2a8a8c8f..463e3250 100644 --- a/images/chromium-headful/client/src/components/video.vue +++ b/images/chromium-headful/client/src/components/video.vue @@ -280,6 +280,10 @@ return this.$accessor.connecting } + get controlling() { + return this.$accessor.remote.controlling + } + get hosting() { return this.$accessor.remote.hosting } @@ -804,14 +808,18 @@ first.target.dispatchEvent(simulatedEvent) } + isMouseDown = false + onMouseDown(e: MouseEvent) { this.unmuteOnInteraction() + this.isMouseDown = true - if (!this.hosting) { - this.$emit('control-attempt', e) + if (this.locked) { + return } - if (!this.hosting || this.locked) { + if (!this.controlling) { + this.implicitHostingRequest(e) return } @@ -820,7 +828,16 @@ } onMouseUp(e: MouseEvent) { - if (!this.hosting || this.locked) { + // only if we are the one who started the mouse down + if (!this.isMouseDown) return + this.isMouseDown = false + + if (this.locked) { + return + } + + if (!this.controlling) { + this.implicitHostingRequest(e) return } @@ -828,6 +845,40 @@ this.$client.sendData('mouseup', { key: e.button + 1 }) } + private reqMouseDown: MouseEvent | null = null + private reqMouseUp: MouseEvent | null = null + + @Watch('controlling') + onControlChange(controlling: boolean) { + if (controlling && this.reqMouseDown) { + this.onMouseDown(this.reqMouseDown) + } + + if (controlling && this.reqMouseUp) { + this.onMouseUp(this.reqMouseUp) + } + + this.reqMouseDown = null + this.reqMouseUp = null + } + + implicitHostingRequest(e: MouseEvent) { + if (this.implicitHosting) { + if (e.type === 'mousedown') { + this.reqMouseDown = e + this.reqMouseUp = null + this.$accessor.remote.request() + } else if (e.type === 'mouseup') { + this.reqMouseUp = e + } + return + } + + if (e.type === 'mousedown') { + this.$emit('control-attempt', e) + } + } + onMouseMove(e: MouseEvent) { if (!this.hosting || this.locked) { return diff --git a/images/chromium-headful/client/src/store/remote.ts b/images/chromium-headful/client/src/store/remote.ts index c8e9d282..5cd40cd0 100644 --- a/images/chromium-headful/client/src/store/remote.ts +++ b/images/chromium-headful/client/src/store/remote.ts @@ -18,6 +18,9 @@ export const state = () => ({ }) export const getters = getterTree(state, { + controlling: (state, getters, root) => { + return root.user.id === state.id + }, hosting: (state, getters, root) => { return root.user.id === state.id || state.implicitHosting }, @@ -89,7 +92,7 @@ export const actions = actionTree( }, request({ getters }) { - if (!accessor.connected || getters.hosting) { + if (!accessor.connected || getters.controlling) { return }