Elyra Desktop
Elyra Desktop is not being continued. There will be no new versions of it. Use Elyra Workspace instead: a native app where you run Elyra, and Claude Code, Codex, Pi and other agents beside it, in a thread for each task, with Git, a browser, a terminal and your files in the same window. Workspace runs the terminal edition of Elyra, so install that once with
npm i -g @elyracode/coding-agent; it reads the same~/.elyra/agent/directory, so your settings and credentials carry over, and the sessions you had in Desktop stay there, to continue in the terminal edition. Workspace is for macOS on Apple silicon; on Linux, use the terminal edition. The terminal edition goes on as before. See Getting started with Elyra Workspace.
Elyra Desktop is the Elyra coding agent in a window: the same agent loop, tools, providers, sessions and extensions as the terminal edition, built with Rust (Tauri 2) and Svelte 5. It reads and writes the same ~/.elyra/agent/ directory — settings, credentials, sessions, skills, prompt templates, themes, memory and decision logs — so you can run both against the same projects and switch between them mid-session.
It is free to use, like the terminal edition, and was downloaded from elyracode.com/elyra; the source is not published.
Which edition?
Terminal (elyra) |
Desktop | |
|---|---|---|
| Install | npm i -g @elyracode/coding-agent |
A disk image or AppImage/deb from the download section |
| Runs on | macOS, Linux, Windows, anywhere Node 20+ runs | macOS on Apple silicon; Linux on x86_64 and ARM64 |
| Sessions, settings, skills | ~/.elyra/agent/ |
The same directory, unchanged |
| Extensions | Node on PATH |
A bundled extension host; extensions themselves still need Node 20+ on PATH (or ELYRA_NODE) |
| Updates | npm update -g or /update |
/update, or the banner on start; signed and installed in place |
| Themes | The six reference themes | The same six plus Tokyo Night, with Elyra as the default (dark as the fallback); custom themes in ~/.elyra/agent/themes/ hot-reload |
Everything the terminal edition can do, the desktop edition does with the same slash commands: /tree, /rewind, /fork, /compact, /goal, /review, /bisect, /replay, /learn, /share, /doctor, /decisions, /config, /update. The usage guide applies to both.
What the desktop does that the terminal cannot do well
Since 0.2.0, three things — each of them a panel rather than a scrollback.
Search every session you ever had — /archive
A derived SQLite/FTS5 index over your JSONL sessions, kept at ~/.elyra/agent/index/sessions.sqlite and refreshed incrementally at startup, when a session closes and when the panel opens. Full-text search across every session, for this project or all of them, by role; the history of one file ("every time we touched settings.rs"); and cost reports by day, model, session or project. The JSONL files stay the source of truth — delete the index and it is rebuilt. elyra archive exposes the same index from the terminal edition.
Several agents on one repository — /worktrees
Start a second agent on its own branch (elyra/<name>) in a git worktree under ~/.elyra/agent/worktrees/, switch the transcript between the live sessions, and merge the result back file by file. The changes are collected with a temporary index — the agent's own staging area is left alone — and applied to the project tree with git apply --3way, uncommitted, for you to review. A file that also changed in the project is skipped unless you ask for a three-way merge.
Parallel agents can see each other (0.4.1). Every agent appends what it just wrote to one board per repository — ~/.elyra/agent/worktrees/<repo>/intents.jsonl: the file, the symbols its changed lines sit in, their signatures. Each model call then carries a short list of what the other agents have touched in the last hour, and an edit into a symbol another agent is holding comes back with a note naming the agent, its branch and the symbol. A shared symbol whose signature the two changed differently is called out as the conflict a merge cannot reconcile. It is advisory and never blocks an edit; intentBoard turns it off. The worktrees panel shows the same rows as Editing right now.
Look symbols up instead of reading whole files — symbols
Rust, TypeScript, TSX, JavaScript, Python, Go, PHP and C files are parsed with tree-sitter and their definitions and references kept in ~/.elyra/agent/index/symbols/. The symbols tool answers "where is X defined, who calls X" with file:line and signatures, so the model reads only the relevant range rather than the file; /symbols <name> and elyra symbols <name> expose the same index. read outlines large Rust, Go, PHP and C files with it. symbols is in the default active tool set beside read, bash, edit and write — and since 0.4.1 so are why and history, below.
What the model actually gets — /context, cmd+i
Since 0.4.0. The context window as a stacked bar and a block list: the system prompt by section (base instructions, AGENTS.md files, memory, decisions, skills), the tool definitions, pinned files, and every conversation message — each with a token estimate, shown next to the provider's last reported usage so you can see how far the estimate is from the truth.
Any conversation block can be excluded from the next model calls without deleting it. Three things keep that honest:
- The exclusion is recorded in the session as a custom entry, so it is part of the history and survives a reload.
- A tool result that is excluded is replaced by a short stub rather than removed, so the tool call it answers stays valid — providers reject a call without a result.
- The transcript marks excluded entries, so what you see and what the model sees never silently diverge.
This is the manual counterpart to /compact: compaction summarises what you no longer need, the inspector lets you decide, block by block, and shows you the cost of each choice before you make it.
What it costs (0.4.1). Every block carries its price at the model's input rate and at its cache-read rate, with the provider's own split for the last call beside it — new tokens, read from cache, written to cache. Below the bar, concrete suggestions with the money attached: a pinned file nothing has read in ten turns, tool results holding more than half the conversation, compaction past 55 percent of the window. Cache hits are not attributed to single blocks, because prompt caching is prefix-based with at most four breakpoints; those totals are shown as the provider reports them. A finished reply in the transcript shows what it cost, too.
What the model is told, and when a turn counts as done
Since 0.4.2. The machinery around the model was the strong part; this is the other half.
A prompt that says what the harness knows
The system prompt used to be the reference's: a role sentence, the tool list, "be concise". Elyra's own prompt says what the model otherwise had to infer — reads are cheap in parallel; a failed edit usually means the file changed after the read, not that the text was wrong; the project has its own type checker and test command; why and history can be asked instead of guessing; a failing test is reported as failing. Four short sections, plus tool notes that appear only when the tool is active. systemPromptStyle: "reference" builds the old text byte for byte, and every prompt carries a version (elyra-1) stamped on each routing-ledger turn, so /routing-stats compares the two on success rate, edit failures, retries and cost per turn. A prompt change is a measurement, not a matter of taste.
The prompt names the project's own checks
Since 0.5.0, as in the terminal version (0.9.38), the typecheck, lint, test and build commands found in package.json (with the lockfile's package manager), composer.json, Cargo.toml, pyproject.toml and go.mod are listed under the technology stack, so "verify your work" points at the exact commands. The Elyra prompt said the project has its own test command and did not say which; it does now. The prompt version in the routing ledger is elyra-3, and systemPromptStyle: "reference" follows the terminal's rules as of 0.9.45 and is stamped reference-0.9.45, so turns before and after the change are compared separately.
The turn is checked before it is called done — verify
A turn ended when the model decided it was finished, which is not the same as the code compiling. After a turn that actually wrote files, Elyra runs the check the project already has — cargo check; your check or typecheck script with the right package manager; phpstan or psalm when the binary is there; go vet; mypy or ruff when pyproject.toml configures them — and hands a failure back to the model as work to finish, with the tail of the output. Once per instruction (verify.maxAttempts); never for a turn that only read code or wrote Markdown, never while a /goal runs, and never a formatter or a test suite unless you name one in verify.command. verify.enabled: false turns it off.
The same failure twice says so
A failed tool result went back to the model exactly as written, so "the text to replace was not found" arrived for the third time with nothing to say it was the third time. Failures are counted per call — tool, subject, and a fingerprint of the error that ignores line numbers and byte counts. The second identical failure carries one line naming what to change; the third says to stop retrying and solve it another way or say what is blocking. Nothing is blocked, no arguments are rewritten: it is information the model was missing, not a new rule.
Old tool output stops being paid for — contextPruning
A 50 KB file read on turn 3 used to be sent again, in full, on turn 40. Tool results older than ten turns are sent as one line naming the tool, its subject and roughly how much was dropped, so the model knows the answer existed and can run it again. Nothing is lost — the session file, transcript and archive keep it all, and the context inspector marks those blocks pruned with the saving. The boundary moves in steps of ten turns rather than sliding every turn, because rewriting a message invalidates the prompt cache from that point on, and a cache read costs a tenth of fresh input: one miss buys ten cheap turns. Short results and results carrying an image are left alone. contextPruning.enabled: false restores the old behaviour; keepTurns, batchTurns, minBytes adjust it. Since 0.5.0 old tool output that ended in an error is no longer pruned: it records what did not work, and recovering it would mean repeating the failure.
An instruction from here on — /system
/system answer in Norwegian from now on — or tests must pass before committing — adds an instruction to the session. It is kept in the session file, replayed when you resume, carried through compaction, and reaches the model wrapped in <system-reminder> tags that the system prompt explains as instructions rather than something you said. Because it is appended to the end of the conversation it leaves the prompt cache intact, and the transcript shows it as a system instruction. Extensions get the same through elyra.sendSystemMessage(text, { display, triggerTurn, deliverAs }), and sessions from the terminal version that carry such instructions behave the same here.
Switching a tool off keeps the prompt cache
The tools block is the root of Anthropic's cache and part of OpenAI's hashed prefix, so removing a tool made the next request pay for everything again. A switched-off tool now stays in the request and the system prompt, and the model gets a clear refusal if it calls it anyway; a tool switched on is added at the end, so nothing already cached moves. stableToolSet: false sends only the active tools, as before, and the context panel counts what is actually sent.
Compaction reaches the model
Until 0.4.5 it did not. The session converted its history for the provider with the bare agent's default, which keeps only your messages, the model's replies and tool results, so after a compaction the model got neither the summary nor anything before it, the output of a !command never reached it, and branch summaries and every message an extension sent were lost. That had been so since the first build. The session now uses the coding agent's conversion, as the terminal version does.
Skills that say whether they are about this project
Twenty skills arrived as twenty names in load order. Elyra now names the project — detected frameworks, stack profile, the languages its manifests are unambiguous about — and lists first the skills that name one of them. Matching is on whole words, so go inside google is not Go. Nothing is hidden, and the prompt says so.
Thinking per turn, and a router that reads its own ledger
Thinking was one switch for the whole session. It is now scaled per turn inside the level you set: a powerful turn gets all of it, an ordinary one a step less, a short question answered with read-only tools none. Your level is a ceiling never exceeded, off stays off, and /route shows what the next turn would use (thinkingAuto: false brings the single switch back). And the routing ledger written after every turn — stop reasons, failed edits, retries, failovers — now decides something: inside a tier, models are ranked by what those turns measured before price is considered, so a cheap model that fails a third of its edits stops being the default pick. Five measured turns minimum, your pins still win, and the reason names the model passed over and why.
MCP servers, built in — /mcp
The servers you already configured for Claude Code, Cursor, VS Code or Elyra are available to the agent from 0.5.0, as in the terminal version, and no Node is needed. Elyra reads ~/.claude.json, ~/.cursor/mcp.json and ~/.elyra/agent/mcp.json, and in trusted projects .mcp.json, .elyra/mcp.json, .cursor/mcp.json and .vscode/mcp.json, and gives the agent two tools, mcp_search and mcp_call, so the tools block stays the same however many servers you have. Servers start only when a tool needs them, and tool lists are cached in the same file the terminal version uses. Local servers get a minimal environment, not your shell's API keys; remote servers work over streamable HTTP and the older SSE transport.
/mcp shows every server, skipped project files and config problems, and does refresh, enable, disable and reload; "mcp": false in settings turns it off. A server that requires OAuth logs in with /mcp login <server>, and the agent offers the login itself when a call needs it; the login is shared with the terminal version, and /mcp logout forgets it. "directTools": true on a server also gives the agent each of its tools by name, mcp__<server>__<tool>. Project files are only read in trusted projects, because they define commands that run on your machine.
A decision model beside the coding model — decide, /jev
Since 0.4.3. Three questions the coding model was answering badly, or expensively, or not at all: is this shell command destructive, how much model does this instruction need, and — afterwards — did the run actually do what was asked. TypeSafe Jev is a decision model, not a chat model: give it a piece of content and typed questions and it answers with calibrated probabilities in about a second for about $0.00002. It never writes prose.
The decide tool asks it noul (yes/no with a probability), choice (one option plus the whole distribution) or score (an ordinal scale) — several questions at once for the price of one — so classifying, triaging or gating no longer costs a main-model turn. It works through TypeSafe directly (TYPESAFE_API_KEY) or through the OpenRouter key you already have, preferring the first; jev.backend, jev.model and jev.baseUrl override that, and the JEV_* variables from the terminal edition still work. The tool only appears when a key is configured. /jev shows the active backend, both key states and the gate.
A second opinion before a destructive command — /jev gate on. Every shell command is first sent to Jev with one question — would running this destroy data or make a change that is hard to reverse — and anything at or above jev.bashGateThreshold (0.7) goes through the same confirmation dialog the rule-based guard uses. Off by default, because it adds about a second per command, and it fails open: no key or a failed call is reported and the command runs. This is a second opinion, not a security boundary — the rule-based guard is the boundary, and it does not consult anyone.
Smart routing stops guessing from keywords. The tier for a turn used to be decided by matching words like refactor and architect, so refactor the spelling of this one variable bought the most expensive model and the checkout flow drops every third order bought the cheapest. With a key and smart routing on, Jev classifies the instruction instead — one call, cached per instruction — and the reason line says what it decided and how sure it was. The signals that read the turn rather than the message (a tool error, many edits, a huge result) still escalate; those are evidence, not opinion. If Jev cannot be reached, the keyword rules take over for that run. jev.routing: false keeps the old behaviour.
The ledger can record whether the work got done — jev.turnLabels. Everything measured about a turn has been mechanical: stop reason, failed edits, retries, cost. A model that cheerfully answers the wrong question scores perfectly, and comparing two system prompts compares their error rates. With turn labels on, each finished run is judged afterwards — completed, partial, blocked or failed, plus whether the final message claimed something works without the run containing anything that would show it — and the verdicts land in the routing ledger, where /routing-stats reports them per model and per system prompt, including what a completed run actually costs on each. It runs after the turn, so it costs no waiting. It is off by default, and this is the one place the ledger leaves the machine: a label sends the instruction, the final message and the list of tool names with their targets. File contents are never sent.
Getting around, and knowing where a line came from
Since 0.3.0.
Project switcher — cmd+p
Every folder with sessions on disk, most recently used first, with fuzzy search; Enter continues that folder's most recent session. ctrl+p pins a project to the top (desktop.pinnedProjects). The list is derived from ~/.elyra/agent/sessions/ rather than kept as state, so a folder you only ever opened from the terminal edition is there too — and a folder that has been moved, deleted or sits on an unmounted disk is shown as missing rather than failing to open.
File explorer — cmd+1
The project tree on the right, .gitignore honoured, listed one directory level at a time as you expand folders, so a large repository costs nothing until you open it. Clicking a file appends @path to the editor — which is how you point the agent at a file. Off by default; cmd+1 or desktop.fileExplorer turns it on, and it remembers.
Line provenance — "why is this line here?"
/view <path>, or a click in the explorer, opens a file viewer, not an editor: the file as it is on disk, with layers you switch on — which turn wrote each line and what it was asked to do, which lines are uncommitted, and identifiers that jump to their definition. Selecting lines is the point: Enter mentions @path:12-40 in the editor, Ask why sends the question, and Change… prefills the editor so the agent makes the change and the provenance chain stays whole. Writing from the window is deliberately absent — a human write would bypass the mutation queue and leave lines behind with nothing to explain them; Open in the system editor is there for when you do want to type. It works for files git does not track and for projects that are not repositories. For commits Elyra made, the gutter attributes each line to the turn behind it: the prompt, the model, and what it cost.
Three pieces make that work, and none of them stores a line number:
git blame --porcelain -Cresolves a line to a commit on demand, following moves between files — so an edit above the line later cannot invalidate the answer.- The
Elyra-Turns:commit trailer —Elyra-Turns: <sessionId> <entryId>,<entryId>— resolves that commit to a session and its turns. Errored turns are left out; they changed nothing a line can descend from. A trailer rather thangit notes, because a trailer is part of the message and survives rebase and push without per-clone configuration. Off withgitTurnsTrailer. - The session archive resolves those turns to the prompt, model and cost.
A commit written outside Elyra is shown as exactly that, not left blank. Long windows write one trailer line per 16 turn ids, and the reader concatenates them — until 0.4.1 the trailer stopped at 32 ids and abbreviated the rest, losing the provenance of every later turn for good.
Two more trailers. Elyra-Prompt: carries up to 120 characters of the instruction behind the commit, so git blame reads back to a prompt on a machine that does not have the session archive — the turn ids alone resolve to nothing there. Prompt text therefore lands in the git history; gitPromptTrailer turns it off. And Elyra-Reviewed: records that a person signed off: marking a commit reviewed writes an empty commit carrying one trailer per commit, with the repository's git identity as the reviewer, so the record travels with a clone. The index and the working tree are left untouched. In the viewer every agent-written line whose commit nobody has reviewed carries a dot in the gutter, and the header counts the share of agent-written lines with a recorded review. An LLM /review does not count; only an explicit human action does.
Two tools that read the chain — why and history. why(path, line) blames the line and answers with the instruction behind the commit, the model, what the turn cost and the decisions recorded for the file; where the archive is missing, the commit's own Elyra-Prompt still answers, and a commit made outside Elyra is reported as such. history(path | symbol) lists the earlier turns that read or wrote the file, newest first, with the instruction behind each. Both are in the default active tool set, so the model can check whether odd-looking code was deliberate before rewriting it.
Changes by intent — /intent, /pr. A branch reviewed by what was asked for instead of by file: each group is one instruction, with the commits that answered it, the files and hunks they touched, the model, what the turns cost, the decisions recorded in that session and the test commands that ran green. Commits without an Elyra-Turns trailer are kept in their own group and named as written outside Elyra. /pr turns the same data into a pull-request description — from the trailers and the archive, with no model call — and the panel copies it.
edit says when the file changed after you read it. read stamps every line with a content hash (#xxxx | ), and edit checks them before matching: a hash no longer anywhere in the file means the file changed after the read; a hash that does not match the text sent with it means the line was rewritten after copying; hashes all still present mean an insertion or a move broke the block apart. The edit still fails and writes nothing, but the message says which of the three it is.
The transcript's width — desktop.transcriptWidth
The transcript is capped at 880 px and centred, because a fixed measure keeps prose and code readable. On a very wide window that leaves a gap; set desktop.transcriptWidth to another cap, or to 0 to fill whatever the sidebar and explorer leave.
About — /about
Or click the version in the title bar: the version, the website, and who made it. No repository link; the repository is private.
Trust, secrets and the commands that do damage
Since 0.4.1. Four things that used to be true and are not any more.
Opening a folder no longer runs its code — /trust
A repository can carry .elyra/extensions (TypeScript with full Node access) and a .elyra/settings.json that sets how commands run: the shell binary, a prefix prepended to every bash command, the npm argv, extra extension paths. Cloning and opening a repository used to be enough to run all of it. Now those keys are ignored, and no extension inside the project is loaded, until you trust the folder — however it was discovered. Since 0.4.5 that covers the project's packages too: a .elyra/settings.json can list them, opening the folder used to resolve and install them (a git clone of the repository's choosing, or an npm install whose lifecycle scripts run whenever npmIgnoreScripts is off), and the gate missed it. Project packages, extensions, autoExtensions and autoSkills now wait with the shell settings — the same set the terminal version gates since 0.9.45 — and nothing is installed, updated or loaded for them until the folder is trusted. The app asks when a folder actually carries something, lists exactly what would run, and since 0.5.0 gives the terminal version's four answers: trust this folder, trust its parent folder (which covers every project in it), trust it for this session only, or continue without its code. /trust offers the same choices when a project is restricted, /trust revoke takes trust away (and says so if a parent folder still covers the project) and /trust list shows every trusted folder. From the shell: elyra trust [path], elyra trust --status, --list and --revoke [path]. elyra trust with no arguments now trusts the current folder, as the terminal's does; it used to show the status, which is elyra trust --status now, and the earlier allow, status, list and revoke words still work. elyra install -l trusts the project, since adding a project package yourself is that decision, and elyra update, elyra list and elyra config say when they skip an untrusted project's packages instead of leaving them silently out. elyra doctor preflight says what is being withheld. Reading files, AGENTS.md, skills, prompts and themes are unaffected. The desktop and the terminal keep the decisions in one file, ~/.elyra/agent/trusted-projects.json, readable only by you; since the terminal's own project trust they wrote it in two formats and each erased the other's decisions on its next save, so the desktop now reads and writes the terminal's format (and converts its old one on the next change). A trusted folder covers everything below it. --trust-project trusts a project for a single run, for scripts and CI where no prompt can be shown; unlike ELYRA_TRUST_PROJECT, the commands the agent runs do not inherit it. Package installs run with --ignore-scripts (npmIgnoreScripts turns it back on for packages that must build a native binding).
Commands the model runs no longer see your keys
The whole process environment used to be handed to every command, so a single env put provider keys, GITHUB_TOKEN, AWS_SECRET_ACCESS_KEY and database URLs into the transcript, the session file and the archive index. Variables whose names read as secrets are now withheld from tool commands and from the extension host, while everything a build needs passes through — SSH_AUTH_SOCK included, so git push keeps working. The bash tool tells the model it cannot read them. Your own !command is untouched; shellEnvAllow and shellEnvDeny adjust the rule, and shellEnvPolicy: "inherit" restores the old behaviour. The read, write and edit tools also refuse the canonical credential files (Elyra's auth.json, SSH keys, AWS and gh credentials, .netrc, .npmrc, .git-credentials, Docker and Kubernetes configs) — bash can still reach them, so this removes the accident rather than the ability.
One question before the commands that do damage — bashGuard
A short list is confirmed rather than just run: a recursive force-delete that leaves the project, sudo, a download piped into a shell, git push --force, git reset --hard, git clean -f, history rewrites, writing over a disk, chmod 777, publishing a release, changing what the machine runs later, appends into your home configuration. The dialog says which command and why, and a no goes back to the model as a reason so it can propose something narrower. Ordinary work is never interrupted; bashGuard: "off" turns it off, and headless hosts run everything as before. A command that arrives without a timeout is killed after ten minutes (bashDefaultTimeout, 0 for none).
/share asks before uploading, and takes the secrets out
The command used to export the session and create a gist immediately, with whatever the tools had printed: .env contents, env output, bearer tokens, connection strings. Now the export is scanned first and anything unmistakably a credential is replaced with a label, and you approve the upload with the size, the entry count and a list of what was removed in front of you. The dialog is explicit that the gist is private but readable by anyone with the link, and that the same content stays on this machine in the session file, the archive index and any HTML export. The redaction is a net with holes, not a guarantee — if you shared a session before 0.4.1, read the gist.
When something goes wrong
/bug <description> writes a bug report (0.5.0, as in the terminal version): version, OS, model and thinking level, session counts, the last tool calls, loaded extensions and their load errors, and the last error if a turn failed. Keys, tokens and your home directory are taken out. The report is shown with a copy button and saved to a file; unlike the terminal version it does not open a GitHub issue, because the desktop has no public issue tracker.
There is a log (0.4.1). Events go to one file per day under ~/.elyra/agent/logs — five kept, the folder readable only by you — and to the terminal as well when there is one. Panics are logged with their location and a backtrace before the process dies; errors inside the interface land in the same file and say so with a toast rather than leaving a silently frozen panel. elyra doctor logs prints the path and the last lines; /debug reports it too. The log holds Elyra's own events only, never prompts or tool output.
Errors say what to do. A missing API key, no model selected, a provider that is rate limiting, an extension host that will not start and a restricted project each render with an explanation and the one or two buttons that fix them — sign in, choose a model, reload extensions, trust the folder, try again, show the log. A crashed extension host that recovers says so.
The stores survive a bad day. Rewriting a session (migration, fork, move) goes through a temporary file and an atomic rename, so a crash mid-write cannot truncate the session everything else is derived from. A session index that cannot be opened is moved aside and rebuilt, the unreadable file kept and named in the log. The background index refresh no longer holds the shared handle, so /archive stays responsive while a large archive is indexed.
Ubuntu 22.04, and a command that ran out of time. Until 0.4.1, stopping a timed-out or cancelled command ran kill -9 -<pgid>, and the kill Jammy ships (procps-ng 3.3.17) reads that form as a signal to the caller's group: Elyra died and the command lived on. It is a killpg syscall now. The new CI gate found it — its runner is Ubuntu 22.04, and the test binary, cargo and the runner's shell vanished together, four times, without a log.
Small things. cmd+f filters the session by words — what was said and what was run, not tool output. Every message and every code block has a copy button. cmd+c copies again; clearing the editor is ctrl+c as in the terminal, and undo puts a cleared draft back. A half-written prompt survives a restart, per project. A notification arrives when a turn finishes and you are elsewhere — nothing while the window has focus; desktop.notifyOnFinish turns it off.
Installing
macOS
Download the disk image, open it and drag Elyra Desktop to Applications. The build is for Apple silicon, signed with a Developer ID and notarized by Apple, so it opens without warnings. No Intel build is offered: it has not been run on Intel hardware, and shipping one nobody has run would be worse than shipping none.
Linux
Two packages per architecture, x86_64 and ARM64 (aarch64):
-
AppImage — one file, no installation. Make it executable and run it:
chmod +x Elyra-Desktop-*-linux-x86_64.AppImage ./Elyra-Desktop-*-linux-x86_64.AppImageThis is the package the in-app updater replaces, so use it if you want
/updateto work. -
deb — for Debian and Ubuntu (22.04 or later, which provides WebKitGTK 4.1):
sudo apt install ./Elyra-Desktop-*-linux-x86_64.deb
Both need libwebkit2gtk-4.1, which apt pulls in for the deb and the AppImage expects on the system.
Checking what you downloaded
Every download has a .sha256 beside it on the download page:
shasum -a 256 Elyra-Desktop-0.1.0-arm64.dmg
Updates
Elyra Desktop checks https://elyracode.com/elyra/desktop/latest.json for a newer version. /update downloads and installs it and restarts; the banner on start offers the same when a release is installable. Every update is verified against a public key compiled into the application before it is installed — a manifest or a download that does not carry the right signature is refused, whatever it says.
Release notes are in the Elyra Desktop changelog. The terminal edition's are in the Elyra changelog; the two are versioned separately.
Providers and credentials
All nine provider protocols — Anthropic, OpenAI completions and responses, OpenAI Codex, Azure OpenAI, Google, Vertex, Mistral, Bedrock — and the OAuth logins (Anthropic, OpenAI Codex, GitHub Copilot, Google Antigravity, Gemini CLI, Cloud Code) with a token refresher. Credentials are stored where the terminal edition stores them, so a login in one is a login in the other. See Providers.
Extensions
Elyra Desktop ships its own extension host, a Node sidecar that loads the same TypeScript extensions the terminal edition does over a JSONL protocol. It is bundled with the application; the extensions it loads run under the Node 20+ found on your PATH (or named by ELYRA_NODE). An app started from Finder or the Dock inherits the bare system PATH, not your shell's — so since 0.3.1 Elyra Desktop asks your login shell for its PATH once at startup, merges it in, and adds the usual tool directories (Homebrew, ~/.npm-global/bin, nvm, volta, bun, cargo) where they exist. Before that, a Finder launch could not find npm or node at all.
Since 0.5.0 an extension can register a tool with { defer: true } and remove it with unregisterTool, as in the terminal version (0.9.45): a tool that appears or disappears mid-conversation waits for the next prompt-cache boundary (the cache expiring, a model switch, or applyPendingToolChanges()), so the cached prompt survives, and changing MCP tool lists use the same mechanism. ctx.isProjectTrusted() lets an extension that runs project-supplied configuration refuse while the folder is untrusted; when the agent writes a project extension in an untrusted folder it is told the extension will not load until you run /trust.
And until 0.3.2 the bundled host never started from the installed app at all — five toasts of extension host exited before replying (restart). The host decided whether it had been started as a script by comparing process.argv[1] with its own module URL, and the URL kept %20 for the space in Elyra Desktop.app while the argument had a real space; the check failed and the process exited silently. Real paths are compared now, and when the host does exit before replying, the message carries the exit code and its last lines, so a missing module or an old node is visible rather than a bare (restart). Extension tools, commands, dialogs, status lines and widgets appear in the window. /reload re-discovers extension paths; /config turns individual extensions, skills, prompts and themes on or off.
Extensions that shell out to the terminal edition (elyra -p …) need it installed separately: the desktop bundle carries the extension host, not the elyra command.