diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-24 12:00:28 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-24 12:00:28 +0200 |
| commit | ef75bb03861b4cab59038846fcff7f6137c639b2 (patch) | |
| tree | 5ba9e54c1e3852dc9f531de706eebbfd685cc7cd | |
| parent | 2f800ea58f91faa9bd48d77d45fc00f7dbc24633 (diff) | |
| download | quickshell-ef75bb03861b4cab59038846fcff7f6137c639b2.tar.gz quickshell-ef75bb03861b4cab59038846fcff7f6137c639b2.zip | |
docs: companion seat, implementation plan
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| -rw-r--r-- | docs/superpowers/plans/2026-10-24-companion-seat.md | 383 |
1 files changed, 383 insertions, 0 deletions
diff --git a/docs/superpowers/plans/2026-10-24-companion-seat.md b/docs/superpowers/plans/2026-10-24-companion-seat.md new file mode 100644 index 0000000..e9c902a --- /dev/null +++ b/docs/superpowers/plans/2026-10-24-companion-seat.md @@ -0,0 +1,383 @@ +# Companion seat Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** While the assistant's tall card is up, the companion sits on its top edge and plays sitting versions of her reactions. + +**Architecture:** `Assistant.qml` exposes `tall` and `cardTop`. `Companion.qml` derives `seated`, hops through the existing monitor-hop path to a margin-computed seat, and maps frames through a pure `Mood.seatFrame`. The art is seven new `frames.txt` stanzas per outfit, generated by the user. + +**Tech Stack:** Quickshell 0.3.1 QML, node for `test_mood.js`, `gen-frames`/`prep-frames` for art. + +Spec: `docs/superpowers/specs/2026-10-24-companion-seat-design.md`. + +Rules from AGENTS.md that apply: never start `qs` from a tool call and trust a later `pgrep`; `pkill -x qs` only, never `-f`; check `qs log -p desktop --log-times` against file mtime after each edit, since hot reload sometimes does not fire; visual checks are the user's. + +--- + +### Task 1: Do the typing binds reach Hyprland while the canvas holds the keyboard? + +**Files:** none changed. + +- [ ] **Step 1: Listen on socket2 and type into the canvas** + +Run (agent, foreground, 30 s cap): + +```bash +timeout 30 socat -u UNIX-CONNECT:"$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock" - | grep --line-buffered keystone- +``` + +Ask the user to open the canvas (SUPER+Home), type a few words into it and press BackSpace twice within those 30 s. + +Expected: `custom>>keystone-typing` and `custom>>keystone-delete` lines. If they appear, Task 5 is skipped. If none appear, Task 5 is required. + +### Task 2: `Mood.seatFrame` + +**Files:** +- Modify: `desktop/companion/Mood.js` (append after `canSpeak`) +- Test: `desktop/companion/test_mood.js` + +- [ ] **Step 1: Write the failing test** + +In `test_mood.js`, add `seatFrame` to the returned names: + +```js + "\nreturn { T, click, clickFrame, pick, outfit, mockAllowed, canSpeak, monologue, admireAllowed, seatFrame };")(); +``` + +and before `console.log("ok");`: + +```js +// Seated on the assistant's card: each standing frame to its sitting one, +// the goodbye's sway to the one wave, and anything else to the plain sit. +assert.strictEqual(M.seatFrame("watch"), "sit-read"); +assert.strictEqual(M.seatFrame("point"), "sit-point"); +assert.strictEqual(M.seatFrame("smirk"), "sit-smirk"); +assert.strictEqual(M.seatFrame("think"), "sit-think"); +assert.strictEqual(M.seatFrame("listen"), "sit-listen"); +assert.strictEqual(M.seatFrame("wave"), "sit-wave"); +assert.strictEqual(M.seatFrame("wave-l"), "sit-wave"); +assert.strictEqual(M.seatFrame("wave-r"), "sit-wave"); +assert.strictEqual(M.seatFrame("dj-scratch"), "sit"); +assert.strictEqual(M.seatFrame("base"), "sit"); +``` + +- [ ] **Step 2: Run, expect failure** + +Run: `node desktop/companion/test_mood.js` +Expected: `ReferenceError: seatFrame is not defined`. + +- [ ] **Step 3: Implement** + +Append to `Mood.js`: + +```js +// Seated on the assistant's tall card (Companion.qml, sitting): the frames +// with a sitting version show it, everything else the plain sit. Typing's +// watch becomes the lean over the card, reading. +var SEAT = { + watch: "sit-read", point: "sit-point", smirk: "sit-smirk", think: "sit-think", + listen: "sit-listen", wave: "sit-wave", "wave-l": "sit-wave", "wave-r": "sit-wave", +}; +function seatFrame(frame) { return SEAT[frame] || "sit"; } +``` + +- [ ] **Step 4: Run, expect pass** + +Run: `node desktop/companion/test_mood.js && node desktop/companion/test_lines.js` +Expected: `ok` twice. + +- [ ] **Step 5: Commit** + +```bash +git add desktop/companion/Mood.js desktop/companion/test_mood.js +git commit -m "companion: seatFrame, the sitting version of a frame + +Pure, so the seat's frame choice is tested with the rest of Mood rather +than by eye. Frames without a sitting version fall to the plain sit: a +standing frame on the card's edge would float above it. + +Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>" +``` + +### Task 3: The canvas says when it is tall and where the card's top is + +**Files:** +- Modify: `desktop/assistant/Assistant.qml` (near `screenObj`, line ~112, and the `Card {}` at ~552) +- Modify: `desktop/assistant/Card.qml:31-32` + +- [ ] **Step 1: Add the properties to `Assistant.qml`**, after `screenObj`: + +```qml + // The tall card (a chat, or a panel) is fullHeight and centred, so its + // top only moves on entering or leaving tall. The companion sits there. + readonly property bool tall: root.mode === "chat" || root.historyOpen || root.pickerOpen || root.modelsOpen + readonly property int fullHeight: Math.round(root.screenObj.height * 0.7) + readonly property int cardTop: Math.round((root.screenObj.height - root.fullHeight) / 2) +``` + +and in the `Card {}` replace `fullHeight: Math.round(parent.height * 0.7)` with: + +```qml + fullHeight: root.fullHeight +``` + +- [ ] **Step 2: `Card.qml` reads `tall` from the canvas**, replacing line 32: + +```qml + readonly property bool tall: card.canvas.tall +``` + +(`panel` on line 31 stays; other code in the card uses it.) + +- [ ] **Step 3: Check the reload** + +Run: `qs log -p desktop --log-times | tail -5` and `stat -c %y desktop/assistant/Assistant.qml` +Expected: a `Reloading configuration` line after the mtime, no `TypeError` or `ReferenceError`. Ask the user to open a chat and confirm the card is the same size as before. + +- [ ] **Step 4: Commit** + +```bash +git add desktop/assistant/Assistant.qml desktop/assistant/Card.qml +git commit -m "assistant: the canvas owns tall and the card's top + +The companion needs both to sit on the chat card, and should not reach +into the card for them. Card.tall now reads the canvas's, so there is one +definition of when the card is tall. + +Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>" +``` + +### Task 4: She sits + +**Files:** +- Modify: `desktop/companion/Companion.qml` (lines 66-83 the monitor block and margins; 295-300 `shoved`; 455-456 `frame`; 466-472 `settled`) + +- [ ] **Step 1: Replace the monitor block (lines 66-79)** with: + +```qml + // Where she goes: the focused monitor's corner, or seated on the + // assistant's tall card. here and sitting are where the window is now + // and trail the goal: a change plays her exit first, and only then + // moves the window (settled, below). + readonly property string focused: Hyprland.focusedMonitor?.name ?? "" + readonly property bool seated: root.asst.open && root.asst.tall + readonly property string goal: root.seated ? root.asst.monitor : root.focused + property string here: "" + property bool sitting: false + property bool hopping: false + // No focused monitor (outputs gone over a suspend) is not a destination. + // Already out of view, there is no exit to wait for: settled would never + // fire, hopping would stick and hide her for good, so move at once. + function move() { + if (root.goal === "" || (root.goal === root.here && root.seated === root.sitting)) return; + if (root.here === "" || root.away) { + root.here = root.goal; + root.sitting = root.seated; + } else { + root.hopping = true; + } + } + onGoalChanged: root.move() + onSeatedChanged: root.move() + Component.onCompleted: root.move() +``` + +- [ ] **Step 2: Margins (line 83)** become: + +```qml + // Seated, her right edge is 90 px right of the screen's centre, which + // puts her in the gap in the card's header, and her feet 35 px below the + // card's top. The right edge rather than her centre, because frames + // differ in width and the window would shift with every frame. + margins { + right: root.sitting ? Math.round(root.asst.screenObj.width / 2 - 90) : 24 + bottom: root.sitting ? root.asst.screenObj.height - root.asst.cardTop - 35 : 16 + } +``` + +- [ ] **Step 3: No shove while seated.** First line of the `shoved` binding: + +```qml + if (root.sitting) return false; +``` + +- [ ] **Step 4: Sitting frames.** Replace the `frame` property: + +```qml + readonly property string standing: root.leaving ? root.waveCycle[root.waveIndex] + : root.cycle ? root.cycle.frames[root.cycleIndex % root.cycle.frames.length] : root.mood.frame + readonly property string frame: root.sitting ? Mood.seatFrame(root.standing) : root.standing +``` + +- [ ] **Step 5: `settled` applies the seat too:** + +```qml + // The exit has played: move, and the entrance plays. The goal may have + // changed during the exit; the latest one wins. + function settled(out) { + if (out && root.hopping) { + root.here = root.goal; + root.sitting = root.seated; + root.hopping = false; + } + } +``` + +- [ ] **Step 6: Check the reload** as in Task 3 Step 3 (file `desktop/companion/Companion.qml`). + +- [ ] **Step 7: Hand check, by the user.** No sit art exists yet, so she shows `base` standing on the ledge (the fallback). Ask the user: + 1. Open the canvas: she stays in the corner over the dim. + 2. Start a chat: she drops out and lands on the card's top edge, right of centre, feet a little below the edge. + 3. Close the canvas: she hops back to the corner of the focused monitor. + 4. With a chat open, focus another monitor: she stays on the card. + 5. With a chat open, a notification balloon: she does not move. + +Adjust the 90 and 35 to what the user reports, then commit. + +- [ ] **Step 8: Commit** + +```bash +git add desktop/companion/Companion.qml +git commit -m "companion: sit on the assistant's chat card + +While the canvas shows its tall card she hops onto its top edge, through +the monitor hop: the exit plays, the window's margins move, the entrance +plays. The tall card's top is fixed, so nothing follows a moving edge. +Seated she ignores focus changes and balloons, and shows Mood.seatFrame +of whatever she would show standing. + +Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>" +``` + +### Task 5 (only if Task 1 saw no events): typing from the card's input + +**Files:** +- Modify: `desktop/assistant/Assistant.qml` (signal next to `signal replied()`, line 91) +- Modify: `desktop/assistant/Card.qml` (the `input` TextEdit, near line 501) +- Modify: `desktop/companion/Companion.qml` (the `Connections { target: root.asst }` block) + +- [ ] **Step 1:** In `Assistant.qml` add `signal typed(bool deleted)` after `signal replied()`. + +- [ ] **Step 2:** On the card's `input`, add: + +```qml + property int lastLength: 0 + onTextChanged: { + card.canvas.typed(text.length < lastLength); + lastLength = text.length; + } +``` + +- [ ] **Step 3:** In the companion's `Connections { target: root.asst }`, add: + +```qml + // Hyprland's binds do not fire while the canvas holds the keyboard, + // so the card's input reports typing itself. + function onTyped(deleted) { + const t = Date.now(); + root.typingAt = t; + if (deleted && Mood.mockAllowed(root.mockAt, t)) { + root.mockAt = t; + root.say("mock"); + } + } +``` + +- [ ] **Step 4:** Check the reload, ask the user to type and delete in a chat, then commit: + +```bash +git add desktop/assistant/Assistant.qml desktop/assistant/Card.qml desktop/companion/Companion.qml +git commit -m "companion: typing in the canvas comes from its input + +The canvas holds the keyboard exclusively and Hyprland's typing binds do +not fire under it (checked on socket2), so the card's input reports +typing and deletes itself. + +Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>" +``` + +### Task 6: The sitting frames + +**Files:** +- Modify: `desktop/companion/frames.txt` (a `SIT` macro after `SING`; a `# --- seated` group before the leotard set; leotard lines at the end) + +- [ ] **Step 1: Add the macro** after the `SING = ...` line: + +``` +# Seated on the assistant's card: the original sitting rest pose, legs +# dangling over the edge, from base-sitting (the image dress/base came from). +SIT = keep her sitting pose exactly: sitting on a ledge, front view, legs dangling down over its edge. +``` + +- [ ] **Step 2: The dress frames**, before `# --- the leotard set`: + +``` +# --- seated, on the assistant's chat card (Mood.seatFrame) +dress/sit <- base-sitting : ${SIT} Hands resting on the ledge beside her, a calm closed mouth. ${EYES} ${KEEP} +dress/sit-read <- dress/sit : She leans forward from the waist and tilts her head down, looking at something just below her feet, as if reading over the edge; hands still on the ledge. ${SIT} ${EYES} ${KEEP} +dress/sit-point <- dress/sit : She points down past her knees with the hand on the left side of the image, a teasing smirk; her other hand stays on the ledge. ${SIT} ${KEEP} +dress/sit-smirk <- dress/sit : Change only her expression to a teasing one-sided smirk. ${SIT} Both eyes fully visible and uncovered; the blindfold is nowhere on her head or face. ${BARE} ${KEEP_BARE} +dress/sit-think <- dress/sit : One gloved hand at her chin, thinking, head tilted slightly; her other hand on the ledge. ${SIT} ${KEEP} +dress/sit-listen <- dress/sit : She cups the hand on the left side of the image behind her ear, listening attentively; her other hand on the ledge. ${SIT} ${KEEP} +dress/sit-wave <- dress/sit : Raise only the hand on the left side of the image beside her head, waving, with a small smile; her other hand stays on the ledge. ${SIT} ${KEEP} +``` + +- [ ] **Step 3: The leotard frames**, at the end of the leotard set: + +``` +leotard/sit <- dress/sit : ${LEOTARD} +leotard/sit-read <- dress/sit-read : ${LEOTARD} +leotard/sit-point <- dress/sit-point : ${LEOTARD} +leotard/sit-smirk <- dress/sit-smirk : ${LEOTARD} +leotard/sit-think <- dress/sit-think : ${LEOTARD} +leotard/sit-listen <- dress/sit-listen : ${LEOTARD} +leotard/sit-wave <- dress/sit-wave : ${LEOTARD} +``` + +- [ ] **Step 4: Dry run** + +Run: `desktop/companion/gen-frames --list` +Expected: exactly the 14 new outputs listed, no existing frame. + +- [ ] **Step 5: Commit** + +```bash +git add desktop/companion/frames.txt +git commit -m "companion: the seated frames' recipes + +Edited from base-sitting, the original sitting rest pose dress/base came +from, so the legs already dangle and klein only changes the arms and +expression. The leotard set follows its dress frames as before. + +Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>" +``` + +- [ ] **Step 6: Generation, by the user**: start sd-webui with FLUX.2 klein, run `desktop/companion/gen-frames`, and review the frames. Rerolls go by `--seed N dress/sit-x leotard/sit-x`, recorded in `frames.txt` comments as for headbang-down. + +### Task 7: Docs + +**Files:** +- Modify: `desktop/companion/README.md` (a bullet in Inputs, after the openlayer one) +- Modify: `docs/superpowers/specs/2026-10-24-companion-seat-design.md` (status line; the seat's x is the right edge, per Task 4) + +- [ ] **Step 1: README bullet:** + +```markdown +- The assistant's tall card (a chat, or its history, picker or models + panel) seats her on its top edge, right of centre, through the monitor + hop. `Assistant.qml` gives `tall` and `cardTop`; seated she ignores + focus changes and balloons and shows `Mood.seatFrame` of her frame, the + `sit-*` set. The bar and the file card leave her in the corner: + standing frames face out, not down at the text. +``` + +- [ ] **Step 2: Spec:** status to `implemented`, and in Seat replace "with her centre 60 px right of the screen's centre ... `seatWidth / 2`" with "with her right edge 90 px right of the screen's centre (`margins.right = screenW / 2 - 90`), since frames differ in width", using the final numbers from Task 4 Step 7. Add Task 1's result to "Verified before designing". + +- [ ] **Step 3: Commit** + +```bash +git add desktop/companion/README.md docs/superpowers/specs/2026-10-24-companion-seat-design.md +git commit -m "docs: the companion's seat on the assistant card + +Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>" +``` |
