diff --git a/packages/catalog/CHANGELOG.md b/packages/catalog/CHANGELOG.md index f16a6fc12..518f1aef6 100644 --- a/packages/catalog/CHANGELOG.md +++ b/packages/catalog/CHANGELOG.md @@ -197,7 +197,7 @@ ### Fixed -- Fixed Claude 4.6 routing on the `google-antigravity` (and `google-gemini-cli`) Cloud Code Assist providers, whose backend exposes the models asymmetrically: `claude-sonnet-4-6` has no `-thinking` twin and `claude-opus-4-6` has only the `-thinking` twin. The shared `thinkingPair` family was routing thinking efforts on `claude-sonnet-4-6` to a non-existent `claude-sonnet-4-6-thinking` wire id (404 `Requested entity was not found`); replaced both 4.6 entries with bespoke single-wire families that declare the dead ids as `retiredMembers` so `reconcileRetiredRouting` re-points stale bundled-catalog and SQLite-cache rows away from the 404 wire id. Refreshed the bundled `models.json` Sonnet 4.6 entry whose stored `effortRouting` still targeted the dead `-thinking` id. Added `claude-sonnet-4-6` and `claude-opus-4-6-thinking` entries to `ANTIGRAVITY_MODEL_WIRE_PROFILES` capped at the backend's 64000-output-token limit (over-cap requests 400'd with `Request contains an invalid argument`); `modelEnum` is now optional on `AntigravityModelWireProfile` since the Claude wire ids are accepted without a captured `labels.model_enum`. ([#3067](https://github.com/can1357/oh-my-pi/issues/3067)) +- Fixed Claude 4.6 routing on the `google-antigravity` (and `google-gemini-cli`) Cloud Code Assist providers, whose backend exposes the models asymmetrically: `claude-sonnet-4-6` has no `-thinking` twin and `claude-opus-4-6` has only the `-thinking` twin. The shared `thinkingPair` family was routing thinking efforts on `claude-sonnet-4-6` to a non-existent `claude-sonnet-4-6-thinking` wire id (404 `Requested entity was not found`); replaced both 4.6 entries with bespoke single-wire families so every effort and off resolve to the live wire id. Added `claude-sonnet-4-6` and `claude-opus-4-6-thinking` entries to `ANTIGRAVITY_MODEL_WIRE_PROFILES` capped at the backend's 64000-output-token limit (over-cap requests 400'd with `Request contains an invalid argument`); `modelEnum` is now optional on `AntigravityModelWireProfile` since the Claude wire ids are accepted without a captured `labels.model_enum`. ([#3067](https://github.com/can1357/oh-my-pi/issues/3067)) ## [16.1.3] - 2026-06-19 diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 0687b217c..73c619312 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,10 +2,27 @@ ## [Unreleased] +### Breaking Changes + +- **Settings:** `hooks` and `customTools` arrays replaced with single `extensions` array +- **CLI:** `--hook` and `--tool` flags replaced with `--extension` / `-e` +- **Directories:** `hooks/`, `tools/` → `extensions/`; `commands/` → `prompts/` +- **Types:** See type renames above +- **SDK:** See SDK migration above + ### Added - Added `providers.anthropic.serverSideFallback` (default off; UI in the "Model → Retry & Fallback" group). When enabled, Claude Fable 5 / Mythos 5 requests carry `fallbacks: [{ model: "claude-opus-4-8" }]` via Anthropic's server-side-fallback beta chain so classifier-blocked turns are retried on Opus 4.8 without breaking the current call. Opt-in only — leaving it off preserves the pre-fallback behavior. ([#4177](https://github.com/can1357/oh-my-pi/issues/4177)) - Added `task.softRequestBudgetNotice` (default off) to opt into the subagent soft-budget wrap-up steering notice while keeping the 1.5x graceful abort guard active. +- Added `WebSearchProviderError` class with HTTP status for actionable provider error messages +- Hook API: `ctx.ui.setTitle(title)` allows hooks to set the terminal window/tab title ([#446](https://github.com/badlogic/pi-mono/pull/446) by [@aliou](https://github.com/aliou)) +- Configurable double-escape action: choose whether double-escape with empty editor opens `/tree` (default) or `/branch`. Configure via `/settings` or `doubleEscapeAction` in settings.json ([#404](https://github.com/badlogic/pi-mono/issues/404)) +- Vertex AI provider (`google-vertex`): access Gemini models via Google Cloud Vertex AI using Application Default Credentials ([#300](https://github.com/badlogic/pi-mono/pull/300) by [@default-anton](https://github.com/default-anton)) +- Built-in provider overrides in `models.json`: override just `baseUrl` to route a built-in provider through a proxy while keeping all its models, or define `models` to fully replace the provider ([#406](https://github.com/badlogic/pi-mono/pull/406) by [@yevhen](https://github.com/yevhen)) +- Automatic image resizing: images larger than 2000x2000 are resized for better model compatibility. Original dimensions are injected into the prompt. Controlled via `/settings` or `images.autoResize` in settings.json. ([#402](https://github.com/badlogic/pi-mono/pull/402) by [@mitsuhiko](https://github.com/mitsuhiko)) +- Alt+Enter keybind to queue follow-up messages while agent is streaming +- `Theme` and `ThemeColor` types now exported for hooks using `ctx.ui.custom()` +- Terminal window title now displays "pi - dirname" to identify which project session you're in ([#407](https://github.com/badlogic/pi-mono/pull/407) by [@kaofelix](https://github.com/kaofelix)) ### Changed @@ -14,13 +31,34 @@ - Improved TUI responsiveness and reduced CPU usage during long-running tool sessions by throttling status-line redraws and optimizing subagent persistence checks. - Updated the advisor system prompt and documentation to accurately reflect WATCHDOG.yml tool grants. - Improved DuckDuckGo web search error clarity and documented datacenter/shared-egress limitations in provider settings. +- Changed `isAutoresearchShCommand()` to use proper command-line argument parsing instead of regex, improving accuracy for complex shell invocations +- Changed autoresearch initialization prompt to display collected tradeoff metrics in the setup summary +- Changed `command-initialize.md` template to include guidance on preflight requirements, comparability invariants, and marking measurement-critical files as off-limits +- Changed `command-initialize.md` to instruct users to write or update `autoresearch.program.md` with durable heuristics and repo-specific strategy +- Changed autoresearch resume guidance to emphasize continuing on the current protected branch rather than switching branches +- Changed autoresearch prompt to clarify that `autoresearch.md` holds durable conclusions while `autoresearch.ideas.md` is the scratch backlog +- Changed autoresearch prompt guidance to require stable measurement harness and fixed benchmark inputs unless intentionally starting a new segment +- Changed autoresearch prompt to recommend keeping equal or near-equal results when they materially simplify implementation +- Changed `init_experiment` to reset pending run state (checks, duration, ASI, artifact directory) when initializing a new segment +- Changed `log_experiment` to set `autoResumeArmed` flag after successfully logging a run to enable auto-resume on next agent turn +- Changed `run_experiment` to set `autoResumeArmed` flag and update dashboard after completing a run +- Changed auto-resume logic to only prompt when a new pending run exists or when `autoResumeArmed` is explicitly set, preventing duplicate prompts +- Changed path normalization in contract validation to use `path.posix.normalize()` for consistent path handling +- Extended extension `registerProvider()` typing with OAuth provider support and source-aware registration metadata. +- Extensions can have their own `package.json` with dependencies (resolved via jiti) +- Documentation: `docs/hooks.md` and `docs/custom-tools.md` merged into `docs/extensions.md` +- Examples: `examples/hooks/` and `examples/custom-tools/` merged into `examples/extensions/` +- README: Extensions section expanded with custom tools, commands, events, state persistence, shortcuts, flags, and UI examples +- SDK: `customTools` option now accepts `ToolDefinition[]` directly (simplified from `Array<{ path?, tool }>`) +- SDK: `extensions` option accepts `ExtensionFactory[]` for inline extensions +- SDK: `additionalExtensionPaths` replaces both `additionalHookPaths` and `additionalCustomToolPaths` +- Editor component now uses word wrapping instead of character-level wrapping for better readability ([#382](https://github.com/badlogic/pi-mono/pull/382) by [@nickseelert](https://github.com/nickseelert)) ### Fixed - Fixed transcript rebuilds preserving synthetic assistant stop errors for tool calls instead of letting later tool results replace them. - Fixed sequential patch/edit failures leaving dirty buffers in multi-file edits by flushing the LSP writethrough queue upfront on early abort paths - Fixed the subagent task preview erroneously showing "ctrl+o: Expand" hints on output lines capped inside the already-expanded view - - Fixed the cross-file write batching regression in `apply_patch` multi-file operations by reverting to flushing only on the last file write or explicitly on early failure paths, and refactored error counting to use clean booleans. - Fixed collab teardown (`/collab stop`, unrecoverable relay drop) cancelling a hook selector/editor the host user was actively typing in: teardown resolved pending guest asks the same way as a guest cancel, so the mirrored race dismissed the local dialog and dropped its input. Teardown now settles guest asks as `unavailable`, and the local dialog keeps running and wins with its eventual answer. - Fixed RPC mode deferred shutdown (`pi.shutdown()`) killing the process while a background-dispatched `bash` command was still running: the response frame is now written before exit, and a shutdown requested mid-bash fires once the command settles even when no further client frames arrive. @@ -46,7 +84,7 @@ - Fixed /copy code and /copy cmd commands being treated as normal prompts instead of copying the requested blocks. - Fixed interactive bash status line not updating after directory changes (cd). - Fixed session title refreshes ignoring user TITLE_SYSTEM.md overrides during replans, and prevented auto-generated titles from incorrectly preserving all-caps text from user messages. -- Fixed the live todo HUD going stale during long tool-use loops by adding mid-run reminders for incomplete items. +- Fixed the live todo HUD going stale during long tool-use loops via a hidden mid-run reconciliation hint. The hint is model-only (never rendered in the TUI or transcript), fires only after sustained mutating work (`bash`/`eval`/`edit`/`write`/`ast_edit` results — read-only exploration never counts), and is capped at two per prompt cycle, separate from the stop-time incomplete-todo reminder ladder. - Fixed /shake and mid-stream chat rebuilds erasing active LLM output. - Fixed RpcClient failing to restart after being stopped or failing on initial startup. - Fixed /collab web guests being unable to answer ask tool questions by routing host UI requests through writable collab peers. @@ -74,7 +112,7 @@ - Improved robustness of MCP authentication error detection and header-based server discovery. - Fixed reliable detection of 401/403 authorization failures during Smithery commands and HTTP RPCs. - Improved streaming preview responsiveness for write, edit, and eval tools by decoding streamed string arguments incrementally. -- Added retry-path diagnostics for assistant-tail removal and scheduled continuations after transient provider errors. +- Fixed retry handling after transient provider errors: the failed assistant tail is restored before the rescheduled continuation, and skipped continuations carry their skip reason. - Fixed CJK history rendering issues across repeated compactions. - Fixed user-invoked skills failing to identify themselves or resolve relative paths across various execution paths. - Fixed type errors introduced by the merge sweep: restored the ask row-budget priority field, narrowed dereferenced schema property access, and updated stale test API usage. @@ -85,6 +123,17 @@ - Fixed user-configured LiteLLM discovery providers keeping stale reseller display-name suffixes for up to 24 hours after upgrade by invalidating the warm model cache. - Fixed `mergeTaskBranches` and `applyNestedPatches` leaving stage 1/2/3 unmerged entries in `.git/index` when the post-merge stash pop conflicted with the cherry-picked HEAD. The corrupted index survived indefinitely and every subsequent overlay-isolated task inherited it through the lower layer, causing `captureRepoDeltaPatch` to emit `diff --cc` output that `git apply` rejects with "No valid patches in input". The stash restore now runs behind a 3-way preflight check (`git apply --3way --check`) and a `reset --hard HEAD` cleanup fallback; the stash entry is preserved for manual recovery on conflict, and the merged commits still land on HEAD. ([#4175](https://github.com/can1357/oh-my-pi/issues/4175)) - Fixed macOS `Command+V` image pastes in Ghostty by binding the Kitty `super+v` key event to the image-paste action alongside `Ctrl+V`. ([#4178](https://github.com/can1357/oh-my-pi/issues/4178)) +- Fixed a rare Bun GC segfault (exit 133) during model discovery: every discovery fetch armed an uncancellable `AbortSignal.timeout(...)` whose timer outlived the request (instantly against a mocked or fast endpoint). The pending timer fired later — sometimes during an unrelated allocation or test teardown — set the signal's abort `reason`, and crashed JSC's concurrent garbage collector while it marked the wrapped reason (`JSAbortSignal::visitAdditionalChildren`). Discovery now runs each fetch under a `withTimeoutSignal` helper that clears the backing timer the instant the operation settles, so the signal is never left armed on the heap. +- Fixed boundary duplication warnings to always display when replacement lines match the next surviving line, even when auto-correction is disabled +- Fixed secondary metrics validation to properly reject missing configured metrics and new metrics without force flag +- Fixed ASI data cloning to prevent prototype pollution attacks by filtering reserved property names +- Fixed CLI `--api-key` handling for deferred model resolution by applying runtime API key overrides after extension model selection. +- Fixed extension provider registration cleanup to remove stale source-scoped custom API/OAuth providers across extension reloads. +- Edit tool diff not displaying in TUI due to race condition between async preview computation and tool execution +- `/model` selector now opens instantly instead of waiting for OAuth token refresh. Token refresh is deferred until a model is actually used. +- Shift+Space, Shift+Backspace, and Shift+Delete now work correctly in Kitty-protocol terminals (Kitty, WezTerm, etc.) instead of being silently ignored ([#411](https://github.com/badlogic/pi-mono/pull/411) by [@nathyong](https://github.com/nathyong)) +- `AgentSession.prompt()` now throws if called while the agent is already streaming, preventing race conditions. Use `steer()` or `followUp()` to queue messages during streaming. +- Ctrl+C now works like Escape in selector components, so mashing Ctrl+C will eventually close the program ([#400](https://github.com/badlogic/pi-mono/pull/400) by [@mitsuhiko](https://github.com/mitsuhiko)) ## [16.2.13] - 2026-07-01 @@ -92,7 +141,6 @@ - Fixed `models.yml` remote compaction schema support for V2 streaming endpoint fields. ([#4146](https://github.com/can1357/oh-my-pi/issues/4146)) - Fixed the SSH tool to reject `cwd` values of `~` and `~/...` before sending guaranteed-bad quoted tilde paths to remote POSIX shells. ([#4002](https://github.com/can1357/oh-my-pi/issues/4002)) -- Status line token throughput segment now uses a dedicated tachometer icon (`icon.throughput`) instead of reusing the output arrow; cache read/write segments use a single database icon instead of stacking input/output arrows alongside it. ## [16.2.12] - 2026-07-01 @@ -481,7 +529,7 @@ ### Fixed -- Fixed Ctrl+Z hanging the terminal after any tool call had run: the TUI tore down (`ui.stop()`) but the process kept running in `Sl+` state, leaving the user with a dead terminal recoverable only via `kill -9`. The embedded `brush-core` shell behind every bash tool call installs a tokio SIGTSTP listener on `Process::wait` (`crates/vendor/brush-core/src/sys/unix/signal.rs::tstp_signal_listener` → `tokio::signal::unix::signal(SIGTSTP)`); per tokio's contract, the first call for a SignalKind permanently replaces the kernel-default handler for the lifetime of the process. So the first bash invocation — even `/usr/bin/true` — silently overrode SIGTSTP's "stop" default, and `InputController.handleCtrlZ`'s subsequent `process.kill(0, "SIGTSTP")` was swallowed by tokio. The handler now sends `SIGSTOP` (uncatchable, unblockable, unignorable) to the foreground process group, so the kernel parks omp regardless of installed handlers and the shell sees the whole job stop even when omp runs behind a wrapper (`npx`, `pnpm exec`, `bunx`, …) or as one stage of a pipeline. MCP stdio servers now spawn detached into their own session — they're insulated both from terminal job-control signals (which used to stop their process trees and leave the JSONL read loop blocked on silent pipes) and from the new pgid=0 suspend itself ([#3461](https://github.com/can1357/oh-my-pi/issues/3461)). +- Fixed Ctrl+Z hanging the terminal after any tool call had run: the TUI tore down (`ui.stop()`) but the process kept running in `Sl+` state, leaving the user with a dead terminal recoverable only via `kill -9`. The embedded `brush-core` shell behind every bash tool call installs a tokio SIGTSTP listener on `Process::wait` (`crates/brush-core-vendored/src/sys/unix/signal.rs::tstp_signal_listener` → `tokio::signal::unix::signal(SIGTSTP)`); per tokio's contract, the first call for a SignalKind permanently replaces the kernel-default handler for the lifetime of the process. So the first bash invocation — even `/usr/bin/true` — silently overrode SIGTSTP's "stop" default, and `InputController.handleCtrlZ`'s subsequent `process.kill(0, "SIGTSTP")` was swallowed by tokio. The handler now sends `SIGSTOP` (uncatchable, unblockable, unignorable) to the foreground process group, so the kernel parks omp regardless of installed handlers and the shell sees the whole job stop even when omp runs behind a wrapper (`npx`, `pnpm exec`, `bunx`, …) or as one stage of a pipeline. MCP stdio servers now spawn detached into their own session — they're insulated both from terminal job-control signals (which used to stop their process trees and leave the JSONL read loop blocked on silent pipes) and from the new pgid=0 suspend itself ([#3461](https://github.com/can1357/oh-my-pi/issues/3461)). - Fixed image-only composer submissions while the agent is streaming being treated as empty input, which dropped the image or aborted the active turn when another message was queued. Pending pasted images now count as submit content for Enter and Ctrl+Enter follow-ups. ([#3467](https://github.com/can1357/oh-my-pi/issues/3467)) - Fixed `omp gallery --state` accepting lifecycle tokens that did not match displayed state labels and rendering unknown state values as `· undefined`; displayed labels now work as aliases, invalid values fail with a valid-token list, and failed gallery fixtures visibly render failures. ([#3473](https://github.com/can1357/oh-my-pi/issues/3473)) - Fixed the bash tool's snapshotted `mise()` shell function dying with `command: command not found:` because `$__MISE_EXE` was empty in the replay shell. `generateSnapshotScript` captured the function via `declare -f`/`typeset -f` but only ever re-exported `PATH`, so every other env var the rc file set (notably the `*_EXE` sidecar `mise activate` exports) was lost; the function body then expanded `command "$__MISE_EXE" "$@"` to `command "" …` and died with exit 127. The snapshot script now scans captured function bodies for `$VAR` / `${VAR…}` references and re-emits `export NAME='value'` for each referenced var that is currently set (with a denylist for shell-internal names like `PATH`/`HOME`/`BASH_*`/`LC_*` plus a likely-secret denylist for `*TOKEN*`/`*SECRET*`/`*API_KEY*`/`*PASSWORD*`/`*PRIVATE_KEY*`/`*ACCESS_KEY*`/`*CREDENTIAL*`/`*SESSION_KEY*`), the snapshot script `umask 077`s itself and the JS caller chmods the snapshot file/dir to `0600`/`0700` so the new export pass can't leak secrets into a shared tmp dir. Fixes mise, asdf shims, direnv-style helpers, and other activation idioms that pair a function with a helper env var. `getShellConfigFile` now also honours `env.HOME` (falling back to `os.homedir()`) so sandboxed callers can target a non-default rc. ([#3470](https://github.com/can1357/oh-my-pi/issues/3470)) @@ -3380,12 +3428,6 @@ ## [15.2.1] - 2026-05-21 -### Added - -- Added `.omp-plugin/marketplace.json` as a preferred marketplace catalog path. `fetchMarketplace` now searches `.omp-plugin/marketplace.json` before `.claude-plugin/marketplace.json` for every local and cloned source. Lets a single marketplace repository publish a tool-specific catalog (e.g. an omp-only superset of a shared Claude Code marketplace) without forcing the omp/Claude distinction into per-plugin tagging. Mirrors the `package.json#omp.extensions` precedence pattern; the `.claude-plugin/marketplace.json` fallback keeps every existing marketplace loading unchanged. - -## [15.2.1] - 2026-05-21 - ### Fixed - Fixed compaction routing to the wrong provider when `modelRoles.default` is set to a different model than the active chat. Auto- and manual compaction now prefer the active session's model and only fall back to role-based candidates when the current model has no usable credentials. Previously, an Anthropic chat with `modelRoles.default = openai/gpt-5` would compact through OpenAI (including the remote-compaction endpoint), even though the live conversation never used OpenAI. @@ -11491,6 +11533,72 @@ pi --extension ./safety.ts -e ./todo.ts - Expanded keybinding documentation to list all 32 supported symbol keys with notes on ctrl+symbol behavior ([#450](https://github.com/badlogic/pi-mono/pull/450) by [@kaofelix](https://github.com/kaofelix)) +## [0.34.0] - 2026-01-04 + +### Added + +- Hook API: `before_agent_start` handlers can now return `systemPromptAppend` to dynamically append text to the system prompt for that turn. Multiple hooks' appends are concatenated. +- Hook API: `before_agent_start` handlers can now return multiple messages (all are injected, not just the first) +- New example hook: `tools.ts` - Interactive `/tools` command to enable/disable tools with session persistence +- New example hook: `pirate.ts` - Demonstrates `systemPromptAppend` to make the agent speak like a pirate +- Tool registry now contains all built-in tools (read, bash, edit, write, grep, find, ls) even when `--tools` limits the initially active set. Hooks can enable any tool from the registry via `pi.setActiveTools()`. +- System prompt now automatically rebuilds when tools change via `setActiveTools()`, updating tool descriptions and guidelines to match the new tool set +- Hook errors now display full stack traces for easier debugging + +### Changed + +- Removed image placeholders after copy & paste, replaced with inserting image file paths directly. ([#442](https://github.com/badlogic/pi-mono/pull/442) by [@mitsuhiko](https://github.com/mitsuhiko)) + +### Fixed + +- Fixed potential text decoding issues in bash executor by using streaming TextDecoder instead of Buffer.toString() +- External editor (Ctrl-G) now shows full pasted content instead of `[paste #N ...]` placeholders ([#444](https://github.com/badlogic/pi-mono/pull/444) by [@aliou](https://github.com/aliou)) + +## [0.33.0] - 2026-01-04 + +### Breaking Changes + +- **Key detection functions removed from `@mariozechner/pi-tui`**: All `isXxx()` key detection functions (`isEnter()`, `isEscape()`, `isCtrlC()`, etc.) have been removed. Use `matchesKey(data, keyId)` instead (e.g., `matchesKey(data, "enter")`, `matchesKey(data, "ctrl+c")`). This affects hooks and custom tools that use `ctx.ui.custom()` with keyboard input handling. ([#405](https://github.com/badlogic/pi-mono/pull/405)) + +### Added + +- Clipboard image paste support via `Ctrl+V`. Images are saved to a temp file and attached to the message. Works on macOS, Windows, and Linux (X11). ([#419](https://github.com/badlogic/pi-mono/issues/419)) +- Configurable keybindings via `~/.pi/agent/keybindings.json`. All keyboard shortcuts (editor navigation, deletion, app actions like model cycling, etc.) can now be customized. Supports multiple bindings per action. ([#405](https://github.com/badlogic/pi-mono/pull/405) by [@hjanuschka](https://github.com/hjanuschka)) +- `/quit` and `/exit` slash commands to gracefully exit the application. Unlike double Ctrl+C, these properly await hook and custom tool cleanup handlers before exiting. ([#426](https://github.com/badlogic/pi-mono/pull/426) by [@ben-vargas](https://github.com/ben-vargas)) + +### Fixed + +- Subagent example README referenced incorrect filename `subagent.ts` instead of `index.ts` ([#427](https://github.com/badlogic/pi-mono/pull/427) by [@Whamp](https://github.com/Whamp)) + +## [0.32.3] - 2026-01-03 + +### Fixed + +- `--list-models` no longer shows Google Vertex AI models without explicit authentication configured +- JPEG/GIF/WebP images not displaying in terminals using Kitty graphics protocol (Kitty, Ghostty, WezTerm). The protocol requires PNG format, so non-PNG images are now converted before display. +- Version check URL typo preventing update notifications from working ([#423](https://github.com/badlogic/pi-mono/pull/423) by [@skuridin](https://github.com/skuridin)) +- Large images exceeding Anthropic's 5MB limit now retry with progressive quality/size reduction ([#424](https://github.com/badlogic/pi-mono/pull/424) by [@mitsuhiko](https://github.com/mitsuhiko)) + +## [0.32.2] - 2026-01-03 + +### Added + +- `$ARGUMENTS` syntax for custom slash commands as alternative to `$@` for all arguments joined. Aligns with patterns used by Claude, Codex, and OpenCode. Both syntaxes remain fully supported. ([#418](https://github.com/badlogic/pi-mono/pull/418) by [@skuridin](https://github.com/skuridin)) + +### Changed + +- **Slash commands and hook commands now work during streaming**: Previously, using a slash command or hook command while the agent was streaming would crash with "Agent is already processing". Now: + - Hook commands execute immediately (they manage their own LLM interaction via `pi.sendMessage()`) + - File-based slash commands are expanded and queued via steer/followUp + - `steer()` and `followUp()` now expand file-based slash commands and error on hook commands (hook commands cannot be queued) + - `prompt()` accepts new `streamingBehavior` option (`"steer"` or `"followUp"`) to specify queueing behavior during streaming + - RPC `prompt` command now accepts optional `streamingBehavior` field + ([#420](https://github.com/badlogic/pi-mono/issues/420)) + +### Fixed + +- Slash command argument substitution now processes positional arguments (`$1`, `$2`, etc.) before all-arguments (`$@`, `$ARGUMENTS`) to prevent recursive substitution when argument values contain dollar-digit patterns like `$100`. ([#418](https://github.com/badlogic/pi-mono/pull/418) by [@skuridin](https://github.com/skuridin)) + ## [0.32.1] - 2026-01-03 ### Added diff --git a/packages/coding-agent/test/agent-session-todo-mid-run-nudge.test.ts b/packages/coding-agent/test/agent-session-todo-mid-run-nudge.test.ts index 1e3e8d0c9..ead608277 100644 --- a/packages/coding-agent/test/agent-session-todo-mid-run-nudge.test.ts +++ b/packages/coding-agent/test/agent-session-todo-mid-run-nudge.test.ts @@ -7,22 +7,27 @@ import { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry"; import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; import { AgentSession, type AgentSessionEvent } from "@oh-my-pi/pi-coding-agent/session/agent-session"; import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage"; +import type { CustomMessage } from "@oh-my-pi/pi-coding-agent/session/messages"; import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager"; import { TodoTool, type ToolSession } from "@oh-my-pi/pi-coding-agent/tools"; import { TempDir } from "@oh-my-pi/pi-utils"; /** - * Regression coverage for issue #3651: the only structured "reconcile your - * todos" reminder used to fire at a text-only `agent_end`. A model running a - * long tool-use loop therefore got no nudge until the very last turn, then - * batch-flipped every task `done`. The contract this defends: + * Regression coverage for issue #3651 and its redesign: the mid-run todo + * reconciliation nudge keeps the live HUD honest during long runs, but is a + * gentle MODEL-ONLY hint — deliberately separate from the user-visible + * stop-time reminder ladder. The contract this defends: * - * 1. After {@link MID_RUN_TODO_NUDGE_TURN_THRESHOLD} consecutive tool-use - * turns without invoking the `todo` tool, the aside provider injects a - * `` for the next turn AND emits a `todo_reminder` event. - * 2. Sub-threshold counts do NOT inject anything. - * 3. Any `todo` tool call inside the run resets the counter, so an interleaved - * todo turn keeps the nudge silent. + * 1. Only SUCCESSFUL MUTATING tool results (bash/eval/edit/write/ast_edit) + * tick the counter. Read-only exploration (grep/read/glob/lsp) and + * errored results never do. + * 2. At {@link MID_RUN_TODO_NUDGE_MUTATION_THRESHOLD} mutations without a + * `todo` call, the aside provider injects a hidden custom message + * (`display: false`) — NO `todo_reminder` event, nothing renders. + * 3. A `todo` tool result resets the counter. + * 4. At most {@link MID_RUN_TODO_NUDGE_MAX_PER_CYCLE} nudges fire per + * prompt cycle. + * 5. The counter update lands synchronously with the message_end emit. * * Drives the aside provider directly: the production agent loop polls it * between tool-use turns (mid-work boundary in `agent-loop.ts`), so calling it @@ -38,7 +43,9 @@ describe("AgentSession mid-run todo reconciliation nudge", () => { let reminderEvents: Array>; let asideProvider: (() => AsideMessage[] | Promise) | undefined; - const THRESHOLD = 8; // mirrors MID_RUN_TODO_NUDGE_TURN_THRESHOLD + const THRESHOLD = 12; // mirrors MID_RUN_TODO_NUDGE_MUTATION_THRESHOLD + const MAX_PER_CYCLE = 2; // mirrors MID_RUN_TODO_NUDGE_MAX_PER_CYCLE + const NUDGE_TYPE = "mid-run-todo-nudge"; // mirrors MID_RUN_TODO_NUDGE_MESSAGE_TYPE function toolUseAssistant(toolName: string): AssistantMessage { const id = `call_${toolName}_${Date.now()}_${Math.random()}`; @@ -62,10 +69,6 @@ describe("AgentSession mid-run todo reconciliation nudge", () => { }; } - function emitToolUseTurn(toolName: string): void { - session.agent.emitExternalEvent({ type: "message_end", message: toolUseAssistant(toolName) }); - } - function textOnlyAssistant(): AssistantMessage { return { role: "assistant", @@ -85,6 +88,7 @@ describe("AgentSession mid-run todo reconciliation nudge", () => { timestamp: Date.now(), }; } + async function emitTextOnlyStop(): Promise { const msg = textOnlyAssistant(); session.agent.emitExternalEvent({ type: "message_end", message: msg }); @@ -92,9 +96,10 @@ describe("AgentSession mid-run todo reconciliation nudge", () => { session.agent.emitExternalEvent({ type: "agent_end", messages: [msg] }); } - function emitToolResult(toolName: string): void { + /** Production-shaped tool round trip: assistant toolCall turn + toolResult. */ + function emitToolResult(toolName: string, opts?: { isError?: boolean }): void { const toolCallId = `call_${toolName}_${Date.now()}_${Math.random()}`; - emitToolUseTurn(toolName); + session.agent.emitExternalEvent({ type: "message_end", message: toolUseAssistant(toolName) }); const content: TextContent[] = [{ type: "text", text: "ok" }]; session.agent.emitExternalEvent({ type: "message_end", @@ -103,7 +108,7 @@ describe("AgentSession mid-run todo reconciliation nudge", () => { toolCallId, toolName, content, - isError: false, + isError: opts?.isError ?? false, timestamp: Date.now(), }, }); @@ -114,24 +119,25 @@ describe("AgentSession mid-run todo reconciliation nudge", () => { * chain on `#messageEndPersistenceTail`. After a batch of synchronous emits * the counter only catches up once every queued persist task drains, so * tests yield a full event-loop tick before draining asides. + * + * Real-timer exception (ts-no-test-timers): `Bun.sleep(0)` is a single + * event-loop tick, not a tuned duration — the private persistence tail + * exposes no drain promise to await, and fake timers cannot flush it. */ async function settle(): Promise { await Bun.sleep(0); } - async function drainAsides(): Promise> { + async function drainNudges(): Promise { if (!asideProvider) throw new Error("aside provider was never captured"); const thunks = await asideProvider(); - const out: Array<{ role: string; text: string }> = []; + const out: CustomMessage[] = []; for (const entry of thunks) { const message = typeof entry === "function" ? entry() : entry; if (!message) continue; - if (message.role !== "developer") continue; - const content = message.content; - if (!Array.isArray(content)) continue; - for (const part of content) { - if (part.type === "text") out.push({ role: message.role, text: part.text }); - } + if (message.role !== "custom") continue; + if ((message as CustomMessage).customType !== NUDGE_TYPE) continue; + out.push(message as CustomMessage); } return out; } @@ -213,51 +219,73 @@ describe("AgentSession mid-run todo reconciliation nudge", () => { vi.restoreAllMocks(); }); - it("stays silent until the threshold of non-todo tool-use turns is reached", async () => { - for (let i = 0; i < THRESHOLD - 1; i++) emitToolUseTurn("edit"); + it("read-only exploration never ticks the counter, no matter how long", async () => { + for (let i = 0; i < THRESHOLD * 3; i++) emitToolResult(i % 2 === 0 ? "grep" : "read"); await settle(); - const messages = await drainAsides(); - expect(messages).toEqual([]); + expect(await drainNudges()).toEqual([]); expect(reminderEvents).toEqual([]); }); - it("injects a developer-role reminder once the threshold is reached", async () => { - for (let i = 0; i < THRESHOLD; i++) emitToolUseTurn("edit"); + it("stays silent below the mutation threshold", async () => { + for (let i = 0; i < THRESHOLD - 1; i++) emitToolResult("edit"); await settle(); - const messages = await drainAsides(); - expect(messages.length).toBe(1); - const text = messages[0]?.text ?? ""; - expect(text).toContain(""); - // Surfaces every incomplete task by content, not just a count. - expect(text).toContain("Sweep call sites"); - expect(text).toContain("Update tests"); - expect(text).toContain("Polish docs"); - // Carries the mid-run framing so the agent does not treat it as a stop-time prompt. - expect(text).toContain("Mid-run reminder 1/3"); + expect(await drainNudges()).toEqual([]); + expect(reminderEvents).toEqual([]); + }); - expect(reminderEvents.length).toBe(1); - expect(reminderEvents[0]?.attempt).toBe(1); - expect(reminderEvents[0]?.maxAttempts).toBe(3); - expect(reminderEvents[0]?.todos.length).toBe(3); + it("injects a hidden custom nudge at the threshold — no event, no render", async () => { + for (let i = 0; i < THRESHOLD; i++) emitToolResult("edit"); + + await settle(); + const nudges = await drainNudges(); + expect(nudges.length).toBe(1); + const nudge = nudges[0]; + // Hidden from the TUI/transcript, visible to the model only. + expect(nudge?.display).toBe(false); + const text = typeof nudge?.content === "string" ? nudge.content : ""; + expect(text).toContain(""); + expect(text).toContain("3 todo items"); + // Gentle hint, not the stop-time escalation ladder: no per-task + // enumeration, no attempt counter. + expect(text).not.toContain("Sweep call sites"); + expect(text).not.toMatch(/reminder \d\/\d/i); + + // SEPARATE concept from the stop-time reminder: no todo_reminder event, + // so nothing renders a TodoReminderComponent or reaches extensions. + expect(reminderEvents).toEqual([]); // Counter reset: another full runway is required before the next nudge, // so an immediate poll right after firing must NOT re-inject. - const followUp = await drainAsides(); - expect(followUp).toEqual([]); + expect(await drainNudges()).toEqual([]); + }); + + it("errored mutating results do not tick the counter", async () => { + for (let i = 0; i < THRESHOLD; i++) emitToolResult("bash", { isError: true }); + + await settle(); + expect(await drainNudges()).toEqual([]); }); it("does not nudge when a `todo` call has reset the counter mid-window", async () => { - // Seven non-todo turns get us within one of the threshold... - for (let i = 0; i < THRESHOLD - 1; i++) emitToolUseTurn("edit"); - // ...then a todo call resets the counter; the remaining runway is fresh. - emitToolUseTurn("todo"); - for (let i = 0; i < THRESHOLD - 1; i++) emitToolUseTurn("edit"); + for (let i = 0; i < THRESHOLD - 1; i++) emitToolResult("write"); + emitToolResult("todo"); + for (let i = 0; i < THRESHOLD - 1; i++) emitToolResult("write"); await settle(); - const messages = await drainAsides(); - expect(messages).toEqual([]); + expect(await drainNudges()).toEqual([]); + expect(reminderEvents).toEqual([]); + }); + + it("caps nudges per prompt cycle", async () => { + let fired = 0; + for (let cycle = 0; cycle < MAX_PER_CYCLE + 2; cycle++) { + for (let i = 0; i < THRESHOLD; i++) emitToolResult("edit"); + await settle(); + fired += (await drainNudges()).length; + } + expect(fired).toBe(MAX_PER_CYCLE); expect(reminderEvents).toEqual([]); }); @@ -265,57 +293,51 @@ describe("AgentSession mid-run todo reconciliation nudge", () => { // Regression for the review on PR #3652: pre-fix the counter update sat // after `await messageEndPersistence.persist(...)`, so the live counter // only caught up once microtasks drained. A poll between the emit burst - // and the persistence chain settling would observe stale state — a turn - // that JUST flipped a todo could still trip the nudge against the - // pre-reset counter. With the hoisted (synchronous) update, the - // production-shaped contract holds even when the aside poll runs in the - // same JS task as the emit, before any microtask gets a chance to fire. - for (let i = 0; i < THRESHOLD; i++) emitToolUseTurn("edit"); + // and the persistence chain settling would observe stale state. With the + // hoisted (synchronous) update, the production-shaped contract holds even + // when the aside poll runs in the same JS task as the emit. + for (let i = 0; i < THRESHOLD; i++) emitToolResult("edit"); if (!asideProvider) throw new Error("aside provider was never captured"); const result = asideProvider(); if (result instanceof Promise) throw new Error("aside provider unexpectedly returned a Promise"); - const messagesAfterThreshold = result + const nudges = result .map(entry => (typeof entry === "function" ? entry() : entry)) .filter((m): m is NonNullable => Boolean(m)) - .filter(m => m.role === "developer"); - // The threshold-hit fire is the proof point: pre-hoist, the eight - // increments are all queued microtasks, so this sync poll would see - // counter=0 and skip the nudge entirely. - expect(messagesAfterThreshold.length).toBe(1); + .filter(m => m.role === "custom" && (m as CustomMessage).customType === NUDGE_TYPE); + expect(nudges.length).toBe(1); }); it("stays silent when `todo` is not in the active-tool list, even if `todo.enabled` is still on", async () => { - // Regression for the review on PR #3652: an explicit active-tool list - // (or discovery-mode filtering) can drop `todo` from the slate while the - // setting flag stays true and an incomplete persisted/user-edited todo - // list survives. Asking the model to call a tool that is not in its - // schema would produce fabricated/unknown tool calls or loop on - // impossible reminders. Mirror {@link #createEagerTodoPrelude}'s guard. + // An explicit active-tool list (or discovery-mode filtering) can drop + // `todo` from the slate while the setting flag stays true. Asking the + // model to call a tool that is not in its schema would produce + // fabricated/unknown tool calls. Mirror {@link #createEagerTodoPrelude}. await session.setActiveToolsByName([]); expect(session.getActiveToolNames()).not.toContain("todo"); - for (let i = 0; i < THRESHOLD; i++) emitToolUseTurn("edit"); + for (let i = 0; i < THRESHOLD; i++) emitToolResult("edit"); await settle(); - const messages = await drainAsides(); - expect(messages).toEqual([]); + expect(await drainNudges()).toEqual([]); expect(reminderEvents).toEqual([]); }); - it("does not spend the pre-stop tool-turn count immediately after a stop-time reminder", async () => { + it("does not spend the pre-stop mutation count immediately after a stop-time reminder", async () => { vi.spyOn(session.agent, "continue").mockResolvedValue(); - for (let i = 0; i < THRESHOLD - 1; i++) emitToolUseTurn("edit"); + for (let i = 0; i < THRESHOLD - 1; i++) emitToolResult("edit"); await settle(); await emitTextOnlyStop(); await session.waitForIdle(); + // The stop-time path is the user-visible ladder: it emits the event. expect(reminderEvents.length).toBe(1); expect(reminderEvents[0]?.attempt).toBe(1); + // The stop-time reminder reset the mutation counter, so one more landed + // mutation (crossing the stale pre-reminder threshold) must stay silent. emitToolResult("edit"); await settle(); - const messages = await drainAsides(); - expect(messages).toEqual([]); + expect(await drainNudges()).toEqual([]); expect(reminderEvents.length).toBe(1); }); }); diff --git a/packages/coding-agent/test/issue-953-repro.test.ts b/packages/coding-agent/test/issue-953-repro.test.ts deleted file mode 100644 index 91371eb71..000000000 --- a/packages/coding-agent/test/issue-953-repro.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { beforeAll, describe, expect, it } from "bun:test"; -import type { SegmentContext } from "@oh-my-pi/pi-coding-agent/modes/components/status-line/segments"; -import { renderSegment } from "@oh-my-pi/pi-coding-agent/modes/components/status-line/segments"; -import { initTheme, theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme"; - -beforeAll(async () => { - await initTheme(); -}); - -function createCtx(usage: Partial): SegmentContext { - return { - session: { - state: {}, - isFastModeEnabled: () => false, - modelRegistry: { isUsingOAuth: () => false }, - sessionManager: undefined, - } as unknown as SegmentContext["session"], - width: 120, - compactThinkingLevel: false, - options: {}, - planMode: null, - loopMode: null, - goalMode: null, - collab: null, - usageStats: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - premiumRequests: 0, - cost: 0, - tokensPerSecond: null, - ...usage, - }, - contextPercent: 0, - contextTokens: 0, - contextWindow: 0, - autoCompactEnabled: false, - subagentCount: 0, - activeMs: 0, - activeRepo: null, - worktree: null, - git: { - branch: null, - status: null, - pr: null, - }, - usage: null, - }; -} - -describe("issue #953 cache status line icons", () => { - it("renders cache reads as cache output and cache writes as cache input", () => { - const cacheRead = renderSegment("cache_read", createCtx({ cacheRead: 28_919_910 })); - const cacheWrite = renderSegment("cache_write", createCtx({ cacheWrite: 1_759_992 })); - - expect(cacheRead.visible).toBe(true); - expect(cacheRead.content).toContain(theme.icon.cache); - expect(cacheRead.content).toContain(theme.icon.output); - expect(cacheRead.content).not.toContain(theme.icon.input); - - expect(cacheWrite.visible).toBe(true); - expect(cacheWrite.content).toContain(theme.icon.cache); - expect(cacheWrite.content).toContain(theme.icon.input); - expect(cacheWrite.content).not.toContain(theme.icon.output); - }); -}); - -describe("cache_hit segment", () => { - it("shows hit rate from cacheRead / (cacheRead + cacheWrite) when cacheWrite > 0", () => { - const segment = renderSegment("cache_hit", createCtx({ cacheRead: 7_500, cacheWrite: 2_500 })); - - expect(segment.visible).toBe(true); - expect(segment.content).toContain(theme.icon.cache); - // 7500 / (7500 + 2500) = 75% - expect(segment.content).toContain("75.00%"); - }); - - it("shows hit rate from cacheRead / (cacheRead + input) when cacheWrite = 0 (DeepSeek fallback)", () => { - // DeepSeek: cacheWrite=0, input=miss_tokens - const segment = renderSegment("cache_hit", createCtx({ cacheRead: 6_000, cacheWrite: 0, input: 4_000 })); - - expect(segment.visible).toBe(true); - // 6000 / (6000 + 4000) = 60% - expect(segment.content).toContain("60.00%"); - }); - - it("shows 100% when all input was cached (cacheWrite=0, input=0)", () => { - const segment = renderSegment("cache_hit", createCtx({ cacheRead: 5_000, cacheWrite: 0, input: 0 })); - - expect(segment.visible).toBe(true); - expect(segment.content).toContain("100.00%"); - }); - - it("is hidden when cacheRead is 0", () => { - const segment = renderSegment("cache_hit", createCtx({ cacheRead: 0, cacheWrite: 5_000 })); - - expect(segment.visible).toBe(false); - }); - - it("is hidden when there is no cache activity at all", () => { - const segment = renderSegment("cache_hit", createCtx({ cacheRead: 0, cacheWrite: 0, input: 1_000 })); - - expect(segment.visible).toBe(false); - }); -}); diff --git a/packages/collab-web/CHANGELOG.md b/packages/collab-web/CHANGELOG.md index 9e3bb3021..e88537056 100644 --- a/packages/collab-web/CHANGELOG.md +++ b/packages/collab-web/CHANGELOG.md @@ -77,16 +77,6 @@ - Fixed mobile layout issues where the entire chat flow would overflow horizontally and text was rendered too large on iOS Safari (by setting `text-size-adjust: 100%`) - Made transcript rows stack vertically on small screens to optimize reading space, and prevented grid track expansion - Hid non-essential metadata (such as the model name, thinking level, and working directory path) and context gauge tracks on mobile headers to prevent overflow -- Fixed mobile layout issues where the entire chat flow would overflow horizontally and text was rendered too large on iOS Safari (by setting `text-size-adjust: 100%`) -- Made transcript rows stack vertically on small screens to optimize reading space, and prevented grid track expansion -- Hid non-essential metadata (such as the model name, thinking level, and working directory path) and context gauge tracks on mobile headers to prevent overflow -- Wrapped composer button labels to display icon-only on mobile devices for a more compact and readable layout -- Made the connect screen, ended session card, and notification toasts fully responsive for smaller device viewports -- Fixed mobile layout issues where the entire chat flow would overflow horizontally and text was rendered too large on iOS Safari (by setting `text-size-adjust: 100%`) -- Made transcript rows stack vertically on small screens to optimize reading space, and prevented grid track expansion -- Hid non-essential metadata (such as the model name, thinking level, and working directory path) and context gauge tracks on mobile headers to prevent overflow -- Wrapped composer button labels to display icon-only on mobile devices for a more compact and readable layout -- Made the connect screen, ended session card, and notification toasts fully responsive for smaller device viewports ## [15.13.1] - 2026-06-15 @@ -100,7 +90,17 @@ ### Fixed +- Fixed mobile layout issues where the entire chat flow would overflow horizontally and text was rendered too large on iOS Safari (by setting `text-size-adjust: 100%`) - Pinned the app shell grid to a single `minmax(0, 1fr)` column so a long session title can no longer set a min-content floor that pushes the header, transcript, and composer wider than narrow or in-app mobile viewports; the title now ellipsizes instead of clipping every row's right edge +- Made transcript rows stack vertically on small screens to optimize reading space, and prevented grid track expansion +- Hid non-essential metadata (such as the model name, thinking level, and working directory path) and context gauge tracks on mobile headers to prevent overflow +- Wrapped composer button labels to display icon-only on mobile devices for a more compact and readable layout +- Made the connect screen, ended session card, and notification toasts fully responsive for smaller device viewports +- Fixed mobile layout issues where the entire chat flow would overflow horizontally and text was rendered too large on iOS Safari (by setting `text-size-adjust: 100%`) +- Made transcript rows stack vertically on small screens to optimize reading space, and prevented grid track expansion +- Hid non-essential metadata (such as the model name, thinking level, and working directory path) and context gauge tracks on mobile headers to prevent overflow +- Wrapped composer button labels to display icon-only on mobile devices for a more compact and readable layout +- Made the connect screen, ended session card, and notification toasts fully responsive for smaller device viewports ## [15.12.4] - 2026-06-13 diff --git a/packages/hashline/CHANGELOG.md b/packages/hashline/CHANGELOG.md index e4bee1434..b13c76c06 100644 --- a/packages/hashline/CHANGELOG.md +++ b/packages/hashline/CHANGELOG.md @@ -9,6 +9,9 @@ - Fixed recovery and edit-preview paths still treating a 16-bit snapshot tag as exact identity after collision-aware retention landed: ambiguous colliding tags now fall through to recovery/rejection (`byHashExact`) instead of applying anchors against the most-recent collider. - Reduced stale-anchor remap validation from quadratic to linear: duplicate-line detection and anchor-neighbor context are now precomputed once per pass instead of scanning the whole file per anchor. - Fixed hashline edit guidance for Markdown list rows by teaching `+- item` escaping in the model prompt and minus-row parser error. ([#4179](https://github.com/can1357/oh-my-pi/issues/4179)) +- Auto-repaired one-sided multi-line boundary echoes by dropping delimiter-neutral duplicated boundary lines and emitted a boundary-echo warning +- Parser now treats a leading `\` on inline payload bodies as the payload delimiter, matching standalone payload rows. +- Restored the warning emitted when escaped indented payload rows (`\\ TEXT`) are accepted as payload delimiters. ## [16.2.8] - 2026-06-30 diff --git a/packages/natives/CHANGELOG.md b/packages/natives/CHANGELOG.md index ed6ab0ef8..c4eeff66b 100644 --- a/packages/natives/CHANGELOG.md +++ b/packages/natives/CHANGELOG.md @@ -11,6 +11,9 @@ - Fixed an issue where panics in native worker tasks (such as grep, AST parsing, globbing, workspace listing, HTML-to-markdown conversion, fuzzy finding, and clipboard image reading) would abort the host process instead of properly rejecting the returned JavaScript Promise. Panics recovered this way are recorded in the native crash log (disk only, no stderr noise) so real native bugs still leave a diagnostic artifact. - Fixed the blocking-task panic recovery itself aborting the host when a panic payload's own destructor panics; the message is extracted first and the payload disposed without unwinding across the FFI boundary. - Fixed a crash on Windows under low memory/commit charge conditions when spawning worker threads for token counting or sorting operations. +- Fixed shipped Linux native addons failing to load with `version 'GLIBC_2.39' not found` on distributions older than Ubuntu 24.04. After native builds moved onto the Ubuntu 24.04 (glibc 2.39) self-hosted runner, the x64 addon was a plain host build that linked the runner's glibc and the arm64 cross-build floated up to GLIBC_2.30; the `linux-x64` (baseline + modern) and `linux-arm64` addons are now built through `cargo-zigbuild` against a pinned glibc 2.17 floor, restoring portability to any glibc ≥ 2.17 (CentOS 7 / Ubuntu 14.04 era). +- Fixed Linux native builds hard-failing when `RUSTC_WRAPPER=sccache` points at an unavailable shared cache backend. The native build script now retries the `napi` build once without the sccache wrapper after a cache-storage startup failure, so install smoke tests and local fallback builds can proceed while preserving the cached fast path when the backend is healthy. +- Fixed shell cancellation cleanup failing to reap child processes inside containers whose guest kernel was built without `CONFIG_PROC_CHILDREN` (e.g. some Kata/microVM guests): the Linux descendant walk relied solely on `/proc//task//children`, which does not exist there, so `children()` / `live_descendants()` returned empty and termination waves never reached the children. It now falls back to scanning `/proc` and grouping by parent pid (the primitive the macOS path already uses) when no `children` file is readable, keeping the cheap per-task fast path on kernels that support it. ## [16.2.11] - 2026-07-01 diff --git a/packages/snapcompact/CHANGELOG.md b/packages/snapcompact/CHANGELOG.md index 1ca6f85fa..375996728 100644 --- a/packages/snapcompact/CHANGELOG.md +++ b/packages/snapcompact/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added `openai-codex` to first-party provider image budgets so ChatGPT Plus/Pro Codex sessions use the same 200-image request cap as OpenAI API sessions instead of the unknown-provider floor. + ## [16.2.8] - 2026-06-30 ### Fixed diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 0aa00821e..f160e665c 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -12,25 +12,6 @@ ### Fixed - Fixed fuzzy-search filtering for CJK and other non-ASCII queries by preserving Unicode letters and numbers during query normalization ([#4114](https://github.com/can1357/oh-my-pi/issues/4114)). -- Bounded terminal input parsing so a malformed CSI/OSC/DCS/APC or a large non-bracketed paste no longer blocks the event loop. `StdinBuffer.extractCompleteSequences` now resolves each escape by a single linear scan with a per-type length cap (CSI 4 KiB, OSC/DCS/APC 16 MiB) and carries a resume-search offset so a chunked OSC 5522 payload stays O(total) instead of O(total²). `BracketedPasteHandler` gained a byte cap (default 64 MiB) that aborts paste mode and delivers the accumulated bytes when a lost end marker would otherwise hold memory forever — defense in depth for callers that bypass `StdinBuffer`. The `ProcessTerminal` data handler now takes a fast path when the sequence is not ESC-prefixed and no reassembly buffer is active, so a large non-bracketed paste skips six escape-probe regex tests per Unicode scalar ([#4073](https://github.com/can1357/oh-my-pi/issues/4073)). -- Added adaptive render backpressure: a frame that overruns the 30 fps cadence now inflates the following frame's delay to at most twice its own cost (capped at 200 ms), preventing the render loop from busy-looping when a slow paint would otherwise fire the next frame at `setTimeout(0)`. ([#4145](https://github.com/can1357/oh-my-pi/issues/4145)) - -### Added - -- Added regression coverage for `findCommittedPrefixResync` — the tui seam that re-anchors the committed prefix when a live block re-lays-out at settle. Locks in the earliest-audited-mismatch re-anchor, the hard-scan escape from tail-sample tolerance for a newly-permanent forced-overflow row, exempt-window drift silence, and shrink-into-prefix truncation, so a future refactor of the resync path cannot silently strand pending SSH placeholder chrome above the settled block ([#4124](https://github.com/can1357/oh-my-pi/issues/4124)). -- Added `Editor.setTopBorderProvider()` so hosts can install a lazy top-border builder that runs once per painted frame instead of eagerly rebuilding after every state change. Falls back to the existing `setTopBorder()` slot when no provider is registered. - -## [16.2.13] - 2026-07-01 - -### Fixed - -- Fixed fuzzy-search filtering for CJK and other non-ASCII queries by preserving Unicode letters and numbers during query normalization ([#4114](https://github.com/can1357/oh-my-pi/issues/4114)). - -## [16.2.12] - 2026-07-01 - -### Fixed - -- Optimized streaming markdown rendering to reuse already-rendered prefix lines and only render new content deltas, improving performance and reducing redraw flicker. ## [16.2.12] - 2026-07-01 @@ -44,34 +25,12 @@ - Fixed mid-prompt `/skill:` autocomplete acceptance wiping the user's draft. The autocomplete now inserts the `/skill: ` token at the cursor (replacing only the partial `/sk` slash token) and preserves prose typed before and after it, so a user can compose a prompt and reach for a skill without losing their train of thought ([#3913](https://github.com/can1357/oh-my-pi/issues/3913)). -## [16.2.10] - 2026-06-30 - -### Fixed - -- Fixed mid-prompt `/skill:` autocomplete acceptance wiping the user's draft. The autocomplete now inserts the `/skill: ` token at the cursor (replacing only the partial `/sk` slash token) and preserves prose typed before and after it, so a user can compose a prompt and reach for a skill without losing their train of thought ([#3913](https://github.com/can1357/oh-my-pi/issues/3913)). - ## [16.2.9] - 2026-06-30 ### Added - Added `Editor.submit()` to allow programmatic composer submission, enabling integration with speech input and other automated flows. -## [16.2.9] - 2026-06-30 - -### Added - -- Added `Editor.submit()` to allow programmatic composer submission, enabling integration with speech input and other automated flows. - -## [16.2.7] - 2026-06-30 - -### Fixed - -- Fixed an issue where a fast double-Escape keypress was swallowed and ignored, preventing double-escape gestures and subsequent Escape key handlers from firing. - -### Changed - -- Sped up fuzzy filtering in selectors (model, settings, file/tree, hook, OAuth) by preparing the query once per filter instead of once per candidate, and memoizing the per-text search index across keystrokes. Incremental typing over a 400-item list drops ~57% (6.7ms → 2.9ms for an 8-keystroke session) with no change to match ranking and no first-keystroke regression; long texts (pasted prompts, transcripts) bypass the cache so memory stays bounded. - ## [16.2.7] - 2026-06-30 ### Fixed @@ -637,7 +596,7 @@ ### Changed -- Changed native-scrollback safety defaults to treat unknown POSIX, SSH, and multiplexer-shaped terminals as ED3-risk for passive rendering; checkpoint replay now requires a positive at-tail viewport proof instead of assuming prompt submit makes host scrollback safe ([#1799](https://github.com/can1357/oh-my-pi/issues/1799)). +- Changed native-scrollback safety defaults to treat unknown POSIX, SSH, and multiplexer-shaped terminals as ED3-risk for passive rendering; checkpoint replay now requires a positive at-tail viewport proof instead of assuming prompt submit makes host scrollback safe. - Changed synchronized-output defaults to a conservative opt-in profile: DEC 2026 paint wrappers stay disabled for remote/multiplexer/VTE/unknown terminals unless explicitly forced, while the autowrap guards remain active. ### Fixed diff --git a/packages/tui/src/tui.ts b/packages/tui/src/tui.ts index 48f7815eb..d59015ab2 100644 --- a/packages/tui/src/tui.ts +++ b/packages/tui/src/tui.ts @@ -2836,6 +2836,7 @@ export class TUI extends Container { } window = this.#prepareLinesArray(window, width); } + const cursorTrackingLineCount = hasVisibleOverlay ? Math.max(frame.length, windowTop + height) : frame.length; const intent: RenderIntent = fullPaint ? { kind: "fullPaint", clearScrollback: replaceRequested || geometryRebuild ? !isMultiplexerSession() : false } @@ -2866,6 +2867,7 @@ export class TUI extends Container { clearScrollback: intent.clearScrollback, chunkTo, windowTop, + cursorTrackingLineCount, }); this.#committedPrefix = rawFrame.slice(0, chunkTo); this.#updateCommittedAuditRows( @@ -2892,6 +2894,7 @@ export class TUI extends Container { prevHardwareCursorRow, forceWindowRewrite: this.#forceViewportRepaintOnNextRender || (geometryChanged && resizeRepaintsInPlace()), repaintVirtualScrollInPlace: hasVisibleOverlay, + cursorTrackingLineCount, }); for (let i = this.#committedPrefix.length; i < chunkTo; i++) { this.#committedPrefix.push(rawFrame[i] ?? ""); @@ -3273,10 +3276,15 @@ export class TUI extends Container { cursorPos: { row: number; col: number } | null, purgeSequence: string, imageTransmitBuffer: string, - options: { clearScrollback: boolean; chunkTo: number; windowTop: number }, + options: { + clearScrollback: boolean; + chunkTo: number; + windowTop: number; + cursorTrackingLineCount: number; + }, ): void { this.#fullRedrawCount += 1; - const { chunkTo, windowTop } = options; + const { chunkTo, windowTop, cursorTrackingLineCount } = options; // Map the frame-space cursor into paint space: committed-prefix rows // keep their index, visible-window rows land after the prefix, and a // cursor in neither region (hidden behind the overlay gap) hides. @@ -3376,7 +3384,9 @@ export class TUI extends Container { buffer += this.#paintEndSequence; this.terminal.write(buffer); - const committedCursorState = paintCursorPos ? this.#targetHardwareCursorState(cursorPos, frame.length) : null; + const committedCursorState = paintCursorPos + ? this.#targetHardwareCursorState(cursorPos, cursorTrackingLineCount) + : null; const committedCursor = committedCursorState ? { toRow: committedCursorState.row, @@ -3646,6 +3656,7 @@ export class TUI extends Container { prevHardwareCursorRow: number; forceWindowRewrite: boolean; repaintVirtualScrollInPlace: boolean; + cursorTrackingLineCount: number; }, ): void { const { @@ -3655,6 +3666,7 @@ export class TUI extends Container { prevHardwareCursorRow, forceWindowRewrite, repaintVirtualScrollInPlace, + cursorTrackingLineCount, } = options; const chunkFrom = this.#committedRows; const chunkLength = chunkTo - chunkFrom; @@ -3706,7 +3718,7 @@ export class TUI extends Container { } cursorFromRow = windowTop + lastChanged; } - const cursorControl = this.#cursorControlSequence(cursorPos, frame.length, cursorFromRow); + const cursorControl = this.#cursorControlSequence(cursorPos, cursorTrackingLineCount, cursorFromRow); buffer += cursorControl.seq; buffer += this.#paintEndSequence; this.terminal.write(buffer); @@ -3717,16 +3729,17 @@ export class TUI extends Container { } } - // In-window diff: nothing commits. While an overlay is visible, commits - // are frozen; if the underlying windowTop moves, repaint in place rather - // than falling through to a seam rewrite that would scroll native history - // without appending to the commit tape. - const virtualScrollRewrite = repaintVirtualScrollInPlace && scroll !== 0; - if (chunkLength === 0 && (scroll === 0 || virtualScrollRewrite)) { - if (forceWindowRewrite || virtualScrollRewrite) this.#fullRedrawCount += 1; - let firstChanged = forceWindowRewrite || virtualScrollRewrite ? 0 : -1; - let lastChanged = forceWindowRewrite || virtualScrollRewrite ? height - 1 : -1; - if (!forceWindowRewrite && !virtualScrollRewrite) { + // In-window diff: nothing commits. While an overlay is visible, repaint + // the full viewport in place from a top-clamped cursor origin. Overlay + // cursor-only frames can leave the tracked row behind the physical cursor; + // a relative partial rewrite from that stale origin can CRLF on the bottom + // row and scroll native history without appending to the commit tape. + const overlayInPlaceRewrite = repaintVirtualScrollInPlace; + if (chunkLength === 0 && (scroll === 0 || overlayInPlaceRewrite)) { + if (forceWindowRewrite || overlayInPlaceRewrite) this.#fullRedrawCount += 1; + let firstChanged = forceWindowRewrite || overlayInPlaceRewrite ? 0 : -1; + let lastChanged = forceWindowRewrite || overlayInPlaceRewrite ? height - 1 : -1; + if (!forceWindowRewrite && !overlayInPlaceRewrite) { const comparable = previousWindow.length === height; for (let r = 0; r < height; r++) { if (comparable && (window[r] ?? "") === (previousWindow[r] ?? "")) continue; @@ -3736,13 +3749,13 @@ export class TUI extends Container { } if (firstChanged === -1) { if (purgeSequence.length > 0) this.terminal.write(purgeSequence); - this.#writeCursorPosition(cursorPos, frame.length); + this.#writeCursorPosition(cursorPos, cursorTrackingLineCount); this.#previousWidth = width; this.#previousHeight = height; return; } let buffer = this.#paintBeginSequence + purgeSequence; - if (virtualScrollRewrite) { + if (overlayInPlaceRewrite) { // The cursor tracker can be stale after overlay-only frames. A large // CUU clamps at the viewport top without using absolute cursor home, // so the following full-window rewrite cannot overflow the bottom. @@ -3777,7 +3790,7 @@ export class TUI extends Container { buffer += `\x1b[${lastChanged - contentBottomScreenRow}A`; cursorFromRow = contentBottomRow; } - const cursorControl = this.#cursorControlSequence(cursorPos, frame.length, cursorFromRow); + const cursorControl = this.#cursorControlSequence(cursorPos, cursorTrackingLineCount, cursorFromRow); buffer += cursorControl.seq; buffer += this.#paintEndSequence; this.terminal.write(buffer); @@ -3807,7 +3820,7 @@ export class TUI extends Container { } const parkUp = height - 1 - (contentBottomRow - windowTop); if (parkUp > 0) buffer += `\x1b[${parkUp}A`; - const cursorControl = this.#cursorControlSequence(cursorPos, frame.length, contentBottomRow); + const cursorControl = this.#cursorControlSequence(cursorPos, cursorTrackingLineCount, contentBottomRow); buffer += cursorControl.seq; buffer += this.#paintEndSequence; this.terminal.write(buffer); diff --git a/packages/tui/test/overlay-scroll.test.ts b/packages/tui/test/overlay-scroll.test.ts index 2b760443c..d17c04303 100644 --- a/packages/tui/test/overlay-scroll.test.ts +++ b/packages/tui/test/overlay-scroll.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it } from "bun:test"; -import { type Component, CURSOR_MARKER, TUI } from "@oh-my-pi/pi-tui"; +import { type Component, CURSOR_MARKER, type Focusable, type OverlayFocusOwner, TUI } from "@oh-my-pi/pi-tui"; import { VirtualTerminal } from "./virtual-terminal"; class LineComponent implements Component { @@ -64,6 +64,54 @@ class CursorOnlyComponent implements Component { } } +class FocusedMutableOverlay implements Component, Focusable { + focused = false; + #text: string; + + constructor(text: string) { + this.#text = text; + } + + setText(text: string): void { + this.#text = text; + } + + invalidate(): void { + // No cached state + } + + render(_width: number): string[] { + return [`${this.#text}${this.focused ? CURSOR_MARKER : ""}`]; + } +} + +class OverlayFocusDelegator implements Component, OverlayFocusOwner { + #text: string; + + constructor( + text: string, + private readonly ownedFocusTarget: Component, + ) { + this.#text = text; + } + + setText(text: string): void { + this.#text = text; + } + + ownsOverlayFocusTarget(component: Component): boolean { + return component === this.ownedFocusTarget; + } + + invalidate(): void { + // No cached state + } + + render(_width: number): string[] { + return [this.#text]; + } +} + function buildRows(count: number): string[] { return Array.from({ length: count }, (_v, i) => `row-${i}`); } @@ -159,6 +207,52 @@ describe("TUI overlays", () => { expect(term.getScrollBuffer().length).toBeLessThan(200); }); + it("keeps the native viewport anchored when an overlay repaint follows a focused cursor below the frame tail", async () => { + const term = new VirtualTerminal(24, 6, 100); + const tui = new TUI(term, true); + const base = new MutableContentComponent(buildRows(8)); + const cursorOverlay = new FocusedMutableOverlay("overlay-cursor"); + const statusOverlay = new OverlayFocusDelegator("status-before", cursorOverlay); + tui.addChild(base); + + try { + tui.start(); + await flushRender(term); + + tui.showOverlay(cursorOverlay, { row: 5, col: 0, width: 16 }); + tui.showOverlay(statusOverlay, { row: 0, col: 0, width: 16 }); + tui.setFocus(cursorOverlay); + tui.requestRender(); + await flushRender(term); + + base.setLines(["base-0", "base-1"]); + tui.requestRender(); + await flushRender(term); + expect(term.getCursor().row).toBe(5); + + const before = term.getBufferPosition(); + const beforeScrollBufferLength = term.getScrollBuffer().length; + + statusOverlay.setText("status-after"); + tui.requestRender(); + await flushRender(term); + + expect(term.getBufferPosition()).toEqual(before); + expect(term.getScrollBuffer()).toHaveLength(beforeScrollBufferLength); + expect(term.getViewport().map(line => line.trimEnd())).toEqual([ + "status-after", + "base-1", + "", + "", + "", + "overlay-cursor", + ]); + expect(term.getCursor().row).toBe(5); + } finally { + tui.stop(); + } + }); + it("clamps tall overlays without an explicit maxHeight to the available rows", async () => { const term = new VirtualTerminal(80, 24); const tui = new TUI(term); diff --git a/packages/utils/CHANGELOG.md b/packages/utils/CHANGELOG.md index f08a98a15..fc8e718d3 100644 --- a/packages/utils/CHANGELOG.md +++ b/packages/utils/CHANGELOG.md @@ -113,24 +113,16 @@ - Added profile-aware directory helpers and isolated profile state roots, while keeping the install ID shared across profiles. - Added a named-profile API to the `dirs` module — `setProfile()`, `getActiveProfile()`, `getProfileRootDir()`, and `normalizeProfileName()` — plus `resolveProfileEnv()`, which selects the active profile from `OMP_PROFILE` (canonical; takes precedence) then `PI_PROFILE` (legacy fallback, consulted only when `OMP_PROFILE` is unset). +- Added support for a runtime `overrides` map in `RuntimeInstallSpec`, which is now written into generated runtime `package.json` manifests to force dependency pins (including transitive ones) across the runtime tree +- Added a lightweight loop-phase breadcrumb stack (`pushLoopPhase`/`popLoopPhase`/`currentLoopPhase`, plus `takeRecentLoopPhase` which returns the live phase or the most recently popped one and clears it) so the TUI event-loop watchdog can attribute a main-thread block to the phase that caused it — including a synchronous phase already popped before the watchdog's delayed tick runs ([#2485](https://github.com/can1357/oh-my-pi/issues/2485)) +- Added `FetchWithRetryOptions.timeout` (forwarded to the underlying `fetch` call). `false` disables Bun's native ~300s pre-response timeout; a positive number overrides the ceiling. Bare browser/Node fetch ignores it ([#2422](https://github.com/can1357/oh-my-pi/issues/2422)) - Added the side-effect-free `@oh-my-pi/pi-utils/worker-host` module (`declareWorkerHostEntry()` / `workerHostEntry()`), extracted from `env` (still re-exported there) so worker spawn sites can resolve the self-dispatching CLI host entry without importing `env`'s side-effecting module graph. ### Fixed - Fixed profile directory isolation when a profile's agent `.env` customizes directory roots: directory-affecting keys (`XDG_DATA_HOME`/`XDG_STATE_HOME`/`XDG_CACHE_HOME`, and a default-mode `PI_CODING_AGENT_DIR`) are now honored. The `env` loader rebuilds the `dirs` resolver after applying `.env` files (`refreshDirsFromEnv()`), so a profile `.env` that points XDG roots elsewhere no longer leaks state into the home-based config dir. -- Fixed `installRuntimeModuleResolver()` to keep bare requests from runtime-cache modules inside that registered runtime before falling back to host/workspace packages. - -## [15.13.1] - 2026-06-15 - -### Added - -- Added support for a runtime `overrides` map in `RuntimeInstallSpec`, which is now written into generated runtime `package.json` manifests to force dependency pins (including transitive ones) across the runtime tree -- Added a lightweight loop-phase breadcrumb stack (`pushLoopPhase`/`popLoopPhase`/`currentLoopPhase`, plus `takeRecentLoopPhase` which returns the live phase or the most recently popped one and clears it) so the TUI event-loop watchdog can attribute a main-thread block to the phase that caused it — including a synchronous phase already popped before the watchdog's delayed tick runs ([#2485](https://github.com/can1357/oh-my-pi/issues/2485)) -- Added `FetchWithRetryOptions.timeout` (forwarded to the underlying `fetch` call). `false` disables Bun's native ~300s pre-response timeout; a positive number overrides the ceiling. Bare browser/Node fetch ignores it ([#2422](https://github.com/can1357/oh-my-pi/issues/2422)) - -### Fixed - - Made `TempDir` cleanup retry transient Windows `EBUSY`/`EPERM`/`ENOTEMPTY` removal failures so tests are less likely to fail when deleting just-used temp directories. +- Fixed `installRuntimeModuleResolver()` to keep bare requests from runtime-cache modules inside that registered runtime before falling back to host/workspace packages. ## [15.12.4] - 2026-06-13