aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AGENTS.md10
-rw-r--r--assistant/Assistant.qml261
-rw-r--r--assistant/Place.js23
-rw-r--r--assistant/README.md17
l---------assistant/Theme.qml1
-rw-r--r--assistant/shell.qml16
-rw-r--r--assistant/test_place.js32
-rw-r--r--desktop/README.md3
-rw-r--r--desktop/modules/ai/README.md3
9 files changed, 8 insertions, 358 deletions
diff --git a/AGENTS.md b/AGENTS.md
index c70c98d..4d7b35c 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -6,20 +6,20 @@ Guidance for agents working in this repository.
Quickshell components for a Hyprland desktop. The repo is a hybrid: `desktop/`
is the integrated process, hosting the parts that need to talk to each other,
-the drawer, which is the control center, and the island. The other directories
-stay separate shells, each complete on its own: any of them runs alone, and
-running one does not require the others. `desktop/` is itself a host for
+the drawer, which is the control center, the island, and the assistant
+canvas. The other directories stay separate shells, each complete on its own:
+any of them runs alone, and running one does not require the others. `desktop/` is itself a host for
modules.
desktop/ the drawer (SUPER alone): user details, sound, mail, VMs, status,
performance, clipboard, updates, appearance
island/ top-centre pills on DP-1: clock, volume, tray, layout, privacy,
calendar
+ assistant/ the assistant canvas on SUPER+Home, a view over desktop-assistant
appearance/ wallpaper picker and colour scheme switcher
window-switcher/ expose: open windows scattered as live previews, on ALT+TAB
or the top-right hot corner of DP-1
keybinds/ live keybind reminder, on SUPER+k
- assistant/ overlay for the voice assistant, fed by its event socket
notifications/ balloons for the notification daemon's published files
They are started from `~/.config/hypr/sections/autostart.lua` and keep running
@@ -333,7 +333,7 @@ changing that component. The ones that generalise:
the failed `QLocalSocket`, so setting `connected = true` again (or toggling
it, or changing `path`) is a no-op: `setConnected` only connects when no
socket object exists. Retry with a fresh `Socket`, e.g. inside a
- `LazyLoader` toggled off and on (`assistant/Assistant.qml`). A dropped
+ `LazyLoader` toggled off and on (`desktop/assistant/Assistant.qml`). A dropped
connection is different: `disconnected` clears the socket, so a later
`connected = true` works, until one of those attempts fails.
- **llama-server's `/slots?model=<id>` loads the model.** In router mode a
diff --git a/assistant/Assistant.qml b/assistant/Assistant.qml
deleted file mode 100644
index 6d1eeb2..0000000
--- a/assistant/Assistant.qml
+++ /dev/null
@@ -1,261 +0,0 @@
-// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
-//
-// This program is free software; you can redistribute it and/or modify
-// it under the terms of the GNU General Public License version 2 as
-// published by the Free Software Foundation.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-
-import Quickshell
-import Quickshell.Io
-import Quickshell.Wayland
-import Quickshell.Hyprland
-import QtQuick
-import "Place.js" as Place
-
-// The voice assistant's overlay. desktop-assistant broadcasts its state,
-// context use and audio levels as JSON lines on a Unix socket; this only
-// listens, and keeps reconnecting while the assistant is down.
-Scope {
- id: root
-
- // Replaced by the assistant's hello; these render only before it.
- property string name: "Assistant"
- property string position: "center"
- property real hideDelay: 3
- property string userColor: "accent"
- property string assistantColor: "green"
-
- property int ctxUsed: 0
- property int ctxMax: 0
- property string state: "idle"
- property string tool: ""
- property real level: 0 // latest loudness, 0..1
- property string who: "user" // whose voice that was
- property real heardAt: 0 // when it arrived, ms
- property bool shown: false
- property var shownScreen: Quickshell.screens[0]
-
- // Faded, not switched: the window stays loaded until the fade has run.
- property real fade: shown ? 1 : 0
- Behavior on fade { NumberAnimation { duration: 200 } }
-
- // Colour roles are palette names from udt, so a theme change recolours
- // the waves live. An unknown name falls back to the accent.
- function colorOf(role) { return Theme[role] ?? Theme.accent; }
-
- function handle(line) {
- let e;
- try { e = JSON.parse(line); } catch (err) { return; }
- if (e.type === "hello") {
- root.name = e.name;
- root.position = e.position;
- root.hideDelay = e.hide_delay;
- root.userColor = e.user_color;
- root.assistantColor = e.assistant_color;
- root.ctxMax = e.ctx_max;
- root.setState(e.state, e.tool ?? "");
- } else if (e.type === "state") {
- root.setState(e.state, e.tool ?? "");
- } else if (e.type === "ctx") {
- root.ctxUsed = e.used;
- root.ctxMax = e.max;
- } else if (e.type === "level") {
- root.level = e.value;
- root.who = e.who;
- root.heardAt = Date.now();
- }
- }
-
- function setState(s, t) {
- root.state = s;
- root.tool = t;
- if (s === "listening" && !root.shown) {
- // The monitor in focus when it appears, not wherever it last was.
- const focused = Hyprland.focusedMonitor?.name;
- root.shownScreen = Quickshell.screens.find(x => x.name === focused) ?? Quickshell.screens[0];
- root.level = 0;
- root.shown = true;
- }
- if (s === "idle") hideTimer.restart(); else hideTimer.stop();
- }
-
- Timer {
- id: hideTimer
- interval: root.hideDelay * 1000
- onTriggered: root.shown = false
- }
-
- // The assistant may start after this shell, or restart: keep trying with
- // a fresh Socket each time. Quickshell 0.3.1 keeps the QLocalSocket of a
- // failed connect, so setting `connected` again on the same Socket never
- // retries: an overlay started before the assistant stayed dark for good.
- LazyLoader {
- id: link
- active: true
- Socket {
- path: `${Quickshell.env("XDG_RUNTIME_DIR") || "/tmp"}/desktop-assistant.sock`
- connected: true
- onConnectionStateChanged: if (!connected) root.shown = false
- parser: SplitParser { onRead: line => root.handle(line) }
- }
- }
-
- Timer {
- interval: 2000
- repeat: true
- running: !(link.item?.connected ?? false)
- onTriggered: { link.active = false; link.active = true; }
- }
-
- // A config with no visible window exits, reporting nothing. This 1x1
- // click-through window holds the shell open while the overlay is hidden.
- PanelWindow {
- anchors { top: true; left: true }
- implicitWidth: 1
- implicitHeight: 1
- color: "transparent"
- exclusionMode: ExclusionMode.Ignore
- mask: Region {}
- WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
- }
-
- LazyLoader {
- active: root.fade > 0
-
- PanelWindow {
- screen: root.shownScreen
- anchors { top: true; bottom: true; left: true; right: true }
- color: "transparent"
- exclusionMode: ExclusionMode.Ignore
- mask: Region {}
- WlrLayershell.layer: WlrLayer.Overlay
- WlrLayershell.namespace: "quickshell-assistant"
- WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
-
- Item {
- id: orb
- // Every size below scales from this one value.
- readonly property real zoom: 1.3
- readonly property int size: 360 * zoom
- readonly property var spot: Place.at(root.position, parent.width, parent.height, size, 24)
- x: spot.x
- y: spot.y
- width: size
- height: size
- opacity: root.fade
-
- // The waves: three sine ripples round the circle, drifting at
- // different speeds, their height following the loudness and
- // their colour whoever is talking. Silence settles them into a
- // thin flat ring.
- Canvas {
- id: waves
- anchors.fill: parent
- renderTarget: Canvas.FramebufferObject
- property real amp: 0
- property real phase: 0
-
- // One repaint per display frame (vsync, not a timer: a
- // 16 ms Timer drifted against the refresh and stuttered).
- // Motion scales by the frame's real duration, so a late
- // frame does not jump. The level stream only sets the
- // height; a level older than 150 ms means the audio
- // stopped, so the height eases back to zero.
- FrameAnimation {
- running: root.fade > 0
- onTriggered: {
- const target = Date.now() - root.heardAt < 150 ? root.level : 0;
- waves.amp += (target - waves.amp) * (1 - Math.exp(-frameTime * 15));
- waves.phase += frameTime * 3;
- waves.requestPaint();
- }
- }
-
- onPaint: {
- const ctx = getContext("2d");
- ctx.reset();
- const c = width / 2, r0 = 86 * orb.zoom, reach = 70 * orb.zoom, n = 128;
- ctx.strokeStyle = `${root.colorOf(root.who === "user" ? root.userColor : root.assistantColor)}`;
- ctx.lineWidth = 2.5 * orb.zoom;
- [[3, 1.3, 0.9], [5, -0.9, 0.55], [7, 2.1, 0.35]].forEach(([k, speed, alpha]) => {
- ctx.globalAlpha = alpha;
- ctx.beginPath();
- for (let i = 0; i <= n; i++) {
- const a = i / n * 2 * Math.PI;
- const r = r0 + 4 + amp * reach * (0.55 + 0.45 * Math.sin(k * a + phase * speed));
- const x = c + r * Math.cos(a), y = c + r * Math.sin(a);
- if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
- }
- ctx.closePath();
- ctx.stroke();
- });
- }
- }
-
- Rectangle {
- anchors.centerIn: parent
- width: 160 * orb.zoom
- height: width
- radius: width / 2
- color: Qt.rgba(Theme.base.r, Theme.base.g, Theme.base.b, 0.8)
- border.color: root.colorOf(root.assistantColor)
- border.width: 2
-
- Column {
- anchors.centerIn: parent
- spacing: 4
-
- Text {
- anchors.horizontalCenter: parent.horizontalCenter
- text: root.name
- color: Theme.text
- font.family: Theme.fontFamily
- font.pixelSize: (Theme.fontSize + 2) * orb.zoom
- font.bold: true
- }
- // Working without sound (transcribing, thinking):
- // three dots pulsing in turn, on the waves' clock.
- Row {
- anchors.horizontalCenter: parent.horizontalCenter
- visible: root.state === "thinking" || root.state === "transcribing"
- spacing: 6 * orb.zoom
- Repeater {
- model: 3
- Rectangle {
- required property int index
- width: 7 * orb.zoom
- height: width
- radius: width / 2
- color: root.colorOf(root.assistantColor)
- opacity: 0.25 + 0.75 * (0.5 + 0.5 * Math.sin(waves.phase * 2 - index * 0.9))
- }
- }
- }
- // nf-fa-wrench, present in Inconsolata Nerd Font.
- Text {
- anchors.horizontalCenter: parent.horizontalCenter
- visible: root.state === "tool"
- text: ""
- color: Theme.peach
- font.family: Theme.iconFamily
- font.pixelSize: 22 * orb.zoom
- }
- Text {
- anchors.horizontalCenter: parent.horizontalCenter
- text: root.state === "tool" ? root.tool
- : root.ctxMax > 0 ? `${root.ctxUsed} / ${root.ctxMax}`
- : `${root.ctxUsed}`
- color: Theme.subtext
- font.family: Theme.fontFamily
- font.pixelSize: (Theme.fontSize - 3) * orb.zoom
- }
- }
- }
- }
- }
- }
-}
diff --git a/assistant/Place.js b/assistant/Place.js
deleted file mode 100644
index f6d895c..0000000
--- a/assistant/Place.js
+++ /dev/null
@@ -1,23 +0,0 @@
-.pragma library
-// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
-//
-// This program is free software; you can redistribute it and/or modify
-// it under the terms of the GNU General Public License version 2 as
-// published by the Free Software Foundation.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-
-// Top-left corner of a size x size square on a W x H screen, margin away
-// from the edges its position names. Anything unknown is the centre.
-function at(position, W, H, size, margin) {
- const x = position.endsWith("left") ? margin
- : position.endsWith("right") ? W - size - margin
- : (W - size) / 2;
- const y = position.startsWith("top") ? margin
- : position.startsWith("bottom") ? H - size - margin
- : (H - size) / 2;
- return { x, y };
-}
diff --git a/assistant/README.md b/assistant/README.md
deleted file mode 100644
index 9c989cf..0000000
--- a/assistant/README.md
+++ /dev/null
@@ -1,17 +0,0 @@
-# assistant
-
-Overlay for desktop-assistant, the local voice assistant. It appears when the
-mic button is pressed and shows the assistant's name, its model context use,
-the tool running, and a ring of sound waves that follows the real audio: the
-user's voice in one palette colour, the reply in another. It fades out a few
-seconds after the reply.
-
-The assistant owns a Unix socket, `$XDG_RUNTIME_DIR/desktop-assistant.sock`,
-and writes one JSON event per line (`hello`, `state`, `ctx`, `level`); this
-component only reads it and reconnects every 2 s while the assistant is down.
-Name, position, fade delay and the two colour roles come from the
-assistant's `config.toml` `[overlay]` table, in the `hello` event. The
-protocol is specified in the desktop-assistant repo,
-`docs/superpowers/specs/2026-09-26-overlay-design.md`.
-
-Run: `qs -p <this dir>`. Test the position maths: `node assistant/test_place.js`.
diff --git a/assistant/Theme.qml b/assistant/Theme.qml
deleted file mode 120000
index 3d2e40f..0000000
--- a/assistant/Theme.qml
+++ /dev/null
@@ -1 +0,0 @@
-../shared/Theme.qml \ No newline at end of file
diff --git a/assistant/shell.qml b/assistant/shell.qml
deleted file mode 100644
index 06e247a..0000000
--- a/assistant/shell.qml
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
-//
-// This program is free software; you can redistribute it and/or modify
-// it under the terms of the GNU General Public License version 2 as
-// published by the Free Software Foundation.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-
-import Quickshell
-
-ShellRoot {
- Assistant {}
-}
diff --git a/assistant/test_place.js b/assistant/test_place.js
deleted file mode 100644
index 9f1376e..0000000
--- a/assistant/test_place.js
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
-//
-// This program is free software; you can redistribute it and/or modify
-// it under the terms of the GNU General Public License version 2 as
-// published by the Free Software Foundation.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-
-// Run: node assistant/test_place.js
-// Place.js is a QML library, so its .pragma line is stripped before node
-// evaluates it.
-
-const fs = require("fs");
-const assert = require("assert");
-
-const src = fs.readFileSync(__dirname + "/Place.js", "utf8")
- .replace(/^\.pragma library$/m, "");
-const P = new Function(src + "\nreturn { at };")();
-
-assert.deepStrictEqual(P.at("center", 1920, 1080, 360, 24), { x: 780, y: 360 });
-assert.deepStrictEqual(P.at("top", 1920, 1080, 360, 24), { x: 780, y: 24 });
-assert.deepStrictEqual(P.at("bottom", 1920, 1080, 360, 24), { x: 780, y: 696 });
-assert.deepStrictEqual(P.at("top-left", 1920, 1080, 360, 24), { x: 24, y: 24 });
-assert.deepStrictEqual(P.at("top-right", 1920, 1080, 360, 24), { x: 1536, y: 24 });
-assert.deepStrictEqual(P.at("bottom-left", 1920, 1080, 360, 24), { x: 24, y: 696 });
-assert.deepStrictEqual(P.at("bottom-right", 1920, 1080, 360, 24), { x: 1536, y: 696 });
-// An unknown position is the centre, not an error.
-assert.deepStrictEqual(P.at("middle", 1920, 1080, 360, 24), { x: 780, y: 360 });
-console.log("ok");
diff --git a/desktop/README.md b/desktop/README.md
index aad446c..092baae 100644
--- a/desktop/README.md
+++ b/desktop/README.md
@@ -3,7 +3,8 @@
One drawer, left of DP-1, hosting the things a desktop lets you adjust.
Reached over IPC (`ipc call drawer toggle`), from SUPER pressed and released
alone, a Hyprland release bind.
-The same process also hosts the island, see `island/README.md`.
+The same process also hosts the island, see `island/README.md`, and the
+assistant canvas on SUPER+Home, see `assistant/README.md`.
The drawer opens as a grid of tiles, and clicking a tile either opens that
module's page or fires an action. A module's back arrow, and Escape while a
diff --git a/desktop/modules/ai/README.md b/desktop/modules/ai/README.md
index 7498f06..5e204ca 100644
--- a/desktop/modules/ai/README.md
+++ b/desktop/modules/ai/README.md
@@ -35,8 +35,7 @@ written in the last 5s. A step slower than that reads idle.
| fanfictioner | `pgrep` on `python3 .../fanfictioner` | none | TERM its children, then it, by pid |
Each runs detached, so no exit code comes back; the next poll shows whether it
-worked. `assistant.sh status` fails when either the assistant or its overlay
-is down, so a dead overlay reads as stopped and Start restarts it.
+worked. `assistant.sh status` fails when the daemon is down.
## Service lifetime