Skip to content

Repository files navigation

ruri

ruri

One desktop workspace for all your projects — each a folder of live coding sessions.
A folder-organized sidebar on the left, a real agent session on the right.


ruri is a macOS desktop app (Electron). Claude sessions run through @justin06lee/yagami's AgentSession (the Claude Agent SDK pointed at your installed, signed-in claude CLI, claude_code system prompt preset, terminal parity) — so behavior, settings, CLAUDE.md, skills, hooks, and login are identical to your terminal, just with one UI over all of them. And through yagami's provider layer, a session can just as well run on any other coding harness you have installed — Codex, OpenCode, Gemini, any ACP agent — picked per project from the same model dropdown. Sessions stay warm: switch projects instantly while agents keep working in the background, with activity dots in the sidebar. Closing the window keeps sessions alive (the Dock icon reopens it); ⌘Q quits and tears them down.

Run it

make            # build → install ruri.app to /Applications → launch
make update     # stop the running app, rebuild, reinstall, relaunch

Requires a signed-in Claude Code CLI (yagami comes from npm as @justin06lee/yagami). The packaged app bundles the whole backend into one file and ships no node_modules; the Claude engine is your installed claude binary, resolved at runtime (with your login shell's PATH, so tools inside sessions behave like your terminal).

For development:

bun install
bun run dev       # browser mode: server on :7777, UI at http://localhost:5173
bun run desktop   # run the desktop app unpackaged (built UI + Electron)

What it does

  • Native desktop appruri.app with Dock icon, single-instance, inset title bar; the window serves the UI and WebSocket from one local port picked at launch.
  • Home — the workspace agent. The default view (and the pinned Home row) is a session that manages the app itself: tell it "today let's work on X, Y, Z" and it finds the matching directories under your workspace root, opens them in the sidebar, and can kick each session off with a delegated first prompt. On Claude that happens through in-process MCP tools (open_project, close_project, list_projects — closing takes the sidebar entry and transcripts, never files); on any other harness Home drops open/close requests into .ruri/open.jsonl at the workspace root and ruri applies them as the turn ends — so Home orchestrates on whatever model you point it at. It also carries a touch of its namesake's personality (RuriDragon's Aoki Ruri: deadpan, unbothered, gets it done anyway). The workspace root is configurable from Settings; deep work happens in each project's own session, Home just orchestrates. Its chat is deliberately ephemeral — every app launch starts it blank, and navigating away and back wipes it too (a running turn finishes first): it's a dispatcher, not a place to accumulate context. What persists instead is its write-ahead log (~/.config/ruri/home-log.md): every prompt, tool call, and reply is appended programmatically as the events stream — the model spends no tokens writing it — in per-session blocks headed SESSION <n> — YYYY-MM-DD (Day) HH:MM. Home is told that file is its memory and to grep it for dates and keywords when you refer to earlier work, never to read it whole. It also skips the header bar — nothing between the titlebar skyline and the conversation. When you name a project, Home looks it up rather than guessing: find_projects walks the workspace (and the usual code folders) by folder name, fuzzy, and answers with paths marked as projects when they hold a repo or a manifest; on the drop-file harnesses the prompt says to find the same way. Above the agent sits the board: every open project as a card — a status word, three live lines of what each session is doing (a streaming reply's tail moves as it streams, a tool call shows the moment it's made), and what it has spent today and all told. The strip across the top adds it up: projects working, projects waiting on you, and tokens / dollars / turns for today, the week, and all time. Click a card to land in that session; fold the board to its numbers when you only want the agent. The figures come off a ledger (~/.config/ruri/ledger.json, one bucket per project per day), which is what keeps them true across rewinds, compactions and Home's own amnesia — the result lines in a transcript are where a turn's cost is shown, never where it is counted.
  • Manga look — Space Grotesk type, pure black-and-white UI on warm paper (deliberately low blue channel, e-ink vibes): ink borders, screentone shading, hard offset shadows, grayscale syntax highlighting that leans on weight and slant instead of hue. Statuses are shapes, not colors — rows stay clean, and a small diamond pip appears on a row only when a background turn finishes. Scrollbars are hairline-thin (3px) everywhere, and while a turn thinks, a two-frame hand-drawn Ruri doodle flips poses in the transcript, wiggling between little tilts in the same hard half-second steps.
  • Projects sidebar as folders of sessions: each project is a collapsible folder holding any number of parallel sessions, and each session is auto-named by the small model the moment its first prompt goes out (in parallel with the turn, not after it) — by the ROLE it serves ("Frontend UI", "Backend API"), not the literal prompt. Star a project to pin it to the top of the list (the filled star is the only marker — no separate section); + on a folder adds a session. Folders are folded by default — new projects arrive closed, and only the ones you open stay open across restarts. Removing a session deletes its transcript and nothing else — even the last one just leaves an empty folder (only the folder's own X closes the project); files on disk are never touched. Projects are opened by asking Home.
  • Persistent sessions. Sessions start lazily on first message, run in their project's directory, and auto-restart with resume if they die — context carries over.
  • App-side prompt queue — send another prompt while a turn is running and it doesn't touch the harness at all: it waits in the app, shown as a dashed "queued" bubble you can still edit or remove, and goes out the moment the running turn (and anything queued before it) finishes. Stop clears the queue.
  • Prompt splitting — the scissors button sends your prompt like any other: it appears in the transcript immediately, exactly as written. Behind the scenes the small model splits it into separate near-verbatim requests (strictly no invented intentions) and feeds them to the session one turn at a time, attachments following their [image #N] markers into the right sub-prompt — none of which shows in the UI. If the split fails or no small model is set, the whole prompt just goes out as one turn; nothing is ever lost.
  • Attachments — drop or paste anything into the composer (25MB each): images, videos, PDFs, text/source files, whatever. A [image #N]/[video #N]/[file #N] marker lands in the text right where you dropped it (or at the caret on paste; spaced so nothing sticks to the brackets, and strictly inserted — what you've already typed, blank lines included, is never touched) with thumbnails above it — non-media files show a doc tile with their extension; click any thumbnail for the full-size view (PDFs render inline, text files show their contents). Drag on a full-size image to mark regions and attach a note to each — every region goes to the model as its own image, cropped with generous breathing room and the region drawn on it as a numbered white box, so the model sees both exactly what you marked and where it sits (a tight crop of empty space would mean nothing without the surroundings). Images go to the model directly; videos and other files are saved to disk and referenced by path for tool-based inspection. Uploads persist under ~/.config/ruri/uploads/. Unsent drafts — text and attachments both — are kept per session, so switching sessions mid-composition and coming back restores exactly what you were writing.
  • Streaming responses rendered as markdown (GFM, syntax-highlighted code blocks with copy buttons), tool-use chips (Bash — npm test; absolute paths inside the project collapse to projectname/relative/path — a full path in a chip means the tool reached outside the project), and per-turn result lines with duration and what the turn would have cost at API prices. Interrupting a turn ends it with a plain "you stopped this response" line — never the CLI's diagnostic soup dressed up as an error. A prompt that's just a slash command (/compact, …) shows as an inverted command tag instead of a speech bubble; click the tag to clear it (and its result) from the transcript.
  • Permission prompts: when Claude Code would ask in the terminal, ruri shows an Allow / Always allow / Deny card instead (your ~/.claude/settings.json allow-rules apply exactly as in the CLI; "Always allow" persists the CLI's own suggested rule). Plan-mode approvals render the plan as markdown.
  • Per-project model, reasoning effort, and permission mode in the composer — permission modes switch between ask-first / accept-edits / plan / bypass live mid-session; effort (Low/Medium/High/XHigh/Max, XHigh when unset — no ambiguous "default" row, same philosophy as the model picker; Claude natively, Codex via model_reasoning_effort, others ignore it) reaches a warm session on its next prompt by quietly rebuilding it with resume; all persist per project. The Home composer has the same controls. Settings holds the device-wide model catalog: a searchable list of every model every installed harness can serve — probed live from each harness (Codex's app-server, an ACP agent's own model list), never hardcoded, and re-probed whenever Settings opens. Models go by their own names alone — the serving harness appears only as a tag in the catalog — and they go by their versions: Claude's own display names are bare families ("Opus", "Fable", "Haiku", each of which has been several different models), so the version is read back off the id the alias resolves to, or the first words of the description, and the picker says Opus 5, Fable 5.1, Haiku 4.5. Marketing parentheticals are cut, except that a big-window model keeps a 1M mark whenever the catalog also lists the ordinary one, since that choice is what the context gauge measures against; star the ones you actually use and every picker offers exactly those (with nothing starred it falls back to everything); star one a second time to make it the small-tasks model. An unset model means Fable — there is no ambiguous "default" row anywhere.
  • Any harness, same workspace — verbatim — the model picker also lists every other coding CLI yagami detects on your machine (Codex, OpenCode, Gemini, Copilot, any ACP agent), as provider:model ids. Pick one and that project's sessions run the harness's real interactive engine (Codex rides codex app-server — the same engine its TUI runs on; ACP agents keep a warm protocol connection), exactly as its own CLI would: its config (~/.codex/config.toml trust levels and approval policy included), its sandbox, its system prompt — ruri overrides nothing. Tool calls stream into the transcript as chips, and when the harness would ask for approval in its own UI, ruri shows the same Allow / Always allow / Deny card it shows for Claude ("Always allow" answers with the harness's own approve-for-session). Sessions stay warm across turns with resume across restarts. Permission modes are still a Claude concept, so that dropdown hides on other harnesses; ~/.config/yagami/config.json custom providers/paths are honoured.
  • Interrupt button to stop a running turn, and a transcript that opens at the latest message (relaunches included) and follows output only while you're at the bottom (a jump-to-latest pill floats just above the composer otherwise).
  • Edit & rewind — hover any past prompt in an idle Claude session and a pencil appears on its shoulder; click it and the prompt opens in an editable card that states the real stakes: rewinding returns the conversation and the project's files to the moment before that prompt ran. Edit it right there and hit Rewind & send (or Enter): the CLI restores its file checkpoints (sessions run with checkpointing on), the session resumes truncated — forked at the kept turn's last message, so the original chain survives on disk — the discarded turns leave the transcript, and your edited prompt goes straight out as the next turn. Needs yagami ≥ 0.6.1 (which exposes the CLI's /rewind); attachments don't ride back, and rewinds don't cross a compaction.
  • Art tuner (make tuner) — a dev page for placing the art by hand. First the twelve hero faces at the circle's real 132px: drag a face to move it, scroll over it to resize, arrows to nudge the picked one, x/y/zoom typed in directly if you'd rather, fit to show the whole picture and fill to cover the circle, double-click to reset. Then the titlebar band at its true 264×46 with the five heads draggable (scroll to resize, arrows to nudge). Last, folded away because each is a page tall, the raw pages themselves — drag a box over a head and it's cut at the page's own resolution, scaled to the width the other heads use, white dropped to transparent, and written straight into web/public/peek. Saving writes web/src/peek.ts, which is the file the app reads. Dev server only; never in a build.
  • Fast switching — a session opens on its last screenful and fills in the rest behind you: the tail paints immediately, the older turns arrive on idle frames, and scrolling back pulls in more as you go. Rendered markdown is cached by its own text, so a session you've already read re-opens from strings that are already HTML, and the sessions you haven't opened are rendered ahead of time on idle frames — clicking one is a paint, not a parse. Measured on a 700-event transcript: 144ms to switch before, 28ms after; a first open of an unvisited 400-event session went from ~100ms to ~10ms.
  • Catch up — a per-project brief written for a model that has never seen the project: one sentence saying what it is, then one line per capability. The small model keeps it current as turns finish, merging what belongs together rather than growing a changelog — a fix or a polish pass adds nothing, a new capability edits the line it belongs to or earns one of its own. It is not a page you open, because it was never for you: it lands in the project as .ruri/catchup.md and every session is told the file is there, so a model that finds itself somewhere it doesn't recognise reads it and stops guessing. Costs nothing until something opens it.
  • Ideas — a per-project board of things you want, in your own words. The tracker's opposite number: the model writes that one and you review it; nothing writes this one but you, and nothing leaves it on its own. Click a row to edit, the box to cross it off, the arrow to drop it into the composer as a prompt. Kept per project, not per session — an idea belongs to the thing being built.
  • Components — your own names for the parts of a project, collected at the only moment both sides are looking at the same thing: the model has just built it. It calls ruri's naming tool (mcp__ruri__name_component; harnesses without ruri's tools append a line to .ruri/components.jsonl, drained when the turn ends), and a card comes up in the chat — "I made something. What do you call it?" — with its suggested name in the words a person would use, a screenshot of the thing, the files it lives in, and a line on what it is. The picture is the point: a filename and a sentence are what the model has, and neither is what you recognise. ruri copies it into its own uploads the moment the card goes up (so a shot the model left in /tmp is still there when you answer, and a skipped card leaves nothing behind), and keeps it with the entry — click it for the full view. Edit the name to whatever you'll actually say, hit Name it, and that's the name from then on; Skip writes nothing down. Nothing is ever typed into the index by hand, because nobody remembers a filename well enough to start there — the Components page is where the entries collect, to read and to correct. The model reaches them two ways: the index is written into the project as .ruri/components.md, and a prompt that names an entry carries that entry — files, note, screenshot paths — down with it on the model's copy, so the transcript still shows exactly what you typed. mcp__ruri__list_components answers "what is what" when you ask. That only ever catches what gets built from now on, so the page has a Name everything button for everything that was already there: it reads the repo (the small model, so any harness), names the parts nobody has named, and writes down the CSS class each one sets — then starts the project with its own dev script, opens it in a window nobody sees, and photographs each component by that selector. Anything behind a login or a click it can't reach comes back without a picture; the card's on screen field takes the whole path (/settings >> .tab >> .panel), and the next sweep goes and gets it. It refuses a picture of nothing — an element that captures as a flat rectangle of background is no screenshot — and it only re-reads files touched since the last sweep, so pressing it again is nearly free. New entries wear a spinning star: beside the name for what the last prompt named, hooked over the card's top-left corner for what has been waiting longer, and on the Components button itself so a turn that named something says so without the page being open. Looking is what takes it off — hover one, or leave the page.
  • The vault — passwords, tokens and the accounts they belong to, held by ruri so the model can use them without ever reading them. Add one in Settings (name, optional username, the value, what it's for); the value is written to ~/.config/ruri/secrets.json at 0600 and never crosses the wire again. The model is told the names only, and reaches a value two ways: it writes {{name}} into a command or a file and a PreToolUse hook swaps the real value in after the model has finished writing and before the tool runs, or it uses $RURI_SECRET_NAME (and $RURI_USER_NAME), which every harness ruri spawns inherits whether ruri can hook it or not. Anything a value leaks back into — a command's output, a permission card — is redacted to its handle before it reaches the transcript or the screen. Neither path can stop a model that deliberately prints one, which is why it is told not to and why the redaction exists.
  • Skills — the folders of instructions a harness reads before it works, in both scopes: ~/.claude/skills for every project on this machine, <project>/.claude/skills for this one only. The page lists what's installed with where it came from, installs and removes through bmo (any source bmo takes — repo, folder, zip, url), and updates them all in one press. Click a skill's name and its SKILL.md opens rendered — headings, lists, code blocks, the lot — because a skill is prose and reading it as a file is not the same as reading it. Off is ruri's own word for it, since neither Claude Code nor bmo has one: the folder moves to a sibling skills-off/, out of the tree the harness scans, and straight back when it's turned on again — nothing is deleted. Claude Code loads a project's local skills itself; every other harness is told they exist and where to read one.
  • Shells in the composer — the >_ button turns the prompt box into a terminal in that project's directory: your login shell, your rc files, your prompt, your colors, on a real pty. A numbered tab row across the top opens as many as you need (⌘T for another, ⌘1–9 to switch, × to close); every one keeps running while you're elsewhere, so switching back finds them where you left them. The tab row itself outlives the app — reopening the panel finds the same row, each tab starting a fresh shell the first time it's looked at — and the shells die with the project.
  • Three themes, on a clock — light (paper), dark (the same page at night), and ember: warm through and through, no blue channel to speak of, for late sessions. Settings can hand the choice to the clock, on a dial: one turn is one day (24 hours, not 12 — the thing being set is a whole day, and half of it would otherwise sit behind an am/pm switch), and the ring around it is painted in the three themes, each stretch in that theme's own paper and edged in its own ink, so the evening looks like the evening while you're reading it in daylight. Drag a mark round the ring to move a boundary, or nudge the times beside it for exactness; a hand shows where the day is now. Picking a theme by hand turns the clock back off. The theme, the clock, which folders are unfolded and the player's volume are the window's preferences and are kept on the machine (~/.config/ruri/prefs.json), so they come back the way you left them; the window's own storage is a cache in front of that, for the theme to be on the page before the first paint.
  • Rapid fire — the bolt row under Home turns the main pane into assembly-line prompting for parallel work. It shows one session at a time, whichever is ready for a prompt, as the ordinary chat page — the full transcript, the real composer, everything where you expect it — with the line's controls in the header. The sidebar doesn't move: the app's own session stays selected, and leaving the line puts you back on it. Its controls — how many are ready, skip, leave — sit just above the composer rather than in the header, which belongs to the project. Send, and the prompt lands and sits there a beat, the card eases out, and then the next project announces itself — its name in large type, and its session's role under it, held long enough to register — before its chat rises into place behind the name. You never have to look up at the header to know where you are. Skip passes to the next one; a session that's working is passed over until it finishes, and when everyone's working the card stays where it is so you can watch that turn end.
  • Music player in the sidebar (ported from home): point Settings → Music at any folder (default ~/Music/ruri, or RURI_MUSIC_DIR) — each subfolder is a playlist, loose files are "Unsorted". Two-deck Web Audio engine with 6s equal-power crossfade, shuffle, seek, volume, and a repeat button that cycles off → loop the playlist → loop the current track (a little "1" marks track mode; deliberately session-only — every launch starts with repeat off); tracks stream from ruri's own server with Range support. While a track plays, a five-bar waveform fed by a real analyser tap on the master output rides the gap between the track title and the chevron (the note icon stays put), and little notes wobble upward from the player bar.
  • Dark mode — proper dark: black and dark-gray surfaces, white ink, gray borders; Light/Dark switch in Settings (gear on the account bar at the bottom of the sidebar), persisted per machine. Settings also holds the workspace root, the music folder, and the model catalog.
  • A different Ruri every time — the hero face above the composer is drawn from a pool of 12 cropped Ruri panels: Home rolls a fresh one each app launch, and every project keeps the face it was born with. Each sits in its circle fitted whole at zoom 1 and framed by the tuner from there, so a panel that isn't square (three of them are the same hand-cut, background-removed heads that peek from the titlebar — thinking, brushing her teeth, holding her horns; a fourth is her mid-laugh, re-cut taller so her horn tips end inside the picture instead of being sliced off by its edge) can be placed rather than blind-cropped. Up in the title bar sits the peek skyline: five hand-cut head PNGs (background removed and missing parts redrawn by hand) resting low in the bar, just horns and hair showing — hover one and she lifts slightly, just enough to see her face. The whole bar stays a window-drag region — the desktop app's main process polls the cursor to drive the hover, since drag regions never deliver mouse events to the page.
  • The dragon gauges — four dragons flank the composer, each filling from his feet up, and past 80% he starts sweating. Left of the box: how full the session's context window is, and the 5-hour window. Right of it: the weekly window across every model, and the weekly window scoped to one model, which the account names itself ("Fable", "Opus"). Under each name is how long until that window rolls over — in 2h 41m, in 6d 22h — because the percentage answers how much is left, not how long until you get it back; hovering gives the reading in full, with the date. They belong to whichever harness the session's model runs on, never to Claude by default: Claude's come from the same endpoint Claude Code's own /usage uses (your existing sign-in, no second one), Codex's from the windows it writes into every session rollout on disk, and a harness that reports neither shows empty dragons rather than somebody else's numbers. The last good reading is kept on disk, so a relaunch opens on numbers instead of four dashes. The context gauge counts the whole window — everything sent, cached or fresh, plus what came back — measured against the window that channel's model actually has: 1M on a [1m] model, 200k otherwise, or whatever size the harness names for itself.
  • Feature tracker — the small model splits each prompt you send into its distinct requests — one checklist item per request, in your own words, nothing invented or embellished (it never sees the replies) — the moment the prompt goes out, so a stopped turn or a "continue" follow-up can't lose requests and nothing from a long prompt gets forgotten. Items stay tied to the prompt they were split from: edit & rewind a prompt and its auto items (plus those of every discarded later prompt) vanish, then the edited prompt re-extracts fresh ones on send — the checklist always mirrors the prompts that actually stand. Manual items are yours and never touched. The header's tracker button swaps the whole chat pane for the todo page (no navigation — the same lit button swaps back), and the page opens itself when new items land. Reviewing is one pass: click anywhere on an item — once for works (check), twice for needs-work (x), which smoothly folds out a note field (foldable again to stay focused) — and items never move while you review. Notes save on blur and on the way out, so closing the page mid-thought loses nothing. Paste or drop files straight into a note; they show as little numbered bumps on the textbox's top edge (click one to remove it). Finish review then clears the checked items, reopens the crossed ones pinned at the top with a repeat badge on their checkbox (new extractions land below them), and assembles one fix-it prompt mechanically — each crossed item's title with your note verbatim under it, note attachments as stored file paths for tool inspection; no model call, so it's instant and exactly what you wrote — dropped straight into the composer (persisted as that session's draft, so it survives switching around). Persists in ~/.config/ruri/tracker/.
  • Turn memory — a small model writes two terse recall notes per exchange, telegraphic to the point of caveman ("header flickers on scroll; shrink logo"): the prompt's, the moment you send it, and the reply's, the moment the turn finishes. Which model: star one twice in the Settings catalog to crown it the small-tasks model (an inverted "small tasks" tag marks it; any harness works — yagami routes qualified ids), used everywhere the small model runs (recall notes, session role titles, prompt splitting, the tracker); with nothing double-starred it's RURI_SMALL_MODEL or Haiku, and RURI_NO_MEMORY=1 disables the layer. The notes stay out of the way — turns always show in full; a hover chevron on a turn's top-left folds it down to its note, and clicking the folded card pulls it back. Transcripts, notes, and the resumable session id persist in ~/.config/ruri/sessions/, so history and context survive app restarts.
  • The catch-up brief writes itself whole. .ruri/catchup.md used to grow a turn at a time, which left a project that arrived with a year of work in it briefless until work happened here. Now a project new to ruri (or one without a brief at launch) gets its repo read — README, manifest, Makefile, CLAUDE.md/AGENTS.md, the tree, the openings of the files that matter — and the small model writes the brief in one go: what it is, what's in it, the stack, how to run it, where things are, and the conventions a session must follow. Turns keep folding into the description and features as before; the Components page's foot says when the repo was last read for it and has a Rebuild for reading it again. Briefs are kept per project (older per-session copies are gathered up at launch).
  • Chats from outside ruri. Under each project's sessions sits a "recent chats" row: unfold it and ruri lists the sessions on disk for that directory that it did not make — a claude run from a terminal, a codex one — newest first, titled by the first thing said, tagged by harness. Click one and it becomes a session here: its conversation on screen as a transcript, and the next prompt resumes the real thing (when the project runs on the harness it ran on; otherwise it continues from a brief of it). Every id ruri's own chats have ever run on is remembered, so nothing ruri made is offered back to it.
  • Questions never go to a hole. The model's AskUserQuestion card waits for you; if the tool call behind it stops waiting first — the turn ended, or the CLI gave up on the hook — the card stays, says so beside its title, and your answers go out as a new prompt of their own ("Send as a prompt") rather than into a call nobody is listening to.
  • Fork a chat at any exchange. Under the pencil on every prompt sits a branch: click it and a new session opens in the same project holding everything through that exchange, titled after the original, and carries on from there — the original is left exactly as it is. On Claude the CLI session itself is forked at that point (the fork shares the file up to there, then keeps its own), so the fork's memory is the real thing, not a summary; a chat forked at its latest exchange forks at the tip. On other harnesses, or when a compaction has since retired the session that held the exchange, the fork opens on a brief of what it holds, the way a rewind does.
  • A sketch pad. The pen next to the composer's shell toggle opens a pad across the pane: pen, arrows, lines, boxes, ellipses, text, five colours, three widths, undo (⌘Z), keys for every tool. Attach puts the drawing in the prompt as an image like any other — marker, thumbnail, the lot — because half of what is hard to say about an interface is easy to draw. Open an attached picture in the viewer and Draw on it opens the same pad on the picture, at its own size; Put it back replaces the attachment with the drawn-on one under the same marker. The pad also opens any image from disk to draw on. Shapes are kept as shapes and redrawn, so undo is exact and the export is drawn fresh at full size. Text is written first and placed second: the text tool (T) opens a box to write in — several lines, Font… for a face (Grotesk, Mono, Serif, Hand) and a size — and Place… hangs the words on the pointer, stamped wherever the next click lands (Esc puts them back in the box). Every stroke is saved as it lands, per channel and per picture, so leaving for another channel, closing the pad, or relaunching ruri brings the drawing back; only Attach starts the next pad clean. The pad closes on its own × alone — Escape never closes it, since Escape is what you press to get out of a text box.
  • Selection flags. Double-click a word in the transcript (or drag out any selection) and a flag stands at each end of it. Drag a flag to change what is selected — scroll with the wheel first, the selection stays put, then pull the flag to where it should reach; near the top or bottom of the transcript a dragged flag scrolls it for you. Copy as always; Escape or a click elsewhere clears it.
  • Markers are chips. An attached file's [image #1] in the prompt draws as a chip over the words — hover it and it inverts and its thumbnail in the strip lights up, drag it and it moves to wherever you drop it (snapping to a word boundary), click it and the attachment opens in the viewer. A slash command standing on its own (/compact, /anything) is a chip too — a path, a quoted mention or a URL is not — and clicking a command chip takes it out. Backspace right after a chip (or Delete right before it) removes the whole marker, never half; removing an attachment with its × removes its marker and its regions' markers from the prompt. Underneath it is still plain text in a plain textarea: the chips are a mirror of the prompt laid over the box, wrapping exactly where the box does (the space inside a marker is a non-breaking one, so a marker is never wrapped in half, and it goes out as a plain space). The box keeps its height, caret and scroll across a trip to the shell.
  • The bridge — a session sees and drives what it built. After a model changes an interface, it can look at the result instead of merely asserting it — without ever interrupting you. mcp__bridge__web_open loads a page (a dev server, a built file, any URL) in a browser window ruri owns and keeps hidden; web_click (by CSS selector, by the visible words on a thing, or at coordinates), web_type, web_press (chords like Meta+A), and web_scroll drive it with real input events, so React and every handler see a genuine pointer and keystrokes; web_wait_for (selector, text, URL, or network idle), web_eval and web_logs (console and network ring buffers) read it; web_screenshot photographs it — and every driving tool returns a fresh scaled screenshot inline, so the model sees what it just did. app_launch does the same for desktop apps: a dev-built Electron app (started with a debugging port and driven over CDP, app_click/app_type/app_eval/app_screenshot) or any macOS app (open -g, driven through the Accessibility tree — app_ui_tree dumps the front window's controls, app_ui runs AppleScript UI scripting like click button "OK" of window 1, app_screenshot photographs it even behind ruri via the window server). Nothing appears in front of you, nothing steals your focus, and you can't accidentally touch the thing under test. What you get is a small live preview above the composer — the bridge strip — with the title, the address, a Take over / Give back toggle (take over and the real window comes forward for you to work in; give it back and it hides again with the session still driving), and a close. Claude holds the tools in-process (auto-allowed); every other harness gets the identical set over HTTP at POST /bridge/<channelId>, the channel id a capability told only to that session. Persistent per-project login cookies, never your own browser's. First use may prompt for macOS Accessibility and Screen Recording (needed only for driving and photographing native apps).
  • ruri's own /compact — type /compact and ruri compacts the session itself instead of letting the harness do it, instantly and at zero model cost: the live session is retired (resume id cleared — the next prompt opens a brand-new session with no context beyond the brief), every prior exchange's full record — prompt, response, tool activity, and preserved attachment paths — is written to ~/.config/ruri/turns/<session>/NNN.md, and a brief is built from the precomputed recall notes as strict prompt/reply pairs — user: then you: per exchange, oldest first — each ending with its exchange's file path so the fresh model can Read the full record whenever a note isn't detail enough, then open any referenced image to see its actual pixels. The brief rides invisibly on your next prompt. Commands written inside a prompt run first: /compact on its own line (or as a word anywhere — it takes no arguments) and any installed skill or custom command on its own line (/simplify web/src) are lifted out and run in order, and what's left is sent after them as the prompt — so you can /compact in the middle of writing a long prompt without losing your place. Quoting one ('/compact', "/compact", `/compact`) makes it a mention, sent as words. The transcript keeps your full history and just shows a uniform zigzag line — "compacted", in the result lines' own voice — at the break; click the label and the pairs unfold as a clean numbered list (your note in ink, the reply's under it in soft) instead of raw model text. Works on any harness, since it's all app-side.

Architecture

ruri.app (Electron)
  ├─ main process: startServer() in-process        ─┐
  │    └─ yagami AgentSession → Agent SDK           ├─ one WebSocket + static UI
  │       → your installed claude CLI               │  on one localhost port
  └─ renderer: React + Vite + zustand (dist-web)   ─┘  (shared/protocol.ts)

One HTTP server carries everything: GET /healthz, GET /music/playlists, GET /music/track?p=… (Range-capable), GET /uploads/<file>, the built UI on every other GET, and the WebSocket on the same port. shared/protocol.ts is the single wire contract — every client command and server message is a variant of ClientMessage/ServerMessage.

Server

  • server/server.ts — importable startServer(): WebSocket hub + static file serving for the built UI; snapshot on connect, broadcasts events/deltas/statuses/permissions, handles client commands, owns the model-probe cache and the app-side prompt queue (visible queued prompts and silent split sub-prompts alike).
  • server/sessions.ts — per-session lifecycle. Claude sessions ride yagami's AgentSession (warm process, terminal parity, appName: "ruri"): SDK message → transcript event translation, permission plumbing (with the CLI's suggested "always allow" rules), model/permission-mode switching, resume-on-restart. Non-Claude models route to ProviderAgentSession: yagami's agentic session layer (openSession — Codex app-server, ACP), the harness verbatim with tool chips and approval cards; ProviderTurnSession (one sandboxed provider.run() per turn) remains as the fallback for providers without it. Both use provider-prefixed resume ids streamed into the same events.
  • server/finder.ts — finding a project folder by the name a person uses for it: walks the workspace, scores folder names against what was said, prefers folders that look like projects. Behind Home's find_projects tool.
  • server/ledger.tsLedgerStore: what each project has spent, by the day — tokens, dollars at API prices, turns, wall time — added to as results land and never pruned. Behind the board's figures.
  • server/recent.ts — the chats that happened outside ruri: lists a project's Claude and Codex session files by working directory, and reads one into transcript events for import.
  • server/manager.ts — the Home agent: an in-process MCP server (find_projects, open_project, close_project, list_projects) plus a workspace-manager system prompt (with the Ruri personality), layered onto a normal session at the workspace root. Non-Claude harnesses get the same duties via a system-prompt-described drop file (.ruri/open.jsonl) drained at end of turn.
  • server/homelog.ts — Home's write-ahead log: appends each Home event as a greppable one-liner under numbered, dated SESSION headers; the model reads it (via search), never writes it.
  • server/providers.tsProviderRegistry over yagami's provider layer: detects the installed harnesses once at startup (honouring ~/.config/yagami/config.json), lists their models for the picker as provider:model ids, and builds per-project provider instances working in the project directory.
  • server/projects.tsProjectStore: projects, their sessions, and the device-wide settings (workspace root, music dir, Home's model/mode, starred models, the small-tasks model), persisted to projects.json.
  • server/archive.tsSessionArchive: transcript events, per-turn recall notes, the resumable session id, and a pending compaction brief per session, debounce-written to disk. The source of truth behind the connect snapshot and /compact.
  • server/commands.ts — slash commands inside a prompt: which names count (ruri's own, the harness's, every installed skill and custom command), and the lift that runs them ahead of the prompt they were written in.
  • server/compaction.ts — ruri's own /compact: writes each exchange's full record and attachment paths to turns/<session>/NNN.md and builds the model-facing brief out of the precomputed recall notes as prompt/reply pairs, one file hook per exchange. No model call — it's instant.
  • server/smallmodel.ts — the small-tasks layer over yagami's completions client: prompt and reply recall notes, session role titles, prompt splitting, tracker extraction, plus TurnTracker, which assembles prompt→result turns out of the event stream.
  • server/tracker.tsTrackerStore: the per-session feature checklist, auto-extracted or hand-added.
  • server/uploads.ts — attachment intake (images, videos, arbitrary files): base64 in, files under uploads/ and small URLs in the transcript, region crops expanded into extra model-visible images, non-image files referenced by path in the prompt.
  • server/usage.ts — the account limit windows behind the dragon gauges: Claude's from the OAuth usage endpoint (token from the keychain, or the credentials file), Codex's from the token_count entries in its session rollouts, each with the percentage used and when the window resets. Polled every five minutes and after every turn, cached to usage.json so a relaunch opens on the last good reading; all best-effort — an unreadable source just leaves that harness's gauges empty.
  • server/music.ts — the music library scan (folder = playlist, loose files = "Unsorted") and the path allowlist for the track route.
  • server/index.ts — standalone entry for dev/smoke (same server, no Electron).

Desktop & UI

  • desktop/main.ts — Electron main: login-shell PATH recovery, window/menu/lifecycle, the native folder picker, and the cursor poll that drives the titlebar peek hover; esbuild bundles it together with the server, yagami, and the Agent SDK into a single file (scripts/build-main.ts).
  • web/src/store.ts — zustand store fed by the socket; drafts (streaming text) are kept separately from finalized transcript events.
  • web/src/components/ChatPane.tsx — transcript, turn grouping and compaction, permission cards, composer, tracker drawer. A long session is kept cheap to switch into: the tail paints first and the rest fills in on idle frames, every re-bottoming folds into one layout read per frame, and every turn above the last four gets content-visibility so the browser lays out what you're looking at rather than the whole history.
  • web/src/components/RapidFire.tsx — rapid fire: the client-side line of prompt-ready sessions, the hand-off timing, and the header bar. It renders no pane of its own — the chat pane takes the session it picks.
  • server/brief.ts — the catch-up brief: the store (per project), and the .ruri/catchup.md it writes into each project — description, features, stack, how to run, layout, conventions.
  • server/catchup.ts — the brief written whole: reads a repo the way a person joining it would and has the small model write every section at once. Runs for a project that arrives without a brief, and on request.
  • server/briefing.ts — what every project session is told about ruri before it starts: the catch-up file, the component index, the vault. Pointers, never contents — none of it costs a token until the model opens it.
  • web/src/components/Ideas.tsx / server/ideas.ts — the ideas board.
  • web/src/components/Components.tsx / web/src/components/NameCard.tsx / server/components.ts — the component index, the naming card and its screenshot, the tool and drop file the model registers through, the .ruri/components.md it writes, and the entries it hands to a prompt that names one.
  • server/sweep.ts / server/shots.ts / desktop/capture.ts — the repo sweep: which files are worth reading and what the small model is given of each (the top of it, what it exports, every class it sets), then the project's dev server started and stopped around a hidden window that finds each selector and captures its rectangle.
  • server/bridge.ts / desktop/bridge.ts / server/cdp.ts / desktop/apps.ts / web/src/components/Bridge.tsx — the bridge: the tool definitions and their two faces (in-process MCP server bridge for Claude, POST /bridge/<channelId> for every other harness) and the session briefings, in server/bridge.ts; the harness-neutral DevTools-protocol driver (find/click/type/press/scroll/screenshot/logs/wait, over either Electron's webContents.debugger or a raw ws socket) in server/cdp.ts; the per-channel hidden windows, previews and take-over in desktop/bridge.ts; the macOS app launching, Accessibility UI scripting and window-server captures in desktop/apps.ts; and the preview strip above the composer in web/src/components/Bridge.tsx.
  • web/src/components/NameCard.tsx — the card that asks what to call the thing the model just built.
  • server/secrets.ts — the vault: storage, the environment it exports, the {{handle}} substitution, and the redaction that puts handles back.
  • web/src/components/Skills.tsx / server/skills.ts — the skills page and the bmo/filesystem layer under it.
  • web/src/components/Terminal.tsx / server/terminal.ts — the composer's shells: xterm.js over a pty, a row of tabs per channel, with the row persisted to ~/.config/ruri/terminals.json.
  • web/src/tuner.tsx / web/src/peek.ts — the art tuner and the placements it writes; the sidebar and the hero read the latter.
  • web/src/components/Dragon.tsx / web/src/dragonArt.ts — the four gauges flanking the composer: the traced dragon art, the waterline that clips it, the reset countdowns, and the harness whose windows each session reads.
  • web/src/components/HomeBoard.tsx — the board above the Home agent: a card per project with live activity lines and its spend, and the strip that adds every project up.
  • web/src/components/Sketch.tsx — the sketch pad: a shapes-based canvas with pen, arrows, boxes, ellipses, placed text; saved as it is drawn; attaches to the prompt, or draws on an attached picture and puts it back.
  • web/src/components/Markers.tsx — the composer's markers and commands as chips: a mirror of the prompt over the textarea, drag to move, click to open or take out.
  • web/src/components/Selection.tsx — the selection flags: a draggable flag at each end of a transcript selection, with edge scrolling.
  • web/src/components/Sidebar.tsx — Home and rapid-fire rows, project folders and their sessions, the peek skyline, the account bar.
  • web/src/components/Settings.tsx — a page (it outgrew being a dialog), in groups: appearance, folders, the vault, and the searchable model catalog. The page is the only thing that scrolls — nothing inside it has its own scrollbar — and every value starts at the same left edge.
  • web/src/press.ts — pointer capture on every pressable thing, so a press that starts on a button ends on that button however far the press animation moves it; a release well away from it still cancels.
  • web/src/components/Player.tsx + web/src/lib/audio.ts — the sidebar player and its two-deck Web Audio engine.
  • web/src/components/Attachments.tsx — composer thumbnails, the full-size viewer, and drag-to-annotate region crops.
  • web/src/markdown.tsx — marked + DOMPurify + highlight.js markdown renderer shared by messages, streaming drafts, and plan cards. Finished text is cached by its own string, so a session you've read re-renders from HTML; a reply still being written renders on a throttle and skips the cache, since every prefix of it is thrown away a moment later.
  • web/src/fixture.ts — the canned ?fixture state used for token-free UI work.

Where things live

All app state sits under ~/.config/ruri (move it wholesale with RURI_CONFIG_DIR):

Path What
projects.json Projects and their sessions, workspace root, music dir, Home's model/mode, starred models, small-tasks model
sessions/<sessionId>.json Transcript events, per-turn recall notes, resumable session id, pending compaction brief
turns/<sessionId>/NNN.md Full per-exchange records and preserved attachment paths /compact leaves for the fresh session to Read
tracker/<sessionId>.json Feature-tracker checklist
ideas/<projectId>.json The ideas board
components/<projectId>.json The component index
briefs.json Catch-up briefs, before they're written into their projects
ledger.json What each project has spent, by the day — the board's figures
secrets.json The vault, mode 0600. Values, and nothing that reads them but ruri
prefs.json The window's own preferences: theme, the theme clock, unfolded folders, the player's volume
terminals.json Which shell tabs each project had open
home-log.md Home's write-ahead activity log, one block per Home session
uploads/ Attached images and videos
bridge/<channelId>/ The bridge's screenshots for a session (shot-<n>.png) and its live preview (preview.png)

Inside each project ruri keeps a .ruri/ folder, which ignores itself (one .gitignore saying *, so none of it shows up in your git status): catchup.md and components.md for whatever harness is working there, and open.jsonl at the workspace root for Home.

Everything else is yours and untouched: ~/.claude (settings, CLAUDE.md, skills, hooks, the CLI's own session files) — except that turning a skill off in the Skills page moves its folder to ~/.claude/skills-off/ and back — ~/.codex/config.toml, ~/.config/yagami/config.json.

Environment variables:

Variable Effect
RURI_CONFIG_DIR Where all app state lives (default ~/.config/ruri)
RURI_PORT Server port — the desktop app uses 7776 unless it's taken, the dev server 7777
RURI_MUSIC_DIR Music library root before Settings overrides it (default ~/Music/ruri)
RURI_SMALL_MODEL Small-tasks model when nothing is double-starred (default haiku)
RURI_NO_MEMORY=1 Turn the small-model layer off entirely
RURI_FIXTURE=1 Canned UI state instead of a live server
RURI_SCREENSHOT=/path.png Capture the window to a PNG shortly after load
RURI_USER_DATA Isolated Electron userData, so a dev run doesn't fight the installed app for the single-instance lock
RURI_SMOKE_SPAWN What bun run smoke boots instead of the standalone dev server

The workspace root defaults to ~/Workspace when it exists, otherwise your home directory. Out of the box, Fable and Codex's default model come pre-starred.

Testing

bun run typecheck                    # server + web, no emit
bun run build                        # no tokens; produces dist-app/mac-arm64/ruri.app
bun run smoke                        # live E2E: 3 real turns incl. Bash + permission round-trip
bun run bridge-test                  # no tokens: drives the bridge (hidden window + a native app) end to end

# same E2E against the packaged app (Finder-style stripped PATH recommended):
RURI_SMOKE_SPAWN="dist-app/mac-arm64/ruri.app/Contents/MacOS/ruri" bun run smoke

The smoke test boots the server, connects over the WebSocket exactly like the UI does, and drives three real turns in a scratch project — a plain reply, a Bash turn, and a WebFetch turn that must round-trip a permission card. It runs against a throwaway RURI_CONFIG_DIR, so your real projects are never involved.

bun run bridge-test costs no tokens: it serves a tiny local page, boots the real desktop app with an isolated config/userData/port (never touching the installed ruri.app), and exercises the bridge end to end over its HTTP face — web_openweb_clickweb_wait_forweb_typeweb_screenshotweb_logsweb_close, then a tier-2 pass that launches TextEdit in the background, walks its Accessibility tree, types into the document via app_ui, photographs the window, and quits it (skipped with a note, not failed, if macOS hasn't granted Accessibility or Screen Recording), and finally an Electron app driven over CDP.

For UI work there's a token-free fixture mode — canned transcript, pending permission, folder groups: open http://localhost:5173/?fixture in dev, or RURI_FIXTURE=1 (with RURI_SCREENSHOT=/path.png) for the desktop app. If the installed ruri.app is running, add RURI_USER_DATA=/tmp/ruri-dev so the dev instance doesn't lose the single-instance lock to it.

Not yet (iterate next)

A custom in-app file finder (replacing the native picker entirely), tool results/diffs in the transcript, effort controls, session history browser, drag-and-drop folder management, git status in the sidebar, worktree support for parallel agents in one repo, notifications, Windows/Linux packaging.

About

One workspace for all your projects — each with its own live Claude Code session.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages