Skip to content

Commit 8a8f266

Browse files
authored
fix(tui): use braille dots for the active task/status spinner (#16)
Replace the pulsing-star active-marker animation (✦✧✶✷✸✹) with the same braille dotted frames used by the bottom thinking-word spinner, so the working/thinking indicator is a single standardized animation. The active marker now reuses SPINNER_FRAMES via ACTIVE_MARKER_FRAMES; STAR_SPINNER_FRAMES is kept as a backward-compatible alias. The reduced-motion / static active marker becomes the calm dot (●) instead of a static star. Tests updated to assert braille frames and the dot reduced-motion glyph.
1 parent 8629976 commit 8a8f266

7 files changed

Lines changed: 45 additions & 35 deletions

File tree

‎src/pythinker_code/ui/shell/glyphs.py‎

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,29 +21,32 @@
2121
SHAPE_FRAME_INTERVAL_S: Final = 0.45
2222
#: Static stand-in used when motion is disabled.
2323
REDUCED_MOTION_GLYPH: Final = "●"
24-
#: Pulsing-star frames for the active task/status marker. Ten one-cell frames
25-
#: ease through neighboring star glyphs so the header twinkle changes smoothly
26-
#: while keeping the label column stable.
27-
STAR_SPINNER_FRAMES: Final = ("✦", "✧", "✶", "✷", "✸", "✹", "✸", "✷", "✶", "✧")
28-
#: Seconds each star-spinner frame stays on screen.
29-
STAR_SPINNER_FRAME_INTERVAL_S: Final = 0.08
24+
#: Braille dotted frames for the active task/status marker beside pinned todos.
25+
ACTIVE_MARKER_FRAMES: Final = SPINNER_FRAMES
26+
#: Seconds each active-marker braille frame stays on screen.
27+
ACTIVE_MARKER_FRAME_INTERVAL_S: Final = SPINNER_FRAME_INTERVAL_S
28+
#: Backward-compatible aliases for the historical star spinner names.
29+
STAR_SPINNER_FRAMES: Final = ACTIVE_MARKER_FRAMES
30+
STAR_SPINNER_FRAME_INTERVAL_S: Final = ACTIVE_MARKER_FRAME_INTERVAL_S
3031

3132
#: Transcript row marker for assistant/tool-call lines.
3233
TRANSCRIPT_ASSISTANT_MARKER: Final = "⏺"
3334
#: Transcript prompt marker for submitted user input.
3435
TRANSCRIPT_PROMPT_MARKER: Final = "❯"
3536
#: Transcript marker for completed thinking/status timing rows.
3637
TRANSCRIPT_STATUS_MARKER: Final = "✻"
37-
#: Transcript marker for active task/status rows.
38-
TRANSCRIPT_ACTIVE_MARKER: Final = "✶"
38+
#: Transcript marker for active task/status rows when motion is disabled.
39+
TRANSCRIPT_ACTIVE_MARKER: Final = REDUCED_MOTION_GLYPH
3940
#: Transcript gutter marker for tool results.
4041
TRANSCRIPT_TOOL_GUTTER: Final = "⎿"
4142

4243
__all__ = [
4344
"SPINNER_FRAMES",
4445
"SHAPE_FRAMES",
46+
"ACTIVE_MARKER_FRAMES",
4547
"SPINNER_FRAME_INTERVAL_S",
4648
"SHAPE_FRAME_INTERVAL_S",
49+
"ACTIVE_MARKER_FRAME_INTERVAL_S",
4750
"STAR_SPINNER_FRAMES",
4851
"STAR_SPINNER_FRAME_INTERVAL_S",
4952
"REDUCED_MOTION_GLYPH",

‎src/pythinker_code/ui/shell/motion.py‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@
1414
from pythinker_code.ui.shell.components.render_utils import cell_width
1515
from pythinker_code.ui.shell.design_system import ShellTone, shell_style
1616
from pythinker_code.ui.shell.glyphs import (
17+
ACTIVE_MARKER_FRAME_INTERVAL_S,
18+
ACTIVE_MARKER_FRAMES,
1719
REDUCED_MOTION_GLYPH,
1820
SHAPE_FRAME_INTERVAL_S,
1921
SHAPE_FRAMES,
2022
SPINNER_FRAME_INTERVAL_S,
2123
SPINNER_FRAMES,
22-
STAR_SPINNER_FRAME_INTERVAL_S,
23-
STAR_SPINNER_FRAMES,
2424
TRANSCRIPT_ACTIVE_MARKER,
2525
)
2626
from pythinker_code.ui.theme import tui_rich_style
@@ -155,14 +155,17 @@ def spinner_frame_at(
155155

156156

157157
def active_marker_frame(elapsed_s: float, *, reduced_motion: bool = False) -> str:
158-
"""Return the current pulsing-star frame for the active task marker (``✶``).
158+
"""Return the current braille dotted frame for the active task marker.
159159
160-
Reduced motion pins to the static ``✶`` so the marker stays calm.
160+
Reduced motion pins to the static dot so the marker stays calm.
161161
"""
162162
if reduced_motion or reduced_motion_enabled():
163163
return TRANSCRIPT_ACTIVE_MARKER
164-
idx = int(max(0.0, elapsed_s) / STAR_SPINNER_FRAME_INTERVAL_S) % len(STAR_SPINNER_FRAMES)
165-
return STAR_SPINNER_FRAMES[idx]
164+
return spinner_frame_at(
165+
elapsed_s,
166+
frames=ACTIVE_MARKER_FRAMES,
167+
interval_s=ACTIVE_MARKER_FRAME_INTERVAL_S,
168+
)
166169

167170

168171
def _candidate_parts(snapshot: ActivitySnapshot) -> list[str]:

‎src/pythinker_code/ui/shell/tool_renderers/_render_utils.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,8 @@ def loading_marker(
148148
"""Return the app-wide task marker.
149149
150150
Running tasks pulse the transcript marker in muted grey; completed tasks
151-
show the same marker as a static green dot. The animated braille spinner is
152-
reserved for the bottom thinking-word status.
151+
show the same marker as a static green dot. The animated braille spinner stays
152+
reserved for activity/status lines such as the pinned todo header.
153153
"""
154154
if done:
155155
return Text(f"{TRANSCRIPT_ASSISTANT_MARKER} ", style=tui_rich_style("success"))

‎tests/ui_and_conv/test_live_view_notifications.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ def _todo_tool_result(items: list[TodoDisplayItem]) -> ToolResult:
109109
def test_working_indicator_pins_todos_under_spinner(monkeypatch):
110110
now = 1000.0
111111
monkeypatch.setattr(live_view_module.time, "monotonic", lambda: now)
112-
# Pin the animated star marker to its static ``✶`` frame so this
112+
# Pin the animated braille marker to its static dot so this
113113
# structure-focused assertion does not depend on the animation phase.
114114
monkeypatch.setenv("PYTHINKER_REDUCED_MOTION", "1")
115115
view = _LiveView(StatusUpdate())
@@ -138,7 +138,7 @@ def test_working_indicator_pins_todos_under_spinner(monkeypatch):
138138
now = 1060.0
139139
rendered = _render(view._working_indicator())
140140

141-
assert "✶ Explore project context — blogs page and image components… (1m 0s)" in rendered
141+
assert "● Explore project context — blogs page and image components… (1m 0s)" in rendered
142142
# Active todo now appears both in the spinner header and the pinned list;
143143
# done todos sort to the bottom and are dropped by the 5-row cap when
144144
# active + pending already fill the rows.

‎tests/ui_and_conv/test_live_view_todos.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def _todo_result(call_id: str = "todo-1") -> ToolResult:
8585
def test_todo_update_pins_current_task_under_activity_line(monkeypatch) -> None:
8686
now = 1000.0
8787
monkeypatch.setattr(_live_view_module.time, "monotonic", lambda: now)
88-
# Pin the animated star marker to its static ``✶`` frame for a deterministic
88+
# Pin the animated braille marker to its static dot for a deterministic
8989
# assertion on the activity-line content.
9090
monkeypatch.setenv("PYTHINKER_REDUCED_MOTION", "1")
9191
view = _LiveView(StatusUpdate(context_tokens=10_000))
@@ -96,7 +96,7 @@ def test_todo_update_pins_current_task_under_activity_line(monkeypatch) -> None:
9696
now = 1460.0
9797
rendered = _render(view._working_indicator())
9898

99-
assert "✶ Implement pinned todos… (7m 40s · ↓ 10k tokens)" in rendered
99+
assert "● Implement pinned todos… (7m 40s · ↓ 10k tokens)" in rendered
100100
assert rendered.count("Implement pinned todos") == 2
101101
assert "⎿ ■ Implement pinned todos" in rendered
102102
assert "✓ Explore UI" in rendered
@@ -118,7 +118,7 @@ def test_active_todo_activity_line_does_not_alternate_with_spinner_verb(monkeypa
118118
now = 1465.0
119119
rendered = _render(view._working_indicator())
120120

121-
assert "✶ Implement pinned todos… (7m 45s · ↓ 10k tokens)" in rendered
121+
assert "● Implement pinned todos… (7m 45s · ↓ 10k tokens)" in rendered
122122
assert _live_view_module.spinner_message(now) not in rendered
123123
assert "⎿ ■ Implement pinned todos" in rendered
124124
assert "✓ Explore UI" in rendered
@@ -138,7 +138,7 @@ def test_spinner_verb_shows_until_next_todo_becomes_active(monkeypatch) -> None:
138138
now = 1465.0
139139
rendered = _render(view._working_indicator())
140140

141-
assert f"✶ {_live_view_module.spinner_message(now)} (7m 45s · ↓ 10k tokens)" in rendered
141+
assert f"● {_live_view_module.spinner_message(now)} (7m 45s · ↓ 10k tokens)" in rendered
142142
assert "⎿ □ Next task" in rendered
143143
assert "✓ Finished task" in rendered
144144

‎tests/ui_and_conv/test_render_hardening.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ def test_spinner_frames_have_single_source() -> None:
3030
# All three modules must reference the same frames object — no copies.
3131
assert motion._FRAMES is glyphs.SPINNER_FRAMES
3232
assert spinner_words.SPINNER_FRAMES is glyphs.SPINNER_FRAMES
33+
assert glyphs.ACTIVE_MARKER_FRAMES is glyphs.SPINNER_FRAMES
3334
assert motion._FRAME_INTERVAL_S == glyphs.SPINNER_FRAME_INTERVAL_S
35+
assert glyphs.ACTIVE_MARKER_FRAME_INTERVAL_S == glyphs.SPINNER_FRAME_INTERVAL_S
3436

3537

3638
def test_reduced_motion_glyph_centralized() -> None:

‎tests/ui_and_conv/test_shell_motion.py‎

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
from rich.style import Style
66

77
from pythinker_code.ui.shell.glyphs import (
8+
ACTIVE_MARKER_FRAME_INTERVAL_S,
9+
ACTIVE_MARKER_FRAMES,
10+
REDUCED_MOTION_GLYPH,
811
SHAPE_FRAME_INTERVAL_S,
9-
STAR_SPINNER_FRAME_INTERVAL_S,
10-
STAR_SPINNER_FRAMES,
11-
TRANSCRIPT_ACTIVE_MARKER,
12+
SPINNER_FRAME_INTERVAL_S,
13+
SPINNER_FRAMES,
1214
)
1315
from pythinker_code.ui.shell.motion import (
1416
ActivitySnapshot,
@@ -65,20 +67,20 @@ def test_reduced_motion_uses_static_glyph():
6567
assert spinner_frame_at(0.2, reduced_motion=True) == "●"
6668

6769

68-
def test_active_marker_frame_animates_through_star_frames():
70+
def test_active_marker_frame_animates_through_braille_dot_frames():
6971
seen = {
70-
active_marker_frame(i * STAR_SPINNER_FRAME_INTERVAL_S)
71-
for i in range(len(STAR_SPINNER_FRAMES))
72+
active_marker_frame(i * ACTIVE_MARKER_FRAME_INTERVAL_S)
73+
for i in range(len(ACTIVE_MARKER_FRAMES))
7274
}
73-
assert len(STAR_SPINNER_FRAMES) == 10
74-
assert "✷" in STAR_SPINNER_FRAMES
75-
assert STAR_SPINNER_FRAME_INTERVAL_S == 0.08
76-
assert seen == set(STAR_SPINNER_FRAMES)
77-
assert all(len(frame) == 1 for frame in STAR_SPINNER_FRAMES)
75+
assert ACTIVE_MARKER_FRAMES is SPINNER_FRAMES
76+
assert ACTIVE_MARKER_FRAME_INTERVAL_S == SPINNER_FRAME_INTERVAL_S
77+
assert "⠸" in ACTIVE_MARKER_FRAMES
78+
assert seen == set(ACTIVE_MARKER_FRAMES)
79+
assert all(len(frame) == 1 for frame in ACTIVE_MARKER_FRAMES)
7880

7981

80-
def test_active_marker_frame_reduced_motion_pins_static_star():
81-
assert active_marker_frame(0.5, reduced_motion=True) == TRANSCRIPT_ACTIVE_MARKER
82+
def test_active_marker_frame_reduced_motion_pins_static_dot():
83+
assert active_marker_frame(0.5, reduced_motion=True) == REDUCED_MOTION_GLYPH
8284

8385

8486
def test_activity_status_line_contains_label_elapsed_tokens_and_interrupt_hint():

0 commit comments

Comments
 (0)