kolu
Changelog

Release notes

Changelog.

What changed, why it matters, and the work behind it. Product notes, not a commit dump. Newest first.

Next release · live on master

Unreleased

Everything that has landed since v2.0.0.

3changes
nix run github:juspay/kolu
Added
2
Changed
1
3 shown

The Right Panel

  • AddedSee what a terminal is serving1 PR
    When something in a terminal starts listening on a TCP port, a Ports section appears in the Inspector listing it — the number and the program holding it — usually within a second. No more scrolling back through a wall of agent output hunting for a URL that went by ten minutes ago. It is not a guess at printed text: kolu reads the machine’s own socket table every few seconds (and immediately whenever the terminal produces output), and attributes each listener to the terminal whose processes hold it. So a port appears because something genuinely is listening, and it disappears when that stops. Splits count as the same terminal, which matters because a dev server is usually where you put it — in the split. A port bound to all interfaces on the machine running kolu already answers at the address you are viewing kolu at, so it simply opens in a new tab; a loopback-only port, or one on a remote host, needs a door — see Port forwarding below.
  • ChangedThe Inspector now leads with what needs you1 PR
    The Inspector used to be eight equally-grey label-and-value sections you read top to bottom, which meant the one fact you check constantly — is this agent waiting on me? — was a small grey pill in the middle of the panel, styled exactly like the name of your colour theme. It now opens with the agent’s status: what it’s working on, and a state chip in the colour that state wears everywhere else in kolu (violet when it’s your turn, rust while it’s working), so you read the answer off the colour before reading a word. Branch, repo, pull request and CI collapse into one row of chips beneath it — CI as a single ✓ 9 passed rollup — with the working directory shown once instead of twice. Everything you only occasionally need now folds away: the per-check list, the repo’s root and worktree paths, and the whole terminal-CLI section. The check list is the one that folds intelligently — nine passing checks stay behind their rollup, but a check that fails or is still running expands the list on its own with the exceptions sorted to the top, so you never open a list to find good news and never miss bad news because it was hidden. The Attach section is rebuilt around the shape its commands actually have: pick a pane and a verb (attach · snapshot · send) and copy the one command line, rather than scrolling six near-identical ones that repeated the same long socket path. And the Compose box rests as a single line, growing when you click into it, instead of opening as an empty six-row box at the top of the panel.

Port forwarding

  • AddedClick a port on any host, get the page1 PR
    A dev server on 127.0.0.1:5173 is invisible from the laptop you are looking at, because loopback never leaves the machine it is on — and a port on a remote host is further away still. kolu now opens a door for both: click the port and it makes that port answer on the machine serving this page, then opens the tab. An ssh -L tunnel when the port is on a remote host, a plain TCP relay when it is on kolu’s own machine and merely on loopback — kolu reads the bind address and picks, rather than guessing. The row then carries a ⇄ :<port> badge naming where it answers, alongside a copy button and a to close the door — one Ports list, one row per port, rather than the same port listed twice. A door you opened by hand, or one whose server has since died, trails that list under “also forwarded on this host”, because a door belongs to the machine rather than to any one terminal. Wherever a forwarded port appears, kolu names the terminal serving it, and that name is a link — click it and you land on the agent that started the thing. The host tab’s dropdown carries the same rows, and the tab’s connection dot wears a thin teal ring while doors are open there. Doors open lazily — only for ports you actually click — and they close themselves when the scanner sees the listener die. One you ask for by hand (⌘KForward a port…, accepting host:port) stays until you cancel it, since kolu has no listener to watch on your behalf there. A restarted dev server gets its old door back, so links you saved keep working. Everything dies with the kolu server — guaranteed by the kernel rather than a timer — so a deploy never leaves an orphaned door open. One case is reported honestly rather than papered over: a port bound to a single interface of a remote machine answers at that address and by no door kolu can open, so its row says so instead of offering a button that would fail. A forwarded port is unauthenticated on every interface of the machine running kolu — the same exposure as starting the dev server on 0.0.0.0 yourself — so keep it to a network you trust.

Release

v2.0.0

93changes
nix run github:juspay/kolu/v2.0.0
Added
34
Changed
16
Fixed
40
Heads-up
3

Remote Hosts

  • AddedWork across several machines — remote hosts on the canvas22 PRs
    kolu is no longer tied to the machine it runs on. Point it at another over ssh and its whole canvas becomes that machine — terminals, the Code tab, paste/upload — provisioned onto the host with Nix and durable across both the ssh link and a kolu-server restart. A host strip in the chrome bar is a real tab barlocal is a Home icon plus the machine hostname kolu runs on, remotes show the ssh target, each tab has its own identity colour, a live awaiting-you count, and an always-on connection pip (green / amber / red); click the pip for the diagnostics popover (padi/kaval detail, remove-with-confirm) without hover-opening on switch; click one — or press ⌘⇧H, or hit + to add a machine anytime — and the canvas switches with no reload, with a switch-back that repaints instantly from warm state. The add-host form includes a Remote hosts — Learn more → link to the docs on desktop and mobile, without the old Alpha badge or early-feature warning. The machines you add survive a restart, a fresh connect streams its real Nix build — nix’s own verbose per-path lines in the connect tail, plus an elapsed timer that keeps climbing every second even when a single large fetch is quiet — so a slow first connect never reads as a hang, and a boot that genuinely gets stuck now ends honestly instead of sitting at “Connecting…” forever, and the escape card tells the truth about who can fix it: while a remote machine is still setting itself up over ssh the card stays non-terminal — it says kolu is still retrying (naming the phase) and its verb is Retry connection, which recycles the server-side connection a browser reload can’t touch, with a half-hour on-screen campaign backstop so a setup that never connects still surfaces the escape even as the phase flaps (that window is measured from when the tab started watching, so a reload starts it fresh); when instead the stall is client-side delivery — the host list or your saved session never arriving, or a connected remote’s agent never reporting — the card names what didn’t come and offers Reload. Either way Switch to local is offered on a remote machine, and a machine that’s legitimately still copying or building its agent gets a generously longer deadline, so a real first connect is never cut short. (A local machine whose own agent never starts keeps its existing “kaval didn’t start” card with Restart.) An agent that needs you on a background machine rolls up a badge, a notification, and one-click focus of the exact terminal. Scrolling up a just-switched terminal backfills its true history from the host. Your local machine is the always-present first tab, the whole thing works from a phone (a host row in the pull-down sheet), and the Code tab reads the connected machine’s disk — previews and the running-daemons list included.

Kaval

  • AddedYour terminals now live in kaval, a daemon that outlives kolu14 PRs
    kolu no longer owns your shells inside its own process — it starts kaval (Tamil kāval — watch/guard), a standalone PTY daemon that owns your terminals and serves them over a local socket, so they survive a kolu update or restart untouched: shells, scrollback, and running agents stay exactly where they were, with only a brief “N terminals reattached” note to show for it. If kaval ever stops, the canvas says so plainly and offers a one-click Restart kaval that captures your session first; the chrome bar’s kaval column shows the daemon’s live health and flags an ⬆ update when a newer build is ready. And when the host’s kaval is too old to speak this kolu’s wire contract, kolu names that as its own daemon state rather than a dead-end failure: the canvas card and the kaval dialog say which version the host’s kaval speaks and which your kolu needs, the plain Restart — which could only respawn the same outdated binary — gives way to a single Update & restart kaval (behind a confirm) that re-provisions the host’s build and starts a correct-version kaval, and the same chrome that flags a newer build carries the incompatible verdict. Even an unclean kaval crash — a SIGKILL or OOM — now brings your agents back resumed on their exact prior session, not as bare shells, and kaval keeps a bounded per-terminal scrollback mirror so a long-lived workspace can’t run it out of memory. And when a kaval itself shuts down for good, it reaps the shell and agent processes inside its terminals on the way out — nothing is left orphaned on the machine. And if a stray process is ever left holding kaval’s socket without its lockfile — the orphaned-daemon state that used to wedge the canvas until someone killed the process by hand — kolu now identifies that holder over the socket and recycles it on its own, so the incompatible-daemon card converges on a single Update & restart instead of looping forever; a process that turns out not to be a kaval is named and left untouched, never killed. And when a host’s channel goes dark — an ssh link that timed out, a daemon still connecting — the kaval dialog and its chip stay honest: the contract badge, socket path, build, and running-daemons list read unknown / unavailable rather than a frozen last-good value, and an unreachable host is named as unreachable, never mislabelled “connecting”.
  • AddedDrive your terminals from any shell — kaval-tui17 PRs
    kaval-tui is kaval’s terminal client — everything you can do to a terminal, from a script. create spawns one (it appears as a kolu tile instantly), attach takes it over full-screen, snapshot --viewport reads the live screen and history the older scrollback, send types into it (text or a --key, submitting as its own step; --file for a byte-exact payload), wait --until idle blocks until its agent goes quiet, and kill ends it. It reaches a remote machine over ssh (--host), takes short ids (any unique prefix, with a Copy terminal ID action in-app), and inside any kolu terminal two env vars — $KAVAL_SOCKET (the daemon that owns it) and $KAVAL_TERMINAL_ID (its own id) — let a process drive its siblings or itself with zero guessing. It’s the write half of one agent driving another; combining text and a submit --key in one call is a deliberate hard error, because observing the settle then submitting is what dodges the paste-race. A created terminal gets a clean, composed environment — the canonical vars a shell needs (HOME, PATH, SHELL, TERM, …), not a wholesale copy of the launching shell’s — so an orchestrating agent’s private identity vars can’t ride in and cost a spawned agent its saved conversation; add a specific var back with --env K=V.

Padi

  • AddedYour session lives in padi, the workspace daemon4 PRs
    kolu’s per-machine workspace — your terminals’ layout, saved session, repo context, restore state, and agent awareness — now lives in its own long-lived process, padi, while kolu-server thins to the web shell that connects your browser to it. So restarting or redeploying kolu-server no longer loses your session: padi keeps the registry warm and your canvas re-attaches already-whole, with no restore-card flash on a normal reload (the card shows only after a real restart). A deploy that changes padi’s own code drains the old daemon and brings the new build up with your live terminals untouched. padi’s state-root is never a silent default — binding requires KOLU_PADI_STATE_DIR or --state-root (production wrappers still supply ~/.local/state/padi; a bare launch dies with one clear line instead of sharing production’s chair).
  • AddedDrive an agent by its real state — padi-tui6 PRs
    padi-tui is padi’s shell client — kaval-tui’s sibling, over the workspace daemon. status/watch show what each terminal is in (repo·branch · PR · agent · foreground, with a for live output), and wait --until <buckets> blocks until a terminal’s agent reaches working/awaiting/waiting — the precise done-signal for one agent driving another, read off real agent state rather than guessed from silence. create spawns a terminal, a split (--parent), or a worktree’d agent live on the canvas — and a split you create this way now opens exactly like one you made in the browser. It reaches a remote padi over ssh (--host), and inside a kolu terminal $PADI_SOCKET makes every verb flag-less.

Sessions, Sleep & Wake

  • AddedPut a terminal to sleep and wake it with the conversation intact5 PRs
    A terminal you’re done with for now can be put to sleep (☾): its PTY, agent, and GPU context are released while the tile stays on the canvas, moonlit and dormant, still showing the directory, branch, and PR it was on. Wake it and kolu re-spawns it in place and resumes the agent on its exact prior conversation (claude --resume <id>, and the equivalents for Codex and OpenCode) — unless you’d quit the agent to a bare shell first, in which case waking lands you at that shell rather than silently relaunching something. Sleeping tiles survive a full restart, and the dock’s Filters row can hide sleeping (or merely stale) terminals so the list stays to what’s awake.
  • FixedReconnecting no longer makes kolu's memory briefly spike1 PR
    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 open it could briefly balloon its memory by gigabytes before settling. That 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. Nothing changes on screen.

Agent Detection

  • FixedAn agent run as a terminal's root command now shows in the dock1 PR
    Start an agent as a terminal’s own process — kaval-tui create -- codex …, the agent as the root with no shell wrapping it — and kolu now tracks it on the tile and in the dock like any other, instead of leaving it invisible. A shell-less terminal sends none of the command marks kolu reads, so it never learned what such a terminal was running; and a second check that asks “is the shell sitting idle?” misread the agent-as-root as an idle prompt and dropped the one clue it had. This bit hardest an agent whose program name differs from what you typed — an npm-installed CLI that runs as node — which could only be recognized by the command it was launched with. kolu now remembers that command from the moment the terminal is spawned and reads a command-rooted terminal’s busy foreground correctly. (Agents that write an on-disk session keyed by process — Claude Code — were already found this way; this closes the gap for the rest.)
  • AddedGrok Build is a first-class agent3 PRs
    Run grok (xAI’s Grok Build) in any kolu terminal and it gets the same treatment as Claude Code, Codex, and OpenCode — a Grok icon and live state on the tile (thinking · tools · waiting · awaiting you), dock ranking, and sleep/restore that resumes the session. It exports the same HTML transcript the others do, and the tile shows its context-window token count. Detection reads Grok’s on-disk session files under ~/.grok — no config step. Launch shape is remembered the way the other agents’ is: flags such as --no-alt-screen (inline TUI instead of the alternate screen) stay on the recent agents list and on sleep/wake resume, not dropped as unknown.
  • AddedSee which terminal needs you — agent state on the tile border1 PR
    With several terminals open it was hard to tell at a glance which is thinking, which is waiting on you, and which just pinged you while you were looking elsewhere. Now each live tile wears its agent’s run-state as motion on its border, in the tile’s own repo colour: a working terminal runs a calm marching-ants outline; one that needs you sweeps a comet whose speed is the urgency — fastest for a fresh “your turn”, slowing as the wait goes stale; and a missed alert throbs loudest until you focus it. Idle tiles stay dark, so a sweep of the eye reads the whole fleet’s state without reading anything. The tile you’re in is marked by a crisp outline just off its edge, and reduced-motion freezes every aura to a static ring.
  • AddedSee how long an agent has been running — in the Inspector and on the tile2 PRs
    The right-panel Inspector’s Agent section, and the tile title bar beside the context-token count, now carry a Running for row — the time elapsed since the current session’s first message, ticking up live (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. Hover either readout for the absolute start time (Started Apr 26, 2026, 1:40 PM); both stay hidden until the session’s start resolves.
  • FixedThe dock again flags Claude Code questions waiting for you1 PR
    When Claude Code asks a multiple-choice question, kolu marks that terminal as awaiting you so it surfaces in the dock — but a Claude Code update added an n to add notes hint to the question’s footer that 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.
  • FixedRe-running a recent agent with quoted arguments no longer mangles them1 PR
    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 --settings '{"ultracode": true}' came back as --settings {"ultracode": true} and the shell split the JSON apart. kolu 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.
  • FixedYour agent's prompt no longer leaks into the recent-agents list1 PR
    The recent agents list — in the command palette and the New terminal flow — is meant to remember how you launched an agent (claude --dangerously-skip-permissions), not the one-off prompt you typed after it. But a launch like claude --dangerously-skip-permissions 'You are BOOT1. Read BRIEF-BOOT1.md…' kept the whole prompt, showing it verbatim in the list. kolu now drops the trailing prompt the way it always intended, so the list stays to the launch shape and your prompt text isn’t left sitting in it.
  • FixedAgent state no longer gets stuck on a fast turn1 PR
    On a very quick turn — a sub-second reply from a small local model, or any fast completion — a tile’s agent state could freeze on the wrong reading and never catch up: the pill stayed thinking long after the turn had finished, and never reconciled to waiting. kolu watches each agent’s on-disk session file for changes, but the operating system is allowed to quietly drop the file-change notification for the last write of a fast turn — after which nothing re-read the file, so the stale state stuck (macOS was the most exposed). kolu now backs every agent-session watch with a steady low-rate poll that re-checks the file on its own, so a dropped notification self-heals within about a second instead of stranding the indicator. Covers Claude Code, Codex, OpenCode, and Grok.

Notifications

  • FixedFinished alerts wait for real quiet — and don't re-chime forever on idle terminals1 PR
    When an agent marks its turn done while background sub-agents are still typing, kolu used to chime finished immediately. The finished signal now waits until the terminal has been actually quiet for a few seconds (the same host-side meaningful-output doorbell the live ring already uses, with a longer window). Once that finished signal has fired for the turn, idle TUI redraws (statusline clocks, context meters) no longer un-finish and re-chime every few seconds — finished sticks until the agent truly runs again. Asking-for-you is still immediate; pure resize repaints still never count as work.
  • FixedA question that lands right after a finished turn now still chimes1 PR
    When an agent stops to ask you something — a question or a permission prompt — kolu plays its sound and shows a backgrounded-tab notification. But if the agent had just finished its previous turn when the prompt arrived, that chime was sometimes swallowed and only the dock badge lit up; whether you heard it came down to timing. Now a live gate reliably rings even when it lands on the heels of a finished turn, and a question kolu catches cleanly announces itself as “needs your input” rather than a bare “finished” — a brief flicker between states as the prompt settles still rings only once (that one ring may keep the earlier “finished” wording, but it fires).
  • ChangedAgent attention now reaches you the same way on every host1 PR
    Agent attention is now consistent across every host: an agent that finishes or starts waiting on you on a background machine reaches you exactly the way the active machine’s does — a sound, a backgrounded-tab popup, and a quiet amber mark on that host’s tab — instead of the thinner ping background hosts used to get. An agent that’s waiting for your input additionally counts toward the app-icon badge (which tallies agents blocked on you, across every host). And the Activity alerts setting is now Attention alerts to match.

The Right Panel

  • AddedThe right panel — Compose a prompt, and the whole terminal CLI4 PRs
    The tile’s right panel gained a Compose box: write a prompt in peace and Send it (or ⌘/Ctrl+Enter) straight into the terminal’s input line — the in-app twin of kaval-tui send (it inserts, you press Enter). Its Inspector now documents the whole terminal CLI — an attach/snapshot/send card per pane and split, a reference for the rest of kaval-tui and padi-tui, and a Drive one agent from another callout — and whether the panel is open, and which tab it’s on, is remembered per terminal across session restore.

The Dock

  • ChangedLive rings track real host output — resize can't fake them, and background terminals light too1 PR
    The dock’s green live ring (and the matching tile-title cue) no longer counts bytes only in the browser attach of a tile you’re looking at. It mirrors a host-wide signal from kaval, the process that owns every PTY: real shell output lights the ring, a pure resize/reveal repaint does not, and a terminal on a background tile can still show live. Switching to a quiet tile no longer flashes the ring for a beat.
  • AddedResize the dock in maximized mode1 PR
    In maximized mode the dock is a real sidebar beside the canvas — and you can now drag its right edge to resize it, just like the right panel. Grab the edge (the cursor turns to a resize arrow), drag to the width you want, and it reflows live; double-click the edge to snap back to the default. The width persists per device, so a wide monitor and a laptop each keep their own. The narrow rail stays a fixed strip, and the floating dock in tiled mode is unchanged.
  • ChangedThe dock shows every terminal by default1 PR
    The activity-window filter used to default to 24h, so 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 machine, but the default is to show what you have rather than hide it.
  • ChangedTell repos apart at a glance in the dock1 PR
    When you keep terminals across several repos, the dock groups its rows by repo — but the only repo cue was a tiny label on a grey band that scrolled out of sight. Each repo section now carries a continuous repo-coloured spine down its left edge and a faintly repo-tinted header that stays pinned while you scroll that repo’s rows. The per-branch row colours are unchanged, and the same treatment rides the mobile drawer.
  • ChangedOne status indicator per dock row — live output, agent state, and unread alert, merged3 PRs
    A dock row used to carry up to three separate signals jostling for space: a green dot for moving bytes right now, the agent-state pip, and a loud badge that replaced the pip with an unread alert when an agent finished in the background. They’re now one indicator: the agent-state pip is the core, a thin green ring sweeps around it while the terminal produces output, and a small amber corner badge appears when a notification you haven’t seen has fired — so a single glance reads overall activity, and a pending alert no longer hides the live state. Switching to a quiet terminal no longer briefly flashes its ring.
  • ChangedA just-finished agent no longer sits in the dock's needs-you row1 PR
    The dock sorts 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 rides that row; a just-finished agent drops to the quieter idle group.
  • FixedA busy terminal's 'last activity' no longer freezes in the dock1 PR
    The dock’s per-terminal recency — the “Nh ago” chip, the activity-window filter, the switcher’s order — had stopped moving for a terminal running one long-lived agent session. It only advanced when the agent’s identity changed, so a terminal churning away in the same week-old session could read “24h ago” while actively producing output. Recency now also advances on the terminal’s output — 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 genuinely idle terminal still shows its true last-active time.
  • FixedNo more error spam — or a false auth warning — on repos that aren't on github.com2 PRs
    A terminal in a repo hosted anywhere but github.com — Codeberg, a self-hosted Forgejo/Gitea, GitLab, even a local repo with no remote — used to spit an error-level line into the log on every poll, re-log a “gh: not authenticated” warning every 30 seconds, and flash 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. kolu now asks gh only about github.com remotes; everything else simply shows no PR, exactly like a branch with none — quiet, no popover, no warning. (Real PR support for Forgejo/Codeberg is still planned; this stops the false alarm first.)
  • FixedSwitching branches no longer flashes the previous branch's PR1 PR
    When you switched a terminal to a different branch (or left the repo) while its PR was still being looked up, the tile could briefly show the old branch’s pull request — number, title, CI status — until the new lookup finished. 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.

The Canvas

  • AddedDouble-click the empty canvas to open a new terminal1 PR
    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. A double-click on a tile (its title bar still maximizes) or the minimap is left alone.
  • AddedNew terminals inherit the size of the one you're working in1 PR
    Every new terminal used to open at the same fixed default size, so if you’d widened one to suit your work each new one came up small. A new tile now opens at the size of the terminal you’re currently in — the active one, not whichever was created last — and it picks up a size you’ve just dragged even before the change has finished saving. The first terminal still opens at the default; only the size carries over, not where the tile lands.
  • FixedFlinging the canvas no longer hitches, even on a slow machine1 PR
    Panning or zooming the canvas 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, and with many tiles open on a slow machine the waste piled up enough to briefly freeze the canvas mid-gesture. 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, landing in exactly the same place as before.
  • FixedA canvas full of agents stays smooth1 PR
    The dock’s per-terminal state animations — the orbiting glow on a working chip, the breath on one awaiting you, the pulse on a card’s border — were drawn by animating box-shadow and background, which the browser repaints on the main thread every frame. With many terminals open a busy rail was doing 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 compositor. The animations now run entirely on the compositor, so they cost almost nothing no matter how many terminals are live, while looking exactly the same. prefers-reduced-motion still freezes them.
  • FixedSwitching terminals in maximized mode no longer re-renders them1 PR
    With a terminal maximized, switching to another — from the dock, Cmd/Ctrl+19, or cycling — used to make both terminals visibly re-render on every switch and round-trip a resize to the shell, because kolu kept the inactive terminals at their small canvas size while the maximized one filled the screen. Hidden terminals in maximized mode now match the maximized terminal’s size exactly, so switching is a pure show/hide — no reflow, no repaint, no shell resize.
  • FixedText selection in a zoomed canvas terminal tracks the pointer1 PR
    Dragging to select text in a terminal tile while the canvas was zoomed landed off the mouse — the selection started at an offset that grew the more you zoomed, making text out of an interactive TUI a guessing game. Canvas tiles scale with a CSS transform, but the terminal measured the pointer against its un-zoomed cell grid. kolu now inverts the tile’s scale before the terminal reads the pointer, so selection, link hovering, and mouse input forwarded to TUI apps all land on the cell under your cursor at any zoom.
  • FixedThe minimap's zoom controls stay reachable when terminals stack into a tall, narrow column1 PR
    The minimap’s strip of zoom / arrange / activity-window controls used to narrow along with the overview when tiles stacked into a tall, narrow column, until the buttons were clipped off the right edge and unclickable. The panel now never narrows below the width its controls need: the overview still shrinks to fit, while the control strip keeps every button reachable.
  • FixedClosing every terminal at once no longer briefly marks a departed one 'active'1 PR
    When all of your top-level terminals left in the same instant — closing the last of them, or a batch that all exited together — kolu’s focus hand-off picked the “survivor” to focus by skipping only the one terminal it happened to be processing, never the rest of the departing batch. So instead of settling on no active terminal when nothing remained, it landed focus on another terminal that was also leaving, and echoed that just-departed id to the server as the active one. kolu now clamps focus past every terminal leaving in that frame: an emptied canvas correctly has no active terminal, and a batch where some tiles survive still hands focus to one that’s actually still there.
  • FixedReduced motion actually stops the canvas tile auras1 PR
    A 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, so the auras kept moving regardless. The override now matches the animating selectors’ specificity, so requesting reduced motion holds every tile aura still, as intended.

Inside a Tile

  • AddedPer-terminal font zoom in canvas mode1 PR
    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.
  • AddedSee which pane has focus in a split terminal1 PR
    Split a tile into two stacked terminals and, until now, nothing showed which one your keystrokes were going to. 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.
  • FixedTerminals no longer silently freeze1 PR
    A WebSocket that died without notice — a laptop sleeping, Wi-Fi roaming, or a network 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.
  • FixedTerminal output no longer freezes until you press a key1 PR
    A terminal could silently stop painting new output — an agent seemingly stuck for minutes while its work 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 with nothing to release it. The lock now engages only for scrolls you actually make; 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.
  • FixedOutput that arrived while kolu was in the background appears the moment you return — no keypress needed1 PR
    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. 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 for a stall when focus never changed).
  • FixedA terminal flooding output no longer freezes when kolu sheds its slow view1 PR
    kolu’s terminal daemon caps how much un-drawn output it buffers per connected view, and when one falls far behind it drops that view rather than growing memory without bound, expecting a re-attach. But the drop looked exactly like the shell exiting, so the terminal froze — scrollback stuck, no new output — as if the process had died (it hadn’t). 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. You lose only the sliver of scrollback the cap discarded; the live screen comes right back.
  • FixedSwitching to a terminal no longer shows garbled text1 PR
    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 re-fit to the same size — a no-op that skipped the atlas rebuild — so every glyph drew from a stale slot. kolu now rebuilds the atlas whenever a tile becomes visible again, regardless of whether its size changed. Most visible on Linux and, recently, macOS under load.
  • FixedWorking across many terminals no longer crashes the GPU on AMD Linux3 PRs
    On Linux with an AMD GPU, running kolu across several terminals in Chrome/Chromium could slowly exhaust GPU memory until the graphics driver reset — blanking terminals, and on some setups taking down the whole desktop. The cause: kolu kept the hardware (WebGL) renderer on only the 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 memory until garbage collection — so the churn outran cleanup until the GPU faulted. kolu now keeps the renderer on your whole working set at once, up to a safe cap below the browser’s per-tab context limit, so ordinary switching no longer churns contexts and the leak is gone. It also cured the subtler tells of the same churn — text that subtly resized as you clicked between tiles, and tiles that intermittently rendered corrupted on long sessions. Firefox was never affected.
  • FixedClicking a long, wrapped URL no longer opens a clipped address1 PR
    When a URL was long enough to wrap across several terminal lines, resizing the terminal — zooming a tile, resizing the window, rearranging the canvas — could truncate it, so clicking 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 restored scrollback, so a wrapped link always opens the whole URL.
  • FixedClosing a terminal no longer logs a spurious 'Terminal not found' error1 PR
    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. A terminal sends its own little background messages (a focus-change blip), and one could arrive a split-second after the close had already removed the terminal. 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.
  • FixedA tile's title bar no longer lags the dock on PR and agent state1 PR
    A canvas tile’s title bar shows the terminal’s pull request, agent state, and running process — but it could keep showing stale values while the dock row for the same terminal was already current (a PR that just resolved, an agent that changed state). The header read those facts from a snapshot that only refreshed when the branch or directory changed, so an update on a settled branch never reached it until something unrelated nudged it. The title bar now reads the same live source the dock does, so the two can no longer disagree.

The Code Tab

  • AddedRhai files are syntax-highlighted in the Code tab1 PR
    Open a .rhai source file or diff and kolu now recognizes the language and highlights its syntax instead of showing plain text. The grammar comes from the official Rhai language server and stays lazy-loaded with the rest of the Code tab’s language support.
  • AddedRead a footnote without leaving your place in the Markdown preview1 PR
    The Code tab’s Markdown preview renders GitHub-style footnotes, 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. Dismiss it by clicking elsewhere, pressing Escape, scrolling, or clicking the marker again; a see all ↓ link drops to the bottom list. The bottom “Footnotes” section is unchanged — the popover reads its content from there.
  • AddedPDFs open inside the Code tab1 PR
    Click a .pdf in the Code tab and kolu opens it with the browser’s native PDF viewer, so pages, zoom, and downloads work in-pane instead of showing a broken sandbox preview.
  • AddedMarkdown previews show a file's YAML front-matter1 PR
    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. Front-matter that can’t be tabulated (malformed YAML, or a block that isn’t a mapping) is shown raw as a code block, so it stays visible and fixable.
  • AddedClick a folder in terminal output to reveal it in the Code tab1 PR
    A path/to/file reference in terminal output was already clickable. Now a folder reference (packages/client/, or any slash path naming a directory) is clickable too: instead of a “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 to browsing that directory.
  • AddedSee your git changes in the Code tab — live, and on the machine you're viewing2 PRs
    The Code tab’s git views got two upgrades since 1.0. The Local (uncommitted) / Branch (vs base) / All files picker is now a segmented control instead of a dropdown, and the Local and Branch segments carry a live change-count badge — you can see you have, say, 3 uncommitted files and 12 changes versus your branch base without switching into either view. And the changed-files list and its diffs now repaint live the instant a file changes on disk — a payload-free repo-change pulse re-running git status, no polling — through a watcher loop that works over a remote link too, so your git status and diffs stay live even when the Code tab is pointed at another machine, not just this one.
  • AddedExternal links in HTML previews open in a new tab1 PR
    Clicking a link to an outside site 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.
  • ChangedToggling a Markdown file between Source and Rendered is instant and keeps your place1 PR
    The Code tab’s Source ⇄ Rendered toggle 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 opened and simply shows or hides them, so toggling is instant and your scroll position survives the round-trip. The view still refreshes when the file’s contents actually change.
  • ChangedBig diffs no longer hitch the Code tab while they syntax-highlight1 PR
    Opening a large diff or file used to colour the syntax on the main thread, so a 50k-line lockfile could briefly freeze scrolling while it tokenized. That work now runs in a background worker pool: the code paints immediately, the colours stream in a beat later, and the UI never blocks. The highlighter also moved to Shiki’s lighter JS engine. Diffs and files look exactly the same — they just don’t stutter.
  • FixedThe Code tab no longer flickers between two files in a runaway loop1 PR
    Clicking around files in the Code tab while an agent was working in that terminal could suddenly latch into a runaway loop — the selection flickered between two files ~60–120 times a second, pegging a CPU core, and only stopped when you switched terminals. Pierre (the file-tree engine) sometimes re-emits its selection on its own, with no user input; the app forwarded each of those echoes back as if you’d clicked, and then re-applied it into the tree — the two ends driving each other in a frame-rate loop. The Code tab now writes a selection back only when a real click or keypress caused it, so an autonomous re-emit can’t start the loop — while a genuine click still updates the preview immediately. Affected both All files and Local/Branch views.
  • FixedNo more error spam in fresh repos1 PR
    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.
  • FixedNo more scary error on upgrade1 PR
    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.
  • FixedAn HTML preview stops jumping to the top while you read it1 PR
    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 machine): a checkout re-writes files with the same bytes but a new modification time, and the preview keyed its cache-buster on that time. The preview now keys on a hash of the file’s actual bytes, so a same-content rewrite leaves your scroll position exactly where it was.
  • FixedCode tab remembers the file you picked, not the one you clicked in the terminal1 PR
    After you clicked a path:line reference in the terminal to preview a file, then manually picked a different file in the 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 switch; kolu now treats it as consumed once handled, so your last selection survives the round-trip.
  • FixedCode tab keeps your tree/preview split1 PR
    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 outside a git repo) could silently overwrite the saved split with a garbage value, so coming back brought the panes up at the wrong size.

Mobile

  • AddedUnfolded foldables and tablets get a two-pane layout1 PR
    On a wide touch screen — a Galaxy Z Fold unfolded, an iPad, an Android tablet — kolu used to fall back to the mouse-driven desktop canvas, clumsy to drive with a finger, because the layout was chosen on viewport width alone. kolu now treats a roomy finger-driven device as its own form factor with a two-pane layout: a persistent terminal rail down one side, the active terminal filling the rest, with the same touch-sized controls as the phone. Phones keep the single fullscreen tile; a real mouse still gets the canvas. The split keys off the pointer, not just the width.
  • FixedMobile key bar types into the focused split1 PR
    On a phone or tablet, the on-screen helper bar’s keys — Esc, Tab, the arrows, Ctrl-C, /, 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 where your typed characters already do.

Quickstart

  • ChangedWelcome moments grow up — state-aware rows that act and link to docs2 PRs
    The empty-canvas welcome card is no longer three static rows pointing at the home page. It is a prioritized list (Pin it · Reach it anywhere · Run agents · Search everything · Add a host · Shortcuts): done items collapse into a muted header, the card paints the next three still-open moments, and each row acts through the real affordance (one-click Install, ⌘Enter for a new terminal, open the unified switcher, open the shortcuts overlay) and carries a Learn more → link to the matching product-docs page. The ambient tip banner can do the same when a tip has a docs page — a trailing Docs → opens it.
  • AddedThe kolu binary grows subcommands — `kolu web` names today's server1 PR
    kolu web now names what bare kolu has always done — the two spellings accept the same flags and boot the same server, so nothing you run today changes. kolu tui (a terminal-side canvas) is reserved: it exits with a clear not-shipped-yet message pointing at the plan, instead of silently booting the web server. kolu --help now lists the subcommands.
  • Changedkolu opens far faster on first load — the app bundle is served compressed1 PR
    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. 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 where it hurt worst: opening kolu from your phone or over Tailscale. Repeat opens were already instant and are unchanged.

MCP

  • Added`kolu mcp` — drive your terminals from a coding agent1 PR
    kolu mcp connects a coding agent — Claude Code, Codex, any MCP host — to this host’s terminals over MCP (stdio), no browser involved. One claude mcp add kolu -- kolu mcp and the agent can create terminals, type into them, wait for output to settle or for another agent to finish or need input, read the screen, and watch which terminals need a human — the same live state the browser canvas shows. --host user@box reaches a machine over ssh. What an agent may touch is a deliberate, reviewed allowlist: creating, driving, and observing terminals are in; administering the background service, session policy, and canvas arrangement are not. See the MCP page.

Clipboard & Files

  • AddedDrag and drop video files onto a terminal1 PR
    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 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 previews in the Code tab, so what you can drop and what you can play back stay in lockstep.
  • ChangedUpload larger files to agents — up to 50 MB1 PR
    The per-file drag-and-drop / paste upload cap is now 50 MB, up from 10 MB. That leaves room for a useful app screen recording while keeping the 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.

Keyboard Shortcuts

  • Added⌘K finds terminals and hosts too2 PRs
    The command palette is now a single search box for everything: type a branch, repo, command, or machine name at the root and jump there (nested command leaves included). Top-level terminals on every connected host (including plain shells; splits stay under their parent) rank above hosts above commands. Empty root opens with Recent, Hosts, a Terminals section, then the usual command sections. Recent follows a client-side visit trail (when you activate a tile), ranked by max(visit, server activity) so a visit competes with background server activity rather than always losing to it — including across hosts, with host chips on foreign rows; Ctrl+Tab uses the same trail on the active host (survives reload). Recent hides the active terminal, so ⌘K then Enter jumps to the previous one. The palette is width-capped; kind tags (term/host/cmd) appear only when you type a cross-kind search. Rows reuse Dock state pips and Dock identity (intent headline when set); cross-host hits switch + activate in one step. ⌘⇧K opens Terminals as a flat multi-host list under host headers; the dock search icon opens Terminals › this host; ⌘⇧H opens the same Hosts rows as root. Hover only follows a real pointer move. See The Switcher.
  • AddedMaximize a tile without the mouse1 PR
    Toggle a tile between the freeform canvas and maximized view straight from the keyboard with Cmd/Ctrl+Shift+M, or run “Maximize terminal” from the command palette. The command’s label flips to “Restore canvas” once you’re maximized, so it always names what the next click does.
  • AddedCmd/Ctrl+F finds in whatever you're focused on1 PR
    Find-in-terminal is now confined to the terminal — the one place kolu’s own 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 viewer or diff, rendered Markdown, an .html preview, the palette, the workspace switcher — and it hands off to your browser’s own find-in-page, which reaches inside the preview frame too. With focus in a terminal the chord still opens kolu’s find bar.

Theming

  • ChangedChoose how new terminals get their theme — inherit or shuffle — and steer the shuffle1 PR
    The Settings “Shuffle theme” on/off toggle is now two settings. New terminal theme picks the strategy for a fresh terminal: Inherit copies the active terminal’s theme, or Shuffle (default) auto-picks a background perceptually distinct from your other open terminals. Shuffle behaviour then steers every shuffle: Auto (default) keeps picks in the app’s current light or dark family so a tile can’t come up as a jarring bright panel in a dark workspace; Dark / Light force one family; Random spans the whole catalogue. Your old setting carries over automatically.
  • FixedA terminal on the default theme keeps its theme pill when it loses focus1 PR
    Each tile’s title bar carries a small theme pill. A tile on the default theme (Tomorrow Night, none 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. Every tile now shows its effective theme name whether focused or not, so the pill stays put.

Power Features

  • AddedLink straight to a terminal, file, or host — deep links4 PRs
    Any kolu view now has a URL you can link to. A #/t/<host>/<id> link opens that terminal focused; #/t/<host>/<id>/code?path=…&line=… lands the Code tab on a file (at a line); #/t/<host>/<id>/inspector opens the Inspector; #/h/<host> switches machines; #/settings opens settings. Click one from an orchestrator board, an issue, a Slack message, or a bookmark and the installed app focuses the already-open window to it (no PWA? any browser opens it fresh). They even work from inside a Code-tab HTML preview: a previewed dashboard’s #/… links route the surrounding app — click a lane pill, land on that agent’s terminal. A link to a split lands the sub-terminal live, not just its parent tile. Links are view-only by law — the worst a hostile link can do is change what you’re looking at, never create, kill, write, or type into a terminal — and durable: the link stays in the bar and opens fresh anywhere, while a reload restores your session to the view you’d reached (so a stale link can’t yank you to an old host) and the back button never replays a link you already followed (it quietly restores the URL and leaves your view alone). And a link to a terminal that’s still loading on a cold boot waits for the host to settle instead of falsely reporting it gone.
  • AddedExport an agent session as a chat log or a full transcript1 PR
    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. Both share the same high-contrast design — a patterned page, hard role colours, dark code blocks, and ↑/↓ controls for jumping between prompts. The chat log stays small by omitting hidden tool payloads.
  • FixedThe workspace switcher re-floats a terminal when a new agent starts after the last one finished1 PR
    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 there didn’t bump its recency, so the terminal you’d just put back to work stayed buried. It also reset every terminal’s recency to “just now” on a server restart, scrambling the order after a redeploy. A precise signal now replaces the over-broad rule — a new agent identity bumps recency on kolu’s own clock, while re-observing the same session on restart or wake does not.

Surface

  • AddedProcedures can declare their error union — typed failures end to end1 PR
    A defineSurface procedure can now declare errors: { CODE: { data } } (oRPC contract-level typed errors): the handler mints the declared code via typed opts.errors constructors, every hop preserves it — a SurfaceMap’s keyed proxy and mirrorRemoteSurface’s forwarders included — and the client narrows the rejection with isDefinedError/safe to a typed { code, data } instead of parsing message prose or receiving an opaque INTERNAL_SERVER_ERROR. Undeclared throws still cross as INTERNAL_SERVER_ERROR — the fail-fast channel is untouched; declare exactly what a caller can act on. The daemon-supervisor’s endpoint also gained the honest verdict this exists for: a proven contract skew now reports as its own incompatible state carrying both versions, never collapsed into dead.
  • AddedAgents can drive any @kolu/surface over MCP1 PR
    The new sibling package @kolu/surface-mcp re-exposes any @kolu/surface spec to an MCP host — mapping each cell/stream/event to a subscribable resource and each procedure to a tool under a default-deny allowlist — so a coding agent can read and drive a live surface as structured tool calls. See exposing a surface to agents.
  • ChangedA stdio agent now exits when its link ends1 PR
    A subprocess agent served with serveOverStdio’s default transport used to rely on the app to terminate the process after the link died — and any live timer or watcher silently kept a dead agent alive forever, piling orphaned --stdio processes onto remote hosts (one per reconnect). The framework now owns that lifetime: when the link ends, the agent exits — cleanly (0) for a normal disconnect from either direction, 1 for a genuine transport error — so an orphaned agent can no longer exist. Serving over an explicit transport (tests, sockets, embedded peers) is unchanged: the caller keeps owning the process.
  • ChangedClosing a unix-socket listener now disconnects its connected clients1 PR
    Closing a serveOverUnixSocket listener used to only stop new connections — clients already attached kept being served by the “closed” host, their subscriptions and timers running until the process died. close() now runs the full teardown: stop accepting, disconnect every established peer (each connection’s subscriptions finalize and timers clear), remove the socket file. A daemon shutting down through the standard skeleton drops its clients cleanly instead of leaving them attached to a ghost.
  • ChangedAn abandoned remote session no longer keeps its host process alive1 PR
    The same cure on the client side: a makeSession session whose consumer dropped it without destroy() used to pin its host’s event loop with a ref’d reconnect timer — a process redialing a gone endpoint forever, immortal, respawning ssh children on every attempt. The session’s internal timers are now unref()’d, so a session never keeps an otherwise-finished process running by itself; a held session in a living process reconnects exactly as before, and the one timeout that settles a caller’s pending pin() still always fires. One consequence to know: a wait derived purely from session state (a pump parked on ClientCursor.next() across a reconnect gap) is not a process hold either — a process with nothing else left exits instead of waiting forever.
  • ChangedA daemon bin now exits when its tenure ends1 PR
    The same cure at the daemon layer: a binary built on daemonMain used to map the resolved DaemonExit to a process exit code itself — and a live resource or timer (a PTY child, a sampler interval) silently kept a finished daemon alive if a bin forgot that homework. The new daemonProcessMain({ name, run }) owns the process’s life instead: run the daemon to completion, exit with the right code, narrate and exit 1 on a crash — a lingering daemon is unspellable for a bin that adopts it. daemonMain’s contract is unchanged — it still resolves a value and never exits, so tests drive the lifecycle in-process — and it now arms its shutdown triggers before announcing readiness, so a supervisor that signals on “ready” can no longer catch the daemon unarmed. The kaval and padi binaries use the shared bin half.
  • Heads-upA remote agent on localhost spawns with a composed env, never the caller's1 PR
    The sshConnector / dialAgentOnce localhost arm used to spawn the agent binary with no env override, so Node inherited the caller’s whole process.env — the one spawn path an ssh boundary doesn’t scrub. Both now take a required localEnv: the complete env a "localhost" dial spawns with, so a locally-hosted agent can never silently inherit ambient identity vars or secrets. Omitting it is a compile error, not a review catch. surface-remote stays policy-free — the consumer composes the env (kolu from a fixed allowlist, kolu-pty’s composeSpawnEnv); a real ssh host ignores it (that child is the LOCAL ssh client, which legitimately inherits for SSH_AUTH_SOCK / ~/.ssh). Migration: add localEnv to every sshConnector / dialAgentOnce call.

Troubleshooting

  • AddedSee every process's health in the chrome bar6 PRs
    The chrome bar’s identity rail shows kolu’s live processes — kolu-server, this browser, and the kaval and padi daemons — each with its build, a health dot, uptime, and live memory, so a process creeping upward is visible at a glance. The memory reading stays honest: a padi that’s mid-reconnect reads (absent) rather than freezing its last figure, so a number on screen always reflects a live process. It prints the shared deploy commit once (a muted ≡ when the client matches the server, spelled out only when a stale bundle disagrees), and each daemon’s details open on click — memory, socket, and a lifetime row that reads forever for a production daemon or bound-to-run for a test one.
  • FixedReturning to kolu no longer flashes 'Disconnected' for a moment1 PR
    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. The connection was usually fine; the liveness watchdog mistook “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 a sleep interrupted, and the “Disconnected” overlay waits a beat before appearing, so a sub-second reconnect never flashes the alarm.
  • FixedThe 'App updated' prompt no longer loops forever1 PR
    The “App updated — Reload to apply” card could reappear immediately after every click, leaving the client stuck a build behind no matter how many times you clicked. The cause: kolu baked the build version into a JavaScript bundle the browser caches for a year, so a deploy that changed only docs 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.
  • FixedThe packaged kolu honours KOLU_STATE_DIR again1 PR
    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 machine 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 honours 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.
  • Fixedkolu no longer freezes in the browser when a git command hangs1 PR
    Open kolu in a few worktrees and the whole web UI could go unresponsive — the page stops answering, terminals freeze — until someone restarts the server by hand. The cause: kolu resolved each repo’s git directory with a blocking git rev-parse on the server’s single work loop, so if that call ever wedged (a stuck filesystem, a contended repo) it froze the entire server for as long as it hung. kolu now does that lookup off the work loop with a hard time limit, so a slow or stuck git command can never freeze the server.

Before you update

  • Heads-upCross-site pages can no longer drive kolu's terminal RPC1 PR
    kolu’s terminal control plane is unauthenticated, so any web page you visited could quietly reach your local kolu and drive it — create terminals, type into them, kill them all — without you knowing. Both its transports (a WebSocket and plain HTTP) were reachable cross-site (a WebSocket upgrade needs no CORS preflight; a cross-site form can POST into an HTTP procedure), and binding to localhost didn’t help because the malicious page runs in your own browser. kolu now checks the browser’s Origin on every request, on both transports, and rejects cross-site ones; the kolu UI talking to itself and non-browser clients (a CLI, curl) are unaffected. If you front kolu with a reverse proxy or tailscale serve whose address differs from what kolu sees, allow it through with the new KOLU_ALLOWED_ORIGINS env var.
  • Heads-upGitHub Enterprise repos no longer show a PR pill (for now)1 PR
    To stop kolu mistaking non-GitHub repos for GitHub ones, 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, tracked alongside the broader multi-forge work. Everything on github.com is unaffected.

Release

v1.0.0

11changes
nix run github:juspay/kolu/v1.0.0
Added
9
Heads-up
2

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.

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.