# Companion seat: design Date: 2026-10-24. Status: implemented; the sitting art is generated by the user. ## Goal While the assistant canvas shows its tall card (a chat, or the history, picker or models panel), the companion sits on the card's top edge, right of centre, legs dangling over the empty middle of the header. Typing makes her lean over and read; BackSpace makes her point and smirk from the ledge; thinking, listening and the reply wave happen there too. The bar and the file card do not seat her: she stays in her corner, since standing on a short card would need its own art to read anything below her feet. ## Verified before designing - **Hyprland's typing binds fire under the canvas.** With the canvas open and holding the keyboard `Exclusive`, socket2 carried `custom>>keystone-typing` and `custom>>keystone-delete` for every burst typed into it, so the companion needs no input of the canvas's own. - **The tall card has a fixed top.** `Card.tall` is `mode === "chat" || panel`, and while tall its height is `fullHeight`, set by the window to 70% of the screen height. The card is centred, so its top is `(screenH - fullHeight) / 2` and only changes on entering or leaving the tall state. Nothing needs to follow a moving edge. - **The header's middle is empty.** The model name is on the left, the context meter and buttons on the right, both in a 32 px row inside a 14 px margin. Her legs fall in the gap between them. - **Sitting art exists.** `base-sitting.png` in the art directory is the original rest pose: front view, sitting, legs dangling. `dress/base` was made from it, so the new sitting frames edit it directly. - **Opening the canvas already remaps her.** `openlayer` for `keystone-assistant` unmaps and maps her again above the dim, with a drop out and a drop in. A seat change made while she is unmapped needs no animation of its own. ## Behaviour ### Seat `Assistant.qml` gains `readonly property bool tall`, the same expression as `Card.tall`, and `readonly property real cardTop`, the card's top on its screen. The canvas owns both so the companion does not reach into the card. The companion's `seated` is `asst.open && asst.tall`. Seated, her window is on `asst.screenObj` and its margins put her seat's bottom 35 px below the card's top, with her right edge 90 px right of the screen's centre: the window stays anchored right and bottom, `margins.bottom = screenH - cardTop - 35`, `margins.right = screenW / 2 - 90`. Her right edge rather than her centre, because frames differ in width and the window would move with every frame. Both numbers were checked by eye. Not seated, the margins are today's 24 and 16. `seated` changing runs the monitor hop's path: she drops out, the window moves when the exit has settled, and the entrance plays. The first seat on opening a chat coincides with the remap and is hidden by it. While seated, a focused monitor change does not hop her; on unseating she goes to the focused monitor. Balloons do not shove her while seated: their stack is in a screen corner, not on the card. ### Frames Seated, the frame Mood picks goes through `Mood.seatFrame(frame)`: | Standing | Sitting | | --- | --- | | `watch` | `sit-read`: leaning forward, looking down at the card | | `point` | `sit-point` | | `smirk` | `sit-smirk` | | `think` | `sit-think` | | `listen` | `sit-listen` | | `wave`, `wave-l`, `wave-r` | `sit-wave` | | anything else | `sit` | Cycles (the goodbye wave, dj, headbang) map frame by frame, so the goodbye shows `sit-wave` throughout and dj shows `sit`. `Mood.pick` is unchanged: typing still gives `watch`, delete still gives the mock. Seven new frames in each outfit, from `base-sitting` in `frames.txt`, with a sitting counterpart of `POSE` and the leotard stanzas like the others. `gen-frames` then `prep-frames`, by the user, as with every other frame. A missing frame falls back to `base`, standing, which is how an ungenerated set shows. ### Inputs Typing and BackSpace keep arriving as `keystone-typing` and `keystone-delete` from Hyprland, which fire under the canvas (see above). ## Check `test_mood.js` gains `seatFrame` cases: each row of the table, a cycle frame, and an unknown frame to `sit`. The seat position and the bind check are by hand. ## Not doing - Standing on the bar or the file card: needs new art. - Following the card while it animates: the tall card does not move. - Sitting on ordinary windows.