Watch
1
0
Fork
You've already forked souveraine
0

shell: dock reorder, fullscreen detection fix, idle-power, sessiond, misc shell work

- Dock drag-to-reorder for pinned apps (insertion gap, quick-slide vs dwell)
- Fullscreen detection: scan all windows via HyprlandData.windowList
- IdleCoordinator, GlobalStates, Session.qml updates
- Deploy script, qmldir, settings, wallpaper, visualizer fixes
- sessiond server, memory module updates

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Fimeg 2026-07-22 22:18:20 -04:00
commit 19fe6b1d0e
59 changed files with 19450 additions and 139 deletions

View file

@ -157,6 +157,33 @@ Singleton {
return str.replace(/\\/g, '\\\\');
}
/**
* Cleans assistant reply text for text-to-speech: strips <think> blocks,
* code fences, and markdown formatting so only the spoken voice remains.
* Returns the cleaned text (empty string if nothing speakable survives).
* @param { string } text
* @returns { string }
*/
function ttsClean(text) {
if (!text)
return "";
return text
// Remove <think>...</think> blocks entirely
.replace(/<think>[\s\S]*?<\/think>/g, "")
// Remove markdown headers, bold, italic, code fences
.replace(/```[\s\S]*?```/g, " ")
.replace(/#{1,6}\s/g, " ")
.replace(/\*\*([^*]+)\*\*/g, "$1")
.replace(/\*([^*]+)\*/g, "$1")
.replace(/`([^`]+)`/g, "$1")
// Remove markdown links and images
.replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1")
.replace(/\[([^\]]*)\]\([^)]+\)/g, "$1")
// Collapse whitespace
.replace(/\n{3,}/g, "\n\n")
.trim();
}
/**
* Wraps words to supplied maximum length
* @param { string | null } str

View file

@ -34,11 +34,22 @@ AbstractBackgroundWidget {
"data_format = ascii\nascii_max_range = 100\n";
}
// Souveraine (idle-power task 4): cava only earns its keep while the
// widget can actually be seen. Gate the watchdog on visibility + display
// activity, and tear cava down when either goes away.
readonly property bool wantCava: parent.visible && GlobalStates.displayActive
onWantCavaChanged: {
if (!wantCava && cavaProcess.running) {
root.cavaReady = false;
cavaProcess.running = false;
}
}
// Watchdog: every 2s, if cava isn't running, write config and start it
Timer {
id: watchdog
interval: 2000
running: true
running: root.wantCava
repeat: true
triggeredOnStart: true
onTriggered: {
@ -190,7 +201,14 @@ AbstractBackgroundWidget {
}
ctx.stroke();
}
Timer { interval: 33; running: root.style === "wave"; repeat: true; onTriggered: parent.requestPaint() }
// 30fps repaint only while this canvas is actually visible.
// Was `running: root.style === "wave"` but the Canvas's own
// `visible` (line above) already encodes that, AND collapses to
// false when the widget subtree is hidden. Gating the timer on
// `parent.visible` stops the continuous redraw whenever the
// canvas isn't shown, instead of painting behind a hidden
// background. No new import needed (pure QtQuick `visible`).
Timer { interval: 33; running: parent.visible; repeat: true; onTriggered: parent.requestPaint() }
}
}
}

View file

@ -15,6 +15,7 @@ Singleton {
{ identifier: "resources", materialSymbol: "browse_activity" },
{ identifier: "notes", materialSymbol: "note_stack" },
{ identifier: "volumeMixer", materialSymbol: "volume_up" },
{ identifier: "subconsciousEventPanel", materialSymbol: "psychology" },
]
readonly property bool hasPinnedWidgets: root.pinnedWidgetIdentifiers.length > 0

View file

@ -13,6 +13,7 @@ import qs.modules.ii.overlay.fpsLimiter
import qs.modules.ii.overlay.recorder
import qs.modules.ii.overlay.resources
import qs.modules.ii.overlay.notes
import qs.modules.ii.overlay.subconsciousEventPanel
DelegateChooser {
id: root
@ -25,4 +26,5 @@ DelegateChooser {
DelegateChoice { roleValue: "resources"; Resources {} }
DelegateChoice { roleValue: "notes"; Notes {} }
DelegateChoice { roleValue: "volumeMixer"; VolumeMixer {} }
DelegateChoice { roleValue: "subconsciousEventPanel"; SubconsciousEventPanel {} }
}

View file

@ -0,0 +1,173 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import qs.modules.ii.overlay
// Tier 2 of the subconscious three-tier visibility design (see
// docs/tasks/subconscious-surfacing-threshold.md): a persistent, interactive
// event log the shell's equivalent of the TUI "cockpit". Shows surfaced
// subconscious events (pass snapshots, reflections, archivist syntheses,
// surfacings) from past turns. Each entry click-expands to its full text,
// closing docs/bugs.md B-005 ("subconscious messages not expandable").
// Fed by Ai.subconsciousEvents; the live Tier-1 ticker is snapshotted into
// it on pass end.
StyledOverlayWidget {
id: root
title: Translation.tr("Subconscious")
showCenterButton: true
contentItem: Rectangle {
radius: root.contentRadius
color: Appearance.colors.colLayer1
implicitWidth: 340
implicitHeight: 420
ColumnLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 6
// Header
RowLayout {
Layout.fillWidth: true
spacing: 6
MaterialSymbol {
iconSize: Appearance.font.pixelSize.larger
text: "psychology"
color: Appearance.colors.colSubtext
}
StyledText {
Layout.fillWidth: true
text: Translation.tr("Subconscious event log")
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.small
font.bold: true
}
StyledText {
text: Ai.subconsciousEvents.length
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.small
visible: Ai.subconsciousEvents.length > 0
}
}
// Event list
ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
ListView {
id: eventList
model: Ai.subconsciousEvents
spacing: 4
boundsBehavior: Flickable.StopAtBounds
delegate: Rectangle {
width: eventList.width
height: entryCol.implicitHeight + 12
radius: Appearance.rounding.small
color: Appearance.colors.colLayer2
border.width: 1
border.color: Appearance.colors.colLayer1Inactive
property bool expanded: false
ColumnLayout {
id: entryCol
anchors.fill: parent
anchors.margins: 6
spacing: 3
RowLayout {
Layout.fillWidth: true
spacing: 4
MaterialSymbol {
iconSize: Appearance.font.pixelSize.small
text: modelData.kind === "halt" ? "error"
: modelData.kind === "reflection" ? "lightbulb"
: modelData.kind === "archivist" ? "auto_stories"
: "psychology"
color: Appearance.colors.colSubtext
}
StyledText {
Layout.fillWidth: true
text: modelData.source ?? Translation.tr("Subconscious")
color: Appearance.colors.colOnLayer1
font.pixelSize: Appearance.font.pixelSize.small
font.bold: true
elide: Text.ElideRight
}
MaterialSymbol {
iconSize: Appearance.font.pixelSize.small
text: expanded ? "expand_less" : "expand_more"
color: Appearance.colors.colSubtext
}
}
StyledText {
Layout.fillWidth: true
visible: !expanded
text: (modelData.content ?? "").split("\n")[0]
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.small
elide: Text.ElideRight
}
StyledText {
Layout.fillWidth: true
visible: expanded
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
text: modelData.content ?? ""
color: Appearance.colors.colOnLayer1
font.pixelSize: Appearance.font.pixelSize.small
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: parent.expanded = !parent.expanded
}
}
}
}
// Empty state
StyledText {
Layout.fillWidth: true
Layout.fillHeight: true
visible: Ai.subconsciousEvents.length === 0
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: Translation.tr("No subconscious events yet.\nThey appear here after a pass.")
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.small
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
}
// Clear log
StyledText {
Layout.alignment: Qt.AlignRight
text: Translation.tr("Clear")
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.small
visible: Ai.subconsciousEvents.length > 0
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: Ai.subconsciousEvents = []
}
}
}
}
}

View file

@ -66,6 +66,7 @@ RowLayout {
focus: GlobalStates.overviewOpen
font.pixelSize: Appearance.font.pixelSize.small
placeholderText: Translation.tr("Search, calculate or run")
inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase
implicitWidth: root.searchingText == "" ? Appearance.sizes.searchWidthCollapsed : Appearance.sizes.searchWidth
Behavior on implicitWidth {
@ -78,7 +79,14 @@ RowLayout {
}
}
onTextChanged: LauncherSearch.query = text
// Souveraine: drive the search from committed text PLUS live
// input-method composition. Stevia (sm.puri.OSK0) delivers typed
// letters as pre-edit that it may never commit, so `text` alone stays
// empty while the letters are visibly in the field hardware
// keyboards and IPC setQuery worked, OSK typing searched nothing
// (observed 2026-07-22: field showed letters, query stayed "").
onTextChanged: LauncherSearch.query = text + preeditText
onPreeditTextChanged: LauncherSearch.query = text + preeditText
onAccepted: {
if (appResults.count > 0) {

View file

@ -1,7 +1,6 @@
pragma ComponentBehavior: Bound
import Qt.labs.synchronizer
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Layouts
import Quickshell
@ -47,6 +46,25 @@ Item { // Wrapper
LauncherSearch.query = text;
}
function debugState() {
return {
showResults: root.showResults,
width: root.width,
height: root.height,
contentWidth: searchWidgetContent.width,
contentHeight: searchWidgetContent.height,
columnWidth: columnLayout.width,
columnHeight: columnLayout.height,
listVisible: appResults.visible,
listCount: appResults.count,
listWidth: appResults.width,
listHeight: appResults.height,
listImplicitHeight: appResults.implicitHeight,
listContentHeight: appResults.contentHeight,
modelCount: resultModel.count
};
}
Keys.onPressed: event => {
// Prevent Esc and Backspace from registering
if (event.key === Qt.Key_Escape)
@ -146,16 +164,6 @@ Item { // Wrapper
}
spacing: 0
// clip: true
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: searchWidgetContent.width
height: searchWidgetContent.width
radius: searchWidgetContent.radius
}
}
SearchBar {
id: searchBar
property real verticalPadding: 4

View file

@ -195,11 +195,51 @@ Rectangle {
onClicked: {
Ai.regenerate(root.messageIndex)
}
StyledToolTip {
text: Translation.tr("Regenerate")
}
}
AiMessageControlButton {
id: speakButton
// stop icon only while THIS message is the active speaker
buttonIcon: (Speech.speaking && Ai.speakingMessageIndex === root.messageIndex)
? "stop_circle" : "volume_up"
visible: Speech.enabled && messageData?.role === 'assistant'
enabled: visible
onClicked: {
if (Speech.speaking && Ai.speakingMessageIndex === root.messageIndex) {
Speech.stop() // Ai.qml's Connections clears speakingMessageIndex
} else {
Ai.speakingMessageIndex = root.messageIndex
Speech.speak(StringUtils.ttsClean(root.messageData?.content ?? ""))
}
}
StyledToolTip {
text: Translation.tr("Speak")
}
}
AiMessageControlButton {
id: respeakButton
buttonIcon: "replay"
visible: Speech.enabled && messageData?.role === 'assistant'
enabled: visible
onClicked: {
// Re-synthesize fresh audio for this message's text
// when the previous synth came out broken. Does
// not re-run the agent.
Ai.speakingMessageIndex = root.messageIndex
Speech.stop()
Speech.speak(StringUtils.ttsClean(root.messageData?.content ?? ""))
}
StyledToolTip {
text: Translation.tr("Re-synthesize audio")
}
}
AiMessageControlButton {
id: copyButton

View file

@ -0,0 +1,82 @@
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import QtQuick
import QtQuick.Layouts
// Tier 1 of the subconscious three-tier visibility design (see
// docs/tasks/subconscious-surfacing-threshold.md): a transient, fading line
// that shows the subconscious's live reasoning/tools while its N+1 pass runs.
// Visible only while Ai.subconsciousActive; fed by Ai.subconsciousStream;
// cleared (and snapshotted to the Tier-2 log) when the pass ends.
Rectangle {
id: root
Layout.fillWidth: true
implicitHeight: row.implicitHeight + 2 * root.padding
radius: Appearance.rounding.small
color: Appearance.colors.colLayer2
visible: Ai.subconsciousActive && Ai.subconsciousStream.length > 0
opacity: visible ? 1 : 0
property real padding: 6
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on implicitHeight {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
// Fader: retire the oldest line a few seconds after it arrives so the
// ticker reads as a live stream, not an accumulating log. The full stream
// is snapshotted to Ai.subconsciousEvents on pass end regardless.
Timer {
interval: 4000
repeat: true
running: root.visible
onTriggered: {
if (Ai.subconsciousStream.length > 1)
Ai.subconsciousStream = Ai.subconsciousStream.slice(1);
}
}
RowLayout {
id: row
anchors.fill: parent
anchors.margins: root.padding
spacing: root.padding
MaterialSymbol {
Layout.alignment: Qt.AlignVCenter
iconSize: Appearance.font.pixelSize.larger
text: "psychology"
color: Appearance.colors.colSubtext
NumberAnimation on opacity {
// gentle pulse while the pass runs
from: 0.5; to: 1; duration: 1200
running: root.visible; loops: Animation.Infinite
easing.type: Easing.InOutSine
}
}
StyledText {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
text: {
const s = Ai.subconsciousStream;
if (s.length === 0) return "";
const last = s[s.length - 1];
const tag = last.kind === "tool_call" || last.kind === "tool_result"
? Translation.tr("tool") : Translation.tr("thinking");
return `${tag} · ${last.text}`;
}
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.small
elide: Text.ElideRight
maximumLineCount: 2
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
}
}
}

View file

@ -26,9 +26,13 @@ Singleton {
property string uptime: "0h, 0m"
Timer {
interval: 10
// Uptime advances one minute per minute; reload /proc/uptime once a
// minute, not 100x/second. (Was interval: 10 a 10ms busy-reload of a
// disk file, an always-on drain independent of the SystemClock above.)
interval: 60000
running: true
repeat: true
triggeredOnStart: true
onTriggered: {
fileUptime.reload();
const textUptime = fileUptime.text();

View file

@ -1,6 +1,7 @@
pragma Singleton
pragma ComponentBehavior: Bound
import qs
import QtQuick
import Quickshell
import Quickshell.Io
@ -148,7 +149,10 @@ Singleton {
Timer {
interval: 1000
running: true
// Souveraine: pause while the display is inactive (idle-power task 4).
// The rate math is elapsed-time based, so the first tick after resume
// yields one correctly averaged sample rather than a spike.
running: GlobalStates.displayActive
repeat: true
triggeredOnStart: true
onTriggered: root.update()

View file

@ -1,6 +1,7 @@
pragma Singleton
pragma ComponentBehavior: Bound
import qs
import qs.modules.common
import QtQuick
import Quickshell
@ -61,7 +62,9 @@ Singleton {
Timer {
interval: 1
running: true
// Souveraine: pause while the display is dimmed/locked/asleep
// stats history nobody can see is pure CPU burn (idle-power task 4).
running: GlobalStates.displayActive
repeat: true
onTriggered: {
// Reload files