<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Arthur Jean</title>
    <description>The latest articles on DEV Community by Arthur Jean (@arthurj-dev).</description>
    <link>https://dev.to/arthurj-dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3942560%2Fee58ec3c-91e5-46ef-9d33-d26d85b1e901.png</url>
      <title>DEV Community: Arthur Jean</title>
      <link>https://dev.to/arthurj-dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/arthurj-dev"/>
    <language>en</language>
    <item>
      <title>Building a native terminal for AI coding agents in Rust + GPUI</title>
      <dc:creator>Arthur Jean</dc:creator>
      <pubDate>Wed, 20 May 2026 19:18:52 +0000</pubDate>
      <link>https://dev.to/arthurj-dev/building-a-native-terminal-for-ai-coding-agents-in-rust-gpui-2bg4</link>
      <guid>https://dev.to/arthurj-dev/building-a-native-terminal-for-ai-coding-agents-in-rust-gpui-2bg4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Author: Arthur Jean, solo indie maker. More on &lt;a href="https://paneflow.dev" rel="noopener noreferrer"&gt;paneflow.dev&lt;/a&gt; and &lt;a href="https://arthurjean.com" rel="noopener noreferrer"&gt;arthurjean.com&lt;/a&gt;.&lt;br&gt;
Repo: &lt;a href="https://github.com/ArthurDEV44/paneflow" rel="noopener noreferrer"&gt;github.com/ArthurDEV44/paneflow&lt;/a&gt;. MIT licensed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I spend my days running Claude Code, Codex, and OpenCode in parallel panes. Different agents on different branches, each with their own dev server, all in one terminal. tmux can do this, but every multiplexer I tried treated agents the same as any other shell process: no first-class branch context, no live dev-server detection, no session restore that survives a reboot, no programmatic way for an external tool to drive the editor. I built &lt;a href="https://paneflow.dev" rel="noopener noreferrer"&gt;Paneflow&lt;/a&gt; because I needed it.&lt;/p&gt;

&lt;p&gt;This is a post-mortem, not a launch post. Paneflow is a native terminal workspace, splits, panes, branch-aware workspaces, session restore, built in pure Rust on top of &lt;a href="https://zed.dev" rel="noopener noreferrer"&gt;Zed's GPUI framework&lt;/a&gt; and the upstream &lt;code&gt;alacritty_terminal&lt;/code&gt; crate. It started as a port of &lt;a href="https://github.com/manaflow-ai/cmux" rel="noopener noreferrer"&gt;cmux&lt;/a&gt;, a macOS-only Swift/AppKit project, and the Rust rewrite forced a string of decisions I had no good intuition for at the start. I want to walk through the ones that mattered: which UI frameworks I tried and rejected, how the GPUI/alacritty boundary actually looks, how dev-server detection works under the hood, the N-ary layout tree that replaced binary splits, the cross-platform PTY plumbing, the JSON-RPC control plane that makes agents first-class, and four lessons that surprised me.&lt;/p&gt;

&lt;p&gt;If you take one thing away, take this: for a terminal emulator, the UI framework must own glyph rasterization. Everything else flows from that single constraint.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frr4mowejyh8t4wmtiobn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frr4mowejyh8t4wmtiobn.png" alt="Paneflow showing a multi-workspace sidebar, an in-pane markdown viewer reading a PRD document, and the OpenCode AI agent active in the right pane" width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a terminal for AI coding agents
&lt;/h2&gt;

&lt;p&gt;VSCode and classic IDEs matter less and less when you work with Claude Code or Codex. The work has shifted: instead of typing code in an editor, I'm directing agents from a shell. One pane runs Claude Code working on a feature branch. Another runs Codex doing a refactor on a second branch. A third tails the dev server. A fourth has notes and a markdown viewer open.&lt;/p&gt;

&lt;p&gt;The terminals we know weren't designed for this. They render a grid, they pipe stdin/stdout, they let you split. They don't know which workspace is on which branch, which pane has a server bound to port 5173, which session restored from yesterday's setup. Every multiplexer treats every pane identically, so the orchestration cognitive load lands entirely on you.&lt;/p&gt;

&lt;p&gt;Paneflow is built exactly for this mode. Workspaces are git-aware: each one knows its branch and the sidebar surfaces it. Dev-server detection scans for Vite, Next.js, Webpack, and others, then resolves their actual listening ports through the kernel, not by trusting whatever the framework printed. AI agent buttons in the tab bar launch Claude Code, Codex, or OpenCode in the active pane with one keystroke. A local JSON-RPC server exposes the editor so external tools can drive workspaces, send keystrokes, or post agent lifecycle events programmatically. Sessions save and restore so closing the app and reopening it tomorrow lands you exactly where you left off.&lt;/p&gt;

&lt;p&gt;Side-by-side comparisons vs cmux, WezTerm, iTerm2, and Warp at &lt;a href="https://paneflow.dev/compare" rel="noopener noreferrer"&gt;paneflow.dev/compare&lt;/a&gt;. The rest of this post is about how the engine works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not Electron, why not Tauri
&lt;/h2&gt;

&lt;p&gt;The first option I evaluated was Electron + xterm.js. The architecture review rejected it immediately: &lt;em&gt;"Electron: heavy memory footprint (contradicts cmux's 'not Electron' philosophy)."&lt;/em&gt; cmux was conceived as a native, low-RAM tool, and the whole point of porting it was to keep that property on Linux and Windows. So Electron was out without debate.&lt;/p&gt;

&lt;p&gt;The second option was &lt;a href="https://tauri.app" rel="noopener noreferrer"&gt;Tauri&lt;/a&gt;, and I genuinely spent time there. I started a real implementation, laid out the architecture, got the first interactions running. On paper it held up: lighter binary than Electron, frontend in whichever stack you want, Rust backend for the sensitive logic, cross-platform distribution with minimal friction. But as I went deeper, two things sharpened up.&lt;/p&gt;

&lt;p&gt;First, the technical blocker. The webview API surface is too narrow for the kind of low-level keyboard, IME, and input grabbing a multiplexer needs. Everything you take for granted in a native terminal (precise key chords without a JS intermediary, clean Asian IME, focus management between panes without a round-trip to the webview) becomes a workaround or a hole. For an editor or a typical productivity app, those trade-offs are acceptable. For a terminal that has to absorb every keystroke at sub-frame latency, they are not.&lt;/p&gt;

&lt;p&gt;Second, and this is what really decided it: I wanted to build something new, not wrap a webview with one more layer on top. When &lt;a href="https://zed.dev" rel="noopener noreferrer"&gt;Zed&lt;/a&gt; started, they could have built on Electron, on Tauri, on GTK or Qt. They chose to write their own framework, &lt;a href="https://github.com/zed-industries/zed/tree/main/crates/gpui" rel="noopener noreferrer"&gt;GPUI&lt;/a&gt;, because no existing option could deliver what they wanted for code editors: dense GPU-accelerated text rendering at 120 fps with sub-frame keystroke-to-pixel latency. The result speaks for itself.&lt;/p&gt;

&lt;p&gt;Zed's bet for IDEs applies word for word to terminals. Not wrap, not lean on a portability layer that flattens every feature to the lowest common denominator, but build on a foundation designed specifically for dense, native, low-latency text rendering. The difference with Zed is that I did not have to write the framework: they had already done it. GPUI owns text rendering end-to-end: shaping, atlasing, GPU draw, the lot. There is no "bring your own text path," there is no webview to work around. Adopting GPUI meant choosing the "dedicated native framework" approach over "webview with a Rust shell." Architectural ambition over ecosystem ease. That was the decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  The GPUI mental model
&lt;/h2&gt;

&lt;p&gt;A two-paragraph briefing so the rest of this post reads cleanly. Mutable state in GPUI lives in &lt;code&gt;Entity&amp;lt;T&amp;gt;&lt;/code&gt;. You mutate through a &lt;code&gt;Context&amp;lt;T&amp;gt;&lt;/code&gt;, you signal observers with &lt;code&gt;cx.notify()&lt;/code&gt;, you spawn async work with &lt;code&gt;cx.spawn()&lt;/code&gt;. Views implement &lt;code&gt;Render&lt;/code&gt; and return a &lt;code&gt;div()&lt;/code&gt; tree built with a Tailwind-shaped builder API. Every observable thing in Paneflow (the app root, the cursor blink phase, every terminal view, every pane, the sidebar) is an &lt;code&gt;Entity&amp;lt;T&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Keyboard input flows through GPUI's &lt;code&gt;actions!&lt;/code&gt; macro, which generates zero-sized typed structs in the &lt;code&gt;paneflow&lt;/code&gt; namespace (&lt;code&gt;SplitHorizontally&lt;/code&gt;, &lt;code&gt;LayoutTiled&lt;/code&gt;, &lt;code&gt;UndoClosePane&lt;/code&gt;, ...) that the framework dispatches through the focus chain. Adding a keybinding is one struct, one handler, one bind. GPUI's repaint is diff-based and dependency-tracked at observation time, so a &lt;code&gt;cx.notify()&lt;/code&gt; only invalidates the rects that subscribed. That's the whole surface area you need to follow what comes next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plugging in &lt;code&gt;alacritty_terminal&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The terminal grid itself is &lt;code&gt;alacritty_terminal = "0.26"&lt;/code&gt; from crates.io (&lt;code&gt;src-app/Cargo.toml:26&lt;/code&gt;). I migrated off Zed's internal fork as soon as 0.26 was released. Staying on a vendored fork of someone else's editor's fork of alacritty was not a sustainable path.&lt;/p&gt;

&lt;p&gt;The shared state is unsurprising: &lt;code&gt;Arc&amp;lt;FairMutex&amp;lt;Term&amp;lt;ZedListener&amp;gt;&amp;gt;&amp;gt;&lt;/code&gt;. &lt;code&gt;ZedListener&lt;/code&gt; is a thin newtype wrapping a futures &lt;code&gt;UnboundedSender&amp;lt;AlacEvent&amp;gt;&lt;/code&gt;. alacritty's &lt;code&gt;Term&lt;/code&gt; calls &lt;code&gt;listener.send_event(event)&lt;/code&gt; whenever the grid mutates; the receiver lives on the GPUI main thread.&lt;/p&gt;

&lt;p&gt;What is surprising is that Paneflow does &lt;strong&gt;not&lt;/strong&gt; use alacritty's &lt;code&gt;EventLoop::spawn()&lt;/code&gt;. That helper is convenient for embedded-terminal-in-an-editor cases, but a multiplexer wants finer control over OSC scanning, synchronized output, and shutdown. Instead, every terminal session runs two hand-rolled detached threads (&lt;code&gt;src-app/src/terminal/pty_loops.rs&lt;/code&gt;):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;pty_reader_loop&lt;/code&gt; reads PTY bytes into a 4096-byte buffer, runs OSC scanners (OSC 7 for CWD, OSC 133 for prompt boundaries, XTVersion for shell identification), advances the VTE &lt;code&gt;Processor&lt;/code&gt; against the locked &lt;code&gt;Term&lt;/code&gt;, and emits a single wakeup when bytes were processed outside a DEC 2026 synchronized-output window.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pty_message_loop&lt;/code&gt; receives &lt;code&gt;Msg&lt;/code&gt; from a &lt;code&gt;PtyNotifier&lt;/code&gt; over &lt;code&gt;std::sync::mpsc&lt;/code&gt; and writes input or resize commands back to the PTY master.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The reader's hot path is roughly this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;term&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;term&lt;/span&gt;&lt;span class="nf"&gt;.lock&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="nf"&gt;.advance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;term&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nf"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;term&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="nf"&gt;.sync_bytes_count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;listener&lt;/span&gt;&lt;span class="nf"&gt;.send_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;AlacEvent&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Wakeup&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The check on &lt;code&gt;sync_bytes_count()&lt;/code&gt; is the synchronized-output coalesce: a TUI like neovim or btop announces "I'm about to draw a frame, hold the wakeup until I'm done," and we honor it. Without that check, a busy TUI would generate hundreds of wakeups per logical frame and the GPU would render them all.&lt;/p&gt;

&lt;p&gt;On the GPUI side, the &lt;code&gt;TerminalView&lt;/code&gt;'s &lt;code&gt;cx.spawn&lt;/code&gt; event loop drains the wakeup channel for up to 4 ms (max 100 events), then issues one &lt;code&gt;cx.update()&lt;/code&gt; and one &lt;code&gt;cx.notify()&lt;/code&gt;. That's the keystroke-to-pixel path: shell writes, reader thread, VTE, wakeup, 4 ms batcher, &lt;code&gt;cx.notify()&lt;/code&gt;, GPUI diff repaint, atlas draw. There's a &lt;code&gt;PANEFLOW_LATENCY_PROBE=1&lt;/code&gt; env var on debug builds that traces this path end-to-end, which is the right tool when you suspect the batcher is doing something wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detecting dev servers from &lt;code&gt;/proc/net/tcp&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;One of the things I wanted from the start was a sidebar that knows what's running in each workspace. If a pane is hosting a Vite dev server on port 5173, I want a one-click open. If a Next.js app is on 3000, same. The naive solution is to regex the terminal output: catch "vite dev: &lt;a href="http://localhost:5173" rel="noopener noreferrer"&gt;http://localhost:5173&lt;/a&gt;" when the framework prints it. That works half the time. The other half, the line scrolls off, or the framework prints to stderr in a way the OSC scanner missed, or the user piped it through &lt;code&gt;tee&lt;/code&gt; and the announcement never came back.&lt;/p&gt;

&lt;p&gt;So Paneflow does both. &lt;code&gt;src-app/src/terminal/service_detector.rs&lt;/code&gt; regex-matches a list of 22 framework signatures against the terminal output as a fast, immediate signal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FRAMEWORKS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"next.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Next.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"turbopack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Next.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"vite"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Vite"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"nuxt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Nuxt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"remix"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Remix"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"astro"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Astro"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"webpack-dev-server"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Webpack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"uvicorn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"uvicorn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"flask"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Flask"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"axum"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Axum"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The third tuple element is &lt;code&gt;is_frontend&lt;/code&gt;: frontend frameworks get a clickable URL in the sidebar; backend ones get a status badge.&lt;/p&gt;

&lt;p&gt;The ground truth, though, comes from the kernel. &lt;code&gt;src-app/src/workspace/ports.rs&lt;/code&gt; walks the PID tree of every workspace and queries listening sockets directly. On Linux, that means parsing &lt;code&gt;/proc/net/tcp&lt;/code&gt; and &lt;code&gt;/proc/net/tcp6&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[cfg(target_os&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"linux"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;detect_ports&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u16&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;all_pids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;HashSet&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pids&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;descendant&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;collect_descendant_pids&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;all_pids&lt;/span&gt;&lt;span class="nf"&gt;.insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;descendant&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;owned_inodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;collect_socket_inodes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;all_pids&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;ports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"/proc/net/tcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/proc/net/tcp6"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;read_capped&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="nf"&gt;.lines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.skip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="nf"&gt;.split_whitespace&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;"0A"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// 0A = TCP_LISTEN&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;port_hex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nf"&gt;.split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;':'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.next_back&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;u16&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_str_radix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;port_hex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="py"&gt;.parse&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;owned_inodes&lt;/span&gt;&lt;span class="nf"&gt;.contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;inode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;ports&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;ports&lt;/span&gt;&lt;span class="nf"&gt;.sort_unstable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;ports&lt;/span&gt;&lt;span class="nf"&gt;.dedup&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;ports&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The walk is in two steps. First, &lt;code&gt;collect_descendant_pids&lt;/code&gt; BFS-walks &lt;code&gt;/proc/{pid}/task/{pid}/children&lt;/code&gt; to gather every descendant of the workspace's shell PID. Then &lt;code&gt;collect_socket_inodes&lt;/code&gt; reads &lt;code&gt;/proc/{pid}/fd/&lt;/code&gt; for each PID, looking for symlinks of the shape &lt;code&gt;socket:[&amp;lt;inode&amp;gt;]&lt;/code&gt;, and accumulates the inode set. Finally, &lt;code&gt;/proc/net/tcp&lt;/code&gt; is parsed line-by-line; column 3 is the TCP state (hex &lt;code&gt;0A&lt;/code&gt; is &lt;code&gt;TCP_LISTEN&lt;/code&gt;), columns 1 and 9 are the local address and socket inode. We keep only ports whose inode is owned by our PID set, so we don't surface the SSH listener on port 22 just because someone happens to have a shell open.&lt;/p&gt;

&lt;p&gt;macOS doesn't have &lt;code&gt;/proc&lt;/code&gt;, so the branch there uses &lt;code&gt;libproc&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[cfg(target_os&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"macos"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;detect_ports&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u16&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;libproc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;libproc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;file_info&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;ListFDs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ProcFDType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pidfdinfo&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;libproc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;libproc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;net_info&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;SocketFDInfo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SocketInfoKind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TcpSIState&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;libproc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;libproc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;proc_pid&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;listpidinfo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// ... for each pid in the workspace tree:&lt;/span&gt;
    &lt;span class="c1"&gt;//   listpidinfo::&amp;lt;ListFDs&amp;gt;(pid) to get the FD table&lt;/span&gt;
    &lt;span class="c1"&gt;//   filter to ProcFDType::Socket&lt;/span&gt;
    &lt;span class="c1"&gt;//   pidfdinfo::&amp;lt;SocketFDInfo&amp;gt;(pid, fd) to get the socket info&lt;/span&gt;
    &lt;span class="c1"&gt;//   keep entries where soi_kind == IPv4 and tcpsi_state == LISTEN&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Windows is currently a stub: &lt;code&gt;detect_ports&lt;/code&gt; returns an empty &lt;code&gt;Vec&lt;/code&gt; and the sidebar falls back to the regex path. The native Win32 API is &lt;code&gt;GetExtendedTcpTable&lt;/code&gt;, which is on the roadmap.&lt;/p&gt;

&lt;p&gt;The combination matters. Regex catches the immediate "Vite started" line within the second; &lt;code&gt;/proc&lt;/code&gt; catches anything actually listening even when the announcement was lost. Both paths feed into the same sidebar entity via GPUI's &lt;code&gt;EventEmitter&lt;/code&gt;, which fires &lt;code&gt;ActivityBurst&lt;/code&gt; events on PTY activity rather than polling (see lesson #3 below).&lt;/p&gt;

&lt;h2&gt;
  
  
  The N-ary layout tree
&lt;/h2&gt;

&lt;p&gt;My first attempt at panes used a binary &lt;code&gt;SplitNode&lt;/code&gt; enum: &lt;code&gt;Leaf | Split { direction, ratio, first, second }&lt;/code&gt;. It worked. It also produced terrible UX the moment you wanted three equal columns. You had to nest a split inside a split, and a 50/50 split-of-a-50/50 is a 25/75, not 33/33. Every preset became a special case. Every drag-resize on the outer divider produced visually inconsistent inner sizes.&lt;/p&gt;

&lt;p&gt;I rewrote the tree as an N-ary structure. The relevant types live in &lt;code&gt;src-app/src/layout/tree.rs:42-58&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;LayoutChild&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LayoutTree&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;ratio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Rc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Cell&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;computed_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Rc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Cell&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;LayoutTree&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;Leaf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Entity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Pane&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;Container&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SplitDirection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LayoutChild&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;drag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Rc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Cell&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DragState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;container_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Rc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Cell&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things to call out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Vec&amp;lt;LayoutChild&amp;gt;&lt;/code&gt;, not &lt;code&gt;(Box&amp;lt;Self&amp;gt;, Box&amp;lt;Self&amp;gt;)&lt;/code&gt;.&lt;/strong&gt; A container holds any number of children. Three columns is one container with three children, not a binary tree of containers. Drag-resizing across siblings becomes a single ratio rebalance, not a recursive walk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Rc&amp;lt;Cell&amp;lt;f32&amp;gt;&amp;gt;&lt;/code&gt; for ratios.&lt;/strong&gt; GPUI's render tree is single-threaded, and the layout body is rebuilt on every &lt;code&gt;Render&lt;/code&gt; call. Putting ratios behind &lt;code&gt;Rc&amp;lt;Cell&amp;lt;f32&amp;gt;&amp;gt;&lt;/code&gt; lets the render closure read the ratio while the drag handler writes it, with no &lt;code&gt;Arc&lt;/code&gt; and no lock. This is one of the patterns I underestimated coming in: GPUI's "no &lt;code&gt;Arc&amp;lt;Mutex&amp;lt;...&amp;gt;&amp;gt;&lt;/code&gt; for UI state" rule pushes you toward &lt;code&gt;Rc&amp;lt;Cell&amp;lt;...&amp;gt;&amp;gt;&lt;/code&gt; and &lt;code&gt;Rc&amp;lt;RefCell&amp;lt;...&amp;gt;&amp;gt;&lt;/code&gt; everywhere, and that's correct.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Constants are deliberately small.&lt;/strong&gt; &lt;code&gt;MIN_PANE_SIZE = 80.0&lt;/code&gt; (&lt;code&gt;tree.rs:62&lt;/code&gt;), &lt;code&gt;DIVIDER_PX = 4.0&lt;/code&gt; (&lt;code&gt;tree.rs:60&lt;/code&gt;), max 32 panes per workspace, max 20 workspaces. The clamp on resize is dynamic, &lt;code&gt;MIN_PANE_SIZE / container_size&lt;/code&gt; at drag time, not a fixed range.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four presets ship today: &lt;code&gt;even_h&lt;/code&gt; and &lt;code&gt;even_v&lt;/code&gt; (built from the same &lt;code&gt;from_panes_equal&lt;/code&gt; constructor), &lt;code&gt;main_vertical&lt;/code&gt; (60% left, 40% stacked right), and &lt;code&gt;tiled&lt;/code&gt; (the tmux algorithm: increment rows then cols alternately until &lt;code&gt;rows*cols &amp;gt;= N&lt;/code&gt;, then fill row-by-row). Rendering emits GPUI flex divs with &lt;code&gt;flex_basis(relative(ratio))&lt;/code&gt; per child, and the divider is a 4 px element with a drag listener that rewrites two adjacent ratios in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-platform PTY via &lt;code&gt;portable-pty&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Paneflow uses &lt;a href="https://crates.io/crates/portable-pty" rel="noopener noreferrer"&gt;&lt;code&gt;portable-pty = "0.8"&lt;/code&gt;&lt;/a&gt; (&lt;code&gt;src-app/Cargo.toml:29&lt;/code&gt;). Its &lt;code&gt;native_pty_system()&lt;/code&gt; resolves to ConPTY on Windows and openpty on Unix with no caller-side conditional compilation. The &lt;code&gt;PtyBackend&lt;/code&gt; trait in &lt;code&gt;src-app/src/pty.rs&lt;/code&gt; has exactly one production implementation, &lt;code&gt;PortablePtyBackend&lt;/code&gt;, and the call site has zero &lt;code&gt;#[cfg]&lt;/code&gt; guards. The two I/O thread loops in &lt;code&gt;pty_loops.rs&lt;/code&gt; are pure &lt;code&gt;std::io::Read&lt;/code&gt;/&lt;code&gt;Write&lt;/code&gt; over the boxed handle.&lt;/p&gt;

&lt;p&gt;Where platform-specific code does appear, it's at exactly two seams:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Process shutdown.&lt;/strong&gt; Unix uses &lt;code&gt;libc::kill(pid, SIGTERM)&lt;/code&gt; with a grace window before &lt;code&gt;SIGKILL&lt;/code&gt;. Windows uses &lt;code&gt;TerminateProcess&lt;/code&gt; and &lt;code&gt;WaitForSingleObject&lt;/code&gt; from &lt;code&gt;windows-sys&lt;/code&gt;. Both branches are &lt;code&gt;#[cfg]&lt;/code&gt;-guarded in &lt;code&gt;pty_session.rs&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CWD detection.&lt;/strong&gt; Linux reads &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/cwd&lt;/code&gt;. macOS calls &lt;code&gt;proc_pidinfo&lt;/code&gt;. The Windows branch returns &lt;code&gt;None&lt;/code&gt; for now and falls back to OSC 7 emission from the shell prompt. Three small &lt;code&gt;#[cfg(target_os = "...")]&lt;/code&gt; blocks; nothing leaks into the rest of the codebase.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the entire surface area of "this code knows what OS it's running on", fewer than 100 lines across the whole app. Everything else compiles unchanged on Linux, macOS, and Windows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agent orchestration via JSON-RPC
&lt;/h2&gt;

&lt;p&gt;This is the part the rest of the article was building toward. Paneflow exposes a JSON-RPC 2.0 server on a local socket so external tools, scripts, and AI agents can drive the editor programmatically. Workspace navigation, sending text to panes, splitting surfaces, and a six-method &lt;code&gt;ai.*&lt;/code&gt; namespace that lets agents publish their lifecycle events back to the UI: session_start, prompt_submit, tool_use, notification, stop, session_end.&lt;/p&gt;

&lt;p&gt;Transport: the &lt;a href="https://crates.io/crates/interprocess" rel="noopener noreferrer"&gt;&lt;code&gt;interprocess&lt;/code&gt;&lt;/a&gt; crate's &lt;code&gt;local_socket&lt;/code&gt; module. On Unix it's a Unix domain socket at &lt;code&gt;$XDG_RUNTIME_DIR/paneflow/paneflow.sock&lt;/code&gt; (with &lt;code&gt;$TMPDIR&lt;/code&gt; fallback on macOS); on Windows it's a named pipe at &lt;code&gt;\\.\pipe\paneflow&lt;/code&gt;. Same wire protocol (newline-delimited JSON-RPC 2.0), same Rust call sites, zero &lt;code&gt;#[cfg]&lt;/code&gt; at the dispatch level. The socket is &lt;strong&gt;strictly local&lt;/strong&gt;: no network surface, no port binding. Trust derives from filesystem mode &lt;code&gt;0600&lt;/code&gt; set immediately after &lt;code&gt;bind()&lt;/code&gt;, plus &lt;code&gt;getsockopt(SO_PEERCRED)&lt;/code&gt; on Linux and &lt;code&gt;LOCAL_PEERCRED&lt;/code&gt; on macOS: every accepted connection checks the peer's UID against the server's before any method dispatches. A mismatch returns JSON-RPC &lt;code&gt;-32001 permission denied&lt;/code&gt; and closes the stream.&lt;/p&gt;

&lt;p&gt;Architecturally, methods fall in two buckets. Stateless methods (&lt;code&gt;system.ping&lt;/code&gt;, &lt;code&gt;system.capabilities&lt;/code&gt;, &lt;code&gt;system.identify&lt;/code&gt;) reply directly on the socket thread:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="nf"&gt;.as_str&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"system.ping"&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nd"&gt;json!&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s"&gt;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"pong"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="s"&gt;"system.identify"&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nd"&gt;json!&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s"&gt;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Paneflow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nd"&gt;env!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CARGO_PKG_VERSION"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s"&gt;"protocol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"jsonrpc-2.0"&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;dispatch_to_gpui&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;request_tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stateful methods (&lt;code&gt;workspace.*&lt;/code&gt;, &lt;code&gt;surface.*&lt;/code&gt;, &lt;code&gt;ai.*&lt;/code&gt;) need the GPUI main thread because that's where all mutable state lives. The socket thread cannot touch &lt;code&gt;Entity&amp;lt;T&amp;gt;&lt;/code&gt; directly. So &lt;code&gt;dispatch_to_gpui&lt;/code&gt; wraps the request in an &lt;code&gt;IpcRequest&lt;/code&gt; with a one-shot response channel, sends it across an &lt;code&gt;mpsc&lt;/code&gt; queue, and blocks on the reply with a 5-second timeout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;dispatch_to_gpui&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;request_tx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nn"&gt;mpsc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Sender&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IpcRequest&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp_tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resp_rx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;mpsc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;ipc_req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;IpcRequest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;response_tx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;resp_tx&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;request_tx&lt;/span&gt;&lt;span class="nf"&gt;.send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ipc_req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.is_err&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nd"&gt;json!&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s"&gt;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;32000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"App shutting down"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;resp_rx&lt;/span&gt;&lt;span class="nf"&gt;.recv_timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_secs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;promote_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nd"&gt;json!&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s"&gt;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;32000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Timeout"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the GPUI side, &lt;code&gt;process_ipc_requests&lt;/code&gt; (in &lt;code&gt;src-app/src/app/ipc_handler.rs&lt;/code&gt;) drains the receiver each tick, dispatches by method name, and sends the result back through the per-request response channel. Stateful handlers can return a structured &lt;code&gt;JsonRpcError&lt;/code&gt; via a &lt;code&gt;_jsonrpc_error&lt;/code&gt; sentinel value that &lt;code&gt;promote_response&lt;/code&gt; rewrites into a proper JSON-RPC &lt;code&gt;error&lt;/code&gt; envelope at the boundary, so handlers stay synchronous and don't have to construct envelopes themselves.&lt;/p&gt;

&lt;p&gt;What this buys, concretely: a Claude Code session can announce itself with &lt;code&gt;ai.session_start&lt;/code&gt; and the sidebar marks the pane as agent-active. A shell script can push commands into the active surface with &lt;code&gt;surface.send_text&lt;/code&gt;. A test harness can spin up a workspace, run a battery of agent calls, and tear it down. The protocol is bytes on a socket; the security model is filesystem mode + peer credentials; the dispatch is a &lt;code&gt;mpsc::channel&lt;/code&gt; between two threads.&lt;/p&gt;

&lt;p&gt;Here's the smallest useful client you can write today, just &lt;code&gt;socat&lt;/code&gt; and a JSON line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc":"2.0","method":"surface.send_text","params":{"text":"ls\n"},"id":1}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | socat - UNIX-CONNECT:&lt;span class="nv"&gt;$XDG_RUNTIME_DIR&lt;/span&gt;/paneflow/paneflow.sock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full method surface is documented in the &lt;a href="https://github.com/ArthurDEV44/paneflow#ipc" rel="noopener noreferrer"&gt;README&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four things I got wrong
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. I over-invested in Tauri.&lt;/strong&gt; I pushed a Tauri implementation pretty far before admitting that the webview API surface could not carry a multiplexer's constraints (low-level input, IME, inter-pane focus). The right time to pivot was about two weeks before the moment I actually pivoted. Lesson: when a framework asks you to work around its own abstraction for two basic features, you're building against the framework, not with it. Pivot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Block-character rendering gaps.&lt;/strong&gt; Paneflow had a visible gap between adjacent block characters (U+2580 to U+259F) when rendered at certain font sizes. I spent six fix attempts on it: integer cell dimensions, origin rounding, shader edge math, shared-boundary subpixel adjustments. Every fix was plausible. None of them worked. The actual bug was that my block-character coverage table was incomplete: eleven codepoints (U+2594, U+2596 to U+259F) were missing, so they fell through to the default font path which has slight metric differences. The fix was extending the codepoint table, found by binary-scanning the binary of a TUI that triggered the bug. Lesson: when GPU quad math looks correct on every probe but the visual artifact persists, the problem is probably which codepoints you're emitting, not how.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Polling timers defeat &lt;code&gt;cx.notify()&lt;/code&gt;.&lt;/strong&gt; An early version of the sidebar polled for port-scan results every 500 ms and CWD changes every 2 s. That produced six to eight unnecessary repaints per second when nothing was happening. The fix was a full migration to GPUI's &lt;code&gt;EventEmitter&lt;/code&gt;/&lt;code&gt;cx.subscribe&lt;/code&gt;/&lt;code&gt;cx.emit&lt;/code&gt; push model, with custom events like &lt;code&gt;ActivityBurst&lt;/code&gt; and &lt;code&gt;CwdChanged&lt;/code&gt; emitted from the PTY reader. Idle repaints went to zero. GPUI's diff repaint is cheap, but it isn't free, and a &lt;code&gt;cx.notify()&lt;/code&gt; in a &lt;code&gt;setInterval&lt;/code&gt;-shaped loop is exactly the pattern the framework was designed to replace.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Session persistence is table-stakes.&lt;/strong&gt; I shipped the first internal build without session save/restore because the layout code was already complex and I wanted to land the v1 commit. The first thing I did with the build was open four workspaces, rebuild the binary, and lose all four. Every multiplexer user has tmux in their muscle memory. Restart-survives-state isn't a feature, it's the price of admission. Save it to &lt;code&gt;~/.cache/paneflow/session.json&lt;/code&gt; on &lt;code&gt;CloseWindow&lt;/code&gt;, restore on startup, scope it into v1.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The repo is &lt;a href="https://github.com/ArthurDEV44/paneflow" rel="noopener noreferrer"&gt;github.com/ArthurDEV44/paneflow&lt;/a&gt;, MIT licensed. Linux (Wayland and X11) and macOS (Apple Silicon) ship today as signed and notarized builds; native Windows is in flight, the code is largely ready, the signing infrastructure is being wired up. Prebuilt artifacts are on the &lt;a href="https://paneflow.dev/download" rel="noopener noreferrer"&gt;download page&lt;/a&gt;; honest side-by-side comparisons vs cmux, WezTerm, iTerm2, and Warp at &lt;a href="https://paneflow.dev/compare" rel="noopener noreferrer"&gt;paneflow.dev/compare&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Issues, suggestions, and "your N-ary tree should really be a Z-tree" arguments are all welcome on the tracker. I'm especially curious about what's missing for your workflow versus the multiplexer you use today; that's the feedback that shapes the next release.&lt;/p&gt;

&lt;p&gt;If this was useful, the engineering work I'm most proud of is in the &lt;code&gt;terminal/element/&lt;/code&gt; module, that's where glyph shaping, atlas blits, and the APCA adjustment pipeline all live, and it didn't fit in this post. I'll write that one up next.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>terminal</category>
      <category>programming</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
