1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
|
# Companion seat: design
Date: 2026-10-24. Status: designed, not implemented.
## 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
- **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 centre 60 px right of the screen's centre: the
window stays anchored right and bottom, `margins.bottom = screenH - cardTop
- 35`, `margins.right = screenW / 2 - 60 - seatWidth / 2`. 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. The canvas holds the keyboard
`Exclusive`; Hyprland binds should still fire, since the compositor
matches binds before delivering keys. Checked first, by typing into the
canvas with `qs log` open. If they do not fire, the card's input
`onTextChanged` sets `typingAt`, and a shorter text sets the mock.
## 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.
|