Changed
-
Upload larger bug-repro videos to agents
Dropped files can now be up to 50 MB, up from 10 MB. That leaves room for a useful app screen recording while keeping the current whole-file upload path bounded. The same limit applies to every allowed file type and to pasted clipboard images, with matching checks in the browser and padi. -
The dock now shows every terminal by default
The activity-window filter used to default to 24h, so on a fresh surface any terminal you hadn’t touched in a day was already tucked behind aN hidden · show all. It now defaults to All — nothing is hidden until you narrow the window. Time-based decay (4h / 12h / 24h / 48h) is still one click away and still persists per host, but the default is to show what you have rather than hide it. -
Host tabs redesigned — each machine wears its own colour
The host strip got a visual pass. Each host now carries its own identity hue — a soft accent on its tab and a faint wash across the canvas floor, so switching machines re-colours the whole workspace and you always feel where you are. The active tab lifts out of a now-neutral, borderless header (colour lives on the tabs, not a same-hue band that swallowed them), the Padi/Kaval marks quiet to grey glyphs at rest and bloom on the active host, and the local host reads as a house glyph + “local” — a role, not a hostname you might mistake for a real machine. The canvas grid is now soft dots instead of lines. Legible in both light and dark. -
Add a remote host anytime — no KOLU_PADI_HOST needed
Connecting another machine no longer requires settingKOLU_PADI_HOSTfirst — the + in the host strip is always there. Clicking it opens a short notice that remote hosts is an alpha feature (with a link to the docs) and an ssh-target box; add a host and the canvas jumps to it.KOLU_PADI_HOSTstill works as an optional way to pre-seed hosts at launch. Your local machine is still the always-present first chip. -
The right panel now follows the terminal
Whether the right panel is showing — and which tab it’s on — is now remembered per terminal, not one global switch. Collapse it on a build-log tile and it stays collapsed there; leave it open on a branch diff and switching back restores it exactly. It rides session restore, so each terminal’s panel state survives a reload. New terminals still open with the panel showing by default; the panel width and the Code-tab tree/content split stay global (viewer taste, tuned once and left put). -
Host-first chrome: Padi and Kaval on every host chip
The chrome bar no longer leads with three co-equal version pills. Hosts are primary nav — each host chip carries that host’s Padi and Kaval marks (icon + connection dot). Clicking Kolu / Padi / Kaval opens a dropdown under the icon (not a center modal), with the host name on Padi/Kaval panels so ownership is obvious. Steady versions and detail live in the tip and the panel. The strip now reads and behaves like a real browser tab bar — the active host is a soft accent-outlined tab, inactive hosts are clean outlined tabs, and it stays legible in both light and dark mode. (A later pass in this same release — the host-tabs redesign above — moved host colour onto the tabs themselves and left the header neutral.) The bar is a full-width top rail now, spanning over the inspector like maximized mode, with Kolu/Padi/Kaval marks aligned on one row. A kaval that’s a build behind shows an amber update pip at a glance again (not just in the tooltip).
Added
-
Switching hosts is instant, and scrolling up loads real history
Switching to a host — or reloading the page — now paints the terminal instantly. Before, every cross-host switch replayed the terminal’s whole 10,000-line history up front before you could type; now only the recent screenful is sent, and the prompt is live immediately. Then scrolling up just works: as you near the top, kolu quietly fetches the next older chunk from the host and splices it into the terminal’s own scrollback — the same buffer, scrollbar, and select/copy/search, no separate pager. The view never jumps while it fills; the only tell is the scrollbar thumb shrinking. The backfilled history is real terminal content — colors, wrapped lines, wide characters all survive — up to the full 10,000 lines the host holds. -
kaval-tui history — dump a terminal's older scrollback
kaval-tui history <id>prints the scrollback above the current screen — the older outputsnapshot --viewportcan’t show — VT-serialized so colors survive, cursor-paged from the newest older lines back.--lines Nprints just the one page immediately above the screen; omit it for the full history, oldest-first. Read-only, takes no TTY — an agent-orchestration or incident-forensics read that no longer needs a report-file workaround. -
The Kaval and Padi dialogs now show each daemon's lifetime
Each daemon’s info dialog gained a lifetime row beside its build/socket/memory. A durable production padi/kaval readsforever; a daemon a test or smoke run spawned readsbound to run pid N— the boundToPid lifetime that makes it die with the run that started it (see the leak fix below). It’s a small diagnostic window: in normal use it just saysforever, but it makes a stray test/dev daemon — or one bound to a run — legible at a glance instead of invisible. Each daemon publishes its own lifetime on the wire (kaval on itssystem.versionhandshake, padi on its identity), so the row is the daemon’s honest self-report, not a guess. -
The hosts you add now survive a restart
A remote host you add from the selector strip is now remembered across restarts. Before, a kolu restart forgot everything but your local machine and you re-added your fleet by hand; now every remembered host reappears in the strip and reconnects on its own through the normal connect pipeline — and a host that has since gone away comes back as a failed chip with the reason, never silently dropped. The only way a host leaves the strip is your explicit remove. Membership lives beside the server that owns it (not in the browser), so every device you open sees the same fleet.KOLU_PADI_HOSTstill works for seeding hosts declaratively at launch; you just no longer need it to make a strip-added host stick. -
An agent on another host that needs you now reaches you — badge, notification, one-click focus
When you’re parked on one host, an agent that flips to awaiting you on a different host used to be invisible unless you were already looking at that host’s chip. Now the needs-you signal rolls up to where it can reach you: the app-icon badge counts awaiting agents across every connected host (not just the one on screen), an OS notification fires for a background host, and clicking it switches to that host and focuses the exact terminal — one action. The host-strip chips already showed per-host counts; this adds the reach that escapes the app. A host whose link drops dims its chip rather than taking anything down. Nothing new crosses the wire — it reads the same tiny per-host “awaiting” projection the chips do. -
Every terminal knows its own id — $KAVAL_TERMINAL_ID
An agent running inside a kolu terminal can now name itself. Every terminal kolu (orkaval-tui create) spawns exports$KAVAL_TERMINAL_ID— its own terminal id — the self-knowledge twin of$KAVAL_SOCKET(which names the daemon that owns it). Together they let a process read its own screen or drive its own tile with zero guessing:kaval-tui snapshot "$KAVAL_TERMINAL_ID" --socket "$KAVAL_SOCKET". It re-owns nested terminals correctly — a kolu spawned inside a kolu terminal stamps its own id over the inherited one, so each terminal always sees the right value — and it’s set for every shell (even ones kolu doesn’t wrap) and for remote (--host) terminals too. -
padi-tui --host — drive a padi on another machine
padi-tui --host <ssh>reaches a padi on a remote machine over ssh — the exact twin ofkaval-tui --host. It provisions padi’s workspace daemon onto the host with Nix, runspadi --stdioover the link, and runs every verb unchanged against the remote:padi-tui status --host nix@prodsnapshots that machine’s terminals,watchfollows them live,waitreads real agent state off the remote padi, andcreatespawns a real terminal on the host — which survives the ssh link, because padi’s--stdiomode fronts the durable daemon. Read verbs are safe;createlands a live terminal (that’s the point); a tui is a dial, so it never drains, converges, or recycles the remote padi. padi runs as your ssh user (the socket dir is owner-only), a remotecreateopens in the host’s home, and a remote--worktreetakes an explicit--repo <host path>.--hostis mutually exclusive with--socket/--state-root, and needspadi-tuirun from its Nix wrapper (nix run .#padi-tui) so the per-arch derivation is baked in. -
Switch machines from the keyboard — ⌘⇧H host switcher
A host switcher, mirroring the ⌘⇧K workspace switcher: press ⌘⇧H (Ctrl+Shift+H elsewhere) — or click the search icon now in the host bar — to open a fuzzy picker over your hosts, with the active one marked. Pick one and the whole canvas switches to that machine, instant and no-reload. It appears once you have more than the local host. -
See, switch, and add hosts from a phone — the host row on mobile
The touch layout had no host chrome at all: the desktop host strip is hidden on a phone, so you couldn’t see which machine you were on, switch hosts, or add one — even though the multi-host engine underneath already worked everywhere. Now the pull-down chrome sheet carries a host row: a chip per host with the same connection dot, identity colour, and amber “needs you” pill you get on desktop; a tap to switch (instant, no reload); and a + that opens a full-width add-host section right in the sheet (the desktop popover would clip at phone width). The chips are comfortable 44px touch targets, and the row scrolls sideways when your fleet outgrows the screen. Same engine, same vocabulary — just a surface a thumb can reach. -
Connecting to a fresh remote host now tells you what it's doing
The first time kolu connects to a new remote host (KOLU_PADI_HOST), it compiles and copies its workspace daemon onto that machine over ssh with Nix — a build that can take minutes. The canvas used to show one mute word, “Connecting…”, indistinguishable from a hang. Now the connect surface tells the truth: it names the actual phase — “Provisioning kolu onto<host>…” while it ships the recipe, then “Building on<host>… this can take a few minutes” during the compile — streams a live tail of the real build output, and shows an elapsed timer, so a long-but-progressing first connect never reads as stuck. A warm reconnect (the closure’s already there) connects fast, with no build UI at all; a genuine failure still shows the real reason on the existing host-down card, and an unreachable host reads differently from a rejected one. No fake progress bar — a Nix build has no honest percentage, so the reassurance is the named phase, the real output, and the clock. -
Export a Grok transcript and show its context-window usage
Grok sessions now export the same HTML transcript as Claude / Codex / OpenCode — the conversation is read fromchat_history.jsonlunder~/.grok/sessions/…(user turns, reasoning, assistant replies, tool calls and results). The tile inspector also shows context-window tokens from Grok’ssignals.json(contextTokensUsed), so the running counter is no longer stuck blank for Grok alone. -
Grok Build is a first-class agent — tile chrome, dock state, and resume
Rungrok(xAI’s Grok Build coding agent) in any kolu terminal and it now gets the same treatment as Claude Code, Codex, and OpenCode: a Grok icon and live state on the tile chrome (thinking · tools · waiting · awaiting you), dock ranking that matches the other agents, and sleep/restore that resumes withgrok -corgrok --resume <uuid>. Detection reads Grok’s on-disk session files under~/.grok— no config step — and the worktree agent picker remembers a normalizedgroklaunch the same way it does for the other three. When Grok opensask_user_question(multiple-choice / freeform), the dock and chrome show awaiting you, not a spinning “Running tools” ring. -
Switch which machine's terminals you're viewing — live, from the chrome bar
The in-app host switcher #1675 promised (“no UI yet… the in-app host switcher coming next”) is here. SetKOLU_PADI_HOSTto a comma-separated list of ssh hosts and kolu keeps a warm pool of their workspace daemons — padi, one per host, each provisioned over ssh with Nix — and a selector strip appears in the chrome bar: one chip per host, each showing its connection health and a live awaiting-you count read straight off that machine. Click a chip and the whole canvas switches to that machine — its terminals, Code tab, paste/upload/transcripts — with no reload, the switch a single instant signal write (the old host’s subscriptions drop, the new host’s populate). Your local machine is always the first, unremovable chip. Add or remove a host at runtime right from the strip (removing the local default is refused — the canvas always keeps a machine to fall back to). Each browser tab remembers its own active host, so two tabs can watch two machines side by side. (A later pass in this same release made the strip — with its always-present + add affordance — visible even withKOLU_PADI_HOSTunset, so a local-only kolu shows your local chip and the + rather than nothing; see the ungated-remote-host entry above.) -
Draft a prompt in the Inspector and send it to the terminal
The right-panel Inspector now opens with a Compose box — a multiline textarea over a Send button — so you can write out a prompt for the agent, editing it in peace, before it ever reaches the terminal. Hit Send (or ⌘/Ctrl+Enter) and the draft is written straight into the active terminal’s input line — the in-app twin ofkaval-tui send. It inserts, it doesn’t submit: the text lands in the agent’s input box for you to review and press Enter yourself, dodging the same bracketed-paste racekaval-tui sendis careful about. A multiline draft is sent as one bracketed paste so it arrives as a single block, not line-by-line. Each terminal keeps its own draft, saved in your browser, so a half-written prompt survives switching tiles or reloading the page — pick up exactly where you left off. -
kaval-tui send --file <path> — pass a big prompt without shell mangling
Handing a large brief to an agent as"$(cat brief.md)"let the shell execute any backticks or$(...)in the file beforekaval-tuiever saw them (a payload with backticked identifiers got partly run).kaval-tui send <id> --file <path>reads the payload straight from the file — byte-exact, no shell in the loop. It’s mutually exclusive with positional text and piped stdin, and it doesn’t change what goes down the wire (still a bracketed paste) — it fixes the shell hazard, not the wire bytes. -
Preview a file on the machine you're connected to
Clicking a file in the Code tab while kolu was pointed at another machine (KOLU_PADI_HOST) used to show “file preview is unavailable while bound to a remote padi” — a deliberate refusal, so kolu would never accidentally serve the wrong machine’s bytes (the terminal’s files live on the remote disk; the preview route was reading the local one). Now it just works: kolu fetches the file’s bytes from the machine you’re actually connected to, so text, images, video, PDFs — the whole binary preview — render against the remote host’s disk exactly as they do locally. Large files stay cheap: the bytes come back in bounded chunks rather than one giant blob, so a multi-GB video streams without ballooning memory, and a<video>still seeks (byte ranges are honoured over the hop). Local mode is byte-for-byte unchanged — no extra round trip when the terminal is on this machine. -
PDFs open inside the Code tab
Click a.pdfin the Code tab and kolu now opens it with the browser’s native PDF viewer, so pages, zoom, and downloads work in-pane instead of showing a broken sandbox preview. -
The 'Running daemons' list now shows the machine you're actually using
The Kaval and Padi rail dialogs each list the running daemons on your host — the leak diagnostic that spots a straypadi/kavalbefore it wastes memory. When kolu is pointed at another machine over ssh (KOLU_PADI_HOST), that list now shows the connected machine’s daemons, not just the box kolu-server runs on — so a leaked daemon on the machine you’re actually using is finally visible. padi reports its own host’s daemons over the same re-served surface, so the list works identically whether you’re local or remote. Bound remotely, the dialog shows two labeled groups — the bound host’s daemons, and a separate “this machine, not the bound host” group for kolu-server’s own box; bound locally it’s the single list it always was (no duplicate). A daemon the scan couldn’t reach reads an honest “—”, never a silently missing row. -
Point kolu at another machine's terminals over ssh (opt-in, no UI yet)
SetKOLU_PADI_HOST=<ssh host>and kolu-server binds a remote host’s workspace daemon — padi — over ssh instead of the local one, and the whole canvas becomes that machine: terminals spawn there, the Code tab reads its repos, paste/upload/transcripts land there — byte-identical to a local canvas, just a different box. kolu provisions padi (and the kaval it owns — one Nix closure) onto the host over ssh, so nothing need be installed there first; the remote daemon is durable, so its terminals survive the ssh link dropping and a kolu-server restart (it re-adopts the running daemon, sessions intact). The remote padi runs as your ssh user and owns its own state on that host — the ssh identity is the daemon owner. Off by default, no UI yet: it’s one all-or-nothing knob, with the in-app host switcher coming next (W3.2). (Needs passwordless ssh and your user trusted by the remote’s Nix daemon — the same reachkaval-tui --hostrides.) -
padi-tui — drive an agent by its real state
padi-tui is kolu’s new terminal-side CLI over padi, the per-host workspace daemon — kaval-tui’s sibling, and pulam-tui’s replacement.padi-tui statusprints a one-shot snapshot of every terminal (record state · repo·branch · PR · agent · foreground);padi-tui watch [<id>]follows the workspace live until Ctrl+C, marking a●on any terminal moving bytes right now;padi-tui wait <id> --until <buckets>blocks until that terminal’s agent reaches a coarseworking/awaiting/waitingstate, then exits — the done-signal for one agent driving another, with--timeoutfailing loud (exit 2), a terminal that exits first exiting 3, and--jsonprinting{ id, agent }. Andpadi-tui createspawns a terminal, a split tile (--parent <id>), or a worktree’d agent (--worktree <branch>) that appears live on the canvas. Inside a kolu terminal$PADI_SOCKETmakes every verb flag-less; elsewhere it autodiscovers the running padi (--socket/--state-rootto point it). -
Every terminal kolu opens now knows its own daemon — $KAVAL_SOCKET
Driving your sibling terminals from an agent running inside a kolu terminal meant first hunting for the daemon’s socket — scanning/tmp, or guessing a port-namespaced path that is simply unknowable on macOS (where$XDG_RUNTIME_DIRis unset). Every terminal a kolu (or standalonekaval) daemon spawns now carries$KAVAL_SOCKET— the socket of the daemon that owns it — exactly the waytmuxsets$TMUXandscreensets$STY. So inside any kolu terminal,echo "$KAVAL_SOCKET"names your daemon andkaval-tui list --socket "$KAVAL_SOCKET"drives its siblings, with zero guessing — and it disambiguates precisely when several daemons are up, which autodiscovery can only report as “many”. The value re-owns nested daemons correctly (a daemon spawned inside a terminal stamps its own socket over the inherited one); a remote (--host) terminal leaves it unset, since the remote daemon’s socket path isn’t the local CLI’s to know. The/koluskill now names$KAVAL_SOCKETas the first thing to reach for, and — the bug that started this — its documented socket paths and thekaval-tui --help/ connection-error hints no longer point at akolu/-namespaced path that was wrong on every platform (kolu-server namespaces its daemon per listen port,kaval-<port>/, and on macOS the old path collapsed to a broken/kolu/…). -
Hide sleeping terminals in the dock, in a unified Filters row
The dock’s footer now presents a Filters row with two matched chips: the existing activity window (which hides stale terminals) and a new ☾ sleeping chip (which hides the ones you deliberately put to sleep). The two are independent filters over the same list — the window filters by staleness, the ☾ chip by dormancy — so you can dismiss a pile of parked-for-later sleeping shells and keep the dock to just what’s awake, without touching your activity-window choice. Both chips share the same look and the same “accent when it’s actively hiding” cue, so it’s obvious they’re two of a kind; a singleN hidden · show allat the end reports what both filters are hiding together and clears them in one click. The ☾ chip appears only when there’s a sleeping terminal to act on, and stays reachable to bring them back even when every row is a hidden sleeper. Your choices persist across reloads, per device. -
Block until a terminal goes quiet with kaval-tui wait — no shell hooks
Driving an agent that drives another agent, the missing piece was a reliable “its turn ended” signal for a rawkaval-tui create’d terminal:pulam-tui waitneeds hooked terminals, and pollingsnapshotfor the screen to settle is brittle and laggy.kaval-tui wait <id> --until idle:<ms>now blocks on the terminal’s raw output stream — the same bytes the daemon serves toattach/snapshot— and returns the instant no output has arrived for<ms>(the turn ended / it’s awaiting you). It’s agent-agnostic and needs no shell hooks: the same command works forclaude/codex/grok/opencode, with no busy-word list to maintain and no poll-tick lag.--until match:'<regex>'waits for new output to match a completion marker or returned-prompt sentinel instead;--timeout <ms>fails loud (exit 2) so a wedged agent can’t hang the loop, and a terminal that exits first fails loud too (exit 3 — the agent you were driving died).--jsonprints one result frame per outcome —{ id, result, … }, whereresultismet/timeout/gone/interrupted/closed(ametframe addsfired—idle/match—,elapsedMs, andmatchedLineon a match) — and it works over--socketand--hostlike every other verb. The kolu skill’screate → send → snapshotloop now waits withkaval-tui wait --until idlebetween submit and read, keeping the screen-settle poll only as a fallback for a daemon too old to have the subcommand. -
New terminals inherit the size of the one you're working in
Every new terminal opened on the canvas appeared at the same fixed default size, so if you’d widened a terminal to suit your work — a tall log, a side-by-side diff — each new one came up small and you resized it by hand, again and again. A new tile now opens at the size of the terminal you’re currently in: resize one to taste and the next inherits its width and height. It reads the active tile specifically — the one you last focused — not whichever happened to be created last, and it picks up a size you’ve just dragged even before the change has finished saving. The first terminal, with nothing to inherit from, still opens at the default. Only the size carries over — where a new tile lands on the canvas is unchanged. -
Read just the visible screen with kaval-tui snapshot --viewport
kaval-tui snapshot <id>printed a terminal’s whole scrollback — thousands of lines on a long-running or compacted agent session — sosnapshot | tail -8handed you the bottom of the buffer (often just trailing blanks), not the live screen, and the screen-settle done-signal for driving an agent diffed huge noisy dumps. Two flags now bound it:--viewportprints just the visible screen — the terminal’s last screenful, resolved against the daemon’s own grid so it’s correct no matter how tall your shell is (over--hostthe remote terminal is a different size, and your stdout is usually a pipe) — and--tail N(alias--lines N) the last N rendered lines.--viewportis the right read for “what’s on screen now” when one agent drives another; the bare default still dumps the full scrollback forgrep. The kolu skill’screate → send → snapshotloop now reads with--viewportinstead of\| tail. -
Copy a terminal's ID for kaval-tui attach
Grabbing a terminal withkaval-tui attach <id>meant first hunting down its id — the Inspector’s Attach row only handed you the whole command, or you rankaval-tui list. Now the bare id is one click or one command away. The command palette has a Copy terminal ID action (active-terminal section) that copies the focused terminal’s id straight to your clipboard; and the right-panel Inspector’s Attach row gains a copy id button beside its attach/snapshot commands, for the tile’s main terminal and each split. Both copy the focused / that pane’s full id — each split is its own PTY with its own id — ready to paste afterkaval-tui attach(or into any script). The palette action is live-terminals only, exactly like Copy terminal text: a sleeping tile has no PTY to attach to. -
Send input to a terminal from your shell with kaval-tui send
kaval-tuicouldlist,create,snapshot,attach, andkillyour terminals, but never type into one without taking it over.kaval-tui send <id> [text]now writes input straight to a terminal the daemon owns — a prompt to a Claude Code / Codex / opencode agent running in it, so one agent can drive another. It sends exactly the text (and any--keys) you pass — no implicit Enter; submitting is its own explicit step,kaval-tui send <id> --key Enter(a separate write that lands after the text settles, dodging a race against Claude Code’s bracketed-paste input). A multiline prompt — or one piped on stdin (cat task.md | kaval-tui send <id>) — is sent as a single bracketed paste so it lands as one block instead of submitting line-by-line;--keyalso sends control keys (--key Escapeto interrupt a running agent,C-c, the arrows).--jsonreports{ id, bytes, paste, keys }. It’s the write half of driving an agent from a script:createa terminal running an agent,sendit a prompt,send --key Enter, then read the reply withsnapshot. -
Block until a terminal's agent finishes its turn with pulam-tui wait
pulam-tuicouldstatus-snapshot andwatchagent awareness; nowpulam-tui wait <id> --until awaiting,waitingblocks until that terminal’s AI agent reaches a state, then exits — the moment it stops working and hands control back to you. It reads the very same agent-awarenesspulamalready serves (the coarseworking/awaiting/waitingbuckets), so a script can prompt an agent withkaval-tui send, wait for its turn to end, then read the reply — one agent driving another, no polling.--timeout <ms>makes a wedged agent fail loud (exit 2) rather than hang the loop forever;--jsonprints{ id, agent }. Together withkaval-tui sendand a new kolu skill that teaches thecreate → send → snapshotloop, it turns “an agent orchestrating other agents in kolu terminals” into a plain shell workflow —waitsharpening the done-signal once you drive hooked terminals. -
A legend on the fleet dashboard explains the status indicators
pulam-web (the browser agent dashboard) packs a lot into each row’s little status indicator — a shape for the agent’s state, a green ring when it’s producing output, an amber badge when a notification has fired — and nothing on screen said what they meant. A Legend now sits at the bottom of the board, spelling out each one: the state shapes (working, needs you, idle, sleeping), the green live ring, and the amber unread badge. It’s drawn from the very same component the rows use, so it always matches exactly what you’re looking at. -
Export agent sessions as a chat log or full transcript
The command palette’s Export agent session as HTML action now asks what kind of document to make: a lightweight chat log that keeps only theHuman/AIconversation, a full transcript that keeps tools, results, reasoning, edits, and subtasks collapsed, or both files. Both outputs share the same high-contrast design: a patterned page, hard role colours, visible borders, dark code blocks, and tiny ↑/↓ controls for jumping between human prompts. The chat log stays small because it omits hidden tool payloads instead of carrying them behind a toggle. -
Install the pulam fleet dashboard as an app
pulam-web — the browser view of every agent across your fleet — is now an installable app, just like kolu. Add it to your home screen or dock and it opens in its own window, chromeless, with its own teal pulam icon (a> pulam/ புலம் mark, sibling to kaval’s). It carries the same freshness guarantee as before — a reload always lands on the current build. Leave it pinned on a second monitor and a glance still tells you who’s waiting; now it’s one tap away. -
Watch every agent across your fleet, sorted by what needs you
pulam-web grew from a flat terminal list into an agent dashboard: every agent across every configured host, sorted by what needs you. A blocked agent — one awaiting your input — floats to the top with a breathing alert strip; working agents spin, idle ones sit dim. Each row shows the agent, itsrepo · branch, how long it’s been in that state, and a green activity dot when it’s moving bytes right now. Terminals not running an agent are hidden by default — footer toggles fold in idle agents, non-agent terminals, and sleeping shells. Leave it open on a second monitor and a glance tells you who’s waiting. -
The Inspector's Attach row now covers splits and adds a snapshot command
The right-panel Inspector’s Attach section showed akaval-tui attach …command for the tile’s main terminal only. It now lists a command pair for every terminal in the tile — the main pane and each split — and adds a snapshot command beside each attach, so dumping a terminal’s scrollback (kaval-tui snapshot …) is now one click away, just like taking it over. Each split is its own PTY in the daemon with its own id, so each gets its own pair, labelled Main / Split 1 / Split 2 … once a tile is split; an unsplit tile stays a single, unlabelled pair. The section shows only while the terminal is live — a sleeping tile released its PTY, so it’s no longer one of kaval’s terminals and the commands disappear rather than handing you one that can’t connect. Same click-to-copy, hover-for-the-full-id behaviour as before. -
See each fleet terminal's live git status — and drill into it
pulam-tui fleetshowed each remote terminal’srepo·branch, PR, and agent state, but never whether the working tree was dirty. Each row now carries a live working-tree cell — a changed-file count and the branch’s ahead/behind vs its upstream — that repaints the instant a file changes anywhere in the fleet. Select a row with ↑/↓ and press Enter to drill in: a panel opens the fullgit statusfor that repo — thestaged · modified · untrackedsummary and the list of changed files with their status codes — and Esc closes it. It rides the same change-pulse thepulamdaemon already serves (a repo change re-queriesgit status), so it stays live without polling; the daemon and the rest of kolu stay on Node, the viewer Bun, exactly as before. -
Read a footnote without leaving your place in the Markdown preview
The Code tab’s Markdown preview renders GitHub-style footnotes — a[1]marker linking to a definitions list at the very bottom — and reading one meant scrolling all the way down and back. Now a footnote marker is a button: click or tap it and its definition opens in a small popover anchored right under the marker, where your eye already is. Dismiss it by clicking anywhere else, pressing Escape, scrolling the document, or clicking the marker again; a see all ↓ link drops to the bottom list when you want the whole set. The bottom “Footnotes” section is unchanged — the popover reads its content from there, so it’s an additional way in, not a replacement — and a footnote body keeps its links, code, and images, each opening exactly the way it does in the document. Pointer-only for now: keyboard activation and a screen-reader relationship are a tracked follow-up, and the bottom list stays the accessible record. -
Double-click the empty canvas to open a new terminal
The bare canvas surface is now itself an affordance for creating a terminal: double-click any empty spot on the canvas and the command palette opens pre-drilled into the New terminal group — the same menu the dock’s+button reaches, so you can pick a repo, name a worktree, or start in the current directory without first finding the button or remembering Cmd/Ctrl+T. A double-click that lands on a tile (its title bar still maximizes) or the minimap is left alone — only the empty surface spawns a terminal. -
See which pane has focus in a split terminal
Split a tile into two stacked terminals and, until now, nothing showed which one your keystrokes were going to — both panes looked equally live. The pane that does not have focus now recedes (dims slightly) so the focused one reads as the foreground, and the cue follows focus as you click between them. It shows only in the tile you’re working in and only while the split is open; a collapsed split has just one terminal, so there’s nothing to distinguish. The active sub-tab already had its own highlight in the tab bar — this adds the coarser main vs. sub signal that was missing. -
Live memory usage in the chrome bar
The identity rail at the top of the window now shows a live memory figure beside each of the three processes it already names — the kolu-server (its resident set size), this browser (its JS heap), and the kaval terminal daemon (its resident set size). Each number sits under its own column (srv … MB │ client … MB │ kaval … MB) and refreshes as memory moves, so you can spot a process creeping upward at a glance without opening Diagnostic Info. The kaval figure shows only while its daemon is alive; the browser figure appears on Chromium-based browsers (Firefox and Safari don’t expose it). -
Remote terminals show a live activity dot on the fleet board
pulam-tui fleetshowed each remote terminal’s branch, PR, and agent state — but not whether it was moving bytes right now. The green “live output” dot kolu’s dock has always had was missing for remote machines, because only the host sees its own terminals’ raw output. Thepulamdaemon now serves that liveness as a stream, so a terminal churning output anywhere in the fleet lights a soft green dot the instant bytes flow and goes dark a second after it quiets — the same at-a-glance “working vs idle” cue the local dock gives, now across every machine. It’s a different signal from the agent-state colour: that tracks an AI agent thinking/waiting, while this lights for any terminal moving bytes, agent or not. -
pulam-tui fleet — a live, multi-host agent board
pulam-tuishowed one machine’s terminals as a point-in-time snapshot.pulam-tui fleetturns it into a live board across machines:pulam-tui fleet --host nix@a --host nix@bfans the dial over every host (your local pulam included unless--no-local), mirrors each one’s awareness, and updates in place as agents change state — no re-run. Every agent that’s awaiting you floats to the top across the whole fleet, and a breathing amber strip names the hosts that need you, so a glance from across the room tells you who’s blocked on you, and where. Terminals are grouped per host, each header carrying its count and an honest badge when a box is unreachable or running a version-skew build (a dead host shows as a distinct row, never silently vanishing while the others keep updating).--by needsflattens the fleet into one urgency list (“who’s waiting on input, anywhere”),--by agentgroups by state;--jsondumps a flat[{ host, terminalId, … }]for scripting (e.g. a notifier). Hosts can also come from your~/.ssh/configwith--ssh-config, and--kaval <host>=<socket>pins which kaval a host’s pulam reads when several run there (otherwise that host showsunreachableuntil you say which). The board fills the terminal width — therepo·branchcolumn grows to show long branches in full on a wide screen instead of clipping. It rides the same Nix-over-ssh provisioning aspulam-tui --host, viewer-only Bun, thepulamdaemon stays Node. (Superseded by the Heads-up below:pulam-tui fleetis gone — the multi-host board is now the browser app pulam-web.) -
Agent run-time on the tile title bar, with a start-time tooltip
The Running for duration kolu added to the Inspector now also rides the tile title bar, right beside the agent’s context-token count — so you can see how long a session has been going without opening the right panel, across every agent tile at a glance. Hover either one (the title-bar badge or the Inspector row) and a tooltip spells out the absolute start time (Started Apr 26, 2026, 1:40 PM), so “2h” is one hover away from “since when”. Both readouts tick live every second and stay hidden until the session’s start resolves. -
Put a terminal to sleep and wake it later with the conversation intact
A terminal you’re done with for now — a Claude session blocked on a long run, a worktree you’ll return to tomorrow — can be put to sleep with the ☾ button on its title bar (or the Sleep terminal command): its PTY, agent, and GPU context are released, exactly as if you’d closed it, while the tile stays on the canvas, moonlit and dormant. Wake it and kolu re-spawns the terminal in the same directory and resumes the agent right where it left off — the same path your terminals come back through after a restart, so the conversation returns, not a blank shell. A sleeping tile stays a full canvas citizen: drag it, resize it, rename it, re-theme it — only typing is fenced off (there’s no live shell to type into). The dormant tile shows the last-known context it was working — the working directory, git branch, and any open GitHub PR (frozen at sleep) — so a parked session stays recognizable at a glance. Sleeping terminals survive a full restart, read moonlit in the dock and minimap, and closing one discards it rather than killing a process (there’s nothing running to end); any splits are closed first, with a heads-up so nothing vanishes silently.
Heads-up
-
KOLU_PADI_HOST is now a seed list — the canvas boots local, and you switch
PreviouslyKOLU_PADI_HOST=<host>made the whole canvas that one remote machine. Now it’s a pool seed: the named host(s) join a warm pool, but the canvas boots on your local machine (the always-present, unremovable default) and you switch to a remote from the new chrome-bar strip. So a singleKOLU_PADI_HOST=<host>no longer opens straight into that host — it opens local, with the host one click away. The remote’s terminals are durable as ever and nothing about the remote binding changed — only which host the canvas starts on. (A comma-separated list seeds several at once.) -
Two kolus can't fight over one machine's terminals — the second refuses to start
Only one kolu-server may supervise a given machine’s workspace daemon (padi) at a time — two would drain-and-respawn the same daemon in a tug-of-war. So starting a second kolu-server pointed at a machine another is already supervising now fails fast at boot with a clear message naming the conflict and the fix, instead of silently degrading. To run two independent instances on purpose, give the second its own state:KOLU_STATE_DIR=<dir>(andKOLU_PADI_STATE_DIR=<dir>for an isolated local padi). For a remote host in the pool, setKOLU_REMOTE_PADI_STATE_DIR=<dir>so each kolu reaches its own remote padi rather than contending for one — and if a genuine fight is detected mid-flight, that host now shows an honest “another kolu owns this host” card with a one-click Switch to local, never a silent spinner.just devand the e2e harness already isolate themselves, so nothing changes there. -
The Inspector shows how long the current agent has been running
The right-panel Inspector’s Agent section tells you what the agent is — its kind, state, model, task, context size — but never how long it had been at it. It now carries a Running for row: the time elapsed since the current session’s first message, ticking up live while the panel is open (12s→5m→2h→3d). It reads each agent’s own start time straight from its on-disk state — Claude Code’s transcript, Codex’s thread id, OpenCode’s first message — so the clock measures the conversation’s age and keeps counting across a resume, and it simply stays hidden for a session whose start can’t be pinned down. -
A live dot shows which terminals are actively producing output
Nothing told you, at a glance, whether a terminal was sitting idle or churning — a long compile, atail -f, an agent printing tokens all looked the same as a quiet shell once you weren’t staring at them. Now a soft green dot appears in the dock the instant a terminal’s output is streaming and fades a moment after it goes quiet: it takes the place of the “5s ago” timestamp on that row (while it’s happening now, how long ago is moot), and rides the touch drawer, the collapsed rail, and the sub-terminal tabs the same way. It’s a different signal from the agent state border — that one tracks an AI agent’s thinking/waiting, while this lights for any terminal moving bytes, agent or not — so a sweep of the eye down the dock separates the sessions doing work from the ones merely open. The swap reserves the cell’s width so the row never reflows, and reduced-motion stills the dot like every other kolu indicator. (Superseded by PR 1475 below: the dot now leads the row on the left and the timestamp stays put.) -
End a terminal from your shell with kaval-tui kill
kaval-tuicouldlist,create,snapshot, andattachyour live terminals, but never end one from the shell — a terminal the daemon owns outlives every client, with no way to tear it down except from inside kolu.kaval-tui kill <id>now does it: the daemon kills the PTY, it drops out oflist, and any client still attached to it sees the shell exit.<id>is the short id fromlistor any unique prefix — the same formsnapshotandattachtake — and a missing id fails loud rather than silently doing nothing. It completes the raw multiplexer’s lifecycle (createmakes a terminal,killends it) and works against a local daemon, a running kolu (--socket), or a remote one over ssh (--host). -
Terminals created with kaval-tui now appear in kolu live
A terminal you spawn from the shell withkaval-tui create— against the verykavaldaemon your running kolu is a client of — now shows up as a tile in kolu instantly, the moment it’s created, instead of only after the next kolu restart. The two clients already shared one daemon (sokaval-tui listreaches kolu’s terminals); now the visibility runs both ways. Under the hood the daemon gained a live inventory feed that every client subscribes to, so kolu’s terminal list is a continuous projection of the one set of PTYs the daemon owns — the daemon is the single source of truth, not a list kolu re-syncs only at boot. A terminalkaval-tuiends the same way drops from the canvas. (A barekaval-tuishell carries none of kolu’s shell hooks, so its tile shows the live shell and scrollback but no agent/title detection until you run something in it.) -
Watch the agents on another machine with pulam-tui --host
pulam-tuicould read the awareness of terminals on your machine; nowpulam-tui --host nix@prodreads it from a remote machine over ssh, with nothing to install there first. It provisions thepulamdaemon with Nix (ships the right-arch build over ssh and realises it), runs it against the remotekavalit discovers, and dials it — so you see one row per remote terminal (branch · PR · agent · foreground), exactly as you do locally. The discovery is what makes it land on a real setup: a kolu installed on the remote owns its terminals through a kaval namespaced by listen port, andpulam-tui --hostfinds it with no extra flag (pass--kaval <path>only to pick one when several kavals run on the host). Unlikekaval-tui --host, pulam is ephemeral: it owns nothing and recomputes awareness from scratch on each connection, so there’s no remote state to outlive the link. (Needs passwordless ssh and your user trusted by the remote’s Nix daemon;--hostand--socketare mutually exclusive — the same reachkaval-tui --hostrides.) -
Drag and drop video files onto a terminal
Drag-and-drop upload now accepts video —.mp4,.m4v,.webm,.mov, and.ogv— alongside the text, code, structured-data, document, and image types it already took. Drop a screen recording onto a terminal and, like any other dropped file, it’s saved server-side and its path is bracketed-pasted into the PTY for the agent to pick up. The accepted set mirrors the video formats kolu already previews in the Code tab, so what you can drop and what you can play back stay in lockstep. The per-file 10 MB cap is unchanged — video is allowed, not exempted — so a longer recording can still be rejected as too large. -
pulam & pulam-tui: a standalone terminal-awareness daemon and its viewer (beta)
pulam (Tamil pulam, புலம் — “field · domain”, from the same root as pulan, “a sense”; a sibling to kaval, watch/guard) is a small, standalone daemon that turns the raw terminals kaval owns into awareness: for every terminal it derives the repo branch, the open PR and its CI checks, which AI agent is running and whether it’s working or waiting on you, and the foreground process. Run it on a box where kolu has never been installed —nix run github:juspay/kolu#kavalfor the terminals,nix run github:juspay/kolu#pulamfor the awareness over them — and read it with pulam-tui:pulam-tuishows one row per terminal (id · branch · PR · agent · foreground). It’s a “what is every agent doing, across every repo” dashboard you cansshinto, with zero kolu-server and no browser — the runtime is justnode · git · gh. It runs the same awareness sensors kolu uses internally, now as a process of its own; kolu still derives awareness in-process for its own terminals today, with the remote-host story to come. -
Click a folder in terminal output to reveal it in the Code tab
Apath/to/filereference in terminal output was already clickable — it opens that file in the Code tab. Now a folder reference (packages/client/, or any slash path that names a directory) is clickable too: instead of the old “File reference not found” toast, clicking it switches the Code tab to the All files tree, expands that folder and its parents, and scrolls it into view — so you can jump straight from a path in a build log orlsoutput to browsing that directory. Files still open exactly as before; a path that’s genuinely a directory wins over a same-named file elsewhere, sosrc/corereveals the folder rather than guessing at a straycorefile. -
Unfolded foldables and tablets get a two-pane layout
On a wide touch screen — a Samsung Galaxy Z Fold 6 unfolded, an iPad, an Android tablet — kolu used to fall back to the full desktop view: the mouse-driven pan/zoom canvas and floating dock, clumsy to drive with a finger. The layout was chosen on viewport width alone, and an unfolded Fold is ~900px wide — past the phone breakpoint — so it read as a desktop even though it has no mouse. kolu now treats a roomy finger-driven device as its own form factor and gives it a two-pane layout: a persistent terminal rail down one side, the active terminal filling the rest, with the same touch-sized controls and bottom-sheet panel as the phone. Phones (and a Z Fold folded) keep the single fullscreen tile; a real mouse still gets the canvas. The split now keys off the pointer, not just the width — a coarse-pointer screen never masquerades as a desktop again. -
Reach a terminal on another machine with kaval-tui --host
kaval-tuicould drive akavaldaemon on your machine; nowkaval-tui <command> --host nix@proddrives one on a remote machine over ssh, with nothing to install there first. It provisions the daemon with Nix (ships the right-arch build over ssh and realises it), runs it, and dials it — every subcommand (list/create/snapshot/attach) works exactly as it does locally, just pointed at the remote. The remote daemon is durable, so a terminal survives the connection that made it:kaval-tui create --host nix@prodstarts a shell on prod, and a separatekaval-tui attach --host nix@prod <id>later — even after you closed the laptop and changed networks — finds it still running, scrollback and all. One shared daemon per host. (Needs passwordless ssh and your user trusted by the remote’s Nix daemon;--hostand--socketare mutually exclusive.) -
Spawn a new terminal from your shell with kaval-tui create
kaval-tuicould alreadylist,snapshot, andattachyour live terminals from any shell — but not make a new one, so a freshly-startedkavaldaemon (which owns no terminals yet) had nothing to attach to.kaval-tui createnow spawns a new terminal on the daemon — a plain$SHELL, or a command you give it (kaval-tui create -- htop -d 5) — and prints its short id;kaval-tui attach <id>then takes it over. The daemon owns the terminal, so it outlives thecreatecommand that made it — create it from one shell, attach from another, detach and reattach at will.kaval-tui create --jsonemits{ id, pid, cwd }with the full id for scripts. -
Grab any terminal from your shell, by short id
kolu’s terminal ids are uuids — far too long to type — sokaval-tui listnow prints just the first 8 characters, andkaval-tui attach/snapshottake that short id or any unique prefix of it:kaval-tui attach a1is enough when nothing else starts witha1, an ambiguous prefix lists the candidates so you can add a character, and a full id pasted from elsewhere still resolves (an id is a prefix of itself). To make that reach even shorter, the right-panel Inspector now carries an Attach row showing the exactkaval-tui attach …command for the terminal you’re looking at — click to copy it (hover for the full id) and paste it into any shell to take that same terminal over.kaval-tui list --jsonstill emits the full id for scripts. -
The kaval panel flags when a newer terminal daemon is ready
Now that your terminals survive a kolu update untouched, the running kaval (the daemon that owns your shells) can be a build behind the one kolu would start fresh — it kept running across an update that changed kaval’s code. The chrome bar’s kaval column now shows an amber ⬆ update chip when that happens; click the column and it spells out the running build vs the build kolu would spawn, with a one-click Restart kaval to pick up the new one (your session is captured and offered for restore on the fresh daemon). It stays silent when an update didn’t actually change kaval, so it never nags you about a restart that would gain nothing. -
Your terminals now survive a kolu update
When kolu updates and restarts, your terminals — shells, scrollback, and any running agent — now stay exactly where they were, untouched. kaval (the daemon that owns your shells) keeps running across the update, and kolu re-adopts its live terminals on boot instead of starting them fresh, so a deploy that doesn’t change kaval itself no longer interrupts your work. The only sign anything happened is a brief “N terminals reattached” note and the kaval panel’s uptime, which keeps counting from before the update rather than resetting to zero. (Terminals whose shell had already exited are dropped, exactly as when a shell exits normally; an update that does change the daemon still does a clean supervised restart with your session offered for restore.) -
See which terminal needs you — agent state on the tile border
With several terminals open on the canvas it was hard to tell at a glance which one is thinking, which is waiting on you, and which just pinged you while you were looking elsewhere — the state was there, but only as a title-bar label you had to read. Now each live tile wears its agent’s run-state as motion on its border, drawn in the tile’s own repo colour: a working terminal runs a calm marching-ants outline (busy, asks nothing); one that needs you sweeps a comet around the border whose speed is the urgency — fastest for a fresh “your turn”, slowing as the wait goes stale; and a missed alert (a tile that flipped to needing you while you weren’t looking) throbs loudest until you focus it. Idle and parked tiles stay dark. So a sweep of the eye across the canvas reads the state of the whole fleet without reading anything. The tile you’re actually in is marked by a crisp outline floating just off its edge — one colour throughout, the repo’s, so focus and state never fight over a hue — and reduced-motion freezes every aura to a static ring. -
Restart kaval in one click — your session is preserved
When kaval (the daemon that owns your shells) stops mid-session, the canvas no longer just tells you to restart kolu — it gives you a Restart kaval button that recovers it in place. The same button lives in the kaval panel on the chrome bar’s identity rail, so you can also restart a running daemon to pick up a freshly deployed build. Either way kolu captures your terminals first, brings a fresh daemon up, and offers your session for restore on the empty canvas — the rail’s kaval dot shows a “restarting…” pulse throughout. (Adoption — terminals that survive a kolu update untouched — is the next step.) -
Your terminals now run in kaval, a daemon kolu spawns
kolu no longer owns your shells inside its own process — it starts kaval (the PTY daemon) and talks to it over a local socket. Day to day nothing changes: your terminals look and behave exactly as before, restored across reloads the same way. What’s new is honesty and reach. The chrome bar’s identity rail gains a kaval column with a live green/red health dot, the daemon’s build + closure-hash, and its uptime, beside the existing server and client columns — click it for a panel showing the daemon’s status and how to reach these same terminals from your shell withkaval-tui. If kaval ever stops mid-session, the canvas now says so plainly — an explicit “the terminal daemon stopped” surface with your session preserved — instead of silently looking like an empty workspace with no terminals. Andkaval-tui(the terminal-side client) now reaches kolu’s terminals with no--socketflag — justkaval-tui list/snapshot/attach. Restarting kolu still starts your terminals fresh (persistence across a daemon restart is the next step). -
kaval & kaval-tui: a standalone PTY daemon and its terminal client (beta)
kaval (Tamil kāval — watch, guard) is a small, standalone PTY daemon: it owns your shells, mirrors their screens, and serves them over a local unix socket, outliving the clients that come and go. Run it on a box where kolu has never been installed —nix run github:juspay/kolu#kaval— and drive it with kaval-tui, its terminal client:kaval-tui listprints every live terminal (id · pid · idle · command · cwd, or--json) andkaval-tui snapshot <id>dumps a terminal’s scrollback to stdout for piping and grepping (#1084), whilekaval-tui attach <id>takes one over full-screen with raw passthrough — every keystroke and chord reaches the inner program, an ssh-style line-start~.detaches (~~sends a literal tilde,~?shows help,--escaperebinds), the scrollback repaints on attach, and it exits with the inner program’s code, your terminal always restored cleanly even on a crash (#1255). The home-manager module installs kaval-tui alongside the server; to drive a running kolu-server’s in-process terminals instead, pass--socket $XDG_RUNTIME_DIR/kolu/pty-host.sock, and an unreachable daemon is an honest one-line error, never a hang. It’s the same code that powers kolu’s terminals, now runnable as a program of its own; kolu still embeds it in-process today, with the daemon flip to come. -
Maximize a tile without the mouse
You can now toggle a tile between the freeform canvas and maximized view straight from the keyboard with Cmd/Ctrl+Shift+M, or by running “Maximize terminal” from the command palette — no more reaching for the chrome-bar button or double-clicking a title bar. The command’s label flips to “Restore canvas” once you’re maximized, so it always names what the next click does. -
Cmd/Ctrl+F now finds in whatever you're focused on
Find-in-terminal is now confined to the terminal — the one place kolu’s search is needed, since terminal output is drawn on a canvas the browser can’t search. Press Cmd/Ctrl+F anywhere else — the Code tab’s file viewer or diff, rendered Markdown, a sandboxed.htmlpreview, the command palette, the workspace switcher — and it now hands off to your browser’s own find-in-page, which searches the real page (and reaches inside the preview frame, which terminal search never could). Click into the surface you want to search first; with focus in a terminal the chord still opens kolu’s find bar as before.
Changed
-
Your terminals now survive a kolu-server restart — and the memory rail shows a third process
Kolu split its per-host terminal workspace into its own long-lived process — padi — that owns your terminals, their layout, and your saved session; kolu-server thins to the web shell that connects your browser to it. The visible payoff: restarting or redeploying kolu-server no longer loses your terminals’ metadata. padi keeps the registry warm across the restart, so your canvas re-attaches already-whole instead of re-deriving itself — and the transient window where a mid-restart crash could eat the session is gone. Nothing else about the workspace looks different, with one deliberate exception: the memory readout in the identity rail and the Diagnostic Info dialog now lists three processes — kolu-server, padi, and kaval — because there genuinely is a third process now, each showing its own honest resident memory (or an honest “no daemon”/error state, never a fake zero). -
The chrome rail now owns app and daemon identity
Kolu’s desktop header now keeps the Kolu and Kaval logos inside the version rail instead of repeating a separate canvas watermark. The icons sit directly on the rail, without extra logo tiles, and the whole Kaval segment — including its update or memory warning chips — opens the Kaval details panel. The floating Dock also drops a little lower under the header, so it reads as a canvas tool rather than something stuck to the chrome. -
Choose how new terminals get their theme — inherit or shuffle — and steer the shuffle
The Settings “Shuffle theme” on/off toggle is now two settings that split apart what was one bit. New terminal theme picks the strategy for a freshly opened terminal: Inherit copies the active terminal’s theme — set one theme you like and every new terminal follows, exactly like new terminals already inherit the active one’s size — or Shuffle (the default) auto-picks a background perceptually distinct from your other open terminals. Shuffle behaviour then steers every shuffle — a shuffled new terminal and the ⌘⇧J “shuffle this terminal” shortcut alike: Auto (the default) keeps picks in the app’s current light or dark family, so a tile can’t come up as a jarring bright-white panel in a dark workspace; Dark / Light force one family; Random spans the whole catalogue. Your old setting carries over automatically — on → Shuffle · Auto, off → Inherit — and flipping the app between Light and Dark only affects terminals you open afterward, never ones already on screen. -
kolu opens far faster on first load — the app bundle is now served compressed
Opening kolu fresh — a first visit, a cache-miss, or the first load after a deploy — used to download the whole ~2.6 MB JavaScript bundle uncompressed, even though every browser asks forgzip/brotli. The server now serves build-time-compressed assets with the rightContent-Encoding, so that bundle arrives as 571 kB instead of 2.56 MB — 4.6× smaller. The difference shows up most exactly where it hurt worst: opening kolu from your phone or over Tailscale, where those bytes cross a slow link. Repeat opens were already instant (immutable caching) and are unchanged. -
A terminal you quit out of wakes to a bare shell, not a relaunched agent
When you put a terminal to sleep — or it’s restored after a restart — kolu brings its agent back only if the agent was still running at that moment. If you’d quit the agent to a plain shell first, waking the terminal now lands you back at that bare shell, instead of silently relaunching the most-recent agent in that directory. Restoring the exact conversation you left running (juspay/kolu#1495) is unchanged — this only affects terminals where you’d deliberately exited the agent, where an auto-relaunch was rarely what you wanted. (A session saved by an older kolu still relaunches most-recent on restore, exactly as before.) -
The Inspector's Attach section is redesigned and now covers the whole terminal CLI
The right panel’s Inspector carried an Attach section that stackedkaval-tui attachandsnapshotcommands as a row of look-alike grey bars — and surfaced only those two of the CLI’s commands. It’s been reworked into something you can actually read: each terminal (the tile’s main pane and every split) is now a tidy card headed by its copyable short id, with copy-paste attach, snapshot, and send commands beneath it. A compact command reference lists the rest ofkaval-tui(list · create · kill) and its awareness siblingpulam-tui(status · watch · wait), so the panel documents the whole CLI rather than two corners of it. And a Drive one agent from another callout spells out the send → wait → snapshot loop that lets one agent supervise another, pointing at the/koluskill with llm-debate as a worked example. The daemon socket is surfaced once at the foot for the reference commands to target. -
The fleet dashboard shows idle agents by default
pulam-web (the browser agent dashboard) opened showing only active agents — ones working or blocked on you — with idle agents tucked behind an opt-in toggle. So a fleet whose agents had all just finished read as an empty board, even though every one of them was still there. It now shows every agent by default, active and idle alike, so the board is the full picture out of the box; the toggles for non-agent terminals and sleeping shells (which aren’t agents) are unchanged. Flip “idle” off in the footer if you only want the busy ones. -
One status indicator per row — the live dot, the agent pip, and the unread alert, merged
A dock row (and a pulam-web dashboard row) used to carry up to three separate signals jostling for space: a green dot for moving bytes right now, the agent-state pip beside it, and — when an agent finished in the background — a loud disk that replaced the state pip with an unread alert. They’re now one indicator. The agent-state pip is the core; a thin green ring gently sweeps around it while the terminal is producing output; and a small amber corner badge appears when a notification has fired you haven’t seen — so a single glance reads overall activity, and a pending alert no longer hides the live state (the badge sits beside the core, not over it). On the Dock this reclaims the dead left margin the separate live dot used to sit in, and the indicator now centres across the whole row; on the pulam-web dashboard it adds a per-row alert the board never had (beyond its fleet-wide “needs you” strip) and retires its off-palette green dot. Both surfaces draw the identical indicator from one shared component, so the live ring and the alert badge can’t drift apart the way the two green dots had. Switching to a quiet terminal no longer briefly flashes its live ring, either — the resize that reveals a tile no longer counts as activity. -
The fleet dashboard and the Dock now show the same status pip
pulam-web (the browser agent dashboard) drew each agent’s status with its own little glyph — a filled dot for needs-you, a◜for working, an○for idle — on its own amber/cyan palette, while kolu’s on-canvas Dock drew the same states as shaped pips (a hollow spinning ring for working, a quiet dot for awaiting, a muted dot for idle, a ☾ for a sleeping shell) in kolu’s “your turn = violet” colours. Two icons for one set of states, and they had drifted apart. Both surfaces now render the same status pip from one shared component, so a given agent state looks identical wherever you see it — the dashboard inherits the Dock’s exact shapes and the sleeping ☾, and its agent colours move onto kolu’s shared palette (“your turn” reads the same cool violet as the Dock, working the same teal). The needs-you sort, the breathing “needs you” strip, and the green activity dot are unchanged. -
A just-finished agent no longer sits in the Dock's needs-you row
The Dock sorts your terminals by what needs you — but a terminal whose agent had just finished its turn (the briefwaitinglull before it settles) was floating up into the awaiting-you row, alongside agents genuinely blocked on your input. Now only an agent actually waiting on you (awaiting_user) rides that row; a just-finished agent drops to the quieter idle group. This is the same “what needs you” logic the pulam-web agent dashboard uses — the Dock now shares that one definition, so the two can never disagree. Your “agent finished” notifications are unchanged. -
pulam-tui is now a live OpenTUI dashboard
Runningpulam-tuiused to print a static text block —pulam-tui listfor one row per terminal,pulam-tui watch <id>to follow one. It’s now a single OpenTUI dashboard: a barepulam-tuiopens a compact, truecolour table — one row per terminal (id ·repo·branch· PR + checks · agent · state · foreground · recency), with the agent state and PR coloured so the eye lands on what matters (awaiting you → amber, working → cyan, checks pass/fail → green/red). Thelistandwatchsubcommands are gone — the table is the default view, andpulam-tui --jsondumps the same data as a flat array for scripts (jq-friendly, full ids). For now the table is a point-in-time snapshot read when it opens, with a clock ticking in the header so you can see it’s live; live row updates and a multi-host fleet board are coming next. It renders under Bun (OpenTUI’s native core needs it); thepulamdaemon and the rest of kolu stay on Node, and--host/--socketwork exactly as before. A piped run now asks for--jsonexplicitly instead of dumping text. (Superseded by the Heads-up below: pulam-tui reverted to a thinstatus/watchCLI under Node; the dashboard and fleet board are now pulam-web’s.) -
The live-activity dot now sits on the left of each dock row
The green “this terminal is producing output right now” dot used to ride the right edge of a dock row, where it stood in for the “Xs ago” timestamp while output streamed. It now leads the row on the left, just before the agent state pip — so the two glance-signals read on opposite edges instead of sharing one slot: moving bytes now on the left beside the agent’s working/waiting state, how long since last active on the right. The timestamp consequently stays put rather than vanishing whenever a terminal goes live. The dot keeps its own fixed-width column, so a row lighting up never nudges the state pips out of alignment, and the same move rides the touch drawer and compact rail. (Superseded by PR 1555 above: the dot is now the state pip’s green ring, not a separate left column.) -
Toggling a Markdown file between Source and Rendered is now instant and keeps your place
The Code tab’s Markdown preview offers a Source ⇄ Rendered toggle, and flipping it used to throw the inactive view away and rebuild the other from scratch — re-parsing, re-sanitizing, and re-highlighting the whole document, so on a large file every flip meant a visible hitch and a reset scroll position. kolu now keeps both views alive once you’ve opened them and simply shows or hides them, so toggling is instant and your scroll position (and the rendered document’s state) survives the round-trip. The view still refreshes when the file’s contents actually change. -
Tell repos apart at a glance in the dock
When you keep terminals across several repos, the dock groups its rows by repo — but the only repo cue was a tiny coloured label on a grey band that scrolled out of sight, while each row wore its own branch colour, so the grouping was hard to see. Each repo section now carries a continuous repo-coloured spine down its left edge and a faintly repo-tinted header that stays pinned to the top while you scroll that repo’s rows — so which repo a terminal belongs to is obvious without hunting, and the label never disappears. The per-branch row colours are unchanged (they still tell branches apart within a repo), and the same treatment rides the mobile drawer. -
Big diffs no longer hitch the Code tab while they syntax-highlight
Opening a large diff or file in the Code tab used to color the syntax on the main thread, so a 50k-line lockfile — or a whole PR’s worth of hunks in one view — could briefly freeze scrolling and clicks while it tokenized. That work now runs in a background worker pool: the code paints immediately, the colors stream in a beat later, and the UI never blocks. The highlighter also moved to Shiki’s lighter JS engine in place of the WebAssembly one. Diffs and files look exactly the same — they just don’t stutter. -
The chrome bar's identity rail stops repeating itself
The srv · client · kaval rail used to print the same commit three times in a normal deploy — the server you’re on, this browser’s build, and the kaval daemon are all built from one source. It now shows that shared commit once (in srv): the client column collapses to a muted ≡ (“same build as the server”) and only spells out its own commit + a ≠ srv reload nudge when a stale cached bundle actually disagrees, and the kaval column drops its duplicate commit and build hash into its panel (click it for the daemon details, restart, andkaval-tuiattach), keeping just its health dot and uptime on the strip. The amber ⬆ update chip still surfaces there when the daemon is a build behind. Same familiar three-column rail — minus the echo. -
The Code tab's file-scope picker is now a segmented control with live change counts
The Code tab’s “All files / Local / Branch” dropdown is gone — the three views now sit side by side as a segmented control, so switching is a single click instead of open-then-pick. Because every view is visible at once, the Local and Branch segments carry a small change-count badge: you can see at a glance that you have, say, 3 uncommitted files and 12 changes versus your branch base without switching into either view. The whole-repo “All files” browser is set apart from the two git views and never badged, and each view’s description now rides a hover tooltip.
Heads-up
-
kaval-tui send: text + --key in one command is now a hard error
Sending a prompt AND submitting it in one command —kaval-tui send <id> "prompt" --key Enter— used to look like it worked but silently drop the Enter: a same-breath Enter races the TUI’s bracketed-paste debounce, leaving the prompt staged on the❯line whilesendreported success. Combining text with--keyis now a hard error that teaches the fix, becausekavalcannot observe when the TUI settled — but the caller can. Submitting a normal-size prompt is three honest steps, timed by watching the TUI go quiet rather than guessing:kaval-tui send <id> --file brief.md(the text) ·kaval-tui wait <id> --until idle:300(observe the settle) ·kaval-tui send <id> --key Enter(submit). No grace to tune, no dropped Enter. Typing the removed--submit(or any unknown flag) no longer silently sends your text unsubmitted —sendrejects it loudly, and--submitspecifically prints a migration message pointing at the two-command flow. Large pastes are different: a big paste that Claude Code folds into a[Pasted text +N lines]placeholder still does not reliably submit on Enter (tracked separately) — for a large brief, write it to a file and send a short prompt telling the agent to read it, rather than pasting the whole thing. -
pulam & pulam-tui retired — the done-signal moved to padi-tui
The standalone pulam awareness daemon and its pulam-tui viewer are gone. Everything they did is now served by padi — the per-host workspace daemon that already runs beside every kolu — and read by its CLI padi-tui:padi-tui status/watchshow what each terminal is in (record state · repo·branch · PR · agent · foreground, with a●for live byte activity), andpadi-tui wait <id> --until awaiting,waitingis the precise agent-state done-signal for driving one agent from another — read off real padi state, not a guess from silence, and robust against the stale-state race via the two-phase--until workingthen--until awaiting,waitingloop. Discovery is flag-less: inside a kolu terminal$PADI_SOCKETpoints padi-tui at the padi that owns it, so an agent driving its siblings never guesses a path. Swap anynix run …#pulam-tuifornix run …#padi-tui(thepulam/pulam-tuiflake outputs no longer exist); the browser fleet view pulam-web is unaffected. -
pulam-tui is now a thin status/watch CLI — the fleet dashboard moved to the browser
Earlier in this cyclepulam-tuigrew into a full-screen OpenTUI dashboard and a live multi-hostpulam-tui fleetboard. With the browser twin pulam-web now carrying that rich, leave-it-on-a-second-monitor fleet view,pulam-tuihas been reverted to a thin, scriptable single-daemon CLI — kaval-tui’s sibling — with exactly two subcommands:pulam-tui status(a one-shot snapshot of every terminal,--jsonfor a flat array) andpulam-tui watch [<id>](follow awareness live, one line per change,--jsonfor newline-delimited JSON; pass an id to narrow to one terminal). The OpenTUI dashboard, thefleetsubcommand (and its--by/--no-local/--ssh-configflags), and the Bun runtime are all gone — pulam-tui now runs under Node like kaval-tui.--socketand--host <ssh>work as before, just after the subcommand (pulam-tui status --host nix@prod). Want the multi-host board, the live git drill-in, or the colour-coded needs-you sort? That’s all pulam-web now. (This supersedes the OpenTUI dashboard andpulam-tui fleetentries above, which never reached a stable release.) -
GitHub Enterprise repos no longer show a PR pill (for now)
To stop kolu mistaking non-GitHub repos for GitHub ones (see the fix below), kolu now treats onlygithub.comas GitHub — it no longer guesses that an arbitrary remote host might be a GitHub server. The trade-off: a repo on a GitHub Enterprise host (your company’s owngithub.example.com) no longer shows its PR pill, even whenghis authenticated for it. This was a deliberate scope call — recognizing arbitrary Enterprise hosts needs per-host configuration, which is tracked alongside the broader multi-forge work. Everything ongithub.comis unaffected.
Fixed
-
Switching to a terminal no longer shows garbled text
On a busy workspace — many terminals, splits, maximized mode — switching to a tile could paint the whole terminal in the wrong glyphs (every character substituted —background→tackground), clearing only when you selected the text or reloaded. The cause: a tile hidden withdisplay:nonemissed the resize events that keep its GPU glyph atlas fresh, and on show the terminal re-fit to the same column/row size — a no-op that skipped the atlas rebuild — so every glyph drew from a stale atlas slot. (A resize of the window fixed it, because that did change the grid and trigger a rebuild.) kolu now rebuilds the atlas whenever a tile becomes visible again, regardless of whether its size changed — the per-tile counterpart of the rebuild it already did on browser-tab return. Most visible on Linux and, recently, macOS under load. -
A split created from outside the browser now opens like one you made yourself
Spawning a split from the shell —padi-tui create --parent <id>— or from any other client used to land the new terminal on the canvas without opening it: the parent’s split panel stayed as it was and the new pane never painted, so a split you asked for appeared to do nothing until you hunted for its tab and clicked it. It now behaves exactly like a split you create in the browser: the parent’s panel expands and the new split is selected, so it’s there and ready the moment it’s created. The one deliberate difference is don’t-steal — if you’re already working in another split of that terminal, the new one opens as an unselected tab in the now-open panel rather than yanking your focus away. Restore and host-switching are untouched: a reload still brings each split back exactly as you left it, and switching to a host doesn’t auto-open its existing splits. -
Switching hosts no longer re-fires a notification for an agent that was already waiting
An agent sitting in awaiting-you on one host used to re-notify — sound, banner, dock badge — every single time you switched back to that host, even though nothing about it had changed. (A terminal parked on a review prompt would ding on every return, forever.) The cause: kolu compared each host’s agents to the previous moment’s list by position, but switching hosts swaps the whole list — and even opening or closing a terminal reorders it — so a still-waiting agent that landed where a working one used to be read as a fresh “just started waiting” and fired again. kolu now tracks each terminal by its own identity, so only an agent that genuinely enters the waiting state notifies you; a reshuffled list can’t manufacture a phantom alert. A real transition still fires exactly once, as before. -
After an unclean kaval crash, your agents come back — resumed, not bare shells
If kaval — the daemon that holds your live terminals — was killed uncleanly (a SIGKILL, an OOM, a crash), session restore used to bring the terminal list back but every agent returned as a bare shell: the conversation gone, each one restarted by hand. A graceful kaval restart resumed them fine; only an unclean death lost them. The cause was subtle — the instant kaval died, kolu could no longer see each terminal, and mistook “can’t observe” for “the agent ended”, writing that over the good resume targets within half a second, before you ever reached the restore card. Now kolu tells the two apart: while a terminal is unobservable its resume target is kept, so an unclean death survives by construction — restore brings each agent back on its exact prior session (same conversation, same context). An agent that genuinely quits to a shell still clears, so restore never resurrects a dead one. The only thing an unclean death may still cost is the last few seconds of on-screen output. -
On a phone, you can no longer get stranded on a connecting or unreachable host
Switching to a host that was still connecting (or one that’s down) used to take the top pull-down handle away with it — the whole screen became the host’s status card, and on a still-connecting host there was no “switch back” button, so a phone user was stuck with no way to reach the host row and pick another machine. The pull-down chrome now lives above the canvas (the mobile twin of the desktop’s always-present host bar), so the handle — and the host row inside it — stays reachable in every state: connecting, unreachable, or even with no terminals open. -
Test and smoke runs no longer leak padi/kaval daemons
A daemon a test or smoke run spawns is deliberately detached so it can survive a server restart (the property some scenarios exercise) — but that meant it also outlived the run whenever teardown was skipped (a signal-killed run never runs its cleanup hooks) or scoped to the wrong workspace, andci/smoke.shreaped only its two kolu-servers, never their daemons — so every green smoke run left 2 padi + 2 kaval behind.@kolu/surface-daemonnow offers a third daemon lifetime,boundToPid: a test/smoke-spawned padi/kaval watches the run that spawned it and exits cleanly the moment that run is gone, so a killed or crashed run can’t strand daemons. This is internal only — production daemons are untouched (they still liveforever; the bind is opt-in and absent in production, so there is no way to weaken a real daemon), and no on-screen behavior changes. A pid-reuse inside the ~2s watch window is a documented residual (at worst one extra run’s daemon lingers), not a leak class. -
Switching back to a host you've already visited is instant again
Returning to a machine you’d viewed earlier used to rebuild the whole canvas — the dock emptied and refilled, the terminal went black, and the Code tab dropped to “Loading…” for about a second — even though kolu still held all of that state from moments before. A switch-back is now instant: the dock shows the full terminal list in one frame, the panels stay put, and the Code tab keeps what it had, then a fresh copy loads the moment you’re back. Each host you visit keeps its terminal list, saved session, activity feed, daemon status, and Code-tab reads warm until it leaves the pool — a host you’re not looking at isn’t polled, so nothing runs in the background; its state is simply held and refreshed the instant you return. The only thing you ever wait on is the terminal’s own sub-second re-attach. First-time visits to a host you’ve never opened are unchanged. -
An HTML preview stops jumping to the top while you read it
Reading a rendered HTML file in the Code tab, scrolled halfway down, the preview would reload and snap back to the top at seemingly random moments — even though the file’s text never changed. The trigger was another terminal doing git work in the same repo (a branch checkout during a PR loop, most often on a remote host): a checkout re-writes working-tree files with the same bytes but a new modification time, and the preview keyed its cache-buster on that modification time — so an identical-content rewrite read as an edit and forced a reload. The preview now keys on a hash of the file’s actual bytes: a real content change still reloads it live, but a same-content rewrite leaves your scroll position exactly where it was. -
A busy terminal's 'last activity' no longer freezes in the dock
The dock’s per-terminal recency — the “Nh ago” chip, the activity-window filter, and the switcher’s ordering — had stopped moving for a terminal running one long-lived agent session. It only ever advanced when the agent’s identity changed (a session started, finished, or a new one began), so a terminal churning away in the same week-old Claude/Codex session could read “24h ago” while it was actively producing output that very second. Recency now also advances on the terminal’s output, so a busy stable session stays fresh — throttled to at most once a minute, so it never re-introduces the write chatter that made kolu drop per-tick recency in the first place. A terminal that’s genuinely idle still shows its true last-active time, and adopting a terminal across a restart still keeps the recency it had (re-seeing a survivor isn’t counted as fresh activity). -
The session-restore card no longer flashes on page reload
Reloading the page with terminals open could flash the “Restore session” card for a frame before the workspace appeared —connecting → restore card → your terminals— most visible on a heavy session. Nothing was wrong: on reload the list of terminals arrives before each terminal’s details finish loading, so for a beat kolu counted zero terminals and — mistaking “still loading” for “nothing to restore” — briefly offered to restore a session that was already live. kolu now tells those two apart: while a reload’s terminals are still arriving it holds the neutral connecting surface, and only shows the restore card when a session genuinely needs restoring (after a real server restart). A reload goes straight to your terminals, no card in between. -
kaval-tui send no longer hangs forever on a terminal that isn't reading its input
If yousendto a terminal running a program that has stopped draining its input (a plaincatwhose output has backed up, say), the write had no deadline — it could block for minutes with no way out. Now asendwhose write can’t complete fails loud in a few seconds, naming the stalled terminal, instead of hanging. The bound is fixed in code (not a flag) so the never-hang guarantee can’t be tuned away. -
The Kaval status chip no longer gets stuck gray after a fresh server start
Right after kolu-server started (a redeploy, or the first connect when bound to a remote workspace daemon over ssh), the header Kaval status chip could sit gray and never recover — even though the daemon was up and everything else on the page worked — until you did a full page reload. The cause was a boot-time race: the chip asks the server for the daemon’s status the instant the page loads, but for the first moment after a fresh start the server hasn’t heard from the daemon yet, so it answered “not found” — and the browser treated that as a permanent error and gave up watching, so when the daemon reported in a second later the chip never noticed. Now a request for a value that hasn’t arrived yet simply waits (an honest “connecting” state) instead of erroring, so the chip lights up on its own the moment the daemon is ready — no reload. The same fix hardens every live per-value view against the same start-up race. -
A deploy that changes padi's code now actually restarts padi
Deploying a kolu build that changed padi — the per-host daemon that owns your terminal workspace — silently kept the old padi running: the new server adopted the daemon already up and never brought up the new build, so a padi code change didn’t take effect until a crash, a host reboot, or a manual kill (there is no in-app button that restarts padi). Now, when the server boots and finds a running padi built from different code than the one it would spawn, it asks that padi to drain — save its session and exit, while your live terminals stay alive in kaval — and starts its own build in its place. Your PTYs and scrollback survive untouched, the session stays warm (no restore card), and the new padi code is live the moment the deploy settles. It fires once per server start and never on a same-build redeploy, so two servers can never sit draining each other. A build change reaches you at deploy cadence, exactly like a version bump. -
Kaval's memory reads out in its details dialog again
When the identity rail was slimmed down to two compact chips, the kaval daemon’s live memory figure fell out of the always-visible strip — and, unlike Kolu (whose memory still shows in both its chip tooltip and its details dialog), kaval’s was left only in the chip’s hover text. The Kaval details dialog now carries a memory row, matching the Kolu dialog: the daemon’s resident set size while it’s alive, an honest poll failed when a connected daemon doesn’t answer its memory probe, and unavailable when there’s no daemon to measure. It reads the same source the rail’s hover figure does, so the dialog and the tooltip can’t disagree. -
A terminal flooding output no longer freezes when kolu sheds its slow tab
kolu’s terminal daemon caps how much un-drawn output it buffers for each connected view, and when one falls far behind — a tab wedged behind a flood of output, or one whose connection briefly stalled — it drops that view rather than growing memory without bound, expecting it to re-attach for a fresh snapshot. But the drop looked exactly like the shell exiting, so instead of re-attaching the terminal froze: scrollback stuck, no new output, as if the process had died (it hadn’t — the shell was alive and the same terminal kept working everywhere else). The daemon now marks that drop distinctly from a real exit, so the terminal re-attaches on its own and repaints from a fresh snapshot the moment it’s shed, rather than sitting frozen until you reload. You lose the sliver of scrollback between the snapshot and the drop (that’s the buffer the cap discarded), but the live screen comes right back. -
Closing a terminal no longer logs a spurious 'Terminal not found' error
Closing a terminal — especially via Close and remove worktree — could spit anerror-levelTerminal <id> not foundline into the server log, even when you hadn’t typed a thing. Nothing was actually wrong: a terminal sends its own little background messages (a focus-change blip that an agent’s focus-tracking turns on, for one), and one could arrive a split-second after the close had already removed the terminal — a harmless, correctly-dropped message that kolu was logging as if it were an error. kolu now recognizes a keystroke or resize landing for an already-closed terminal as the expected race it is and drops it quietly; a request for a terminal that genuinely never existed still surfaces loudly where that matters. -
Repos that aren't on github.com stop logging errors and flashing a 'gh: unknown error' popover
A terminal in a repository hosted anywhere but github.com — Codeberg, a self-hosted Forgejo/Gitea, GitLab, even a local repo with no remote — spat anerror-level line into the log on every poll and flashed a scary “gh: unknown error” popover on its tile. Nothing was wrong: kolu was asking GitHub’sghtool about a repo that isn’t on GitHub and choking on the refusal. kolu now asksghonly about github.com remotes; everything else simply shows no PR, exactly like a branch with none — quiet, no popover. (Real PR support for Forgejo/Codeberg is still planned; this stops the false alarm first.) -
The workspace switcher re-floats a terminal when a new agent starts after the last one finished
The recency clock that floats your most-recently-active terminals to the top of the switcher had a blind spot: once an agent in a terminal finished, starting a new agent in that same terminal didn’t bump its recency, so the terminal you’d just put back to work stayed buried where it was. It also reset every terminal’s recency to “just now” on a server restart, scrambling the ordering after a redeploy. Both came from one over-broad rule that couldn’t tell a genuinely-new agent from a terminal merely re-observing its existing one; the awareness rework below replaces it with a precise signal — a new agent identity (a start, a finish, a fresh session) bumps recency on kolu’s own clock, while re-observing the same session on restart or wake does not. So the terminal you just relaunched an agent in floats back up, and a restart leaves your switcher order intact. -
kolu no longer freezes in the browser when a git command hangs
Open kolu in a few worktrees and the whole web UI could go unresponsive — the page stops answering, terminals freeze, nothing recovers — until someone restarts the server by hand. The cause was that kolu resolved each repo’s git directory with a blockinggit rev-parserun directly on the server’s single work loop every time it began watching a repo; if that one git call ever wedged (a stuck filesystem, a contended repo), it froze the entire server with it, for as long as the call hung. kolu now does that git lookup off the work loop and gives it a hard time limit, so a slow or stuck git command can never freeze the server — turning what used to be a minutes-long hang you had to notice and restart by hand into no hang at all. -
The kaval status dot no longer shows a stale 'running' when the server connection drops
kolu’s chrome rail carries a kaval status dot — and its info dialog an uptime — telling you the terminal daemon is alive. When the connection between your browser and the kolu server dropped, or silently half-opened (a laptop sleep, a Wi-Fi roam), that dot kept glowing a confident green running and the uptime kept climbing off a stale value on the local clock, as if kaval were fine — even though kolu could no longer actually see it. The dot now goes grey “unknown” and the uptime stops the moment the link to the server goes down or wedges, so it never paints a reassuring “running” over a connection kolu has lost; it returns to green and resumes counting the instant the link is healthy again. -
Returning to kolu no longer flashes 'Disconnected' for a moment
Switching back to the kolu app — or opening the lid after your laptop slept — could flash a brief “Disconnected from server” overlay before everything carried on as normal. The connection was usually fine; the culprit was the new liveness watchdog mistaking “the computer was asleep” for “the server went silent.” While a tab is frozen its timers pause but the wall clock keeps moving, so on resume an overdue health-check fired and forced a needless reconnect. The watchdog now compares real elapsed time against running time and ignores a check that a sleep or freeze interrupted — re-checking the link fresh instead — and probes again the instant you refocus the window, so a genuinely dropped connection still recovers at once. As a backstop, the full-screen “Disconnected” overlay now waits a beat before appearing, so a sub-second reconnect (which was always invisible recovery work) never flashes the alarm, while a real, lasting outage still shows it promptly. -
The dock's activity-window now hides old sleeping terminals too
The dock’s activity-window selector (All / 4h / 12h / 24h / 48h) is meant to compress everything you haven’t touched recently out of the list — but sleeping terminals slipped through it. A terminal you put to sleep days ago kept its row no matter which window you picked, so a4hwindow still showed dormant tiles last active 3 days ago and only the live ones were filtered. Sleeping tiles now respect the window like any other row: a freshly-slept one still shows with its ☾, but once you’ve had it asleep longer than the window it drops into the hidden count (with “show all” to bring it back). The clock is when you put it to sleep, not its last agent activity — so a tile you slept just now keeps its ☾ even if its agent went quiet days ago, and a plain shell you slept long ago still compresses away. So the window finally does what it says — yesterday’s slept terminals stop piling up in the dock. -
The pulam-web fleet no longer says 'no terminals' for a host it can't actually reach
pulam-web reaches each host’s agents over an ssh link — your browser talks to a backend, and the backend dials the remote host. The per-host “connected” dot and the terminal list only ever reflected the browser-to-backend leg; they were blind to whether the backend could actually reach the remote host. So when that remote leg was down — an unreachable box, an ssh drop that exhausted its retries, or a kolu build mismatch that makes the remote agent exit on startup — the board still showed a calm green dot and “no terminals”, as if the host were simply empty (it lied: there were terminals, you just couldn’t see them). The dashboard now surfaces the real link health per host: one that’s provisioning, connecting, or reconnecting says so; one the backend has given up on shows a “Remote connection failed” card with the actual error and a Reconnect button; and “no terminals” appears only when a host is genuinely connected and genuinely empty. The same health signal is now a shared part of the remote-host toolkit — one folded “is it connected?” fact and a single status-dot component both viewers render — so the sibling fleet monitor (drishti) and any future viewer inherit it by construction rather than re-implementing, or forgetting, it: a status dot’s green is now fact-only, with no narrower raw signal left for it to paint a false green from (its amber/red “reconnecting”/“failed” tones still read the raw state, exactly as they should). And a transient hiccup no longer sticks: if the backend restarts — say your laptop sleeps and wakes and the service bounces — a host that briefly errored now heals on its own the moment the link returns, instead of freezing on a stale “Internal server error” over a fleet that has already reconnected (it would sit there until you reloaded the page). -
Reconnecting no longer makes kolu's memory briefly spike
When the connection to kolu dropped and came back — a reload, a Wi-Fi roam, a laptop waking — the server re-attached to every open terminal at once, and with many terminals open it could briefly balloon its memory by gigabytes before settling back down. That reconnect burst now reuses a single screen snapshot per terminal instead of rebuilding one per attach, and skips the work entirely for attaches whose tab has already gone — so a reconnect storm no longer spikes memory. Nothing changes on screen, and a full reload still restores the same scrollback as before. -
A codex agent on another machine no longer shows as a plain 'node' terminal
When you watch the agents on another machine — pulam-web’s fleet board, or apulamthat attached to your terminals after they were already running — acodexagent could show up as an ordinary non-agent terminal instead of being recognized as codex. The reason: codex is installed through npm, so its process is namednodeand the only way to recognize it is the command you ran — and the signal carrying that command was fire-and-forget, so a watcher that started or restarted after you launched codex never saw it. kolu now remembers the last command each terminal ran and replays it to a late-joining watcher, so codex (and any agent recognized by its command rather than its process name) is detected even when the watcher arrives late. Agents already detected at launch — kolu’s own terminals,claude— were never affected. -
The fleet dashboard no longer pins a stale 'working' agent after a reconnect
pulam-web (the browser agent dashboard) reaches each host’s agents over a reconnecting ssh link. When that link blipped — a sleep, a Wi-Fi roam, a dropped idle connection — and an agent finished its turn (or its terminal closed) while the link was down, the dashboard kept painting the last state it saw: an agent that had gone idle still showed as working, diverging from kolu’s own Dock, which read it correctly. A terminal that closed during the down window could leave a phantom row that never cleared at all. The cause was a per-host cache that carried its last snapshot across the reconnect instead of rebuilding from the remote’s fresh one. The dashboard now drops that cache the instant a link drops, so the next reconnect repaints every agent from the authoritative snapshot — no staleworking, no lingering ghost. -
The pulam-web fleet no longer freezes when a connection silently dies
pulam-web watches every agent across every host over a live connection per host — and those per-host connections had no way to notice a silently dead link. When a laptop sleeps, Wi-Fi roams, or a NAT/proxy drops an idle connection, the socket dies with no close event, so the fleet board would freeze on that host’s terminals until you reloaded the page. kolu’s own terminals already self-heal from this (a heartbeat detects the dead socket and reconnects); now every connection in the fleet does, by construction — the browser-to-backend leg for each host, and the backend-to-remote ssh leg too. A silently wedged remote (the box is up but its agent stopped answering) is force-cycled within ~25s instead of waiting on an ssh timeout, and a half-open browser socket reconnects on its own. Each host now also wears a connection indicator — a green dot when it’s connected, an amber “reconnecting…” while it re-establishes, a red “disconnected — reload” if the server restarted out from under it — so the link’s health (and its recovery) is always visible rather than silent. The liveness check rides a framework-reserved no-op every kolu surface answers, so it needs no per-app wiring and can’t be left off. -
Waking or restoring a terminal brings back the exact conversation that was running
When you put a terminal to sleep (☾) — or restore your whole session after a restart — kolu brings its agent’s conversation back. It used to reopen the most-recent conversation in that terminal’s folder: the right one in the usual one-agent-per-worktree setup, but the wrong one if a second conversation had since run in the same folder, so the slept terminal would wake up on that newer conversation instead of the one it was actually on. kolu now remembers the exact conversation each terminal was running and resumes that one by id (claude --resume <id>,codex resume <id>,opencode --session <id>) — for Claude, Codex, and OpenCode alike. If a terminal never started a real conversation there’s nothing to pin, so it keeps the previous most-recent behavior and nothing else changes. -
Terminal tiles no longer render corrupted on long, busy sessions
On a long-lived session with several terminals open, tiles could intermittently render corrupted — mostly-blank, with a stray glyph or a lone cursor block — until a full page refresh cleared them. The fix #1416 shipped for the AMD GPU-memory crash kept the hardware (WebGL) renderer on your whole working set of terminals, but the limit it chose for how many GPU contexts to hold sat too close to the browser’s hard per-tab ceiling. Over hours, contexts the browser hadn’t yet garbage-collected stacked up and drifted the count to that ceiling; the browser then force-dropped a live one, and the affected tile sat on a blank frame for ~3 seconds while xterm waited to fall back to its software renderer — several tiles hitting that window at once is what you saw. kolu now keeps a wider margin below the ceiling (while still covering a realistic 5–8 terminal working set without re-introducing the context churn #1416 fixed), so the count never reaches it and the corruption is gone. Firefox was never affected. -
A terminal on the default theme keeps its theme pill when it loses focus
Each tile’s title bar carries a small theme pill showing its terminal’s theme. A tile sitting on the default theme (Tomorrow Night, with no theme explicitly chosen) used to drop that pill the moment it lost focus — so in a canvas of several such terminals only the focused one showed a pill, and it appeared to “follow” focus around. Tiles given a custom theme were unaffected, which is why it could seem to come and go. The focused tile already showed the default theme’s name as a fallback; the non-focused path didn’t, so an unset theme read as a blank pill. Every tile now shows its effective theme name whether focused or not, so the pill stays put. -
Long-running kolu no longer runs its terminal daemon out of memory
Over days of heavy use across many terminals, kaval (the daemon that owns your shells) could slowly exhaust memory and crash — taking kolu down with it for a brief reattach-everything restart every few days. The cause: kaval kept a deep 50,000-line scrollback mirror of every live terminal, and live terminals accumulate (they survive every kolu update), so a few hundred of them exhausted its heap. kaval now keeps a leaner 10,000-line mirror per terminal — still enough to repaint a freshly-opened browser tab and to feed agent/command detection — which roughly quadruples how many terminals it takes before memory is a concern. Your in-browser scrollback and PDF export are untouched at the full 50,000 lines (those read the browser’s own buffer, not the server’s). Per-daemon heap diagnostics now let any future pressure leave a diagnosable heap snapshot behind instead of a mystery restart. -
Working across many terminals no longer crashes the GPU on AMD Linux
On Linux with an AMD GPU, running kolu in Chrome/Chromium across several terminals could slowly exhaust GPU memory until the graphics driver reset — blanking terminals, and on some setups taking down the whole desktop session (#1399, after #1308). The cause: kolu kept the hardware (WebGL) renderer on only the 2 most-recently-used terminals, so switching among more of them destroyed and recreated a GPU context on every focus change — and Chrome on AMD doesn’t reclaim that GPU memory until garbage collection, so the churn outran cleanup until the GPU faulted. kolu now keeps the renderer on your whole working set of terminals at once (up to a safe cap below the browser’s per-tab context limit), so ordinary terminal-switching no longer churns GPU contexts and the leak is gone. Holding more live contexts doesn’t itself leak (measured flat), and the cap stays under the browser limit so nothing is evicted. Firefox was never affected — running kolu there remains a clean workaround. -
The packaged kolu honors KOLU_STATE_DIR again
The Nix-built (production) kolu silently ignoredKOLU_STATE_DIR, always storing your session, preferences, and activity feed under~/.config/kolu— so running a second instance on one host meant the two clobbered each other’s saved state, and the only way to relocate it was to hijack$HOME(which also moves your ssh and git config). The packaged wrapper now honors an inheritedKOLU_STATE_DIR, while still defaulting to~/.config/koluwhen you don’t set one — so a second production instance can keep its own state directory. -
Switching terminals in maximized mode no longer re-renders them
With a terminal maximized, switching to another terminal — from the dock, Cmd/Ctrl+1…9, or cycling — used to make both terminals visibly re-render on every switch: the one you left and the one you opened reflowed their text and repainted, and the switch round-tripped a resize to the shell. The cause was a size mismatch — kolu kept the inactive (hidden) terminals at their small canvas-layout size while the maximized one filled the screen, so every switch resized both grids. Hidden terminals in maximized mode now match the maximized terminal’s size exactly, sitting behind it, so switching is a pure show/hide — no reflow, no repaint, no shell resize. -
Re-running a recent agent with quoted arguments no longer mangles them
Launching a previously-run agent from the New terminal → recent agents list — or having a restored session auto-resume one — used to strip the quotes off its arguments. Soclaude --dangerously-skip-permissions --settings '{"ultracode": true}'came back as--settings {"ultracode": true}, the shell split the JSON back apart, and Claude failed withError: Settings file not found: {ultracode:. kolu remembers these commands by tokenizing the line you typed, which drops the shell quoting; it now re-quotes any argument carrying spaces, JSON, or other shell-significant characters before storing and replaying it, so a recent agent runs exactly the way you first launched it. The same fix covers the resume command typed back in on session restore. -
The minimap's zoom controls stay reachable when terminals stack into a tall, narrow column
The minimap in the canvas’s bottom-left corner is a shrink-to-fit overview with a strip of zoom / arrange / activity-window controls below it. When your tiles stacked into a tall, narrow column the overview collapsed to match that shape — and because the whole panel shared one width, the strip narrowed with it until the zoom-in, arrange, and window buttons were clipped off its right edge and became unclickable. The panel now never narrows below the width its controls actually need: the overview still shrinks to fit a narrow canvas (just gaining a little empty space on its right), while the control strip below always keeps every button reachable. -
Switching between terminals no longer resizes their text
Clicking between two terminal tiles on the canvas used to make the one you just left subtly resize its text — it reflowed about 8% wider and snapped back when you returned — because kolu handed the GPU (WebGL) renderer to only the single focused tile and dropped every other tile to the DOM renderer, which measures characters slightly differently. kolu now keeps WebGL on the 2 most-recently-active tiles (and each one’s open split), so ping-ponging between two terminals — where the wobble was most distracting — no longer crosses that boundary and the text stays put. Opening a third terminal still drops the least-recently-used one, and the GPU-context budget stays well within the browser’s limit. (Prefer rock-steady text everywhere over GPU crispness? The terminal-renderer setting’s dom option still forces the DOM renderer on every tile.) -
Text selection in a zoomed canvas terminal now tracks the pointer
Dragging to select text in a terminal tile while the canvas was zoomed landed off the mouse — the selection started at an offset from the pointer that grew the more you zoomed and the further you reached from the tile’s top-left, which made grabbing text out of an interactive TUI like opencode a guessing game. Canvas tiles scale with a CSS transform to zoom, but the terminal measured the pointer against its un-zoomed cell grid, so every hit-test drifted by the zoom factor in both axes. kolu now inverts the tile’s scale before the terminal reads the pointer, so text selection, link hovering, and the mouse input forwarded to TUI apps all land on the cell under your cursor at any zoom. Split and panel terminals were never affected (they aren’t scaled). -
Cross-site pages can no longer drive kolu's terminal RPC
kolu’s terminal control plane is unauthenticated and carries no cookie or same-origin protection — so any web page you happened to visit could quietly reach your local kolu and drive it (create terminals, type into them, kill them all, restart the daemon) without you ever knowing. It runs over two transports — a WebSocket (/rpc/ws) for live streams and plain HTTP (/rpc/*) for one-shot calls — and both were reachable from a cross-site page: a WebSocket upgrade needs no CORS preflight, and a cross-site HTML form can POST amultipart/form-databody straight into an HTTP procedure (the attacker can’t read the reply, but the side effect already happened). Binding to localhost did not help: the malicious page runs in your own browser, which can reachlocalhostlike any other address. kolu now checks the browser’sOriginon every request, on both transports, and rejects cross-site ones; the kolu UI talking to itself (same-origin) and non-browser clients (a CLI,curl, server-to-server — they send noOrigin) are unaffected. If you front kolu with a reverse proxy ortailscale servewhose address differs from what kolu itself sees, allow it through with the newKOLU_ALLOWED_ORIGINSenv var (comma-separated origins). -
Reduced motion now actually stops the canvas tile auras
A terminal tile’s animated border — the marching ants when it’s working, the comet when it needs you — is meant to freeze to a static ring when your system asks for reduced motion. A CSS specificity slip meant the freeze rule never won the cascade against the per-state animation rules, so the auras kept moving regardless of the setting. The reduced-motion override now matches the animating selectors’ specificity, so requesting reduced motion holds every tile aura still, as intended. -
Output that arrived while kolu was in the background now appears the moment you return — no keypress needed
Switch to another window while an agent keeps working — exactly what running a fleet of background subagents looks like — and the terminal could stop painting: new output piled up unseen and only snapped into view when you came back and pressed a key. This sat below the two earlier freeze fixes (a dead WebSocket in #1235, a stuck scroll-lock in #1273): when a window is occluded the browser stops producing animation frames, and xterm schedules every repaint on one — so a paint queued while you were away never ran until an input event forced a frame. kolu now forces an immediate repaint the instant the window regains focus (with a short watchdog that also catches a stall when focus never changed), so returning to a backgrounded terminal shows the latest output at once. The Diagnostic Info dialog gained render-pipeline fields — time since last paint, whether a repaint is stuck pending, and page focus/visibility — so any future paint stall identifies itself instead of being guessed at. -
kaval-tui --host connects faster — one reused ssh connection instead of several
Reaching a remote machine withkaval-tui --hostopens ssh several times in quick succession — to detect the host’s CPU architecture, to check the daemon is already set up, and to dial it — and each used to pay its own full ssh handshake, the slow part on a high-latency link. kolu now shares one ssh connection across all of them: the first opens it, the rest ride it as near-instant channels, and it stays warm briefly so a secondkaval-tuimoments later reuses it too. It needs no change to your~/.ssh/config— kolu sets the shared connection up itself over a private socket — and quietly falls back to a normal connection if it can’t. (Pairs with the warm-host fix below: that removed the redundant work on a host that’s already set up; this removes the redundant handshakes.) -
kaval-tui --host reconnects faster — no re-provisioning a host that's already set up
The first timekaval-tui --host nix@prodreaches a machine it ships and builds thekavaldaemon there; every later connection then redundantly re-shipped and re-pinned the exact same build —nix copyreporting “copying 0 paths”, a realise that rebuilt nothing, a GC root re-pinned to where it already pointed — three slow ssh round-trips that did no work. kolu now asks the host once whether the daemon’s already present and, if so, goes straight to dialing it, roughly halving the time to connect to a warm host. A host that genuinely needs (re)provisioning still gets the full ship-and-build, unchanged. -
Flinging the canvas no longer hitches, even on a slow machine
Panning or zooming the terminal canvas with the mouse wheel or trackpad used to update every tile’s position on every scroll tick — and a fast fling fires those far quicker than the screen can repaint, so most of that work was thrown away before you ever saw it. With many tiles open on a slower machine the wasted work piled up enough to briefly freeze the canvas mid-gesture (a zoom fling on a heavily throttled client could lock the main thread for the better part of a second). Kolu now batches each gesture’s movement and applies it once per frame instead of once per tick — so the canvas keeps up no matter how fast you flick, while panning and zooming land in exactly the same place as before. (Measured ~60× less per-frame work during a fling; see the canvas-gesture investigation.) -
The 'N terminals reattached' note no longer repeats on every reconnect
After a kolu update that keeps your terminals running, kolu shows a brief N terminals reattached note — meant to appear once. It was instead reappearing on every reconnect: constantly on iPhone, where Safari reloads a backgrounded tab when you return to it, and on any desktop hard refresh — even though nothing had been re-adopted and your terminals were never lost. kolu now remembers it has already shown the note for a given update, so it appears once and stays quiet across every later reconnect and refresh. -
A canvas full of agents stays smooth
The dock’s per-terminal state animations — the orbiting glow on a working chip, the breath on one that’s awaiting you, the pulse on a card’s border — were drawn by animatingbox-shadowandbackground, properties the browser repaints on the main thread every single frame. With many terminals open that added up fast: on a real GPU a busy rail was doing roughly 150× the graphics work and tens of thousands of repaints a second while you weren’t even touching it — enough to spin a fan, and on some Linux/Wayland setups enough to wedge the desktop compositor (#1308). The animations now run entirely on the compositor —opacity/transformover a glow that’s drawn once — so they cost almost nothing no matter how many terminals are live (measured ~93% less GPU work, with the per-frame repaint gone), while looking exactly the same.prefers-reduced-motionstill freezes them to a static ring. -
Code tab remembers the file you picked, not the one you clicked in the terminal
After you clicked apath:linereference in the terminal to preview a file, then manually picked a different file in the Code tab’s tree, switching to another terminal and back used to snap the preview back to the terminal-clicked file — quietly discarding your manual pick. The clicked-file request lingered and re-fired on every terminal switch; kolu now treats it as consumed once handled, so your last selection is the one that survives the round-trip. -
The 'App updated' prompt no longer loops forever
The “App updated — Reload to apply the latest version” card could reappear immediately after every click, leaving the client stuck a build behind no matter how many times you clicked (and a manual force-reload only fixed it for a single load). The cause was in how kolu stamps its build version: the commit was baked into a JavaScript bundle the browser is told to cache for a year, so a deploy that changed only docs or other non-app files rewrote that file’s contents without changing its name — and your browser kept serving the year-cached old copy. kolu now carries the build version in the always-fresh page shell instead of the cached bundle, so a normal Reload always lands the deployed build and the card clears for good — no force-reload, no clearing site data. -
Clicking a long, wrapped URL no longer opens a clipped address
When a URL was long enough to wrap across several terminal lines, resizing the terminal — zooming a tile, resizing the window, or rearranging the canvas — could truncate it, so clicking the link opened a broken address with the middle missing. Kolu now preserves the wrapped line’s full contents through a resize, on both the live terminal and the scrollback restored when you reattach, so a wrapped link always opens the whole URL. -
Mobile key bar now types into the focused split
On a phone or tablet, the on-screen helper bar’s keys — Esc, Tab, the arrows, Ctrl-C,/, and Enter — were always sent to a tile’s main terminal, even when you’d split that tile and were working in the split. They now reach whichever terminal actually has focus, so the helper keys land in the same place your typed characters already do. -
The dock again flags Claude Code questions waiting for you
When Claude Code asks you a multiple-choice question, kolu marks that terminal as awaiting you so it surfaces in the dock — but a recent Claude Code update (v2.1.173) added ann to add noteshint to the question’s footer, which slipped between the two words kolu watched for and silently broke the detection. kolu now recognizes the new footer (and tolerates future hints landing there), so a pending question lights up the dock again instead of looking idle. -
Markdown previews now show a file's YAML front-matter
A Markdown file that opens with a---YAML front-matter block now renders that metadata as a tidy table at the top of the preview — keys beside their values, the way GitHub shows it — instead of dropping it or letting the---fences misrender as a horizontal rule and a stray heading. Front-matter that can’t be tabulated (malformed YAML, or a block that isn’t a key/value mapping) is shown raw as a code block, so it stays visible and fixable rather than silently vanishing. -
Terminal output no longer freezes until you press a key
A terminal could silently stop painting new output — an agent seemingly stuck for minutes while its work actually continued — until any keypress made everything appear at once. The cause: a viewport scroll you never made (a touch artifact, a TUI leaving its alternate screen, a scroll tick delivered late after backgrounding the tab) engaged the scroll-lock, which held all output in a buffer with nothing to ever release it. The lock now engages only for scrolls you actually make — wheel, touch, scroll keys, find-in-terminal jumps; any other scroll snaps back to the bottom and output keeps flowing. Returning to the tab also releases a lock left engaged while you were away, and the Diagnostic Info dialog now shows each terminal’s lock state and the exact transition that engaged it. -
Switching branches no longer flashes the previous branch's PR
When you switched a terminal to a different branch (or left the repo) while its PR was still being looked up, the tile could briefly show the old branch’s pull request — number, title, and CI status — until the new lookup finished, up to a few seconds later. Kolu now discards a stale in-flight lookup whose branch no longer matches, so the PR pill only ever reflects the branch you’re actually on. -
No more false GitHub auth warnings on non-GitHub repos
A terminal in a repository hosted outside GitHub (Forgejo, Codeberg, GitLab, …) used to show a “gh: not authenticated” warning on its tile and re-log it every 30 seconds — but there was nothing to authenticate; the repo simply isn’t on GitHub. Kolu now recognizes that case and stays quiet, exactly like a branch with no PR. (Real PR metadata for Forgejo and friends is planned — this stops the lie first.) -
External links in HTML previews now open in a new tab
Clicking a link to an outside site (a different host) inside a previewed.htmlfile now opens it in a new browser tab, instead of silently doing nothing or replacing the preview with the remote page. Links between files in the same preview still open in place and move the file tree, as before. -
No more error spam in fresh repos
Opening a terminal in a freshlygit init’d repository (no remote, no commits) no longer logs a repeated “no base branch found” error on every change. The Code tab’s Branch view now shows an empty diff there instead of failing — while repos that simply haven’t fetched their remote still get the actionable “run git fetch” prompt. -
No more scary error on upgrade
Upgrading from an older build no longer logs anEVENT_ITERATOR_VALIDATION_FAILEDerror at launch. Your saved Code-tab layout now carries forward cleanly instead of needing the preferences file deleted by hand. -
Terminals no longer silently freeze
A WebSocket that died without notice — a laptop sleeping, Wi-Fi roaming, or a network/proxy dropping an idle connection — used to leave a terminal frozen until you reloaded the page. A heartbeat now detects the dead connection and reconnects on its own, so live output resumes without a reload. -
Per-terminal zoom in canvas mode
Pressing Cmd/Ctrl +/- to change a terminal’s font size now zooms only the focused tile. Previously every open terminal zoomed at once, so two tiles could never be aligned to the same font size if one was zoomed before the other opened. -
Code tab keeps your tree/preview split
Resizing the split between the file tree and the preview in the Code tab now sticks. Previously, switching to another terminal (or any moment the Code tab left the screen, such as a terminal sitting outside a git repository) could silently overwrite the saved split with a garbage value, so coming back — or reloading — brought the panes up at the wrong size.
Internal
-
The boot active-host handshake is grounded against membership
The per-tab active host is restored synchronously fromsessionStoragea tick before the async host-membership snapshot arrives, so at boot it briefly names a host the pool hasn’t confirmed yet. That transient is benign — the canvas reaches the workspace regardless — but it made the per-host state owner (scopedByEntry) read the active host as a removal race and log a misleading dev-console warning on every boot (active() names non-member local …), the noise that sent a prior debugging pass chasing the wrong cause. The active host fed to the per-host scope is now grounded: it is the persisted host only while membership actually holds it, elsenull— an honest “nothing grounded yet”, no warning — so a client can never hand the per-host world an active host membership doesn’t ground, and the boot warn is gone.null, not a local substitute, is the correct empty (a substituted local world under a non-local active host would be a lie); a genuinely departed host is still re-pointed by the one membership reconcile with its toast, and a should-never-happen server-invariant violation — a loaded membership missing the unremovable local host — now fails loud in dev instead of silently blanking the canvas. No on-screen change in normal use. This does not fix the separate, intermittent “Connecting to local…” wedge tracked in #1763 — it closes the misleading boot warn and the ungrounded-active-host class that masked it. -
Terminal awareness: the producer observes, kolu remembers
The engine behind every terminal’s live context — its repo · branch · PR · agent state · foreground — was re-architected. It used to derive awareness by mutating a per-terminal record it also read back as its own memory, through a host “sink” that two homes (kolu and the standalonepulamdaemon) each had to wire correctly; that fusion of observing-and-remembering was the root of a whole class of restart bugs. It’s now split clean: a memoryless producer emits oneTerminalSnapshotper change — exactly the five fields a host can re-observe — and kolu folds that stream into state, last-write-wins, while keeping the two facts a host genuinely can’t re-observe (when an agent last moved, on kolu’s own clock; the launch line to replay) as its own remembered values. A producer literally cannot spell those facts, so however buggy or restarted its stream, it can never overwrite kolu’s memory — the old “two narrowed mutators” write-fence is now enforced by the type itself. The agent PR is now a remembered, restore-relevant fact (so a dormant tile’s PR survives and re-resolves on wake rather than flashing pending), the resume target is a fold-derived discriminated value on kolu’s record (so an absent field can’t be misread as “resume most-recent” — the quit-to-shell change under Changed above), and the change carries a one-step state migration. The recency fix above falls out of it. Design note: the Awareness — the producer observes, kolu remembers Atlas entry. -
client.health() — a framework subscription-health fact for every @kolu/surface client
The “is this surface actually healthy” check every@kolu/surfaceconsumer used to hand-roll — folding each subscription’s error into one signal — is now a framework primitive. A surface client exposesclient.health(), a FACT ({ live, subs: [{ name, pending, error }] }) that enrols every framework subscription it creates (each cell, a collection’s keys-stream and its per-key value subs, and every stream). A raw stream that owns its own loop joins throughclient.rawStream— the structural path that enrols it and throws if driven outside a reactive owner, so a raw stream can’t silently escape the fact rather than merely being asked not to; the one genuinely-exempt bare export is now namedunenrolledStreamCallso a deliberate hand-enrol can never read as a forgotten one. Theliveleg is the WHOLE truth, too: it folds BOTH the socket transport’s actual status AND every readiness-gated cell’s own predicate (a genericliveWhencell hook), so a half-open/retired socket OR a mirrored surface whose backend↔remoteconnectioncell isn’tconnectedeach readlive: falseinstead of a confident “ready” over a dead link. A consumer reads that ONE folded boolean — it no longer hand-ANDsconnection.state === "connected", because composing the mirror seam folds that leg into the fact by construction (the ssh vocabulary stays app-side;@kolu/surfaceonly invokes the predicate). Each sub’s error flows through its own self-clearing reactiveerror(), so the fact un-latches by construction — the exact bug class behind #1564, where a hand-folded?? prevfroze a host on a stale “Internal server error” after its link had already recovered. A companion<SurfaceGate>owns the policy — it derivesconnecting | degraded | readyfrom the fact; its DEFAULT renders-while-reconnecting — after the first ready frame a transport drop keeps the last-good view (a stale view beats a blank one) instead of hard-blanking, and only a cold connect blocks — with hard-gating an explicitreadyopt-in. The fact/policy split is exercised in-repo over one shared fact: pulam-web’s fleet board mounts<SurfaceGate>hard-gating the host body, while the sibling fleet monitor (drishti) mounts its own<SurfaceGate>reading the same fact stale-while-degraded (a non-blocking “data is stale” strip) — one fact, two policies. The per-host connection dot is now the framework’s<SurfaceGate>companion<HostStatusPip health={…}>, rendered by BOTH viewers, whose green comes ONLY from the folded fact’s readiness verdict — so a staleconnectedcell can no longer paint a green dot over a dead link (the same lie, one widget over, made unrenderable).surfaceClientsHealthfolds a multi-surface app’s sibling clients into one fact, and the newconnectSurfacesseam wires one socket → that fold + a by-construction half-open heartbeat, solivemeans “bytes are flowing,” not “no close event fired,” no matter how the socket was built. The hand-fold every consumer used to do — and the per-viewer dot one once colored from a raw cell — are both gone: there is now ONE folded “is it connected?” value, and no widget can read half of it. -
Syntax-highlighting libraries bumped: Shiki 4 and the latest Pierre diffs
The two libraries behind kolu’s syntax highlighting — the Markdown preview and the Pierre code/diff viewer in the Code tab — moved to their latest releases: Shiki3 → 4and @pierre/diffs1.2.1 → 1.2.10. Nothing changes on screen: Shiki’s major version is a non-event for kolu — the highlighter API and its dual-theme output (the.shikiclass and the--shiki-light/--shiki-darkvariables the Markdown styles follow for light/dark) are unchanged — verified by typecheck, the unit suite, an empirical highlight-output check, and a full build. The two bumps ship together by necessity: Pierre and the Markdown renderer share one Shiki install, and moving to Shiki 4 needs Pierre’s matching release (which widened its Shiki range to allow v4), so the workspace keeps a single shared copy rather than splitting into two. -
Drive CI from a coding agent (odu MCP server)
kolu’s CI runner odu now ships an MCP server —nix run .#odu -- mcp, wired into your agent config automatically. A coding agent (Claude Code, Codex, opencode, Gemini CLI) can start a run, snapshot the pipeline, tail a node’s log, rerun just the failed node, and block until the run settles — or the instant a node goes red — all as structured tool calls instead of scraping the terminal. The live pipeline is also a subscribable MCP resource, so notification-aware agents get pushed updates as nodes change. -
odu's live-attach command is now `odu attach` (was `odu monitor`)
kolu’s CI runner odu renamed its interactive live-view command frommonitortoattach— matching the “a CI runner you attach to” model — and unified it withrun’s view: attaching now paints the same recipes×platforms matrixrunshows, with a focused-node log pane andrto rerun the lane under the cursor.odu monitorno longer exists; usenix run .#odu -- attach. -
Turn any @kolu/surface into an MCP server
The new sibling package@kolu/surface-mcpre-exposes any@kolu/surfacespec to an MCP host —serveSurfaceAsMcpmaps each cell/stream/event to a subscribable resource and each procedure (plus optional bespoketools) to a tool under a default-denyexposeallowlist, owning the subscribe/teardown lifecycle, the zod→JSON-Schema bridge, and the stdio discipline so nobody hand-writes them twice. It builds on a new core primitive,projectSurface(withsurfaceClientRef+deriveCell/deriveStream/deriveEvent), that derives a curated surface B from a live client of surface A — a server that’s a client — so the observer-safe view an agent touches is shaped in surface-land and shared by every face.