diff --git a/src/pythinker_code/ui/shell/glyphs.py b/src/pythinker_code/ui/shell/glyphs.py index 8c157046..157040e8 100644 --- a/src/pythinker_code/ui/shell/glyphs.py +++ b/src/pythinker_code/ui/shell/glyphs.py @@ -21,12 +21,13 @@ SHAPE_FRAME_INTERVAL_S: Final = 0.45 #: Static stand-in used when motion is disabled. REDUCED_MOTION_GLYPH: Final = "●" -#: Pulsing-star frames for the active task/status marker. Ten one-cell frames -#: ease through neighboring star glyphs so the header twinkle changes smoothly -#: while keeping the label column stable. -STAR_SPINNER_FRAMES: Final = ("✦", "✧", "✶", "✷", "✸", "✹", "✸", "✷", "✶", "✧") -#: Seconds each star-spinner frame stays on screen. -STAR_SPINNER_FRAME_INTERVAL_S: Final = 0.08 +#: Braille dotted frames for the active task/status marker beside pinned todos. +ACTIVE_MARKER_FRAMES: Final = SPINNER_FRAMES +#: Seconds each active-marker braille frame stays on screen. +ACTIVE_MARKER_FRAME_INTERVAL_S: Final = SPINNER_FRAME_INTERVAL_S +#: Backward-compatible aliases for the historical star spinner names. +STAR_SPINNER_FRAMES: Final = ACTIVE_MARKER_FRAMES +STAR_SPINNER_FRAME_INTERVAL_S: Final = ACTIVE_MARKER_FRAME_INTERVAL_S #: Transcript row marker for assistant/tool-call lines. TRANSCRIPT_ASSISTANT_MARKER: Final = "⏺" @@ -34,16 +35,18 @@ TRANSCRIPT_PROMPT_MARKER: Final = "❯" #: Transcript marker for completed thinking/status timing rows. TRANSCRIPT_STATUS_MARKER: Final = "✻" -#: Transcript marker for active task/status rows. -TRANSCRIPT_ACTIVE_MARKER: Final = "✶" +#: Transcript marker for active task/status rows when motion is disabled. +TRANSCRIPT_ACTIVE_MARKER: Final = REDUCED_MOTION_GLYPH #: Transcript gutter marker for tool results. TRANSCRIPT_TOOL_GUTTER: Final = "⎿" __all__ = [ "SPINNER_FRAMES", "SHAPE_FRAMES", + "ACTIVE_MARKER_FRAMES", "SPINNER_FRAME_INTERVAL_S", "SHAPE_FRAME_INTERVAL_S", + "ACTIVE_MARKER_FRAME_INTERVAL_S", "STAR_SPINNER_FRAMES", "STAR_SPINNER_FRAME_INTERVAL_S", "REDUCED_MOTION_GLYPH", diff --git a/src/pythinker_code/ui/shell/motion.py b/src/pythinker_code/ui/shell/motion.py index fa9ed391..1757dab1 100644 --- a/src/pythinker_code/ui/shell/motion.py +++ b/src/pythinker_code/ui/shell/motion.py @@ -14,13 +14,13 @@ from pythinker_code.ui.shell.components.render_utils import cell_width from pythinker_code.ui.shell.design_system import ShellTone, shell_style from pythinker_code.ui.shell.glyphs import ( + ACTIVE_MARKER_FRAME_INTERVAL_S, + ACTIVE_MARKER_FRAMES, REDUCED_MOTION_GLYPH, SHAPE_FRAME_INTERVAL_S, SHAPE_FRAMES, SPINNER_FRAME_INTERVAL_S, SPINNER_FRAMES, - STAR_SPINNER_FRAME_INTERVAL_S, - STAR_SPINNER_FRAMES, TRANSCRIPT_ACTIVE_MARKER, ) from pythinker_code.ui.theme import tui_rich_style @@ -155,14 +155,17 @@ def spinner_frame_at( def active_marker_frame(elapsed_s: float, *, reduced_motion: bool = False) -> str: - """Return the current pulsing-star frame for the active task marker (``✶``). + """Return the current braille dotted frame for the active task marker. - Reduced motion pins to the static ``✶`` so the marker stays calm. + Reduced motion pins to the static dot so the marker stays calm. """ if reduced_motion or reduced_motion_enabled(): return TRANSCRIPT_ACTIVE_MARKER - idx = int(max(0.0, elapsed_s) / STAR_SPINNER_FRAME_INTERVAL_S) % len(STAR_SPINNER_FRAMES) - return STAR_SPINNER_FRAMES[idx] + return spinner_frame_at( + elapsed_s, + frames=ACTIVE_MARKER_FRAMES, + interval_s=ACTIVE_MARKER_FRAME_INTERVAL_S, + ) def _candidate_parts(snapshot: ActivitySnapshot) -> list[str]: diff --git a/src/pythinker_code/ui/shell/tool_renderers/_render_utils.py b/src/pythinker_code/ui/shell/tool_renderers/_render_utils.py index a6d345f5..6298c5f6 100644 --- a/src/pythinker_code/ui/shell/tool_renderers/_render_utils.py +++ b/src/pythinker_code/ui/shell/tool_renderers/_render_utils.py @@ -148,8 +148,8 @@ def loading_marker( """Return the app-wide task marker. Running tasks pulse the transcript marker in muted grey; completed tasks - show the same marker as a static green dot. The animated braille spinner is - reserved for the bottom thinking-word status. + show the same marker as a static green dot. The animated braille spinner stays + reserved for activity/status lines such as the pinned todo header. """ if done: return Text(f"{TRANSCRIPT_ASSISTANT_MARKER} ", style=tui_rich_style("success")) diff --git a/tests/ui_and_conv/test_live_view_notifications.py b/tests/ui_and_conv/test_live_view_notifications.py index e0a09cf0..51f2a36d 100644 --- a/tests/ui_and_conv/test_live_view_notifications.py +++ b/tests/ui_and_conv/test_live_view_notifications.py @@ -109,7 +109,7 @@ def _todo_tool_result(items: list[TodoDisplayItem]) -> ToolResult: def test_working_indicator_pins_todos_under_spinner(monkeypatch): now = 1000.0 monkeypatch.setattr(live_view_module.time, "monotonic", lambda: now) - # Pin the animated star marker to its static ``✶`` frame so this + # Pin the animated braille marker to its static dot so this # structure-focused assertion does not depend on the animation phase. monkeypatch.setenv("PYTHINKER_REDUCED_MOTION", "1") view = _LiveView(StatusUpdate()) @@ -138,7 +138,7 @@ def test_working_indicator_pins_todos_under_spinner(monkeypatch): now = 1060.0 rendered = _render(view._working_indicator()) - assert "✶ Explore project context — blogs page and image components… (1m 0s)" in rendered + assert "● Explore project context — blogs page and image components… (1m 0s)" in rendered # Active todo now appears both in the spinner header and the pinned list; # done todos sort to the bottom and are dropped by the 5-row cap when # active + pending already fill the rows. diff --git a/tests/ui_and_conv/test_live_view_todos.py b/tests/ui_and_conv/test_live_view_todos.py index 230ff842..04f23539 100644 --- a/tests/ui_and_conv/test_live_view_todos.py +++ b/tests/ui_and_conv/test_live_view_todos.py @@ -85,7 +85,7 @@ def _todo_result(call_id: str = "todo-1") -> ToolResult: def test_todo_update_pins_current_task_under_activity_line(monkeypatch) -> None: now = 1000.0 monkeypatch.setattr(_live_view_module.time, "monotonic", lambda: now) - # Pin the animated star marker to its static ``✶`` frame for a deterministic + # Pin the animated braille marker to its static dot for a deterministic # assertion on the activity-line content. monkeypatch.setenv("PYTHINKER_REDUCED_MOTION", "1") view = _LiveView(StatusUpdate(context_tokens=10_000)) @@ -96,7 +96,7 @@ def test_todo_update_pins_current_task_under_activity_line(monkeypatch) -> None: now = 1460.0 rendered = _render(view._working_indicator()) - assert "✶ Implement pinned todos… (7m 40s · ↓ 10k tokens)" in rendered + assert "● Implement pinned todos… (7m 40s · ↓ 10k tokens)" in rendered assert rendered.count("Implement pinned todos") == 2 assert "⎿ ■ Implement pinned todos" in rendered assert "✓ Explore UI" in rendered @@ -118,7 +118,7 @@ def test_active_todo_activity_line_does_not_alternate_with_spinner_verb(monkeypa now = 1465.0 rendered = _render(view._working_indicator()) - assert "✶ Implement pinned todos… (7m 45s · ↓ 10k tokens)" in rendered + assert "● Implement pinned todos… (7m 45s · ↓ 10k tokens)" in rendered assert _live_view_module.spinner_message(now) not in rendered assert "⎿ ■ Implement pinned todos" in rendered assert "✓ Explore UI" in rendered @@ -138,7 +138,7 @@ def test_spinner_verb_shows_until_next_todo_becomes_active(monkeypatch) -> None: now = 1465.0 rendered = _render(view._working_indicator()) - assert f"✶ {_live_view_module.spinner_message(now)} (7m 45s · ↓ 10k tokens)" in rendered + assert f"● {_live_view_module.spinner_message(now)} (7m 45s · ↓ 10k tokens)" in rendered assert "⎿ □ Next task" in rendered assert "✓ Finished task" in rendered diff --git a/tests/ui_and_conv/test_render_hardening.py b/tests/ui_and_conv/test_render_hardening.py index 62e28d1e..581a95e7 100644 --- a/tests/ui_and_conv/test_render_hardening.py +++ b/tests/ui_and_conv/test_render_hardening.py @@ -30,7 +30,9 @@ def test_spinner_frames_have_single_source() -> None: # All three modules must reference the same frames object — no copies. assert motion._FRAMES is glyphs.SPINNER_FRAMES assert spinner_words.SPINNER_FRAMES is glyphs.SPINNER_FRAMES + assert glyphs.ACTIVE_MARKER_FRAMES is glyphs.SPINNER_FRAMES assert motion._FRAME_INTERVAL_S == glyphs.SPINNER_FRAME_INTERVAL_S + assert glyphs.ACTIVE_MARKER_FRAME_INTERVAL_S == glyphs.SPINNER_FRAME_INTERVAL_S def test_reduced_motion_glyph_centralized() -> None: diff --git a/tests/ui_and_conv/test_shell_motion.py b/tests/ui_and_conv/test_shell_motion.py index 82ffbddd..846cbbf5 100644 --- a/tests/ui_and_conv/test_shell_motion.py +++ b/tests/ui_and_conv/test_shell_motion.py @@ -5,10 +5,12 @@ from rich.style import Style from pythinker_code.ui.shell.glyphs import ( + ACTIVE_MARKER_FRAME_INTERVAL_S, + ACTIVE_MARKER_FRAMES, + REDUCED_MOTION_GLYPH, SHAPE_FRAME_INTERVAL_S, - STAR_SPINNER_FRAME_INTERVAL_S, - STAR_SPINNER_FRAMES, - TRANSCRIPT_ACTIVE_MARKER, + SPINNER_FRAME_INTERVAL_S, + SPINNER_FRAMES, ) from pythinker_code.ui.shell.motion import ( ActivitySnapshot, @@ -65,20 +67,20 @@ def test_reduced_motion_uses_static_glyph(): assert spinner_frame_at(0.2, reduced_motion=True) == "●" -def test_active_marker_frame_animates_through_star_frames(): +def test_active_marker_frame_animates_through_braille_dot_frames(): seen = { - active_marker_frame(i * STAR_SPINNER_FRAME_INTERVAL_S) - for i in range(len(STAR_SPINNER_FRAMES)) + active_marker_frame(i * ACTIVE_MARKER_FRAME_INTERVAL_S) + for i in range(len(ACTIVE_MARKER_FRAMES)) } - assert len(STAR_SPINNER_FRAMES) == 10 - assert "✷" in STAR_SPINNER_FRAMES - assert STAR_SPINNER_FRAME_INTERVAL_S == 0.08 - assert seen == set(STAR_SPINNER_FRAMES) - assert all(len(frame) == 1 for frame in STAR_SPINNER_FRAMES) + assert ACTIVE_MARKER_FRAMES is SPINNER_FRAMES + assert ACTIVE_MARKER_FRAME_INTERVAL_S == SPINNER_FRAME_INTERVAL_S + assert "⠸" in ACTIVE_MARKER_FRAMES + assert seen == set(ACTIVE_MARKER_FRAMES) + assert all(len(frame) == 1 for frame in ACTIVE_MARKER_FRAMES) -def test_active_marker_frame_reduced_motion_pins_static_star(): - assert active_marker_frame(0.5, reduced_motion=True) == TRANSCRIPT_ACTIVE_MARKER +def test_active_marker_frame_reduced_motion_pins_static_dot(): + assert active_marker_frame(0.5, reduced_motion=True) == REDUCED_MOTION_GLYPH def test_activity_status_line_contains_label_elapsed_tokens_and_interrupt_hint():