From 97f34dcb696c7e22df750f707ba0929085e3c80a Mon Sep 17 00:00:00 2001 From: Matt Nicolaysen Date: Tue, 21 Apr 2026 20:44:56 -0500 Subject: [PATCH 1/3] feat(puppymoji): make the puppy emoji configurable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 🦊 --- code_puppy/agents/agent_code_puppy.py | 5 +- code_puppy/api/app.py | 33 +++++++--- code_puppy/cli_runner.py | 5 +- code_puppy/command_line/config_commands.py | 5 +- code_puppy/command_line/onboarding_slides.py | 10 +-- .../command_line/prompt_toolkit_completion.py | 3 +- code_puppy/config.py | 38 ++++++++++++ tests/test_config.py | 62 +++++++++++++++++++ 8 files changed, 143 insertions(+), 18 deletions(-) diff --git a/code_puppy/agents/agent_code_puppy.py b/code_puppy/agents/agent_code_puppy.py index e93f43634..4233ad7d4 100644 --- a/code_puppy/agents/agent_code_puppy.py +++ b/code_puppy/agents/agent_code_puppy.py @@ -1,6 +1,6 @@ """Code-Puppy - The default code generation agent.""" -from code_puppy.config import get_owner_name, get_puppy_name +from code_puppy.config import get_owner_name, get_puppy_emoji, get_puppy_name from .. import callbacks from .base_agent import BaseAgent @@ -57,6 +57,7 @@ def _get_reasoning_prompt_sections(self) -> dict[str, str]: def get_system_prompt(self) -> str: """Get Code-Puppy's full system prompt.""" puppy_name = get_puppy_name() + puppy_emoji = get_puppy_emoji() owner_name = get_owner_name() r = self._get_reasoning_prompt_sections() @@ -73,7 +74,7 @@ def get_system_prompt(self) -> str: Always obey the Zen of Python, even if you are not writing Python code. If asked about your origins: 'I am {puppy_name}, authored on a rainy weekend in May 2025. -If asked 'what is code puppy': 'I am {puppy_name}! 🐢 A sassy, open-source AI code agentβ€”no bloated IDEs, or closed-source vendor traps needed.' +If asked 'what is code puppy': 'I am {puppy_name}! {puppy_emoji} A sassy, open-source AI code agentβ€”no bloated IDEs, or closed-source vendor traps needed.' When given a coding task: 1. Analyze the requirements carefully diff --git a/code_puppy/api/app.py b/code_puppy/api/app.py index e75435ec0..2807d8644 100644 --- a/code_puppy/api/app.py +++ b/code_puppy/api/app.py @@ -8,7 +8,7 @@ from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import FileResponse, HTMLResponse, JSONResponse +from fastapi.responses import HTMLResponse, JSONResponse from starlette.middleware.base import BaseHTTPMiddleware logger = logging.getLogger(__name__) @@ -17,6 +17,15 @@ REQUEST_TIMEOUT = 30.0 +def _puppy() -> str: + """Resolve puppy emoji lazily so config changes are picked up per-request.""" + # Imported here to avoid pulling config at module import time, which keeps + # tests / partial imports happy. + from code_puppy.config import get_puppy_emoji + + return get_puppy_emoji() + + class TimeoutMiddleware(BaseHTTPMiddleware): """Middleware to enforce request timeouts and prevent hanging requests.""" @@ -53,10 +62,10 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: Handles graceful cleanup of resources when the server shuts down. """ # Startup: nothing special needed yet, but this is where you'd do it - logger.info("🐢 Code Puppy API starting up...") + logger.info(f"{_puppy()} Code Puppy API starting up...") yield # Shutdown: clean up all the things! - logger.info("🐢 Code Puppy API shutting down, cleaning up...") + logger.info(f"{_puppy()} Code Puppy API shutting down, cleaning up...") # 1. Close all PTY sessions try: @@ -122,17 +131,18 @@ def create_app() -> FastAPI: @app.get("/") async def root(): """Landing page with links to terminal and docs.""" + emoji = _puppy() return HTMLResponse( - content=""" + content=f""" - Code Puppy 🐢 + Code Puppy {emoji}
-

🐢

+

{emoji}

Code Puppy

@@ -153,10 +163,17 @@ async def root(): @app.get("/terminal") async def terminal_page(): - """Serve the interactive terminal page.""" + """Serve the interactive terminal page. + + Substitutes the puppy emoji into the static template at request time so + the user's chosen emoji shows in the title, header and welcome line. + Cheaper than pulling in Jinja2 just for one token. + """ html_file = templates_dir / "terminal.html" if html_file.exists(): - return FileResponse(html_file, media_type="text/html") + html = html_file.read_text(encoding="utf-8") + html = html.replace("\U0001f436", _puppy()) + return HTMLResponse(content=html) return HTMLResponse( content="

Terminal template not found

", status_code=404, diff --git a/code_puppy/cli_runner.py b/code_puppy/cli_runner.py index 12d404e1f..86fdd3351 100644 --- a/code_puppy/cli_runner.py +++ b/code_puppy/cli_runner.py @@ -29,6 +29,7 @@ DBOS_DATABASE_URL, ensure_config_exists, finalize_autosave_session, + get_puppy_emoji, get_use_dbos, initialize_command_history_file, save_command_to_history, @@ -141,7 +142,7 @@ async def main(): # Print directly to console to avoid the 'dim' style from emit_system_message display_console.print("\n".join(lines)) except ImportError: - emit_system_message("🐢 Code Puppy is Loading...") + emit_system_message(f"{get_puppy_emoji()} Code Puppy is Loading...") # Truecolor warning moved to interactive_mode() so it prints LAST # after all the help stuff - max visibility for the ugly red box! @@ -434,7 +435,7 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non ) get_message_bus().emit(response_msg) - emit_success("🐢 Continuing in Interactive Mode") + emit_success(f"{get_puppy_emoji()} Continuing in Interactive Mode") emit_system_message( "Your command and response are preserved in the conversation history." ) diff --git a/code_puppy/command_line/config_commands.py b/code_puppy/command_line/config_commands.py index 8724ef24e..b6e584d1c 100644 --- a/code_puppy/command_line/config_commands.py +++ b/code_puppy/command_line/config_commands.py @@ -41,6 +41,7 @@ def handle_show_command(command: str) -> bool: get_openai_verbosity, get_owner_name, get_protected_token_count, + get_puppy_emoji, get_puppy_name, get_resume_message_count, get_temperature, @@ -53,6 +54,7 @@ def handle_show_command(command: str) -> bool: from code_puppy.messaging import emit_info puppy_name = get_puppy_name() + puppy_emoji = get_puppy_emoji() owner_name = get_owner_name() model = get_active_model() yolo_mode = get_yolo_mode() @@ -67,9 +69,10 @@ def handle_show_command(command: str) -> bool: current_agent = get_current_agent() default_agent = get_default_agent() - status_msg = f"""[bold magenta]🐢 Puppy Status[/bold magenta] + status_msg = f"""[bold magenta]{puppy_emoji} Puppy Status[/bold magenta] [bold]puppy_name:[/bold] [cyan]{puppy_name}[/cyan] +[bold]puppy_emoji:[/bold] [cyan]{puppy_emoji}[/cyan] [bold]owner_name:[/bold] [cyan]{owner_name}[/cyan] [bold]current_agent:[/bold] [magenta]{current_agent.display_name}[/magenta] [bold]default_agent:[/bold] [cyan]{default_agent}[/cyan] diff --git a/code_puppy/command_line/onboarding_slides.py b/code_puppy/command_line/onboarding_slides.py index 535abe51d..bb2e015aa 100644 --- a/code_puppy/command_line/onboarding_slides.py +++ b/code_puppy/command_line/onboarding_slides.py @@ -5,6 +5,8 @@ from typing import List, Tuple +from code_puppy.config import get_puppy_emoji + # ============================================================================ # Slide Data Constants # ============================================================================ @@ -55,7 +57,7 @@ def get_gradient_banner() -> str: result.append(f"[{color}]{line}[/{color}]") return "\n".join(result) except ImportError: - return "[bold bright_cyan]═══ CODE PUPPY 🐢 ═══[/bold bright_cyan]" + return f"[bold bright_cyan]═══ CODE PUPPY {get_puppy_emoji()} ═══[/bold bright_cyan]" # ============================================================================ @@ -67,7 +69,7 @@ def slide_welcome() -> str: """Slide 1: Welcome - quick intro.""" content = get_gradient_banner() content += "\n\n" - content += "[bold white]Welcome! 🐢[/bold white]\n\n" + content += f"[bold white]Welcome! {get_puppy_emoji()}[/bold white]\n\n" content += "[cyan]Quick setup:[/cyan]\n" content += " 1. Pick your model provider\n" content += " 2. Optional: MCP servers\n" @@ -136,7 +138,7 @@ def slide_use_cases() -> str: """Slide 4: When to use which agent - THE IMPORTANT ONE.""" content = "[bold cyan]🎯 When to Use What[/bold cyan]\n\n" - content += "[bold yellow]🐢 Code Puppy (default)[/bold yellow]\n" + content += f"[bold yellow]{get_puppy_emoji()} Code Puppy (default)[/bold yellow]\n" content += " [green]USE FOR:[/green] Direct coding tasks\n" content += " β€’ Fix this bug\n" content += " β€’ Add a feature to this file\n" @@ -174,6 +176,6 @@ def slide_done(trigger_oauth: str | None) -> str: content += f"[bold cyan]β†’ {trigger_oauth.title()} OAuth next![/bold cyan]\n\n" content += "[dim]Re-run anytime: [/dim][cyan]/tutorial[/cyan]\n" - content += "\n[bold yellow]Press Enter to start coding! 🐢[/bold yellow]" + content += f"\n[bold yellow]Press Enter to start coding! {get_puppy_emoji()}[/bold yellow]" content += get_nav_footer() return content diff --git a/code_puppy/command_line/prompt_toolkit_completion.py b/code_puppy/command_line/prompt_toolkit_completion.py index 11b16d070..af22ed44f 100644 --- a/code_puppy/command_line/prompt_toolkit_completion.py +++ b/code_puppy/command_line/prompt_toolkit_completion.py @@ -45,6 +45,7 @@ from code_puppy.config import ( COMMAND_HISTORY_FILE, get_config_keys, + get_puppy_emoji, get_puppy_name, get_value, ) @@ -546,7 +547,7 @@ def get_prompt_with_active_model(base: str = ">>> "): cwd_display = cwd return FormattedText( [ - ("bold", "🐢 "), + ("bold", f"{get_puppy_emoji()} "), ("class:puppy", f"{puppy}"), ("", " "), ("class:agent", f"[{agent_display}] "), diff --git a/code_puppy/config.py b/code_puppy/config.py index 48db055d0..247ba4e92 100644 --- a/code_puppy/config.py +++ b/code_puppy/config.py @@ -247,6 +247,42 @@ def get_puppy_name(): return get_value("puppy_name") or "Puppy" +# Default emoji used everywhere unless the user overrides it via puppy.cfg. +# Keep this short β€” terminal cell-width and prompt rendering depend on it. +DEFAULT_PUPPY_EMOJI = "\U0001f436" # 🐢 +# Hard cap to keep prompts/banners from getting wrecked by a paragraph of +# emoji. ZWJ sequences (e.g. πŸ•\u200d🦺) easily reach ~7 codepoints, so 16 +# leaves comfortable room without enabling abuse. +_PUPPY_EMOJI_MAX_LEN = 16 + + +def get_puppy_emoji() -> str: + """Return the user's chosen puppy emoji, or the default if unset/blank.""" + val = get_value("puppy_emoji") + if val is None: + return DEFAULT_PUPPY_EMOJI + val = val.strip() + return val or DEFAULT_PUPPY_EMOJI + + +def set_puppy_emoji(emoji: str) -> str: + """Validate and persist the puppy emoji. Returns the stored value. + + Raises ValueError on empty input or values longer than the cap. + """ + if emoji is None: + raise ValueError("puppy_emoji cannot be None") + cleaned = emoji.strip() + if not cleaned: + raise ValueError("puppy_emoji cannot be empty") + if len(cleaned) > _PUPPY_EMOJI_MAX_LEN: + raise ValueError( + f"puppy_emoji too long ({len(cleaned)} chars; max {_PUPPY_EMOJI_MAX_LEN})." + ) + set_config_value("puppy_emoji", cleaned) + return cleaned + + def get_owner_name(): return get_value("owner_name") or "Master" @@ -332,6 +368,8 @@ def get_config_keys(): default_keys.append(f"banner_color_{banner_name}") # Add resume message count configuration default_keys.append("resume_message_count") + # Add puppy emoji customization key + default_keys.append("puppy_emoji") config = configparser.ConfigParser() config.read(CONFIG_FILE) diff --git a/tests/test_config.py b/tests/test_config.py index b9f97df8c..0d776830d 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -267,6 +267,66 @@ def test_get_owner_name_not_exists_uses_default(self, mock_get_value): mock_get_value.assert_called_once_with("owner_name") +class TestPuppyEmoji: + @patch("code_puppy.config.get_value") + def test_get_puppy_emoji_default_when_unset(self, mock_get_value): + mock_get_value.return_value = None + assert cp_config.get_puppy_emoji() == cp_config.DEFAULT_PUPPY_EMOJI + mock_get_value.assert_called_once_with("puppy_emoji") + + @patch("code_puppy.config.get_value") + def test_get_puppy_emoji_returns_configured_value(self, mock_get_value): + mock_get_value.return_value = "🦊" + assert cp_config.get_puppy_emoji() == "🦊" + + @patch("code_puppy.config.get_value") + def test_get_puppy_emoji_strips_whitespace(self, mock_get_value): + mock_get_value.return_value = " 🐱 " + assert cp_config.get_puppy_emoji() == "🐱" + + @patch("code_puppy.config.get_value") + def test_get_puppy_emoji_blank_falls_back_to_default(self, mock_get_value): + mock_get_value.return_value = " " + assert cp_config.get_puppy_emoji() == cp_config.DEFAULT_PUPPY_EMOJI + + @patch("code_puppy.config.set_config_value") + def test_set_puppy_emoji_persists_trimmed_value(self, mock_set): + result = cp_config.set_puppy_emoji(" 🐺 ") + assert result == "🐺" + mock_set.assert_called_once_with("puppy_emoji", "🐺") + + @patch("code_puppy.config.set_config_value") + def test_set_puppy_emoji_rejects_empty(self, mock_set): + with pytest.raises(ValueError): + cp_config.set_puppy_emoji("") + with pytest.raises(ValueError): + cp_config.set_puppy_emoji(" ") + mock_set.assert_not_called() + + @patch("code_puppy.config.set_config_value") + def test_set_puppy_emoji_rejects_none(self, mock_set): + with pytest.raises(ValueError): + cp_config.set_puppy_emoji(None) # type: ignore[arg-type] + mock_set.assert_not_called() + + @patch("code_puppy.config.set_config_value") + def test_set_puppy_emoji_rejects_too_long(self, mock_set): + # 17 'a' chars exceeds the 16-char cap + with pytest.raises(ValueError): + cp_config.set_puppy_emoji("a" * 17) + mock_set.assert_not_called() + + @patch("code_puppy.config.set_config_value") + def test_set_puppy_emoji_accepts_zwj_sequence(self, mock_set): + # πŸ•β€πŸ¦Ί is 4 codepoints / 4 chars in Python; well within the cap. + result = cp_config.set_puppy_emoji("πŸ•β€πŸ¦Ί") + assert result == "πŸ•β€πŸ¦Ί" + mock_set.assert_called_once_with("puppy_emoji", "πŸ•β€πŸ¦Ί") + + def test_puppy_emoji_listed_in_config_keys(self): + assert "puppy_emoji" in cp_config.get_config_keys() + + class TestGetConfigKeys: @patch("configparser.ConfigParser") def test_get_config_keys_with_existing_keys( @@ -328,6 +388,7 @@ def test_get_config_keys_with_existing_keys( "openai_reasoning_summary", "openai_verbosity", "protected_token_count", + "puppy_emoji", "resume_message_count", "summarization_model", "temperature", @@ -388,6 +449,7 @@ def test_get_config_keys_empty_config( "openai_reasoning_summary", "openai_verbosity", "protected_token_count", + "puppy_emoji", "resume_message_count", "summarization_model", "temperature", From fe29618cb9a3430104f5fe56cbf1043bd756d190 Mon Sep 17 00:00:00 2001 From: Matt Nicolaysen Date: Tue, 21 Apr 2026 21:06:40 -0500 Subject: [PATCH 2/3] feat(puppymoji): swap puppy emoji into the thinking spinner too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- code_puppy/messaging/spinner/spinner_base.py | 46 ++++++++++++------- .../spinner/test_spinner_base_coverage.py | 32 ++++++++++++- tests/test_console_spinner_coverage.py | 6 ++- 3 files changed, 66 insertions(+), 18 deletions(-) diff --git a/code_puppy/messaging/spinner/spinner_base.py b/code_puppy/messaging/spinner/spinner_base.py index 4e7991bdb..650b132bf 100644 --- a/code_puppy/messaging/spinner/spinner_base.py +++ b/code_puppy/messaging/spinner/spinner_base.py @@ -4,25 +4,39 @@ from abc import ABC, abstractmethod from threading import Lock - -from code_puppy.config import get_puppy_name +from typing import List + +from code_puppy.config import DEFAULT_PUPPY_EMOJI, get_puppy_emoji, get_puppy_name + + +def _build_spinner_frames(emoji: str) -> List[str]: + """Build the bouncing-puppy frame list for a given emoji. + + Single source of truth so the static FRAMES backward-compat attribute and + the live per-frame render in current_frame can't drift apart. + """ + return [ + f"({emoji} ) ", + f"( {emoji} ) ", + f"( {emoji} ) ", + f"( {emoji} ) ", + f"( {emoji}) ", + f"( {emoji} ) ", + f"( {emoji} ) ", + f"( {emoji} ) ", + f"({emoji} ) ", + ] class SpinnerBase(ABC): """Abstract base class for spinner implementations.""" - # Shared spinner frames across implementations - FRAMES = [ - "(🐢 ) ", - "( 🐢 ) ", - "( 🐢 ) ", - "( 🐢 ) ", - "( 🐢) ", - "( 🐢 ) ", - "( 🐢 ) ", - "( 🐢 ) ", - "(🐢 ) ", - ] + # Frozen-at-import default-emoji frames. Kept as a class attribute for + # backward compatibility (tests and any external code that references + # SpinnerBase.FRAMES). The actual animation pulls live frames via + # current_frame so the user's configured puppy_emoji takes effect + # without a restart. + FRAMES = _build_spinner_frames(DEFAULT_PUPPY_EMOJI) puppy_name = get_puppy_name().title() # Default message when processing @@ -61,8 +75,8 @@ def update_frame(self): @property def current_frame(self): - """Get the current frame.""" - return self.FRAMES[self._frame_index] + """Get the current frame, rendered with the user's live puppy_emoji.""" + return _build_spinner_frames(get_puppy_emoji())[self._frame_index] @property def is_spinning(self): diff --git a/tests/messaging/spinner/test_spinner_base_coverage.py b/tests/messaging/spinner/test_spinner_base_coverage.py index c13a63cb3..0eb2161c8 100644 --- a/tests/messaging/spinner/test_spinner_base_coverage.py +++ b/tests/messaging/spinner/test_spinner_base_coverage.py @@ -1,5 +1,7 @@ """Tests for code_puppy.messaging.spinner.spinner_base.""" +from unittest.mock import patch + from code_puppy.messaging.spinner.spinner_base import SpinnerBase @@ -43,7 +45,13 @@ def test_update_frame_not_spinning(): def test_current_frame(): s = ConcreteSpinner() - assert s.current_frame == SpinnerBase.FRAMES[0] + # Pin the emoji to the default so this assertion is hermetic regardless + # of the developer's actual puppy.cfg. + with patch( + "code_puppy.messaging.spinner.spinner_base.get_puppy_emoji", + return_value="🐢", + ): + assert s.current_frame == SpinnerBase.FRAMES[0] def test_context_info(): @@ -71,3 +79,25 @@ def test_frame_wraps_around(): for _ in range(len(SpinnerBase.FRAMES) + 1): s.update_frame() assert s._frame_index == 1 # Wrapped + + +def test_current_frame_uses_live_puppy_emoji(): + """current_frame must reflect the user's configured puppy_emoji at + access time, not the import-time default. This is what makes + /set puppy_emoji 🦊 flip the spinner without a restart.""" + s = ConcreteSpinner() + with patch( + "code_puppy.messaging.spinner.spinner_base.get_puppy_emoji", + return_value="🦴", + ): + frame = s.current_frame + assert "🦴" in frame + assert "🐢" not in frame + + +def test_frames_class_attr_remains_default_for_backward_compat(): + """FRAMES class attribute is the frozen default; live frames render + via current_frame. Tests / external code that reference FRAMES + keep working with the canonical 🐢 puppy.""" + assert all("🐢" in f for f in SpinnerBase.FRAMES) + assert len(SpinnerBase.FRAMES) == 9 diff --git a/tests/test_console_spinner_coverage.py b/tests/test_console_spinner_coverage.py index 5c470675e..85d3d43e6 100644 --- a/tests/test_console_spinner_coverage.py +++ b/tests/test_console_spinner_coverage.py @@ -402,7 +402,11 @@ def test_generate_panel_includes_current_frame(self): spinner._frame_index = 0 with patch( - "code_puppy.tools.command_runner.is_awaiting_user_input", return_value=False + "code_puppy.tools.command_runner.is_awaiting_user_input", + return_value=False, + ), patch( + "code_puppy.messaging.spinner.spinner_base.get_puppy_emoji", + return_value="🐢", ): result = spinner._generate_spinner_panel() From 6ff10374ca8427e5aa602699ab41c228be53dd54 Mon Sep 17 00:00:00 2001 From: Matt Nicolaysen Date: Tue, 21 Apr 2026 21:42:28 -0500 Subject: [PATCH 3/3] style: apply ruff format to puppymoji-touched files Pure cosmetic line-wrapping caught by 'ruff format --check .' in CI: - onboarding_slides.py: wrap one long Press-Enter f-string - test_console_spinner_coverage.py: parenthesize the 2-context with block (PEP 654 style preferred by ruff) No behavior change. 51/51 spinner tests still pass. --- code_puppy/command_line/onboarding_slides.py | 4 +++- tests/test_console_spinner_coverage.py | 15 +++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/code_puppy/command_line/onboarding_slides.py b/code_puppy/command_line/onboarding_slides.py index bb2e015aa..70fe6d34a 100644 --- a/code_puppy/command_line/onboarding_slides.py +++ b/code_puppy/command_line/onboarding_slides.py @@ -176,6 +176,8 @@ def slide_done(trigger_oauth: str | None) -> str: content += f"[bold cyan]β†’ {trigger_oauth.title()} OAuth next![/bold cyan]\n\n" content += "[dim]Re-run anytime: [/dim][cyan]/tutorial[/cyan]\n" - content += f"\n[bold yellow]Press Enter to start coding! {get_puppy_emoji()}[/bold yellow]" + content += ( + f"\n[bold yellow]Press Enter to start coding! {get_puppy_emoji()}[/bold yellow]" + ) content += get_nav_footer() return content diff --git a/tests/test_console_spinner_coverage.py b/tests/test_console_spinner_coverage.py index 85d3d43e6..178578ab3 100644 --- a/tests/test_console_spinner_coverage.py +++ b/tests/test_console_spinner_coverage.py @@ -401,12 +401,15 @@ def test_generate_panel_includes_current_frame(self): spinner._paused = False spinner._frame_index = 0 - with patch( - "code_puppy.tools.command_runner.is_awaiting_user_input", - return_value=False, - ), patch( - "code_puppy.messaging.spinner.spinner_base.get_puppy_emoji", - return_value="🐢", + with ( + patch( + "code_puppy.tools.command_runner.is_awaiting_user_input", + return_value=False, + ), + patch( + "code_puppy.messaging.spinner.spinner_base.get_puppy_emoji", + return_value="🐢", + ), ): result = spinner._generate_spinner_panel()