Aller au contenu

v0.11.0Retour au changelog

Worktrees per tab, faster terminal

Sortie le .

A tab is a worktree, and the terminal is faster

Paneflow 0.11.0 gives a tab a git identity. Picking a branch from the "New pane" palette or a tab's context menu creates or reuses a worktree under <workspace>.worktrees/ and starts the pane there, so a workspace of parallel agents stops fighting over one checkout. "Remove worktree" in the same menu takes it back down, with the guards that keep a checkout you still need from disappearing.

The other half of the release is the terminal pipeline. Four stages were doing redundant work, most of it invisible on Linux and very visible on Windows. On the project's eight-pane render benchmark the median input-to-frame time drops 32%, its 95th percentile 26%, and total CPU 35%. A pane sitting at its prompt wakes 9 times a second on Windows instead of 64.

No configuration key changes meaning, the session format stays at v2, and there is no breaking change in this release. Install over 0.10.x and launch.

Behavior changes

The terminal publishes frames on a display's schedule, not the PTY's

The runtime thread used to rebuild and publish a full grid snapshot for every output batch, and OUTPUT_BATCH_MAX_TIME closes a batch every millisecond. PublishGate now holds a frame back for two reasons: the program is mid-redraw under DEC 2026 synchronized output, which is the same check Ghostty's renderer makes in src/renderer/generic.zig, or another frame is already queued behind it and the last one is newer than 8 ms. A hold expires after 150 ms, so a program that opens a synchronized frame and dies cannot freeze the pane, and resizes, scrolls, the first frame of a session and the frame preceding ChildExited all bypass the gate.

Keystroke echo is untouched: with the PTY queue drained there is nothing to coalesce with, so a lone change publishes immediately. What you should see is fewer torn frames from full-screen TUIs and no delayed leading wakeup on Windows and macOS, which were holding every wakeup for an extra event-batch window precisely because ConPTY can split a synchronized-output sequence around a redraw.

Measured on the reproducible suite (scripts/bench-terminal.sh, release profile, x86_64 Windows), against the pre-work baseline 4066faf6:

Metric Before After
publish_echo_220x60 median 166 us 8.3 us
publish_scroll_220x60 median 701 us 448 us
line_text_at_220x60 median 7.1 us 0.6 us
pipeline_corpus_mib_s 0.23 MiB/s 0.90 MiB/s
idle_wakeups_shell_per_s 64 9.0
gate_trickle_publishes 1000 per 1000 chunks 250

The eight-pane render benchmark, same host, moves median input-to-frame from 1827 to 1245 us, p95 from 2426 to 1804 us, and total CPU from 2531 to 1656 ms. Peak RSS goes from 36.8 to 49.8 MiB: that is the retained per-pane layout, roughly 1.6 MB per open pane, which is the one thing this release costs you.

A pane opened for an agent loads your PowerShell profile

On Windows an agent pane started PowerShell with -NoProfile. The shell you got back after the agent exited had none of your prompt, aliases, functions or PSReadLine setup: a bare PS C:\dev\project> that read as if Paneflow had launched something other than PowerShell. It also defeated Paneflow's own prompt integration, which dot-sources after $PROFILE specifically to wrap a prompt you defined rather than replace it. Windows was the only platform doing this; the zsh, bash and fish paths always loaded the user's rc files.

An agent pane now starts the same way as any other pane. If your $PROFILE is slow, an agent pane now pays that cost too. The Clear-Host already prefixed to the agent command keeps the TUI's first frame clean.

Windows holds a 1 ms timer resolution while the window is open

Windows delivers timer expirations on a 15.6 ms clock tick unless a process asks for better, and since Windows 10 2004 that default is per-process. Every short timeout in the terminal pipeline was being rounded up to it, which capped the update rate and added up to 15 ms of latency to output that had already been parsed. Paneflow now requests 1 ms for the lifetime of the window and releases it on close. There is a small battery cost to a 1 ms tick, which is why it is scoped to the window being open rather than to the process.

The shell you picked in Settings is the shell that launches, on Windows

Choosing PowerShell stored a bare pwsh.exe, resolved only through PATH. An app launched from Explorer inherits whatever environment Explorer was started with, so a stale or truncated PATH silently rejected the choice and let the fallback chain pick another shell, occasionally the Command Prompt. Each named shell now also resolves from its absolute install location, the way the unconfigured fallback already did, and Windows PowerShell 5.1 is found under System32 even when its own PATH entry is missing. Picking PowerShell no longer gets you Windows PowerShell, or the other way round.

PowerShell 7 discovery is resolved once per run instead of once per pane, so restoring a many-pane workspace no longer re-walks ProgramFiles and PATH for every pane while the disk is busy. RUST_LOG=info now reports the shell each pane actually launched next to the configured value.

Worktrees

A tab binds to its own worktree

The "New pane" palette and the tab context menu list the repository's branches. Picking one that has no worktree creates it under <workspace>.worktrees/, picking one that already has a worktree reuses it, and the pane starts there. An agent that creates a branch from inside a pane now moves that tab alone, where it used to drag every tab of the workspace with it. Refs discussion #41.

The binding is stored per tab in the session as an optional worktree path. The session schema stays at v2: the field is additive and defaulted, a file written by 0.10.x parses with no binding, and a path that no longer exists at restore is dropped rather than resurrected.

"Remove worktree" takes a checkout back down

The sidebar could create checkouts but never take one away, so <repo>.worktrees/ grew for the life of a project with no way back. A checkout prepare_branch_checkout makes is deliberately not a ManagedWorktree, so workspace-close teardown never touches it.

Removal holds the same invariants teardown holds for orchestration's own worktrees, and each refusal is a toast rather than a log line, because you asked for this one:

  • The branch is never deleted, only its checkout.
  • A checkout holding uncommitted changes is refused.
  • A checkout without Paneflow's owner marker is refused: it belongs to somebody else.
  • A checkout that is itself an open workspace is refused, because the panes over there would be left in a directory that no longer exists.

The git work runs off the render thread. Removing also invalidates the repository's Worktree-scope diff hosts, which is what makes a lane appear or disappear without a scope toggle or a restart.

Sidebar

Customize Sidebar

The rail header gains a Customize Sidebar menu with a switch per value: Branch, PR, Diffstat and the indent guide, all off by default, so the rail you have today does not change until you turn something on. A branch that already has a pull request swaps its glyph for the pull-request one, in GitHub's state colors. Below them, Expand all and Collapse all.

The fold state of each workspace row now survives a restart, through an optional sidebar_collapsed flag written only when a row is folded.

The four switches persist under a new optional sidebar_show object in paneflow.json:

{
  "sidebar_show": {
    "branch": false,
    "diffstat": false,
    "pr": false,
    "indent_guide": false
  }
}

branch and diffstat read the tab's bound worktree, or its workspace's checkout when the tab is unbound. pr needs the gh CLI and answers for GitHub remotes only.

Added

  • DEC 2026 synchronized output is exposed by the engine. The mode was decoded but never readable, so nothing downstream could act on a program's "do not show this yet". It is now on the snapshot, which is what lets the publish gate skip a torn frame at the source.

Fixed

  • The last frame of a program is no longer dropped. The publish rate limit applied unconditionally, so a change landing within 8 ms of the previous frame waited for the runtime loop, and the loop exits as soon as the child is reaped. A program's final output could disappear, and the bigger the closing burst, the more of it was lost. CI caught it on live_runtime_runs_platform_shell_and_reports_one_exit, which on Linux x86_64 saw the shell's first line but not the stty size that followed, and on the slower aarch64 runner saw an empty grid. The frame preceding ChildExited is now flushed explicitly on both the POSIX and the Windows teardown path.
  • The pull request marker works at all. gh has no global directory flag, so gh -C <repo> pr list exited 1 with unknown shorthand flag: 'C' on every call, and each failure blacklisted the repository for the rest of the session because is_stale never asks again. The lookup now runs with the repository as its working directory. A log::debug! on the failing path prints what gh said, since the caller turns this error into a silent blacklist entry and a wrong invocation used to look exactly like a checkout with no GitHub remote.
  • Windows verbatim paths no longer break worktree detection. \\?\-prefixed paths are handed to git as arguments and compared against the paths git prints, and the verbatim spelling fails at both: git worktree add cannot create leading directories under it, and it never compares equal to git's forward-slash output. "Is this checkout the repository's own?" therefore answered no for every branch on Windows. strip_verbatim_prefix now lives once in runtime_paths and workspace::git::canonicalize_or strips too; the two private copies that had grown in the IPC workspace.create path and in install-method detection call the shared helper.

Under the hood

  • A reproducible terminal benchmark. An ignored release-profile test measures the pipeline with no GPU and no window: snapshot plus conversion on scroll and on keystroke echo, the window-free layout pass, the per-frame render-thread lookups, the publish gate on a trickle, corpus throughput, and the idle wakeups of a display-only and of a live shell session. A counting allocator reports exact bytes and calls per iteration, and the run records its own CPU share and warns when another workload was competing. scripts/bench-terminal.sh and .ps1 build it, stamp the result, archive it under bench/results, and print a Markdown comparison against bench/baseline.json. See bench/README.md.
  • The pinned libghostty source bump is automated. Re-pinning libghostty-vt was a manual pass across two build hosts; a workflow now stages the manifest, regenerates the bindings, rebuilds all four reviewed targets against the staged manifest with --verify-reproducible --allow-hash-drift, writes the hashes those builds produced, and opens the pull request. Reproducibility is proven in the bump run rather than on the resulting pull request, which would be circular evidence. Two pins it refuses to move on its own, both checked before any build starts: minimum_zig_version and src/terminal/formatter.zig. #49, #50, #51.

Upgrade notes

Install over 0.10.x and launch. No manual step is required, and nothing needs to be edited in paneflow.json.

  • There is no breaking change and no removed setting in this release.
  • Two optional keys are added: sidebar_show in the config, worktree and sidebar_collapsed in the session. All default to the 0.10.x behavior when absent.
  • The session format stays at v2, and the packaging contract and the IPC and MCP method names are unchanged.
  • Downgrading to 0.10.x works. An older build ignores the three new keys, losing only the per-tab worktree binding and the remembered fold state.
  • If you turn on the PR switch, install the gh CLI and authenticate it. Without it the branch keeps its own glyph and nothing else changes.
  • Worktrees Paneflow creates live in <workspace>.worktrees/, a sibling of your repository. Add it to your global gitignore if your tooling walks siblings.

Install and validation

Download the signed artifacts from this release or use the installation instructions.

Every primary artifact carries a .sha256 sidecar and a minisign .minisig; each AppImage also ships an .AppImage.zsync for delta updates. 35 assets in total.

Pipeline: run 33609444456.

Legs that passed:

  • Build: x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, aarch64-apple-darwin, x86_64-pc-windows-msvc
  • Release test gate on Linux x86_64: cargo fmt --check, cargo clippy --workspace --locked -- -D warnings, cargo test --workspace --locked
  • Package smoke tests: Debian 12 and Ubuntu 22.04 (.deb postinst), Fedora 40 and openSUSE Tumbleweed (.rpm postinst), Arch Linux (tar.gz binary), Windows MSI install and relay
  • Auto-update end to end on Linux x86_64

Full Changelog: v0.10.0...v0.11.0