Skip to content

feat: first-class herdr multiplexer support — native splits + classic attach (WIP) - #69

Draft
salemsayed wants to merge 42 commits into
jossephus:mainfrom
salemsayed:feat/herdr-support
Draft

feat: first-class herdr multiplexer support — native splits + classic attach (WIP)#69
salemsayed wants to merge 42 commits into
jossephus:mainfrom
salemsayed:feat/herdr-support

Conversation

@salemsayed

@salemsayed salemsayed commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Status: WIP / RFC. This is feature-complete enough to use daily but still under active on-device testing. Opening as a draft to get your eyes on the direction and the herdr abstraction before I polish. Happy to split it into smaller topical PRs if you'd prefer to land it in stages — see the commit ladder below, each commit is independently green.

What this adds

First-class support for herdr — a terminal multiplexer built for orchestrating coding agents (workspaces → tabs → panes, each pane running an agent with an idle / working / blocked / done status). This wires herdr into chuchu the same way iTerm2 integrates tmux with tmux -CC: chuchu can either take over herdr's UI and render it natively, or attach to herdr's own TUI over a plain SSH session.

herdr is driven entirely through its JSON CLI (herdr api snapshot, herdr terminal session control|observe <pane>, focus/create/close verbs), so there's no wrapper service and no herdr-specific network protocol — it's the same MultiplexerType abstraction the tmux/zmx support already uses, extended with a runtime control plane.

Two modes (per-host toggle)

Native splits (default-on, hidden setting). chuchu hides herdr's TUI entirely and becomes the multiplexer chrome:

  • herdr's tabs become chuchu's tab strip; a switcher home lists every workspace with per-status agent counts, each expandable in place (caret) to jump straight to a specific tab or pane.
  • Each pane is streamed via herdr terminal session control|observe <pane> --cols N --rows M, which emits an ndjson terminal.frame stream (server re-renders ANSI at the exact geometry chuchu asks for). Each pane is fed into its own native ghostty VT and laid out in a native split that mirrors herdr's own layout geometry (layouts[] from the snapshot, in cell units).
  • Input/resize/scroll are routed to the focused pane over the terminal.input / terminal.resize / terminal.scroll uplink. Tap a pane to focus; tap the focused pane to raise the keyboard.
  • A "needs you" section surfaces agents that are done-and-unseen. This is grounded in herdr's attention semantics: done = finished + unseen (wants your attention), idle = already seen. Focusing a pane flips done → idle server-side, so the triage list stays honest.
  • Create/destroy from the UI. Because native mode routes keystrokes to the focused pane's PTY, herdr's own keybindings never reach the server — so pane/tab/workspace lifecycle is surfaced explicitly and dispatched via herdr's CLI verbs (pane split --direction right|down, pane close, tab create/close, workspace create/close). A compact + menu on the tab strip (split right / split down / close pane / new tab) acts on the focused pane/tab; long-press a tab to close it; the switcher home has + new and a per workspace. Destructive closes go through a confirmation since they terminate the running agent.
  • Transport is one shared SSH connection carrying N exec channels (added to the Zig/libssh2 bridge), so switching panes/workspaces doesn't spin up a new connection each time.

Classic attach (toggle off). Exactly today's behavior — a normal SSH session attached to herdr's own TUI, with chuchu's terminal rendering it. No native takeover, no extra streams. This is the fallback for anything the native path doesn't cover yet (e.g. inline images, which aren't in the frame stream).

Screenshots

Switcher home + "needs you" triage (native mode)

herdr switcher home

Native splits — two panes rendered side-by-side, focused pane has the accent border, layout mirrors herdr's own geometry:

native split — package list
native split — dev server

How it's built (commit ladder)

Each commit is independently green (./gradlew testDebugUnitTest + compileDebugKotlin); the native path stays inert until the "render herdr tabs as native splits" commit flips it on:

  1. parse herdr layout geometry in snapshot — kotlinx.serialization models + fixtures + parser tests
  2. add herdr terminal frame codec — ndjson framing, seq/restart policy, terminal.input/resize/scroll encoders (pure + tested)
  3. stream herdr panes into native terminalsHerdrPaneHost (one VT + one channel per pane) + engine stream reconciliation
  4. route input to the focused herdr pane — focus state + sink branching
  5. render herdr tabs as native splitsHerdrSplitLayout + the flip
  6. add per-host herdr native splits toggleHostProfile.herdrNativeMode + Room v11→v12 AutoMigration + AddServer switch
  7. multi-exec-channel SSH bridge, switcher home, needs-you triage, workspace create, and a series of on-device fixes (pane geometry sync, scroll direction, tap-to-focus, connection labels, crash-hardening)

Alignment with the repo's conventions

  • Multiplexer-agnostic — reuses the existing MultiplexerType / MultiplexerRegistry abstraction; no wrapper services, no herdr daemon.
  • kotlinx.serialization throughout (no org.json).
  • Thin ViewModels — control-plane logic lives in the engine/repository; the VM combines flows and forwards intents.
  • Hidden, default-on setting — the native toggle only appears once herdr is selected as the multiplexer.
  • Battery-conscious — pane streams are opened lazily (only for the visible tab's panes), disposed on background, and rebuilt on foreground; polling cadence is tuned and cancellable.
  • Tests — 8 new unit-test suites (framing, seq policy, encoders, snapshot/layout parsing, stream policy, shared-connection, notifier).

Testing status

  • ✅ Native splits mirror the desktop layout; tap-to-focus + typing reach the right pane; rotation re-streams at the new geometry; two-finger scroll drives server scrollback.
  • ✅ Switcher home, "needs you" triage (verified against a live agent going done), and workspace-create verified on-device.
  • ✅ Pane lifecycle verified on-device: + pane → splits (new pane, focus follows), ✕ pane → confirm → pane closes and the layout reflows, with the neighbouring pane's session surviving.
  • ✅ Multi-channel transport verified (stable connection count, no per-switch connection spikes).
  • ✅ Classic attach + non-herdr hosts (tmux/zmx/plain) regression-checked.
  • 🚧 Still shaking out: takeover-vs-observe edge cases when the desktop TUI holds control, VT mode replay (bracketed paste / DECCKM arrows), and pane renaming/reordering. Mouse forwarding to herdr's own UI is deliberately out of this PR — the explicit lifecycle controls above cover the create/destroy gap that native takeover would otherwise leave.

Tested on an Oppo Pad Mini against herdr 0.7.4 (protocol 16) over SSH.

How to try it

  1. Add/edit a host, pick herdr as the multiplexer. "Native splits (beta)" is on by default; toggle it off for classic attach.
  2. Requires a herdr session already running on the host (native mode does not launch one for you yet — the classic toggle is the fallback).
  3. Connect → you land on the switcher home; tap a workspace to open its tabs as native splits.

Would love feedback on the herdr abstraction and whether you'd rather see this as one PR or staged. 🤖 Generated with Claude Code

salemsayed and others added 30 commits July 20, 2026 00:06
Adds herdr (https://herdr.dev) as a runtime-supported multiplexer type
alongside tmux and zmx, following the generic Multiplexer abstraction:
availability detection, JSON session listing, attach-or-create launch,
and attach-only guard with graceful shell fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
Serializable models for the herdr api snapshot document (workspaces,
tabs, panes with agent status) with a lenient agent-status decoder that
maps unknown values to Unknown for protocol forward-compatibility.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
Streams herdr api snapshots over a dedicated client-paced SSH exec
channel (one connection, newline-per-poll), with idle backoff, poke on
mutation, exponential retry, and teardown on disconnect/reconnect.
Adds focus/create/close control verbs via the existing exec command
envelope. Also replaces Thread.sleep with cancellable delay in
readExecOutput.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
Renders herdr workspaces, tabs, and panes with agent-status badges,
tap-to-focus, tab create/close, and refresh, sourced from the engine
snapshot stream. ViewModel is a thin pass-through of engine state and
verbs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
Posts throttled notifications when a herdr agent transitions to blocked
or done while the app is backgrounded, with tap-to-focus of the source
tab. Polling cadence now follows app visibility: 3s active tab, 15s
inactive, 25s background, 60s when notifications are off. Toggle lives
in terminal settings, default on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
When the active session is a herdr multiplexer, the always-visible strip
renders the focused workspace's herdr tabs with agent-status dots and
tap-to-focus, a host chip opening the tab manager, and a plus button
that creates a herdr tab. Falls back to the session strip otherwise.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
Adds layouts[] models (tab area, per-pane rects in terminal cells,
focused pane, zoom flag) needed to render herdr tabs as native splits.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
Codec for the herdr terminal stream plane: terminal.frame/closed
parsing, ndjson framing, input/resize/scroll encoders, seq-gap
disposition, and the control/observe stream command builder.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
Adds HerdrPaneHost, which drives a per-pane herdr terminal.frame stream
into a dedicated ghostty VT: control/observe modes with automatic
read-only fallback when another client holds control, frame-header
authoritative resize, seq-gap restart, and input/scroll uplink. The
engine owns a map of pane hosts with ensure/retain/focus/takeover/stop
lifecycle and a pure desiredHerdrPaneStreams reconciliation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
Repository tracks the focused herdr pane and routes key/text/paste/scroll/
focus to its pane host (suppressing mouse and full-canvas resize in native
mode), reconciles visible pane streams from the layout, and follows
server-side focus changes. Engine flattens pane-host state into
herdrPaneStates; ViewModel exposes herdrNativeState + pane tap/viewport/
scroll/takeover handlers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
Adds HerdrSplitLayout, which scales herdr's pane geometry onto the
screen and renders one native terminal per pane (focused pane
interactive, others tap-to-focus), with connecting/error placeholders,
zoom, and a read-only takeover chip. TerminalScreen shows it in place
of the single canvas when native mode is active; herdr native launch
runs a keepalive channel instead of the TUI so the splits own the
screen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
Persists a per-host herdrNativeMode flag (default on, Room v12
migration) that gates native splits vs classic TUI attach, threaded
through TabSpec/ConnectionParams and exposed as a "native splits (beta)"
switch in Add Server when herdr is selected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
Pane streams were only created by the canvas onResize, but the canvas
only mounts once a snapshot exists, which needs a running stream — a
deadlock that left every pane stuck on "connecting…". Reconciliation now
creates desired-but-missing streams with a bootstrap geometry; the canvas
refines it once it mounts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
Focus changes previously reconnected each pane (control<->observe swap =
fresh SSH handshake), making switching slow. Panes are distinct herdr
terminals that can each hold control concurrently, so every visible pane
now opens and holds its own control stream; focus is pure input routing.
Switching between panes is instant. Read-only fallback and takeover for
panes another client controls are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
The persistent connection tab now renders a native herdr home listing
workspaces and agents with live status; tapping a workspace switches to
it and tapping an agent jumps into its pane as native splits. Adds the
agent snapshot model, a workspace-focus verb, a home-visible state that
resets on connect, and a home chip in the tab strip. This gives
workspace switching and makes the connection tab the herdr navigator.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
Pane VTs are created after the app's color scheme is set, so they fell
back to ghostty's dark default while earlier panes rendered light. Apply
the engine's pending color scheme and default colors to each pane host on
creation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
Adds an additive channel table (up to 16 exec channels) on the native
SSH session plus JNI to open, exchange (read/write), query EOF, and
close individual exec channels, so one connection can carry many
simultaneous streams. Existing single-channel shell/exec/pty/SFTP paths
are unchanged (writeChannel/readChannel now take an explicit channel;
legacy callers pass the single channel). Teardown frees all table
channels.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
Introduces SharedSshConnection: one authenticated SSH session per active
herdr session, with pane streams and control verbs multiplexed as
separate exec channels over it instead of a fresh SSH handshake each. All
channel ops are serialized on a dedicated per-connection dispatcher
(libssh2 is not thread-safe), and connection setup is guarded by a mutex
so concurrent pane starts share one connection. Pane retries reopen a
channel rather than reconnecting. Collapses per-pane and per-verb
handshakes (~0.8s each over a high-latency link) into cheap channel opens.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
Split panes used fitSnapshotToCanvas=true (the command-palette preview
mode), which scales the grid to fit and letterboxes when the grid aspect
differs from the pane rect. Interactive panes should render at native
cell size like the main terminal (false); each pane's grid is already
resized to fill its rect via onResize, so it fills without bars.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
A pane's real-size onResize often fires while its stream channel is still
connecting, so the resize command was dropped and the pane stayed at the
bootstrap 80x24 forever (content overflowed narrow split rects). After a
channel opens, re-send a terminal.resize with the latest known geometry
so herdr renders at the pane's actual size.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
Panes were sized independently by width, so rounding accumulated and the
last pane extended past the screen edge, clipping its content. Compute
each pane's box from its scaled left/right and top/bottom edges instead,
so adjacent panes share exact borders and the rightmost/bottom pane ends
exactly at the container edge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
Scroll delta mapped to the reversed herdr scroll direction; flip it so
dragging scrolls content the natural way. Tapping a pane always raised
the soft keyboard, making focus switching in a split annoying; now
tapping a non-focused pane only focuses it, and tapping the already
focused pane raises the keyboard for typing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
Native herdr previously ran the tmux/zmx-style multiplexer session-picker
preflight on connect, which showed an empty "no terminal sessions" state
until an SSH round-trip resolved a session (or the user tapped "new
connection"). Native mode always attaches to the default session, so open
the tab directly and skip the preflight. Also force the tab strip when the
active tab is native herdr, since the split UI and herdr tab navigation
depend on it and break under the palette tab interface.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
Native herdr mode replaced the cross-server tab strip with herdr's own
tabs, leaving no easy way to reach other server connections. Add a
"connections" section at the top of the herdr home listing every open
connection tab (tap to switch) plus a "server list" entry, so the home is
the single place to move between servers and herdr workspaces.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
The connections list and session hint showed chuchu's random per-tab
alias (e.g. "amber-signal"), which didn't match the server names shown in
the server list and tab endpoints. Show the server name (tabLabel)
instead, falling back to the alias only as a secondary hint when more
than one connection targets the same host.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
The workspace header showed herdr's single aggregate agent_status, which
read "working" even when only one of several agents was working and
"unknown" for workspaces with no agents. Compute the summary from the
workspace's actual agents instead: a per-status count (e.g. "2 working
1 idle"), and nothing at all when a workspace has no agents.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
Per herdr's status semantics, blocked (waiting for input) and done
(finished, result unseen) are the states that require the user, while
idle/working are passive. Surface them at the top of the home: a "needs
you" list of blocked/done agents across the session, blocked first, tap
to jump straight to the pane. Hidden when nothing needs attention. This
turns the home into a triage screen and complements the existing
blocked/done notifications.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
- Pluralize tab/pane counts ("1 tab · 1 pane").
- Collapse workspaces to a header + status summary by default; auto-expand
  the focused workspace and any with a blocked/done agent, so the home
  stays scannable while surfacing what matters.
- Drop the agent-row cwd when it just repeats the workspace name, and
  strip the repeated workspace prefix/suffix from agent titles.
- Add spacing between workspace groups.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
Tapping a herdr agent/tab/workspace runs a focus verb over the shared SSH
connection. If that connection had gone stale (e.g. the phone's IP
changed), openExecChannel threw "Channel open timed out" which propagated
uncaught to the main thread and crashed the app. Catch failures in the
control path: surface them as an action error, and drop the stale shared
connection so it reconnects on the next command. Also guard the ViewModel
launch so no control-verb exception can reach the main thread.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
Thread an optional herdr session name through the snapshot stream, pane
terminal streams, and control verbs, emitting `herdr --session <name>`
when the configured session is not the default. Behavior is unchanged for
the default session (no flag emitted); this is groundwork for a per-host
herdr session selector.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
salemsayed and others added 3 commits July 20, 2026 15:42
Add a "+ new" action to the home's workspaces header that prompts for an
optional name and creates a focused herdr workspace (herdr workspace
create --focus [--label]). Previously workspaces could only be created
from the herdr TUI/CLI on the host; the app could enter and switch
workspaces but not add one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
The connect flow only bypassed the tmux/zmx-style session-picker preflight
for native herdr; classic herdr (native splits off) still ran it and
showed the empty "no terminal sessions" state until the user manually
tapped "new connection". herdr is single-primary-session in both modes, so
any herdr host now attaches to the default session directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
The strip's herdr-tab mode was gated on "is a herdr host" rather than
native mode, so in classic mode (native splits off) chuchu's tab strip
mirrored herdr's tabs — duplicating the tab bar herdr already draws inside
its own TUI. Gate it on native mode so classic mode shows normal chuchu
connection tabs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9a58336b-86d1-4be2-af65-43568df18971

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

salemsayed and others added 2 commits July 20, 2026 18:51
Native mode routes keystrokes to the focused pane's PTY, so herdr's own
keybindings for creating/destroying panes never reach the server. Add the
missing CLI control-plane verbs (pane split --direction right|down, pane
close, workspace close) alongside the existing focus/create-tab verbs so
these can be driven from native chuchu UI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
Native mode routes keystrokes to the focused pane's PTY, so herdr's own
keybindings for creating/destroying panes never reach the server. Surface
these actions explicitly instead:

- A compact '+' menu on the tab strip (split right, split down, close pane,
  new tab) acting on the focused pane/tab.
- Long-press a herdr tab to close it.
- A close (✕) on each workspace row in the switcher home.

Destructive closes go through a confirmation dialog since they terminate any
running agent. Built with the app's own Popup + ChuText rows (no material3,
which the module doesn't depend on).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
@salemsayed
salemsayed force-pushed the feat/herdr-support branch from 262c769 to 765c563 Compare July 20, 2026 16:35
salemsayed and others added 3 commits July 20, 2026 21:02
Newly opened pane streams used a hardcoded 80x24 bootstrap until the canvas
measured and resized, so switching to a workspace whose tab holds one pane
flashed a full-screen TUI (e.g. a coding agent) at the wrong size before the
resize round-trip landed. Remember the last real geometry a native canvas
reported and reuse it when a single new pane opens (a lone pane fills the
whole area, so it is exact); splits keep the safe 80x24 fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
Workspace expansion was tied to server focus, so the only way to reveal a
workspace's tabs/panes was to enter it (navigating away) and come back. Add a
caret toggle that expands a workspace in place: agent-bearing tabs list their
panes (tap to jump to that pane) and agent-less tabs show a tab row (tap to
focus that tab). Auto-expand for focused/attention workspaces is preserved as
the default when the user hasn't toggled.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
The per-pane rectangular border framed every pane in a heavy box that
doubled up at shared edges. Drop borders entirely: separate panes with a
faint hairline gutter (2+ panes only) and indicate focus by dimming the
inactive panes (iTerm2-style) so the active pane stays full brightness. A
single pane keeps its clean full-bleed rendering — no border, gutter, or dim.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxaFcjTqr7Pny76bzzGFdD
@salemsayed
salemsayed force-pushed the feat/herdr-support branch from c65193c to 65601cd Compare July 20, 2026 18:41
Wire onFontSizeChange into HerdrSplitLayout so pinch-to-zoom works in
focused herdr native panes (was a dead onZoom = {}), and include the
stepped-zoom canvas rework: anchored whole-sp steps with haptic ticks,
pointerInput(Unit) + rememberUpdatedState holders so committed steps no
longer restart the gesture mid-pinch, and the absolute onFontSizeChange
API with minFontSizeSp/maxFontSizeSp bounds.

The canvas/call-site portion duplicates upstream PR jossephus#70; once that merges,
rebase drops it here and keeps only the herdr wiring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01634UryFkSv94cq7S4WD6vq
@jossephus

Copy link
Copy Markdown
Owner

Hey @salemsayed, keep making updates on the PR. Although i doubt we will land this to main, i can see how useful it can be.

a while ago, i was working on plugins to chuchu, i got distracted by other stuff but the ultimate goal was to enable customizations like this. Let me see if I can push on the plugin implementation this week, and i will be using this PR as the demo.

also loving herdr so far

@salemsayed

Copy link
Copy Markdown
Contributor Author

Hey @salemsayed, keep making updates on the PR. Although i doubt we will land this to main, i can see how useful it can be.

a while ago, i was working on plugins to chuchu, i got distracted by other stuff but the ultimate goal was to enable customizations like this. Let me see if I can push on the plugin implementation this week, and i will be using this PR as the demo.

also loving herdr so far

Oh a plugin is exactly the correct way for this, I also agree that it it's too far to land on main but I've been using it for a couple of days and I think it's really beneficial for working with herdr and especially on a small phone as the herdr tui is hard to manage on a phone when you've adjusted it on your big screen computer.

salemsayed and others added 3 commits July 20, 2026 22:51
The leading chip in native mode was a static "⌂ herdr", so there was no
way to tell which workspace you were in without opening the switcher.
Label it with the focused workspace instead — label, falling back to
"ws <number>", then "herdr" when the snapshot has no match. Tap behavior
(open switcher home) unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01634UryFkSv94cq7S4WD6vq
…ic focus

Tab switching in native mode took ~1.2s measured on-device: ~185ms for the
herdr focus CLI exec, ~250ms for the next control snapshot poll, and ~790ms
re-opening pane streams, because pane hosts were disposed the instant a tab
lost focus.

Two changes, benchmarked at each step:

- Warm pane streams: desiredHerdrPaneStreams keeps the panes of up to two
  recently focused tabs alive alongside the focused tab's (12-stream cap,
  zoomed recent tabs contribute only their focused pane). The repository
  tracks per-connection recency; everything still drops when the app is
  backgrounded. Switch-back cost fell from ~1.2s to ~375ms, with content
  already live on arrival.

- Optimistic focus: tapping a tab immediately swaps the layout, strip
  highlight, pane focus, and stream policy to the target using the layout
  geometry already present in the snapshot, then confirms via the focus
  command in the background. Failure or a 4s reconciliation timeout snaps
  back to snapshot truth; switching to another chuchu connection clears the
  override so it can never bleed across sessions. Warm switches now land in
  the same frame as the tap (~0ms perceived); cold tabs start their streams
  at tap time behind an instant layout swap.

Adds policy unit coverage for recency, caps, override, and background
gating in HerdrPaneStreamPolicyTest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01634UryFkSv94cq7S4WD6vq
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants