Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
e00b88b
feat(theme): add accessible theme-aware terminal styling
mpfaffenberger Jul 10, 2026
322cae7
chore: bump version [ci skip]
github-actions[bot] Jul 10, 2026
df73f6e
refactor(cli): simplify settings command surfaces
mpfaffenberger Jul 10, 2026
a01c85d
fix(chatgpt): align GPT-5.6 model capabilities
mpfaffenberger Jul 10, 2026
b738852
feat(queue): add full-screen prompt manager
mpfaffenberger Jul 10, 2026
b0907de
refactor(ui): remove emojis from tool output
mpfaffenberger Jul 10, 2026
27e0454
feat: improve compaction flow and Codex usage status
mpfaffenberger Jul 10, 2026
651a2f7
feat: improve queued turns and shell backgrounding
mpfaffenberger Jul 10, 2026
521460b
test: include queued emitter in messaging exports
mpfaffenberger Jul 10, 2026
460c821
chore: bump version [ci skip]
github-actions[bot] Jul 10, 2026
bda3b66
fix(retry): retry gateway 502s wrapped in ExceptionGroup instead of c…
JulienEllie Jul 10, 2026
6e60cd1
chore: bump version [ci skip]
github-actions[bot] Jul 10, 2026
ff3fdfe
feat(callbacks): let run_shell_command hooks rewrite the command (#545)
ashishsinghi Jul 10, 2026
52095fd
chore: bump version [ci skip]
github-actions[bot] Jul 10, 2026
db62a4d
feat: calm OAuth pages and refine queued banner
mpfaffenberger Jul 10, 2026
7725fa1
chore: bump version [ci skip]
github-actions[bot] Jul 10, 2026
634ea31
fix(skills): demote missing-skill-path warnings to debug
mpfaffenberger Jul 10, 2026
fd69a12
chore: bump version [ci skip]
github-actions[bot] Jul 10, 2026
0ce1cef
fix(statusline): cross-platform Windows support + Unicode crash preve…
rinadelph Jul 10, 2026
67c159c
fix(windows): disable VT input under Wave Terminal
mpfaffenberger Jul 10, 2026
269ff2d
chore: bump version [ci skip]
github-actions[bot] Jul 10, 2026
2a5157f
feat(diff): derive default highlights from active theme
mpfaffenberger Jul 10, 2026
e967536
Revert "fix(windows): disable VT input under Wave Terminal"
mpfaffenberger Jul 10, 2026
24bb580
chore: bump version [ci skip]
github-actions[bot] Jul 10, 2026
6e223f3
fix(fork): complete detached agent lifecycle
mpfaffenberger Jul 10, 2026
f38c66a
chore: bump version [ci skip]
github-actions[bot] Jul 10, 2026
e8cb918
Add runtime YOLO CLI override
mpfaffenberger Jul 11, 2026
80502c8
Revert "Add runtime YOLO CLI override"
mpfaffenberger Jul 11, 2026
b9a73df
feat: add runtime yolo CLI override (#551)
acoliver Jul 11, 2026
12448d7
Make diff and prompt colors theme-aware
mpfaffenberger Jul 11, 2026
2413e88
Isolate pytest from user XDG config
mpfaffenberger Jul 11, 2026
9880231
Make terminal rendering fully theme aware
mpfaffenberger Jul 11, 2026
79db63f
fix(terminal): disable XON/XOFF while persistent editor owns tty
mpfaffenberger Jul 11, 2026
d7c427d
Make prompt-toolkit TUIs theme aware
mpfaffenberger Jul 11, 2026
e904b18
Centralize prompt-toolkit semantic theme roles (#577)
mpfaffenberger Jul 11, 2026
9888305
Theme the /agent TUI (#578)
mpfaffenberger Jul 11, 2026
9dbc084
Theme the /model picker TUI (#579)
mpfaffenberger Jul 11, 2026
e9c860c
Theme the model settings TUI (#580)
mpfaffenberger Jul 11, 2026
769130e
Theme the add-model TUI (#581)
mpfaffenberger Jul 11, 2026
12e0a28
Theme the /resume TUI (#582)
mpfaffenberger Jul 11, 2026
62b541b
Theme the /set TUI (#583)
mpfaffenberger Jul 11, 2026
9316742
Theme the /colors TUI (#584)
mpfaffenberger Jul 11, 2026
f500faf
Theme the diff selector TUI (#585)
mpfaffenberger Jul 11, 2026
fb32dfe
Theme the judges TUIs (#586)
mpfaffenberger Jul 11, 2026
f434573
Theme the MCP binding TUIs (#587)
mpfaffenberger Jul 11, 2026
f6af7b4
Theme the /mcp install TUI (#588)
mpfaffenberger Jul 11, 2026
ecd2dae
Theme the MCP custom-server form TUI (#589)
mpfaffenberger Jul 11, 2026
9238678
Theme the onboarding wizard TUI (#590)
mpfaffenberger Jul 11, 2026
923942d
Theme the Universal Constructor TUIs (#591)
mpfaffenberger Jul 11, 2026
23159d7
Theme the skills manager TUI (#592)
mpfaffenberger Jul 11, 2026
c64c714
Theme the skills installer TUI (#593)
mpfaffenberger Jul 11, 2026
49cd74b
Theme the hook manager TUI (#594)
mpfaffenberger Jul 11, 2026
07aece0
Theme the plugin manager TUI (#595)
mpfaffenberger Jul 11, 2026
953b978
Theme the prune TUI (#596)
mpfaffenberger Jul 11, 2026
ff31255
Theme the spinner picker TUI (#597)
mpfaffenberger Jul 11, 2026
5bcac8c
Theme the steer queue TUI (#598)
mpfaffenberger Jul 11, 2026
64ae3ff
Theme the theme picker TUI (#599)
mpfaffenberger Jul 11, 2026
ea2e3ad
Theme the ask-user-question TUI (#600)
mpfaffenberger Jul 11, 2026
0b9b7bf
Theme the shared tool selector TUIs (#601)
mpfaffenberger Jul 11, 2026
782e597
Fix CI model provisioning for releases
mpfaffenberger Jul 11, 2026
1f2c500
chore: bump version [ci skip]
mpfaffenberger Jul 11, 2026
d11c6c9
chore: trigger release build
mpfaffenberger Jul 11, 2026
5f19391
chore: bump version [ci skip]
github-actions[bot] Jul 11, 2026
075393d
Manually increment version to satisfy PyPI
mpfaffenberger Jul 11, 2026
2af48b1
chore: bump version [ci skip]
github-actions[bot] Jul 11, 2026
c9b1ccf
ci: add maintainer-triggered OCR reviews
mpfaffenberger Jul 12, 2026
907dd35
ci: allow slow OCR model reviews
mpfaffenberger Jul 12, 2026
5abd608
chore: bump version [ci skip]
github-actions[bot] Jul 12, 2026
47761de
ci: disable MiniMax thinking for OCR
mpfaffenberger Jul 12, 2026
36fb522
chore: bump version [ci skip]
github-actions[bot] Jul 12, 2026
27547b8
fix(retry): retry in-band SSE 5xx (gateway 502 over 200 stream) + wra…
JulienEllie Jul 13, 2026
284c694
chore: bump version [ci skip]
github-actions[bot] Jul 13, 2026
d8d6f70
split: db/query layer
sskmlm Jul 13, 2026
8e8227f
split: session persistence and load
sskmlm Jul 13, 2026
211058a
split: permission plumbing
sskmlm Jul 13, 2026
2c8e312
split: websocket protocol and runtime
sskmlm Jul 13, 2026
5103bf0
split: browser chat frontend
sskmlm Jul 13, 2026
fcf4aed
fix(frontend): harden permission request rendering
sskmlm Jul 14, 2026
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
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ jobs:
}
}
JSON
echo "Wrote $HOME/.code_puppy/extra_models.json"
# Tests isolate user config, and several fallback contracts require a
# non-empty bundled catalog.
cp "$HOME/.code_puppy/extra_models.json" code_puppy/models.json
echo "Wrote CI model catalog and test-only bundled fallback"

- name: Debug environment variables
env:
Expand Down
77 changes: 77 additions & 0 deletions .github/workflows/ocr-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: OpenCodeReview

on:
issue_comment:
types: [created]

# Only an authorized /open-code-review comment shares the PR's concurrency
# group. Unrelated comments must not cancel a review already in progress.
concurrency:
group: >-
${{
(
github.event.issue.pull_request
&& github.event.comment.user.type != 'Bot'
&& (
github.event.comment.author_association == 'MEMBER'
|| github.event.comment.author_association == 'OWNER'
|| github.event.comment.author_association == 'COLLABORATOR'
)
&& startsWith(github.event.comment.body, '/open-code-review')
)
&& format('ocr-{0}', github.event.issue.number)
|| format('ocr-noop-{0}', github.run_id)
}}
cancel-in-progress: true

permissions:
contents: read
pull-requests: write

jobs:
review:
name: Review pull request
if: |
github.event.issue.pull_request
&& github.event.comment.user.type != 'Bot'
&& (
github.event.comment.author_association == 'MEMBER'
|| github.event.comment.author_association == 'OWNER'
|| github.event.comment.author_association == 'COLLABORATOR'
)
&& startsWith(github.event.comment.body, '/open-code-review')
runs-on: ubuntu-latest
timeout-minutes: 30

steps:
- name: Resolve pull request
id: pr
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
script: |
const { data: pullRequest } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
});
core.setOutput('base_ref', pullRequest.base.ref);
core.setOutput('head_sha', pullRequest.head.sha);
core.setOutput('title', pullRequest.title);

- name: Run OpenCodeReview
uses: alibaba/open-code-review@c3da87885086ff41ed8f2f77b65c7a3c471e01aa
with:
llm_url: ${{ secrets.OCR_LLM_URL }}
llm_auth_token: ${{ secrets.OCR_LLM_AUTH_TOKEN }}
llm_model: ${{ vars.OCR_LLM_MODEL }}
llm_use_anthropic: ${{ vars.OCR_LLM_USE_ANTHROPIC }}
llm_extra_body: '{"chat_template_kwargs":{"thinking_mode":"disabled"}}'
llm_timeout: "900"
base_ref: ${{ steps.pr.outputs.base_ref }}
head_sha: ${{ steps.pr.outputs.head_sha }}
background: ${{ steps.pr.outputs.title }}
ocr_version: "1.7.7"
review_concurrency: "1"
sticky_summary: "true"
incremental: "true"
upload_artifacts: "true"
6 changes: 5 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,11 @@ jobs:
}
}
JSON
echo "Wrote $HOME/.code_puppy/extra_models.json"
# Tests isolate user config, and several fallback contracts require a
# non-empty bundled catalog. Keep this test-only copy out of releases:
# the publish job starts from a fresh checkout.
cp "$HOME/.code_puppy/extra_models.json" code_puppy/models.json
echo "Wrote CI model catalog and test-only bundled fallback"

- name: Debug environment variables
env:
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,6 @@ code_puppy/bundled_skills/
.claude/hooks/ts-hooks/dist/

.json

.state/
.maestro/*
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ and skills (`<CWD>/.code_puppy/skills/`).
| `agent_run_start` | Before agent task | `(agent_name, model_name, session_id=None) -> None` |
| `agent_run_end` | After agent run | `(agent_name, model_name, session_id=None, success=True, error=None, response_text=None, metadata=None) -> None` |
| `load_prompt` | System prompt assembly | `() -> str \| None` |
| `run_shell_command` | Before shell exec | `(context, command, cwd=None, timeout=60) -> dict \| None` (return `{"blocked": True}` to block) |
| `run_shell_command` | Before shell exec | `(context, command, cwd=None, timeout=60) -> dict \| None` (return `{"blocked": True}` to block, `{"rewrite": "<new cmd>"}` to transparently transform) |
| `file_permission` | Before file op | `(context, file_path, operation, ...) -> bool` |
| `pre_tool_call` | Before tool executes | `(tool_name, tool_args, context=None) -> Any` |
| `post_tool_call` | After tool finishes | `(tool_name, tool_args, result, duration_ms, context=None) -> Any` |
Expand Down
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,7 @@ You can toggle DBOS via either of these options:

- CLI config (persists): `/set enable_dbos false` to disable (enabled by default)


Config takes precedence if set; otherwise the environment variable is used.
DBOS durable execution is implemented by the `dbos_durable_exec` plugin and uses its own DBOS system database. It is separate from the Puppy Desk chat history database at `~/.puppy_desk/chat_messages.db`.

### Configuration

Expand Down
12 changes: 8 additions & 4 deletions code_puppy/agents/_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,12 +370,16 @@ def load_model_with_fallback(
continue
try:
model = ModelFactory.get_model(candidate, models_config)
emit_info(
f"Using fallback model: {candidate}", message_group=message_group
)
return model, candidate
except ValueError:
continue
if model is None:
# Missing credentials/provider reachability can make a model
# "configured" but unavailable at runtime. Keep searching for
# a *real* fallback instead of returning a None model that only
# explodes later in pydantic-ai run().
continue
emit_info(f"Using fallback model: {candidate}", message_group=message_group)
return model, candidate

friendly = (
"No valid model could be loaded. Update the model configuration or "
Expand Down
16 changes: 14 additions & 2 deletions code_puppy/agents/_compaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
get_compaction_threshold,
get_protected_token_count,
)
from code_puppy.messaging import emit_error, emit_info, emit_warning
from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
from code_puppy.messaging.spinner import format_context_info, update_spinner_context
from code_puppy.summarization_agent import SummarizationError, run_summarization_sync

Expand Down Expand Up @@ -282,6 +282,8 @@ def compact(
messages: List[ModelMessage],
model_max: int,
context_overhead: int,
*,
force: bool = False,
) -> Tuple[List[ModelMessage], List[ModelMessage]]:
"""Unified compaction entrypoint. Replaces ``message_history_processor``.

Expand All @@ -291,6 +293,8 @@ def compact(
messages: Current message history (already accumulated by the caller).
model_max: Effective model context window in tokens.
context_overhead: Estimated overhead for system prompt + tool schemas.
force: Compact regardless of the configured context threshold. Used by
mid-run ``/compact`` at the next safe model-call boundary.

Returns:
``(new_messages, dropped_messages_for_hash_tracking)``.
Expand All @@ -312,7 +316,7 @@ def compact(
update_spinner_context(context_summary)

threshold = get_compaction_threshold()
if proportion_used <= threshold:
if not force and proportion_used <= threshold:
return messages, []

strategy = get_compaction_strategy()
Expand Down Expand Up @@ -473,12 +477,20 @@ def history_processor(messages: List[ModelMessage]) -> List[ModelMessage]:
history.append(msg)
messages_added += 1

from code_puppy.messaging.pause_controller import get_pause_controller

pause_controller = get_pause_controller()
force_compaction = pause_controller.take_compaction_request()
new_history, dropped = compact(
agent,
history,
agent._get_model_context_length(),
agent._estimate_context_overhead(),
force=force_compaction,
)
if force_compaction:
detail = "" if dropped else " History was already minimal."
emit_success(f"Mid-run compaction complete.{detail}")
agent._message_history = new_history
for m in dropped:
compacted_hashes.add(hash_message(m))
Expand Down
16 changes: 14 additions & 2 deletions code_puppy/agents/_key_listeners.py
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,15 @@ def _enter_cbreak() -> None:
# every control char is delivered verbatim, exactly like the
# raw mode (tty.setraw) the classic prompt_toolkit path used.
#
# setcbreak ALSO leaves software flow control enabled. With
# IXON set, an accidental Ctrl+S tells the tty to STOP output;
# the next editor repaint then blocks forever in stdout.flush()
# ON THE KEY-LISTENER THREAD. Input is alive but can't dispatch
# another key (including Ctrl+Q) because it is trapped writing
# the prompt — indistinguishable from a bricked terminal. This
# exact stack was captured live on 2026-07-11. Clear IXON/IXOFF
# while we own stdin; original attrs restore on suspend/exit.
#
# Pure-keybinding Ctrl+C: disable the tty's INTR character so
# ^C is delivered as a raw \x03 byte instead of becoming a
# SIGINT — the POSIX mirror of Windows' session-wide
Expand All @@ -896,8 +905,11 @@ def _enter_cbreak() -> None:
# semantics return whenever we release stdin.
try:
attrs = termios.tcgetattr(fd)
attrs[0] &= ~termios.ICRNL # iflag
attrs[3] &= ~termios.IEXTEN # lflag
attrs[0] &= ~termios.ICRNL # preserve Ctrl+J vs Enter
attrs[0] &= ~termios.IXON # Ctrl+S must reach the editor
if hasattr(termios, "IXOFF"):
attrs[0] &= ~termios.IXOFF
attrs[3] &= ~termios.IEXTEN # deliver Ctrl+V/Ctrl+O raw
try:
vdisable = os.fpathconf(fd, "PC_VDISABLE")
except (OSError, ValueError, AttributeError):
Expand Down
21 changes: 12 additions & 9 deletions code_puppy/agents/_run_signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,8 @@ def schedule_agent_cancel(force: bool = False) -> None:
# =============================================================================
#
# ``PauseController`` is a process-wide singleton. Without explicit hygiene:
# - A cancelled run that left ``request_steer`` items in the queue would
# have those items consumed by the NEXT agent run (possibly a totally
# different session / agent), as if the user had typed them.
# - A ``now`` steer that missed the final model boundary would linger into
# the next run instead of becoming the queued turn the user still expects.
# - A run that crashed mid-pause would leave the controller in a paused
# state, freezing the next run's spinner + event stream.
# Both bugs are bad. The two helpers below scrub that state.
Expand All @@ -117,20 +116,24 @@ def reset_pause_state_at_run_start() -> None:
"""Scrub stale ``PauseController`` state before a fresh agent run.

Called from the top of ``run_with_mcp`` BEFORE any agent work begins.
If we find pending steers from a prior cancelled run, emit a warning
(with a preview of the first one) so the user knows we discarded
something rather than silently swallowing it.
Undelivered ``now`` steers are preserved as queued turns: they missed
their intended history-processor boundary, but they are still user input.
"""
from code_puppy.messaging.pause_controller import get_pause_controller

pc = get_pause_controller()
# Clear any stale paused state (e.g. from a prior run that crashed
# mid-pause). Safe / idempotent if already resumed.
pc.resume()
stale_steers = pc.drain_pending_steer()
if stale_steers:
stale_compactions = pc.drain_compaction_requests()
if stale_compactions:
emit_warning(
f"Discarded {len(stale_steers)} stale steering message(s) from a previous run."
f"Discarded {stale_compactions} stale compaction request(s) from a previous run."
)
deferred_steers = pc.defer_pending_steer_now()
if deferred_steers:
emit_info(
f"Queued {deferred_steers} steering message(s) that missed the previous run."
)


Expand Down
Loading
Loading