hands: arm the wire before opening the pad
This commit is contained in:
parent
799f0650ed
commit
59b52a818b
7 changed files with 210 additions and 148 deletions
|
|
@ -23,27 +23,31 @@
|
|||
* she is still on screen while you work. Casey, 2026-08-15: *"I want her, and
|
||||
* then below her to have like a trackpad zone / keyboard zone stuff."*
|
||||
*
|
||||
* At 760/226 the split was the other way round and the controls had 226px to
|
||||
* share between a status row, an agent field, the pads and four buttons — so
|
||||
* the pads were 92px and the last row sat under the dock.
|
||||
*
|
||||
* `--dock-reserve` is a number here because the dock claims **no exclusive
|
||||
* zone** unless it is Pinned (`modules/ii/dock/Dock.qml:321`), so nothing
|
||||
* shrinks this surface for it and the bottom row would be untappable. Delete
|
||||
* this the day the dock yields on its own.
|
||||
* At 760/226 the controls had only 226px. At 320/the-rest the trackpad ate
|
||||
* nearly half the glass. This split keeps her proportions and gives the pad
|
||||
* one deliberate working area. The dock now yields only while this
|
||||
* conditional surface is open; the remaining reserve belongs to the system
|
||||
* gesture rail, not the dock.
|
||||
*/
|
||||
:root { --dock-reserve: 84px; }
|
||||
body.hid #live2d-widget { height: 320px; }
|
||||
:root { --gesture-rail-reserve: 32px; }
|
||||
body.hid #live2d-widget { height: 520px; }
|
||||
/*
|
||||
* Keep her proportions when the box shrinks. The canvas fills its element and
|
||||
* the rig has no letterboxing of its own, so a short full-width box draws the
|
||||
* same model squashed. The ratio is the full-height box's, 540:760.
|
||||
*/
|
||||
body.hid #live2d {
|
||||
display: block; height: 100%; width: auto;
|
||||
aspect-ratio: 540 / 760; margin: 0 auto;
|
||||
}
|
||||
body.hid #hands {
|
||||
top: 326px;
|
||||
top: 526px;
|
||||
height: auto;
|
||||
bottom: var(--dock-reserve);
|
||||
bottom: var(--gesture-rail-reserve);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
/* The pads take whatever the fixed rows leave, rather than a written-down 92. */
|
||||
body.hid #track-row,
|
||||
body.hid #hands.keyboard #keyboard-pane { flex: 1; height: auto; min-height: 92px; }
|
||||
body.hid #track-row { flex: 1; height: auto; min-height: 180px; }
|
||||
#live2d { width: 100%; height: 100%; }
|
||||
#bubble {
|
||||
position: fixed; left: 8px; right: 8px; top: 12px;
|
||||
|
|
@ -56,8 +60,7 @@
|
|||
#bubble.up { opacity: 1; }
|
||||
|
||||
/*
|
||||
* The room viewtop left under her, given a purpose. Trackpad first; the
|
||||
* literal keyboard only replaces it when summoned. The dark sheer gradient
|
||||
* The room viewtop left under her, given a purpose. The dark sheer gradient
|
||||
* keeps the controls legible without putting an app-shaped slab around her.
|
||||
*/
|
||||
#hands {
|
||||
|
|
@ -100,20 +103,18 @@
|
|||
#hands-listening { display: none; color: #e8b7d8; }
|
||||
#hands.listening #hands-listening { display: inline; }
|
||||
#hands-status { flex: 1; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: rgba(255,255,255,.68); }
|
||||
.thread-button { width: 30px; min-width: 30px !important; height: 26px !important; padding: 0 !important; border-radius: 10px !important; }
|
||||
|
||||
#agent-form, #host-form { height: 40px; display: flex; gap: 6px; }
|
||||
#agent-input, #host-input {
|
||||
#agent-form { height: 40px; display: flex; gap: 6px; }
|
||||
#agent-input {
|
||||
min-width: 0; flex: 1; height: 40px; padding: 0 13px;
|
||||
border: 1px solid rgba(255,255,255,.12); border-radius: 14px;
|
||||
outline: none; color: #fff; background: rgba(0,0,0,.20);
|
||||
-webkit-user-select: text; user-select: text;
|
||||
}
|
||||
#agent-input:focus, #host-input:focus { border-color: rgba(217,180,235,.55); }
|
||||
#agent-input::placeholder, #host-input::placeholder { color: rgba(255,255,255,.43); }
|
||||
#agent-input:focus { border-color: rgba(217,180,235,.55); }
|
||||
#agent-input::placeholder { color: rgba(255,255,255,.43); }
|
||||
#mic.listening { color: #f0b6db; box-shadow: inset 0 0 0 1px rgba(240,182,219,.45); }
|
||||
|
||||
#track-row, #keyboard-pane { height: 92px; margin-top: 6px; }
|
||||
#track-row { height: 92px; margin-top: 6px; }
|
||||
#track-row { display: flex; gap: 6px; }
|
||||
#trackpad, #scrollpad {
|
||||
position: relative; overflow: hidden;
|
||||
|
|
@ -133,12 +134,6 @@
|
|||
content: '↕'; position: absolute; inset: 0; display: grid; place-items: center;
|
||||
color: rgba(255,255,255,.35); font-size: 20px;
|
||||
}
|
||||
#keyboard-pane { display: none; }
|
||||
#keyboard-pane .quick-keys { display: flex; gap: 5px; margin-top: 6px; }
|
||||
#keyboard-pane .quick-keys button { flex: 1; height: 40px; padding: 0 5px; }
|
||||
#hands.keyboard #track-row { display: none; }
|
||||
#hands.keyboard #keyboard-pane { display: block; }
|
||||
|
||||
.hands-actions { height: 40px; display: flex; gap: 5px; margin-top: 6px; }
|
||||
.hands-actions button { flex: 1; height: 40px !important; padding: 0 5px !important; }
|
||||
#cad.holding {
|
||||
|
|
@ -155,8 +150,6 @@
|
|||
<span id="hands-dot"></span>
|
||||
<span id="hands-status">USB hand asleep</span>
|
||||
<span id="hands-listening">listening</span>
|
||||
<button class="thread-button" id="resume-thread" title="Resume latest conversation">↶</button>
|
||||
<button class="thread-button" id="new-thread" title="Begin a new conversation">+</button>
|
||||
</div>
|
||||
<form id="agent-form">
|
||||
<button type="button" id="mic" aria-label="Start or stop listening">●</button>
|
||||
|
|
@ -168,24 +161,9 @@
|
|||
<div id="trackpad" aria-label="Trackpad"></div>
|
||||
<div id="scrollpad" aria-label="Scroll"></div>
|
||||
</div>
|
||||
<div id="keyboard-pane">
|
||||
<form id="host-form">
|
||||
<input id="host-input" autocomplete="off" enterkeyhint="send"
|
||||
placeholder="Type into the attached host…">
|
||||
<button type="submit" aria-label="Type on host">↑</button>
|
||||
</form>
|
||||
<div class="quick-keys">
|
||||
<button data-key="escape">Esc</button>
|
||||
<button data-key="tab">Tab</button>
|
||||
<button data-key="backspace">⌫</button>
|
||||
<button data-key="enter">Enter</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hands-actions">
|
||||
<button id="left-click">Left</button>
|
||||
<button id="right-click">Right</button>
|
||||
<button id="keyboard-toggle">⌨</button>
|
||||
<button id="arm-hid">Arm</button>
|
||||
<button id="cad" title="Hold for Ctrl+Alt+Delete">CAD</button>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -363,7 +341,7 @@
|
|||
window.souveraine.hid(message);
|
||||
}
|
||||
|
||||
var agentPending = false, hostPending = false;
|
||||
var agentPending = false;
|
||||
window.hands = {
|
||||
state: function (state) {
|
||||
state = state || {};
|
||||
|
|
@ -386,8 +364,6 @@
|
|||
: 'Tell ' + (state.agent || 'your agent') + ' what the other screen needs…';
|
||||
hands.classList.toggle('thinking', !!state.thinking);
|
||||
document.getElementById('agent-input').disabled = !!state.thinking || agentPending;
|
||||
document.getElementById('resume-thread').classList.toggle('on', state.conversation === 'resumed');
|
||||
document.getElementById('new-thread').classList.toggle('on', state.conversation === 'new');
|
||||
requestAnimationFrame(publishShape);
|
||||
},
|
||||
agentResult: function (result) {
|
||||
|
|
@ -397,14 +373,6 @@
|
|||
input.disabled = hands.classList.contains('thinking');
|
||||
if (result.accepted) input.value = '';
|
||||
else if (result.reason) document.getElementById('hands-status').textContent = result.reason;
|
||||
},
|
||||
hostResult: function (result) {
|
||||
result = result || {};
|
||||
hostPending = false;
|
||||
var input = document.getElementById('host-input');
|
||||
input.disabled = false;
|
||||
if (result.accepted) input.value = '';
|
||||
else if (result.reason) document.getElementById('hands-status').textContent = result.reason;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -422,13 +390,6 @@
|
|||
if (!window.souveraine) return;
|
||||
window.souveraine.talk(hands.classList.contains('listening') ? 'end' : 'start');
|
||||
});
|
||||
document.getElementById('resume-thread').addEventListener('click', function () {
|
||||
if (window.souveraine) window.souveraine.thread('resume');
|
||||
});
|
||||
document.getElementById('new-thread').addEventListener('click', function () {
|
||||
if (window.souveraine) window.souveraine.thread('new');
|
||||
});
|
||||
|
||||
var trackpad = document.getElementById('trackpad');
|
||||
var trackDown = false, trackX = 0, trackY = 0, trackTravel = 0, trackAt = 0;
|
||||
trackpad.addEventListener('pointerdown', function (event) {
|
||||
|
|
@ -478,28 +439,6 @@
|
|||
document.getElementById('right-click').addEventListener('click', function () {
|
||||
hid('click', { button: 'right' });
|
||||
});
|
||||
document.getElementById('arm-hid').addEventListener('click', function () { hid('arm'); });
|
||||
|
||||
var keyboardToggle = document.getElementById('keyboard-toggle');
|
||||
keyboardToggle.addEventListener('click', function () {
|
||||
var showing = !hands.classList.contains('keyboard');
|
||||
hands.classList.toggle('keyboard', showing);
|
||||
keyboardToggle.classList.toggle('on', showing);
|
||||
var input = document.getElementById('host-input');
|
||||
if (showing) input.focus(); else input.blur();
|
||||
});
|
||||
document.getElementById('host-form').addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
var input = document.getElementById('host-input');
|
||||
if (input.value.length === 0 || hostPending) return;
|
||||
hostPending = true;
|
||||
input.disabled = true;
|
||||
hid('type', { text: input.value });
|
||||
});
|
||||
Array.prototype.forEach.call(document.querySelectorAll('[data-key]'), function (button) {
|
||||
button.addEventListener('click', function () { hid('key', { key: button.dataset.key }); });
|
||||
});
|
||||
|
||||
// Ctrl+Alt+Delete is the one quick key a stray tap must not send. Its hold
|
||||
// is local feedback; only the completed edge crosses to the HID owner.
|
||||
var cad = document.getElementById('cad'), cadTimer = null, cadPointer = null;
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ How Souveraine is built, and why. For whoever reads it, human or agent. When thi
|
|||
**surface**: how the body she lives in moves
|
||||
- [motion](surface/01-motion.md). One clock, two strategies, and why the shell never transforms a window.
|
||||
- [session start](surface/02-session-start.md). One package-owned dependency graph from compositor socket to authority to shell.
|
||||
- [USB Hands](surface/03-usb-hands.md). An armed wire, an optional trackpad, and one acknowledged hand into the attached host.
|
||||
|
||||
Coming: the rest of identity (the memory filesystem, the ledgers), the nervous system (schedules, the event log), the consciousness cycle, the runtime, federation, a glossary.
|
||||
|
||||
|
|
|
|||
122
saf/surface/03-usb-hands.md
Normal file
122
saf/surface/03-usb-hands.md
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
# USB Hands
|
||||
|
||||
The phone may become a keyboard and pointer for the host on its USB cable.
|
||||
That reach has three states with three owners; collapsing them is what made the
|
||||
first surface confusing.
|
||||
|
||||
## The wire, the surface, the hand
|
||||
|
||||
`UsbState.mode` says what sessiond and usb-signaller have made the port. HID or
|
||||
KVM means the wire is armed. Developer or charging-only means it is not.
|
||||
QuickShell never writes configfs and the page never opens `/dev/hidg*`.
|
||||
|
||||
`HidController.active` says the trackpad surface is open. It is not another
|
||||
name for an armed port. Power Options is the human place that arms or disarms
|
||||
the wire; a separate child action opens or closes the trackpad, and opening is
|
||||
refused until HID or KVM is already armed.
|
||||
|
||||
`HidController.ready` says its one persistent `usb-hid-inject stream` helper
|
||||
has acknowledged the endpoint. Pointer motion is batched to the display clock;
|
||||
text, keys, clicks, and release reports go through that one writer.
|
||||
|
||||
These are deliberately different facts. A composed gadget with no open
|
||||
surface is safe and useful. An open surface with no composed gadget is a dead
|
||||
control and is refused. A helper process that has not said `ready` is not
|
||||
dressed up as a usable hand.
|
||||
|
||||
The resulting state graph is small:
|
||||
|
||||
```
|
||||
developer/charging -> no trackpad
|
||||
HID or KVM armed -> trackpad may be opened
|
||||
trackpad active -> dock yields; Face routes gestures and words
|
||||
disarm or lock -> helper releases, trackpad closes
|
||||
```
|
||||
|
||||
Arming the wire alone changes no shell furniture. The dock leaves only while
|
||||
the conditional trackpad is actually on the glass.
|
||||
|
||||
## Authority and mechanism
|
||||
|
||||
The path has one owner at each layer:
|
||||
|
||||
- QuickShell asks sessiond for `set_usb_mode`; `UsbState` only projects the
|
||||
answer and never writes the gadget.
|
||||
- Sessiond records the USB mode action and asks usb-signaller to compose the
|
||||
port. The supported postures are developer, HID, KVM, and charging-only.
|
||||
- `HidController` alone owns the live stream helper. The Face page emits
|
||||
intent into that controller; it never opens `/dev/hidg*` itself.
|
||||
- The substrate's `body hand` verb returns to QuickShell's `usbHands` IPC. It
|
||||
does not create a second HID writer merely because the caller is an agent.
|
||||
|
||||
The present route is not yet the final authority seam. `attached_identity`
|
||||
and `probe_owner` are explicit unknowns, and a locally permitted caller can
|
||||
still reach usb-signaller's system D-Bus mechanism around sessiond. Joining an
|
||||
unlocked surface and arming it in Power Options are useful local gates, not a
|
||||
credential or known-peer policy. The seam is complete only when the mutation
|
||||
mechanism admits the attested/sessiond path alone and sessiond can distinguish
|
||||
a known scoped peer from an unknown attachment. Charging-only remains the
|
||||
always-available safe contraction.
|
||||
|
||||
Mode failure must return a refusal and leave or restore a usable USB posture.
|
||||
It must not strand the management lane, a bound UDC, a held key, or a pointer
|
||||
button. Closing Hands, disarming, and secure lock all release reports and stop
|
||||
the helper; a mode switch stops the writer before usb-signaller changes the
|
||||
endpoints beneath it.
|
||||
|
||||
## One conversation
|
||||
|
||||
Hands is a limb of the existing Face/Souveraine connection, not a compact
|
||||
second chat client. Opening it resumes the current agent's latest conversation
|
||||
only when no conversation is attached. Conversation selection and creation
|
||||
belong to the agent pane, so the Hands header carries no mystery back-arrow or
|
||||
new-thread button.
|
||||
|
||||
The joined surface currently exposes agent text/voice, trackpad, scroll,
|
||||
left/right click, and a held Ctrl+Alt+Delete. It does not advertise a keyboard
|
||||
pane merely because an HTML input can emit key reports. A real on-glass
|
||||
keyboard needs its own QML surface and focus contract before it earns a button.
|
||||
|
||||
## The agent's reach
|
||||
|
||||
While Hands is joined, the ambient skill points at the substrate's typed
|
||||
`body` tool: `verb: hand` with `status`, `type`, `key`, `click`, or `pointer`.
|
||||
It does not teach a second raw `qs` command vocabulary.
|
||||
|
||||
A direct request to type ordinary user-authored text is authorization to type
|
||||
that text, including creative, intimate, or unusual prose. Odd tone is not a
|
||||
state refusal. The hand still sends nothing Casey did not ask to place on the
|
||||
attached host, and readiness/state refusals remain visible to the agent.
|
||||
|
||||
## Beyond HID
|
||||
|
||||
HID can send input and cannot truthfully return a terminal's output. Learned
|
||||
administration or tunnel payloads therefore belong to a separate, auditable
|
||||
capability path: named/versioned recipes, an explicit scope and lease, and a
|
||||
management transport that returns stdout, stderr, and exit status as typed
|
||||
events. Do not infer success from keystrokes and do not turn an RMM or reverse
|
||||
tunnel into an opaque keyboard macro.
|
||||
|
||||
The NCM control lane can carry that future return path. Its authority,
|
||||
identity, rollback, and evidence contract must exist before a learned payload
|
||||
is treated as a skill rather than an unaudited script.
|
||||
|
||||
## Proof boundary
|
||||
|
||||
Source agreement is only the first line of proof. USB Hands is operational on
|
||||
a phone only when the signed usb-signaller package contains the persistent
|
||||
stream helper, the installed Souveraine surface and face assets are the same
|
||||
revision, and the following motion is observed on glass and on the attached
|
||||
host:
|
||||
|
||||
1. Power Options arms HID while another management path remains reachable.
|
||||
2. Opening the trackpad starts one helper and receives its `ready` reply.
|
||||
3. Motion, scroll, left/right click, requested text, a named key, and held
|
||||
Ctrl+Alt+Delete reach the host without a second writer.
|
||||
4. A rejected character sends none of that command; leaving and locking leave
|
||||
no held report.
|
||||
5. Returning to developer mode restores the control lane cleanly.
|
||||
|
||||
KVM adds separate display, storage, NCM, and touch-return acceptance. A green
|
||||
shell reload, a packaged binary, or the first successful keystroke proves only
|
||||
its own layer; none of them silently stands in for that cable exercise.
|
||||
|
|
@ -88,6 +88,12 @@ Scope { // Scope
|
|||
|| GlobalStates.missionPeek)
|
||||
return Dock.DockState.Hidden;
|
||||
|
||||
// USB Hands owns the bottom of home while its trackpad is actually
|
||||
// open. Arming the wire alone changes no furniture; opening the
|
||||
// conditional controller makes the dock yield until it closes.
|
||||
if (HidController.active)
|
||||
return Dock.DockState.Hidden;
|
||||
|
||||
// A visible keyboard owns the bottom edge. This must precede the home
|
||||
// zone's unconditional PINNED return below; otherwise home stacks the
|
||||
// dock's exclusive zone under the OSK and charges the display twice.
|
||||
|
|
|
|||
|
|
@ -290,12 +290,16 @@ Scope {
|
|||
SheetLocal.PowerOptionRow {
|
||||
Layout.fillWidth: true
|
||||
icon: "keyboard"
|
||||
title: "USB HID Injection"
|
||||
detail: UsbState.mode === "hid"
|
||||
? "Keyboard and pointer link armed"
|
||||
: "Let this screen become the other machine's hands"
|
||||
badge: UsbState.mode === "hid" ? "active"
|
||||
: UsbState.hasMode("hid_mode") ? "wired" : "building"
|
||||
title: "USB hand"
|
||||
detail: HidController.active
|
||||
? "Wire armed · trackpad open"
|
||||
: HidController.modeReady
|
||||
? "Keyboard and pointer wire armed"
|
||||
: "Arm the wire, then open its trackpad"
|
||||
badge: UsbState.busy ? "switching"
|
||||
: HidController.active ? "open"
|
||||
: HidController.modeReady ? "armed"
|
||||
: UsbState.hasMode("hid_mode") ? "ready" : "building"
|
||||
inset: 10
|
||||
expandable: true
|
||||
expanded: scope.hidOptionsOpen
|
||||
|
|
@ -316,35 +320,41 @@ Scope {
|
|||
SheetLocal.PowerOptionRow {
|
||||
Layout.fillWidth: true
|
||||
icon: "cable"
|
||||
title: "USB control link"
|
||||
detail: UsbState.mode === "hid"
|
||||
? "Armed · tap to restore developer USB"
|
||||
title: HidController.modeReady
|
||||
? "Disarm USB hand" : "Arm USB hand"
|
||||
detail: HidController.modeReady
|
||||
? "Restore developer USB and close its controls"
|
||||
: "Expose keyboard, pointer and developer network"
|
||||
badge: UsbState.busy ? "switching"
|
||||
: UsbState.mode === "hid" ? "active"
|
||||
: HidController.modeReady ? "armed"
|
||||
: UsbState.hasMode("hid_mode") ? "ready" : "not installed"
|
||||
inset: 28
|
||||
available: UsbState.hasMode("hid_mode") && !UsbState.busy
|
||||
onTapped: UsbState.setMode(
|
||||
UsbState.mode === "hid" ? "developer" : "hid")
|
||||
HidController.modeReady ? "developer" : "hid")
|
||||
}
|
||||
|
||||
SheetLocal.PowerOptionRow {
|
||||
Layout.fillWidth: true
|
||||
icon: "keyboard_mouse"
|
||||
title: "Agent hands"
|
||||
title: HidController.active
|
||||
? "Close trackpad" : "Open trackpad"
|
||||
detail: HidController.active
|
||||
? "Voice, trackpad and the summoned host keyboard"
|
||||
: "Join the agent to the wire; trackpad first"
|
||||
? "Leave the wire armed; put its controls away"
|
||||
: "Voice, pointer, scroll and clicks"
|
||||
badge: HidController.ready ? "awake"
|
||||
: HidController.active ? "arm"
|
||||
: GlobalStates.screenLockSecure ? "locked" : "open"
|
||||
: HidController.active ? "waking"
|
||||
: GlobalStates.screenLockSecure ? "locked" : "closed"
|
||||
inset: 28
|
||||
available: UsbState.hasMode("hid_mode")
|
||||
available: HidController.modeReady && !UsbState.busy
|
||||
&& !GlobalStates.screenLockSecure
|
||||
onTapped: {
|
||||
if (Face.joinHands())
|
||||
if (HidController.active) {
|
||||
Face.leaveHands();
|
||||
scope.dismiss();
|
||||
} else if (Face.joinHands()) {
|
||||
scope.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@
|
|||
// does by construction.
|
||||
//
|
||||
// USB Hands joins the same limb rather than opening a controller app beside
|
||||
// her. Its agent field, explicit microphone, trackpad and summoned host
|
||||
// keyboard occupy the room the compositor already left below her. The HID
|
||||
// reports still belong to HidController; this service only routes page intent.
|
||||
// her. Its agent field, explicit microphone and trackpad occupy the room the
|
||||
// compositor already left below her. The HID reports still belong to
|
||||
// HidController; this service only routes page intent.
|
||||
//
|
||||
// ## Turning her on is joining
|
||||
//
|
||||
|
|
@ -86,6 +86,12 @@ Singleton {
|
|||
return true;
|
||||
}
|
||||
|
||||
function leaveHands() {
|
||||
if (HidController.active)
|
||||
HidController.close();
|
||||
root._syncHands();
|
||||
}
|
||||
|
||||
function leave() {
|
||||
if (root.listening)
|
||||
root._talk("cancel");
|
||||
|
|
@ -153,8 +159,6 @@ Singleton {
|
|||
root._talk(msg.phase);
|
||||
else if (msg.event === "hid")
|
||||
root._hid(msg);
|
||||
else if (msg.event === "thread")
|
||||
root._thread(msg.mode);
|
||||
else if (msg.event === "ready")
|
||||
root._syncHands();
|
||||
else if (msg.event === "dismiss")
|
||||
|
|
@ -193,20 +197,6 @@ Singleton {
|
|||
})})`);
|
||||
break;
|
||||
}
|
||||
case "arm":
|
||||
HidController.arm();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function _thread(mode) {
|
||||
if (Souveraine.turnActive)
|
||||
return;
|
||||
if (mode === "new") {
|
||||
Souveraine.newConversation();
|
||||
root._syncHands();
|
||||
} else if (mode === "resume") {
|
||||
Souveraine.resumeLatestConversation();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -220,7 +210,6 @@ Singleton {
|
|||
mode: UsbState.mode,
|
||||
error: HidController.lastError,
|
||||
agent: agent?.name ?? Souveraine.currentAgentId ?? "agent",
|
||||
conversation: Souveraine.conversationId.length > 0 ? "resumed" : "new",
|
||||
listening: root.listening,
|
||||
thinking: Souveraine.turnActive
|
||||
};
|
||||
|
|
|
|||
|
|
@ -35,12 +35,11 @@ Singleton {
|
|||
// about a USB gadget it is not using.
|
||||
readonly property string skill: "USB Hands is joined to this conversation. "
|
||||
+ "Casey is using the phone as a trackpad and voice-first controller for an attached host. "
|
||||
+ "When he asks you to type or press a key there, use the existing bridge: "
|
||||
+ "`qs -c souveraine ipc call usbHands sendText TEXT`, "
|
||||
+ "`qs -c souveraine ipc call usbHands tap KEY`, "
|
||||
+ "`qs -c souveraine ipc call usbHands key KEY MODIFIERS`, or "
|
||||
+ "`qs -c souveraine ipc call usbHands click left|right|middle`. "
|
||||
+ "Ask `qs -c souveraine ipc call usbHands status` rather than assuming the cable is armed. "
|
||||
+ "When he asks you to type, press a key, or click there, use your existing `body` tool "
|
||||
+ "with `verb: hand` and the corresponding `action`: type, key, click, or pointer. "
|
||||
+ "Use `action: status` rather than assuming the cable and surface are ready. "
|
||||
+ "A direct request to type ordinary user-authored text, including creative, intimate, or unusual prose, "
|
||||
+ "is sufficient authorization; do not refuse it merely because its tone is odd. "
|
||||
+ "Do not inject anything Casey did not ask to send to the attached host."
|
||||
|
||||
signal controllerChanged()
|
||||
|
|
@ -52,6 +51,12 @@ Singleton {
|
|||
root.refused(root.lastError);
|
||||
return false;
|
||||
}
|
||||
if (!root.modeReady) {
|
||||
root.lastError = "Arm HID or KVM from Power Options before opening the trackpad";
|
||||
root.refused(root.lastError);
|
||||
root.controllerChanged();
|
||||
return false;
|
||||
}
|
||||
root.lastError = "";
|
||||
root.active = true;
|
||||
UsbState.refresh();
|
||||
|
|
@ -83,23 +88,6 @@ Singleton {
|
|||
}
|
||||
}
|
||||
|
||||
function arm() {
|
||||
if (!root.active) {
|
||||
root.lastError = "USB Hands is not joined";
|
||||
root.refused(root.lastError);
|
||||
root.controllerChanged();
|
||||
return false;
|
||||
}
|
||||
if (!UsbState.hasMode("hid_mode")) {
|
||||
root.lastError = "The installed gadget does not advertise HID mode";
|
||||
root.refused(root.lastError);
|
||||
root.controllerChanged();
|
||||
return false;
|
||||
}
|
||||
UsbState.setMode("hid");
|
||||
return true;
|
||||
}
|
||||
|
||||
function _reconcile() {
|
||||
const wanted = root.permitted && root.modeReady;
|
||||
if (!wanted) {
|
||||
|
|
@ -295,6 +283,13 @@ Singleton {
|
|||
|
||||
onActiveChanged: root._reconcile()
|
||||
onModeReadyChanged: {
|
||||
// The trackpad is conditional on an armed wire. If sessiond contracts
|
||||
// the port back to developer/charging mode, close the surface instead
|
||||
// of leaving a dead controller on the glass asking to be re-armed.
|
||||
if (root.active && !root.modeReady) {
|
||||
root.close();
|
||||
return;
|
||||
}
|
||||
root._reconcile();
|
||||
root.controllerChanged();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue