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 {
|
||||
let uid = libc::getuid();
|
||||
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 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() {
|
||||
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/TouchLockSurface.qml souveraine/modules/ii/lock/TouchLockSurface.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/qmldir souveraine/modules/souveraine/lock/qmldir
|
||||
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.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
|
||||
LockMediaCard 1.0 LockMediaCard.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
|
||||
// shown again after unlock (the message stays in history), but the
|
||||
// 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 {
|
||||
target: GlobalStates
|
||||
function onScreenLockedChanged() {
|
||||
if (!GlobalStates.screenLocked) return;
|
||||
if (!root.streamingMessage) return;
|
||||
// Lock fired mid-stream. Redact the displayed content.
|
||||
// rawContent preserves the actual response for post-unlock.
|
||||
if (root.streamingMessage.content.length > 0) {
|
||||
root.streamingMessage.content = Translation.tr("[content hidden until unlock]");
|
||||
console.log("[ai] lock fired during stream — redacted personal output");
|
||||
if (GlobalStates.screenLocked) {
|
||||
// Lock fired mid-stream — redact the in-flight content.
|
||||
if (root.streamingMessage) {
|
||||
root._redactMessage(root.streamingMessage);
|
||||
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 ────────────────────────────────────────
|
||||
property AiMessageData streamingMessage
|
||||
property bool inThinkBlock: false
|
||||
|
|
@ -171,6 +222,12 @@ Singleton {
|
|||
}
|
||||
root.streamingMessage.thinking = false;
|
||||
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) {
|
||||
root.postResponseHook();
|
||||
root.postResponseHook = null;
|
||||
|
|
|
|||
Loading…
Reference in a new issue