v0.3.3
发布于 。
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:
- 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.
- Editor jumps from terminal output — Cmd/Ctrl-click on
path/foo.rs:42:7in 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. - ACP capabilities upgrade — Paneflow now declares the same client capabilities Zed declares to Codex / Claude Code. Result: Codex stops skipping
AgentThoughtChunkreasoning 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.AgentStateenum (Thinking/WaitingForInput/Finished);Inactiveis 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+Nsuffix 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))metaflagsterminal_outputandterminal-authclient_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:
$VISUALenv, then$EDITORenv. The string is parsed as a shell command (binary + flags) so users runningEDITOR="code --wait"get their pre-set flags carried over.- Probed fallback chain:
code→cursor→zed→subl→nvim→vim→hx→emacs. First binary found on PATH wins. - 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_linesconfig (default 10_000, clamp[100, 100_000]) replaces a fixed 1024-line buffer. Long compiler / log output no longer truncates mid-session.- Drop
SHLVLfrom 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
Resizemessages so a drag-resize no longer floods the child with SIGWINCH storms. - PTY failure falls back to a display-only
TerminalStatethat 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
PromptStartmarkers — skim by command boundary like in modern terminals. - OSC 8 cells always underline regardless of the cell flags.
Mouse / input:
mouse_button_codereturnsOption<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.
BatchAccumulatordrops combining marks on empty buffer instead of panicking.- URL regex /
is_url_scheme_openableextended withipfs:,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 visibleThreadItemon eachtool_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 newTurnEndedarrives;title_generation_failedflag drives the affordance.persist_deadlinethrottles 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>inThreadItem::ToolCall— ref-counted clone instead of full struct copy per visible frame.
Embedded agent terminal (agents/agent_terminal.rs):
strip_ansi_bytesremoves CSI / OSC / DCS sequences from the LLM-context snapshot socargo/bat/gitcolour 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()returnsfutures::channel::mpsc::UnboundedReceiver<RuntimeEvent>. Internal senders migrated fromstd::sync::mpsc::Sendertofutures::channel::mpsc::UnboundedSender;poll()removed.- Dedicated
_event_taskin 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_deadlinedetects an agent that took the handshake but never advertised a session.on_releasehook 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 = 20cap with localizable message, enforced incomplete_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_CACHEnow usesparking_lot::Mutex(~2x faster under contention, no poisoning). Already a transitive dep via gpui/tokio.- New
ui_colors_with(&TerminalTheme)— identical output toui_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 awarn!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.jsonformat unchanged.threads.dbunchanged (theArc<ToolCallSnapshot>change is in-memory only).- IPC protocol additive — old shims without
PANEFLOW_AI_PIDstamp still work via the synthetic-PID fallback path (session_startbecomes 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-devsubdirs — your existingcargo runstate file will appear "missing" after upgrading; copy~/.config/paneflow/paneflow.json→~/.config/paneflow-dev/paneflow.jsonif you want it back.
Commits
chore:ignore localLOG_*.textcodex/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_withhelper (988b532)chore(runtime):isolate dev and release state viaAPP_SUBDIR(ec54f14)feat(config):addterminal.scrollback_linesandexternal_editorfields (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
说明直接同步自 GitHub release。