All checks were successful
release / build-linux-amd64 (push) Successful in 12m5s
729 lines
41 KiB
Markdown
729 lines
41 KiB
Markdown
# Changelog
|
||
|
||
All notable changes to patterm are tracked in this file. Format follows
|
||
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project
|
||
loosely follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||
|
||
## [Unreleased]
|
||
|
||
## [0.0.7] - 2026-05-18
|
||
|
||
### Added
|
||
- The top tab bar now prefixes each agent tab's label with its
|
||
idle-state glyph (✕ error, ? permission, ◐ thinking, ○ idle, ●
|
||
working), matching the sidebar's vocabulary so the state of every
|
||
open agent is visible without opening or focusing each tab.
|
||
|
||
### Changed
|
||
- Built-in agent presets (`claude`, `codex`, `opencode`) now live in
|
||
memory and user preset files merge over them by name instead of
|
||
patterm writing default preset files into `$XDG_CONFIG_HOME`. Add
|
||
`"disabled": true` in a matching user preset to hide a built-in.
|
||
- Generated MCP config files for agent launches now live under the
|
||
runtime agent directory instead of `$XDG_CONFIG_HOME/patterm/mcp`.
|
||
- Auto-summarization settings now save as soon as a changed row is
|
||
applied, including cadence/provider/toggle changes and model edits,
|
||
without requiring a separate save step.
|
||
- The Agents / Auto-summarization settings screen no longer shows
|
||
explicit Save, Cancel, or Back rows, and its footer copy no longer
|
||
describes a separate save/cancel flow.
|
||
- Auto-summarization setting rows now visually separate grey labels
|
||
from regular-colour values.
|
||
- The active-thread summary in the tab bar is now constrained to the
|
||
active tab's width instead of spanning the whole top row.
|
||
- Sidebar summary text now wraps from the full summary text instead of
|
||
using an ellipsized single-line value.
|
||
|
||
### Fixed
|
||
- Claude permission prompts are now detected from the rendered pane as
|
||
well as the recent output tail, so the sidebar marks the pane as
|
||
waiting for permission even while `Calling patterm...` continues to
|
||
repaint.
|
||
- Removed the redundant "Back to Settings" row from the
|
||
Agents / Auto-summarization settings screen.
|
||
- Pending `timer_*` entries are now cancelled when their owning or
|
||
watched child is closed via `close_process`, preventing stale
|
||
timer bodies from being re-delivered to the orchestrator pane
|
||
after the work has already been handled.
|
||
|
||
## [0.0.6] - 2026-05-15
|
||
|
||
### Changed
|
||
- Toast notifications now reserve three content rows and word-wrap
|
||
the message body inside the box, replacing the previous
|
||
single-line+ellipsis layout. The `Ctrl-N · N more` inline hint is
|
||
gone; instead the host status strip surfaces a `Ctrl-N · dismiss`
|
||
hint, shown only while a notification is on screen so the chord
|
||
doesn't advertise itself when it has nothing to dismiss.
|
||
|
||
### Fixed
|
||
- Auto-summary no longer fails immediately with `codex summarizer:
|
||
error: unexpected argument '--ask-for-approval' found`. The codex
|
||
CLI dropped that flag; we now rely on `--sandbox read-only` (which
|
||
already implies no approvals) instead of passing it.
|
||
- Toast box no longer flickers / half-erases while the focused
|
||
child (claude, codex, opencode, etc.) repaints its TUI. The
|
||
overlay is now stitched onto the end of the per-chunk PTY write
|
||
under `outMu`, and wrapped in DECSET 2026 (synchronized output)
|
||
brackets so terminals that support it batch the child's redraw +
|
||
the box paint into a single frame instead of racing cell-by-cell.
|
||
|
||
## [0.0.5] - 2026-05-15
|
||
|
||
### Changed
|
||
- Replaced the single-slot status-line "flash" with a stackable toast
|
||
surface anchored at the top-right of the focused pane. `flashError`,
|
||
`flashTransient`, and MCP `request_human_attention` now push onto
|
||
the toast stack (cap 5, oldest drops). Toasts persist until
|
||
dismissed with `Ctrl-N`, or cleared via the new
|
||
"Clear notifications" palette command. The status line no longer
|
||
shows the `[!]` prefix.
|
||
- `Ctrl-N` is consumed by the host only when there is a toast to
|
||
dismiss; an empty stack lets `Ctrl-N` pass through to the focused
|
||
child so readline / nano / emacs / opencode keep their bindings.
|
||
- Command palette is calmer when something is focused. Focused-section
|
||
rows now read as bare verbs (`Rename`, `Close`, `Stop`, `Restart`,
|
||
`Delete`, `Edit`) instead of repeating the focused name (`Close
|
||
agent: codex`); the title bar's `on: codex` / `pad: notes.md`
|
||
carries the subject. Fuzzy queries still match the dropped context
|
||
through the row hint (e.g. typing `close codex` still finds the
|
||
Close row).
|
||
- Dashed `── Focused ──` / `── Open ──` / `── Spawn ──` section
|
||
banners are gone. Sections are separated by a single blank spacer
|
||
row, so the action labels themselves carry the visual weight.
|
||
- The Open section no longer lists a `Switch to <current>` row for
|
||
the pane you're already focused on.
|
||
|
||
## [0.0.4] - 2026-05-15
|
||
|
||
### Changed
|
||
- Release workflow (`.gitea/workflows/release.yml`) now provisions
|
||
Zig and Go through `jdx/mise-action@v2`, reading the versions from
|
||
`.mise.toml` (zig 0.15.2, go 1.26.3). Both toolchains were
|
||
previously installed via `mlugg/setup-zig` and `actions/setup-go`,
|
||
whose mirror chase / GitHub fetch combined for ~8 minutes per run
|
||
before any patterm code compiled. mise pulls each tool once and
|
||
caches the install dir, so subsequent runs hit the cache instead of
|
||
re-downloading. `make deps` still resolves zig via `mise which zig`
|
||
with a PATH fallback; `go.mod` already pinned `go 1.26.3`, so the
|
||
new `go` entry in `.mise.toml` just keeps CI and local builds on
|
||
the same toolchain.
|
||
- A Go module/build cache step (`actions/cache@v4`, keyed on
|
||
`go.sum`) was added so `go build` doesn't re-download dependencies
|
||
on every tag push.
|
||
|
||
## [0.0.3] - 2026-05-15
|
||
|
||
### Added
|
||
- Auto-summarization for top-level agent tabs. patterm now loads
|
||
`$XDG_CONFIG_HOME/patterm/settings.json`, enables Codex-based
|
||
summaries by default (`gpt-5.4-mini`; OpenCode defaults to
|
||
`opencode-go/minimax-m2.7`), and can run Codex, OpenCode, or opt-in
|
||
Claude summarizers with configurable model names. Summary
|
||
attempts are armed by meaningful human input, wait for recent output
|
||
to go quiet, and respect a minimum cadence so unchanged tabs are not
|
||
summarized on a timer. The active thread summary appears under the
|
||
top tab title and in the sidebar below the Agent Tree section.
|
||
- Settings overlay reachable from the command palette via
|
||
`Open Settings`. The searchable Settings picker opens
|
||
`Agents / Auto-summarization`, where users can enable/disable
|
||
summaries, choose provider, edit provider model names, cycle cadence,
|
||
test the selected summarizer (`patterm okay`), summarize the current
|
||
top-level agent immediately, and explicitly save or cancel draft
|
||
settings changes. Cadence choices match Solo: `15s`, `30s`, and
|
||
`1m`; the value is a minimum quiet/activity gap before another
|
||
summary attempt for the same top-level agent, not a background
|
||
periodic timer.
|
||
|
||
### Changed
|
||
- Command palette UX overhaul. The single flat list grew section
|
||
bands (`── Focused ──`, `── Open ──`, `── Spawn ──`, `── Quit ──`)
|
||
so the rows are scannable at a glance; cursor navigation skips
|
||
the dim header rows transparently. A chip strip — `[All] Open
|
||
Spawn Close` — sits below the query line and tracks the active
|
||
macro filter; `Tab` / `Shift-Tab` cycle through the chips, and
|
||
the typed-prefix macros (`sw `, `sp `, `k `) still work and now
|
||
collapse the whole prefix on a single backspace instead of
|
||
leaving a stray `sw` behind. The title bar surfaces the current
|
||
focus subject (`on: <child>` / `pad: <name>`) so the user knows
|
||
which Focused row is targeting what. The duplicate global Close
|
||
list is gone — close is reachable via the Focused-section action,
|
||
the `k ` macro / `[Close]` chip, or the new `Ctrl-X` inline close
|
||
on a Switch row. The "(current)" marker on the focused Switch row
|
||
became a leading `▶`. The empty-state hint now reads `no matches
|
||
· ⌫ to widen` instead of bare `no matches`. The middle divider
|
||
shows a `▼ N more` / `▲ N above` scroll indicator when the list
|
||
overflows, and the footer carries a `cursor/total` counter.
|
||
- Spawn verbs are unified on **Spawn**: `Run process: …` →
|
||
`Spawn process: …`, `New Terminal` → `Spawn terminal`, and the
|
||
freeform-form row is now `Spawn process… (custom)` so the
|
||
trailing ellipsis still signals it opens a form.
|
||
- Filtering switched from binary fuzzy-include to scored ranking.
|
||
Prefix matches beat word-boundary matches beat substring matches
|
||
beat scattered-fuzzy matches; ties fall back to section order so
|
||
a Focused-section hit always outranks an equally tight Spawn
|
||
hit. The matched characters in the rendered label render in
|
||
accent+bold so the user can see why a row matched.
|
||
- Rename forms split the long subject (`scratchpad:
|
||
some-really-long-name.md`) onto its own dim row above the input
|
||
so the title bar no longer truncates with an ellipsis when the
|
||
subject name is wide.
|
||
- New palette accelerators: `Alt-1` … `Alt-9` quick-pick the Nth
|
||
visible row, `Home` / `End` jump to first / last selectable row,
|
||
`?` (with empty query) opens an inline keybinding cheat-sheet
|
||
which any further keystroke dismisses, and `Ctrl-R` inside the
|
||
Spawn-process form toggles "Relaunch on exit" without leaving
|
||
the command field.
|
||
|
||
### Fixed
|
||
- Error/status flashes now restore the currently focused pane instead
|
||
of drawing the empty-state hint over a running agent or process.
|
||
- Release workflow (`.gitea/workflows/release.yml`) now uses
|
||
`mlugg/setup-zig@v2` instead of the deprecated `@v1`. v1 hard-coded
|
||
the pre-0.14 tarball name (`zig-linux-x86_64-<ver>.tar.xz`), so
|
||
every mirror and the official `ziglang.org/builds` returned 404 for
|
||
Zig 0.15.2 and the v0.0.1 / v0.0.2 tag pushes never produced a
|
||
release asset. v2 uses the post-0.14 `zig-x86_64-linux-<ver>.tar.xz`
|
||
layout, so the runner can fetch Zig and build patterm.
|
||
- Typing into a focused child while its emulator viewport is
|
||
scrolled up into scrollback history now auto-snaps the viewport
|
||
back to the live area. Previously the keystroke reached the
|
||
child PTY but the input box was off-screen below the visible
|
||
region, so it looked like typing did nothing. Wheel scrolling
|
||
and Ctrl-B are unchanged; only forwarded keystrokes snap.
|
||
- Top tab bar now keeps the top-level agent's tab highlighted
|
||
when focus is on one of its sub-agents (or on a Processes pane
|
||
entry, matching the existing agent-tree behavior). Previously
|
||
the tab would lose its highlight as soon as you stepped into a
|
||
child agent, even though you were still within that thread.
|
||
|
||
### Changed
|
||
- MCP tool descriptions and `help('coordination')` /
|
||
`help('readiness')` now spell out that a sub-agent's reply to
|
||
`send_message` lands in the caller's own pane (tagged
|
||
`[sub-agent:<name>]`), not in the sub-agent's output. The canonical
|
||
wait-for-reply pattern — `send_message` → `timer_fire_when_idle_any`
|
||
on the sub-agent → read your own pane — is now called out on
|
||
`send_message`, `wait_for_pattern`, both `timer_fire_when_idle_*`,
|
||
the help topics, and the server-instructions preamble every agent
|
||
reads at startup. Previously `wait_for_pattern` was the obvious
|
||
blocking primitive in the catalog, and agents routinely called it
|
||
against the sub-agent for a reply that had already arrived in their
|
||
own pane, deadlocking until the wait timed out. No behaviour
|
||
changes; descriptions only.
|
||
- Agent-initiated `spawn_agent` and `spawn_process` MCP calls no
|
||
longer steal viewport focus from the currently active tab. The
|
||
new child still appears in the sidebar and tab bar; switch to it
|
||
explicitly via the palette or `select_process`. Palette-initiated
|
||
spawns and persistence restores are unchanged — they still auto-
|
||
focus the new pane.
|
||
- Sidebar rows (Processes, Agent Tree, Scratchpads) now truncate
|
||
overflowing names with a trailing `…` instead of spilling into
|
||
the main viewport. The focused row marquees its name when it
|
||
overflows — 1 s hold on the head, ~150 ms per cell scroll until
|
||
the tail is visible, 1 s hold on the tail, snap back. Row
|
||
position never moves while the marquee animates. When budget is
|
||
tight, the trailing timer indicator drops before the name
|
||
ellipses, since the name is the only identifier the row carries.
|
||
|
||
## [0.0.2] - 2026-05-15
|
||
|
||
### Added
|
||
- `.mise.toml` pinning `zig = "0.15.2"` (the minimum version the
|
||
vendored Ghostty commit requires). Contributors run
|
||
`mise install` once; the Makefile picks up the resulting `zig`
|
||
binary automatically via `mise which zig` and falls back to
|
||
PATH when mise isn't available, so the existing build flow
|
||
still works.
|
||
- ASCII-video stress benchmarks (`internal/app/bench_test.go`):
|
||
per-frame and per-stream variants at 30 / 60 / 120 fps targets,
|
||
three workload fixtures (8-colour cells, 24-bit truecolor cells,
|
||
and a Bad-Apple-style 1-bit pattern). Each stream benchmark
|
||
reports `µs/frame`, an achievable `fps_ceiling`, and `budget_pct`
|
||
so you can read off "do we hit N fps?" directly. A matching
|
||
Pipeline_ASCIIVideo_* set includes libghostty-vt's em.Write CGO
|
||
and an io.Discard stdout write so the FPS claim reflects the
|
||
whole pipeline, not just the renderer.
|
||
- MCP `initialize.instructions`, the `spawn_agent` tool description
|
||
(visible to LLMs via `tools/list`), and the `help('spawning')`
|
||
topic now spell out — in the three places vendor TUIs actually
|
||
consult — that the connected `patterm` MCP server is the only
|
||
correct way to drive the host. Anti-patterns called out by name:
|
||
(a) trying to launch `patterm` / `patterm mcp-stdio` themselves,
|
||
(b) piping JSON-RPC into the per-PID Unix socket via `perl` /
|
||
`nc` / `socat` / `curl`, and (c) shelling out to `claude` /
|
||
`codex` / `opencode` to start a peer. Each of those bypasses
|
||
caller identity, so a sub-agent spawned that way reads back as
|
||
a stray top-level tab instead of a child under the spawning
|
||
agent. Codex was hitting (b) and (c) in practice — this is the
|
||
fix.
|
||
- `--debug[=DIR]` flag captures detailed run artefacts for offline
|
||
analysis: a verbose `patterm.log` (the existing `PATTERM_DEBUG_LOG`
|
||
stream), an `events.jsonl` lifecycle log (spawn / exit / idle-state
|
||
changes with timestamps), and per-child `<id>.raw` files containing
|
||
the raw PTY byte stream. With no argument, the dated subdir
|
||
`$XDG_STATE_HOME/patterm/debug/YYYYMMDD-HHMMSS` is used; pass an
|
||
explicit path to override. All output goes to files — stdout/stderr
|
||
are untouched.
|
||
- `--profile[=DIR]` flag captures pprof data plus concrete
|
||
performance counters for performance work: `cpu.pprof` (running
|
||
for the lifetime of the session), plus `heap.pprof` and
|
||
`goroutine.pprof` snapshots written on shutdown; alongside them,
|
||
a per-hot-path metrics tracker writes `metrics.jsonl` (one row
|
||
per second with chunk/byte rates, per-stage mean and max
|
||
latencies, and cache hit rates) plus a final `metrics.json`
|
||
aggregate and a human-readable `summary.txt` on exit.
|
||
Instrumented hot paths: `OnPTYOut`, viewport `renderer.Render`,
|
||
host stdout writes, libghostty-vt `emulator.Write` / `Title`,
|
||
sidebar / tab bar / status line draws (with cache-hit
|
||
accounting), snapshot replays, and the chrome ticker (so you can
|
||
see how often it fires with nothing to do). Defaults to
|
||
`$XDG_STATE_HOME/patterm/profile/YYYYMMDD-HHMMSS`. All
|
||
diagnostics (startup, errors) are written to `profile.log`
|
||
inside the dir, never to stdout/stderr.
|
||
- Renderer benchmark suite (`internal/app/bench_test.go`). Three
|
||
workload fixtures — plain ASCII, SGR-styled lines, and a
|
||
ratatui-style cursor-shuffling burst — plus an OSC-gate
|
||
micro-benchmark. Run via `go test -bench=. -benchmem
|
||
./internal/app/`. Gives a stable reference for the per-chunk
|
||
cost of the viewport renderer so future changes can be compared
|
||
apples-to-apples.
|
||
- "New Terminal" entry in the command palette spawns a bare interactive
|
||
`$SHELL` pane (kind `terminal`). Unlike "Run process: …" presets,
|
||
which are session-persistent and reachable via `restart_process`,
|
||
terminals are ephemeral — once they exit they vanish from the
|
||
Processes sidebar instead of lingering as a dead row. The default
|
||
`shell` process preset that previously seeded on first run has been
|
||
removed; this entry replaces it.
|
||
- Per-child idle-state classifier with five states (`idle`, `working`,
|
||
`thinking`, `permission`, `error`) and three pluggable strategies:
|
||
`output_activity` (claude / opencode defaults), `osc_title_stability`
|
||
(codex), and `osc_title_status` (gemini-style status-in-title agents).
|
||
Optional `permission_patterns` / `thinking_patterns` / `error_patterns`
|
||
regexes promote a base state when matched against the tail of recent
|
||
output. State and last-match reason are exposed via MCP on
|
||
`ProcessInfo` and `get_process_status` (`idle_state`, `idle_reason`).
|
||
- New `idle_detection` block on `preset.Preset` for setting the strategy
|
||
threshold, title-to-state map, and promoter regex lists. Bundled
|
||
defaults are shipped for the first-party claude / codex / opencode
|
||
presets.
|
||
- Sidebar now renders a state glyph per process row (`○` idle, `●`
|
||
working, `◐` thinking, `?` permission, `✕` error) and, when a process
|
||
has a pending or paused timer, appends a nearest-timer indicator
|
||
(`⏱ 12s` or `⏸ paused`).
|
||
- MCP timer surface expanded to match Solo's tool set: `timer_set`,
|
||
`timer_fire_when_idle_any`, `timer_fire_when_idle_all`, `timer_cancel`,
|
||
`timer_pause`, `timer_resume`, `timer_list`. Idle-aware timers
|
||
registered against already-idle children fire synchronously
|
||
(`status: already_satisfied`) for `idle_all`, and report
|
||
`already_idle` / `waiting_on` arrays so callers can introspect the
|
||
watch set. Timer bodies are delivered to the owner process via the
|
||
same orchestrator-injection path as `send_message`.
|
||
- Timer tools accept an explicit `owner_process_id` so top-level
|
||
(non-agent) callers — including the harness MCP client — can attribute
|
||
timers to a specific process. Omitting it treats the caller as the
|
||
orchestrator with universal cancel / pause / resume / list privileges.
|
||
- libghostty-vt `Title()` accessor on the emulator surface, polled from
|
||
the session pump so OSC 0/1/2 title updates feed into the classifier
|
||
without a callback round-trip.
|
||
- Harness `wait_until_mcp` step type that re-runs an MCP method until an
|
||
assertion (Equals / Contains) holds or the timeout elapses. Used by
|
||
the new idle / timer scenarios.
|
||
- User-created top-level command processes now survive a patterm
|
||
restart. Each spawn (palette form, command preset, or MCP
|
||
`spawn_process` with `kind=command`) writes a record to
|
||
`$XDG_DATA_HOME/patterm/projects/<key>/processes.json`; on next
|
||
startup patterm replays those entries before the UI accepts input,
|
||
so things like `bun run dev` or `tail -F log` come back without
|
||
re-typing. `close_process` (and the palette's close action) drops
|
||
the entry, and rename / "relaunch on exit" toggles are mirrored as
|
||
they happen. Agents and terminals stay ephemeral by design.
|
||
- The command palette (Ctrl-K) now surfaces context-aware actions at
|
||
the top of the list, based on what's currently focused:
|
||
- Scratchpad in focus: `Delete`, `Rename` (inline form), and `Edit`
|
||
(fire-and-forget launch of `zed` against the pad file).
|
||
- Agent in focus: `Rename agent` (inline form) and `Close agent`.
|
||
- Process in focus: `Rename process`, `Delete process` (drops the
|
||
entry; SIGKILLs if alive), `Stop process` (SIGTERM, keep entry),
|
||
and `Restart process` (same argv).
|
||
- `patterm --version` prints the build version, git commit, and build
|
||
date (e.g. `patterm v0.0.1 (commit abc1234, built 2026-05-14)`). The
|
||
version string is injected by the build (`make patterm` derives it
|
||
from `git describe`; the release workflow injects the pushed tag).
|
||
Commit and date come from the Go toolchain's embedded VCS info, so
|
||
nothing has to be bumped by hand.
|
||
- Ctrl+R restarts the focused command process from the Processes
|
||
sidebar, including command entries that have already exited.
|
||
- Scratchpads are now first-class navigation targets. Ctrl+W / Ctrl+S
|
||
step from the Processes section and agent tree onto scratchpad
|
||
entries; a focused scratchpad renders its content in the main
|
||
viewport (with the pad name as the header) instead of cramping the
|
||
bottom of the sidebar. The sidebar's scratchpad section is a names-
|
||
only list with the focused pad highlighted, and external MCP
|
||
`scratchpad_write` / `scratchpad_append` updates repaint the pad
|
||
view immediately.
|
||
- Focused scratchpads now render as markdown — headings, bold, inline
|
||
code, fenced code blocks, bullet/numbered lists, blockquotes, and
|
||
horizontal rules pick up styling instead of the previous flat
|
||
word-wrap. Long pads scroll: the mouse wheel is the primary control
|
||
(patterm enables SGR mouse reporting while a pad is focused), and
|
||
Up/Down / PageUp/PageDown / Home / End work for keyboard users. The
|
||
header reports the visible row range and total row count. Esc leaves
|
||
the pad view and falls back to the first running process (or an
|
||
empty viewport). The scroll offset is preserved across MCP
|
||
`scratchpad_write` / `scratchpad_append` writes so a live update
|
||
doesn't snap the view back to the top.
|
||
- Inline wheel scrollback for the focused child, backed by
|
||
libghostty-vt's native 5000-row scrollback history. On the primary
|
||
screen, mouse-wheel events scroll the emulator viewport in-place with
|
||
full SGR styling preserved — no modal view to enter or exit. On the
|
||
alternate screen wheel events still pass through to the child so
|
||
vim / less / codex receive them as input. Ctrl+B snaps the viewport
|
||
back to the live (bottom) area as the escape hatch from a scrolled-up
|
||
state. Patterm now keeps SGR mouse reporting armed on the host
|
||
terminal while the alt screen is active and filters mouse-mode
|
||
toggles from the child stream so wheel events keep arriving even
|
||
after a child program disables mouse tracking.
|
||
|
||
### Changed
|
||
- The palette's per-child "Kill <name>" action is now labelled
|
||
"Close <name>". The underlying signal (SIGTERM) and behaviour are
|
||
unchanged; the new label matches the existing "Close agent: …"
|
||
context entry and reads less violent for what is really just a
|
||
graceful termination.
|
||
- `timer_wait` is now a thin wrapper over the shared timer manager
|
||
(`timer_set` semantics). Existing callers see no behavioural change;
|
||
the timer is visible in `timer_list` while it's pending.
|
||
- CLI flag parsing switched from Go's stdlib `flag` to `spf13/pflag`.
|
||
`--project` (and the internal `--socket` / `--identity` /
|
||
`--scenario` / `--patterm-bin` flags) are now the only accepted form
|
||
— single-hyphen long flags like `-project` are rejected. Help output
|
||
renders the canonical `--flag` form.
|
||
|
||
### Fixed
|
||
- `make deps` now builds libghostty-vt with `-Doptimize=ReleaseFast`
|
||
instead of zig's silent `Debug` default, and resolves `zig`
|
||
through `mise` when a project `.mise.toml` pins it. The
|
||
default-Debug build shipped an unoptimised CSI/SGR parser that
|
||
ate 16-29 ms per 30-70 KiB full-screen frame in benchmarks,
|
||
capping the entire PTY-to-host pipeline at 34-63 fps. After the
|
||
rebuild the same pipeline runs at **930-2030 fps**: 27-32× the
|
||
prior throughput, and 7-16× margin over 120 fps for full-screen
|
||
truecolor ASCII video. Static library size drops from 33 MiB to
|
||
13 MiB. Override with `make deps GHOSTTY_VT_OPTIMIZE=Debug` only
|
||
when debugging the upstream library itself. Apply on existing
|
||
checkouts with `mise install && make clean-deps && make deps`.
|
||
- Long claude session resume (and codex steady-state rendering) is
|
||
noticeably faster. Two costs that scaled per-PTY-chunk are now
|
||
deferred or short-circuited: (1) `drawSidebar()` used to run
|
||
synchronously for every chunk that scrolled — on a session
|
||
resume where every chunk scrolls, this rebuilt the full sidebar
|
||
string hundreds of times for a frame that was almost always
|
||
cache-equal. The sidebar now signals dirty and the chrome ticker
|
||
(60 Hz) handles the repaint. (2) `pumpChild` polled the
|
||
emulator's OSC title after every PTY chunk via CGO, even for
|
||
chunks (the common case under codex/ratatui) that carry no OSC
|
||
bytes at all. The poll is now gated on a containsOSC scan over
|
||
the chunk.
|
||
- Click-and-drag text selection from alt-screen TUIs (codex in
|
||
particular) now works. Patterm used to keep host SGR mouse
|
||
reporting armed continuously, which forced the host terminal to
|
||
forward every click as an escape sequence and prevented native
|
||
selection. The host's mouse mode now follows the focused child's
|
||
screen side: primary-screen children keep mouse armed (so wheel
|
||
scrollback works), alt-screen children get host mouse disabled by
|
||
default. Alt-screen TUIs that need mouse events (vim, less, etc.)
|
||
re-enable mouse-mode themselves; the viewport renderer forwards
|
||
those toggles to the host while the child is on alt. Leaving alt
|
||
re-arms host mouse reporting so wheel scrollback resumes.
|
||
- Exited terminal panes (kind `terminal`, including those launched via
|
||
the new "New Terminal" palette entry or MCP `spawn_process` with
|
||
`kind=terminal`) are now removed from the session and the Processes
|
||
sidebar as soon as they exit. Previously they stuck around as a
|
||
greyed-out row indistinguishable from an exited command process,
|
||
even though terminals have no restart path.
|
||
- `whoami` and `help("timers")` now advertise the full Solo-parity timer
|
||
surface (`timer_set`, `timer_fire_when_idle_any`,
|
||
`timer_fire_when_idle_all`, `timer_cancel`, `timer_pause`,
|
||
`timer_resume`, `timer_list`) so agents using either tool for
|
||
orientation discover them — previously only `timer_wait` was listed.
|
||
- Resuming a paused idle-aware timer now re-checks the satisfaction
|
||
condition. Previously, if every watched process became idle (or, for
|
||
`idle_any`, any non-baseline watcher went idle) while the timer was
|
||
paused, the timer stayed pending forever because no further state
|
||
transitions were observed.
|
||
- Fired and canceled timers are now removed from the timer registry,
|
||
so long-running patterm sessions no longer accumulate completed
|
||
timer records and message bodies. `timer_list` and the sidebar
|
||
indicator already filtered them out; only the in-memory leak is
|
||
fixed.
|
||
- Per-preset idle-detection config is now installed through `SpawnSpec`
|
||
before the child is published to the session, closing a race in
|
||
which the classifier goroutine could observe a freshly spawned
|
||
process before its preset's classifier strategy was attached.
|
||
- Opening the command palette while a scratchpad was focused left the
|
||
palette wedged — typing did nothing and Esc left the palette's top
|
||
border drawn over the pad until you closed the pad with Ctrl-W and
|
||
re-opened the palette. The stdin loop's scratchpad-input branch ran
|
||
before the palette branch and silently dropped every byte except a
|
||
handful of app-level chords, so palette filter input and Esc never
|
||
reached `palette.handleInput`. The palette branch now takes
|
||
precedence whenever the palette is open, and `closePalette` repaints
|
||
the pad (instead of the empty focused-child slot) on cancel / no-op
|
||
action. Switching from a pad to a child via the palette now clears
|
||
the pad focus and wipes the viewport, matching `focusProcess`.
|
||
- Tab bar and bottom status row no longer get overwritten by long
|
||
claude / codex sessions. Three holes were letting child output land
|
||
on the chrome: (1) absolute cursor moves — CUP / HVP / VPA — added
|
||
the row offset but didn't clamp to the viewport, so a child whose
|
||
internal row state drifted past its assigned height could walk the
|
||
host cursor onto the status row (or above the tab bar); (2) relative
|
||
cursor moves — CUU / CUD / CNL / CPL — were forwarded verbatim, so
|
||
a `CSI 50 A` from viewport row 1 walked the host cursor into the
|
||
tab bar before the next printable wiped it; (3) the host's DECSTBM
|
||
scroll region was only set during snapshot-replay preludes, so the
|
||
windows between (startup before first focus, post-SIGWINCH,
|
||
post-clearScreen) left the region defaulted to the full screen and
|
||
any LF / IND / NEL / RI / SU / SD at the viewport bottom scrolled
|
||
the chrome rows along with the pane. The cursor shifter now clamps
|
||
CUP/HVP/VPA rows to mainTop..mainBottom, the viewport renderer
|
||
rewrites CUU/CUD/CNL/CPL with a clamped step (and homes the column
|
||
for CNL/CPL), and patterm installs the host scroll region after
|
||
`enterScreen` and after every `clearScreen` (and resets it cleanly
|
||
on `leaveScreen` so the calling shell isn't left with a constrained
|
||
region).
|
||
- Plain line-feed scrolling at the bottom of a child pane now invalidates
|
||
and repaints the sidebar, so long agent output can no longer drag the
|
||
sidebar border and labels out of view while the chrome cache stays warm.
|
||
- Child DEC origin-mode and left/right-margin controls are now handled
|
||
inside the viewport renderer instead of being forwarded to the host
|
||
terminal, so later tab bar, sidebar, and status-line repaints keep
|
||
using physical screen coordinates and do not appear inside the
|
||
focused pane.
|
||
- Exited command processes in the top Processes section are now reachable
|
||
with Ctrl+W/S navigation, so a dead shell entry can be focused and
|
||
restarted instead of becoming a visible but unreachable row.
|
||
- Resizing the host terminal no longer makes codex (and other
|
||
diff-based TUIs) scroll-jump for several seconds. SIGWINCH is now
|
||
coalesced into a single resize after an ~80ms idle, the resize path
|
||
skips the full snapshot replay (the child's own SIGWINCH-driven
|
||
redraw fills the viewport), and `Child.NudgeRedraw` no longer
|
||
toggles the PTY through rows-1 + rows back-to-back during a
|
||
drag-resize.
|
||
- Steady-state CPU during a long codex session dropped sharply.
|
||
Tab-bar and status-line repaints moved off the per-PTY-chunk path
|
||
to a 16ms chrome ticker; the scratchpad listing is cached and only
|
||
rebuilt when the pads change; the post-spawn / post-repaint
|
||
styled-snapshot replay budget dropped from 8 chunks to 2; URL/port
|
||
scanning short-circuits chunks that don't contain "http"; the
|
||
three writes around the autowrap toggle in `OnPTYOut` collapsed
|
||
into one syscall; the per-PTY-read `make+copy` was removed (the
|
||
64 KiB read buffer is reused, with a documented "do not retain"
|
||
listener contract); session listeners now dispatch through an
|
||
`atomic.Pointer` snapshot instead of a mutex copy on every chunk;
|
||
the per-child output ring is a true wrap-around buffer instead of
|
||
a slide-and-trim slice; `wait_for_pattern` wakes on PTY chunk
|
||
events with a 500ms fallback instead of unconditional 50ms
|
||
polling; ANSI stripping in MCP `get_process_output stream`,
|
||
`search_output`, and `wait_for_pattern scrollback` is now an
|
||
in-place byte walk; and the viewport renderer copies long ASCII
|
||
runs en bloc instead of feeding the state machine one byte at a
|
||
time.
|
||
|
||
## [0.0.1] - 2026-05-14
|
||
|
||
### Fixed
|
||
- Tab bar redraw used `\x1b[2K` to clear rows 1 and 2 before painting
|
||
labels, which wiped the sidebar columns on those rows too. When the
|
||
sidebar cache was still warm the rail never repainted, leaving a
|
||
gap where the sidebar's top border and "Processes" header should be.
|
||
The clear is now bounded to the viewport width.
|
||
- Long-running TUIs (claude / codex) whose internal column state
|
||
drifted past the patterm viewport could spray text into the sidebar
|
||
columns — overwriting the session-tree and scratchpad rail until the
|
||
user opened/closed the palette to force a full repaint. The viewport
|
||
renderer now clamps absolute cursor positioning (CUP / HVP / CHA /
|
||
HPA) to the viewport's right edge and drops printable bytes (ASCII
|
||
and full UTF-8 glyphs) that would otherwise land past it. Covered by
|
||
a unit suite and a new `sidebar_survives_wide_writes` harness
|
||
scenario.
|
||
- Sub-agent panes spawned while another diff-based TUI (claude/codex/
|
||
opencode) held focus could come up corrupted because the new child's
|
||
first incremental updates targeted cells the host viewport hadn't
|
||
populated yet. `OnChildSpawned` now primes the snapshot-replay budget
|
||
for the new child, so its first PTY chunks render from the full
|
||
styled emulator grid — the same path that fixed the symptom when the
|
||
user manually cycled focus with Ctrl+W / Ctrl+S.
|
||
|
||
### Changed
|
||
- Command palette `Kill …` entries now mark the focused tab with the
|
||
same "• … (current)" marker the `Switch to …` entries use, so the
|
||
user can tell at a glance which tab a kill action targets.
|
||
- Status line now advertises the navigation chords (`Ctrl-A/D · tabs`,
|
||
`Ctrl-W/S · tree`) alongside `Ctrl-K · palette`. Hints decay
|
||
shortest-first when the terminal is too narrow to fit all three.
|
||
|
||
### Added
|
||
- "Spawn process…" entry in the command palette opens a two-field form
|
||
for typing an arbitrary command line and ticking "Relaunch on exit".
|
||
The command runs through `sh -lc` so multi-word lines like
|
||
`bun run dev` resolve binaries the way an interactive shell would.
|
||
When the relaunch flag is set, patterm Starts the process again after
|
||
it exits (1s backoff). Killing the process from the palette clears
|
||
the flag so it does not come back.
|
||
- Dedicated "Processes" section in the sidebar above the agent tree,
|
||
listing every top-level command/terminal process. It is global to
|
||
the patterm session — switching between agent tabs no longer changes
|
||
which processes are visible. The relaunch-on-exit indicator (`⟳`)
|
||
shows next to processes the user opted into auto-restart for.
|
||
- Ctrl+W / Ctrl+S now traverse the combined Processes section and the
|
||
active agent tree as one flat list, so the user can step out of the
|
||
agent tree into the Processes pane and back without leaving the
|
||
keyboard.
|
||
- New `lifecycle` help topic spelling out that the caller owns the
|
||
processes it spawns and should call `close_process` when a sub-agent
|
||
or spawned process is no longer needed. The `spawn_agent` and
|
||
`spawn_process` tool descriptions advertised via `tools/list` now
|
||
call out the same cleanup duty (with a pointer to `help('lifecycle')`),
|
||
and the `spawning` help topic and `topics` index cross-reference it.
|
||
- `spawn_agent` now prepends a single-line `[system: …]` orientation
|
||
block to the sub-agent's first prompt so vendor TUIs (claude, codex,
|
||
opencode) know they're a sub-agent, are reminded to `send_message`
|
||
their parent with a summary when done, and to clean up processes /
|
||
scratchpads they created. Skipped for top-level launches and empty
|
||
instructions.
|
||
- Ctrl+A / Ctrl+D step focus between top-level tabs; Ctrl+W / Ctrl+S
|
||
step through processes (root + sub-agents) inside the current tab.
|
||
Recognised in legacy, kitty CSI u, and xterm modifyOtherKeys
|
||
encodings. The chord shadows the corresponding raw byte for the
|
||
focused pane — pressing Ctrl+D no longer sends EOF to the
|
||
underlying shell, for instance.
|
||
- MCP protocol layer (`internal/mcp/protocol.go`) implementing
|
||
`initialize`, `tools/list`, `tools/call`, `ping`, and MCP
|
||
notifications. Tool catalog with input schemas is advertised via
|
||
`tools/list`. Real MCP clients (claude, etc.) can now complete the
|
||
handshake against patterm's per-PID socket. Legacy direct-tool
|
||
dispatch is preserved so the harness keeps working unchanged.
|
||
- `mcp_injection.kind = "cli_override"` for agents that accept inline
|
||
`key=value` config overrides on the command line. The default codex
|
||
preset uses it to emit `-c mcp_servers.patterm.command=…` and
|
||
`-c mcp_servers.patterm.args=[…]` — zero files written, no
|
||
`CODEX_HOME` override.
|
||
- `mcp_injection.kind = "config_env"` for agents that read their
|
||
config from an env var. The default opencode preset uses it to pass
|
||
a merged `opencode.json` inline through `OPENCODE_CONFIG_CONTENT`,
|
||
so auth/agents/tui.json/skills resolve from the user's real `$HOME`
|
||
with no XDG override.
|
||
- Palette macros: typing `sw `, `k `, or `sp ` filters the list to
|
||
switch / kill / spawn entries respectively. Footer shows the
|
||
available macros.
|
||
|
||
### Changed
|
||
- The sidebar's session-tree section is now labeled "Agent Tree" and
|
||
shows only agent sessions (and any sub-agents they spawn). Top-level
|
||
command and terminal processes live in the new "Processes" section
|
||
above it.
|
||
- Tab bar tabs now correspond to agent sessions only. Command/terminal
|
||
processes that previously claimed a top-level tab now appear in the
|
||
Processes sidebar section, so the tab strip is reserved for agent
|
||
context.
|
||
- Focus, lifecycle, and repaint paths now capture terminal layout before
|
||
taking UI state locks, reducing resize-time deadlock risk without
|
||
changing visible behavior.
|
||
- Focused PTY output no longer rebuilds the scratchpad sidebar on every
|
||
chunk. The sidebar still repaints on focus/lifecycle/resize changes
|
||
and when child output scrolls over the chrome, but normal output avoids
|
||
repeated scratchpad disk reads.
|
||
- Harness scenario tests now reuse one built patterm binary per test run
|
||
and write failure artifacts under a repo-rooted, collision-proof
|
||
directory.
|
||
- Palette ordering: open agents/processes (`Switch to …`) now appear
|
||
above the option to spawn new ones, with kill entries pushed down
|
||
toward the end of the list.
|
||
- Tab bar trimmed from three rows (label / subtitle / underline) to
|
||
two (label / underline). Tabs flex to fill the available host width
|
||
evenly with leftover columns distributed to the leftmost tabs; the
|
||
`+ new` hint sits in a reserved slot on the right. Layout's
|
||
`mainTop` consequently drops from 4 to 3, giving each pane one
|
||
extra row of viewport.
|
||
|
||
### Fixed
|
||
- Agent MCP injection no longer writes unused config files for inline
|
||
injection modes (`cli_override` / `config_env`). File-backed injection
|
||
modes track their generated paths and clean them up when the child is
|
||
closed, exits, or patterm shuts down.
|
||
- MCP `tools/list` descriptions now match the runtime argument values
|
||
for process output and pattern waiting, and typed invalid-argument
|
||
errors map to JSON-RPC invalid params instead of generic internal
|
||
errors.
|
||
- Scratchpad writes and appends are serialized inside a patterm process
|
||
so `expected_revision` checks cannot race another local scratchpad
|
||
mutation.
|
||
- The sidebar scratchpad list now refreshes after MCP
|
||
`scratchpad_write` and `scratchpad_append` calls.
|
||
- UI chrome now reads renamed child display names through the
|
||
`DisplayName` accessor, avoiding races with `rename_process`.
|
||
- Child processes spawned by an orchestrator are now killed when the
|
||
orchestrator dies, recursively through the tree. Applies whether the
|
||
parent was closed via MCP, Ctrl-C'd by the user, or exited on its
|
||
own — `reapChild` cascades a SIGTERM (escalating to SIGKILL after 2s)
|
||
to every direct child, and each descendant's own reap fires the same
|
||
cascade so the kill flows through arbitrary depth.
|
||
- Killed agents no longer linger in the command palette. Agent
|
||
entries that aren't running are filtered out of the switch list;
|
||
session-persistent commands (which can be restarted) stay visible.
|
||
- `tools/list` now emits a concrete `properties` object (`{}`) for
|
||
parameterless tools instead of `null`. Claude rejected the
|
||
`null`-properties form with "tools fetch failed" even though the
|
||
initialize handshake had succeeded.
|
||
- Sidebar no longer flickers on every PTY chunk. The tab bar,
|
||
sidebar, and status line now cache their last rendered byte string
|
||
and skip the write when the new frame matches; full repaint paths
|
||
(resize / focus change / palette close / screen clear) invalidate
|
||
the cache so the next draw fires unconditionally.
|
||
- Spawning a child agent now clears the viewport area before it
|
||
paints, so the previous focused child's PTY output no longer bleeds
|
||
through underneath the new pane.
|
||
- Orchestrator-injected input (initial agent prompts, MCP
|
||
`send_input` with `submit: true`, `send_message`, `timer_wait`
|
||
callbacks) now ends with CR (`\r`) instead of LF (`\n`). Claude
|
||
treated `\n` as "newline in textarea"; with CR the prompt actually
|
||
submits, matching what the host terminal sends when a user presses
|
||
Enter directly.
|
||
- Enter is now written to a child PTY as its own `write()` call,
|
||
separated from the preceding text by a short delay. Both
|
||
`InjectAsUser` (user typing forwarded through patterm) and
|
||
`InjectAsOrchestrator` (MCP / send_message / initial-prompt paths)
|
||
share the split. Without it, claude — and other paste-detecting
|
||
TUIs — coalesced `"hello\r"` into one read and inserted the CR as
|
||
literal text instead of treating it as the Enter keystroke.
|
||
- Sidebar (and tab bar) no longer get wiped when the focused child
|
||
issues `CSI 0 J` / `CSI 1 J` (clear-to-cursor). The viewport renderer
|
||
already clamped `CSI 2 J` and `CSI K` to viewport columns, but the
|
||
partial-screen variants were forwarded verbatim, so any tool-call
|
||
expansion in claude (Ctrl+O) would erase every cell to the right of
|
||
the cursor — including the right rail. Both forms are now translated
|
||
into per-row ECH sequences that stop at the viewport's right edge.
|
||
- Sidebar left border no longer vanishes when the viewport repaints.
|
||
The border column was the same column as the viewport's rightmost
|
||
cell, so any child write to that column (or `clearViewport`'s ECH)
|
||
would erase it. The viewport is now one column narrower so the
|
||
border has a dedicated column.
|
||
- Sidebar session-tree entries no longer get pushed downward when an
|
||
agent emits a scroll burst on startup. Codex (Ratatui) issues 8× RI
|
||
(`\x1bM`) right after setting its scroll region, which scrolls the
|
||
region down across every column — dragging the right-rail entries
|
||
with it. The chrome cache then hid the clobber because the computed
|
||
frame still matched. The viewport renderer now flags any chunk that
|
||
contains a scroll-triggering escape (RI / IND / NEL / SU / SD / IL /
|
||
DL) and `OnPTYOut` drops the sidebar cache when the flag is set, so
|
||
the next `drawSidebar` repaints over the drift.
|
||
|
||
## Conventions
|
||
|
||
- This file is the single record of user-visible changes; the TODO is
|
||
scratch space, not history.
|
||
- One bullet per change, written in the past tense from the user's
|
||
point of view. Reference the package or preset name when it helps a
|
||
reader find the code.
|