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:
parent
9ccefd6c73
commit
19fe6b1d0e
59 changed files with 19450 additions and 139 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {} }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue