Zum Inhalt springen
Alle Releases

v0.3.3

Veröffentlicht am .

Paneflow v0.3.3 — Multi-session agents, code-path scanner, IPC singleton guard

Fourth drop in the Agents view (cmux-port-2026-Q2) and the largest one yet — 14 atomic commits across the runtime, the agent UI, the terminal, the config schema, and the IPC layer. The release is organised around three big themes:

  1. Multi-session agent tracking — two Claude Codes or two Codex sessions in the same workspace now show up as two distinct rows in the sidebar instead of collapsing into one. Driven by a full refactor of the agent-state model from a single enum to a per-PID map, plus PID stamping on every IPC frame from the shim.
  2. Editor jumps from terminal output — Cmd/Ctrl-click on path/foo.rs:42:7 in any terminal pane (compiler error, test runner output, stack trace) opens the file at that line + column in your preferred editor (Zed / Cursor / VS Code / Neovim / Vim / Helix / Emacs). Same handler runs for markdown links in assistant messages.
  3. ACP capabilities upgrade — Paneflow now declares the same client capabilities Zed declares to Codex / Claude Code. Result: Codex stops skipping AgentThoughtChunk reasoning streams, "Thinking" blocks render properly, and turns no longer end prematurely.

Plus a long tail of terminal polish (10K scrollback, OSC fixes, Ctrl+click throttle, prompt-mark scrollbar ticks), an IPC singleton guard that stops two Paneflow instances from clobbering each other's socket, a thread-view overhaul with throttled persistence, the default mono font switched to IBM Plex Mono, and the CLI/Agents toggle moved from the title bar into the sidebar footer.


Multi-session agent tracking (per-PID)

Previously Workspace held a single AiToolState enum and a HashMap<String, u32> keyed by tool name. Two Claude Codes in the same workspace meant the second ai.session_start overwrote the first PID, and the sidebar showed one "Claude thinking" badge regardless of how many sessions were really live. Updates from the second session were silently routed to the wrong PID.

The new model:

  • Workspace::agent_sessions: HashMap<u32, AgentSession> — one entry per live PID.
  • AgentState enum (Thinking / WaitingForInput / Finished); Inactive is implicit (no entry).
  • AgentSession { tool, state, active_tool_name } captures the per-PID lifecycle.
  • aggregate_by_tool() collapses N concurrent sessions of the same tool into one sidebar row with a +N suffix and the most salient state (Waiting > Thinking > Finished). One row per tool, deterministic Claude-first ordering.

On the shim/hook side, PANEFLOW_AI_PID is now exported on every child the shim spawns (Claude Code, Codex, JSONL-tee, interrupt/Stop, SessionEnd) and paneflow-ai-hook promotes it to a top-level params.pid field on every lifecycle frame — not just SessionStart. The IPC handlers (ai.prompt_submit, ai.tool_use, ai.stop, ai.session_end) route through PID-keyed helpers; ai.session_start is now a no-op (a freshly-spawned shell with no prompt in flight shouldn't show any badge — the first prompt creates the row).

The stale-PID sweep skips synthetic PIDs (allocated in the upper half of u32 for back-compat with shims that don't stamp pid on every frame), so a legacy session isn't killed mid-turn by a kill(pid, 0) probe that would always say "dead" for an out-of-range PID.

ACP client capabilities (Zed parity)

Two related fixes in paneflow-acp that together unlock the same response richness Zed gets:

InitializeRequest now declares full ClientCapabilities:

  • fs.read_text_file(true) + fs.write_text_file(true)
  • terminal(true)
  • auth(AuthCapabilities::new().terminal(true))
  • meta flags terminal_output and terminal-auth
  • client_info("paneflow", VERSION).title("Paneflow")

Byte-for-byte matches Zed's crates/agent_servers/src/acp.rs:888-904. Empirically observed: a bare InitializeRequest::new(V1) (no caps) makes Codex skip AgentThoughtChunk reasoning streams entirely — "Thinking" blocks never render and turns end prematurely. With the full caps declaration, the reasoning streams come through.

NewSessionRequest now sends explicit empty mcp_servers + additional_directories arrays (instead of omitting the fields). The ACP wrappers (codex-acp, claude-code-acp) treat the absence of these fields as a stripped-down host signal and adopt a terser response style; sending them as empty arrays says "I implement these surfaces, just nothing configured right now".

Required two new agent-client-protocol = "0.12" features: unstable_auth_methods and unstable_session_additional_directories. Features mirrored across paneflow-acp and src-app so Cargo unifies on one compiled artifact.

Streaming-buffer micro-perf bonus: pending_chars is now maintained incrementally on every push/tick/flush instead of calling pending.chars().count() (O(n)) per tick. Drops the hottest non-allocation cost in the agents UI under sustained streams.

Cmd+click on file:line:col opens your editor

New crate-local module src-app/src/editor.rs plus a code-path scanner in terminal/element/hyperlink.rs. Recognises any of path/foo.rs, path/foo.rs:42, path/foo.rs:42:7 style references (40+ recognised extensions; .md deliberately excluded so the markdown viewer keeps that surface; Windows drive-letter-safe). Ctrl/Cmd-click in any terminal pane emits TerminalEvent::OpenCodePath { path, line, col } which the app routes to the editor on the background executor.

Three-stage open strategy:

  1. $VISUAL env, then $EDITOR env. The string is parsed as a shell command (binary + flags) so users running EDITOR="code --wait" get their pre-set flags carried over.
  2. Probed fallback chain: codecursorzedsublnvimvimhxemacs. First binary found on PATH wins.
  3. Last-resort open::that(path) so the OS launcher hands the file to its registered handler (loses the line/col target but always does something useful).

Per-family argv shape so the location actually lands:

  • code -g path:L:C (VS Code / Cursor / Codium clones)
  • nvim +L path / vim +L path (Vim family)
  • emacsclient +L:C path (Emacs)
  • bare path:L:C (Zed, Helix, Sublime)

12 unit tests cover the regex splitter, scanner end-to-end, editor argv construction, and the fallback chain. Markdown link clicks in assistant messages route through the same module (via the new agents/external_editor.rs), so a click on [foo](src/foo.rs:42) opens at the right line just like a terminal click does.

New external_editor config field ("auto" / "system" / "zed" / "cursor" / "windsurf" / "code") lets the user pin a specific editor instead of relying on the auto-probe order.

IPC singleton guard

Without the guard, two parallel Paneflow processes entered an endless mutual-clobber loop. Each one's 5 s health check noticed the other's rebind, dropped its listener, and recreated the socket. During every micro-window between drop and re-create, the AI shim's connect() failed, IPC messages were silently lost, and a session's Thinking / Done / session_start status stayed stale forever.

start_server now probes the socket with system.identify before the IPC thread spawns and before bind_socket blindly remove_files any existing socket. If a live Paneflow already responds with "PaneFlow", the new process logs an error and exits 1 with a clear message:

paneflow: another Paneflow instance is already running on /run/user/1000/paneflow/paneflow.sock.
Existing instance: PaneFlow 0.3.3
Close the open window first, or set PANEFLOW_ALLOW_MULTIPLE=1 to override.

Other outcomes (missing file, stale socket from a SIGKILL'd prior run, non-Paneflow listener, parse failure, timeout) let the caller proceed normally. Probe is resilient to the legacy rebind race (3 attempts, 70 ms gap, 300 ms timeout per attempt — comfortably crosses the ~10 ms remove_file + create_sync + chmod window).

PANEFLOW_ALLOW_MULTIPLE=1 escape hatch for intentional side-by-side debug instances.

Terminal: scrollback, OSC fixes, Ctrl+hover throttle, mouse/input polish

A grab-bag of terminal-surface improvements that landed during the agents work. Kept atomic at the module level (terminal/ + mouse + keys) because the changes interlock.

PTY + lifecycle:

  • terminal.scrollback_lines config (default 10_000, clamp [100, 100_000]) replaces a fixed 1024-line buffer. Long compiler / log output no longer truncates mid-session.
  • Drop SHLVL from the child env (Zed parity) so the shell starts at level 1, not nested under the GUI launcher.
  • Drop-time force-kill timer routed through GPUI's background executor instead of a detached OS thread — removes a thread leak per closed pane under heavy use.
  • PTY message loop coalesces consecutive Resize messages so a drag-resize no longer floods the child with SIGWINCH storms.
  • PTY failure falls back to a display-only TerminalState that surfaces the error in the pane instead of panicking the whole app.

OSC + escape sequences:

  • OSC 4 palette-color queries (vim, nvim, python-rich) now answered.
  • OSC 133 / OSC 7 scanners accept C1 ST (\x9c) as a terminator (fish on some locales).
  • OSC 52 Load capped at 100 KiB to match the Store cap.
  • Scrollbar gutter paints amber ticks for OSC 133 PromptStart markers — skim by command boundary like in modern terminals.
  • OSC 8 cells always underline regardless of the cell flags.

Mouse / input:

  • mouse_button_code returns Option<u8>MouseButton::Navigate (side / back / forward buttons) no longer injects phantom Left clicks into TUI apps with mouse mode active.
  • Modifier+Delete now sends \x1b[3;{m}~ (matches xterm).
  • Ctrl+hover hyperlink rescan throttled to fire only when the hovered cell changes (was ~60 regex sweeps/s under continuous hover).

Rendering:

  • APCA contrast adjustment skipped for truecolor + xterm-256 palette cells so bat / lazygit / Neovim themes stop getting washed out.
  • Grid cell-count uses next_up().floor() to prevent off-by-one on exact multiples.
  • Read-only paths use lock_unfair() so the main thread no longer queues behind the PTY reader.
  • Linux/FreeBSD PRIMARY selection updated live during drag.
  • Snap-to-bottom on keystroke when scrolled back.
  • BatchAccumulator drops combining marks on empty buffer instead of panicking.
  • URL regex / is_url_scheme_openable extended with ipfs:, ipns:, magnet: schemes.

Agents: thread view overhaul

Visual + behavioural pass on ThreadView, the embedded agent terminal, the assistant-message renderer, and the markdown link handler. Motivated by the visual-parity loop with Zed's agent panel.

  • tool_call_index: HashMap<String, usize> — O(1) lookup of an existing tool-call by id instead of a linear scan over every visible ThreadItem on each tool_call_update.
  • tool_group_user_open — user-forced expand override, persists past auto-collapse on stream end.
  • streaming_thinking_key + Zed-parity auto-collapse: thinking blocks expand mid-stream and collapse the moment the turn ends.
  • pending_title_generation: Option<Task<()>> — cancellable in-flight summarizer when a new TurnEnded arrives; title_generation_failed flag drives the affordance.
  • persist_deadline throttles SQLite writes to once / 500 ms during streaming. The per-token write was costing 2-4 ms on the GPUI main thread, eating the 16 ms render budget.
  • Arc<ToolCallSnapshot> in ThreadItem::ToolCall — ref-counted clone instead of full struct copy per visible frame.

Embedded agent terminal (agents/agent_terminal.rs):

  • strip_ansi_bytes removes CSI / OSC / DCS sequences from the LLM-context snapshot so cargo / bat / git colour codes stop wasting tokens. 15 tests cover the parser.
  • PTY size raised from 24×80 to 500×120 — 80-col wrap in compiler output was forcing the agent to spend tokens reflowing.
  • kill(-pgid, SIGTERM) targets the entire process group, then SIGKILL escalates after a 2 s grace — closes the shell + every subshell with no orphan child processes after a terminal-thread close.

Composer / runtime push-model refactor (agents/runtime.rs, agents/composer.rs):

  • SessionRuntime::take_event_receiver() returns futures::channel::mpsc::UnboundedReceiver<RuntimeEvent>. Internal senders migrated from std::sync::mpsc::Sender to futures::channel::mpsc::UnboundedSender; poll() removed.
  • Dedicated _event_task in the composer awaits the receiver and drains all queued events on every GPUI wake. First-token latency drops from "up to 16 ms" (next render tick) to "next scheduler tick".
  • Exponential-backoff respawn on consecutive Fatal events — a misconfigured agent no longer hot-loops the spawn syscall.
  • session_ready_deadline detects an agent that took the handshake but never advertised a session.
  • on_release hook flushes in-flight streaming and finalizes any open thinking block before teardown — fixes truncated Codex turns when the user closes the pane mid-response.
  • Lazy-cached profiles / model / content-state so the composer no longer re-reads disk + entity state per frame.

Markdown link clicks → external editor: on_url_click two-stage handler — external editor first, then resolve the bare path to a file:// URI for cx.open_url. Fixes the "URI must contain a scheme" error from xdg-open on bare paths.

Polish:

  • Edit-tool card margin aligned to 20 px horizontal (matches prose column) + 6 px vertical (Zed pattern — reads as one continuous turn instead of three bubbles).
  • List bullet colour neutralised (was salmon accent, fought the off-white body); Lilex override on inline code removed so the highlight reads as a tint, not a separate code span.
  • MAX_ATTACHMENTS = 20 cap with localizable message, enforced in complete_image_attach + drag-drop.
  • Inline-rename TextArea pre-selects the existing name so the user can just start typing.

Chrome: CLI/Agents toggle moves to the sidebar footer

The title-bar segmented toggle relied on PaneFlowApp pushing mode + agents_view_shortcut into the TitleBar entity on every render, then re-emitting TitleBarEvent::ToggleAgentsView through the focus chain. Dispatching the action through the focus chain was unreliable when a child entity (e.g. the composer's focused TextArea) intercepted the action before the root on_action listener ever saw it.

The new placement is a render_mode_toggle in sidebar_actions_menu.rs, rendered directly in the footer of both the CLI and Agents sidebars. No focus-chain dispatch, no per-render state push, no TitleBarEvent round-trip. The control is always visible regardless of which mode is active and matches the Linear / Vercel / Cursor segmented-control language.

~170 LOC deleted from title_bar.rs (the render_agents_toggle_button function, the TitleBarEvent::ToggleAgentsView variant, the mode/agents_view_shortcut fields, their per-render push).

Default mono font swap

The four IBM Plex Mono weights (Regular, SemiBold, Italic, SemiBoldItalic) + OFL license are now embedded under src-app/assets/fonts/. EMBEDDED_MONO_FAMILY is promoted from "Lilex" to "IBM Plex Mono".

Lilex remains embedded and opt-in via the user's config (kept in the fast-path of resolve_font_family) for users who prefer programming ligatures. The IBM face matches the rest of the embedded UI family (IBM Plex Sans) and renders better at 14 px on Wayland fractional-scaling setups, which was the dominant complaint on Lilex.

Theme perf: parking_lot + ui_colors_with

The active-theme cache is read several times per render frame from the agents UI (Composer, ThreadView, every visible message). std::sync::Mutex was the dominant lock in that hot path under live streaming.

  • THEME_CACHE now uses parking_lot::Mutex (~2x faster under contention, no poisoning). Already a transitive dep via gpui/tokio.
  • New ui_colors_with(&TerminalTheme) — identical output to ui_colors() but skips the global theme-cache lock when the caller already has the theme in hand. Saves O(visible_items) mutex acquisitions per frame in the agents timeline.

Runtime: APP_SUBDIR debug/release isolation

A cargo run dev instance and a user's installed /usr/bin/paneflow used to share threads.db, session.json, the config file, the IPC socket dir, the shell-integration dir and the AppImage cache. This manifested as crashed threads, lost panes, and the stale-PID sweep killing the wrong process during local development.

New APP_SUBDIR const (paneflow in release, paneflow-dev in debug), mirrored across three crates (runtime_paths, paneflow-config, paneflow-threads) and plumbed through every persistence surface. Tests assert against the const so debug-profile test runs pass.

Config: new fields

  • terminal.scrollback_lines: Option<usize> — Zed parity (max_scroll_history_lines). Default 10_000, clamp range [100, 100_000], out-of-range values log a warn! on first read. Read once at PTY spawn time.
  • external_editor: Option<String>"auto" (default), "system", or one of "zed" / "cursor" / "windsurf" / "code".

Compatibility

  • session.json format unchanged.
  • threads.db unchanged (the Arc<ToolCallSnapshot> change is in-memory only).
  • IPC protocol additive — old shims without PANEFLOW_AI_PID stamp still work via the synthetic-PID fallback path (session_start becomes the synthesizer), they just won't render as multi-session.
  • Cross-platform: full parity across Linux (Wayland + X11), macOS (Intel + Apple Silicon), Windows (x64).
  • Debug builds now write into paneflow-dev subdirs — your existing cargo run state file will appear "missing" after upgrading; copy ~/.config/paneflow/paneflow.json~/.config/paneflow-dev/paneflow.json if you want it back.

Commits

  • chore: ignore local LOG_*.text codex/agent debug dumps (cc0326c)
  • chore(fonts): switch default mono from Lilex to IBM Plex Mono (fcfe336)
  • perf(theme): swap theme-cache Mutex to parking_lot + ui_colors_with helper (988b532)
  • chore(runtime): isolate dev and release state via APP_SUBDIR (ec54f14)
  • feat(config): add terminal.scrollback_lines and external_editor fields (100da84)
  • feat(acp): full client capabilities + multi-session PID stamping (9e019e2)
  • feat(agents): per-PID multi-session tracking and sidebar aggregation (acafac0)
  • feat(ipc): singleton guard so a second instance refuses to start (ae0f2f5)
  • feat(terminal): Cmd+click on file:line:col jumps to source in editor (a655d9f)
  • feat(terminal): scrollback, OSC fixes, hyperlink throttle, Ctrl+click wiring (8858361)
  • feat(agents): push-based ACP event stream, respawn backoff, flush on close (b10a00f)
  • feat(agents): thread view overhaul, external-editor link clicks, terminal capture (02fabfb)
  • refactor(chrome): move CLI/Agents toggle from title bar to sidebar footer (8456839)
  • chore(release): v0.3.3 -- multi-session agents, code-path scanner, IPC guard (d0622d4)
  • fix(tests): make new path-resolution tests cross-platform (cf3db24)

Full Changelog: https://github.com/ArthurDEV44/paneflow/compare/v0.3.2...v0.3.3