Your coding agents, side by side, by voice. Claude, Codex & Grok in one desktop app, with Jev for the fast decisions.
See the codeYour coding agents, side by side, by voice. Claude, Codex and Grok in one desktop app, with Jev (TypeSafe) for the fast decisions. Jauvex Personal, version 1.1, for macOS; Apache License 2.0. Source: github.com/reindent/jauvex; site: jauvex.reindent.com. Made by Reindent (one human and agents).
Jauvex is an Electron client for the Claude Code, Codex and Grok Build sessions on your Mac. Add a folder, pick up any of its sessions or start new ones with either provider, and talk to them: a voice channel that answers in three beats (a quick word, what it understood, a summary of the agent's answer), steers a working agent without interrupting it, names and starts agents by voice, and lets agents talk to each other inside the app. No server: the window talks to the main process over IPC, and your sessions stay where Claude Code and Codex keep them.
One command in Terminal:
curl -fsSL https://jauvex.reindent.com/install | sh
It needs Node 22.18 or newer. It downloads this source, checks its SHA-256 and builds Jauvex on your Mac: nothing
prebuilt is downloaded, so there is nothing for Apple to notarize. Jauvex lands in Applications (~/Applications when
/Applications is not writable), a real app with its own name, icon and microphone permission; the source and the build
stay in ~/.jauvex/personal/app. Run the command again to update, with Jauvex closed, or let Jauvex do it: it asks
jauvex.reindent.com which version is the latest (/api/personal/version), at launch and every six hours, and when a newer one is out
the sidebar's footer says so ("1.2.0 is out") and the Jauvex agent asks you, once per version, whether to update now. On a yes
(or "update the app" at any time) it runs node scripts/jauvex.ts update: refused while other agents work (--now on your word);
otherwise the app fetches the same install command, checks that it installs the version offered, leaves it and a small runner in
~/.jauvex/personal/update/, hands the runner to launchd and quits. The runner waits for the app to exit, runs the install command
(it rebuilds the app on your Mac and opens it), opens the old app again if it does not finish, and removes it; its log is
update/update.log. Only the app the install command made updates itself; a clone updates with git (T-165). To remove it, quit it and delete
Jauvex.app and ~/.jauvex/personal/app; its settings stay in ~/.jauvex/personal. To work on the code, clone this
repository instead: npm start runs it from the clone, and npm run app makes the same app in tmp/mac-app/Jauvex.app
(scripts/mac-app.ts: Electron's app renamed Jauvex, with the built app, the Whisper models, Claude and Codex inside, signed
ad hoc on your Mac).
Jauvex needs a few things that are not in this repository; the install command and npm start take care of most of them. The welcome screen (on the first start, and from Jauvex
settings after) checks each of them and tells you what is missing.
npm install in this folder (it also fetches the Electron binary; if the app ever says
Electron.app does not exist, run npx install-electron).npm install (the Claude Agent SDK
brings Claude Code, @openai/codex brings Codex), and Jauvex uses the account each one is signed in to on this Mac.
You sign in with their own command lines, in Terminal: claude auth login (Claude Code; to install it,
curl -fsSL https://claude.ai/install.sh | bash) or codex login (Codex; brew install codex). Jauvex signs no one in
itself: Anthropic does not let apps built on its Agent SDK offer the Claude.ai login. The welcome screen and the accounts
panel (the Jauvex button below the sidebar) say who is signed in and give the command; with one signed in, the Jauvex
agent can walk you through the other.curl -fsSL https://x.ai/cli/install.sh | bash,
then grok login). Jauvex runs it as grok agent stdio with your Grok account and settings; without it, Grok is simply not
offered.npm start installs it with Homebrew if it is missing and downloads the two models into
models/ (git-ignored): ggml-small-q5_1.bin for the transcript and ggml-base-q5_1.bin for the live words while you
speak. Each is checked against the size and SHA-256 Hugging Face lists for it (scripts/models.sh): a download goes to a
.part file and becomes the model once it is whole, so one cut short is downloaded again the next time, never taken for done. Other models from huggingface.co/ggerganov/whisper.cpp can be dropped in the same folder and picked in the
voice settings (ggml-large-v3-turbo-q5_0.bin, 574 MB, hears better and is still quick on Apple silicon). Without
Whisper you can still type.TYPESAFE_API_KEY or the file ~/.typesafe/token, the key alone (the way Hugging Face keeps its token; the older ~/.typesafe/jev still works). Without it the small voice model
makes the decisions Jev would make, a little more slowly.say. On a fresh Mac that is the basic Samantha, which sounds robotic:
pick a Siri voice in System Settings > Accessibility > Spoken Content > System voice. An app cannot choose a Siri
voice by itself (say -v falls back to Samantha); only that setting reaches them. The welcome screen checks this and
has a button that opens the pane.npm start # builds, then launches through macOS LaunchServices (start.sh)
npm run dev # Vite + Electron with reload, for working on the UI
Put the folder somewhere plain, such as ~/Coding/jauvex: macOS protects Documents, Desktop, Downloads and iCloud Drive,
and an app started there cannot read its own files until it has been granted access (npm start then starts it from the
terminal instead, which works but attributes the permission prompts to the terminal). Only one copy may run at a time
(the app holds a lock; a second launch focuses the first). It keeps its own state in its data folder, ~/.jauvex/personal
(every copy, run from source or compiled; data/ below means that folder; CVC_DATA_DIR moves it), and uses ports 4340 (Vite, dev only) and 4341 (whisper-server), plus 4342 for the live words (Pro uses 4320 to 4322, so both can run side by side). Jauvex is built on
your Mac from this source: npm start runs it from the Electron binary in node_modules, the install command as Jauvex.app.
@lobehub/icons-static-svg, MIT; TypeSafe's is its site icon, assets/typesafe.png). A new session
lets you pick the provider in the composer until the first message is sent; the model and effort pickers follow the
provider (effort: Claude's fixed levels, or the levels Codex or Grok reports for the chosen model; applies from the next message)
(Codex models come from your account through model/list, Grok's from its agent's model list).git pull, build, relaunch, on request), install what is missing, explain how it works, create agents and
add folders (through the app's command line, below), and develop it for contributors. Every other agent is told it
exists and to send it what concerns running the app, and that the app's code is theirs to work on too when the user
asks and the source is in their folder (a Codex agent made a development agent once refused to read it, told that
anything about the app was the Jauvex agent's). It runs on the default agent, keeps one session for life
(ui.jauvexSession), and can be hidden in Jauvex settings (it still exists and still answers other agents).
It moves between providers with its whole context (the only session that does, for now): the app keeps its own
transcript of it (data/jauvex-transcript.json, user and assistant text only), the provider selector in its composer
stays live, and the first message after a move carries the conversation so far as a prelude to a fresh session on the
other provider, so Claude and Codex pick up where the other left off. Moving back works the same way. The thread shows
the app's transcript, tool calls and results included (results trimmed), with a note at every move. A move ends voice
mode (the voice engine and its acknowledgment model belong to the provider it started with): start it again when you
like. Two ways to move, asked on the welcome when both providers are signed in and changeable in Jauvex settings:
unified (the default, experimental: the app replays the whole conversation, and more can break) or handover
(the leaving assistant writes a handover note in a visible turn, saved as data/jauvex-handover.md too, and the
next provider starts from that note: cheaper on long histories, and the note says what mattered).node scripts/jauvex.ts <command>, in the install folder): every action in the app, for
agents, the Jauvex agent above all. list (folders, sessions, agents, ids), add-folder, pick-folder (the folder
dialog for the user; what they choose is added), new-agent (provider, folder, name, purpose, first message; unnamed,
the provider is the Jauvex agent's own; with no first message it starts with its own introduction, since an agent exists
once it has had one: on 2026-09-24 one ordered without it was an empty chat that vanished), open (a session, or the Jauvex agent, on screen; a session not in its folder's sidebar goes in it too: shown only as an open chat,
seven sessions an agent opened dropped out one by one as other chats took the eight open places, T-152, the user, 2026-09-26), import (a folder's existing sessions into its sidebar without opening them,
as its search button does; with no session named, the ones not listed yet: T-150, the user, 2026-09-26, after an agent asked to
import sessions opened them over the chat he was in), send (a message into a session),
rename, settings (default agent, Jauvex row, welcome next time), welcome, reload, restart. It writes a
request file in data/commands/, the running app does the thing and answers in a result file, the script prints the
JSON. Nothing but files: no port, no server.<webview> of their own that starts muted and stays muted and opens no windows. The pane has an "open outside"
button (the Mac for files, the browser for pages), a close button, and a draggable width that is remembered. A navigation
the app did not catch (a link in a place it does not watch) is stopped in the main process and sent to the pane too.
Later the same pane takes terminals and browsers. Relative paths resolve against the session's folder.data/uploads/. Typed while a turn runs, they queue with their text: each queued bubble keeps the images
pasted with it (shown as thumbnails), goes out with them when the turn ends or when its Send now hands it to the running
turn, and the sent bubble shows them. An image never rides along with another queued message. PNG, JPEG, GIF and WebP.noul yes/no, choice, score). Right: the output, each answer with its probabilities as bars, the
confidence, the model, milliseconds and tokens; "Raw JSON" shows the reply as it came. Evaluate or Cmd+Enter. Edits save
themselves, the last 20 runs stay one click away, and the agent lives in the sidebar (filled dot) with rename and
delete. Jev stores nothing, so agents and runs are kept in data/state.json. A new agent starts with a working example.
Trainer (optional): Jev cannot be talked to, so an agent can be coupled with Claude or Codex ("Trainer" next to
Evaluate). A chat panel opens under the pad, with everything a chat has here: text, voice, models, effort,
permissions. The trainer sees the state, the questions and the last output with every message, and changes the agent
by answering with jev-state, jev-questions and jev-evaluate blocks, which the app applies and runs; it is shown
the result (twice at most per request) so it can adjust. Its session stays out of the sidebar. The agent is a reusable
classifier: the questions stay, the state changes.data/state.json and come back with it, across restarts. A new session starts from the last choices made anywhere.renameSession for Claude, a
custom-title entry in the session file; thread/name/set for Codex), so Claude Code and Codex show the same name.query({ resume })). Replies stream in; Stop interrupts; the pencil
icon on a project starts a new session in that folder; the model picker applies to the next message.
Tools that need permission show an Allow once / Always / Deny card in the thread, or pick Auto permissions in the
composer: Claude's permission classifier (permissionMode: 'auto') or Codex's automatic reviewer
(approvalsReviewer: 'auto_review') decides, and only what it will not decide reaches you. Applies from the next message.
YOLO — full access (asked for 2026-09-26) runs tools with no permission prompt and no provider sandbox: Claude's
bypassPermissions with its required opt-in, Codex's never approvals with dangerFullAccess, Grok's yoloMode. Agents can
then use everything this account can reach; it grants no administrator access, and the operating system's own limits still apply.
Provider permissions (Jauvex settings › Safety, or settings --permission-provider claude --permission-mode yolo on the
command line) set Ask, Auto or YOLO for every session of one provider, from each session's next turn; the composer then shows that
mode, locked; "Use session setting" (--permission-mode session) gives the choice back to each composer. Leaving YOLO gives Codex
back the approvals and sandbox the thread had before it, a read-only one included, kept in the state across restarts.
Codex sessions work the same way: replies stream, Stop interrupts the turn, and when Codex asks before running a
command or writing files, the same card appears (Always = for the rest of the session). Approval policy and
sandbox are whatever your Codex config says, the way the Claude side keeps Claude Code's settings, except in YOLO.
Grok sessions work the same way too: replies stream, Stop cancels the turn, and when Grok asks before a tool its
permission rules do not allow already, the same card appears (Always = Grok's own "don't ask again"). The rest is Grok's
own permission rules; the app says the mode every time a session is opened (Ask, Auto permissions, Grok's automatic mode, or
YOLO), so a Grok set to always approve still asks in Ask, and a new mode closes and reopens a loaded session to take it. A message handed to a working Grok is read at its next step (x.ai/interject); one handed over after its last step
Grok runs as a prompt of its own, and the turn stays open until that one is answered too, so its answer is not lost.
A picture Grok makes (its imagine tool) shows under the tool's row, and the answer's link to it points at the file Grok saved
in its own session folder: Grok writes it relative to that folder (images/1.jpg), which read against the project's folder
was a broken image.Press the white round button in the message box. All local except the two Claude calls:
brew install whisper-cpp), kept warm as whisper-server. Put a model in models/
(ignored by git): curl -L -o models/ggml-small-q5_1.bin https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-small-q5_1.bin
(190 MB, about 0.4 s per utterance on Apple Silicon). Transcription starts the moment you go quiet, so the text is
ready when your pause ends. When whisper-server cannot start, the app says why within a second, on the welcome screen and
under the orb: a model it cannot load (named, with the command that downloads it again) or a port another program holds
(named, with that program). whisper-server prints its error and then, on the Mac's GPU, a crash backtrace as it exits; the
app reads the error, and the flight recorder keeps all it printed. The welcome calls Whisper ready only once its server answers.models/ to use (Automatic = the fastest), and
"Names it should know". The names, plus the open folder's name, go to whisper-server as its initial prompt at
start-up (the per-request prompt field is ignored by the server), so changing either restarts it. Measured on this
Mac with test phrases: small without names heard "Clothex / Cotex / Reigned-in"; small with names got every name
right in about 0.55 s; large-v3-turbo took about 2.2 s and still missed them. Whisper's ghosts are dropped: handed a knock or room noise it writes real phrases ("Thank you."), so a transcript is
discarded when Whisper itself was unsure (very low confidence, a known ghost phrase at low confidence, or a no-speech
probability over 0.6 on text that is short or doubtful: a real sentence Whisper was sure of is never dropped on that score alone,
a short greeting once scored 0.74 with every word right); a clearly spoken "thank you" still goes through, the debugger shows
every drop and why, and a dropped sentence of three words or more leaves a note under the composer instead of vanishing. The closing pause is trimmed before
transcription, and every dictated message reaches the main model tagged [voice transcript] (T-76: in the turn, when steered, from the
queue, and on a resend; the window never shows the tag), while a message typed with voice mode on goes untagged: the briefing tells
the model a tagged message is a transcript, to repair names and odd words, and to ask in one line when a word does not fit.say (the system voice by default), rendered per utterance and played inside the app, so it can
fade out instantly and the echo canceller knows what the speakers are playing.say renders are
killed) and it never talks over you. The main thread is not touched: it keeps working, and its answer is still summed
up when it lands. What you type (Enter) while it is busy shows up as a dashed "Queued" bubble and is sent by itself
the moment the turn ends; what you say is steered into the running turn (next point). The voice also weighs what you meant: a clear "stop, cancel that" interrupts the main
thread ("I'll take care of that right away"), and "not that, do X instead" interrupts it and sends X next. In doubt
it queues; work is never stopped on a guess. A short utterance with a stop word ("stop", "cancel that", "para") never
waits for that judgement: it interrupts the main thread straight from the transcript, already from the speculative
one taken 240 ms into your pause. Otherwise only the Stop button interrupts the main thread. Tapping the orb also shuts the voice up.npx vite build and this is enough; a change to
electron/* or shared/* still needs the full restart.data/state.json is overwritten).```message-agent Notes
What is the code word of the day?
When its turn ends the app delivers the text to that session, tagged `(from agent "Sender" [id])`: steered into the
running turn if that agent is working, sent as a new turn otherwise (the session is mounted in the background if it
was not open). Whatever the other agent replies comes back to the sender by itself, tagged the same way, so an
explicit message is a question and no block is needed to answer it; an answer does not bounce back again, so two
agents cannot ping-pong on their own (and the app stops relaying after 30 agent-to-agent messages in ten minutes).
A reply that goes back to its sender leaves out the blocks it addressed to other agents (they went to them) and says who
else was written to. A Claude agent that left a background job running (a shell command in the background, a monitor)
used to swallow the next message: its next turn answers only the job's "stopped" notice, empty, in under a second, which
read as "the third message between agents fails". A Claude turn that ends like that sends its message again, once, with
its reply address; and every agent is told not to start background work from a turn.
This block is the only channel between the app's agents, in both directions, Claude to Codex and Codex to Claude; the
briefing says so plainly, and that the harness's own agent tools, chat skills and shared files never reach them (a Claude
agent once set up a file-based chat protocol instead). A Claude session also gets the same channel as two tools of its
own, `mcp__jauvex__message_agent` and `mcp__jauvex__list_agents` (an in-process MCP server from the Agent SDK, answered
by the window's router, never asking permission), for the models that look for a tool when told "talk to X". Agents are addressed by name (title or summary) or by the short id in brackets, which never changes (6 characters,
longer only where two ids share their start, as Codex ids often do); a fenced `list-agents` block gets the roster back
(name, id, provider, folder, working or idle). The Jauvex agent is listed once, as "Jauvex", whatever its past sessions
across providers, and always, its chat open or not (T-151, the user, 2026-09-26: an agent that wanted to report a bug to it found
three "Jauvex …" agents and not it, since it was listed only while its chat was open): a message to it opens its own chat in the
background, with its session, or starts one; with no session yet, its folder's id stands in for its id; a session with no name of its own is shown by its first message, cut at 60 characters. Every exchange shows in both
threads, marked "From agent X", and in the voice log. The briefing tells every agent all of this.
clientBriefing in shared/types.ts; appended to Claude's system
prompt, sent as Codex's developer instructions): several agents side by side, messages that arrive mid-turn are new
information to fold in (not a restart), the full answer is on screen while a separate small model speaks a short
version (so: conclusion first, short plain answers for simple questions), a turn can be stopped at any moment, and in
voice mode the text is dictated. It names no product, so a rename does not touch it.claude auth login|logout, codex login|logout, grok login|logout); Refresh after using it. The app has no login of its
own: it uses the sign-in of each provider's own tool on this Mac (Claude: claude auth status on the SDK's bundled binary;
Codex: the app-server's account/read; Grok: its agent's x.ai/auth/info). Signing in, out and switching from inside the app is built (the provider's
browser flow, run from the panel and the welcome) but hidden in this edition (SIGN_IN_IN_APP in shared/types.ts,
since 2026-09-24): Anthropic does not let apps built on its Agent SDK offer the Claude.ai login. Sessions are files on this
Mac and stay whichever account is signed in; a running turn keeps the old account until it ends
(tests/window/sign-in-by-cli.test.ts)./usage (the SDK's experimental usage request, asked of a short-lived
idle process: nothing is sent to a model); Codex's come from account/rateLimits/read, Grok's from its agent's x.ai/billing. Only percentages, reset
times, the plan's name, extra usage and the credits balance are read, never account IDs.message.usage, the result's modelUsage[model].contextWindow); Codex's is thread/tokenUsage/updated (the last request's total
against the model's usable window). The numbers are kept with the session (Project.context), so the meter shows them when the
chat opens. A click opens the numbers and a Compact button; the click itself compacts nothing. Compacting is a turn of its own
(Claude Code's /compact, Codex's thread/compact/start) sent with the last turn's settings, so the provider's cached prompt still
applies; a typed /compact does the same. While it runs the sheets pulse and the thread says so; when it is over, a note in the
thread (kept with the session's notes) says it, with the tokens before and after, and so does the flight recorder, whoever started
it (the provider compacting on its own mid-turn included). Auto-compact (Jauvex settings, Context; settings --auto-compact <percent>|provider) is 90 % by default: past it, the chat compacts after the answer, before the next message goes. Claude Code's
own trigger is moved to the same share of the window (CLAUDE_AUTOCOMPACT_PCT_OVERRIDE, scaled, since Claude Code counts it against
the window less the room it keeps for the answer; it can only lower Claude Code's threshold), so a long turn compacts in the middle
too; Codex compacts mid-turn at its own limit (90 % of its model's window, about 95 % on the meter). A message that does not fit
at all ("Prompt is too long", Codex's contextWindowExceeded) is compacted for at once and sent again, once. Why: on 2026-09-23 a
Claude session sat at 962K of its 1M window, the next message pushed it over, and the messages after it failed with "Prompt is
too long" until it was compacted by hand; no meter had shown how full it was, and nothing said it was compacting. Anthropic gives
no recommended percentage (Claude Code's own default is about 967K of a 1M window); 90 % leaves room for the next message.ggml-base, its own server on the next port, asked about once a second and never while the real transcript is being
made), because whisper-server answers one request at a time and the transcript that counts must not wait. Both servers
run half a second of silence at start-up, so the first sentence does not pay for loading the model onto the GPU. Before
starting its own, the app stops any whisper-server a previous run left on its two ports (found by port, checked by command
line, stopped by its own pid): the app dies without them when it is killed, and each restart used to leave a pair behind.
Only this install's are stopped (their model is in its own models/): on 2026-09-24 the other edition, started next to this
one on the same ports, stopped both of this one's servers, and the wake phrase went with them. The editions use ports of
their own; one that finds its port taken by another install's server leaves it and says so in the flight recorder.data/voice-debug.log (rolled at 2 MB), including
the fate of every line that was meant to be spoken (said, or NOT said and why).turn/steer. When the turn ends, queued messages leave one at a time,
each as its own turn, in the order they were typed, never merged into one; a spoken instruction that replaces the
work goes first. The queue, its images and the text typed but not sent are kept on disk per chat: a reload of the
window (an agent's touch data/reload-ui) or a restart never loses a message, and a queue restored with no turn
running goes out at once.stopSaysMore in
shared/orders.ts), and a bare stop is kept with the session's notes. A message handed to a running turn that the model never
read goes again once the turn is over, without a second bubble: after a stop or a replace, everything handed to that turn
(Claude Code drops what it had not taken up, and twice is better than never); after a turn that ends on its own, what was
handed over once its last answer had started (unsent on the turn's end, from chat.ts). Why: on 2026-09-23 a message said
just before a stop, and one said as a turn was ending, were shown as sent and never answered.shared/orders.ts): the exact phrase
("restart the app"), or Jev at 0.85 or more. When nobody is sure (Jev leaning, unsure or absent, the voice model reading an
order) it asks in one short line, shown and said ("Should I open a new agent in website? Say yes to open it; anything
else goes to the agent as you said it"): a clear yes carries the order out, anything else sends the words it asked about to
the agent, as they were said. "Give me the handoff instructions so the other agent can work on this" once opened a new
agent that way. Typed messages get the same treatment while voice mode is on. The order
may follow a few other words ("Okay, let's see if this works. Make a new Codex agent."): it is looked for sentence by
sentence, and inside a longer message it is only acted on when Jev is sure it is for the app. "... named X" / "call it X"
names the agent: a Jev agent at once, a Claude or Codex session when its first turn ends (it shows in the title bar
from the start), written to the provider like any rename.
"... about X" / "for X" is what the agent is for. Jev only says that this is an order to open an agent; the details
are language, so the session's own model reads the sentence once and returns them as JSON (kind, name written as a
person would title it, purpose, folder), overriding what the rules found, and it also writes the new agent's first
message for that order (opened from the app, which folder, name and purpose in the user's intent, other agents may
be at work, change nothing yet, look around, say what it understood and proposes, ask, then wait). The new agent is
never left blank: a session only exists once something is sent. The agent opens at once with what Jev and the rules
found; the model's reading arrives a few seconds later (command:details) and the name and first message follow it.
If the model gives nothing within 30 s, the rules and the static kickoffMessage in shared/types.ts stand. An
agent born from an order never keeps its first message as its title: without a name it becomes "Codex agent" or
"Claude agent". The order itself stays in the thread it was given in, with a note of what the app
did. The microphone does not move: there is one in the whole app, it stays with the session it was started in while
you look at or type into others (that session's row shows a small waveform), and it only moves when you start voice
somewhere else. While you look at another session, its orb waits at the bottom of the sidebar with the session's
name, the phase, and mute / silence / end buttons; the name takes you back. The floating bar that appears over other
apps while voice is on (the tiny bar) has a move handle (four arrows) that shows when the bar is hovered: drag it to
move the bar, and nothing else drags it. It comes back where it was left. Its keyboard button slides a typing box open,
the bar growing with it: Enter sends the text to the listening session the way its composer would (queued while a turn
runs), without talking and without bringing the app forward; Escape closes it.model/list), with "Automatic (the smallest)" first: a Haiku on
Claude, the fast and affordable one on Codex. A saved id that is no longer offered, or one the API refuses, falls back
to the smallest, and the settings say which model is really in use. Nothing about model names is hard-coded beyond the
tier words, so a model that comes or goes never silences the voice.TYPESAFE_API_KEY, or ~/.typesafe/token),
the voice channel's decisions go to Jev, TypeSafe's System One model (electron/jev.ts): it does not write text, it
answers typed questions with probabilities, in about 250 ms against 1 to 3 s for the voice model. It decides: whether
your thought is finished when you pause (a clear "no" holds the words, joins them to what you say next and sends one
message; held at most three times, 4.5 s each). A long dictation is closed at its next short pause once it passes 6 s, and at 12 s whatever the pauses, and the next words join it (not counted as a hold): every
transcription pass costs by the length of the audio, and a paragraph re-transcribed at each pause took seconds a pass, so the words
landed late and the thought was cut in two. The fullest text any pass of a stretch heard is remembered: a final pass that heard far fewer words (a 14 s pass once came back as "I" where earlier passes had heard two sentences) is retried once, and the fuller text wins. Whole transcripts Whisper invents from near-silence ("Thank you for your time.", "Thanks for watching") are dropped whatever their score. The audio of every pass stays in data/voice-audio (the newest 120 files, never sent anywhere), named in the recorder, so a lost sentence can be replayed. One speculative pass at a time: a pause while the previous pass still runs skips it, whether it was a question or a thank-you (answered at once with
the voice answers in three stages: the quick line at once (Jev's fixed phrase, prepared during the pause), then the understanding (the voice model, told the quick line it comes after so it never repeats it, from the request and the last of the conversation: one or two sentences that reinforce what was said without parroting it; skipped for anything under five words, or once the turn is already over), then the summary of the answer when it lands), and queue / steer / stop / replace while the main thread is busy, with the spoken line
picked from a fixed set, in English, never the same line twice in a row. "Decisions" in the voice settings hands all of it back to the voice model. Below 0.6 confidence, in another language, over the 1.2 s budget, on any error, or with no key, the voice
model decides as before. The key stays in the main process: never logged, never sent to the window. CVC_JEV=off
switches it off. The voice settings say who is deciding.say command, Claude signed in, Codex signed in, whisper-server with a model, a TypeSafe key (optional),
with the command to run for whatever is missing. macOS only for now. The window is muted while voice mode is off (nothing in it may make a sound then); the welcome
unmutes it while it is open, so it is heard without starting a voice chat, and mutes it again when closed.electron/debug.ts); never keys or audio.web/) calls the Electron main process over one
IPC channel (electron/preload.ts → electron/main.ts → electron/backend.ts).~/.jauvex, not in the folder the app is installed in: Claude Code files a session under its
working folder, and renaming the install folder once left the agent pointing at a folder that no longer existed, its
conversation gone from the window. An old entry is moved there when the state loads; its briefing names the install folder
and the command line by its full path. When its session cannot be found where it lives, the conversation stays on screen
(the app keeps its own transcript), a note says so, and the next message starts a new session carrying the conversation
so far, the way a move to the other provider does. The app's state is read once and kept in memory, so two changes at the
same moment can no longer overwrite each other (a cleared session once came back, and the agent's entry vanished).~/.jauvex/personal (electron/paths.ts): the state, the window's profile, the logs and the command
files, for every copy, run from source or compiled, outside the folder the app is installed in, so an update or a new copy keeps
everything; data/ in this README means that folder. One data folder, one copy running (the lock in main.ts); CVC_DATA_DIR moves it (the checks).data/notes/<session>.json,
after the message they followed; in the transcript of an agent that moves between providers), so a restart no longer wipes
them. And the app only takes an order when that order is the whole request: "publish and restart the app" goes to the agent,
which does the work and then restarts the app itself (node scripts/jauvex.ts restart; every agent is told so).tests/wake-check.test.ts).@anthropic-ai/claude-agent-sdk): listSessions({ dir }),
getSessionInfo, getSessionMessages. Same data the claude CLI uses, typed, with no CLI scraping and
no parsing of ~/.claude by hand. The same SDK's query({ resume }) is the path for sending messages later.
Claude Code keeps its sessions in ~/.claude, or wherever CLAUDE_CONFIG_DIR points: the app starts through macOS, not from a
terminal, so it takes CLAUDE_CONFIG_DIR and CODEX_HOME from the login shell along with PATH (without that, a user who had
moved Claude Code's folder saw his Codex sessions listed and none of his Claude Code ones). When a folder lists no Claude Code
session, the debug panel says where the app looked.codex app-server (electron/codex.ts): the JSON-RPC interface, one JSON object per
line over stdio, that Codex's own clients use. thread/list from Codex's state database (useStateDbOnly: the
default scan of the rollout files leaves out the threads the desktop app's agents created, which the desktop still
lists), kept when a thread ran in the folder or in a Codex worktree of it (~/.codex/worktrees/<id>/<basename>,
recognised by the basename and the folder's git origin, never by reading Codex's folders); thread/items/list,
thread/start / thread/resume, turn/start, turn/interrupt, plus the approval requests the server sends.
Same login, config and session store as the Codex CLI, nothing in ~/.codex parsed by hand. The binary is the
@openai/codex npm dependency; codex app-server generate-ts --out <dir> prints the protocol types for the
installed version. One server process starts on first use and is shared by every Codex chat.grok agent stdio (electron/grok.ts): the Agent Client Protocol (JSON-RPC 2.0, one object per
line over stdio) that Grok Build speaks to editors, with its own x.ai/* extensions (Grok Build's source is public:
github.com/xai-org/grok-build). session/list per folder; session/load replays a session's history as updates marked
isReplay, and a session the app is not running is closed again after reading, so a Grok window that goes on with it never
races a stale copy here; session/new / session/resume, session/prompt, session/cancel, session/set_config_option
(model, effort), the session/request_permission requests, x.ai/interject, x.ai/session/rename. Grok takes the app's
briefing once, when a session is made (_meta.rules, folded into its system prompt), so the note on dictation is always in
it. One agent process with --no-leader (the app's own, never the leader a Grok window on the Mac may share), started on
first use, and never when no grok is on the Mac. Same login, config and store as the Grok CLI (~/.grok), nothing there
read by hand. Its plan's credits come from x.ai/billing, what Grok's own usage view reads (the share of the period's included
credits used, the period, on-demand spending, bought credits, the plan's name). Not yet: a Grok row on the welcome screen.electron/chat.ts routes each turn by the session's provider; every provider sends the UI the same ChatEvents.ephemeral thread with a read-only sandbox, so nothing lands in your Codex history; for Grok
sessions Grok's fast model, in a session of its own in the system's temporary folder with the voice's prompt as its whole system
prompt: Grok keeps every session, so the voice's is deleted when a new one is made, and its id is kept in the data folder for the
next run to delete when the app quit before Grok answered; each can be changed in the voice settings). About 1.3 to 1.8 s per line once warm; voice mode warms it up. It only
speaks for the main thread: every message it gets starts with a MAIN: line naming the
main thread's provider and model, it has no identity of its own, and asked "what model are you?" it relays the main
thread's answer instead of naming itself (tmp/e2e-voice.ts checks this for both providers).~/.jauvex/personal/state.json.npm start goes through open so macOS attributes privacy permissions (the microphone, later) to
the app instead of the terminal.Apache License 2.0: see LICENSE. The NOTICE file names the authors: whoever passes Jauvex on, changed or not, passes its
notices along with it (section 4 of the license). Versions before 0.2 were published under the MIT License and stay under it.
6 commits
TypeScript
98.4%
Shell
1.6%
Your coding agents, side by side, by voice. Claude, Codex & Grok in one desktop app, with Jev for the fast decisions.
See the codeYour coding agents, side by side, by voice. Claude, Codex and Grok in one desktop app, with Jev (TypeSafe) for the fast decisions. Jauvex Personal, version 1.1, for macOS; Apache License 2.0. Source: github.com/reindent/jauvex; site: jauvex.reindent.com. Made by Reindent (one human and agents).
Jauvex is an Electron client for the Claude Code, Codex and Grok Build sessions on your Mac. Add a folder, pick up any of its sessions or start new ones with either provider, and talk to them: a voice channel that answers in three beats (a quick word, what it understood, a summary of the agent's answer), steers a working agent without interrupting it, names and starts agents by voice, and lets agents talk to each other inside the app. No server: the window talks to the main process over IPC, and your sessions stay where Claude Code and Codex keep them.
One command in Terminal:
curl -fsSL https://jauvex.reindent.com/install | sh
It needs Node 22.18 or newer. It downloads this source, checks its SHA-256 and builds Jauvex on your Mac: nothing
prebuilt is downloaded, so there is nothing for Apple to notarize. Jauvex lands in Applications (~/Applications when
/Applications is not writable), a real app with its own name, icon and microphone permission; the source and the build
stay in ~/.jauvex/personal/app. Run the command again to update, with Jauvex closed, or let Jauvex do it: it asks
jauvex.reindent.com which version is the latest (/api/personal/version), at launch and every six hours, and when a newer one is out
the sidebar's footer says so ("1.2.0 is out") and the Jauvex agent asks you, once per version, whether to update now. On a yes
(or "update the app" at any time) it runs node scripts/jauvex.ts update: refused while other agents work (--now on your word);
otherwise the app fetches the same install command, checks that it installs the version offered, leaves it and a small runner in
~/.jauvex/personal/update/, hands the runner to launchd and quits. The runner waits for the app to exit, runs the install command
(it rebuilds the app on your Mac and opens it), opens the old app again if it does not finish, and removes it; its log is
update/update.log. Only the app the install command made updates itself; a clone updates with git (T-165). To remove it, quit it and delete
Jauvex.app and ~/.jauvex/personal/app; its settings stay in ~/.jauvex/personal. To work on the code, clone this
repository instead: npm start runs it from the clone, and npm run app makes the same app in tmp/mac-app/Jauvex.app
(scripts/mac-app.ts: Electron's app renamed Jauvex, with the built app, the Whisper models, Claude and Codex inside, signed
ad hoc on your Mac).
Jauvex needs a few things that are not in this repository; the install command and npm start take care of most of them. The welcome screen (on the first start, and from Jauvex
settings after) checks each of them and tells you what is missing.
npm install in this folder (it also fetches the Electron binary; if the app ever says
Electron.app does not exist, run npx install-electron).npm install (the Claude Agent SDK
brings Claude Code, @openai/codex brings Codex), and Jauvex uses the account each one is signed in to on this Mac.
You sign in with their own command lines, in Terminal: claude auth login (Claude Code; to install it,
curl -fsSL https://claude.ai/install.sh | bash) or codex login (Codex; brew install codex). Jauvex signs no one in
itself: Anthropic does not let apps built on its Agent SDK offer the Claude.ai login. The welcome screen and the accounts
panel (the Jauvex button below the sidebar) say who is signed in and give the command; with one signed in, the Jauvex
agent can walk you through the other.curl -fsSL https://x.ai/cli/install.sh | bash,
then grok login). Jauvex runs it as grok agent stdio with your Grok account and settings; without it, Grok is simply not
offered.npm start installs it with Homebrew if it is missing and downloads the two models into
models/ (git-ignored): ggml-small-q5_1.bin for the transcript and ggml-base-q5_1.bin for the live words while you
speak. Each is checked against the size and SHA-256 Hugging Face lists for it (scripts/models.sh): a download goes to a
.part file and becomes the model once it is whole, so one cut short is downloaded again the next time, never taken for done. Other models from huggingface.co/ggerganov/whisper.cpp can be dropped in the same folder and picked in the
voice settings (ggml-large-v3-turbo-q5_0.bin, 574 MB, hears better and is still quick on Apple silicon). Without
Whisper you can still type.TYPESAFE_API_KEY or the file ~/.typesafe/token, the key alone (the way Hugging Face keeps its token; the older ~/.typesafe/jev still works). Without it the small voice model
makes the decisions Jev would make, a little more slowly.say. On a fresh Mac that is the basic Samantha, which sounds robotic:
pick a Siri voice in System Settings > Accessibility > Spoken Content > System voice. An app cannot choose a Siri
voice by itself (say -v falls back to Samantha); only that setting reaches them. The welcome screen checks this and
has a button that opens the pane.npm start # builds, then launches through macOS LaunchServices (start.sh)
npm run dev # Vite + Electron with reload, for working on the UI
Put the folder somewhere plain, such as ~/Coding/jauvex: macOS protects Documents, Desktop, Downloads and iCloud Drive,
and an app started there cannot read its own files until it has been granted access (npm start then starts it from the
terminal instead, which works but attributes the permission prompts to the terminal). Only one copy may run at a time
(the app holds a lock; a second launch focuses the first). It keeps its own state in its data folder, ~/.jauvex/personal
(every copy, run from source or compiled; data/ below means that folder; CVC_DATA_DIR moves it), and uses ports 4340 (Vite, dev only) and 4341 (whisper-server), plus 4342 for the live words (Pro uses 4320 to 4322, so both can run side by side). Jauvex is built on
your Mac from this source: npm start runs it from the Electron binary in node_modules, the install command as Jauvex.app.
@lobehub/icons-static-svg, MIT; TypeSafe's is its site icon, assets/typesafe.png). A new session
lets you pick the provider in the composer until the first message is sent; the model and effort pickers follow the
provider (effort: Claude's fixed levels, or the levels Codex or Grok reports for the chosen model; applies from the next message)
(Codex models come from your account through model/list, Grok's from its agent's model list).git pull, build, relaunch, on request), install what is missing, explain how it works, create agents and
add folders (through the app's command line, below), and develop it for contributors. Every other agent is told it
exists and to send it what concerns running the app, and that the app's code is theirs to work on too when the user
asks and the source is in their folder (a Codex agent made a development agent once refused to read it, told that
anything about the app was the Jauvex agent's). It runs on the default agent, keeps one session for life
(ui.jauvexSession), and can be hidden in Jauvex settings (it still exists and still answers other agents).
It moves between providers with its whole context (the only session that does, for now): the app keeps its own
transcript of it (data/jauvex-transcript.json, user and assistant text only), the provider selector in its composer
stays live, and the first message after a move carries the conversation so far as a prelude to a fresh session on the
other provider, so Claude and Codex pick up where the other left off. Moving back works the same way. The thread shows
the app's transcript, tool calls and results included (results trimmed), with a note at every move. A move ends voice
mode (the voice engine and its acknowledgment model belong to the provider it started with): start it again when you
like. Two ways to move, asked on the welcome when both providers are signed in and changeable in Jauvex settings:
unified (the default, experimental: the app replays the whole conversation, and more can break) or handover
(the leaving assistant writes a handover note in a visible turn, saved as data/jauvex-handover.md too, and the
next provider starts from that note: cheaper on long histories, and the note says what mattered).node scripts/jauvex.ts <command>, in the install folder): every action in the app, for
agents, the Jauvex agent above all. list (folders, sessions, agents, ids), add-folder, pick-folder (the folder
dialog for the user; what they choose is added), new-agent (provider, folder, name, purpose, first message; unnamed,
the provider is the Jauvex agent's own; with no first message it starts with its own introduction, since an agent exists
once it has had one: on 2026-09-24 one ordered without it was an empty chat that vanished), open (a session, or the Jauvex agent, on screen; a session not in its folder's sidebar goes in it too: shown only as an open chat,
seven sessions an agent opened dropped out one by one as other chats took the eight open places, T-152, the user, 2026-09-26), import (a folder's existing sessions into its sidebar without opening them,
as its search button does; with no session named, the ones not listed yet: T-150, the user, 2026-09-26, after an agent asked to
import sessions opened them over the chat he was in), send (a message into a session),
rename, settings (default agent, Jauvex row, welcome next time), welcome, reload, restart. It writes a
request file in data/commands/, the running app does the thing and answers in a result file, the script prints the
JSON. Nothing but files: no port, no server.<webview> of their own that starts muted and stays muted and opens no windows. The pane has an "open outside"
button (the Mac for files, the browser for pages), a close button, and a draggable width that is remembered. A navigation
the app did not catch (a link in a place it does not watch) is stopped in the main process and sent to the pane too.
Later the same pane takes terminals and browsers. Relative paths resolve against the session's folder.data/uploads/. Typed while a turn runs, they queue with their text: each queued bubble keeps the images
pasted with it (shown as thumbnails), goes out with them when the turn ends or when its Send now hands it to the running
turn, and the sent bubble shows them. An image never rides along with another queued message. PNG, JPEG, GIF and WebP.noul yes/no, choice, score). Right: the output, each answer with its probabilities as bars, the
confidence, the model, milliseconds and tokens; "Raw JSON" shows the reply as it came. Evaluate or Cmd+Enter. Edits save
themselves, the last 20 runs stay one click away, and the agent lives in the sidebar (filled dot) with rename and
delete. Jev stores nothing, so agents and runs are kept in data/state.json. A new agent starts with a working example.
Trainer (optional): Jev cannot be talked to, so an agent can be coupled with Claude or Codex ("Trainer" next to
Evaluate). A chat panel opens under the pad, with everything a chat has here: text, voice, models, effort,
permissions. The trainer sees the state, the questions and the last output with every message, and changes the agent
by answering with jev-state, jev-questions and jev-evaluate blocks, which the app applies and runs; it is shown
the result (twice at most per request) so it can adjust. Its session stays out of the sidebar. The agent is a reusable
classifier: the questions stay, the state changes.data/state.json and come back with it, across restarts. A new session starts from the last choices made anywhere.renameSession for Claude, a
custom-title entry in the session file; thread/name/set for Codex), so Claude Code and Codex show the same name.query({ resume })). Replies stream in; Stop interrupts; the pencil
icon on a project starts a new session in that folder; the model picker applies to the next message.
Tools that need permission show an Allow once / Always / Deny card in the thread, or pick Auto permissions in the
composer: Claude's permission classifier (permissionMode: 'auto') or Codex's automatic reviewer
(approvalsReviewer: 'auto_review') decides, and only what it will not decide reaches you. Applies from the next message.
YOLO — full access (asked for 2026-09-26) runs tools with no permission prompt and no provider sandbox: Claude's
bypassPermissions with its required opt-in, Codex's never approvals with dangerFullAccess, Grok's yoloMode. Agents can
then use everything this account can reach; it grants no administrator access, and the operating system's own limits still apply.
Provider permissions (Jauvex settings › Safety, or settings --permission-provider claude --permission-mode yolo on the
command line) set Ask, Auto or YOLO for every session of one provider, from each session's next turn; the composer then shows that
mode, locked; "Use session setting" (--permission-mode session) gives the choice back to each composer. Leaving YOLO gives Codex
back the approvals and sandbox the thread had before it, a read-only one included, kept in the state across restarts.
Codex sessions work the same way: replies stream, Stop interrupts the turn, and when Codex asks before running a
command or writing files, the same card appears (Always = for the rest of the session). Approval policy and
sandbox are whatever your Codex config says, the way the Claude side keeps Claude Code's settings, except in YOLO.
Grok sessions work the same way too: replies stream, Stop cancels the turn, and when Grok asks before a tool its
permission rules do not allow already, the same card appears (Always = Grok's own "don't ask again"). The rest is Grok's
own permission rules; the app says the mode every time a session is opened (Ask, Auto permissions, Grok's automatic mode, or
YOLO), so a Grok set to always approve still asks in Ask, and a new mode closes and reopens a loaded session to take it. A message handed to a working Grok is read at its next step (x.ai/interject); one handed over after its last step
Grok runs as a prompt of its own, and the turn stays open until that one is answered too, so its answer is not lost.
A picture Grok makes (its imagine tool) shows under the tool's row, and the answer's link to it points at the file Grok saved
in its own session folder: Grok writes it relative to that folder (images/1.jpg), which read against the project's folder
was a broken image.Press the white round button in the message box. All local except the two Claude calls:
brew install whisper-cpp), kept warm as whisper-server. Put a model in models/
(ignored by git): curl -L -o models/ggml-small-q5_1.bin https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-small-q5_1.bin
(190 MB, about 0.4 s per utterance on Apple Silicon). Transcription starts the moment you go quiet, so the text is
ready when your pause ends. When whisper-server cannot start, the app says why within a second, on the welcome screen and
under the orb: a model it cannot load (named, with the command that downloads it again) or a port another program holds
(named, with that program). whisper-server prints its error and then, on the Mac's GPU, a crash backtrace as it exits; the
app reads the error, and the flight recorder keeps all it printed. The welcome calls Whisper ready only once its server answers.models/ to use (Automatic = the fastest), and
"Names it should know". The names, plus the open folder's name, go to whisper-server as its initial prompt at
start-up (the per-request prompt field is ignored by the server), so changing either restarts it. Measured on this
Mac with test phrases: small without names heard "Clothex / Cotex / Reigned-in"; small with names got every name
right in about 0.55 s; large-v3-turbo took about 2.2 s and still missed them. Whisper's ghosts are dropped: handed a knock or room noise it writes real phrases ("Thank you."), so a transcript is
discarded when Whisper itself was unsure (very low confidence, a known ghost phrase at low confidence, or a no-speech
probability over 0.6 on text that is short or doubtful: a real sentence Whisper was sure of is never dropped on that score alone,
a short greeting once scored 0.74 with every word right); a clearly spoken "thank you" still goes through, the debugger shows
every drop and why, and a dropped sentence of three words or more leaves a note under the composer instead of vanishing. The closing pause is trimmed before
transcription, and every dictated message reaches the main model tagged [voice transcript] (T-76: in the turn, when steered, from the
queue, and on a resend; the window never shows the tag), while a message typed with voice mode on goes untagged: the briefing tells
the model a tagged message is a transcript, to repair names and odd words, and to ask in one line when a word does not fit.say (the system voice by default), rendered per utterance and played inside the app, so it can
fade out instantly and the echo canceller knows what the speakers are playing.say renders are
killed) and it never talks over you. The main thread is not touched: it keeps working, and its answer is still summed
up when it lands. What you type (Enter) while it is busy shows up as a dashed "Queued" bubble and is sent by itself
the moment the turn ends; what you say is steered into the running turn (next point). The voice also weighs what you meant: a clear "stop, cancel that" interrupts the main
thread ("I'll take care of that right away"), and "not that, do X instead" interrupts it and sends X next. In doubt
it queues; work is never stopped on a guess. A short utterance with a stop word ("stop", "cancel that", "para") never
waits for that judgement: it interrupts the main thread straight from the transcript, already from the speculative
one taken 240 ms into your pause. Otherwise only the Stop button interrupts the main thread. Tapping the orb also shuts the voice up.npx vite build and this is enough; a change to
electron/* or shared/* still needs the full restart.data/state.json is overwritten).```message-agent Notes
What is the code word of the day?
When its turn ends the app delivers the text to that session, tagged `(from agent "Sender" [id])`: steered into the
running turn if that agent is working, sent as a new turn otherwise (the session is mounted in the background if it
was not open). Whatever the other agent replies comes back to the sender by itself, tagged the same way, so an
explicit message is a question and no block is needed to answer it; an answer does not bounce back again, so two
agents cannot ping-pong on their own (and the app stops relaying after 30 agent-to-agent messages in ten minutes).
A reply that goes back to its sender leaves out the blocks it addressed to other agents (they went to them) and says who
else was written to. A Claude agent that left a background job running (a shell command in the background, a monitor)
used to swallow the next message: its next turn answers only the job's "stopped" notice, empty, in under a second, which
read as "the third message between agents fails". A Claude turn that ends like that sends its message again, once, with
its reply address; and every agent is told not to start background work from a turn.
This block is the only channel between the app's agents, in both directions, Claude to Codex and Codex to Claude; the
briefing says so plainly, and that the harness's own agent tools, chat skills and shared files never reach them (a Claude
agent once set up a file-based chat protocol instead). A Claude session also gets the same channel as two tools of its
own, `mcp__jauvex__message_agent` and `mcp__jauvex__list_agents` (an in-process MCP server from the Agent SDK, answered
by the window's router, never asking permission), for the models that look for a tool when told "talk to X". Agents are addressed by name (title or summary) or by the short id in brackets, which never changes (6 characters,
longer only where two ids share their start, as Codex ids often do); a fenced `list-agents` block gets the roster back
(name, id, provider, folder, working or idle). The Jauvex agent is listed once, as "Jauvex", whatever its past sessions
across providers, and always, its chat open or not (T-151, the user, 2026-09-26: an agent that wanted to report a bug to it found
three "Jauvex …" agents and not it, since it was listed only while its chat was open): a message to it opens its own chat in the
background, with its session, or starts one; with no session yet, its folder's id stands in for its id; a session with no name of its own is shown by its first message, cut at 60 characters. Every exchange shows in both
threads, marked "From agent X", and in the voice log. The briefing tells every agent all of this.
clientBriefing in shared/types.ts; appended to Claude's system
prompt, sent as Codex's developer instructions): several agents side by side, messages that arrive mid-turn are new
information to fold in (not a restart), the full answer is on screen while a separate small model speaks a short
version (so: conclusion first, short plain answers for simple questions), a turn can be stopped at any moment, and in
voice mode the text is dictated. It names no product, so a rename does not touch it.claude auth login|logout, codex login|logout, grok login|logout); Refresh after using it. The app has no login of its
own: it uses the sign-in of each provider's own tool on this Mac (Claude: claude auth status on the SDK's bundled binary;
Codex: the app-server's account/read; Grok: its agent's x.ai/auth/info). Signing in, out and switching from inside the app is built (the provider's
browser flow, run from the panel and the welcome) but hidden in this edition (SIGN_IN_IN_APP in shared/types.ts,
since 2026-09-24): Anthropic does not let apps built on its Agent SDK offer the Claude.ai login. Sessions are files on this
Mac and stay whichever account is signed in; a running turn keeps the old account until it ends
(tests/window/sign-in-by-cli.test.ts)./usage (the SDK's experimental usage request, asked of a short-lived
idle process: nothing is sent to a model); Codex's come from account/rateLimits/read, Grok's from its agent's x.ai/billing. Only percentages, reset
times, the plan's name, extra usage and the credits balance are read, never account IDs.message.usage, the result's modelUsage[model].contextWindow); Codex's is thread/tokenUsage/updated (the last request's total
against the model's usable window). The numbers are kept with the session (Project.context), so the meter shows them when the
chat opens. A click opens the numbers and a Compact button; the click itself compacts nothing. Compacting is a turn of its own
(Claude Code's /compact, Codex's thread/compact/start) sent with the last turn's settings, so the provider's cached prompt still
applies; a typed /compact does the same. While it runs the sheets pulse and the thread says so; when it is over, a note in the
thread (kept with the session's notes) says it, with the tokens before and after, and so does the flight recorder, whoever started
it (the provider compacting on its own mid-turn included). Auto-compact (Jauvex settings, Context; settings --auto-compact <percent>|provider) is 90 % by default: past it, the chat compacts after the answer, before the next message goes. Claude Code's
own trigger is moved to the same share of the window (CLAUDE_AUTOCOMPACT_PCT_OVERRIDE, scaled, since Claude Code counts it against
the window less the room it keeps for the answer; it can only lower Claude Code's threshold), so a long turn compacts in the middle
too; Codex compacts mid-turn at its own limit (90 % of its model's window, about 95 % on the meter). A message that does not fit
at all ("Prompt is too long", Codex's contextWindowExceeded) is compacted for at once and sent again, once. Why: on 2026-09-23 a
Claude session sat at 962K of its 1M window, the next message pushed it over, and the messages after it failed with "Prompt is
too long" until it was compacted by hand; no meter had shown how full it was, and nothing said it was compacting. Anthropic gives
no recommended percentage (Claude Code's own default is about 967K of a 1M window); 90 % leaves room for the next message.ggml-base, its own server on the next port, asked about once a second and never while the real transcript is being
made), because whisper-server answers one request at a time and the transcript that counts must not wait. Both servers
run half a second of silence at start-up, so the first sentence does not pay for loading the model onto the GPU. Before
starting its own, the app stops any whisper-server a previous run left on its two ports (found by port, checked by command
line, stopped by its own pid): the app dies without them when it is killed, and each restart used to leave a pair behind.
Only this install's are stopped (their model is in its own models/): on 2026-09-24 the other edition, started next to this
one on the same ports, stopped both of this one's servers, and the wake phrase went with them. The editions use ports of
their own; one that finds its port taken by another install's server leaves it and says so in the flight recorder.data/voice-debug.log (rolled at 2 MB), including
the fate of every line that was meant to be spoken (said, or NOT said and why).turn/steer. When the turn ends, queued messages leave one at a time,
each as its own turn, in the order they were typed, never merged into one; a spoken instruction that replaces the
work goes first. The queue, its images and the text typed but not sent are kept on disk per chat: a reload of the
window (an agent's touch data/reload-ui) or a restart never loses a message, and a queue restored with no turn
running goes out at once.stopSaysMore in
shared/orders.ts), and a bare stop is kept with the session's notes. A message handed to a running turn that the model never
read goes again once the turn is over, without a second bubble: after a stop or a replace, everything handed to that turn
(Claude Code drops what it had not taken up, and twice is better than never); after a turn that ends on its own, what was
handed over once its last answer had started (unsent on the turn's end, from chat.ts). Why: on 2026-09-23 a message said
just before a stop, and one said as a turn was ending, were shown as sent and never answered.shared/orders.ts): the exact phrase
("restart the app"), or Jev at 0.85 or more. When nobody is sure (Jev leaning, unsure or absent, the voice model reading an
order) it asks in one short line, shown and said ("Should I open a new agent in website? Say yes to open it; anything
else goes to the agent as you said it"): a clear yes carries the order out, anything else sends the words it asked about to
the agent, as they were said. "Give me the handoff instructions so the other agent can work on this" once opened a new
agent that way. Typed messages get the same treatment while voice mode is on. The order
may follow a few other words ("Okay, let's see if this works. Make a new Codex agent."): it is looked for sentence by
sentence, and inside a longer message it is only acted on when Jev is sure it is for the app. "... named X" / "call it X"
names the agent: a Jev agent at once, a Claude or Codex session when its first turn ends (it shows in the title bar
from the start), written to the provider like any rename.
"... about X" / "for X" is what the agent is for. Jev only says that this is an order to open an agent; the details
are language, so the session's own model reads the sentence once and returns them as JSON (kind, name written as a
person would title it, purpose, folder), overriding what the rules found, and it also writes the new agent's first
message for that order (opened from the app, which folder, name and purpose in the user's intent, other agents may
be at work, change nothing yet, look around, say what it understood and proposes, ask, then wait). The new agent is
never left blank: a session only exists once something is sent. The agent opens at once with what Jev and the rules
found; the model's reading arrives a few seconds later (command:details) and the name and first message follow it.
If the model gives nothing within 30 s, the rules and the static kickoffMessage in shared/types.ts stand. An
agent born from an order never keeps its first message as its title: without a name it becomes "Codex agent" or
"Claude agent". The order itself stays in the thread it was given in, with a note of what the app
did. The microphone does not move: there is one in the whole app, it stays with the session it was started in while
you look at or type into others (that session's row shows a small waveform), and it only moves when you start voice
somewhere else. While you look at another session, its orb waits at the bottom of the sidebar with the session's
name, the phase, and mute / silence / end buttons; the name takes you back. The floating bar that appears over other
apps while voice is on (the tiny bar) has a move handle (four arrows) that shows when the bar is hovered: drag it to
move the bar, and nothing else drags it. It comes back where it was left. Its keyboard button slides a typing box open,
the bar growing with it: Enter sends the text to the listening session the way its composer would (queued while a turn
runs), without talking and without bringing the app forward; Escape closes it.model/list), with "Automatic (the smallest)" first: a Haiku on
Claude, the fast and affordable one on Codex. A saved id that is no longer offered, or one the API refuses, falls back
to the smallest, and the settings say which model is really in use. Nothing about model names is hard-coded beyond the
tier words, so a model that comes or goes never silences the voice.TYPESAFE_API_KEY, or ~/.typesafe/token),
the voice channel's decisions go to Jev, TypeSafe's System One model (electron/jev.ts): it does not write text, it
answers typed questions with probabilities, in about 250 ms against 1 to 3 s for the voice model. It decides: whether
your thought is finished when you pause (a clear "no" holds the words, joins them to what you say next and sends one
message; held at most three times, 4.5 s each). A long dictation is closed at its next short pause once it passes 6 s, and at 12 s whatever the pauses, and the next words join it (not counted as a hold): every
transcription pass costs by the length of the audio, and a paragraph re-transcribed at each pause took seconds a pass, so the words
landed late and the thought was cut in two. The fullest text any pass of a stretch heard is remembered: a final pass that heard far fewer words (a 14 s pass once came back as "I" where earlier passes had heard two sentences) is retried once, and the fuller text wins. Whole transcripts Whisper invents from near-silence ("Thank you for your time.", "Thanks for watching") are dropped whatever their score. The audio of every pass stays in data/voice-audio (the newest 120 files, never sent anywhere), named in the recorder, so a lost sentence can be replayed. One speculative pass at a time: a pause while the previous pass still runs skips it, whether it was a question or a thank-you (answered at once with
the voice answers in three stages: the quick line at once (Jev's fixed phrase, prepared during the pause), then the understanding (the voice model, told the quick line it comes after so it never repeats it, from the request and the last of the conversation: one or two sentences that reinforce what was said without parroting it; skipped for anything under five words, or once the turn is already over), then the summary of the answer when it lands), and queue / steer / stop / replace while the main thread is busy, with the spoken line
picked from a fixed set, in English, never the same line twice in a row. "Decisions" in the voice settings hands all of it back to the voice model. Below 0.6 confidence, in another language, over the 1.2 s budget, on any error, or with no key, the voice
model decides as before. The key stays in the main process: never logged, never sent to the window. CVC_JEV=off
switches it off. The voice settings say who is deciding.say command, Claude signed in, Codex signed in, whisper-server with a model, a TypeSafe key (optional),
with the command to run for whatever is missing. macOS only for now. The window is muted while voice mode is off (nothing in it may make a sound then); the welcome
unmutes it while it is open, so it is heard without starting a voice chat, and mutes it again when closed.electron/debug.ts); never keys or audio.web/) calls the Electron main process over one
IPC channel (electron/preload.ts → electron/main.ts → electron/backend.ts).~/.jauvex, not in the folder the app is installed in: Claude Code files a session under its
working folder, and renaming the install folder once left the agent pointing at a folder that no longer existed, its
conversation gone from the window. An old entry is moved there when the state loads; its briefing names the install folder
and the command line by its full path. When its session cannot be found where it lives, the conversation stays on screen
(the app keeps its own transcript), a note says so, and the next message starts a new session carrying the conversation
so far, the way a move to the other provider does. The app's state is read once and kept in memory, so two changes at the
same moment can no longer overwrite each other (a cleared session once came back, and the agent's entry vanished).~/.jauvex/personal (electron/paths.ts): the state, the window's profile, the logs and the command
files, for every copy, run from source or compiled, outside the folder the app is installed in, so an update or a new copy keeps
everything; data/ in this README means that folder. One data folder, one copy running (the lock in main.ts); CVC_DATA_DIR moves it (the checks).data/notes/<session>.json,
after the message they followed; in the transcript of an agent that moves between providers), so a restart no longer wipes
them. And the app only takes an order when that order is the whole request: "publish and restart the app" goes to the agent,
which does the work and then restarts the app itself (node scripts/jauvex.ts restart; every agent is told so).tests/wake-check.test.ts).@anthropic-ai/claude-agent-sdk): listSessions({ dir }),
getSessionInfo, getSessionMessages. Same data the claude CLI uses, typed, with no CLI scraping and
no parsing of ~/.claude by hand. The same SDK's query({ resume }) is the path for sending messages later.
Claude Code keeps its sessions in ~/.claude, or wherever CLAUDE_CONFIG_DIR points: the app starts through macOS, not from a
terminal, so it takes CLAUDE_CONFIG_DIR and CODEX_HOME from the login shell along with PATH (without that, a user who had
moved Claude Code's folder saw his Codex sessions listed and none of his Claude Code ones). When a folder lists no Claude Code
session, the debug panel says where the app looked.codex app-server (electron/codex.ts): the JSON-RPC interface, one JSON object per
line over stdio, that Codex's own clients use. thread/list from Codex's state database (useStateDbOnly: the
default scan of the rollout files leaves out the threads the desktop app's agents created, which the desktop still
lists), kept when a thread ran in the folder or in a Codex worktree of it (~/.codex/worktrees/<id>/<basename>,
recognised by the basename and the folder's git origin, never by reading Codex's folders); thread/items/list,
thread/start / thread/resume, turn/start, turn/interrupt, plus the approval requests the server sends.
Same login, config and session store as the Codex CLI, nothing in ~/.codex parsed by hand. The binary is the
@openai/codex npm dependency; codex app-server generate-ts --out <dir> prints the protocol types for the
installed version. One server process starts on first use and is shared by every Codex chat.grok agent stdio (electron/grok.ts): the Agent Client Protocol (JSON-RPC 2.0, one object per
line over stdio) that Grok Build speaks to editors, with its own x.ai/* extensions (Grok Build's source is public:
github.com/xai-org/grok-build). session/list per folder; session/load replays a session's history as updates marked
isReplay, and a session the app is not running is closed again after reading, so a Grok window that goes on with it never
races a stale copy here; session/new / session/resume, session/prompt, session/cancel, session/set_config_option
(model, effort), the session/request_permission requests, x.ai/interject, x.ai/session/rename. Grok takes the app's
briefing once, when a session is made (_meta.rules, folded into its system prompt), so the note on dictation is always in
it. One agent process with --no-leader (the app's own, never the leader a Grok window on the Mac may share), started on
first use, and never when no grok is on the Mac. Same login, config and store as the Grok CLI (~/.grok), nothing there
read by hand. Its plan's credits come from x.ai/billing, what Grok's own usage view reads (the share of the period's included
credits used, the period, on-demand spending, bought credits, the plan's name). Not yet: a Grok row on the welcome screen.electron/chat.ts routes each turn by the session's provider; every provider sends the UI the same ChatEvents.ephemeral thread with a read-only sandbox, so nothing lands in your Codex history; for Grok
sessions Grok's fast model, in a session of its own in the system's temporary folder with the voice's prompt as its whole system
prompt: Grok keeps every session, so the voice's is deleted when a new one is made, and its id is kept in the data folder for the
next run to delete when the app quit before Grok answered; each can be changed in the voice settings). About 1.3 to 1.8 s per line once warm; voice mode warms it up. It only
speaks for the main thread: every message it gets starts with a MAIN: line naming the
main thread's provider and model, it has no identity of its own, and asked "what model are you?" it relays the main
thread's answer instead of naming itself (tmp/e2e-voice.ts checks this for both providers).~/.jauvex/personal/state.json.npm start goes through open so macOS attributes privacy permissions (the microphone, later) to
the app instead of the terminal.Apache License 2.0: see LICENSE. The NOTICE file names the authors: whoever passes Jauvex on, changed or not, passes its
notices along with it (section 4 of the license). Versions before 0.2 were published under the MIT License and stay under it.
6 commits
TypeScript
98.4%
Shell
1.6%