All releases

main, unreleased30 min read

Everything since v0.7.0

Unreleased work on main: session layouts that come back after a daemon restart, sessions and panes on other machines, agents you can watch and message, one layout for every client, an interface that answers the mouse, and a large cut in memory use.

GGGaurav Gosain

1,805 commits since v0.7.0, across 1,817 files, up to 22 September 2026. None of it is in a tagged release yet. It will ship as the next one.

The page is grouped by what you notice, most important first. If you already run tuios, read Before you upgrade first.

In short

Before you upgrade

Most of these need nothing from you.

  • Restart the daemon. The wire protocol is version 3, and version 2 is refused with a message that says which side is newer. (A message type added after v0.7.0 shifted every type number without a version change, so mixed builds misread each other.) Run tuios kill-server and start tuios again. This ends every program running in your panes. The layout, window names and working directories come back, each pane with a new shell; scrollback does not. The JSON verb protocol stays at version 1.
  • New defaults on a fresh install. A bare tuios starts or joins the daemon (startup.daemon = true), starts tiled (startup.tiled = true), and draws window controls as dots on the left of the title bar. An existing config file keeps what it says: the [startup] booleans are only read from the file, so a machine that ran standalone and floating still does. The two window-button options are strings and do reach a config that never set them. tuios --standalone or TUIOS_NO_DAEMON=1 runs without the daemon. If the daemon will not start, a bare tuios runs standalone for that run and says so.
  • The SSH server checks keys. tuios ssh used to give a shell to every connection. It now admits the public keys in ~/.config/tuios/authorized_keys, or in ~/.ssh/authorized_keys when the first file is absent. --authorized-keys names another file. With no keys, a loopback bind still runs unauthenticated and prints a warning, and any other address is refused. --no-auth opens it on a network you trust. A keys file that cannot be read, does not parse, or holds no key stops startup.
  • Unnamed SSH connections. A connection that names no session, and whose username maps to none, now gets a session called ssh-session when several exist. It used to get whichever session was listed first. With exactly one session, it still gets that one.
  • Hooks run in the daemon. after-new-window, after-close-window, after-focus-change, after-workspace-switch and after-agent-state now run in the daemon, with the daemon's environment. They fire with nobody attached, and once rather than once per client. after-attach, after-detach, after-resize and after-layout-change stay in the client, and after-detach now also fires when an SSH connection or a browser tab goes away.
  • Window-mode keys. Digits 1 to 4 select windows. They used to snap a window to a corner, which is now ctrl+b L then 1 to 4. A config still carrying the default corner-snap bindings is migrated on load. Bindings you changed are left alone. With tiling on, h and l now focus the pane on that side instead of doing nothing. In the layout picker, delete moved from d to ctrl+d, so typing a filter no longer deletes a layout.
  • Right-click. In window-management mode, a right-click on a pane opens the pane menu and a right-drag resizes. It used to only resize. In terminal mode the right button belongs to the pane: ctrl or shift with right-click opens the menu, and appearance.right_click_opens_menu = true makes a plain right-click open it too. A program that asked for the mouse always keeps the plain right button.
  • Frame rate. appearance.max_fps now tops out at 120, the renderer's own ceiling. Values up to 240 were accepted before and changed nothing on screen.
  • Selection colours. The selection, search matches and copy-mode cursor are now settings under [appearance.selection], with new defaults: a neutral grey selection, amber search matches, and a cyan copy-mode cursor. Selected text is no longer bold. appearance.selection.bold = true brings that back.
  • Sidebar settings moved from flat appearance.sidebar_* keys into an [appearance.sidebar] table. The old keys are still read, the table wins where both are set, and the file is not rewritten in the old spelling.
  • Removed. appearance.separator_style, which parsed and did nothing. The colorshift screen saver effect: a config that names it reads as random.

Layouts come back after a restart

The daemon saves each session's structure under $XDG_STATE_HOME/tuios/sessions and restores it when it starts. This is on by default. A detach loses nothing, because the daemon keeps the shells running. A daemon restart or a reboot ends them: what comes back is the layout, not the programs.

  • Each window comes back with its workspace, its place in the layout, its name and its working directory. The session keeps its display name and accent.
  • Each shell is new, started in the old directory. Scrollback and running programs are not saved.
  • Restored shells get TUIOS_RESTORED=1, and tuios ls marks the session as restored until you first attach.
  • A window whose shell will not start is dropped rather than restored empty.
  • A state file tuios cannot read is moved aside for two weeks instead of stopping the daemon.
  • Working directories now come back on macOS too. The daemon used to read them from /proc, which macOS does not have.

tuios daemon --no-restore turns restore off. tuios resurrect lists what is saved, or brings one session back by name.

The daemon is also easier to reach. A bare tuios uses it by default (see above). tuios attach starts the daemon instead of refusing. tuios ls with no daemon running lists the sessions saved on disk, marked as saved, and exits 3, so a script can tell a stopped daemon from a running one that holds nothing.

Daemon panes now honour two settings they used to ignore: appearance.scrollback_lines (the daemon kept 10,000 lines per pane whatever the file said) and appearance.preferred_shell.

Other machines

tuios can now work across machines. Every link is one ssh process per host, run with BatchMode=yes, so you need to be able to ssh to the machine without typing anything. See Sessions for the full model.

Adding a machine

tuios hosts add build gaurav@buildbox   # writes [hosts.build] in config.toml
tuios hosts test build                  # dials it now and says what it found
tuios hosts remove build
tuios hosts                             # every link and its state
  • The daemon follows the config file, so none of these needs a restart. The settings page has a Hosts section that does the same.
  • An address is anything ssh understands. Your ~/.ssh/config aliases are offered in shell completion and on the settings page.
  • tuios hosts tailnet lists the machines on your tailnet and which ones it would offer. tuios hosts add NAME --tailnet takes the address from the machine of that name. It asks the tailscaled already running, with no root and no auth key.
  • The link finds tuios on the far machine by itself: on the PATH, in the usual install paths, then through the login shell. tuios hosts test prints where it looked when it cannot find it.
  • A link that drops is dialled again with a backoff. tuios hosts shows each link as up, connecting, reconnecting, unreachable, no_daemon, no_tuios or incompatible, with how many times it dropped and why.

Sessions on another machine

tuios attach --host build api   # attach the session api on build
tuios new --host build          # create a session on build and attach it

The session is drawn by this client, with this machine's theme, config and prefix key. Nothing is nested. If the link drops, the pane stays on screen, the dock says it is reconnecting, and the client tries again for three minutes before it returns to the session it left on this machine. --ssh runs the tuios on the host inside this one instead, for a host whose tuios is too old.

In the rail, sessions are grouped under their machine. Enter on a session under a host attaches it, and enter on the + beside a host creates one there. A machine's header shows how many of its sessions want a person, and each row shows what its agents are doing. A machine that is not answering is muted and its rows cannot be clicked, because they are a cached listing.

A pane on another machine

tuios new-window deploy --host build

The window belongs to this session and is laid out here. Only its process runs on build. The title bar says build:deploy, and the rail row names the machine, because the same typed line does different things on different machines. The rail's file section and agent detection work for these panes. The pane ends when the link does.

Global sessions

Once a second machine is reachable, the rail shows a global group above the machines. A session in it holds panes from several machines, and every way of making a window there asks which machine to run it on. Anywhere else, a new window opens on the session's machine without asking.

tuios new deploy --global

appearance.global_session = false hides the group. Making a new session (N in window mode, the new + at the start of the dock's session strip, or the command palette) asks which machine when more than one is reachable.

Addressing another machine from the CLI

Verbs take a host-qualified target: -s build:api for a session, -w build:api:0 for a window. local: addresses a local session whose name contains a colon. list-windows, capture-pane, send-keys, send-text, wait-for, focus-window, kill-session, session-info, get-agent-state, the agent mail verbs and the stash all take it. Every pane gets TUIOS_HOST, the hostname of the machine it runs on.

Agents

The rail shows what your agents are doing

The rail has an agents section. Every pane running a recognised coding agent is listed with its state, ordered by who needs you: errored, then waiting for input, then finished but not yet seen, then working. A finished agent you have looked at moves down the list. Rows show the harness and the last thing the agent said it was doing. The section header counts blocked and finished panes, and clicking the count filters the list. In the command palette, @n finds panes that need input and @w finds working ones, across every session.

When a state changes, tuios can tell you: a dock message, an OSC 9 notification (which reaches you over SSH), and an optional sound. The [notifications.agent] table has per-state switches, a settle window so a flapping state does not spam, and quiet hours. A blocked agent's alert now includes the question it asked. The after-agent-state hook receives the transition in environment variables, never in argv, so an agent's text cannot become shell arguments.

Detection

  • 22 agent CLIs are described by TOML manifests. tuios explain-agent-detect shows why a pane was or was not recognised.
  • A process is identified by what it calls itself: its name, argv[0], its executable, or the script an interpreter runs. A directory called claude or codex is no longer an agent. An agent started through sh -c, timeout, npx, uvx, mise or nix is found.
  • State comes from six ranked sources: the agent's own report, its transcript, escape codes (including window-title rules, such as Codex's "Action Required"), screen rules, the foreground process, and last of all an output stall. A lower source never overwrites a higher one that is still fresh.
  • The transcript reader reads only the last 128KB, so a 151MB transcript costs 652 microseconds.
  • A state clears when the agent exits back to its shell. Agents in panes on other machines are detected too.
  • get-agent-state and list-agents report a confidence and whether the pane needs you.

Agents can message each other, and you

tuios list-agents                      # every agent pane and its state
tuios send-agent-message -w build 'rebased onto main, please retest'
tuios read-agent-messages              # your own inbox
tuios ask-agent -w review 'which test is flaky?'   # ask and wait
  • A message is at most 8KB, with a subject of up to 120 characters and up to eight attachments, sent as paths, never as bytes. A session's mailbox holds 256 messages or 512KB. A sender gets a burst of ten, then thirty messages a minute. Mail lives in memory and ends with the daemon.
  • --reply-to <id> answers a message and joins its thread. read-agent-messages --thread and wait-for agent-message --thread filter by thread.
  • ask-agent refuses a question that would close a loop of agents waiting on each other, not only a question to yourself. Loop detection needs --from "$TUIOS_PANE_ID".
  • The person at the keyboard has an inbox called human. An agent writes to it with send-agent-message -w human. The mail overlay opens on ctrl+b M, from the palette, or with i on a rail row, and you can reply from it. list-agents reports human_unread.
  • tuios stash put <file> copies a file into a store owned by the session and prints its path, ready for --attach. The file stays until the session ends, even if the sender deletes the original. The limits are 16MB per file and 256MB per session. stash get copies a file back out, also across a link.
  • Mail from another machine is marked as such, and a session holds at most 32 unread messages and 32 notices from links.

One prompt, several worktrees

tuios worktree new fix/retry          # a git worktree and a session in it
tuios fan 3 --agent claude 'Add a retry with backoff to the HTTP client.'
tuios fan keep api-fan-add-a-retry-with-2

tuios worktree new creates a worktree under the tuios data directory and a session in it named <repo>-<branch>. worktree ls, worktree diff and worktree rm manage them. rm refuses a worktree with uncommitted changes unless you pass --stash or --force, and it never deletes a branch. tuios fan creates several worktrees, starts the agent in each, and types the prompt once each agent is ready. fan keep removes the others. The rail groups worktree sessions under their repository.

One layout for every client

Two clients on one session used to disagree about pane sizes, because each subtracted its own rail and dock and retiled. Now each client reports how much space it reserves on each edge, and the daemon takes the largest reserve per edge and hands every client one geometry. A client with less chrome draws a blank band instead of moving the panes.

The same rule now covers the rest of the layout:

  • A zoom belongs to the session, so every client draws it.
  • The master-stack ratio is kept per workspace and shared.
  • Where the scrolling layout's strip is scrolled to is shared, so a client that scrolls takes the others with it, and a client that joins lands where the session is looking.
  • A workspace you arranged by hand stays arranged when another client visits it for the first time.
  • A BSP tree built on one client is adopted by the others.
  • Session display names and accent colours are kept by the daemon, so a rename reaches every client and survives a reattach.
  • The rail's width is yours: switching to another session no longer changes it.

Fixes found along the way: broadcasts now reach a client in the order they were made, two clients resizing one pane can no longer leave its emulator at a size nobody draws, and opening or closing a pane no longer leaves a neighbour at its old size on another client. Reattaching to a full-screen program such as top no longer draws its rows one blank line apart (#123).

The mouse

  • Every overlay row highlights under the pointer and acts on a click: settings, the command palette, the launcher, the theme, glyph and effect pickers, the session and workspace lists, the layout menu, and dialogs. The keybind manager's rows select but do not run, and the help panel only scrolls.
  • Right-click opens a menu for what is under the pointer: a pane, a dock segment, a workspace pill, the dock, the desktop, a session row, or a machine heading in the rail. Every row is an action from the keybind registry, and the key it shows comes from the registry too.
  • Alt and the left button move a pane from anywhere in its content. Alt and the right button resize it.
  • The left button selects text: double-click for a word, triple-click for a line.
  • Dragging a tiled pane by its title bar moves it with the pointer in all three layouts, and it swaps on release.
  • A click that only focuses a pane no longer resizes it, and a drag that puts a pane back where it was resizes nothing, so your shell no longer reprints its prompt.
  • Sideways trackpad drift during a vertical scroll no longer jumps lists or walks the scrolling layout. appearance.niri_scroll_cells (default 8) sets how far one scroll event moves the strip.
  • Links in a pane can be clicked. appearance.links sets which: off, marked for OSC 8 links only, or all (the default), which adds bare URLs, including one wrapped across rows. The link under the pointer is underlined, the pointer turns into a hand, and a label shows the target. A click opens it. A file:// link opens in a new pane running $EDITOR, on the machine the panes run on, and a directory opens in the rail's file view. An http link opens a browser only for a local client. Under tuios ssh or tuios-web it copies the URL to your clipboard instead.
  • A shake of the mouse can toggle the spotlight, with spotlight.shake = true.

The dock is now a status line without powerline caps. Its contents are a [dock] table: left, center and right are ordered lists of component names, [dock.clock] sets the time format, and a [dock.custom.NAME] entry runs a shell command and shows what it prints. A command that hangs is killed with its whole process tree at its timeout. Workspace pills can be dragged into a display order, which does not renumber the workspaces.

The rail

The rail was rebuilt from a collapsible tree into flat sections. Which sections it draws, in what order, and how tall each is, is one string, appearance.sidebar.sections, with an editor in the settings panel. spacer may repeat. Drag the line above a pinned section to move the split.

  • Sessions. Each session has its own colour, derived from its name, so it is stable across clients and restarts. A session with a generated name is labelled by its directory, and any session shows its git branch when there is room. Sessions and workspaces can be renamed from one dialog, and session rows can be dragged into an order. appearance.session_border = true tints every pane border with the session's colour.
  • Files. The working directory of the focused pane, following the pane as focus moves, with nerd font icons coloured by file type. appearance.sidebar.folder_click decides whether clicking a folder navigates the listing, sends a cd to the pane, or both. Six file actions: create, rename, delete, copy, cut and paste. Only delete asks first. Delete moves the file to the freedesktop trash in $XDG_DATA_HOME/Trash, and appearance.sidebar.file_delete = "permanent" skips it. A file on another filesystem is refused, and on Windows a delete is permanent and says so. Paste never overwrites: a name that exists gains a number.
  • Git. Add git to the sections string to see the focused pane's repository, branch, how far it is ahead of and behind its upstream, and, with appearance.git_dirty (on by default), how many files are changed.
  • Agents, described above.

A program in a pane can print an OSC 7 escape claiming any directory. When the kernel says the pane's shell is somewhere else, the file actions are switched off and the rail says "read only: wrong folder", so a pane cannot aim a delete at a folder it is not in. This now also works for daemon panes and on macOS.

The rail answers the mouse throughout: rows highlight on hover, a truncated row scrolls so you can read it, the collapsed strip pops a label beside the row under the pointer, and the rail's edge can be dragged to change its width.

Tiling and zoom

There are three tilers: BSP, master-stack, and a scrolling layout in the style of niri, which puts columns on an endless horizontal strip. All three honour appearance.gap. A pane can be floated out of a tiled layout on its own, or pinned so it follows you across workspaces. "All windows" in the command palette searches every window in every workspace and jumps to the one you pick.

Windows created in a daemon session now tile. Before, only windows the attached client made itself were placed, so daemon-created windows stacked on each other or floated over the tiles. BSP and master-stack tiles no longer overlap when panes shrink.

win 1
~ $
win 2
~ $
win 3
~ $
TILING3 windows
Open a few windows in the before position: they land at the same origin and pile up, though the dock still reads TILING. Flip to now and the same windows split and tile. Placement used to run only for windows the attached client created; daemon-created windows now tile too.
  • Zoom slides into place instead of cutting (appearance.zoom_animation). appearance.zoom_size below 100 zooms like a camera: the layout stays on screen around the zoomed pane, with its neighbours running off the edges. Moving focus hands the zoom to the pane you land on (appearance.zoom_follows_focus). A tiled pane's title bar has a zoom button (appearance.window_button_zoom).
  • Scrolling layout. appearance.scroll_column_max (20 to 100, default 90) lets a column fill the screen. Clicking, hovering with focus-follows-mouse, or moving focus with the keyboard brings the whole column on screen (appearance.niri_click_reveals, appearance.niri_hover_reveals). Column widths and stacks survive a session switch (the post), and the strip keeps its scroll position across a workspace switch and a retile.
  • Percentage resize. ctrl+b L then 5 to 9 sets the focused pane's width to 50 to 90 percent of its region, and shift with 5 to 9 sets its height.
  • New windows open where you are. A new window starts in the focused pane's directory. appearance.new_window_inherit_cwd = false turns this off.
  • Popups. tuios popup -- fzf runs a command in a floating pane centred over the layout, and closes it when the command exits. --width and --height take cells or percentages (default 80% and 60%). Esc in window mode closes the focused popup. This is how fzf, gum and other pickers become tuios overlays.
  • Turning tiling on after floating brings back the arrangement you had. Floating windows are no longer drawn over the clock, the log viewer or other overlays, and raising one floating window no longer reshuffles the others.

Resize was rebuilt (the post): dragging a shared border moves only the two panes that share it, an untouched split no longer drifts, and a drag no longer eases toward a target that has moved.

Keys

  • One registry. Every key tuios answers to is in the keybind registry, including the palette on ctrl+p and the launcher on alt+space, which sit in a [keybindings.global] section consulted in every mode. Any action can now be bound there.
  • Keybind manager on ctrl+b k: what is bound, where tuios conflicts with itself, where it conflicts with the pane's program, and a Record tab that captures the next key you press. The same analysis runs as tuios keybinds doctor and tuios keybinds explain <key>. Each finding says how it knows: certain from tuios routing, observed from a live pane, or reference from a list of program defaults.
  • Giving a key back. ctrl+x on a row, or tuios keybinds free <key>, removes that key from every action in every scope so the program in the pane receives it. tuios keybinds unbind removes one action from one key.
  • Launcher on alt+space or ctrl+b a: anything on $PATH, fuzzy-matched and ranked by frecency, with names and icons from desktop entries where the platform has them. Enter runs the selection in a new pane. Tab types it into the focused shell so you can add arguments. All four search boxes in tuios now share one fuzzy matcher.
  • Prefix repeat. After a command worth repeating, the prefix stays armed for half a second, so ctrl+b then left left left walks three panes. appearance.prefix_repeat_time sets the window, and 0 turns it off. New prefix bindings: the arrows walk panes, ( and ) switch sessions, and a opens the launcher. These work on macOS with no terminal settings.
  • New window-mode keys. N makes a new session, and b toggles the spotlight.
  • Entering terminal mode on focus. appearance.auto_enter_terminal_on_focus can be off (the default), targeted (focus by selection and arrows) or all (including Tab).
  • macOS. An opt+ binding on a key that composes no character, such as opt+esc, never fired. It does now, so Option+Escape leaves terminal mode as documented. When Option+Left or Option+Right arrives as the readline word motions, tuios says so and names the terminal setting to change. The default config no longer warns about its own bindings.
  • Keys typed within 150ms of entering terminal mode are no longer dropped.

Look and feel

  • Themes. tuios import-theme converts a kitty, ghostty, alacritty or wezterm colour scheme, detecting the format from the file. tuios list-themes lists the 342 built-in themes plus your themes directory, and an imported theme works without a restart. Switching theme recolours every open pane in place. A theme can describe itself: each colour is measured against the background, and colours under the contrast floor are named.
  • Chrome colours. A theme file may carry a chrome object with accent, accent_bright, success, warning, error and info, so the interface can use an amber accent without turning every blue in every pane amber. chrome.surface sets the colour dialogs are drawn on, and the rest of the chrome is derived from it at readable contrast.
  • Glyph sets. appearance.glyphs picks the shapes: borders, window controls, rail marks, folders. Four ship (default, unicode, heavy, ascii), each glyph with its own ASCII fallback, and a custom set is a file that can inherit from a built-in. tuios list-glyphs lists them, and the settings panel has a picker with a preview.
  • Colour picker. Colour settings open a picker with a hue strip, a shades grid, HSL and RGB sliders, a hex field, a harmony wheel, and the theme's own colours as slots that follow the theme. Settings rows show a swatch of the colour in force. Numeric settings open an editor with a slider and a typed value.
  • Zen mode. appearance.zen_mode hides unfocused borders always, or only while the mouse is still. Content never shifts.
  • Window controls can sit on the left, and a dots style draws them as traffic lights. The scrollbar has two styles: a single stroke in the pane's own ink, or a track with a half-cell thumb.
  • Copy flash. A copy sweeps a band of light over the copied text. appearance.selection.flash turns it off, and flash_ms, flash_color and flash_style (diagonal, diagonal-reverse, horizontal, vertical) tune it. Only the background changes, so the text stays readable.
  • Spotlight. b in window mode dims everything outside a circle around the mouse. The [spotlight] table sets the radius, how dark the rest goes (dim, default 75), and whether it follows the mouse or the cursor.
  • Screen saver. Off until you set screensaver.enabled. It starts after screensaver.idle_minutes idle, with 36 effects ported from terminaltexteffects (screensaver.effect, or random). The picker previews each effect over your real screen. Effects now run at the right speed at any frame rate, and on an unthemed session the saver asks the terminal for its palette instead of guessing xterm's.
  • Keycast. tuios can draw your keypresses in the bottom right as you type, with repeats coalesced, for screen recordings. It toggles from the command palette and the setting persists.
zsh
~ $
click, then type
Click the box and type. Keys are drawn the way the overlay draws them: modifiers prefix the key, a held or repeated key collapses to a count, and older chips fade as new ones arrive. Focus leaves and the overlay clears. Only keys that would scroll the page are swallowed while the box has focus.

Graphics and screenshots

tuios screenshot writes a pane to a file as png, svg, ansi, html or txt. It runs in the daemon and reads the emulator's cell grid, so it works on a detached session from a script: tuios screenshot -w build. On macOS it now finds your fonts without fontconfig, including fonts inside .ttc collections, so Nerd Font icons draw instead of boxes.

  • Kitty graphics over SSH. Passthrough used to be decided by asking the server's terminal, and a server has none, so graphics were off for every SSH client. The client's capabilities now come from the SSH connection: TERM, the forwarded environment, and the pixel size it reports.
  • Animation. The kitty animation protocol, dropped in v0.7.0, is implemented. Animated images send only the changed regions, which took one measured page from 330MB forwarded in 21.6 seconds at 17fps to 22MB at 73fps. A flood of graphics is paced so it cannot starve the keyboard.
  • Unicode placeholders. Images drawn with placeholder cells, such as those in glow's pager, now show inside a pane and scroll and clip with the text. They need a host that supports placeholders (kitty, Ghostty, WezTerm), so not tuios-web. appearance.kitty_placeholders forces the choice.
  • Covered images are cropped. A window over part of an image now cuts that part away instead of hiding the whole image. An image no longer draws over the rail or the dock, and an image in a pane's last rows no longer scrolls the whole screen up.
  • Bounded memory. An animated gif under chafa grew tuios to 5.3GB in two minutes. The same run now stays at 189MB.

The web client

  • Text shaping. The web client can use sip's WebGL renderer with HarfBuzz shaping (opt-in). Arabic letters join, ligatures form, and combining marks land where they belong. Grapheme widths now match a reference terminal, and a freeze where synchronized output was left on is fixed.
  • TLS and phones. tuios-web can serve a LAN address over TLS, and --auto-tls creates and keeps a certificate for the hostname and every interface address. tuios-web cert inspects or replaces it. On a phone, a row of tuios commands sits near your thumb, built from the keybind registry, a long press opens the pane menu, and pane dividers are big enough to grab.
  • Your theme. A browser now shows the theme you picked, and the tab shows tuios instead of "Sip".
  • Settings changed from a browser session apply to that session only, instead of writing the server's config file.
  • tuios-web takes every interface flag tuios takes.
  • The Docker image now includes tuios-web.
per codepoint
اU+0627
لU+0644
سU+0633
لU+0644
اU+0627
مU+0645
U+0020
عU+0639
لU+0644
يU+064A
كU+0643
مU+0645
shaped
السلام عليكم
The top row is 12 codepoints in 12 cells, each glyph isolated in memory order. The bottom row is the same string shaped: letters join, ligatures form, marks land on their base, and Arabic runs right to left. A per-codepoint terminal grid can only draw the top row.

Scripting and control

The daemon speaks 62 verbs over a line-delimited JSON socket, one JSON object per line each way. tuios list-verbs lists them with every error code a verb can return. See the control protocol.

  • Strict parameters. A parameter a verb does not take is refused with the closest spelling and the accepted list, instead of being dropped.
  • Options. One registry holds all 156 settable options. It backs set-option, get-option and list-options, and the settings panel is built from it. set-option refuses a path that does not exist, with suggestions, and validates colour values. It also fixed a bug where a dozen settings, from border style to the leader key, were saved and then read back as defaults on every reload, and a boolean turned off could not be saved.
  • Config reload. Editing config.toml reaches the running client, even with editors like vim that save by renaming. A file with an error is reported on screen and the running config stays.
  • new-session creates a session and its first window from a program, with no client attached.
  • run-command refuses a command name it does not know, instead of reporting success. tuios run-command --list lists the names.
  • Hooks say what happened. tuios list-hooks shows each hook command's run count, last exit code, last run and last error. A failing hook logs its exit code and the tail of its stderr.
  • Daemon logs. The daemon writes errors and basic events to $XDG_STATE_HOME/tuios/daemon.log, so a crash leaves a record. tuios set-config daemon.log_level messages raises the detail without a restart. tuios daemon --pprof serves profiling data.
  • Crash report. A panic in the interface now shows a full-screen report with the build, terminal, layout and last actions. c copies it, and g opens a prefilled issue. It never includes pane contents, titles, paths or session names.
  • tuios update replaces the binary in place, but only when it installed it. It recognises a release download, the install script, go install, Nix, Homebrew and system packages, and for the others it names the installer that owns the binary.
  • Agent skill. tuios --skill prints the recipes for driving tuios from a pane. A test parses every command in it against the real command tree.
  • tuios send-keys no longer pulls a pane you scrolled back to the bottom.
  • tuios stash put and stash get print only the path on stdout, so path=$(tuios stash put FILE) works.

Most of this hardening came from driving the socket the way a program would, which got its own post.

Project tapes and startup

A .tuios.tape in a project directory describes the session for that project: which windows to open, what to run in them, and how to lay them out. When the focused shell changes into a directory that has one, tuios notices, like direnv for terminal layouts.

A tape runs commands, so nothing happens until you have read it. The review dialog shows the whole file, and you choose Run once, Trust and run, or Never. Trust is tied to the file's content hash, so any edit makes it ask again. By default a dock badge and a notification tell you a tape is there. auto_review opens the review dialog on detection. autorun = "auto" runs a tape without asking, but only one you trusted and have not edited since. Running a tape builds a session named after the project and switches to it.

.tuios.tape
zsh
~ $ cd ~/dev/myproject
~/dev/myproject $
Project tape found (untrusted). Press ? to review.
session: myprojecttape: untrusted
Detection is passive. A badge and a notification appear. Nothing runs.

Three startup settings remove the steps every session used to start with: startup.open_default_window, startup.tiled and startup.start_in_terminal_mode. Only tiled is on by default, and only for a fresh install (see Before you upgrade). With all three on, tuios opens tiled with one terminal and your next key goes to the shell. Startup also asks the terminal everything in one round trip, which took the time to the first frame from 566ms to 68ms.

no windows. press the leader key to open one.
NORMALworkspace 1
All three off is a fresh install: an empty workspace. Turn on open_default_window for a floating terminal, add tiled to fill the tile, and start_in_terminal_mode to put the cursor in the shell, where the mode reads TERMINAL and the cursor is solid rather than hollow.

The settings menu now covers everything the config file does, with an inline editor for text values, and changes persist.

The terminal emulator

Behind a ghostty build tag, panes can run on libghostty-vt, the emulation core of the Ghostty terminal, instead of the pure Go emulator. Both backends implement one interface. A differential suite feeds both the same bytes and compares screens, cursors, scrollback, modes and the output a host would receive, after every chunk. The first version of that suite passed while the ghostty backend painted ls on hot pink, which is its own post. Releases ship ghostty builds beside the default ones.

A conformance corpus checks byte strings on a 6x4 screen against literal text dumps, citing esctest and vttest, and can compare against a real tmux. A grammar-based fuzzer feeds the emulator sequences that have broken terminals before. Two more checks need no expected output: an emitted frame must redraw the screen the emulator holds, and the same bytes must draw the same screen however the reads are split. Bugs they found, any of which a program in a pane could hit:

  • The cursor position report had line and column swapped.
  • Under TERM=screen, linux or vt100, box drawing came out as lqk, because SO and SI were unreachable.
  • OSC 8 hyperlinks had their fields swapped and never worked.
  • DECSTBM accepted a scroll region taller than the screen, and the next scroll crashed the daemon and every session in it.
  • A mode map read without its lock, and two captures of one busy pane, could each crash the daemon.
  • NEL, DECALN, DECSTR and DECRQSS were missing, and insert mode was recorded but ignored.
  • ED 3 cleared the visible screen, REP repeated the wrong character, and a wide character cut at a margin left half of itself behind.
  • A window title containing a character from the Dingbats block, which Claude Code puts in its title, ended early and printed the rest into the pane, because a byte inside the character was read as an eight-bit string terminator. Invalid bytes in titles are now dropped.
  • A single shift (SS2, SS3) before a multi-byte character leaked into the next character.
  • Underline and other attributes were dropped on the focused pane's render path.

Where the two backends still disagree, the difference is pinned as a known divergence rather than left to pass quietly.

Faster and smaller

Every number here comes from the commit that made the change.

Speed

  • Idle: with idle shells and the rail open, tuios schedules no timer-driven renders. The render ticker now runs at max_fps, which at the default of 60 took an idle client from 1.0% to 0.6% of a core.
  • Typing a letter into a shell: 18.9us and 177 allocations down to 1.06us and 14. A keystroke over the socket: 4,009ns and four writes down to 967ns and one.
  • Keypress to echo: the median went from 9.75ms to 1.78ms. Typing into one pane while another floods went from an 18 to 28ms median round trip to 1 to 5ms, against tmux's 1.
  • A pane that falls more than 4MiB behind drops to catch-up rate until it recovers, and a busy pane can no longer starve the renderer.
  • Workspace switch: the client asks only for rows it does not have. At four panes that is 11.5MB of encoding down to 0.6MB, and a caught-up switch went from 47.6ms to 2.4ms per pane.
  • Drawing a pane's frame: 6.53ms to 1.58ms when dirty, 3.51ms to 62.7us when cached. The pane renderer stopped re-wrapping content that was already the right width, which was a fifth of every frame.
  • The rail's rows: 82,000ns and 178 allocations per frame down to 288ns and none.
  • Moving the pointer over plain text no longer composes a frame per cell.
  • A truecolor repaint: allocations down from 757,200 to 189,600 per run of the benchmark. Scrolling short lines: 65 to 79 percent less CPU.

Memory

  • Scrollback is stored as text with styles marked where they change. A 175-character line went from 4,864 bytes to 216, and a full 10,000-line history from 48MB to 4.2MB per pane, in the daemon and again in each client.
  • An empty 207x55 pane held 1.7MB before printing anything, 1.3MB of it the screen grid. A row is now allocated only when something is printed on it, and an empty pane's grid holds 28KB. The alternate screen is built on first use.
  • Eight empty panes: heap from 47.5MiB to 14.9MiB.
  • On the ghostty backend, scrollback_lines now holds. The library pruned by bytes first, so a pane asked for 10,000 lines kept about 400 at 207 columns.

Other fixes

  • The last output of a pane, often its prompt, sometimes stayed invisible until the next key. It is now drawn at once.
  • A pane scrolled back stays on the line you stopped at while new output arrives.
  • Ctrl+P opens the palette even when the terminal reports Num Lock in the key event.
  • Quitting a session says "Killed", not "detached", and names the session you were in.
  • A grapheme cluster split across two writes renders as one.
  • Programs in tuios are told graphics forwarding is available, so image tools stop falling back to blocks.
  • Clipboard copy and paste work on GNOME Terminal and other VTE terminals, and over a loopback SSH connection, using the native clipboard tool.
  • Ctrl+D in a pane on another machine closes it at once instead of on the next key.
  • The log viewer's copy keys (E for errors, A for all) now work.
  • Help opened from the leader starts at the top. In help, q closes it outside a search and can be typed inside one.
  • Mouse scroll speed and window title format are configurable, in the file and in the menu.