Skip to content

feat: add mascot animation#23

Merged
hiroppy merged 38 commits into
mainfrom
feat/cat-animation
Apr 27, 2026
Merged

feat: add mascot animation#23
hiroppy merged 38 commits into
mainfrom
feat/cat-animation

Conversation

@hiroppy

@hiroppy hiroppy commented Apr 15, 2026

Copy link
Copy Markdown
Owner
Screenshot 2026-04-15 at 23 47 41

Summary by CodeRabbit

  • New Features

    • Optional animated mascot in the bottom sidebar (idle, walking, working); disabled by default and toggleable via configuration
    • Mascot appearance customizable with body and eye color options
  • UI / Behavior

    • Mascot advances with periodic UI ticks and renders alongside existing sidebar elements without disrupting bottom panel borders
  • Documentation

    • README and docs updated with mascot configuration and animation details
  • Tests

    • Added tests for color defaults, enablement parsing, and mascot rendering/behavior

hiroppy and others added 17 commits April 9, 2026 13:21
- Cat sits idle at left, walks to desk when agents start running
- Works at desk with paper-shuffling animation
- Walks back left when agents stop
- Desk, chair, and paper sprites with proper layout
- Inline snapshot tests for all animation states

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove crouching_sprite and its idle trigger logic
- Change walking sprites from thin ▐▌ to thick ▄ to match sitting sprite

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
# Conflicts:
#	README.md
#	src/state.rs
#	tests/bottom_tests.rs
Gate the cat mascot animation behind a new tmux option so it stays
hidden by default. Users can opt in with `set -g @sidebar_mascot on`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
# Conflicts:
#	docs/state-management.md
#	src/cli/setup.rs
#	src/state.rs
#	src/ui/mod.rs
#	tests/bottom_tests.rs
@coderabbitai

coderabbitai Bot commented Apr 15, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Added a mascot animation subsystem: new tmux option to enable it, AppState mascot fields and tick logic, mascot renderer, theme colors, UI wiring to render each spinner tick, docs/config updates, and tests for parsing, rendering, and mascot behavior.

Changes

Cohort / File(s) Summary
Docs & Config
README.md, agent-sidebar.conf, docs/state-management.md
Documented new @sidebar_mascot option (README); default-seed set -g @sidebar_mascot off added to config; state-management docs extended with mascot state fields and update-cycle wording.
Core state & main loop
src/state.rs, src/app.rs, src/app/setup.rs
Added mascot fields to AppState, reseed helpers, running_count() and tick_mascot(panel_width) state machine with unit tests; wired tick_mascot into spinner tick path; initialize mascot_enabled from tmux during startup.
UI: mascot rendering & integration
src/ui/mascot.rs, src/ui/mod.rs
New mascot module with MascotState, constants, draw_mascot, rendering helpers and tests; exported module and MASCOT_SCENE_HEIGHT; added option-parsing helpers and conditional drawing when mascot_enabled.
Colors & tests
src/ui/colors.rs, tests/color_tests.rs
Added mascot_body and mascot_eye to ColorTheme with defaults and tmux-option loading; tests updated to assert new defaults and overrides.
UI tests / snapshots
tests/bottom_tests.rs
Added snapshot test ensuring bottom panel border and content remain correct when mascot is enabled.

Sequence Diagram(s)

sequenceDiagram
    rect rgba(200,200,255,0.5)
    participant Tmux as Tmux/Options
    end
    rect rgba(200,255,200,0.5)
    participant App as App (main loop)
    participant State as AppState
    participant UI as UI / Mascot Renderer
    participant Frame as TUI Frame
    end

    Tmux->>App: provide `@sidebar_mascot` & color options at init
    App->>State: init_state(mascot_enabled)
    loop spinner tick (~200ms)
      App->>State: tick_spinner()
      alt mascot_enabled
        App->>State: tick_mascot(term_width)
        State->>UI: expose mascot state & fields
        UI->>Frame: draw_mascot(frame, state, bottom_area, running_count)
        Frame->>UI: rendered output
      end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

"I hop and I nudge the tiny screen light,
Papers stack, my little eye blinks bright.
I bob, I walk, then lift a jaunty sheet,
Sidebar hums along to my tiny feet.
— A rabbit, proud of the mascot's little feat" 🐇

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description contains only an image of a pixel art sprite character with no written Summary or Test plan sections as required by the template. Add a Summary section describing the purpose and scope of the mascot animation feature, and a Test plan section documenting the tests or manual checks performed.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add mascot animation' directly and clearly describes the main change—the addition of a mascot animation feature throughout the codebase.
Docstring Coverage ✅ Passed Docstring coverage is 98.10% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cat-animation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@hiroppy hiroppy marked this pull request as ready for review April 15, 2026 14:48

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/state.rs`:
- Around line 1651-1673: The mascot walking logic only toggles self.mascot_frame
between 1 and 2, so frame 3 (walking_right_3/walking_left_3) is never reached;
update the frame advancement in both MascotState::WalkRight and
MascotState::WalkLeft branches (the place that currently does self.mascot_frame
= if self.mascot_frame == 1 { 2 } else { 1 }) to cycle through 1→2→3→1 (e.g.
increment with wraparound using modulus or a small match on self.mascot_frame),
leaving the rest of the movement logic (mascot_x, mascot_walk_tick,
reseed_mascot_walk_bounce, mascot_walk_bounce_lift_until, etc.) unchanged so all
three frames can appear.

In `@src/ui/mascot.rs`:
- Around line 508-519: The helper walking_vertical_lift currently ignores the
scheduled window and only uses the phase calc; update it to also consult
AppState's scheduled fields (mascot_walk_bounce_next_tick and
mascot_walk_bounce_lift_until) so the lift returns 2 only when both the existing
walking/phase conditions and the current tick falls inside the scheduled bounce
window (i.e., between mascot_walk_bounce_next_tick and
mascot_walk_bounce_lift_until); modify the conditional in walking_vertical_lift
to include that time-window check and leave other logic (phase, is_walking)
intact.
- Around line 621-627: The right-edge clipping undercounts by one because
available is computed as frame.area().width.saturating_sub(x.saturating_add(1));
change it to subtract x only (frame.area().width.saturating_sub(x)) so a sprite
starting exactly n cells from the right has n available columns; keep the
existing check for available == 0 and min clipping (w =
line_width.min(available)) and keep creating the Rect with that width
(Rect::new(x, y, w, 1)) — reference the variables available, x, line_width, w
and the Rect::new call in mascot.rs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7d0a6292-cba8-4a51-8125-ae28c639c9ab

📥 Commits

Reviewing files that changed from the base of the PR and between 4016b63 and 1e9d794.

📒 Files selected for processing (10)
  • README.md
  • agent-sidebar.conf
  • docs/state-management.md
  • src/cli/hook.rs
  • src/main.rs
  • src/state.rs
  • src/ui/colors.rs
  • src/ui/mascot.rs
  • src/ui/mod.rs
  • tests/color_tests.rs

Comment thread src/state.rs Outdated
Comment thread src/ui/mascot.rs
Comment thread src/ui/mascot.rs
hiroppy and others added 2 commits April 18, 2026 10:41
# Conflicts:
#	README.md
#	src/cli/hook.rs
#	src/main.rs
#	src/state.rs
- Cycle mascot_frame 1→2→3→1 in WalkRight/WalkLeft so the third sprite
  frame actually renders.
- Replace the phase-based walking bounce lift with the window already
  scheduled by tick_mascot (mascot_walk_bounce_lift_until), so sprite
  lift and state-machine timing stay in sync.
- Fix off-by-one in render_lines right-edge clipping and account for
  non-zero frame.area().x.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/state.rs (1)

778-785: Reuse running_count() inside tick_mascot.

draw_mascot already consumes state.running_count() via src/ui/mod.rs, but tick_mascot keeps a second copy of the same traversal. That makes the motion trigger and the rendered paper height easy to drift apart on the next change.

♻️ Proposed cleanup
-        let running_count = self
-            .repo_groups
-            .iter()
-            .flat_map(|g| &g.panes)
-            .filter(|(p, _)| p.status == crate::tmux::PaneStatus::Running)
-            .count();
+        let running_count = self.running_count();

Also applies to: 1129-1134

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/state.rs` around lines 778 - 785, The traversal that counts running
agents is duplicated inside tick_mascot; replace that duplicated
flat_map/filter/count logic by calling the existing running_count(&self) method
so both draw_mascot and tick_mascot use the same source of truth. Locate
tick_mascot and remove its manual iteration over repo_groups/panes, calling
self.running_count() where the count was used (also apply the same replacement
for the other duplicate traversal in tick_mascot). Ensure borrow/immutability
matches the surrounding code when swapping in the method call.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/ui/mascot.rs`:
- Around line 900-903: Tests compute stop_x using CHAIR_WIDTH + 2 but the
runtime parks the mascot at CHAIR_WIDTH + 3 in AppState::tick_mascot(); update
the test expectations to use CHAIR_WIDTH + 3 (i.e., change the
working_width/stop_x calculation to match CHAIR_WIDTH + 3) so mascot_x aligns
with AppState::tick_mascot()’s parked Working position—apply the same +3 fix to
the other occurrences referenced (the blocks around the other two snapshots).

---

Nitpick comments:
In `@src/state.rs`:
- Around line 778-785: The traversal that counts running agents is duplicated
inside tick_mascot; replace that duplicated flat_map/filter/count logic by
calling the existing running_count(&self) method so both draw_mascot and
tick_mascot use the same source of truth. Locate tick_mascot and remove its
manual iteration over repo_groups/panes, calling self.running_count() where the
count was used (also apply the same replacement for the other duplicate
traversal in tick_mascot). Ensure borrow/immutability matches the surrounding
code when swapping in the method call.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5673b6e2-d841-40a0-93ac-21f58de53cf5

📥 Commits

Reviewing files that changed from the base of the PR and between 9c540fa and 74dc543.

📒 Files selected for processing (2)
  • src/state.rs
  • src/ui/mascot.rs

Comment thread src/ui/mascot.rs
hiroppy and others added 2 commits April 18, 2026 11:13
The runtime parks the mascot at CHAIR_WIDTH + 3 (src/state.rs:1137),
but the three Working-state snapshot tests were locking CHAIR_WIDTH + 2.
Update working_width and the expected frames so a real off-by-one in
the desk layout would fail these snapshots.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mascot sprites were rendering from baseline = chunks[2].y - 1 into a
1-row divider, spilling the head/body into the pane list above. Grow
the divider (chunks[1]) to MASCOT_SCENE_HEIGHT (5) when the mascot is
enabled and pass that band as the mascot's drawable area. The mascot
still visually sits on top of the bottom panel — position unchanged —
but it no longer overdraws pane rows and the bottom panel border stays
intact. Adds a snapshot integration test that locks in this layout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@hiroppy

hiroppy commented Apr 18, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 18, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/ui/mascot.rs (2)

888-898: Consider using insta::assert_snapshot! for visual regression tests.

The snapshot tests use assert_eq! with expected strings, which works but requires manual updates when sprites change. The insta crate provides better ergonomics for maintaining visual snapshots.

♻️ Example refactor using insta
 #[test]
 fn snapshot_idle() {
     let state = AppState::new("%0".into());
     let output = render_mascot_scene(&state, 0, 40, 14);
-    let expected = [
-        "  ▄ ▄",
-        " ▄▀▀▀▄                             ████",
-        "  ▀ ▀                           ██ █  █",
-    ]
-    .join("\n");
-    assert_eq!(output, expected);
+    insta::assert_snapshot!(output, `@r`###"
+      ▄ ▄
+     ▄▀▀▀▄                             ████
+      ▀ ▀                           ██ █  █
+    "###);
 }

Based on learnings: "In UI tests that render a frame, MUST use insta::assert_snapshot!(output, @"") inline snapshots instead of assert!(output.contains(...)) or substring checks for stronger visual regression detection"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/mascot.rs` around lines 888 - 898, The test snapshot_idle currently
compares the rendered mascot string using assert_eq!, which is brittle; change
it to use insta's inline snapshot macro to enable visual regression management:
add the insta crate as a dev-dependency if not present, then in the
snapshot_idle test (which calls AppState::new and render_mascot_scene) replace
the assert_eq!(output, expected) with insta::assert_snapshot!(output, @""<put
the current expected multi-line snapshot here> ""); ensure the snapshot content
matches the existing expected value so the first run records it and future
changes are managed by insta.

625-627: Height clipping check may be incorrect when frame.area().y > 0.

The check y >= frame.area().height compares against the height dimension rather than the actual bottom boundary. If frame.area() ever has a non-zero y origin, valid rows would be incorrectly skipped.

In typical terminal rendering where frame.area().y == 0, this works fine. If the frame area can have a non-zero origin in any use case, consider:

🔧 Suggested fix for edge case
-        if y >= frame.area().height {
+        let area = frame.area();
+        if y < area.y || y >= area.y + area.height {
             continue;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/mascot.rs` around lines 625 - 627, The height clipping check inside
the loop incorrectly compares the absolute row index y against
frame.area().height; update the check in the function containing that loop (look
for the loop using variable `y` and `frame.area()`) to compare against the
frame's bottom boundary instead (e.g., use frame.area().y + frame.area().height
or a provided bottom() method) so rows are clipped relative to the frame's
origin rather than to the height alone.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/ui/mascot.rs`:
- Line 20: Remove the unused public constant MASCOT_WIDTH from src/ui/mascot.rs
or restore its usage; specifically either delete the declaration pub const
MASCOT_WIDTH: u16 = 7; to eliminate the unused symbol, or update the
mascot-related code (e.g., the rendering/dimensions functions in mascot.rs and
any consumers like state.rs) to reference MASCOT_WIDTH where the mascot's width
is required so the constant is actually used.

---

Nitpick comments:
In `@src/ui/mascot.rs`:
- Around line 888-898: The test snapshot_idle currently compares the rendered
mascot string using assert_eq!, which is brittle; change it to use insta's
inline snapshot macro to enable visual regression management: add the insta
crate as a dev-dependency if not present, then in the snapshot_idle test (which
calls AppState::new and render_mascot_scene) replace the assert_eq!(output,
expected) with insta::assert_snapshot!(output, @""<put the current expected
multi-line snapshot here> ""); ensure the snapshot content matches the existing
expected value so the first run records it and future changes are managed by
insta.
- Around line 625-627: The height clipping check inside the loop incorrectly
compares the absolute row index y against frame.area().height; update the check
in the function containing that loop (look for the loop using variable `y` and
`frame.area()`) to compare against the frame's bottom boundary instead (e.g.,
use frame.area().y + frame.area().height or a provided bottom() method) so rows
are clipped relative to the frame's origin rather than to the height alone.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dde07b4c-cf76-4923-9b35-83af676e6bb7

📥 Commits

Reviewing files that changed from the base of the PR and between 5e015c4 and dbcc651.

📒 Files selected for processing (3)
  • src/ui/mascot.rs
  • src/ui/mod.rs
  • tests/bottom_tests.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/bottom_tests.rs
  • src/ui/mod.rs

Comment thread src/ui/mascot.rs Outdated
hiroppy and others added 2 commits April 18, 2026 11:41
It's declared but referenced nowhere in the codebase.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
hiroppy and others added 15 commits April 19, 2026 20:45
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Align docs/ with the current Rust API after a docs-audit pass:
- NoticesState: claude_plugin_installed_version → claude_plugin_status
- PermissionMode: add missing Defer variant
- PopupState: document SpawnInput and RemoveConfirm variants
- TODO-background-shell-detection: rename from_str → from_label

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address blocking issues found by spec-document-reviewer:
- BOTTOM_HEIGHT must remain non-zero so the mascot band renders
- @sidebar_mascot must be enabled per scenario
- Walking timing math corrected (22 ticks / 4.4s, not 24 / 4.8s)
- Idle scenario uses post-build_layout @pane_status overrides
- Add reference/tmux-options entry for @sidebar_mascot

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds idle/walking/working PNGs alongside the existing capture set,
wired into scripts/build-assets.sh.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# Conflicts:
#	docs/state-management.md
#	src/ui/colors.rs
Main's post-split state.rs structure is adopted wholesale; the mascot
additions that previously lived in monolithic state.rs are re-homed:

- 20 mascot_* fields added to the AppState struct in state.rs
- `new()` initialisers for the new fields, plus the opening
  reseed_mascot_idle_motion() call
- src/state/mascot.rs (new) holds:
  - 3 reseed helpers (reseed_mascot_idle_motion,
    reseed_mascot_working_paper_motion, reseed_mascot_walk_bounce)
  - impl AppState { running_count(), tick_mascot() }
  - the 9 mascot state-machine tests

mod mascot; registered in state.rs next to the sibling submodules.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolve conflicts:
- docs/TODO-background-shell-detection.md: removed (resolved by #50)
- src/port.rs: take main (process matching extracted to src/process.rs;
  command_basename already covers path-comm fix from 9cdf046)
- src/tmux/query.rs: take main's process_snapshot_for_panes design; drop
  local helpers; keep apply_codex_permission_modes_matches_path_comm test
  on the new ProcessSnapshot API
- src/ui/colors.rs: adopt main's tmux::SIDEBAR_COLOR_* constants while
  preserving mascot_body / mascot_eye reads; add the two missing
  constants in src/tmux/options.rs and re-export via src/tmux.rs
- src/state/mascot.rs: add bg_shell_cmd to PaneInfo test fixture
- tests/bottom_tests.rs: refresh mascot snapshot for new background icon
@hiroppy hiroppy merged commit 3e5a76a into main Apr 27, 2026
2 checks passed
@hiroppy hiroppy deleted the feat/cat-animation branch April 27, 2026 11:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant