Return the keyboard on Remote shortcuts and announce focus changes

During remote focus a Remote shortcut now ends capture, like F11,
instead of running its command, which would race with the focus change.
The reader announces when the keyboard starts and stops controlling the
remote computer, and the consent wording names the F11 escape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chase CichorzandClaude Opus 5.5 committed 2026-11-06 16:06:38 -05:00
1 parent c234cb82a8
commit 477d860fb8
5 files changed
+43 -25

No files matched your search

+3
View File
@@ -105,8 +105,10 @@ func (runtime *tentacleRuntime) start_remote_capture(ctx context.Context, sessio
id, stream, err := capture.BeginRemoteCapture(request.Context, prefix)
if err != nil {
_ = session.PublishRemoteFocus(request.Lease, tentaclewire.RemoteFocusLocal, "unavailable")
runtime.report("Remote keyboard control is unavailable.")
continue
}
runtime.report("Keyboard controls the remote computer. Press F11 to return.")
active, lease, events, leaseDone = id, request.Lease, stream, request.Context.Done()
if session.PublishRemoteFocus(lease, tentaclewire.RemoteFocusRemote, "requested") != nil {
end()
@@ -118,6 +120,7 @@ func (runtime *tentacleRuntime) start_remote_capture(ctx context.Context, sessio
}
if event.Ended != "" {
_ = session.PublishRemoteFocus(lease, tentaclewire.RemoteFocusLocal, event.Ended)
runtime.report("Keyboard returned to this computer.")
continue
}
key := tentaclewire.RemoteCapturedKey{Code: int(event.Key.Code), Name: event.Key.Name, Pressed: event.Key.Pressed, Repeat: event.Key.Repeat}
+1 -1
View File
@@ -118,7 +118,7 @@ func remote_consent_form(id, role string, grants []string) interaction.Form {
labels := map[string]string{
"output-observe": "Share local reader output with the remote controller",
"remote-present": "Present output from the controlled computer",
"keyboard-capture": "Forward local keyboard input to the controlled computer",
"keyboard-capture": "Forward local keyboard input to the controlled computer while remote focus is on. Press F11 to return to this computer",
"keyboard-inject": "Allow the remote controller to type on this computer",
"clipboard-exchange": "Allow receiving clipboard text; send only on explicit local request",
"braille-stream": "Exchange remote braille cells and supported display gestures",
+10 -11
View File
@@ -60,8 +60,8 @@ func (listener *CommandListener) RemoteCaptureActive() bool {
// BeginRemoteCapture takes an exclusive keyboard grab and returns the session
// and its event stream. Only the core escape (F11) and commands whose IDs start
// with localPrefix stay local; everything else is forwarded, including Cthulhu
// modifier chords. It waits until no key is held. It grants no consent: the
// with localPrefix stay local: either returns the keyboard to this computer.
// Everything else is forwarded, including Cthulhu modifier chords. It waits until no key is held. It grants no consent: the
// caller must hold a live keyboard-capture lease and end capture on revocation.
func (listener *CommandListener) BeginRemoteCapture(ctx context.Context, localPrefix string) (uint64, <-chan input.RemoteCaptureEvent, error) {
if !strings.HasPrefix(localPrefix, "tentacle/") || !strings.HasSuffix(localPrefix, "/") {
@@ -174,7 +174,7 @@ func (owner *commandOwner) end_remote_capture(reason string) {
}
capture.ending = true
for _, transition := range capture.normalizer.Retire() {
owner.route_remote(input.RemoteCaptureKey{Keycode: transition.Keycode}, 0)
owner.route_remote(input.RemoteCaptureKey{Keycode: transition.Keycode})
}
for _, transition := range capture.router.Retire().Remote {
owner.send_remote(input.RemoteCaptureEvent{Key: &input.RemoteCaptureTransition{Code: transition.Keycode}})
@@ -208,7 +208,7 @@ func (owner *commandOwner) send_remote(event input.RemoteCaptureEvent) bool {
func (owner *commandOwner) flush_remote(now time.Time) {
for _, transition := range owner.remote.normalizer.Flush(now) {
owner.route_remote(input.RemoteCaptureKey{Keycode: transition.Keycode}, 0)
owner.route_remote(input.RemoteCaptureKey{Keycode: transition.Keycode})
}
}
@@ -231,14 +231,14 @@ func (owner *commandOwner) handle_remote_key(key C.CommandKey) {
capture.names[transition.Keycode] = name
routed.Unsupported = name == ""
}
owner.route_remote(routed, uint32(key.timestamp))
owner.route_remote(routed)
}
if err != nil && owner.remote != nil {
owner.end_remote_capture("local")
}
}
func (owner *commandOwner) route_remote(key input.RemoteCaptureKey, timestamp uint32) {
func (owner *commandOwner) route_remote(key input.RemoteCaptureKey) {
capture := owner.remote
if capture == nil {
return
@@ -264,10 +264,9 @@ func (owner *commandOwner) route_remote(key input.RemoteCaptureKey, timestamp ui
owner.end_remote_capture("escape")
return
}
if result.Local != "" && owner.workerContext.Err() == nil {
chord := input.CommandChord{Symbol: key.Symbol}
if !owner.listener.enqueueSecureCommand(input.CommandEvent{Command: result.Local, Timestamp: timestamp, Chord: chord}) {
owner.end_remote_capture("local")
}
// A Remote shortcut returns the keyboard here instead of running: running it
// would race with the focus change. Press it again under local focus.
if result.Local != "" {
owner.end_remote_capture("requested")
}
}
@@ -113,23 +113,37 @@ func TestNativeRemoteCaptureForwardsBypassesAndEscapes(t *testing.T) {
expect("t", true, true)
expect("", false, false)
expect("", false, false)
// Remote's own shortcut stays local and withdraws the forwarded prefix.
// Unsupported keys are consumed locally, not forwarded.
run("key", "XF86AudioMute")
// Remote's own shortcut stays local: it withdraws the forwarded prefix and
// returns the keyboard here without running the command.
run("key", "Insert+ctrl+r")
insert := expect("Insert", true, true)
control := expect("Control_L", true, true)
if expect("", false, false) != control || expect("", false, false) != insert {
t.Fatal("prefix not withdrawn newest first")
}
if event := next(); event.Ended != "requested" {
t.Fatalf("shortcut end event = %+v", event)
}
select {
case event := <-listener.Commands():
if event.Command != toggle {
t.Fatalf("local command = %s", event.Command)
t.Fatalf("shortcut ran %s during remote focus", event.Command)
default:
}
if _, ok := <-events; ok {
t.Fatal("capture stream stayed open after shortcut")
}
for listener.RemoteCaptureActive() {
if ctx.Err() != nil {
t.Fatal("shortcut did not end capture")
}
case <-ctx.Done():
t.Fatal("local Remote shortcut did not dispatch")
time.Sleep(time.Millisecond)
}
session, events, err = listener.BeginRemoteCapture(ctx, "tentacle/remote-1/")
if err != nil {
t.Fatal(err)
}
// Unsupported keys are consumed locally, not forwarded.
run("key", "XF86AudioMute")
run("key", "F11")
if event := next(); event.Ended != "escape" {
t.Fatalf("escape event = %+v", event)