aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers/plans/2026-10-24-companion-seat.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/superpowers/plans/2026-10-24-companion-seat.md')
-rw-r--r--docs/superpowers/plans/2026-10-24-companion-seat.md383
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>"
+```