Skip to content

feat: configurable puppy emoji (puppymoji) 🦴#300

Closed
mattnico wants to merge 9 commits intompfaffenberger:mainfrom
mattnico:feat/puppymoji
Closed

feat: configurable puppy emoji (puppymoji) 🦴#300
mattnico wants to merge 9 commits intompfaffenberger:mainfrom
mattnico:feat/puppymoji

Conversation

@mattnico
Copy link
Copy Markdown
Contributor

What

Make the hard-coded 🐶 emoji user-configurable via a new puppy_emoji config key. Mirrors exactly how puppy_name already works — defaults to 🐶 so existing users see no change.

/set puppy_emoji 🦊
/set puppy_emoji 🐺
/set puppy_emoji 🐕‍🦺   # ZWJ sequences work too

Why

Some of us want our pup to be a fox, a wolf, or a bone 🦴. Trivial to ship, zero behavior change for default users, fully reversible.

Surfaces that now respect puppy_emoji

Surface Source
Interactive prompt prefix prompt_toolkit_completion.py
Thinking spinner frames ( 🦴 ) spinner_base.py
Startup banner & 'Continuing in Interactive Mode' cli_runner.py
/show status header + new puppy_emoji: row config_commands.py
Onboarding wizard slides onboarding_slides.py
Agent self-intro ('what is code puppy?') agent_code_puppy.py
API landing page (/) api/app.py
API terminal page (/terminal) api/app.py (substituted at request time)
API startup/shutdown log lines api/app.py

Intentionally NOT touched (YAGNI / branding / content)

  • Pack-leader ASCII art (multi-puppy art, not user identity)
  • MOTD historical content (date-stamped messages)
  • oauth_puppy_html.py stylized sprites (cannons, crying dogs, themed art)
  • READMEs / SETUP docs
  • Plugin examples
  • error_logging.py / __init__.py decorative comments
  • Agent product display name Code-Puppy 🐶 (the agent's brand, not the user's pup)

Validation

  • set_puppy_emoji() rejects empty / whitespace-only / >16 chars / None
  • Whitespace trimmed on read & write
  • Falls back to default if config blanked out
  • 16-char cap allows ZWJ combos like 🐕‍🦺 (4 codepoints) without enabling abuse

Tests

  • 10 new tests in TestPuppyEmoji covering get/set/validation/default/whitespace/ZWJ
  • 2 new spinner tests verifying live emoji propagates to current_frame
  • Updated 4 existing tests (2 TestGetConfigKeys snapshots for the new key + 2 spinner tests made hermetic — they were silently coupled to the dev's real puppy.cfg)
  • End-to-end TestClient verified / and /terminal reflect a custom emoji
  • 128/128 relevant tests pass; full suite clean of regressions from these changes

Diff

11 files, +209 / -36. Each change is small and follows the existing puppy_name pattern.

Pre-existing oddity worth a follow-up (not in scope)

SpinnerBase.THINKING_MESSAGE / WAITING_MESSAGE / puppy_name are evaluated at class-definition time, so changing puppy_name mid-session doesn't propagate to the spinner without restart. Same lazy-resolution pattern this PR uses for emoji would fix it. Happy to do as a separate PR if desired.

Matt Nicolaysen and others added 9 commits February 25, 2026 13:56
termflow's wrap_ansi works character-by-character and has no concept of
word boundaries, causing words to be split mid-character whenever a line
reaches the column limit.

Add _word_boundary_wrap_ansi() in markdown_patches.py that:
  - Tokenises input into ANSI codes, space-runs and word-runs
  - Greedily fills each output line with whole words
  - Re-emits active ANSI SGR codes at the start of every new line
  - Falls back to hard (character-boundary) breaking only for words
    that are individually wider than the configured width

patch_termflow_word_wrap() monkey-patches this function over
termflow.ansi.utils.wrap_ansi (and the public re-export in
termflow.ansi) at startup via messaging/__init__.py, so every
TermflowRenderer instance benefits automatically.
termflow/render/text.py uses
which creates a module-local binding. Monkey-patching only
termflow.ansi.utils and termflow.ansi left this local reference
pointing at the original mid-word-chopping implementation.

Also patch termflow.render.text.wrap_ansi directly so all call
sites pick up the word-boundary-aware replacement.
- New schedule_type='daily_at' with schedule_value='HH:MM' or 'HH:MM,HH:MM,...'
- Daemon fires task if any target time has passed today and last_run predates it
- Restart-safe: daemon downtime at fire-time still fires on next wakeup
- Wizard: new 'Daily at specific time(s)...' menu option with HH:MM validation
- Wizard: friendly summary display ('daily at 09:00,17:00')
- cron remains as a stub with warning (would need croniter)
Covers parse_daily_at_times() and the daily_at branch of should_run_task():
- Single/multiple/whitespace-padded time parsing
- Edge cases: midnight, 23:59, empty string, invalid formats
- Invalid entries skipped with warning, valid entries still used
- Never-run task fires after target, not before (boundary inclusive)
- Does not re-fire after running today
- Fires when last_run was yesterday (restart-safe)
- Fires when daemon missed the window and caught up later
- last_run before target but same day still triggers
- Multi-time: first due + second future, first ran + second future/due
- Disabled tasks never fire
- All-invalid schedule_value returns False with warning
- Midnight target edge case
- Fix test_wizard_code_puppy_first: 'Daily' was removed from
  schedule_map when daily_at replaced it; switched to 'Every hour'
- Add test_daily_at_single_time: single HH:MM → schedule_type=daily_at
- Add test_daily_at_multiple_times: comma list preserved verbatim
- Add test_daily_at_cancel_time_input: None from TextInputMenu → None
- Add test_daily_at_all_invalid_times: no valid times → None
- Add test_daily_at_strips_invalid_times: bad entries dropped, valid kept
- Add test_daily_at_summary_display: confirms 'daily at HH:MM' in
  stdout and raw 'daily_at' type string does not leak into summary
Replace the hard-coded 🐶 at every user-facing runtime surface with a
new puppy_emoji config (defaults to 🐶, mirrors how puppy_name works).

- config.get_puppy_emoji() / set_puppy_emoji() with validation
  (non-empty, max 16 chars to allow ZWJ sequences like 🐕\u200d🦺)
- puppy_emoji exposed via get_config_keys() so /set & tab-completion work
- Replaced hard-coded 🐶 in:
  - interactive prompt prefix (prompt_toolkit_completion)
  - startup banner & 'Continuing in Interactive Mode' (cli_runner)
  - /show status header (config_commands)
  - onboarding wizard slides
  - agent self-intro ('what is code puppy?')
  - API landing page (/) & terminal page (/terminal)
  - API startup/shutdown log lines
- terminal.html stays a static asset; emoji is substituted at request
  time so we don't have to add Jinja2 just for one token.
- Intentionally left alone: pack-leader ASCII art, MOTD historical
  content, oauth_puppy_html sprites, README/SETUP docs, plugin
  examples, error_logging docstring, agent product display name
  ('Code-Puppy 🐶'). Those are branding/content, not user identity.

Tests: 10 new tests for get/set/validation + updated 2 snapshot tests
in TestGetConfigKeys for the new key. End-to-end verified via
TestClient that '/' and '/terminal' both reflect a custom emoji.

Use it: /set puppy_emoji 🦊
Make SpinnerBase.current_frame render with the live puppy_emoji config
on every access, so '/set puppy_emoji 🦴' flips both the prompt prefix
AND the 'Betty White is thinking... ( 🦴 )' spinner without a restart.

- New _build_spinner_frames(emoji) helper as the single source of truth
  for the bouncing-puppy frame template (DRY: same shape used by both
  the frozen FRAMES default and the live current_frame render)
- SpinnerBase.FRAMES kept as a class attribute frozen at import time
  with DEFAULT_PUPPY_EMOJI for backward compatibility — tests and any
  external code that reads it stay green
- current_frame property now calls get_puppy_emoji() per access so the
  user's chosen emoji shows live; frame index logic untouched
- Made two existing spinner tests hermetic by patching get_puppy_emoji
  to the default; they were silently coupled to the developer's real
  puppy.cfg (caught when puppy_emoji=🦴 was set in the dev config)
- Added 2 new tests:
  * current_frame uses live puppy_emoji on access (not import-time)
  * FRAMES class attr stays default for backward compat

Note: SpinnerBase.THINKING_MESSAGE / WAITING_MESSAGE / puppy_name still
freeze at import (pre-existing behavior — puppy_name changes don't
propagate to spinner without restart). Out of scope for puppymoji;
worth a separate ticket if anyone ever cares.
@mattnico
Copy link
Copy Markdown
Contributor Author

Closing in favor of a clean re-cut from origin/main. The branch was inadvertently based on a local main with 7 unrelated WIP commits (termflow word-wrap work) that polluted the diff and caused unrelated CI failures (ruff F841 on markdown_patches.py + missing export-test update for patch_termflow_word_wrap). Reopening shortly with just the 2 puppymoji commits on top of origin/main.

@mattnico mattnico closed this Apr 22, 2026
@mattnico mattnico deleted the feat/puppymoji branch April 22, 2026 02:22
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