Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions src/pythinker_code/ui/shell/glyphs.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,29 +21,32 @@
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 = "⏺"
#: Transcript prompt marker for submitted user input.
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",
Expand Down
15 changes: 9 additions & 6 deletions src/pythinker_code/ui/shell/motion.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]:
Expand Down
4 changes: 2 additions & 2 deletions src/pythinker_code/ui/shell/tool_renderers/_render_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
4 changes: 2 additions & 2 deletions tests/ui_and_conv/test_live_view_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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.
Expand Down
8 changes: 4 additions & 4 deletions tests/ui_and_conv/test_live_view_todos.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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

Expand Down
2 changes: 2 additions & 0 deletions tests/ui_and_conv/test_render_hardening.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
28 changes: 15 additions & 13 deletions tests/ui_and_conv/test_shell_motion.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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():
Expand Down
Loading