kolu
Changelog

Changelog

Newest first. On master:nix run github:juspay/kolu

Changed

  • Upload larger bug-repro videos to agents#1760

    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#1745

    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 a N 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#1743

    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#1743

    Connecting another machine no longer requires setting KOLU_PADI_HOST first — 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_HOST still 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#1753

    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#1725

    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#1783

    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#1783

    kaval-tui history <id> prints the scrollback above the current screen — the older output snapshot --viewport can’t show — VT-serialized so colors survive, cursor-paged from the newest older lines back. --lines N prints 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#1771

    Each daemon’s info dialog gained a lifetime row beside its build/socket/memory. A durable production padi/kaval reads forever; a daemon a test or smoke run spawned reads bound 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 says forever, 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 its system.version handshake, 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#1772

    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_HOST still 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#1759

    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#1752

    An agent running inside a kolu terminal can now name itself. Every terminal kolu (or kaval-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#1749

    padi-tui --host <ssh> reaches a padi on a remote machine over ssh — the exact twin of kaval-tui --host. It provisions padi’s workspace daemon onto the host with Nix, runs padi --stdio over the link, and runs every verb unchanged against the remote: padi-tui status --host nix@prod snapshots that machine’s terminals, watch follows them live, wait reads real agent state off the remote padi, and create spawns a real terminal on the host — which survives the ssh link, because padi’s --stdio mode fronts the durable daemon. Read verbs are safe; create lands 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 remote create opens in the host’s home, and a remote --worktree takes an explicit --repo <host path>. --host is mutually exclusive with --socket / --state-root, and needs padi-tui run from its Nix wrapper (nix run .#padi-tui) so the per-arch derivation is baked in.
  • Switch machines from the keyboard — ⌘⇧H host switcher#1743

    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#1787

    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#1730

    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#1738

    Grok sessions now export the same HTML transcript as Claude / Codex / OpenCode — the conversation is read from chat_history.jsonl under ~/.grok/sessions/… (user turns, reasoning, assistant replies, tool calls and results). The tile inspector also shows context-window tokens from Grok’s signals.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#1732

    Run grok (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 with grok -c or grok --resume &lt;uuid&gt;. Detection reads Grok’s on-disk session files under ~/.grok — no config step — and the worktree agent picker remembers a normalized grok launch the same way it does for the other three. When Grok opens ask_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#1714

    The in-app host switcher #1675 promised (“no UI yet… the in-app host switcher coming next”) is here. Set KOLU_PADI_HOST to 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 with KOLU_PADI_HOST unset, 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#1709

    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 of kaval-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 race kaval-tui send is 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#1704

    Handing a large brief to an agent as "$(cat brief.md)" let the shell execute any backticks or $(...) in the file before kaval-tui ever 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#1685

    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#1736

    Click a .pdf in 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#1686

    The Kaval and Padi rail dialogs each list the running daemons on your host — the leak diagnostic that spots a stray padi/kaval before 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)#1675

    Set KOLU_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 reach kaval-tui --host rides.)
  • padi-tui — drive an agent by its real state#1665

    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 status prints 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 coarse working / awaiting / waiting state, then exits — the done-signal for one agent driving another, with --timeout failing loud (exit 2), a terminal that exits first exiting 3, and --json printing { id, agent }. And padi-tui create spawns 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_SOCKET makes every verb flag-less; elsewhere it autodiscovers the running padi (--socket / --state-root to point it).
  • Every terminal kolu opens now knows its own daemon — $KAVAL_SOCKET#1657

    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_DIR is unset). Every terminal a kolu (or standalone kaval) daemon spawns now carries $KAVAL_SOCKET — the socket of the daemon that owns it — exactly the way tmux sets $TMUX and screen sets $STY. So inside any kolu terminal, echo "$KAVAL_SOCKET" names your daemon and kaval-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 /kolu skill now names $KAVAL_SOCKET as the first thing to reach for, and — the bug that started this — its documented socket paths and the kaval-tui --help / connection-error hints no longer point at a kolu/-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#1645

    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 single N hidden · show all at 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#1630

    Driving an agent that drives another agent, the missing piece was a reliable “its turn ended” signal for a raw kaval-tui create’d terminal: pulam-tui wait needs hooked terminals, and polling snapshot for 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 to attach/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 for claude / 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). --json prints one result frame per outcome — { id, result, … }, where result is met / timeout / gone / interrupted / closed (a met frame adds firedidle / match —, elapsedMs, and matchedLine on a match) — and it works over --socket and --host like every other verb. The kolu skill’s create → send → snapshot loop now waits with kaval-tui wait --until idle between 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#1327

    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#1610

    kaval-tui snapshot <id> printed a terminal’s whole scrollback — thousands of lines on a long-running or compacted agent session — so snapshot | tail -8 handed 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: --viewport prints 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 --host the remote terminal is a different size, and your stdout is usually a pipe) — and --tail N (alias --lines N) the last N rendered lines. --viewport is the right read for “what’s on screen now” when one agent drives another; the bare default still dumps the full scrollback for grep. The kolu skill’s create → send → snapshot loop now reads with --viewport instead of \| tail.
  • Copy a terminal's ID for kaval-tui attach#1600

    Grabbing a terminal with kaval-tui attach <id> meant first hunting down its id — the Inspector’s Attach row only handed you the whole command, or you ran kaval-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 after kaval-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#1589

    kaval-tui could list, create, snapshot, attach, and kill your 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; --key also sends control keys (--key Escape to interrupt a running agent, C-c, the arrows). --json reports { id, bytes, paste, keys }. It’s the write half of driving an agent from a script: create a terminal running an agent, send it a prompt, send --key Enter, then read the reply with snapshot.
  • Block until a terminal's agent finishes its turn with pulam-tui wait#1589

    pulam-tui could status-snapshot and watch agent awareness; now pulam-tui wait <id> --until awaiting,waiting blocks 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-awareness pulam already serves (the coarse working / awaiting / waiting buckets), so a script can prompt an agent with kaval-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; --json prints { id, agent }. Together with kaval-tui send and a new kolu skill that teaches the create → send → snapshot loop, it turns “an agent orchestrating other agents in kolu terminals” into a plain shell workflow — wait sharpening the done-signal once you drive hooked terminals.
  • A legend on the fleet dashboard explains the status indicators#1555

    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#1544

    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 the Human / AI conversation, 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#1556

    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#1535

    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, its repo · 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#1538

    The right-panel Inspector’s Attach section showed a kaval-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#1519

    pulam-tui fleet showed each remote terminal’s repo·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 full git status for that repo — the staged · modified · untracked summary and the list of changed files with their status codes — and Esc closes it. It rides the same change-pulse the pulam daemon already serves (a repo change re-queries git 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#1514

    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#1515

    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#1509

    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#1503

    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#1497

    pulam-tui fleet showed 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. The pulam daemon 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#1486

    pulam-tui showed one machine’s terminals as a point-in-time snapshot. pulam-tui fleet turns it into a live board across machines: pulam-tui fleet --host nix@a --host nix@b fans 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 needs flattens the fleet into one urgency list (“who’s waiting on input, anywhere”), --by agent groups by state; --json dumps a flat [{ host, terminalId, … }] for scripting (e.g. a notifier). Hosts can also come from your ~/.ssh/config with --ssh-config, and --kaval <host>=<socket> pins which kaval a host’s pulam reads when several run there (otherwise that host shows unreachable until you say which). The board fills the terminal width — the repo·branch column grows to show long branches in full on a wide screen instead of clipping. It rides the same Nix-over-ssh provisioning as pulam-tui --host, viewer-only Bun, the pulam daemon stays Node. (Superseded by the Heads-up below: pulam-tui fleet is 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#1478

    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#1487

    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#1714

    Previously KOLU_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 single KOLU_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#1714

    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> (and KOLU_PADI_STATE_DIR=<dir> for an isolated local padi). For a remote host in the pool, set KOLU_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 dev and the e2e harness already isolate themselves, so nothing changes there.
  • The Inspector shows how long the current agent has been running#1471

    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 (12s5m2h3d). 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#1457

    Nothing told you, at a glance, whether a terminal was sitting idle or churning — a long compile, a tail -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#1462

    kaval-tui could list, create, snapshot, and attach your 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 of list, and any client still attached to it sees the shell exit. <id> is the short id from list or any unique prefix — the same form snapshot and attach take — and a missing id fails loud rather than silently doing nothing. It completes the raw multiplexer’s lifecycle (create makes a terminal, kill ends 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#1458

    A terminal you spawn from the shell with kaval-tui create — against the very kaval daemon 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 (so kaval-tui list reaches 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 terminal kaval-tui ends the same way drops from the canvas. (A bare kaval-tui shell 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#1439

    pulam-tui could read the awareness of terminals on your machine; now pulam-tui --host nix@prod reads it from a remote machine over ssh, with nothing to install there first. It provisions the pulam daemon with Nix (ships the right-arch build over ssh and realises it), runs it against the remote kaval it 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, and pulam-tui --host finds it with no extra flag (pass --kaval <path> only to pick one when several kavals run on the host). Unlike kaval-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; --host and --socket are mutually exclusive — the same reach kaval-tui --host rides.)
  • Drag and drop video files onto a terminal#1442

    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)#1428

    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#kaval for the terminals, nix run github:juspay/kolu#pulam for the awareness over them — and read it with pulam-tui: pulam-tui shows one row per terminal (id · branch · PR · agent · foreground). It’s a “what is every agent doing, across every repo” dashboard you can ssh into, with zero kolu-server and no browser — the runtime is just node · 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#1412

    A path/to/file reference 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 or ls output to browsing that directory. Files still open exactly as before; a path that’s genuinely a directory wins over a same-named file elsewhere, so src/core reveals the folder rather than guessing at a stray core file.
  • Unfolded foldables and tablets get a two-pane layout#1380

    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#1373

    kaval-tui could drive a kaval daemon on your machine; now kaval-tui <command> --host nix@prod drives 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@prod starts a shell on prod, and a separate kaval-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; --host and --socket are mutually exclusive.)
  • Spawn a new terminal from your shell with kaval-tui create#1370

    kaval-tui could already list, snapshot, and attach your live terminals from any shell — but not make a new one, so a freshly-started kaval daemon (which owns no terminals yet) had nothing to attach to. kaval-tui create now 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 the create command that made it — create it from one shell, attach from another, detach and reattach at will. kaval-tui create --json emits { id, pid, cwd } with the full id for scripts.
  • Grab any terminal from your shell, by short id#1358

    kolu’s terminal ids are uuids — far too long to type — so kaval-tui list now prints just the first 8 characters, and kaval-tui attach / snapshot take that short id or any unique prefix of it: kaval-tui attach a1 is enough when nothing else starts with a1, 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 exact kaval-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 --json still emits the full id for scripts.
  • The kaval panel flags when a newer terminal daemon is ready#1353

    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#1344

    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#1348

    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#1337

    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#1310

    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 with kaval-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. And kaval-tui (the terminal-side client) now reaches kolu’s terminals with no --socket flag — just kaval-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)#1301

    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 list prints every live terminal (id · pid · idle · command · cwd, or --json) and kaval-tui snapshot <id> dumps a terminal’s scrollback to stdout for piping and grepping (#1084), while kaval-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, --escape rebinds), 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#1248

    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#1298

    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 .html preview, 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#1664

    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#1659

    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#1644

    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#1643

    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 for gzip/brotli. The server now serves build-time-compressed assets with the right Content-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#1626

    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#1602

    The right panel’s Inspector carried an Attach section that stacked kaval-tui attach and snapshot commands 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 of kaval-tui (list · create · kill) and its awareness sibling pulam-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 /kolu skill 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#1555

    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#1555

    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#1551

    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#1541

    The Dock sorts your terminals by what needs you — but a terminal whose agent had just finished its turn (the brief waiting lull 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#1479

    Running pulam-tui used to print a static text block — pulam-tui list for one row per terminal, pulam-tui watch <id> to follow one. It’s now a single OpenTUI dashboard: a bare pulam-tui opens 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). The list and watch subcommands are gone — the table is the default view, and pulam-tui --json dumps 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); the pulam daemon and the rest of kolu stay on Node, and --host/--socket work exactly as before. A piped run now asks for --json explicitly instead of dumping text. (Superseded by the Heads-up below: pulam-tui reverted to a thin status/watch CLI 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#1475

    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#1446

    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#1379

    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#1363

    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#1359

    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, and kaval-tui attach), 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#1331

    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#1704

    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 while send reported success. Combining text with --key is now a hard error that teaches the fix, because kaval cannot 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 unsubmittedsend rejects it loudly, and --submit specifically 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#1665

    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/watch show what each terminal is in (record state · repo·branch · PR · agent · foreground, with a for live byte activity), and padi-tui wait <id> --until awaiting,waiting is 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 working then --until awaiting,waiting loop. Discovery is flag-less: inside a kolu terminal $PADI_SOCKET points padi-tui at the padi that owns it, so an agent driving its siblings never guesses a path. Swap any nix run …#pulam-tui for nix run …#padi-tui (the pulam / pulam-tui flake 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#1582

    Earlier in this cycle pulam-tui grew into a full-screen OpenTUI dashboard and a live multi-host pulam-tui fleet board. With the browser twin pulam-web now carrying that rich, leave-it-on-a-second-monitor fleet view, pulam-tui has 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, --json for a flat array) and pulam-tui watch [<id>] (follow awareness live, one line per change, --json for newline-delimited JSON; pass an id to narrow to one terminal). The OpenTUI dashboard, the fleet subcommand (and its --by/--no-local/--ssh-config flags), and the Bun runtime are all gone — pulam-tui now runs under Node like kaval-tui. --socket and --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 and pulam-tui fleet entries above, which never reached a stable release.)
  • GitHub Enterprise repos no longer show a PR pill (for now)#1631

    To stop kolu mistaking non-GitHub repos for GitHub ones (see the fix below), kolu now treats only github.com as 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 own github.example.com) no longer shows its PR pill, even when gh is 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 on github.com is unaffected.

Fixed

  • Switching to a terminal no longer shows garbled text#1824

    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 — backgroundtackground), clearing only when you selected the text or reloaded. The cause: a tile hidden with display:none missed 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#1819

    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#1809

    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#1784

    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#1789

    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#1771

    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, and ci/smoke.sh reaped only its two kolu-servers, never their daemons — so every green smoke run left 2 padi + 2 kaval behind. @kolu/surface-daemon now 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 live forever; 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#1764

    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#1755

    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#1726

    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#1712

    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#1704

    If you send to a terminal running a program that has stopped draining its input (a plain cat whose output has backed up, say), the write had no deadline — it could block for minutes with no way out. Now a send whose 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#1687

    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#1671

    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#1663

    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#1591

    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#1633

    Closing a terminal — especially via Close and remove worktree — could spit an error-level Terminal <id> not found line 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#1631

    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 an error-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’s gh tool about a repo that isn’t on GitHub and choking on the refusal. kolu now asks gh only 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#1626

    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#1615

    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 blocking git rev-parse run 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#1599

    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#1598

    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#1593

    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 a 4h window 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#1568

    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#1573

    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#1559

    When you watch the agents on another machine — pulam-web’s fleet board, or a pulam that attached to your terminals after they were already running — a codex agent 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 named node and 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#1550

    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 stale working, no lingering ghost.
  • The pulam-web fleet no longer freezes when a connection silently dies#1545

    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#1496

    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#1494

    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#1482

    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#1427

    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#1416

    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#1415

    The Nix-built (production) kolu silently ignored KOLU_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 inherited KOLU_STATE_DIR, while still defaulting to ~/.config/kolu when 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#1410

    With a terminal maximized, switching to another terminal — from the dock, Cmd/Ctrl+19, 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#1407

    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. So claude --dangerously-skip-permissions --settings '{"ultracode": true}' came back as --settings {"ultracode": true}, the shell split the JSON back apart, and Claude failed with Error: 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#1405

    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#1404

    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#1401

    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#1386

    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 a multipart/form-data body 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 reach localhost like any other address. kolu now checks the browser’s Origin on 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 no Origin) are unaffected. If you front kolu with a reverse proxy or tailscale serve whose address differs from what kolu itself sees, allow it through with the new KOLU_ALLOWED_ORIGINS env var (comma-separated origins).
  • Reduced motion now actually stops the canvas tile auras#1384

    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#1381

    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#1378

    Reaching a remote machine with kaval-tui --host opens 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 second kaval-tui moments 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#1377

    The first time kaval-tui --host nix@prod reaches a machine it ships and builds the kaval daemon there; every later connection then redundantly re-shipped and re-pinned the exact same build — nix copy reporting “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#1368

    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#1366

    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#1354

    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 animating box-shadow and background, 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/transform over 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-motion still freezes them to a static ring.
  • Code tab remembers the file you picked, not the one you clicked in the terminal#1345

    After you clicked a path:line reference 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#1324

    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#1303

    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#1300

    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#1284

    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 an n to add notes hint 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#1280

    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#1273

    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#1257

    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#1256

    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#1249

    Clicking a link to an outside site (a different host) inside a previewed .html file 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#1246

    Opening a terminal in a freshly git 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#1239

    Upgrading from an older build no longer logs an EVENT_ITERATOR_VALIDATION_FAILED error 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#1235

    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#1247

    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#1250

    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#1766

    The per-tab active host is restored synchronously from sessionStorage a 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, else null — 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#1626

    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 standalone pulam daemon) 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 one TerminalSnapshot per 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#1568

    The “is this surface actually healthy” check every @kolu/surface consumer used to hand-roll — folding each subscription’s error into one signal — is now a framework primitive. A surface client exposes client.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 through client.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 named unenrolledStreamCall so a deliberate hand-enrol can never read as a forgotten one. The live leg is the WHOLE truth, too: it folds BOTH the socket transport’s actual status AND every readiness-gated cell’s own predicate (a generic liveWhen cell hook), so a half-open/retired socket OR a mirrored surface whose backend↔remote connection cell isn’t connected each read live: false instead of a confident “ready” over a dead link. A consumer reads that ONE folded boolean — it no longer hand-ANDs connection.state === "connected", because composing the mirror seam folds that leg into the fact by construction (the ssh vocabulary stays app-side; @kolu/surface only invokes the predicate). Each sub’s error flows through its own self-clearing reactive error(), so the fact un-latches by construction — the exact bug class behind #1564, where a hand-folded ?? prev froze a host on a stale “Internal server error” after its link had already recovered. A companion <SurfaceGate> owns the policy — it derives connecting | degraded | ready from 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 explicit ready opt-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 stale connected cell can no longer paint a green dot over a dead link (the same lie, one widget over, made unrenderable). surfaceClientsHealth folds a multi-surface app’s sibling clients into one fact, and the new connectSurfaces seam wires one socket → that fold + a by-construction half-open heartbeat, so live means “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#1360

    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: Shiki 3 → 4 and @pierre/diffs 1.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 .shiki class and the --shiki-light/--shiki-dark variables 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)#1258

    kolu’s CI runner odu now ships an MCP servernix 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`)#1262

    kolu’s CI runner odu renamed its interactive live-view command from monitor to attach — matching the “a CI runner you attach to” model — and unified it with run’s view: attaching now paints the same recipes×platforms matrix run shows, with a focused-node log pane and r to rerun the lane under the cursor. odu monitor no longer exists; use nix run .#odu -- attach.
  • Turn any @kolu/surface into an MCP server#1270

    The new sibling package @kolu/surface-mcp re-exposes any @kolu/surface spec to an MCP host — serveSurfaceAsMcp maps each cell/stream/event to a subscribable resource and each procedure (plus optional bespoke tools) to a tool under a default-deny expose allowlist, 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 (with surfaceClientRef + 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.

The first release worth a number. kolu is a terminal workspace built for scale — real xterm.js tiles on an infinite canvas, agent-agnostic, auto-detected, zero setup. Track master for the latest; pin this build with nix run github:juspay/kolu/v1.0.0.

Added

  • Infinite canvas workspace — every terminal is a draggable, resizable tile on a 2D plane. Pan, pinch-zoom, snap-to-grid, and double-click to maximize; the posture persists across reloads.
  • The dock — a two-level (rail / cards) left-edge navigator with per-repo color and agent-state pips: a violet “your turn” disk when an agent needs you, a spinning teal ring while it works, sorted by pure recency.
  • Agent-agnostic by designclaude, codex, opencode, aider, goose, gemini, and whatever ships next week all work the same way; run one once and it surfaces in the worktree-naming flow and the command palette automatically. No registry, no adapters.
  • Command palette (Cmd/Ctrl+K) — search terminals, switch themes, run actions, and drive workspace search across 20+ metadata fields.
  • Dock pings & completion toasts — when a background agent finishes, the dock pulses, a toast names the terminal with a Switch action, and a backgrounded tab gets an OS notification.
  • Git & GitHub, auto-detected — repo, branch, and CWD from OSC 7; live PR merge-state and CI checks on every navigator surface; per-repo hue coding; whole-tree git-status colors in the Code tab.
  • Rich Code tab — syntax-highlighted browsing, rendered Markdown with a source toggle, and sandboxed inline preview of agent-generated .html / .svg / .pdf / image artifacts, all live-reloading.
  • Terminals that keep up — WebGL rendering with canvas fallback, clickable path:line file references, inline images (sixel / iTerm2 / kitty), splits and tabs, and per-terminal font zoom.
  • Mobile — single-tile view with swipe navigation, a two-row soft-key bar for the keys touch keyboards lack, and a bottom-drawer inspector.

Heads-up

  • kolu ships only as a Nix flake — there are no installers. Install Nix, then nix run github:juspay/kolu. The same command updates it; add --refresh to bust the flake cache.
  • A home-manager module runs kolu as a systemd user service on Linux and a launchd agent on macOS.