wry host: sites as apps, the rig as a face; join loads the expression skill
This commit is contained in:
parent
d7965a57fd
commit
9ec031c8a8
12 changed files with 730 additions and 11 deletions
|
|
@ -57,6 +57,7 @@ modules/souveraine/windowSheet/WindowSheet.qml souveraine/modules/souveraine/win
|
|||
modules/souveraine/windowSheet/SheetButton.qml souveraine/modules/souveraine/windowSheet/SheetButton.qml
|
||||
modules/souveraine/windowSheet/PowerMenu.qml souveraine/modules/souveraine/windowSheet/PowerMenu.qml
|
||||
modules/souveraine/windowSheet/qmldir souveraine/modules/souveraine/windowSheet/qmldir
|
||||
services/Face.qml souveraine/services/Face.qml
|
||||
services/ViewtopControl.qml souveraine/services/ViewtopControl.qml
|
||||
modules/souveraine/subconscious/SubconsciousTicker.qml souveraine/modules/souveraine/subconscious/SubconsciousTicker.qml
|
||||
modules/souveraine/subconscious/SubconsciousEventPanel.qml souveraine/modules/souveraine/subconscious/SubconsciousEventPanel.qml
|
||||
|
|
|
|||
|
|
@ -45,6 +45,27 @@ Item {
|
|||
anchors.fill: parent
|
||||
hoverEnabled: !Config.options.bar.tooltips.clickToShow
|
||||
|
||||
// Double tap summons her face, and dismisses it. TASK-59 Q0 asked how
|
||||
// she is reached; Casey, 2026-08-05: *"I double tap on the clock widget
|
||||
// and I get the avatar."* Deliberately not always-on — the webview is
|
||||
// 283 MB measured, which on a 3.5 GB daily driver is a design argument
|
||||
// and not only a taste one.
|
||||
//
|
||||
// A toggle rather than a summon, because the same gesture has to be the
|
||||
// way out: a presence you cannot dismiss is the user's column being
|
||||
// ignored (doctrine §13, and TASK-59 Q4).
|
||||
property real lastTapAt: -1
|
||||
readonly property int doubleTapInterval: 350
|
||||
onClicked: {
|
||||
const now = Date.now();
|
||||
if (lastTapAt > 0 && now - lastTapAt <= doubleTapInterval) {
|
||||
lastTapAt = -1;
|
||||
Face.toggle();
|
||||
} else {
|
||||
lastTapAt = now;
|
||||
}
|
||||
}
|
||||
|
||||
ClockWidgetPopup {
|
||||
hoverTarget: mouseArea
|
||||
}
|
||||
|
|
|
|||
140
surfaces/quickshell/services/Face.qml
Normal file
140
surfaces/quickshell/services/Face.qml
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
// Her face on the glass — TASK-59.
|
||||
//
|
||||
// The face is a *limb*, not a second client. `Souveraine.qml` is the one
|
||||
// connection to the server and stays that way: this owns a `souveraine-web`
|
||||
// process, feeds it what she is already saying on the sidebar's stream, and
|
||||
// sends what the user says to it back through the same `Souveraine.send()`.
|
||||
// Casey, 2026-08-05: *"I will want it to be in sync with the sidebar — meaning
|
||||
// if we 'resume' it's resumed."* Two transports could not promise that; one
|
||||
// does by construction.
|
||||
//
|
||||
// ## Turning her on is joining
|
||||
//
|
||||
// `joined` is the whole state. While it is true she is on the glass **and** the
|
||||
// expression vocabulary rides in the per-send ambient block, so she has a
|
||||
// syntax for shifting expression. While it is false neither happens — and the
|
||||
// second half is the point: that prompt is context nobody asked for when the
|
||||
// face is closed. Casey, 2026-08-05: *"it'll be like a loadable/unloadable
|
||||
// skill… we might have times where we just don't want that extra prompt added
|
||||
// to context."*
|
||||
//
|
||||
// The sidebar ignores the tags it sees, which is why they are safe to leave in
|
||||
// the stream rather than stripped on the way to one surface.
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.modules.common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// On the glass, and in the prompt. One flag, both consequences.
|
||||
property bool joined: false
|
||||
property string socketPath: Quickshell.env("XDG_RUNTIME_DIR") + "/souveraine/face.sock"
|
||||
property string rigDir: Quickshell.env("HOME") + "/.souveraine/face"
|
||||
|
||||
// Accumulated text of the turn in flight, so the bubble shows the whole
|
||||
// line rather than the last delta.
|
||||
property string _line: ""
|
||||
|
||||
// The fragment that rides in `ambient` while she is joined. Kept here
|
||||
// rather than in the server so that leaving costs exactly nothing — there
|
||||
// is no flag to unset and no prompt to remember to remove.
|
||||
readonly property string skill: "You have a face on this device right now. "
|
||||
+ "You may shift expression by emitting a tag on its own line: "
|
||||
+ "[[face:idle]], [[face:alert]], [[face:thinking]], [[face:processing]], "
|
||||
+ "[[face:affectionate]], [[face:straining]], [[face:yawning]], "
|
||||
+ "[[face:listening]], [[face:speaking]]. "
|
||||
+ "These are the postures the presence system already uses. "
|
||||
+ "Use them sparingly, where the shift is real."
|
||||
|
||||
function join() {
|
||||
if (root.joined)
|
||||
return;
|
||||
host.running = true;
|
||||
root.joined = true;
|
||||
}
|
||||
|
||||
function leave() {
|
||||
root.joined = false;
|
||||
root._send({ op: "quit" });
|
||||
host.running = false;
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (root.joined)
|
||||
root.leave();
|
||||
else
|
||||
root.join();
|
||||
}
|
||||
|
||||
Process {
|
||||
id: host
|
||||
command: ["souveraine-web",
|
||||
"--rig", root.rigDir,
|
||||
"--ipc", root.socketPath,
|
||||
"--transparent",
|
||||
"--app-id", "org.souveraine.face"]
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: line => {
|
||||
let msg;
|
||||
try {
|
||||
msg = JSON.parse(line);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
// Anything the page wants to say goes through the one transport.
|
||||
if (msg.event === "said" && msg.text)
|
||||
Souveraine.send(msg.text);
|
||||
else if (msg.event === "tapped")
|
||||
root.tapped(msg.area ?? "body");
|
||||
}
|
||||
}
|
||||
onExited: root.joined = false
|
||||
}
|
||||
|
||||
signal tapped(string area)
|
||||
|
||||
Socket {
|
||||
id: sock
|
||||
path: root.socketPath
|
||||
connected: root.joined
|
||||
}
|
||||
|
||||
function _send(msg) {
|
||||
if (sock.connected)
|
||||
sock.write(JSON.stringify(msg) + "\n");
|
||||
}
|
||||
|
||||
function _eval(script) {
|
||||
root._send({ op: "eval", script: script });
|
||||
}
|
||||
|
||||
// Everything she says on the sidebar's stream reaches the bubble. The face
|
||||
// is a second *view* of one turn, never a second turn.
|
||||
Connections {
|
||||
target: Souveraine
|
||||
enabled: root.joined
|
||||
|
||||
function onStreamEvent(event) {
|
||||
if (event.message_type === "assistant_message" && event.content) {
|
||||
root._line += event.content;
|
||||
// Posture tags are hers to emit and the bubble's to not show.
|
||||
const tag = /\[\[face:([a-z]+)\]\]/g;
|
||||
let m;
|
||||
while ((m = tag.exec(root._line)) !== null)
|
||||
root._eval(`window.face.posture(${JSON.stringify(m[1])})`);
|
||||
const shown = root._line.replace(tag, "").trim();
|
||||
root._eval(`window.face.say(${JSON.stringify(shown)})`);
|
||||
}
|
||||
}
|
||||
|
||||
function onTurnActiveChanged() {
|
||||
if (Souveraine.turnActive)
|
||||
root._line = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -336,6 +336,13 @@ Singleton {
|
|||
if (root._cursorPos.length > 0) {
|
||||
lines.push(`cursor: ${root._cursorPos}`);
|
||||
}
|
||||
// Joining the face loads its skill; leaving unloads it. Attached here
|
||||
// because ambient is already the per-send block the surface owns, so
|
||||
// the cost of not being joined is exactly zero tokens rather than a
|
||||
// flag the server has to remember to check.
|
||||
if (typeof Face !== "undefined" && Face.joined) {
|
||||
lines.push(Face.skill);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ singleton SessionWarnings 1.0 SessionWarnings.qml
|
|||
singleton SessiondBridge 1.0 SessiondBridge.qml
|
||||
singleton SessiondPolicy 1.0 SessiondPolicy.qml
|
||||
singleton SongRec 1.0 SongRec.qml
|
||||
singleton Face 1.0 Face.qml
|
||||
singleton Souveraine 1.0 Souveraine.qml
|
||||
singleton Speech 1.0 Speech.qml
|
||||
singleton StepUpAuth 1.0 StepUpAuth.qml
|
||||
|
|
|
|||
Loading…
Reference in a new issue