From 5f11f9c12f1f7e6fc849f409a9e4a964a3c1853e Mon Sep 17 00:00:00 2001 From: BP <11394934+benjipeng@users.noreply.github.com> Date: Wed, 23 Sep 2026 13:43:34 -0400 Subject: [PATCH] feat(tui): let the activity mark grow from a point, turn, and spin out The user found the mark rigid: it changed shape at one size, in even steps, back along its own path. It now grows from a dot into a circle, turns into a rounded square and a square, spins into a diamond and shrinks away through a smaller one, passing quickly through the small shapes and lingering on the whole ones. The frames hold unequal numbers of the motion clock's phases, 24 in all, so the cycle is still 1.6 s and the clock is unchanged. Every glyph is from the Nerd Font's Material Design family, whose small and medium sizes share one centre, so growing does not shake. While approval waits the mark stands still as the whole circle, since the first frame is too small to be seen waiting. The user chose H from four kitty previews of livelier variants; the spike records the candidates, the measured glyph sizes and centres, and keeps the preview. Frame fixtures change only in the mark's phase-zero glyph. --- .agents/specs/composer.md | 13 ++-- .agents/specs/motion.md | 2 +- .agents/spikes/conversation-chrome/README.md | 16 +++- .../mockups/mark-lively.py | 77 ++++++++++++++++++ .../frames/agents-strip-narrow.txt | 2 +- .../frames/agents-strip-one-column.txt | 2 +- .../frames/agents-strip-tally.txt | 2 +- .../frames/agents-strip-two-columns.txt | 2 +- .../plexmaton-tui/frames/approval-medium.txt | 2 +- crates/plexmaton-tui/frames/approval-wide.txt | 2 +- .../plexmaton-tui/frames/canonical-medium.txt | 2 +- .../plexmaton-tui/frames/canonical-narrow.txt | 2 +- .../plexmaton-tui/frames/canonical-wide.txt | 2 +- .../frames/current-work-responding-medium.txt | 2 +- .../frames/current-work-responding-narrow.txt | 2 +- .../frames/current-work-responding-wide.txt | 2 +- .../current-work-running-tool-medium.txt | 2 +- .../current-work-running-tool-narrow.txt | 2 +- .../frames/current-work-running-tool-wide.txt | 2 +- crates/plexmaton-tui/frames/drawer-medium.txt | 2 +- crates/plexmaton-tui/frames/drawer-narrow.txt | 2 +- .../plexmaton-tui/frames/drawer-ultrawide.txt | 2 +- crates/plexmaton-tui/frames/drawer-wide.txt | 2 +- .../frames/input-queue-medium.txt | 2 +- .../frames/input-queue-narrow.txt | 2 +- .../plexmaton-tui/frames/input-queue-wide.txt | 2 +- .../frames/server-tool-medium.txt | 2 +- .../frames/server-tool-narrow.txt | 2 +- .../frames/server-tool-open-wide.txt | 2 +- .../plexmaton-tui/frames/server-tool-wide.txt | 2 +- .../frames/skill-diagnostic-medium.txt | 2 +- .../frames/skill-diagnostic-narrow.txt | 2 +- .../frames/skill-diagnostic-wide.txt | 2 +- .../plexmaton-tui/frames/tool-open-medium.txt | 2 +- .../plexmaton-tui/frames/tool-open-narrow.txt | 2 +- .../plexmaton-tui/frames/tool-open-wide.txt | 2 +- crates/plexmaton-tui/src/frames.rs | 2 +- crates/plexmaton-tui/src/render/activity.rs | 78 +++++++++++++------ crates/plexmaton-tui/src/render/mod.rs | 6 +- 39 files changed, 188 insertions(+), 70 deletions(-) create mode 100644 .agents/spikes/conversation-chrome/mockups/mark-lively.py diff --git a/.agents/specs/composer.md b/.agents/specs/composer.md index 5ce2e01..fae1cd2 100644 --- a/.agents/specs/composer.md +++ b/.agents/specs/composer.md @@ -56,13 +56,16 @@ instant, so they never move the semantic revision. Elapsed counts the primary's opens until it ends: it carries through the gaps between steps and stops while an approval or a question holds the turn on the user. Quiet appears after five seconds with nothing from the primary's own events, never a child's, and says nothing while the turn waits on the user. A short -row drops effort, then quiet, then elapsed. The row opens with the mark: the Nerd Font's circle, -rounded square and square, outline then filled, in blue for the agent's own work and the -server-tool colour while a provider-run search is the work, standing still in the action-required -colour while approval is required; it moves on the one visible-only clock (MOT-1, MOT-3). The +row drops effort, then quiet, then elapsed. The row opens with the mark, from the Nerd Font's one +Material Design family: a dot grows into a circle, turns into a rounded square and a square, spins +into a diamond and shrinks away through a smaller one, lingering on the whole shapes. It is blue +for the agent's own work and the server-tool colour while a provider-run search is the work, and +stands still as the whole circle in the action-required colour while approval is required; it +moves on the one visible-only clock (MOT-1, MOT-3). The row's right end holds the selection note (SEL-2) and the attention pill (ATT-1); while its rows are closed the selection note rides the status row instead (ui-ux §input). Rejected: current work on -the composer's rule, which mixed the agent's doing with the user's typing. +the composer's rule, which mixed the agent's doing with the user's typing; and a mark that changed +shape at one size, or stepped evenly back along its own path, which the user found rigid. **COM-6 — Input selection names editable source.** A click places the caret; dragging retains a grapheme-boundary anchor, paints the source range, and release copies it through SEL-4. Typing or diff --git a/.agents/specs/motion.md b/.agents/specs/motion.md index de0858d..37a6ba2 100644 --- a/.agents/specs/motion.md +++ b/.agents/specs/motion.md @@ -27,4 +27,4 @@ clock was doing. The phase is a count, not a time: renderers derive their frame from it, so two moving things on screen stay in step. The effort rail's marker and the activity line's mark each complete a cycle every -1.6 s, the mark at three phases a frame over eight frames. +1.6 s, the mark over seven frames holding unequal numbers of its 24 phases. diff --git a/.agents/spikes/conversation-chrome/README.md b/.agents/spikes/conversation-chrome/README.md index a64ddb8..2c96da8 100644 --- a/.agents/spikes/conversation-chrome/README.md +++ b/.agents/spikes/conversation-chrome/README.md @@ -9,14 +9,14 @@ Every decision below was made by the user on a rendered frame, 2026-09-21 and 2026-09-22. The generators are in [`mockups/`](./mockups/); each writes one HTML page to `plexmaton-conversation-chrome/` under the system temporary directory, outside this corpus, and -`spin.py`, `logo_term.py` and `cellsize.py` run in a terminal. Regenerate rather than trusting a description of them. +`spin.py`, `mark-lively.py`, `logo_term.py` and `cellsize.py` run in a terminal. Regenerate rather than trusting a description of them. ## What the user chose | Surface | Chosen | Rejected on sight | | --- | --- | --- | | Activity line composition | mark, label, muted `· 32s · high effort`, `· quiet for 31s` once nothing has arrived; no token count | a red stall tell; estimated tokens | -| Activity mark | N2, the Nerd Font's circle, rounded square and square, outline then filled: chosen 2026-09-22 in a kitty window beside the Unicode cycles; the user asked for the filled half to keep it simple yet ever changing | C2′ and C2″, which shook in the user's terminal because mixed-size Unicode shapes centre differently; filled squares; braille; quarter circles; Claude Code's star | +| Activity mark | H: a dot grows into a circle, turns into a rounded square and a square, spins into a diamond and shrinks away through a smaller one, lingering on whole shapes, 1.6 s a cycle; chosen 2026-09-23 in kitty (`mark-lively.py`) after the user found N2 rigid and wanted it to grow from a point | N2, the circle, rounded square and square outline then filled, chosen 2026-09-22 and later found rigid; one size changing shape, and even steps retracing their path, likewise; a slice-by-slice sweep, which reads as progress; C2′ and C2″, which shook because mixed-size Unicode shapes centre differently; filled squares; braille; quarter circles; Claude Code's star | | User message | band of lifted ground with a blue `›`, continuation indented | the accent bar; a bare prefix; a hairline bar; a band alone | | Mark placement | centred in the empty conversation, name beneath, gone with the first message | a header or drawer slot, not yet asked for | | Mark motion | K4 as the base: frame breathes between weights and drifts between palette slots; centre morphs circle, ring, rounded square, square, solid and outline | K1 to K3 as they stood | @@ -40,8 +40,16 @@ kitty answers `CSI 16 t` with the cell in pixels; `cellsize.py` shows the reply Material Design glyphs used, all present at one advance in Sarasa Term SC Nerd and in Symbols Nerd Font Mono: `circle-small` F09DF, `circle-medium` F09DE, `circle` F0765, `circle-outline` F0766, `square` F0763, `square-outline` F0764, `square-rounded` F14FB, `square-rounded-outline` F14FC, -`radiobox-marked` F043E, `record-circle` F0FEC, `circle-double` F0E95. The product already draws -its header, tally and copy glyphs from this set. +`radiobox-marked` F043E, `record-circle` F0FEC, `circle-double` F0E95, `rhombus` F070B, +`rhombus-medium` F0A10, `square-small` F0A15, `square-medium` F0A13. The product already draws its +header, tally and copy glyphs from this set. + +Measured in Maple Mono NF CN, in units of a 1000-unit em on a 600-unit cell: the family's filled +shapes come at 168, 332 and 461 (the medium diamond), then 750 to 832, with nothing between, so a +mark grows in steps rather than smoothly. Its small and medium sizes and its squares share one +centre; the whole circle and diamond sit 41 units right of it, an offset nobody has seen move. +Maple Mono's own `·`, `•`, `▪`, `●` and `■` centre on the cell instead, 75 to 116 units left of +the family's, so they cannot join its cycle; `▢`, `◼` and `⬤` are not in the font and fall back. ## What the other CLIs do, read from their source diff --git a/.agents/spikes/conversation-chrome/mockups/mark-lively.py b/.agents/spikes/conversation-chrome/mockups/mark-lively.py new file mode 100644 index 0000000..1854e9f --- /dev/null +++ b/.agents/spikes/conversation-chrome/mockups/mark-lively.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""C made lively: growth, a turn and a sweep, from one Material Design family. Ctrl-C to stop. + +Every glyph here shares the family's centring, and each frame lasts a whole number of the motion +clock's 67 ms phases; cycles are 24 phases (1.6 s) or 16 (1.07 s), both of which the product's +clock can drive unchanged. +""" +import sys, time + +TEXT, BLUE, MUTED, RESET, REV = ("\x1b[38;2;230;233;240m", "\x1b[38;2;130;180;240m", + "\x1b[38;2;142;162;196m", "\x1b[0m", "\x1b[7m") +PHASE_MS = 1000 / 15 +G = dict(c_small="\U000F09DF", c_medium="\U000F09DE", circle="\U000F0765", + square="\U000F0763", rounded="\U000F14FB", + rhombus="\U000F070B", r_medium="\U000F0A10") +G.update({f"slice{n}": chr(0xF0A9D + n) for n in range(1, 9)}) + +SLICES = [(f"slice{n}", 1) for n in range(1, 9)] +CANDIDATES = [ + ("E liked: C with pace", [("c_small", 3), ("c_medium", 1), ("circle", 5), ("rounded", 2), + ("square", 5), ("rounded", 2), ("circle", 5), ("c_medium", 1)]), + ("H grow, turn, spin out · 1.6 s", [("c_small", 3), ("c_medium", 1), ("circle", 6), ("rounded", 2), + ("square", 5), ("rhombus", 4), ("r_medium", 3)]), + ("K spark · 1.6 s", [("c_small", 3), ("c_medium", 1), ("r_medium", 2), ("rhombus", 4), + ("square", 4), ("rounded", 2), ("circle", 5), ("c_medium", 3)]), + ("I sweep · 1.6 s", [("c_small", 4), ("c_medium", 1), *SLICES, ("circle", 8), ("c_medium", 3)]), + ("H' grow, turn, spin out · 1.07 s", [("c_small", 2), ("c_medium", 1), ("circle", 4), ("rounded", 1), + ("square", 3), ("rhombus", 3), ("r_medium", 2)]), + ("K' spark · 1.07 s", [("c_small", 2), ("c_medium", 1), ("r_medium", 1), ("rhombus", 3), + ("square", 3), ("rounded", 1), ("circle", 3), ("c_medium", 2)]), + ("I' sweep · 1.07 s", [("c_small", 2), ("c_medium", 1), *SLICES, ("circle", 4), ("c_medium", 1)]), +] + + +def frame_at(frames, phase): + phase %= sum(hold for _, hold in frames) + for glyph, hold in frames: + if phase < hold: + return glyph + phase -= hold + raise AssertionError("unreachable") + + +def strip(): + print(f"\n{TEXT}Frames in order, each in its own cell; the number is how many 67 ms phases it holds:{RESET}\n") + for name, frames in CANDIDATES: + cells = " ".join(f"{REV}{G[glyph]}{RESET}{MUTED}{hold}{RESET}" for glyph, hold in frames) + print(f" {MUTED}{name:38}{RESET} {cells}") + print(f"\n{TEXT}As the activity line:{RESET}\n") + + +def animate(): + sys.stdout.write("\x1b[?25l") + start = time.time() + try: + while True: + elapsed_ms = (time.time() - start) * 1000 + phase = int(elapsed_ms // PHASE_MS) + for name, frames in CANDIDATES: + glyph = G[frame_at(frames, phase)] + seconds = 12 + int(elapsed_ms // 1000) + sys.stdout.write(f" {BLUE}{glyph}{RESET} {TEXT}Thinking…{RESET}" + f"{MUTED} · {seconds}s · max effort{RESET} {MUTED}{name}{RESET}\x1b[K\n\n") + sys.stdout.write(f"\x1b[{2 * len(CANDIDATES)}A") + sys.stdout.flush() + time.sleep(0.01) + except KeyboardInterrupt: + pass + finally: + sys.stdout.write(f"\x1b[{2 * len(CANDIDATES)}B\x1b[?25h{RESET}\n") + + +if __name__ == "__main__": + for name, frames in CANDIDATES: + assert sum(hold for _, hold in frames) in (16, 24), name + strip() + animate() diff --git a/crates/plexmaton-tui/frames/agents-strip-narrow.txt b/crates/plexmaton-tui/frames/agents-strip-narrow.txt index cc21227..08bab43 100644 --- a/crates/plexmaton-tui/frames/agents-strip-narrow.txt +++ b/crates/plexmaton-tui/frames/agents-strip-narrow.txt @@ -16,7 +16,7 @@ │transcript remains interactive. │ │ │ │received from Agent B · UI study · Routing stays centrali…│ -│󰝦 Thinking… ( !2 ) │ +│󰧟 Thinking… ( !2 ) │ │ │ ── Message ───────────────────────────────────────────────── Type a message · ⇥ to focus diff --git a/crates/plexmaton-tui/frames/agents-strip-one-column.txt b/crates/plexmaton-tui/frames/agents-strip-one-column.txt index 5d4a6f7..ab4d516 100644 --- a/crates/plexmaton-tui/frames/agents-strip-one-column.txt +++ b/crates/plexmaton-tui/frames/agents-strip-one-column.txt @@ -16,7 +16,7 @@ │independently; this transcript remains interactive. │ │ │ │received from Agent B · UI study · Routing stays centralized and z-ordered. │ -│󰝦 Thinking… ( !2 ) │ +│󰧟 Thinking… ( !2 ) │ │ │ ── Message ──────────────────────────────────────────────────────────────────────────────────── Type a message · ⇥ to focus diff --git a/crates/plexmaton-tui/frames/agents-strip-tally.txt b/crates/plexmaton-tui/frames/agents-strip-tally.txt index 447c368..2acc931 100644 --- a/crates/plexmaton-tui/frames/agents-strip-tally.txt +++ b/crates/plexmaton-tui/frames/agents-strip-tally.txt @@ -12,7 +12,7 @@ │independently; this transcript remains interactive. │ │ │ │received from Agent B · UI study · Routing stays centralized and z-ordered. │ -│󰝦 Thinking… │ +│󰧟 Thinking… │ │ │ ── Message ──────────────────────────────────────────────────────────────────────────────────── Type a message · ⇥ to focus diff --git a/crates/plexmaton-tui/frames/agents-strip-two-columns.txt b/crates/plexmaton-tui/frames/agents-strip-two-columns.txt index 6c5b002..518646e 100644 --- a/crates/plexmaton-tui/frames/agents-strip-two-columns.txt +++ b/crates/plexmaton-tui/frames/agents-strip-two-columns.txt @@ -16,7 +16,7 @@ │interactive. ││ │ │ ││ │ │received from Agent B · UI study · Routing stays centralized and z-…││ │ -│󰝦 Thinking… ( !2 ) ││ │ +│󰧟 Thinking… ( !2 ) ││ │ │ ││ │ ── Message ───────────────────────────────────────────────────────────│ │ Type a message · ⇥ to focus │Controller unavailable · input locked │ diff --git a/crates/plexmaton-tui/frames/approval-medium.txt b/crates/plexmaton-tui/frames/approval-medium.txt index f8a7e70..dd6209a 100644 --- a/crates/plexmaton-tui/frames/approval-medium.txt +++ b/crates/plexmaton-tui/frames/approval-medium.txt @@ -21,7 +21,7 @@ │independently; this transcript remains interactive. │ │ │ │received from Agent B · UI study · Routing stays centralized and z-ordered. │ -│󰝦 Thinking… ( !1 ) │ +│󰧟 Thinking… ( !1 ) │ │ │ ── Approval required · edit ─────────────────────────────────────────────────────────────────── diff --git a/crates/plexmaton-tui/frames/approval-wide.txt b/crates/plexmaton-tui/frames/approval-wide.txt index 13c5cc4..834963e 100644 --- a/crates/plexmaton-tui/frames/approval-wide.txt +++ b/crates/plexmaton-tui/frames/approval-wide.txt @@ -21,7 +21,7 @@ │transcript remains interactive. ││ │ │ ││ │ │received from Agent B · UI study · Routing stays centrali…││ │ -│󰝦 Thinking… ( !1 ) ││ │ +│󰧟 Thinking… ( !1 ) ││ │ │ ││ │ ── Approval required · edit ────────────────────────────────│ │ │ │ diff --git a/crates/plexmaton-tui/frames/canonical-medium.txt b/crates/plexmaton-tui/frames/canonical-medium.txt index 7494f77..7199798 100644 --- a/crates/plexmaton-tui/frames/canonical-medium.txt +++ b/crates/plexmaton-tui/frames/canonical-medium.txt @@ -32,7 +32,7 @@ │independently; this transcript remains interactive. │ │ │ │received from Agent B · UI study · Routing stays centralized and z-ordered. │ -│󰝦 Thinking… ( !1 ) │ +│󰧟 Thinking… ( !1 ) │ │ │ ── Message ──────────────────────────────────────────────────────────────────────────────────── Type a message · ⇥ to focus diff --git a/crates/plexmaton-tui/frames/canonical-narrow.txt b/crates/plexmaton-tui/frames/canonical-narrow.txt index 8692371..907e5b4 100644 --- a/crates/plexmaton-tui/frames/canonical-narrow.txt +++ b/crates/plexmaton-tui/frames/canonical-narrow.txt @@ -32,7 +32,7 @@ │transcript remains interactive. │ │ │ │received from Agent B · UI study · Routing stays centrali…│ -│󰝦 Thinking… ( !1 ) │ +│󰧟 Thinking… ( !1 ) │ │ │ ── Message ───────────────────────────────────────────────── Type a message · ⇥ to focus diff --git a/crates/plexmaton-tui/frames/canonical-wide.txt b/crates/plexmaton-tui/frames/canonical-wide.txt index 36e9ad4..12852bc 100644 --- a/crates/plexmaton-tui/frames/canonical-wide.txt +++ b/crates/plexmaton-tui/frames/canonical-wide.txt @@ -32,7 +32,7 @@ │remains interactive. │ │ │ │received from Agent B · UI study · Routing stays centralized and z-ordered. │ -│󰝦 Thinking… ( !1 ) │ +│󰧟 Thinking… ( !1 ) │ │ │ ── Message ───────────────────────────────────────────────────────────────────────────────────────────────────────────── Type a message · ⇥ to focus diff --git a/crates/plexmaton-tui/frames/current-work-responding-medium.txt b/crates/plexmaton-tui/frames/current-work-responding-medium.txt index e2b6a7c..409397f 100644 --- a/crates/plexmaton-tui/frames/current-work-responding-medium.txt +++ b/crates/plexmaton-tui/frames/current-work-responding-medium.txt @@ -1,4 +1,4 @@ -│󰝦 Responding… ( !1 ) │ +│󰧟 Responding… ( !1 ) │ │ │ ── Message ──────────────────────────────────────────────────────────────────────────────────── Type a message · ⇥ to focus diff --git a/crates/plexmaton-tui/frames/current-work-responding-narrow.txt b/crates/plexmaton-tui/frames/current-work-responding-narrow.txt index ca724fc..a6b78b6 100644 --- a/crates/plexmaton-tui/frames/current-work-responding-narrow.txt +++ b/crates/plexmaton-tui/frames/current-work-responding-narrow.txt @@ -1,4 +1,4 @@ -│󰝦 Responding… ( !1 ) │ +│󰧟 Responding… ( !1 ) │ │ │ ── Message ───────────────────────────────────────────────── Type a message · ⇥ to focus diff --git a/crates/plexmaton-tui/frames/current-work-responding-wide.txt b/crates/plexmaton-tui/frames/current-work-responding-wide.txt index 9a60619..1b31b60 100644 --- a/crates/plexmaton-tui/frames/current-work-responding-wide.txt +++ b/crates/plexmaton-tui/frames/current-work-responding-wide.txt @@ -1,4 +1,4 @@ -│󰝦 Responding… ( !1 ) │ +│󰧟 Responding… ( !1 ) │ │ │ ── Message ───────────────────────────────────────────────────────────────────────────────────────────────────────────── Type a message · ⇥ to focus diff --git a/crates/plexmaton-tui/frames/current-work-running-tool-medium.txt b/crates/plexmaton-tui/frames/current-work-running-tool-medium.txt index 18ba765..271e76d 100644 --- a/crates/plexmaton-tui/frames/current-work-running-tool-medium.txt +++ b/crates/plexmaton-tui/frames/current-work-running-tool-medium.txt @@ -1,4 +1,4 @@ -│󰝦 Running read_file… ( !1 ) │ +│󰧟 Running read_file… ( !1 ) │ │ │ ── Message ──────────────────────────────────────────────────────────────────────────────────── Type a message · ⇥ to focus diff --git a/crates/plexmaton-tui/frames/current-work-running-tool-narrow.txt b/crates/plexmaton-tui/frames/current-work-running-tool-narrow.txt index 46bcb5f..8b9820a 100644 --- a/crates/plexmaton-tui/frames/current-work-running-tool-narrow.txt +++ b/crates/plexmaton-tui/frames/current-work-running-tool-narrow.txt @@ -1,4 +1,4 @@ -│󰝦 Running read_file… ( !1 ) │ +│󰧟 Running read_file… ( !1 ) │ │ │ ── Message ───────────────────────────────────────────────── Type a message · ⇥ to focus diff --git a/crates/plexmaton-tui/frames/current-work-running-tool-wide.txt b/crates/plexmaton-tui/frames/current-work-running-tool-wide.txt index 2acc265..7219b59 100644 --- a/crates/plexmaton-tui/frames/current-work-running-tool-wide.txt +++ b/crates/plexmaton-tui/frames/current-work-running-tool-wide.txt @@ -1,4 +1,4 @@ -│󰝦 Running read_file… ( !1 ) │ +│󰧟 Running read_file… ( !1 ) │ │ │ ── Message ───────────────────────────────────────────────────────────────────────────────────────────────────────────── Type a message · ⇥ to focus diff --git a/crates/plexmaton-tui/frames/drawer-medium.txt b/crates/plexmaton-tui/frames/drawer-medium.txt index e7e473e..8524b7d 100644 --- a/crates/plexmaton-tui/frames/drawer-medium.txt +++ b/crates/plexmaton-tui/frames/drawer-medium.txt @@ -32,7 +32,7 @@ │independently; this transcript remains interactive. │ │ │ │received from Agent B · UI study · Routing stays centralized and z-ordered. │ -│󰝦 Thinking… ( !1 ) │ +│󰧟 Thinking… ( !1 ) │ │ │ ── Message ──────────────────────────────────────────────────────────────────────────────────── Type a message · ⇥ to focus diff --git a/crates/plexmaton-tui/frames/drawer-narrow.txt b/crates/plexmaton-tui/frames/drawer-narrow.txt index a748154..549563d 100644 --- a/crates/plexmaton-tui/frames/drawer-narrow.txt +++ b/crates/plexmaton-tui/frames/drawer-narrow.txt @@ -32,7 +32,7 @@ │transcript remains interactive. │ │ │ │received from Agent B · UI study · Routing stays centrali…│ -│󰝦 Thinking… ( !1 ) │ +│󰧟 Thinking… ( !1 ) │ │ │ ── Message ───────────────────────────────────────────────── Type a message · ⇥ to focus diff --git a/crates/plexmaton-tui/frames/drawer-ultrawide.txt b/crates/plexmaton-tui/frames/drawer-ultrawide.txt index f738933..20008de 100644 --- a/crates/plexmaton-tui/frames/drawer-ultrawide.txt +++ b/crates/plexmaton-tui/frames/drawer-ultrawide.txt @@ -32,7 +32,7 @@ │I will inspect the workspace and delegate the focused UI study. Agent B is running independently; this transcript remains interactive. │ │ │ │received from Agent B · UI study · Routing stays centralized and z-ordered. │ -│󰝦 Thinking… ( !1 ) │ +│󰧟 Thinking… ( !1 ) │ │ │ ── Message ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Type a message · ⇥ to focus diff --git a/crates/plexmaton-tui/frames/drawer-wide.txt b/crates/plexmaton-tui/frames/drawer-wide.txt index b5d72e6..36e4608 100644 --- a/crates/plexmaton-tui/frames/drawer-wide.txt +++ b/crates/plexmaton-tui/frames/drawer-wide.txt @@ -32,7 +32,7 @@ │remains interactive. │ │ │ │received from Agent B · UI study · Routing stays centralized and z-ordered. │ -│󰝦 Thinking… ( !1 ) │ +│󰧟 Thinking… ( !1 ) │ │ │ ── Message ───────────────────────────────────────────────────────────────────────────────────────────────────────────── Type a message · ⇥ to focus diff --git a/crates/plexmaton-tui/frames/input-queue-medium.txt b/crates/plexmaton-tui/frames/input-queue-medium.txt index 08eec72..e1b7b5d 100644 --- a/crates/plexmaton-tui/frames/input-queue-medium.txt +++ b/crates/plexmaton-tui/frames/input-queue-medium.txt @@ -27,7 +27,7 @@ │received from Agent B · UI study · Routing stays centralized and z-ordered. │ │ │ │ │ -│󰝦 Responding… ( !1 ) │ +│󰧟 Responding… ( !1 ) │ │ │ ── Waiting to send · 2 ──────────────────────────────────────────────────────────────────────── Sends when this turn ends diff --git a/crates/plexmaton-tui/frames/input-queue-narrow.txt b/crates/plexmaton-tui/frames/input-queue-narrow.txt index 5f49fa4..6b116ef 100644 --- a/crates/plexmaton-tui/frames/input-queue-narrow.txt +++ b/crates/plexmaton-tui/frames/input-queue-narrow.txt @@ -27,7 +27,7 @@ │received from Agent B · UI study · Routing stays centrali…│ │ │ │ │ -│󰝦 Responding… ( !1 ) │ +│󰧟 Responding… ( !1 ) │ │ │ ── Waiting to send · 2 ───────────────────────────────────── Sends when this turn ends diff --git a/crates/plexmaton-tui/frames/input-queue-wide.txt b/crates/plexmaton-tui/frames/input-queue-wide.txt index 236215e..7fe7218 100644 --- a/crates/plexmaton-tui/frames/input-queue-wide.txt +++ b/crates/plexmaton-tui/frames/input-queue-wide.txt @@ -27,7 +27,7 @@ │received from Agent B · UI study · Routing stays centralized and z-ordered. │ │ │ │ │ -│󰝦 Responding… ( !1 ) │ +│󰧟 Responding… ( !1 ) │ │ │ ── Waiting to send · 2 ───────────────────────────────────────────────────────────────────────────────────────────────── Sends when this turn ends diff --git a/crates/plexmaton-tui/frames/server-tool-medium.txt b/crates/plexmaton-tui/frames/server-tool-medium.txt index 7b94438..8671fae 100644 --- a/crates/plexmaton-tui/frames/server-tool-medium.txt +++ b/crates/plexmaton-tui/frames/server-tool-medium.txt @@ -32,7 +32,7 @@ │Rust 1.98.1 is the latest stable release. It is a point release over 1.98.0, and the │ │release post on the Rust blog lists what it fixes. │ │ │ -│󰝦 Running web_search… │ +│󰧟 Running web_search… │ │ │ ── Message ──────────────────────────────────────────────────────────────────────────────────── Type a message · ⇥ to focus diff --git a/crates/plexmaton-tui/frames/server-tool-narrow.txt b/crates/plexmaton-tui/frames/server-tool-narrow.txt index a413dde..fbbfec0 100644 --- a/crates/plexmaton-tui/frames/server-tool-narrow.txt +++ b/crates/plexmaton-tui/frames/server-tool-narrow.txt @@ -32,7 +32,7 @@ │point release over 1.98.0, and the release post on │ │the Rust blog lists what it fixes. │ │ │ -│󰝦 Running web_search… │ +│󰧟 Running web_search… │ │ │ ── Message ───────────────────────────────────────────────── Type a message · ⇥ to focus diff --git a/crates/plexmaton-tui/frames/server-tool-open-wide.txt b/crates/plexmaton-tui/frames/server-tool-open-wide.txt index b45dbb2..b68038e 100644 --- a/crates/plexmaton-tui/frames/server-tool-open-wide.txt +++ b/crates/plexmaton-tui/frames/server-tool-open-wide.txt @@ -32,7 +32,7 @@ │Rust 1.98.1 is the latest stable release. It is a point release over 1.98.0, and the release post on the Rust │ │blog lists what it fixes. │ │ │ -│󰝦 Running web_search… │ +│󰧟 Running web_search… │ │ │ ── Message ───────────────────────────────────────────────────────────────────────────────────────────────────────────── Type a message · ⇥ to focus diff --git a/crates/plexmaton-tui/frames/server-tool-wide.txt b/crates/plexmaton-tui/frames/server-tool-wide.txt index 96ec127..51448b3 100644 --- a/crates/plexmaton-tui/frames/server-tool-wide.txt +++ b/crates/plexmaton-tui/frames/server-tool-wide.txt @@ -32,7 +32,7 @@ │Rust 1.98.1 is the latest stable release. It is a point release over 1.98.0, and the release post on the Rust │ │blog lists what it fixes. │ │ │ -│󰝦 Running web_search… │ +│󰧟 Running web_search… │ │ │ ── Message ───────────────────────────────────────────────────────────────────────────────────────────────────────────── Type a message · ⇥ to focus diff --git a/crates/plexmaton-tui/frames/skill-diagnostic-medium.txt b/crates/plexmaton-tui/frames/skill-diagnostic-medium.txt index 91f239e..d9df6fe 100644 --- a/crates/plexmaton-tui/frames/skill-diagnostic-medium.txt +++ b/crates/plexmaton-tui/frames/skill-diagnostic-medium.txt @@ -32,7 +32,7 @@ │ │ │received from Agent B · UI study · Routing stays centralized and z-ordered. │ │[!] skill · failed │ -│󰝦 Thinking… ( !1 ) │ +│󰧟 Thinking… ( !1 ) │ │ │ ── Message ──────────────────────────────────────────────────────────────────────────────────── $review check this change diff --git a/crates/plexmaton-tui/frames/skill-diagnostic-narrow.txt b/crates/plexmaton-tui/frames/skill-diagnostic-narrow.txt index 04a74ee..e46647c 100644 --- a/crates/plexmaton-tui/frames/skill-diagnostic-narrow.txt +++ b/crates/plexmaton-tui/frames/skill-diagnostic-narrow.txt @@ -32,7 +32,7 @@ │ │ │received from Agent B · UI study · Routing stays centrali…│ │[!] skill · failed │ -│󰝦 Thinking… ( !1 ) │ +│󰧟 Thinking… ( !1 ) │ │ │ ── Message ───────────────────────────────────────────────── $review check this change diff --git a/crates/plexmaton-tui/frames/skill-diagnostic-wide.txt b/crates/plexmaton-tui/frames/skill-diagnostic-wide.txt index 248f012..0f05d6e 100644 --- a/crates/plexmaton-tui/frames/skill-diagnostic-wide.txt +++ b/crates/plexmaton-tui/frames/skill-diagnostic-wide.txt @@ -32,7 +32,7 @@ │ │ │received from Agent B · UI study · Routing stays centralized and z-ordered. │ │[!] skill · failed │ -│󰝦 Thinking… ( !1 ) │ +│󰧟 Thinking… ( !1 ) │ │ │ ── Message ───────────────────────────────────────────────────────────────────────────────────────────────────────────── $review check this change diff --git a/crates/plexmaton-tui/frames/tool-open-medium.txt b/crates/plexmaton-tui/frames/tool-open-medium.txt index 958c11d..55b4826 100644 --- a/crates/plexmaton-tui/frames/tool-open-medium.txt +++ b/crates/plexmaton-tui/frames/tool-open-medium.txt @@ -32,7 +32,7 @@ │ │ 197 tests passed │ │ │ stderr: │ │ │ [empty] │ -│󰝦 Thinking… │ +│󰧟 Thinking… │ │ │ ── Message ──────────────────────────────────────────────────────────────────────────────────── Type a message · ⇥ to focus diff --git a/crates/plexmaton-tui/frames/tool-open-narrow.txt b/crates/plexmaton-tui/frames/tool-open-narrow.txt index 34298b2..a7fa1b9 100644 --- a/crates/plexmaton-tui/frames/tool-open-narrow.txt +++ b/crates/plexmaton-tui/frames/tool-open-narrow.txt @@ -32,7 +32,7 @@ │ │ 197 tests passed │ │ │ stderr: │ │ │ [empty] │ -│󰝦 Thinking… │ +│󰧟 Thinking… │ │ │ ── Message ───────────────────────────────────────────────── Type a message · ⇥ to focus diff --git a/crates/plexmaton-tui/frames/tool-open-wide.txt b/crates/plexmaton-tui/frames/tool-open-wide.txt index 753d15c..25ec687 100644 --- a/crates/plexmaton-tui/frames/tool-open-wide.txt +++ b/crates/plexmaton-tui/frames/tool-open-wide.txt @@ -32,7 +32,7 @@ │ │ 197 tests passed │ │ │ stderr: │ │ │ [empty] │ -│󰝦 Thinking… │ +│󰧟 Thinking… │ │ │ ── Message ───────────────────────────────────────────────────────────────────────────────────────────────────────────── Type a message · ⇥ to focus diff --git a/crates/plexmaton-tui/src/frames.rs b/crates/plexmaton-tui/src/frames.rs index 0479b25..7c53f8f 100644 --- a/crates/plexmaton-tui/src/frames.rs +++ b/crates/plexmaton-tui/src/frames.rs @@ -185,7 +185,7 @@ mod tests { let mut rows = drawn.lines(); let activity = rows.next().unwrap_or_default(); assert!( - activity.contains(&format!("{} {label}", crate::render::activity::MARK[0])), + activity.contains(&format!("{} {label}", crate::render::activity::mark(0))), "{name}: the accepted label is on the activity line: {activity:?}" ); assert!( diff --git a/crates/plexmaton-tui/src/render/activity.rs b/crates/plexmaton-tui/src/render/activity.rs index 5f8970d..c3b4fcd 100644 --- a/crates/plexmaton-tui/src/render/activity.rs +++ b/crates/plexmaton-tui/src/render/activity.rs @@ -13,27 +13,49 @@ use crate::{ theme::{Palette, Role}, }; -/// Outline, then filled, each through circle, rounded square, square and back. One icon family -/// from the Nerd Font the product already requires, so every frame centres alike. Rejected: -/// Unicode geometric shapes of mixed sizes, which the user watched shake in their terminal. -pub(crate) const MARK: [&str; 8] = [ - "\u{F0766}", // circle-outline - "\u{F14FC}", // square-rounded-outline - "\u{F0764}", // square-outline - "\u{F14FC}", - "\u{F0765}", // circle - "\u{F14FB}", // square-rounded - "\u{F0763}", // square - "\u{F14FB}", +/// A dot grows into a circle, turns into a rounded square and a square, spins into a diamond and +/// shrinks away through a smaller one, lingering on the whole shapes and passing quickly through +/// the small ones. One icon family from the Nerd Font the product already requires, whose small, +/// medium and whole sizes share one centre, so growing never shakes. Each frame holds a whole +/// number of the motion clock's phases, 24 in all: a cycle every 1.6 s, the effort rail's period. +/// Rejected: Unicode geometric shapes of mixed sizes, which the user watched shake in their +/// terminal; a change of shape at one size, and even steps retracing their path, which the user +/// found rigid; a slice-by-slice sweep, which reads as progress. +pub(crate) const MARK: [(&str, u16); 7] = [ + ("\u{F09DF}", 3), // circle-small + ("\u{F09DE}", 1), // circle-medium + ("\u{F0765}", 6), // circle + ("\u{F14FB}", 2), // square-rounded + ("\u{F0763}", 5), // square + ("\u{F070B}", 4), // rhombus + ("\u{F0A10}", 3), // rhombus-medium ]; -/// Three of the clock's fifteen phases a second per frame: 200 ms, so a cycle takes 1.6 s, the -/// effort rail's own period. -const PHASES_PER_FRAME: u16 = 3; +/// The phases one cycle of the mark takes. +const CYCLE: u16 = { + let mut total = 0; + let mut index = 0; + while index < MARK.len() { + total += MARK[index].1; + index += 1; + } + total +}; + +/// The whole circle, which the mark holds while it stands still for an approval: the first frame is +/// too small to be seen waiting. +pub(crate) const STILL: &str = "\u{F0765}"; /// The frame for a clock phase. pub(crate) fn mark(phase: u16) -> &'static str { - MARK[usize::from(phase / PHASES_PER_FRAME) % MARK.len()] + let mut left = phase % CYCLE; + for (frame, hold) in MARK { + if left < hold { + return frame; + } + left -= hold; + } + unreachable!("the phase is reduced modulo the cycle the holds sum to") } /// The activity line: what the agent is doing on the left; what the reader has selected and what @@ -94,7 +116,7 @@ pub(crate) fn activity_line( let mark = if moving { mark(state.motion_phase()) } else { - MARK[0] + STILL }; left.push(Span::styled(mark, palette.style(mark_role))); left.push(Span::raw(" ")); @@ -240,22 +262,30 @@ mod tests { use unicode_width::UnicodeWidthStr; - use super::{MARK, PHASES_PER_FRAME, mark}; + use super::{CYCLE, MARK, STILL, mark}; /// MOT-2: every frame is one cell, so the mark can never widen the row. #[test] fn mot_2_every_activity_mark_frame_is_one_cell() { - for frame in MARK { + for (frame, _) in MARK { assert_eq!(frame.width(), 1, "{frame:?}"); } + assert_eq!(STILL.width(), 1); } - /// MOT-1: the cycle divides the clock's, so the mark never jumps where the clock wraps. + /// MOT-1: the cycle divides the clock's, so the mark never jumps where the clock wraps, and + /// every frame is shown for the phases it holds. #[test] fn the_mark_cycle_divides_the_clock_cycle() { - let frames = u16::try_from(MARK.len()).expect("small"); - assert_eq!(480 % (frames * PHASES_PER_FRAME), 0); - assert_eq!(mark(0), mark(480 - 480 % (frames * PHASES_PER_FRAME))); - assert_ne!(mark(0), mark(PHASES_PER_FRAME)); + assert_eq!(CYCLE, 24, "one cycle every 1.6 s"); + assert_eq!(480 % CYCLE, 0); + assert_eq!(mark(0), mark(480 - 480 % CYCLE)); + let mut phase = 0; + for (frame, hold) in MARK { + for _ in 0..hold { + assert_eq!(mark(phase), frame, "phase {phase}"); + phase += 1; + } + } } } diff --git a/crates/plexmaton-tui/src/render/mod.rs b/crates/plexmaton-tui/src/render/mod.rs index cb39485..2a32e30 100644 --- a/crates/plexmaton-tui/src/render/mod.rs +++ b/crates/plexmaton-tui/src/render/mod.rs @@ -505,7 +505,7 @@ mod tests { .unwrap_or_else(|| panic!("the canonical scenario creates a primary agent")); let assert_activity = |state: &ViewState, expected: &str, role: Role| { let line = activity_line(state, &palette, 80, false); - let mark = super::activity::MARK[0]; + let mark = super::activity::mark(0); assert!( line.to_string().starts_with(&format!("{mark} {expected}")), "the mark then {expected:?} lead the activity line: {line}" @@ -555,8 +555,8 @@ mod tests { let line = activity_line(&approval.state, &palette, 80, false); assert!( line.to_string() - .starts_with(&format!("{} Approval required", super::activity::MARK[0])), - "approval stands still on the mark's first frame: {line}" + .starts_with(&format!("{} Approval required", super::activity::STILL)), + "approval stands still on the whole circle: {line}" ); assert_eq!(line.spans[0].style, palette.style(Role::ActionRequired)); assert_eq!(line.spans[2].style, palette.style(Role::ActionRequired));