Terminal Deck 0.15.0

Changelog

Everything below is the real commit history, rewritten so it reads as a story rather than a list of subject lines. The short hashes are the commits themselves. Every release between 0.7.0 and 0.14.0 is on GitHub rather than here — those twelve entries have not been written up yet. 0.1.0 is by far the longest entry because it is the entry in which the application was written; 0.1.1 and 0.1.2 are small on purpose; 0.1.3 and 0.1.4 are where remote access and Windows stopped being claims; 0.1.5 and 0.1.6 are each one thing that had never actually worked for anybody; 0.1.7 is the two that had never worked on Windows; 0.2.0 is mostly subtraction; and 0.3.0 is where the app grew an assistant of its own.

Terminal Deck was built in two long days and an overnight run, in waves of parallel work, so the timestamps say almost nothing useful. Grouping by what changed is more honest than grouping by hour.

0.15.0

28 August 2026 current

Sessions on a server stopped dying, and the copilot became a real chat.

Fixed

  • A session started on a headless server was closing on its own right after it began. It runs normally now. The copilot’s tools had been rerouting every session through the copilot’s own channel; a plain session gets the simple browser-only tools again, and the copilot runs on its own.
  • The desktop no longer says a server “can’t run a copilot” or “has no browser”. Both are true now, and the text said otherwise.

The copilot is a real chat

  • Your messages sit in a box, its replies are plain text, each one has a copy button and a time, and a suggested command comes with a Run button. There is a typing indicator, and you can attach files. The floating browser pill is gone while you are in it.

A server is managed from its own page

  • Restart, Stop and Start the host over the relay — so an offline address no longer breaks it — and Connect GitHub, on the phone and on the desktop. Desktop server management is one place now; the duplicate in Settings is gone.
  • Sign in and out of your Claude, Codex and Gemini accounts from the phone. It was desktop-only before.
  • A connected server shows once, under Servers, instead of in both lists.
  • The usage reading only shows when it is fresh, and hides cleanly when it would be stale. No number left lying, and nothing running in the background to keep it updated.

Android

  • The copilot can be restarted; a login the browser asks for can be typed from the phone; tapping a “needs you” notification opens that session; and you can sign out of an account from the bar.

Removed

  • Chat mode is gone — on the Mac, on Windows and on the phone. A session could be drawn as a conversation instead of as a terminal, read out of the agent’s own transcript file. It never worked smoothly: switching into it often showed nothing, and it went on doing that across versions. The toggle, the pane and the reading over the wire are all removed. A session is a terminal. Talking to the copilot is unchanged — that is its own conversation, on its own screen.

0.6.1

19 August 2026

The button that lets Windows hold a session inside its folder.

Windows can hold a session now

  • On a Mac, a session someone starts from a paired device is held inside the folder you granted and can reach nothing else. On Windows the same mechanism was built and switched off — it needs a one-time administrator permission, and there was no button to ask for it. Settings → Remote → Folders has one: it names the folders it would cover and raises the prompt once.
  • The screen believes the machine rather than the press. Dismiss the prompt and nothing changes, because the state is read back after asking rather than assumed from a click.
  • Until somebody presses it, a session from a device on that PC runs unconfined — and the panel says so, instead of claiming the feature does not exist.

0.6.0

19 August 2026

One pairing decides the copilot, a rented server becomes a machine like any other, and Windows stops being half a product.

The copilot no longer has a connection of its own

  • Approving a device as your own is now the whole authorisation: the copilot is simply there, on the phone and in the browser, with nothing to connect. A guest never sees it — not a disabled tab, not a greyed row. Absent.
  • Gone with it: the six-digit copilot code, the separate credential, the file that stored them, and every state where a device was paired, trusted, and told to connect first.

Servers

  • A machine nobody sits at is now the same shape as one you do: a group in the sidebar, sessions with tab pills, and its private localhost opening in the same browser window. Three things to add one — an address, a username, and a password or a key. Nothing to install on it first.
  • Signing in with a key no longer asks you to open the file in a text editor. The app reads your key folder and offers what it finds by name, and never offers the public twin sitting beside it.
  • The headless host installs on a server with no Node on it: it fetches an official runtime, checks its SHA-256 before unpacking, and touches nothing outside its own folder.

Windows

  • Attaching a file to a message did not work on Windows at all — browse, drag-and-drop and paste all failed on a path check that assumed one shape of separator. Fixed, in every door.
  • Every scheduled routine failed on Windows, because the runner kept the agent’s command and threw away its arguments.
  • Windows now runs the same test suite as macOS on every change. It never did before, which is why the three above went unnoticed.

Fixed

  • Token totals were inflated for any project with no session open.
  • Driving mode ran the scan and never showed the answer — the card that combines it fetched once and never again.
  • The usage bar would not read on its own for a session started as a shell.

0.5.0

18 August 2026

A second review of the shipped build, an audit of what the first one actually delivered, and two defects that were live on somebody’s machine while they worked.

Two things it was doing to a real session

  • bfdc4c6 It was typing /usage into a working session and leaving the panel open on top of the conversation, over and over. On an account billed through the API there is no rolling subscription window, so the reading could never be found: every attempt timed out, every attempt left the panel, and it tried again on the next quiet moment for as long as the session was open. The panel is now closed and the close is verified by reading the screen back; “there are no limits to read” is an answer rather than a failure; and an attempt that typed and found nothing stops that session for good.
  • bfdc4c6 A session where you opened a shell and ran an agent in it — which the app offers a button for — had no model, effort or usage controls at all. Two parts of one bar were answering the same question from two places: the account chip read the screen and showed the agent, the controls beside it read the session record, saw “shell”, and drew nothing.

Every tunnelled response larger than a socket buffer was cut short

  • bf4e88c Closing the browser’s socket used destroy(), which does not flush — anything accepted from write() but not yet handed to the kernel was thrown away. Measured at 64 MB into a loopback socket: destroy() delivered 327,212 bytes, end() delivered all 67,108,864. That number is macOS’s auto-tuned loopback buffer, which is why nothing was ever lost there; Windows defaults to 64 KB and does not auto-tune, so a real page was truncated mid-body. Orderly closes flush now; error paths still discard, so no descriptor outlives its tunnel.
  • abffc6a A folder with nothing left unread could describe itself as incomplete. When the scan hits its ceiling it drops what is still queued and counts it as unread — and it was counting files it had already read, which can be back in the queue because the operating system replays file events from just before a watch was attached.

The browser stops caring which machine it is

  • bfdc4c6 A machine chooser beside the address bar. The start page lists what that machine is serving, and typing 3000 opens its port 3000 — same window, same tabs, same everything. Choosing a machine changes what localhost means and nothing else: a public address is the same site from either computer, so it is left alone.
  • bfdc4c6 Port lists and tunnels are for your own devices. A guest gets neither and is never told the capability exists — a port scan can say which program holds a port but never which project, so it cannot be narrowed to the folders a guest was granted. Every port or none, and none is the honest answer.
  • bfdc4c6 A remote machine now looks and behaves like a project: one machine mark on the heading rather than one on every row, the same menu a local project has, and remote sessions get a tab in the strip. Closing a machine ends its sessions and folds it away without disconnecting it.

Smaller, and all of it asked for

  • bfdc4c6 A device waiting for approval says so, instead of appearing only in a settings pane you had to already be looking at. The web app takes an address, with a machine chooser and one theme icon in place of three buttons. Settings → Copilot is half the height it was and every file it lists can be edited in place. The model and effort chips keep their labels at every window width — they had been fading to nothing below 1000 pixels. On the phone the copilot’s tab bar is out of the way of the composer and appears only when it is connected.

Sixteen tests that could only ever have passed on a Mac

  • bf4e88c The Windows job gates this installer, and over three days it caught a file URL’s pathname used as a path, POSIX separators asserted against a normalising function, folders named but never created, and tests that waited one tick for an asynchronous parser. It also caught the truncation above, which no assertion was ever going to find. Where the runner itself was simply slower, the timeouts were raised on Windows only, with the measurements written beside them.

0.4.0

18 August 2026

Ninety-one items from a recorded review of the whole application, and underneath them six doors that were never locked, three tools that could not do what they said, and a list of controls that are now gone rather than pretending.

Two of three doors were open

  • 584d01a A paired device could reach every session on the machine, whatever folders it had been granted. Creating a session checked the grant correctly. Listing sessions took no device id at all, and attaching admitted any id off that list — so starting a new shell in an ungranted folder was refused while typing into an agent already running in one was not. Underneath all three, a device with no grant record was read as “fall back to whatever this desktop is offering”, which is every open project plus the working directory of every running session.
  • 584d01a Approving a device wrote nothing. Approval is now the choice itself, and it writes before it admits — the kind of device, then its folders, then the approval, in that order, held by a test that reads the call order rather than the end state, because the end state looks identical when the approval comes first.
  • 584d01a A device is either yours or a guest, and it cannot change kind afterwards. A guest reaches only the folders you tick and is never offered the copilot at all — absent rather than defaulted off, because an unchecked box still advertises the thing. Folder containment is re-checked on every keystroke. Devices paired by an earlier version have no kind recorded and become guests with nothing: a fix that keeps working for everyone who already had everything has not fixed anything.

The copilot could not drive, and it was right

  • 584d01a Opening a page refused whenever no browser tab happened to be open already and told the copilot to ask a person to open one — so it quietly fetched the page instead and said it had not used the browser. Reading a page returned an outline with no page text in it at all, leaving the model guessing CSS selectors to read one line. Handing control back always failed, because the ninety-second window it waited in outlives the sixty-second limit on the call itself. All twenty of its tools were then re-checked by asking a running copilot to use them in words, rather than against a stand-in.
  • 584d01a It knows where you are: ask what you are looking at and it can answer with the page, the pane, and the address and text of a page it is driving.
  • 584d01a Driving mode scans instead of reading to you. It used to hold each stop long enough for a person to read it. Now it goes through every session at machine speed — including the ones you started yourself — with the window under a field of dots and the quoted line cut clear of them, then returns one answer grouped by session. The reading-time estimate, the pacer and the pace dial are deleted rather than disabled.

Every sign-in that stuck was the same bug

  • 584d01a A page’s request to open a window was always refused, and refusing it makes the page’s own call return null — so a sign-in library opened its popup, was handed nothing, and waited forever for a message from a window it never had. The destination did open, as a tab, and the sign-in did complete. It had no way to say so.
  • 584d01a Google sign-in works. It was being served a cut-down flow because the browser announced itself as Electron; nothing else about the browser is disguised. If a site still refuses there is a deliberate handover — finish in your own browser and press a button to bring the session back.
  • 584d01a Saved logins and browser profiles, with the password store in the main process only: the summary the interface receives has no field a password could be in, and matching is on exact origin rather than domain grouping, so a subdomain somebody else controls cannot ask for a password stored for the parent.

Counted twice

  • 584d01a Token totals were over-counted by about 4%. Resuming or forking a conversation copies its history into a new session record, and each record was de-duplicated only against itself. On one real folder that was 11,110 distinct requests counted 11,598 times. Verified against an independent re-implementation written from the file format rather than from this code.

Built, and reaching nothing

  • 584d01a Artifacts showed files because nothing in the code held the meaning of the word, and the previous fix had written that conclusion into the source as settled. An artifact is a file an agent produced whole; a file it only edited is a change to your project and sits one chip away with its count.
  • 584d01a Source control led to Files because the function that fetches a diff had zero callers anywhere in the interface. It is list-left, diff-right in place now.
  • 584d01a A setting the copilot changes now takes effect on the open window. It used to write the change, report success, and leave the screen as it was until a reload — and it now says which of the two happened, measured rather than claimed.

Removed, rather than left looking usable

  • 584d01a The row of controls inside the message box, which repeated the bar above it. Refresh and Reconnect on the phone — one duplicated a gesture, the other was an admission the automatic reconnect might not work. The language picker, which had one option. The bottom band of the browser. The copy of the hooks page inside Settings. And a list of past sessions where every row opened the same one, which was not a bug in the list but the only thing it could do.

Two platforms, and the runner that proves it

  • baf9b1d Six tests measured the machine they ran on rather than the rule they were about, and every one passed on macOS and failed on the Windows runner that gates this release: a file URL’s pathname used as a path, a POSIX separator written into an expectation for a function whose job is to normalise, folders named but never created, and a test that waited one tick for an asynchronous parser and hoped.
  • 3024a58 That last one earned a real method: the plan-limit tracker can now be asked to flush, resolving in the terminal parser’s own write callback, so the wait is exact instead of a guess about how long parsing takes.

0.3.0

17 August 2026

The app gets an assistant of its own, the browser gets an agent that can drive it, and a long list of screens stop contradicting themselves.

A copilot

  • e6996c3 An assistant for the deck itself, pinned above the session list: it can see your sessions, read their transcripts, start new ones and change settings, and it remembers your conversation with it. A developer’s assistant rather than a general one — no inbox, no calendar. It is built as a real session rather than a hidden service, which is the whole point: its folder, its instructions, its memory and its transcript are files you can open, and Settings → Copilot shows what it is handed before it starts.
  • e6996c3 It runs inside the same sandbox as any session started from another device — including from us, and proved against sandbox-exec rather than asserted: it cannot read your home directory, your SSH keys, your keychain or another account’s login. Its routines and its action log live outside the one folder it can write to, because a routine it could author unasked is not gated and an audit log the audited party can rewrite is not an audit log. A consequence worth stating: it starts signed out, because the keychain is closed to it, and its first screen says so.
  • f5eee9f The copilot got hands, and three separate features turned out to share one shape — the mechanism present, the connection absent. Its tool configuration was being written and never passed to it, so every sentence about being bounded by a consent gate described a gate that was not in the path. The session fan-out honoured a hidden predicate everywhere and was built without one, so a paired phone could attach to the copilot’s own terminal and type into it. And the hook endpoint baked a port into the hook files that changed on every launch.
  • f5eee9f Routines — saved instructions that run on their own, triggered by something happening rather than by a clock: a session finishing, a session failing, git state changing, a file changing. A schedule is one trigger among several rather than the foundation. One that fires while nobody is at the machine and then needs consent is refused immediately and legibly, instead of hanging on a dialog no one can answer.
  • a293ec9 It can show you what it found rather than only telling you: it writes a tour and the app plays it, a box around the line that matters, everything else dimmed, pausing the instant you touch anything. What it refused is the part worth reporting. On the first real run the model quoted a line that was never in the terminal and called an idle session blocked; both were dropped before they reached the screen — one by checking the quote appeared verbatim in the source, one by re-checking the claim against the app’s own view — and the recap names the drops rather than hiding them.

A browser an agent can drive

  • a293ec9 Driving goes through the browser’s own debugging protocol with a real actionability loop — resolve the element, check it is visible, wait for its box to stop moving, check it is enabled, hit-test where the click will land, then dispatch. Screenshots show you what happened; they are not how anything is decided.
  • a293ec9 And no debugging port. The off-the-shelf way to do this wants a listening socket, and a loopback socket is not a user boundary: on a shared machine another logged-in user connects to it freely, and the renderer is a target on that port holding the preload bridge — so a single evaluate would walk past every consent gate in the process. The engine is in-house for that reason. Measured on the way: a protocol-level navigation to file:///etc/passwd succeeds on a view whose navigation guard calls preventDefault, because that guard only covers navigation the page starts.
  • a293ec9 One page, and the two of you take turns on it. Any real interaction of yours takes it back at once and the agent’s next command is refused; while you hold it, every command is refused, reads included. That is deliberate and it is why the rule is not limited to clicks: a screenshot taken while you are typing a password is the leak, and nothing can redact what was never produced.
  • e6996c3 Draw on a page and send it to an agent, alongside inspect and record. A pane can hold a browser page too — before this a pane could only name a session, so anything else was by definition a closed one and the split collapsed.

The window

  • e6996c3 Tabs take Chrome’s shape and join the pane below them. The + leaves the strip; a terminal and a globe sit after the last tab; there is one path to a new session and it is the dialog. A tab’s ✕ closes the view and leaves the session running — only the sidebar’s ✕ ends one.
  • e6996c3 Chrome belongs to the pane rather than the window. An account chip drawn once above two sessions from two projects is wrong for at least one of them with nothing saying which; each pane names its own account, folder and session now, and the main session keeps its chrome in the top bar, which is what makes it read as the main one.
  • f5eee9f Model, effort, connectors and usage sit in the chrome beside the account, for a terminal as much as for a chat. Clicking a model changes the session that is running — and refuses, saying why, when you have something typed, because the way a CLI is told to change model is a line typed at it and the next Return would have been yours.
  • f5eee9f Add your own agent. The list is no longer four names compiled into the app: name a command and it runs, with the caveat drawn on the row that an agent nobody here has characterised gets a terminal and not a model picker. The command is resolved against your login shell’s PATH before it is saved, so a name that cannot be found is refused at the form rather than becoming a row that dies when you pick it.
  • e6996c3 Attach a file from anywhere — the real system dialog, drag and drop, or paste. The picker had only ever offered files inside the project. Notifications open as a popup instead of taking over the window, and Settings is one surface rather than three greys pretending to be one screen.

Fixed

  • e6996c3 Hooks stopped working every time the app restarted. The endpoint took a new port on each launch and that port was written into the hook files, so session-finished events silently never fired — and a credential was written world-readable into an agent’s settings file. The endpoint is a socket now, at a path the kernel cannot hand to a stranger.
  • f5eee9f Two faults that only appear when you run the app rather than read it. Transcripts under a symlinked path were invisible to usage, chat, alerts and loop detection, all of which reported the miss as nothing to see — /tmp is symlinked on every Mac. And a shell session was handed another session’s conversation, complete with 780,000 tokens belonging to a session that had already ended; shells write no transcript, ever.
  • f5eee9f A web page could paint over the terminal, and the trigger was ⌘R: React cleanups do not run on a page unload, and the host is the same object across a reload, so both safety nets missed for the same reason. Visibility is decided by the main process now, from the renderer’s document generation, which a renderer that stops talking cannot defeat. A 21-pixel tooltip also used to blank a 587×644 page, because any intersection counted as total coverage.
  • 34ff13f A confined session died on its first turn on macOS, before producing a token, because the agent CLI keeps scratch files at a path outside every sandbox. The same fault broke sessions started from a paired phone. A claim about the sandbox also moved off the terminal that could not hold it.
  • 92c6b6c Wait for the shell before typing at it, rather than sending the first keystroke into a terminal that is not listening yet.
  • a293ec9 Windows could not run hooks at all: a socket path there maps to a named pipe, which rejects a filename, and the tool the hooks used cannot reach a pipe. Only the program had to change, which an experiment established rather than a document. Two boundary tests were also passing without checking anything, their setup returning early off macOS and leaving the path-agreement case asserting against an empty string.
  • e6996c3 Screens that contradicted themselves: the model picker typed into a live prompt and left a confirmation dialog standing for the next Return to answer; Power’s notice denied its own switch; a language dropdown had one option; a debug file offered Reveal when it did not exist; the file viewer drew characters the file did not contain, because programming ligatures were on in a pane whose whole job is showing text verbatim. Clicking a file in the tree also closed the Files page, because navigation was a toggle.

Removed

  • e6996c3 Every price and cost figure, finished. One of the two could not be computed at all — nothing is published from which to derive what a subscription costs per token — and the other told somebody on a flat monthly fee that they had spent money they had not. Token counts, cache share and the context window stay, because those are facts rather than inferences.

The clients

  • 7df2fe5 iOS gets a Localhost tab and a three-tab shell — and the first build any of that had ever been compiled in.
  • b8e542e The web client catches up: localhost ports grouped and nameable, and more than one machine at a time.
  • 9ede37c One live pairing code per machine, and a code that admits when it cannot find the machine it names rather than failing blankly.
  • a293ec9 The copilot reaches your phone over its own connection — a separate code, a separate credential and four independent refusals, rather than a checkbox on the session pairing. It carries no device id, so a leaked one is half a pair rather than a bearer token.
  • 4a50e69 9,136 tests passed and the release still failed, on a directory removal. The release script is the thing that has to survive a green test run.

0.2.0

16 August 2026

The release that took things away. A pairing code with one method instead of three, a network with one path instead of two, and every price in the app deleted.

Pairing is six digits, and nothing else

  • 3babd08 One way in, and it is six digits on a numeric keypad. The QR code and the pairing link are gone entirely — deleted, not hidden: the QR encoder, the iOS scanner and its camera permission, the Android scanner and its CAMERA permission, and every terminaldeck://pair route into a credential write. The QR did not work, and a link was a two-hundred-character string carrying a live bearer secret that had to travel through a messaging app to be useful — which is a pairing token somebody else’s server then has a copy of.
  • 3babd08 Six digits is a million codes where eight characters were 1.1 trillion, a reduction by a factor of 1,099,511, and the number is stated rather than buried. What makes it sound is unchanged and now pinned by tests rather than assumed: a code lives sixty seconds, is single-use, and dies after five wrong guesses — the code dies, not the guesser, so minting a fresh key for every attempt buys no fresh budget. Redeeming one still only produces a pending device that a human approves on the machine itself.
  • 9ede37c One live code per machine, and a code that admits when it cannot find the machine it names rather than failing blankly.

The relay is the network

  • 3babd08 The “Direct on your tailnet” card is gone from the app. There is one path to a machine now: Terminal Deck’s own rendezvous relay, which both ends dial out to. No port forwarding, no VPN, no account, and nothing anybody has to install or understand before a phone can see a terminal. Everything it carries is sealed under keys the relay never sees, so it can end a session and cannot read one.
  • 3babd08 Machines and Remote are one section, in the sidebar rather than inside Settings. Pairing a device is something you do, standing at two keyboards — not something you configure once and forget, which is what everything else in Settings is.

No prices anywhere

  • aa08a84 Every dollar figure is deleted — the rate card, the per-session and per-day spend, the money column in the session inspector and the cost tile on the dashboard. The API number was correct arithmetic and it still misled: almost everybody running this is on a subscription, and telling someone on a flat monthly fee that they spent four figures states a number that never left their account. The subscription number cannot be computed at all, because no plan publishes a token allowance or a per-token value to measure a session against.
  • aa08a84 What replaced it is what was always underneath: tokens split by fresh input, output, cache read and cache write; the share of the prompt served from cache; the request count; what the project has moved today; and context-window pressure with the compaction count beside it. The dashboard tile is called Usage. Deleting the rate card also removed a live bug — a time-boxed price due to revert on a date nobody was watching, for an increase that has since been cancelled.

The window

  • aa08a84 Overview is a live board of running sessions — what each agent is doing, how long it has been doing it, and which one is waiting on you. Deliberately no progress bar: an agent does not report progress, and a number the app invented would be worse on that screen than no number.
  • aa08a84 Search is replaced by Artifacts — every file your agents wrote or changed, with the diff of each change. Searching past sessions moved into the command palette under ?, one keystroke from what you were already doing. Search’s own results had never been clickable.
  • 8e49830 Sessions can be dragged into the top strip. The strip was built and nothing in the app was draggable, so it looked finished and did nothing. Promote and demote also work from a row toggle and ⌥← / ⌥→.
  • aa08a84 Windows has one title bar instead of an OS strip, a menu strip and the app’s own chrome, with real minimise, maximise, close and snap layouts. About nine hundred words of on-screen prose were removed, with every warning kept.
  • c620f03 An account belongs to an agent. Adding one asks which — Claude Code or Codex CLI, both verified to isolate a login. Gemini is listed and refused, with the reason on the row: its token lives in one keychain slot no config directory moves, so a second login would overwrite the first rather than sit beside it.
  • d060cff Light and dark in the web client, following the system by default.

Fixed

  • aa08a84 Keep-awake held nothing. The app’s own power-save blocker was gated behind a privileged system setting, so on every machine where that had not been granted — including every machine on first run — no lock was held at all.
  • aa08a84 Sessions did not come back on Windows: a WSL path was resolved against the Windows host, so /home/you/x became C:\home\you\x and matched nothing the Linux agent had written. The same fault emptied the Files page there.
  • c620f03 Adding an account always signed you into Claude. The preload dropped the options object, so the provider never crossed the bridge — signing in beside a Codex account opened a Claude session, which then correctly refused the account and silently discarded it.
  • 64da716 Wait for the shell before typing at it, rather than sending the first keystroke into a terminal that is not listening yet.

Removed

  • 942aca5 The GitHub notifications bell. GitHub’s notifications endpoints accept only classic personal access tokens, a GitHub App user token is not one, and no permission can be added to change that. Sign-in is the GitHub App and nothing else now.

0.1.9

16 August 2026

Four batches of work, and the release where one machine could first drive another.

Machines, devices and what they may reach

  • 43273bb One computer can be paired to another and drive its sessions from the Machines panel. A typed code cannot carry an address, so it names a slot at the relay: the machine showing the code sits in that slot for the sixty seconds the code lives and answers with its real URL, host id and public key. A hostile relay cannot answer in its place, because the responder’s key pair is derived from the code as well.
  • 9279d76 Per-device folder grants: the machine that owns the files decides which folders each paired device may start a session in. Read that literally — it is organisation, not a sandbox, and nothing in the app says otherwise. A shell that starts in a granted folder can cd anywhere the user account can reach. The security boundary is the one that was already there: pairing, plus a human approving the device on the machine itself.
  • 9279d76 The credential proxy — their GitHub, from their device, never yours. A session started from another device is handed its own git configuration, so git push cannot reach the owner’s account through the credential helper, the rest of the global config, gh, or ssh. When git needs a login, the request crosses the sealed channel to the device that started the session and is used once in memory; nothing is written to the host’s disk, so revocation is disconnection.
  • 43273bb Sessions in WSL, routed by the folder rather than by a switch. A Linux path launches through wsl.exe inside the distribution; a Windows path launches through cmd.exe as before. There is no “use WSL” toggle on purpose — a toggle is what lets the two disagree.

Two things that were believed and were wrong

  • 0761f87 Notifications were never a signing problem. The theory was that macOS keyed authorisation on a signed bundle identity and refused an ad-hoc one. Measured instead: macOS registers the ad-hoc bundle perfectly well and had simply put up its authorisation prompt, which arrives as a notification banner with Allow hidden behind an Options disclosure nobody had opened. Clicking Allow was the entire fix, and macOS’s own notification store recorded the deliveries afterwards.
  • 72b1015 The app stopped saying “Sent.” for a banner it never watched arrive.

Windows, and the guards that were not running

  • e50f276 Answer for the platform you were asked about, not the one you are running on — with two follow-ups for the same fault in other places.
  • 57ab908 A guard that cannot run on Windows is worse than no guard, because it reads green. Several were rewritten so they run on the platform they were written to protect.
  • 5db67d0 Three ways this Mac lied about being a clean machine, found and closed, so a passing suite here means something on somebody else’s.

Tailscale leaves the product

  • 388ee5b The browser client dials the relay, and Tailscale leaves the product’s face. Nothing anyone downloads needs it, needs to know what it is, or is asked to install it — a point 0.2.0 finished by deleting the tailnet card outright.

0.1.8

14 August 2026

One fix, and a test so it is the last time.

Wording that crosses to a phone

  • fa2d5c8 Nothing sent to a phone calls a Windows PC a Mac any more. Fourteen sentences in the remote server named a platform and ten of them were sealed up and read on the phone — “Pair it again from the Mac.”, “This Mac cannot start sessions from a phone.” — and one phone now holds several machines at once, so those could appear directly beneath a row naming a Windows PC. The phone already prints each machine’s own label beside anything it says, so wire copy names no platform at all; copy the person at the keyboard reads keeps the noun and asks the code that knows which one. This was the third sweep of the same rule by the third pair of hands, so it is scanned now rather than swept again: a string in a module whose output crosses the wire may not name a platform, and the test says which line when one does. Run against the previous release it reports all fourteen, including four a careful reading of the diff had missed.

0.1.7

14 August 2026

The release in which Windows stopped being the platform where two things quietly did not work. Both were measured on a real Windows machine rather than reasoned about.

The localhost tunnel

  • 81a4ced It reaches a dev server on ::1, which on Windows is most of them. Windows resolves localhost to ::1 before 127.0.0.1, so vite, next dev and node --host localhost bind IPv6 and nothing else — and the tunnel only ever dialled 127.0.0.1. The port was scanned, listed, offered to the phone, and then refused the moment it was tapped, with nothing anywhere saying why. A port that is listed and unreachable is worse than one that is not listed. The scan now carries the address family it always knew and the tunnel dials the loopback the port is actually on, deciding once per tap by connecting rather than per browser connection by guessing; when neither loopback answers the tunnel is refused with a sentence naming what was tried. Nothing changed on the wire, and nothing changed about the tunnel being a byte pipe rather than an HTTP proxy — which is what keeps hot reload, server-sent events, cookies and the WebSocket upgrade working. Proved on a real Windows machine in all three ways a dev server is started, pulling a real HTTP response and a real 101 Switching Protocols upgrade through the tunnel each time.

Updating in place

  • 81a4ced Windows installs its own updates without anyone clicking an installer. Finding and downloading already worked there; installing did not. The app quit, an installer window nobody asked for opened, and it sat on a page waiting for a click — fifty seconds later the setup was still running, the old version was still installed, and there was no app on screen at all. It installs silently now and the app comes back, which is what a button saying Restart promises. Watched working end to end: an installed build took itself from 0.1.7 to 0.1.8 through its own panel and relaunched.
  • 81a4ced The portable Windows build no longer offers an update it cannot install. It is the same build as the installed one and carries the same release feed, so it reported itself updatable — and an update on Windows is an installer, which would have put a second, installed copy of the app somewhere the user did not choose while the portable executable they were running stayed old. It says plainly that a portable app cannot update itself and points at the releases page.

0.1.6

14 August 2026

One fix, for something no release had ever contained.

The phone client

  • c7054e2 The page you open on your phone is in the app now. It never has been: 0.1.3, 0.1.4 and 0.1.5 all shipped without it, so the tailnet address the Remote panel prints for you to open answered with nothing at all. The native phone apps were unaffected — they speak the sealed protocol directly and never fetch that page. Nothing failed loudly enough to notice, because the client is build output and therefore gitignored, so the line in the packaging allowlist that names it matched zero files in a clean release checkout — which looks exactly like a line whose files were all excluded on purpose. It only ever worked on a machine where somebody had built the phone client by hand at some point. Packaging builds it now, and the release check fails if the built page is not in the bundle.

0.1.5

14 August 2026

The release that made remote access work without anyone switching it on, which is the whole point of it.

Remote access

  • 3938381 The desktop dials the relay at launch now. The relay had existed for a day and this Mac had never dialled it once outside a test: start() ran only from a two-press switch in Settings → Remote, and nothing re-ran it on the next launch, so a computer that had been restarted simply was not reachable. Measured on the machine this was found on — the host identity on disk, two paired iPhones in the trust store, the relay up, and not one socket to it. A phone attaching to that host was attaching to something that was not there, which is exactly what “connected, but showing me old output” looks like from the sofa.
  • 3938381 Dialling on its own exposes nothing, which is why it can be the default: the relay learns that a host is online and no more, and a device still has to be paired and approved before one byte moves. Switching it off is remembered, so off still means off. Proven on the packaged app rather than asserted — an established socket from its own process to the relay, the panel reading Connected against this machine’s host id, and the relay’s host count rising when the app launches and falling when it quits.
  • 3938381 The Remote switch stopped describing the opposite of its own position. It read “Off by default. Nothing can reach this Mac while it is off.” beside a switch that is now on.
  • c0d88f5 On the phone, the keyboard is raised before you type at a terminal, and the rename alert is presented from the root rather than from a view that may already be gone.

0.1.4

14 August 2026

The release that works on Windows. Everything 0.1.2 could not do there — find an agent CLI, start a session, reach Settings, quit from the menu bar — works now, on a real Windows 11 machine rather than in a runner. The whole test suite runs on a Windows runner and gates the installer, so a red Windows test stops the release rather than being waved through. One Windows fault is still open and is named at the bottom of this page.

Windows

  • 53016ed Agent CLIs report their version. They are .cmd shims on Windows, which Node refuses to spawn without a shell, so every version column was blank. The shim path is quoted too, or the fix would still have failed for anyone whose Node lives under C:\Program Files.
  • 53016ed No more console windows flashing over whatever you were looking at. Seven child processes were spawned without windowsHide, including the Tailscale status check that runs the whole time the remote panel is open.
  • 53016ed Settings, Keyboard Shortcuts, About and Exit are back. All four lived only in the macOS application menu, which Windows drops wholesale, so the menu bar had no way to reach Settings and no way to quit — and Ctrl+, and Ctrl+/ were unregistered along with them.
  • 53016ed The diagnostics bundle prints a readable PATH. It was split on the POSIX separator, so every entry was torn apart at its drive letter — in the one file somebody attaches to an issue that says a CLI cannot be found.
  • 9555b0b Turning remote access on is instant again. It used to sit for fifteen seconds with the panel spinning and then say “Tailscale did not answer”. Tailscale had answered, immediately: Serve is not enabled on your tailnet, with a link to switch it on, and then waited forever for someone to click it. The output is read as it arrives now, so the refusal comes back in under a second carrying Tailscale’s own words and the link. Remote access was never blocked by this — the relay does not go through Tailscale — but nothing said so for fifteen seconds.
  • 9555b0b Starting a session from a phone no longer refuses a folder that is on the list the phone is showing. Two spellings of one Windows folder — a lower-cased drive letter is enough — compared as different directories, and the refusal said “open it on the Mac first” about a folder already open.
  • a0a114a The last six places that build a child process environment go through one helper, so Windows is never left holding two spellings of PATH with no rule about which one the child searches. It affected the GitHub panel, the git status poller, the readiness checks, Copilot detection and every stdio MCP server.
  • a206d5e, 2c96717, 677aa94 The Windows test suite became a gate. The suite was ported and the release workflow no longer lets that job fail without stopping the build. The cases that cannot mean anything on Windows — POSIX file modes, the POSIX shell the hook command is written for, the macOS-only updater — are skipped individually, each saying why, rather than the suite being waved through as a whole.

One phone, several machines

  • 4e45d5f A phone paired to a second computer used to quietly drop the first. Pairing adds a machine now: each owns its own transport, its own sealed channel against that machine’s static key and its own sessions, so two machines cannot read each other’s work. All of them stay connected, so switching needs no handshake, and the keepalive they need is folded into one app-wide tick — 144 radio wake-ups an hour at one machine and at five, measured rather than asserted.
  • 4e45d5f Inspect mode, on the phone. Tap an element on the tunnelled page, say what should change, and it lands in a terminal on the desktop as exactly one line. The rule is transcribed from the desktop’s own capture panel rather than approximated, so both clients hand the agent identical strings — a newline would submit the prompt early and an escape would repaint the terminal it arrived in.
  • 4e45d5f The phone’s Connected badge no longer claims a connection the app does not have. Observed against the live relay: no guest attached for a sustained forty seconds while the app read Connected. “Online” had been decided before the phone went in a pocket, and nothing tells a socket that a carrier NAT reclaimed it. Resuming now doubts the channel, says “Checking”, and probes immediately.
  • 9e00c74 The Android client gained the same several machines the iPhone had.

Surfaces that said something they did not mean

  • 0f0cdcd Every page has one blank, and it is the same blank. Empty states were four different designs across ten panels — GitHub drew its own glyph, title and button, MCP printed a bare sentence with literal backticks in it, the board dropped inline text into a column. There are two now: one for a page with nothing on it, one for a section of a working page or a page still reading, parked at the same height so the answer lands exactly where “Reading…” was.
  • 0f0cdcd The inspector stopped jumping under the pointer. It re-centred on every tab press, because the tallest of its four panels ran past the floor and moved the sheet 50px each time. A definite height parks it and the body scrolls.
  • 0f0cdcd Context → “How it filled” draws something. It plotted a percentage of the context window against a fixed 0–100 axis, and a healthy session peaks near four percent, so every point landed within a pixel of the baseline. The axis scales to the peak now, and both the ticks and the caption print the ceiling — the honesty problem was never the scale, it was a chart that drew nothing.
  • 0f0cdcd “Unknown” is gone where the answer was never knowable. Fast mode said Unknown beside three siblings that always resolve, which read as this app failing; the CLI announces fast mode only when it changes, so a session that has never been told has nothing to report. It says “Not reported” and the menu says why. The no-project screens stopped offering four ways to open a project, and Settings → Help stopped drawing its own copy of the window’s own menu.

The build, and quitting

  • 2004da5 The installer is checked for what is inside it, not just that it exists. A platform files: list in electron-builder replaces the root allowlist rather than extending it, which shipped the whole repository — a 1.0 GB app — until it was caught. Both platform blocks were fixed, and CI now fails if a build carries ios/, android/, relay/, src/ or build/, if the app cannot start, or if an artifact lands outside a sane size band.
  • 2004da5 A release build can be produced from a commit without publishing it. The workflow can be dispatched by hand and publishing stays gated on a version tag, so “let me check the installer size” and “ship it to everyone” stopped being the same button.
  • ae86843 Quitting with a live session no longer prints a wall of errors. Stopping the app while a terminal was still producing output threw Render frame was disposed before WebFrameMain could be accessed, once per message still in flight, because the main process was broadcasting into a window that had already gone. Nothing was lost and nothing was corrupted — the errors were the whole symptom — but they were the last thing a packaged build printed.
  • ae86843 The debug trace no longer writes unless you ask for it. A build could leave an ipc-trace.log of 12 MB and growing in its application-support folder with Debug mode switched off. It is written only while Debug mode is on now, capped at 4 MB with one previous generation kept, and listed in Settings → Advanced so it can be found and cleared. A file left by an earlier version is deleted the next time the app starts with tracing off.

0.1.3

14 August 2026

The release where remote access started working, and the release where Windows was launched for the first time in this project’s history — and then fixed.

Remote access

  • 9728d2b Remote access had never worked, on any platform. Electron links BoringSSL, which ships 28 ciphers and not one ChaCha, so createCipheriv('chacha20-poly1305', …) threw Unknown cipher, a silent catch swallowed it, and every relayed handshake closed with nothing on the wire and nothing in the log. The whole test suite passed throughout, because vitest runs under plain Node. The cipher now comes from one implementation used in every runtime, with no “native when available” fast path, and the suite runs the sealed channel under Electron’s own Node and fails the build if that stops being true.
  • 9728d2b A paired device no longer has to be paired again after a restart. Host identity validated its stored keypair by running a handshake, so the bug above quarantined and regenerated a perfectly good identity on every launch, orphaning every device paired to it.
  • 9728d2b Both phone clients were one byte off the wire spec, and both stand-in hosts shared the bug — so the fixtures agreed with each other and disagreed with reality. The stand-ins import the real framing now rather than reimplementing it.
  • 9728d2b See the desktop’s localhost on your phone. Tap a port and the phone dials 127.0.0.1 on the desktop through the sealed channel. It is a raw TCP byte pipe rather than an HTTP proxy, so WebSockets, hot reload, service workers and cookies all survive untouched. Only 127.0.0.1 is dialable, only ports something is listening on right now, and only after a person taps one.
  • 9728d2b Start a session from the phone. Both phone clients had had a New Session button for a while, gated on a capability no desktop advertised — so it could never appear, and each client had invented its own frame shape against its own stand-in. There is one shape now, parsed and narrowed like every other frame; the desktop advertises it only when its session layer can actually start one; and a session started this way is a real pseudo-terminal made by the same call the desktop’s own button makes. A phone may name a folder only if the desktop is already offering it.
  • 1880202 A session started from a phone appears on the desktop, without focus, so answering something on your phone never pulls the desktop out of the terminal you were typing into.

Windows, run for the first time

  • 9118e11, e18b47e The two faults that made 0.1.2 unusable on Windows, found by installing it there and fixed here. which was spawned as a literal command; spec.bin was spawned where spec.spawn was meant, and an npm-installed CLI answers a Windows PATH lookup with a .cmd shim that CreateProcess will not run; and three sites that spread process.env over a PATH key lost against Windows’ own Path. 31 Windows test failures became 0.

The window

  • 9728d2b The desktop speaks Apple’s language: one sidebar, one toolbar, Settings bottom-left, liquid glass, and both themes first-class. Three entries came out of the reachability allowlist and none went in — each was a setting that turned nothing on.
  • 9728d2b Roughly 87,800 fewer wake-ups a day. One shared renderer scheduler where N jobs cost one wake-up rather than N, nothing armed at all while the window is hidden, and a panel that polled a channel already pushing to it now subscribes instead. The remote panel alone was a 500 ms interval — 172,800 wake-ups a day to move labels that mostly change once a minute.
  • 1880202 Every page got a designed empty state instead of a bare sentence floating in the middle of the window; pages and Settings hold a measure and centre it, so a wide display no longer parks the content in the top-left corner; and the update notice became an inset card wearing the same glass as the sidebar rather than a full-bleed grey strip.

Packaging

  • 9728d2b, 1880202 Every macOS build had been shipping the whole repository. electron-builder’s per-platform files: list replaces the root allowlist rather than extending it, so the mac: block had no allowlist at all — invisible until the phone clients existed, at which point the app reached 1.0 GB. It is 287 MB now, and the identical latent bug in the win: block was fixed in the same pass.
  • 8a988ba Stopped tracking a node_modules symlink that pointed into somebody’s home directory.

0.1.2

13 August 2026

The first release with a Windows build in it. The installer and the portable executable are both x64 and both unsigned. Running them on a real Windows 11 machine is what found the two faults that made this version unusable there — every agent CLI reported missing, and New Session failing — and both were fixed in 0.1.3.

Windows in the release

  • 1ecca75 Version 0.1.2 — the first tag whose artifacts include an x64 installer and a portable executable alongside the macOS disk image.
  • 03b6a42 Let the Windows installer build while its test port is unfinished, so a failing test on a platform nobody has shipped to does not hold back the artifact people are asking for.
  • ef0312d, 5648d65 Stopped the test suite assuming POSIX where the product does not, and fixed two tests that depended on the machine they happened to run on.
  • af1f02d Put back the workflow step that creates the release, missing since the previous tag.

0.1.1

13 August 2026

The release that taught an unsigned build to update itself.

Updates

  • 40e84d9 Update inside the app, without Squirrel and without a signature. Squirrel.Mac refuses to replace a bundle it cannot verify, which is every unsigned build — but the feed, the archive and its SHA-512 are all public, so the app reads the feed itself, proves the bytes, and swaps the bundle. The old app is moved aside rather than deleted, and moved back if the new one fails to land.
  • b161be7 Build Windows on Windows. Cross-building was producing an artifact the prebuilds rule then broke; building it natively in CI does not.

0.1.0

13 August 2026 first release

The first version that is a usable application. It runs from source and packages into an unsigned macOS build, published as a disk image; it predates both the Windows target and the in-app update path, so it can neither be installed on Windows nor replace itself.

The window, and the processes behind it

  • 473d9bb The Electron shell: multiple terminal sessions in one window, each a real process, with the tab strip and scrollback that survive switching away.
  • 51f59c5 Projects and window bounds persist between launches, installed agent CLIs are detected through the login shell’s PATH, and a strict content security policy is set from the main process — permissive only while developing.

Knowing what a session is doing

  • b72448a Session status arrives: working, waiting, needs input, exited — with tests, and a full build plan for the eight phases that follow.
  • 17a418d The first real correction. Status had been classified from the tail of the output stream, which is meaningless for a full-screen program that repaints by moving the cursor. Each session now feeds a headless terminal emulator and the status is read from the rendered screen — including the discovery that Claude Code draws its prompt as ❯, and never on the last line.

The features, built in waves

  • 769ab1d Six modules built in parallel and wired in: transcript reading, cost and context maths, the git panel, the file tree and viewer, project search, and the command palette.
  • 678e6bd Session resume — continue the last conversation in a folder rather than starting a fresh one.
  • 6265ba5 Wave two: the session inspector, the GitHub panel driven by gh, AI-readiness scoring, the project dashboard and the swarm view.
  • b601f77 Wave three: full-text search across past transcripts, the alerts panel, agent profiles with isolated logins, provider hooks, the MCP inspector, the first embedded browser, and one keymap table that replaced three drifting copies of the shortcut list.
  • 7502ef7 Wave four: the settings window, a browser that is a real browser rather than a panel, the new-session dialog, in-app help and a debug panel.

Making it look like one application

  • 0c5724c The Notion-style palette — near-neutral surfaces, warm grey text, hairline dividers, one blue reserved for selection and focus — and a side panel that collapses and resizes.
  • d21799c The app shell proper, which made every feature reachable. Several panels existed and worked before this and simply had no way in.
  • b51b44f The view switcher was styled as a control, because it read as a caption.
  • a2971c3 Tabs moved into the window header, and sessions and browsers were merged into one strip. Two separate controls for “what do I have open” meant the browser was effectively unreachable.
  • 0bf3904 One header bar. Overview and Board moved to the side rail, where views of a project belong — the tab strip is for windows you opened.

Making every panel actually work

  • f8ac57e Fixed the panels and views that rendered nothing. Typechecks and tests had all passed; the failures were contract mismatches at the bridge and runtime throws, neither of which a compiler sees.
  • 56e5273 Every remaining mismatch between the interface and the main process fixed, and the app made installable.
  • 6f4e500 Tracing across the bridge, so a panel that silently fails says why instead of showing an empty box.
  • 2abed94 The browser fix worth naming: a request-style call answered by an event-style handler rejects every time, so every browser action failed.
  • b986623 A test over the seam itself, so a method the interface calls and the main process does not implement fails a test rather than a user.
  • 64b2370 The browser hides while a dialog is open. It is a native view layered above the interface, so pressing ⌘, on a browser tab dimmed the window and showed nothing — settings were underneath the web page.

The browser grows up

  • b98be29 A Setup section that reports what your machine actually has, cookie import from an installed Chrome so a dev server behind a login does not need signing into twice, and per-tab isolation for when you need two accounts at once.

Chat mode

  • 6be5d78 A session as a conversation, read from Claude Code’s own transcript — prompts and replies, none of the tool plumbing. The address bar in the browser stopped guessing at port 3000 and started listing the dev servers actually listening.
  • c4c15f6 Chat mode gained a composer. It writes into the session’s own terminal, so the two views never disagree about what was said.

Housekeeping

  • 3472525 A README and a working guide for the repository — which also closed two of the app’s own readiness findings against itself.
  • 6b99916, d951c4c, a3e6acb The roadmap kept current after each wave landed.
  • 41a96fc Renamed to Terminal Deck. The name lives in exactly one file, so this was an edit rather than a hunt.

Not in any version yet

Listed here so the absence is on the record rather than discovered after installing.

  • Peer-to-peer session sharing with another person. Reaching your own machine from your own phone is a different thing, and it works from 0.1.3 onwards.
  • Updating in place on Windows. On macOS it works, from 0.1.1 onwards. On Windows the check is wired to electron-updater's installer and nobody has watched it run, and the portable executable is a loose file nothing can replace for you.
  • Translations.
  • A signed installer on either platform. The disk image, the Windows installer and the portable executable are all built; nothing signs any of them, so macOS needs the right-click step and Windows needs the SmartScreen override.
  • Split panes. The layout code and its chords are in the repository, but nothing in the app renders the split view, so there is no way to reach it.
  • Anything at all on Linux. Intel Macs are deliberately out of scope.