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
13 changes: 13 additions & 0 deletions code_puppy/plugins/prompt_newline/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Plugin: drop user input onto a fresh line below the prompt chrome.

When enabled, transforms

🐶 puppy [agent] [model] (~/very/long/cwd) >>> typed text

into

🐶 puppy [agent] [model] (~/very/long/cwd) >>>
typed text

Toggle at runtime with ``/prompt_newline [on|off]``. Persisted in puppy.cfg.
"""
21 changes: 21 additions & 0 deletions code_puppy/plugins/prompt_newline/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Plugin-local config for the prompt_newline plugin."""

from __future__ import annotations

from code_puppy.config import get_value, set_config_value

_CONFIG_KEY = "prompt_newline"
_TRUTHY = ("true", "1", "yes", "on")


def is_enabled() -> bool:
"""Return True if the prompt_newline hack is enabled. Default: False."""
cfg_val = get_value(_CONFIG_KEY)
if cfg_val is None:
return False
return str(cfg_val).strip().lower() in _TRUTHY


def set_enabled(enabled: bool) -> None:
"""Persist the on/off switch to puppy.cfg."""
set_config_value(_CONFIG_KEY, "true" if enabled else "false")
163 changes: 163 additions & 0 deletions code_puppy/plugins/prompt_newline/register_callbacks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
"""Register callbacks for the prompt_newline plugin.

This plugin is a tiny ergonomics hack: it places the user's input cursor on a
*new line* below the puppy/agent/model/cwd chrome, so long working-directory
paths don't squeeze the typing area.

It hooks two things:

* ``startup`` — wraps ``get_prompt_with_active_model`` so the FormattedText it
returns gets a trailing ``\\n`` appended **at call time** (so the slash
command toggle takes effect immediately, no restart needed).
* ``custom_command`` / ``custom_command_help`` — exposes ``/prompt_newline``
for runtime on/off, persisted via ``puppy.cfg``.

Default: OFF. Opt-in only.
"""

from __future__ import annotations

from typing import List, Optional, Tuple

from code_puppy.callbacks import register_callback
from code_puppy.plugins.prompt_newline.config import is_enabled, set_enabled

_COMMAND_NAME = "prompt_newline"
_PATCH_ATTR = "_prompt_newline_original"


def _emit_info(message: str) -> None:
from code_puppy.messaging import emit_info

emit_info(message)


def _emit_error(message: str) -> None:
from code_puppy.messaging import emit_error

emit_error(message)


def _emit_success(message: str) -> None:
from code_puppy.messaging import emit_success

emit_success(message)


def _append_newline(formatted_text):
"""Return a new FormattedText with a trailing newline tuple.

``FormattedText`` is a ``list`` subclass of ``(style, text)`` tuples, so we
rebuild it rather than mutating in place — the upstream caller may cache
or reuse the returned object.
"""
from prompt_toolkit.formatted_text import FormattedText

try:
return FormattedText(list(formatted_text) + [("", "\n")])
except Exception:
# Defensive: never break the prompt if the upstream shape changes.
return formatted_text


def _install_prompt_patch() -> None:
"""Monkey-patch ``get_prompt_with_active_model`` to honor ``is_enabled()``.

Idempotent: re-running won't double-wrap.
"""
from code_puppy.command_line import prompt_toolkit_completion as ptc

if getattr(ptc, _PATCH_ATTR, None) is not None:
return # Already patched

original = ptc.get_prompt_with_active_model
setattr(ptc, _PATCH_ATTR, original)

def patched(base: str = ">>> "):
result = original(base)
if is_enabled():
return _append_newline(result)
return result

ptc.get_prompt_with_active_model = patched


def _on_startup() -> None:
try:
_install_prompt_patch()
except Exception as exc:
# Plugins must fail gracefully — never crash the app.
_emit_error(f"prompt_newline: failed to install prompt patch — {exc}")


def _custom_help() -> List[Tuple[str, str]]:
return [
(
_COMMAND_NAME,
"Toggle placing user input on a new line below the prompt chrome",
)
]


def _parse_toggle_arg(command: str) -> Optional[bool]:
"""Parse ``/prompt_newline [on|off|true|false|toggle]``.

Returns:
True/False for an explicit set, or ``None`` to mean "flip current".
"""
tokens = command.strip().split()
if len(tokens) < 2:
return None # bare /prompt_newline → flip
arg = tokens[1].lower()
if arg in ("on", "true", "1", "yes", "enable", "enabled"):
return True
if arg in ("off", "false", "0", "no", "disable", "disabled"):
return False
if arg in ("toggle",):
return None
raise ValueError(arg)


def _handle_prompt_newline_command(command: str) -> bool:
try:
target = _parse_toggle_arg(command)
except ValueError as exc:
_emit_error(
f"/{_COMMAND_NAME}: unknown argument '{exc.args[0]}'. "
"Usage: /prompt_newline [on|off|toggle]"
)
return True

if target is None:
target = not is_enabled()

set_enabled(target)
state = "ON" if target else "OFF"
_emit_success(f"🐶 prompt_newline is now {state}")
if target:
_emit_info("Your input will appear on a fresh line below the prompt chrome.")
else:
_emit_info("Prompt is back to single-line mode.")
return True


def _handle_custom_command(command: str, name: str):
if name != _COMMAND_NAME:
return None
return _handle_prompt_newline_command(command)


register_callback("startup", _on_startup)
register_callback("custom_command", _handle_custom_command)
register_callback("custom_command_help", _custom_help)


__all__ = [
"_append_newline",
"_custom_help",
"_handle_custom_command",
"_handle_prompt_newline_command",
"_install_prompt_patch",
"_on_startup",
"_parse_toggle_arg",
]
181 changes: 181 additions & 0 deletions tests/plugins/test_prompt_newline_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
"""Tests for the prompt_newline plugin."""

from __future__ import annotations

import importlib
import sys
from unittest.mock import MagicMock, patch

import pytest


def _plugin_module():
sys.modules.setdefault("dbos", MagicMock())
return importlib.import_module(
"code_puppy.plugins.prompt_newline.register_callbacks"
)


def _config_module():
return importlib.import_module("code_puppy.plugins.prompt_newline.config")


def test_custom_help_lists_command():
entries = dict(_plugin_module()._custom_help())
assert "prompt_newline" in entries


def test_handle_custom_command_ignores_unrelated_names():
assert _plugin_module()._handle_custom_command("/nope", "nope") is None


def test_parse_toggle_arg_defaults_to_none():
assert _plugin_module()._parse_toggle_arg("/prompt_newline") is None


@pytest.mark.parametrize("arg", ["on", "true", "1", "yes", "ENABLE"])
def test_parse_toggle_arg_truthy(arg):
assert _plugin_module()._parse_toggle_arg(f"/prompt_newline {arg}") is True


@pytest.mark.parametrize("arg", ["off", "false", "0", "no", "Disable"])
def test_parse_toggle_arg_falsy(arg):
assert _plugin_module()._parse_toggle_arg(f"/prompt_newline {arg}") is False


def test_parse_toggle_arg_toggle_keyword_returns_none():
assert _plugin_module()._parse_toggle_arg("/prompt_newline toggle") is None


def test_parse_toggle_arg_rejects_garbage():
with pytest.raises(ValueError):
_plugin_module()._parse_toggle_arg("/prompt_newline banana")


def test_append_newline_returns_formatted_text_with_trailing_newline():
from prompt_toolkit.formatted_text import FormattedText

original = FormattedText([("class:arrow", ">>> ")])
result = _plugin_module()._append_newline(original)

assert isinstance(result, FormattedText)
assert list(result)[-1] == ("", "\n")
# original must not be mutated
assert ("", "\n") not in list(original)


def test_install_prompt_patch_is_idempotent():
module = _plugin_module()
from code_puppy.command_line import prompt_toolkit_completion as ptc

original = ptc.get_prompt_with_active_model
try:
module._install_prompt_patch()
first_patched = ptc.get_prompt_with_active_model
module._install_prompt_patch()
second_patched = ptc.get_prompt_with_active_model
assert first_patched is second_patched
# Original is preserved on the module for restoration
assert getattr(ptc, "_prompt_newline_original") is original
finally:
ptc.get_prompt_with_active_model = original
if hasattr(ptc, "_prompt_newline_original"):
delattr(ptc, "_prompt_newline_original")


def test_patched_prompt_appends_newline_only_when_enabled():
module = _plugin_module()
from code_puppy.command_line import prompt_toolkit_completion as ptc

original = ptc.get_prompt_with_active_model
try:
module._install_prompt_patch()

with patch(
"code_puppy.plugins.prompt_newline.register_callbacks.is_enabled",
return_value=False,
):
disabled_result = ptc.get_prompt_with_active_model()

with patch(
"code_puppy.plugins.prompt_newline.register_callbacks.is_enabled",
return_value=True,
):
enabled_result = ptc.get_prompt_with_active_model()

assert ("", "\n") not in list(disabled_result)
assert list(enabled_result)[-1] == ("", "\n")
finally:
ptc.get_prompt_with_active_model = original
if hasattr(ptc, "_prompt_newline_original"):
delattr(ptc, "_prompt_newline_original")


def test_handle_command_persists_explicit_on(tmp_path, monkeypatch):
# Point the config at a throwaway file so we don't trash real puppy.cfg
cfg_file = tmp_path / "puppy.cfg"
monkeypatch.setattr("code_puppy.config.CONFIG_FILE", str(cfg_file))

module = _plugin_module()
cfg = _config_module()

with (
patch(
"code_puppy.plugins.prompt_newline.register_callbacks._emit_success"
) as mock_success,
patch("code_puppy.plugins.prompt_newline.register_callbacks._emit_info"),
):
result = module._handle_custom_command("/prompt_newline on", "prompt_newline")

assert result is True
assert cfg.is_enabled() is True
assert "ON" in str(mock_success.call_args)


def test_handle_command_flips_when_no_arg(tmp_path, monkeypatch):
cfg_file = tmp_path / "puppy.cfg"
monkeypatch.setattr("code_puppy.config.CONFIG_FILE", str(cfg_file))

module = _plugin_module()
cfg = _config_module()

cfg.set_enabled(False)
with (
patch("code_puppy.plugins.prompt_newline.register_callbacks._emit_success"),
patch("code_puppy.plugins.prompt_newline.register_callbacks._emit_info"),
):
module._handle_custom_command("/prompt_newline", "prompt_newline")
assert cfg.is_enabled() is True

with (
patch("code_puppy.plugins.prompt_newline.register_callbacks._emit_success"),
patch("code_puppy.plugins.prompt_newline.register_callbacks._emit_info"),
):
module._handle_custom_command("/prompt_newline", "prompt_newline")
assert cfg.is_enabled() is False


def test_handle_command_rejects_garbage_arg(tmp_path, monkeypatch):
cfg_file = tmp_path / "puppy.cfg"
monkeypatch.setattr("code_puppy.config.CONFIG_FILE", str(cfg_file))

module = _plugin_module()

with patch(
"code_puppy.plugins.prompt_newline.register_callbacks._emit_error"
) as mock_error:
result = module._handle_custom_command(
"/prompt_newline banana", "prompt_newline"
)

assert result is True
mock_error.assert_called_once()
assert "banana" in str(mock_error.call_args)


def test_is_enabled_defaults_to_false(tmp_path, monkeypatch):
cfg_file = tmp_path / "puppy.cfg"
monkeypatch.setattr("code_puppy.config.CONFIG_FILE", str(cfg_file))

cfg = _config_module()
assert cfg.is_enabled() is False
Loading