lock surface: restore redacted chat on unlock + announce replies
Redacted messages stayed '[content hidden until unlock]' forever — nothing restored them on unlock. Redaction now records the message and unlock restores content from rawContent. Turns finishing while locked are also redacted and surfaced. New LockAgentCard shows a one-line preview of replies that landed during the lock window on the lock glance, like a notification; body stays hidden until unlock. sessiond auth: getpwuid_r buffer is u8 with a c_char cast so it compiles on aarch64 (c_char = u8 there) and x86 (c_char = i8).
This commit is contained in:
parent
dc9fe900ec
commit
e10efbbd56
6 changed files with 160 additions and 9 deletions
|
|
@ -158,9 +158,18 @@ pub fn session_user() -> Result<String, String> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let uid = libc::getuid();
|
let uid = libc::getuid();
|
||||||
let mut pwd: libc::passwd = std::mem::zeroed();
|
let mut pwd: libc::passwd = std::mem::zeroed();
|
||||||
let mut buf = [0i8; 1024];
|
let mut buf = [0u8; 1024];
|
||||||
let mut result: *mut libc::passwd = std::ptr::null_mut();
|
let mut result: *mut libc::passwd = std::ptr::null_mut();
|
||||||
let rc = libc::getpwuid_r(uid, &mut pwd, buf.as_mut_ptr(), buf.len(), &mut result);
|
// libc's `c_char` is i8 on some targets and u8 on others (aarch64),
|
||||||
|
// so the buffer is `u8` and we cast through `c_char` to satisfy
|
||||||
|
// whichever signature the target's libc crate exposes.
|
||||||
|
let rc = libc::getpwuid_r(
|
||||||
|
uid,
|
||||||
|
&mut pwd,
|
||||||
|
buf.as_mut_ptr() as *mut libc::c_char,
|
||||||
|
buf.len(),
|
||||||
|
&mut result,
|
||||||
|
);
|
||||||
if rc != 0 || result.is_null() {
|
if rc != 0 || result.is_null() {
|
||||||
return Err(format!("getpwuid_r failed for uid {uid}"));
|
return Err(format!("getpwuid_r failed for uid {uid}"));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ modules/common/panels/lock/LockScreen.qml souveraine/modules/common/panels/lock/
|
||||||
modules/ii/lock/Lock.qml souveraine/modules/ii/lock/Lock.qml
|
modules/ii/lock/Lock.qml souveraine/modules/ii/lock/Lock.qml
|
||||||
modules/ii/lock/TouchLockSurface.qml souveraine/modules/ii/lock/TouchLockSurface.qml
|
modules/ii/lock/TouchLockSurface.qml souveraine/modules/ii/lock/TouchLockSurface.qml
|
||||||
modules/souveraine/lock/LockMediaCard.qml souveraine/modules/souveraine/lock/LockMediaCard.qml
|
modules/souveraine/lock/LockMediaCard.qml souveraine/modules/souveraine/lock/LockMediaCard.qml
|
||||||
|
modules/souveraine/lock/LockAgentCard.qml souveraine/modules/souveraine/lock/LockAgentCard.qml
|
||||||
modules/souveraine/lock/LockSurfaceHost.qml souveraine/modules/souveraine/lock/LockSurfaceHost.qml
|
modules/souveraine/lock/LockSurfaceHost.qml souveraine/modules/souveraine/lock/LockSurfaceHost.qml
|
||||||
modules/souveraine/lock/qmldir souveraine/modules/souveraine/lock/qmldir
|
modules/souveraine/lock/qmldir souveraine/modules/souveraine/lock/qmldir
|
||||||
modules/souveraine/navigation/SystemGestureRail.qml souveraine/modules/souveraine/navigation/SystemGestureRail.qml
|
modules/souveraine/navigation/SystemGestureRail.qml souveraine/modules/souveraine/navigation/SystemGestureRail.qml
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
// Ambient "the agent replied" card. While the session is locked, personal-tier
|
||||||
|
// agent output is redacted in the chat (Ai.qml). This surface announces that a
|
||||||
|
// reply landed — one-line preview only, never the body — so the lock screen
|
||||||
|
// behaves like a notification: you see that she answered, and the full text
|
||||||
|
// restores in the chat on unlock. See SESSION-TRUST-ARCHITECTURE.md (ambient
|
||||||
|
// output is safe on the lock surface; personal is not).
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import qs.services
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var previews: []
|
||||||
|
|
||||||
|
implicitHeight: card.visible ? card.implicitHeight : 0
|
||||||
|
visible: previews.length > 0
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
root.previews = Ai.redactedPreviews();
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: Ai
|
||||||
|
function onRedactedMessagesChanged() { root.refresh() }
|
||||||
|
}
|
||||||
|
Connections {
|
||||||
|
target: GlobalStates
|
||||||
|
// Repopulate on lock, clear on unlock (the chat takes over).
|
||||||
|
function onScreenLockedChanged() {
|
||||||
|
if (GlobalStates.screenLocked) root.refresh();
|
||||||
|
else root.previews = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: card
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: root.previews.length > 0
|
||||||
|
radius: 16
|
||||||
|
color: "#22000000"
|
||||||
|
border.color: "#33ffffff"
|
||||||
|
border.width: 1
|
||||||
|
implicitHeight: layout.implicitHeight + 24
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: layout
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.margins: 12
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
Text {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: qsTr("Souveraine replied while locked")
|
||||||
|
color: "#ccffffff"
|
||||||
|
font.pixelSize: 12
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.previews.slice(0, 3)
|
||||||
|
delegate: Text {
|
||||||
|
required property string modelData
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: "• " + modelData
|
||||||
|
color: "#e6ffffff"
|
||||||
|
font.pixelSize: 14
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -65,5 +65,11 @@ Item {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LockAgentCard {
|
||||||
|
Layout.topMargin: root.compact ? 6 : 10
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
module qs.modules.souveraine.lock
|
module qs.modules.souveraine.lock
|
||||||
LockMediaCard 1.0 LockMediaCard.qml
|
LockMediaCard 1.0 LockMediaCard.qml
|
||||||
LockSurfaceHost 1.0 LockSurfaceHost.qml
|
LockSurfaceHost 1.0 LockSurfaceHost.qml
|
||||||
|
LockAgentCard 1.0 LockAgentCard.qml
|
||||||
|
|
|
||||||
|
|
@ -131,20 +131,71 @@ Singleton {
|
||||||
// raw content is preserved in the message's rawContent so it can be
|
// raw content is preserved in the message's rawContent so it can be
|
||||||
// shown again after unlock (the message stays in history), but the
|
// shown again after unlock (the message stays in history), but the
|
||||||
// displayed content is cleared.
|
// displayed content is cleared.
|
||||||
|
//
|
||||||
|
// The redaction is reversible: every message whose content we replaced
|
||||||
|
// with the placeholder is recorded in `redactedOnLock`, and on unlock
|
||||||
|
// its content is restored from `rawContent` (the streaming message too,
|
||||||
|
// if it is still in flight). A finished message redacted at lock then
|
||||||
|
// surfaced on the lock screen counts as "delivered" and is restored
|
||||||
|
// normally on unlock; one that never surfaced stays for the chat to
|
||||||
|
// replay once unlocked.
|
||||||
|
property var redactedOnLock: []
|
||||||
|
|
||||||
|
function _redactMessage(msg) {
|
||||||
|
if (!msg || msg.content.length === 0) return;
|
||||||
|
if (msg.content === Translation.tr("[content hidden until unlock]")) return;
|
||||||
|
msg.rawContent = msg.content;
|
||||||
|
msg.content = Translation.tr("[content hidden until unlock]");
|
||||||
|
if (!root.redactedOnLock.includes(msg)) {
|
||||||
|
root.redactedOnLock = [...root.redactedOnLock, msg];
|
||||||
|
}
|
||||||
|
root.redactedMessagesChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _restoreRedacted() {
|
||||||
|
if (root.redactedOnLock.length === 0) return;
|
||||||
|
for (const msg of root.redactedOnLock) {
|
||||||
|
if (msg && msg.rawContent && msg.rawContent.length > 0) {
|
||||||
|
msg.content = msg.rawContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
root.redactedOnLock = [];
|
||||||
|
root.redactedMessagesChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Messages redacted while locked, newest first, for the lock surface to
|
||||||
|
// surface as ambient notifications. Strips to a one-line preview — the
|
||||||
|
// lock screen shows "the agent replied", not the personal-tier body.
|
||||||
|
function redactedPreviews() {
|
||||||
|
return root.redactedOnLock
|
||||||
|
.filter(m => m && m.rawContent && m.rawContent.length > 0 && m.role === "assistant")
|
||||||
|
.map(m => {
|
||||||
|
const firstLine = m.rawContent.split("\n").find(l => l.trim().length > 0) || "";
|
||||||
|
return firstLine.slice(0, 80);
|
||||||
|
})
|
||||||
|
.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
signal redactedMessagesChanged()
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: GlobalStates
|
target: GlobalStates
|
||||||
function onScreenLockedChanged() {
|
function onScreenLockedChanged() {
|
||||||
if (!GlobalStates.screenLocked) return;
|
if (GlobalStates.screenLocked) {
|
||||||
if (!root.streamingMessage) return;
|
// Lock fired mid-stream — redact the in-flight content.
|
||||||
// Lock fired mid-stream. Redact the displayed content.
|
if (root.streamingMessage) {
|
||||||
// rawContent preserves the actual response for post-unlock.
|
root._redactMessage(root.streamingMessage);
|
||||||
if (root.streamingMessage.content.length > 0) {
|
console.log("[ai] lock fired during stream — redacted personal output");
|
||||||
root.streamingMessage.content = Translation.tr("[content hidden until unlock]");
|
}
|
||||||
console.log("[ai] lock fired during stream — redacted personal output");
|
} else {
|
||||||
|
// Unlock: restore every message we hid, so the chat shows
|
||||||
|
// what actually came back rather than lingering placeholders.
|
||||||
|
root._restoreRedacted();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ── Streaming message shaping ────────────────────────────────────────
|
// ── Streaming message shaping ────────────────────────────────────────
|
||||||
property AiMessageData streamingMessage
|
property AiMessageData streamingMessage
|
||||||
property bool inThinkBlock: false
|
property bool inThinkBlock: false
|
||||||
|
|
@ -171,6 +222,12 @@ Singleton {
|
||||||
}
|
}
|
||||||
root.streamingMessage.thinking = false;
|
root.streamingMessage.thinking = false;
|
||||||
root.streamingMessage.done = true;
|
root.streamingMessage.done = true;
|
||||||
|
// If the turn finished while the session is locked, the finished
|
||||||
|
// assistant message is personal-tier output the user hasn't seen —
|
||||||
|
// redact it for the chat view and let the lock surface announce it.
|
||||||
|
if (GlobalStates.screenLocked && root.streamingMessage.content.length > 0) {
|
||||||
|
root._redactMessage(root.streamingMessage);
|
||||||
|
}
|
||||||
if (root.postResponseHook) {
|
if (root.postResponseHook) {
|
||||||
root.postResponseHook();
|
root.postResponseHook();
|
||||||
root.postResponseHook = null;
|
root.postResponseHook = null;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue