Releases: reyanshgupta/ditto-cli
Release list
v0.4.2
What changed
Add isolated Codex Desktop launches and picker shortcut
Launch managed desktop profiles with separate Codex and Electron state without copying credentials. Keep the stock desktop session unchanged, preserve profile selection on launch failures, and resolve directory bindings before launch.
Expose desktop launches as codex-app/cxa and Shift+x in the macOS profile picker. Refresh captured picker documentation and cover private-state redirection.
Show Codex Desktop in profile details and launch menu
Keep desktop profile switches from importing terminal projects
Install
brew install reyanshgupta/tap/ditto-cli # or: brew upgrade ditto-cli
npm install -g @reyanshgupta/ditto-cli@0.4.2
cargo binstall ditto-cli # or: cargo install ditto-cliThe archives below are the same binaries every channel installs; SHA256SUMS covers them.
Full Changelog: v0.4.1...v0.4.2
v0.4.1
What changed
Describe Ditto as agent-agnostic and put every supported agent in one table
Orca was framing the README: the intro listed "every agent Orca runs" and
the extra agents sat in an "Other agents" section after skills, with the
original six in a separate table under How it works and no entry in the
Contents. Ditto is not tied to any agent or launcher, so the README now says
so: How it works explains the one idea (a variable, the XDG bases, or HOME),
a Supported agents section directly after it lists all thirty-six agents in
one table with the same columns for every row, and Orca appears only in its
own integration note beside herdr. The picker mockup matches the current
picker, and wording that named tools one by one now speaks of every agent.
Write release notes from the commits, and capture the README's screens
Every release page carried nothing but the compare link. The workflow asked
GitHub to generate the notes, and generated notes are a list of merged pull
requests; this repository lands its commits on main directly, so the list
was always empty. The notes are now written by .github/release-notes.sh from
the commit messages since the previous tag — subject as heading, body as
text, the bump commit and trailers dropped — which is what those messages
were written for. A re-run of the workflow refreshes them, and every past
release that had only the link has been given its notes the same way.
The picker screens in the README are the picker's own output, and the
launcher changed what the picker draws, so they are captured again rather
than edited by hand: .github/readme-picker.py runs a debug build in a
pseudoterminal with stand-in agents that answer their status commands, and
prints the two blocks. The second is new, and shows the launcher.
Stop treating Orca as a case of its own
Ditto steps aside for herdr because herdr has a pane to report the profile
to. Orca had the same step-aside keyed off ORCA_PANE_KEY but nowhere to say
which profile was chosen, so it bought a launcher-specific branch and paid
for it with a title that no longer named the profile. Ditto is agent- and
launcher-agnostic, and one launcher does not earn a variable in the launch
path: the branch, the README section, and the environment-table row are
gone, and the comments that named Orca now describe the behaviour any
account switcher has. Under Orca a launch now proxies like any other
terminal, which puts the profile back in the title.
Install
brew install reyanshgupta/tap/ditto-cli # or: brew upgrade ditto-cli
npm install -g @reyanshgupta/ditto-cli@0.4.1
cargo binstall ditto-cli # or: cargo install ditto-cliThe archives below are the same binaries every channel installs; SHA256SUMS covers them.
Full Changelog: v0.4.0...v0.4.1
v0.4.0
What changed
Fix two ways the terminal proxy mishandled what it sits between
An OSC sequence carrying no argument came back out carrying one. The
filter split a collected command into its number and its text and then
always wrote the separator back, so ESC ] 112 BEL reached the terminal
as ESC ] 112 ; BEL. The two are not the same request: OSC 104 resets
the whole palette and OSC 112 the cursor colour, while the ; forms
name nothing to reset and are ignored. A tool putting the cursor colour
back on its way out therefore did not, and the claim that everything
which is not a title is forwarded byte for byte was not true. The
difference between no argument and an empty one is now carried through
split_command and write_command.
A proxy that failed after the tool had started launched a second copy of
it. launch answers an error from run by handing the terminal to the
tool itself, which is right when the pseudoterminal could not be set up
and wrong once spawn has returned: raw mode, the descriptor clones and
the read loop can all fail with the tool already running, and the first
copy was then left in a pseudoterminal nobody was reading while a second
started against the same profile. Everything that can fail past the
spawn has left the tool with no terminal, so there is nothing to keep;
the child is killed and reaped before the error goes up.
Size a notice from a width a wide terminal cannot overflow
notice_height multiplied by 64 before widening, and the width it was
given is a u16, so a terminal past 1023 columns wrapped around before
the division ever happened. A debug build panics on it; a release build
takes the wrapped number and sizes the popup from nonsense. Widening
first costs nothing and the arithmetic no longer depends on how wide the
terminal is.
Say what copying a profile's settings does to the status line
AGENTS.md said statusLine was the one key never copied. It has not
been since indicator::inherit existed: the entry cannot travel because
it names the profile it was installed for, but the status line beneath
it is the user's own and does travel, wrapped in the new profile's
statusline --with. Dropping the key outright is what that change
stopped doing, and README.md has described the current behaviour all
along, so this was the one file left disagreeing with both the code and
its tests.
Also add workspace.rs to the layout table, which has been missing
since the per-directory binding landed.
Make the README easier to find your way around
The prose was fine; getting to the right paragraph was not. Six hundred
lines and thirty headings had no table of contents, so add one grouped
by what a reader came for rather than by document order.
Replace the hand-drawn interface mock with a real one. It was drawn by
hand and had drifted: one footer row was a column short of the others
and three rows were off centre, which is a poor first thing to see. The
new block is the picker's own output captured at eighty columns, with
the sign-in glyphs set to a mix worth illustrating.
Lead with what a profile keeps and what it shares, since that is the
question the tool exists to answer, and put three commands above the
screenshot so the shape of it lands before the scrolling starts.
Give Install a table of the four channels. It read as eight alternating
paragraphs and code blocks with nothing saying which one to take. Break
the forty-eight line block under Command-line usage into named groups,
and turn the launch commands into a table pairing each tool with its
alias and an example of passing arguments through.
Rewrite the Quick start steps, which overlapped and disagreed with each
other about which key does what, and fold two reference passages into
details blocks rather than leaving them across the reader's path.
Windows notes lists three differences, so stop calling them two.
Add every agent Orca runs, step aside under Orca, and a launcher in the picker
Ditto now launches the 29 coding agents Orca runs that it did not already:
Gemini CLI, Qwen Code, OpenClaude, Copilot, Cursor Agent, Grok, Devin, Kimi
Code, Cline, Codebuff, Continue, Command Code, Hermes Agent, OpenClaw,
Mistral Vibe, Rovo Dev, Amp, Droid, Goose, Aider, Crush, Kilo Code, Kiro,
Auggie, Antigravity, MiMo Code, Ante, Trae, and Autohand. Each is one entry
in src/tools.rs: which variable, XDG bases, or private HOME points it at a
profile, which file means it is signed in, what is shared and what is
copied once. One Tool::Generic arm in each module reads the entry. They are
launched by name through one external subcommand, listed by --help, and
picked up by status, paths, sync, and shell-init automatically.
Under Orca (ORCA_PANE_KEY) the pseudoterminal proxy is skipped, since Orca
reads the foreground process and the anchored titles the proxy rewrites,
and the default profile follows an ambient CODEX_HOME so Orca's own Codex
account switcher composes with Ditto.
The picker gains a launcher: Enter or t lists every installed tool,
filtered as you type, with its sign-in state. Tool shortcuts wrap on narrow
terminals. The private-home facade generalises to nested paths and never
mirrors a directory that holds part of the profile.
Also brings in fx support: a private home with the user's own mirrored into
it, and the Keychain refused so the login stays inside the profile.
Install
brew install reyanshgupta/tap/ditto-cli # or: brew upgrade ditto-cli
npm install -g @reyanshgupta/ditto-cli@0.4.0
cargo binstall ditto-cli # or: cargo install ditto-cliThe archives below are the same binaries every channel installs; SHA256SUMS covers them.
Full Changelog: v0.3.7...v0.4.0
v0.3.7
What changed
Install
brew install reyanshgupta/tap/ditto-cli # or: brew upgrade ditto-cli
npm install -g @reyanshgupta/ditto-cli@0.3.7
cargo binstall ditto-cli # or: cargo install ditto-cliThe archives below are the same binaries every channel installs; SHA256SUMS covers them.
Full Changelog: v0.3.6...v0.3.7
v0.3.6
What changed
Add Pi profile support
Install
brew install reyanshgupta/tap/ditto-cli # or: brew upgrade ditto-cli
npm install -g @reyanshgupta/ditto-cli@0.3.6
cargo binstall ditto-cli # or: cargo install ditto-cliThe archives below are the same binaries every channel installs; SHA256SUMS covers them.
Full Changelog: v0.3.5...v0.3.6
v0.3.5
What changed
Install npm with trusted publishing support
Install release npm without elevated permissions
Improve update progress UI
Repair the links a skill installer writes through Ditto's own
Sharing a directory by linking it has one cost, and nothing was paying
it. A skill installer puts the skill somewhere central and records it in
each agent's skills directory as a relative link, computed from the
configuration directory that agent was pointed at. Under a profile that
directory is one of shared.rs's links, so the kernel creates the record
in the user's own directory instead, where the same relative path climbs
past the root and lands on nothing. The skill downloads intact and is
readable from nowhere, in every profile at once, for every tool whose
skills directory Ditto shares.
Ditto is what moved the directory, so Ditto is what can say where those
links meant to point: read the target against the path the installer was
handed rather than against where the link ended up. Nothing is rewritten
unless reading it that way names something that exists, so an absolute
link, a relative one that already resolves, and a directory the profile
owns outright are all left exactly as they are.
A launch repairs the tool it is about to start, which is the moment
before that tool reads its skills, and says so on stderr rather than
rewriting a user's links behind their back. sync does every tool at
once and reports them under repaired.
Install
brew install reyanshgupta/tap/ditto-cli # or: brew upgrade ditto-cli
npm install -g @reyanshgupta/ditto-cli@0.3.5
cargo binstall ditto-cli # or: cargo install ditto-cliThe archives below are the same binaries every channel installs; SHA256SUMS covers them.
Full Changelog: v0.3.4...v0.3.5
Ditto CLI 0.3.4
What’s new
Prime Agent profiles
- Added first-class Prime Agent support through
ditto-cli prime-agentand itspaalias. - Added Prime Agent to the profile picker with the
ashortcut and to generated shell integrations. - Added Prime Agent paths and authentication state to Ditto’s human-readable and stable JSON reports.
- Prime Agent sign-in now opens
/logindirectly; sign-out opens/logout. - Managed profiles isolate Prime Agent’s coding-agent configuration and sessions while sharing reusable prompts, skills, extensions, themes, packages, and global harness state.
Compatibility
- Made Prime Agent session-path handling and tests portable across Windows, macOS, and Linux.
Known upstream limitations
Prime Inference still stores its configuration in the user-wide ~/.prime/config.json, and Prime Agent’s background daemon socket remains user-wide. Ditto documents these boundaries rather than claiming isolation that Prime Agent does not currently expose.
Full changelog: v0.3.3...v0.3.4
v0.3.3
What changed
Say that the tag is what publishes a release
The publish workflow fires on a pushed version tag and on nothing else, so
a "Release X.Y.Z" commit left on main publishes nothing and reports no
failure, because no run happened. 0.3.2 sat unpublished on main for that
reason. Write the tag step down where a release is prepared, along with
how to confirm the release actually landed and how to retry one whose
later jobs failed.
CLAUDE.md stays a pointer rather than a second copy, but names the tag
rule directly, since that is the part that gets missed.
fix(herdr): let herdr see the agent Ditto launched
herdr works out which agent is in a pane from the name of that pane's
foreground process, and what that agent is doing from the title the tool
writes. Running the tool in a pseudoterminal costs it both answers at
once: the foreground process becomes ditto-cli, the tool moves to a
session of its own where herdr does not look, and the title arrives with
ditto:<profile> in front of it, which no longer matches rules anchored
to the first character — Claude Code's spinner rule is one. A pane
running Claude Code listed no agent at all, and every herdr agent
command lost sight of it.
Reporting the agent over herdr's socket does not answer this. A
lifecycle report is accepted and then ignored unless herdr detected the
agent itself, so the only thing that brings detection back is not being
in front of the tool. Under herdr Ditto therefore hands the terminal
straight over, exactly as DITTO_NO_PROXY already does, keyed off the
HERDR_PANE_ID that herdr sets in every pane it opens.
That leaves the profile unsaid, so it goes to herdr as display-only pane
metadata, which is the one kind herdr takes from a source that is not
the agent. Failure there is ignored down to herdr not being installed:
a label is worth much less than a launch, and Ditto is in the middle of
one. Nothing is reported about the agent, which is the detection Ditto
has just stepped out of the way of.
Distribute through npm, and let cargo binstall find the archives
Anyone running Claude Code or Codex already has npm, and it is the one
channel that reaches macOS, Linux and Windows at once. npm cannot publish
a single package carrying four platforms' binaries, so a release is five
of them: a wrapper every platform installs, and a binary package per
platform that npm passes over unless os and cpu match. The binary
packages publish first, because a wrapper reaching the registry ahead of
the optional dependencies it names is a version that installs and then
has nothing to run.
They are rendered from the archives the release published rather than
built a second time, on the same reasoning as the tap: what npm serves is
byte for byte what the releases page serves. The scope is there because
plain ditto-cli on npm belongs to an unrelated project.
cargo binstall needs no job at all, only the archive names written down,
since the ones it guesses are not the ones the workflow writes. It earns
its four lines: a source build compiles a bundled SQLite before it links
anything.
ditto-cli update would have shelled out to cargo for a copy cargo
cannot replace, leaving a second one in Cargo's bin directory and the
choice between them to whatever PATH reached first. The launcher says
how this copy arrived, and update names the npm command instead. --git
asks for a source build in so many words, and still gets one.
Install
brew install reyanshgupta/tap/ditto-cli # or: brew upgrade ditto-cli
npm install -g @reyanshgupta/ditto-cli@0.3.3
cargo binstall ditto-cli # or: cargo install ditto-cliThe archives below are the same binaries every channel installs; SHA256SUMS covers them.
Full Changelog: v0.3.2...v0.3.3
v0.3.2
What changed
Distribute through Homebrew, and say what the crate is for
The GitHub description and the crate description had fallen behind the
tools Ditto supports, naming only Claude Code and Codex. Both now say what
someone searching for this would type, and homepage points at the
project page rather than repeating repository.
Homebrew installs the binary the release workflow already builds, so it
needs no Rust toolchain. The formula lives in reyanshgupta/homebrew-tap
and is rendered there on each release from the template added here, with
checksums read from the release's own SHA256SUMS so it cannot describe
archives nobody published. A release does not fail when the tap token is
missing; the channel is optional and says so.
update only replaces a copy in Cargo's bin directory, so the README now
sends Homebrew installs to brew upgrade rather than leaving them to
discover a second, shadowed binary.
Render the Homebrew formula from the tooling, not the tagged tree
The tap job checked out the release tag, so its first real run — against
v0.3.1, a tag cut before the formula tooling existed — died with exit 127
looking for a render script that was not in that tree, and the tap has
been updated by hand ever since. The formula is rendered from a version
number and the checksums the release itself published, so it needs
nothing from the tag's own tree. Checking out the ref the workflow runs
from instead costs a tag push nothing, and lets a fix to the template be
re-run against a tag that predates the fix.
A tap that has never carried the formula would have gone quiet in the
same way for a different reason: git diff reports no change in a file
Git is not yet tracking, so the first release to a fresh tap would have
called itself up to date and pushed nothing. The guard now reads
git status --porcelain, which sees the untracked file.
Share what a profile is not signed in as
Pointing a tool at a profile's configuration directory moved everything
in it, not just the account. A profile started with none of the skills,
subagents, slash commands, hooks, or plugins its owner had set up once
and expected everywhere, so it was a different working environment
rather than the same one signed in as somebody else.
Those paths are now linked back to the user's own configuration by
shared.rs, for every tool: Claude Code, Codex, opencode and OMP. Links
rather than copies, so a skill written tomorrow is in every profile the
moment it is saved, with nothing to sync and no copies to drift apart.
Credentials and session state stay inside the profile, which is the
whole of what a profile keeps to itself.
What is shared is a named list rather than everything-but-the-known-
credentials. Learning about a new extension directory late costs a
missing feature; sharing a new credential file by accident would cost
the isolation the tool exists for, and only one of those is worth
risking. A test asserts no account-bearing name is ever on the list, and
another asserts that deleting a profile unlinks rather than following a
link out into the configuration it points at.
The status line was lost the same way for a different reason: the entry
names the profile it was installed for, so settings::copy skipped the
key outright and a profile's indicator had nothing to draw in front of.
What travels now is the status line underneath it, with the new
profile's own indicator wrapped around that, which is what the Alongside
outcome was built for and never reached. settings.json stays a copy
rather than a link because Ditto writes that key into it, and linking
would mean writing it into the user's own file.
Profiles made before this have real directories where the links go.
sync reports them and leaves them alone; --adopt points them at the
user's own, moving what was there aside rather than deleting it.
Install
brew install reyanshgupta/tap/ditto-cli # or: brew upgrade ditto-cli
npm install -g @reyanshgupta/ditto-cli@0.3.2
cargo binstall ditto-cli # or: cargo install ditto-cliThe archives below are the same binaries every channel installs; SHA256SUMS covers them.
Full Changelog: v0.3.1...v0.3.2
v0.3.1
What changed
docs: say each thing about the README once
Windows behaviour, the default-profile fallback rules, and opencode's XDG
layout were each explained in two or three places, which is two or three
places to keep true. Each now has one home that the others link to, and
the jq recipes that were copies of AGENTS.md give way to a pointer at it.
feat(indicator): keep the status line a profile already has
Claude Code draws one status line, so a profile that had its own could
either keep it or show the profile. Ditto chose theirs and said so, which
was the safe answer and never the one anybody wanted.
indicator --keep-mine now installs Ditto in front of it instead: the
command it writes carries theirs as an argument, runs it with the payload
Claude Code sent, and prints the profile ahead of every line it draws. A
command that fails or says nothing leaves the profile marker on its own,
--off reads the original back out and puts it in place untouched, and a
launch still never changes an arrangement it was not asked to change.
Two things around it that could take a status line away:
- Recognition was a command containing
ditto-cliand ending in
statusline, which claimed a script somebody called
ditto-cli-statuslineand then deleted it on--off. What Ditto writes
is the binary and the subcommand as separate words, so that is what is
recognised. - A status line set for a project or by an administrator outranks the
profile's, leaving Ditto's installed and never drawn.indicatorsays
shadowedrather than reporting it as on.
feat(settings): carry your own Claude Code settings into a profile
Snapshot of the uncommitted work from the other session, committed here so
the two lines of work can be merged rather than interleaved on disk.
Install
brew install reyanshgupta/tap/ditto-cli # or: brew upgrade ditto-cli
npm install -g @reyanshgupta/ditto-cli@0.3.1
cargo binstall ditto-cli # or: cargo install ditto-cliThe archives below are the same binaries every channel installs; SHA256SUMS covers them.
Full Changelog: v0.3.0...v0.3.1