Imported from MrGru/dodo (
AGENTS.md). Install upstream withnpx skills add MrGru/dodo. Copyright stays with the author.
Project agent memory
dodo is a Rust desktop app: a single window with a collapsible sidebar, where each sidebar entry
swaps the main pane to a self-contained developer tool (JSON formatter, Encoder/Decoder, API
Explorer, Docker, Database Explorer, Cleaner, Diagram, Mermaid, and on macOS and Windows an Input
method) plus, in the sidebar footer, a Settings dialog and a Check for updates dialog. It is
built on GPUI (Zed's UI framework) and the gpui-component widget library, both pulled from git
and pinned only by Cargo.lock. README.md is the user-facing description and Cargo.toml names
the exact dependency sources.
The source is the authority here, and it is written to be. src/main.rs owns the startup
sequence and carries, beside every crate alias, the reason that crate exists; src/layout.rs owns
the view model; src/tools.rs is the tool table and adding a tool is one row in it; each crate's
lib.rs doc comment is the authority on that crate. Nearly every module in dodo carries a //!
block stating the decisions behind it. Read those rather than a summary of them — this file is a
map, and the map is deliberately smaller than the territory.
Loading discipline
This file is loaded at the start of every session, so it holds only what is true for every session regardless of what is being touched. Everything else sits behind a trigger, and the triggers are the router below.
- Do not read the docs, the crate
AGENTS.mdfiles or the skills up front, and do not recursively scandocs/orcrates/*/AGENTS.mdto "get oriented". A session that only touches the JSON formatter must not pay for the Cleaner's internals or the input-method stack. - Load exactly what the router points at for the task in hand, at the moment the task reaches it. That is normally one row and one file.
- Load an architecture doc only when the change is genuinely cross-cutting — it crosses a crate boundary, changes a public API, touches shared persisted state, or touches a platform abstraction. An edit that stays inside one crate does not qualify.
- If the router has no row for what you are doing, read the module's own
//!docs. That is the intended fallback and it is usually the whole answer.
Invariants
These hold everywhere in dodo, whatever you are touching.
-
Cargo.lockis the only pin on the four git dependencies, andcargo updatesilently jumps them to upstream HEAD. Never run it as a side effect of another task; only ever as its own reviewed commit. An explicitrev = "…"pin was tried and cannot replace it — upstream depends on itself through unpinned default-branch refs, and the three resulting cargo errors are recorded indocs/build-optimization.md. Hence--lockedon every cargo invocation. -
gpui-pre-macosis a vendored patch, not an upstream release. dodo overrides that one GPUI package via[patch.crates-io]withpatches/gpui-pre-macos/; the only functional change vs upstream is aNSView::removeFromSuperviewcall inMacWindow::dropthat fixes the macOS tray-reopen IOSurface memory leak (full rationale: commit66e90e5). A GPUI version bump can silently drop or invalidate it, so on any bump: re-verify the leak stays fixed (three-cycle same-PIDfootprintretest with a real window), re-apply theremoveFromSuperviewhunk onto the new source if the bump drops it, and prefer upstreaming the fix so this local patch can be removed. -
Every string a user reads goes through
dodo-i18n, never a bare literal in view code. Loaddodo-i18n-textbefore writing or changing one; twocargo testguards enforce it, and a failing guard means the code is wrong rather than the test. -
Cheap
renderbodies are a contract, not an optimisation. "renderonly runs when something changed" is false in gpui: a dirty view marks its whole ancestor path dirty, and an ancestor re-rendering setsWindow::refreshing, which bypasses the element cache for every descendant — so a child view scrolling, a progress tick, or a redraw anywhere above re-runs yourrenderwith nothing of its own changed. Arenderthat copies a whole collection pays that copy per frame, however well its rows are virtualized. Stamp a revision where the data is mutated and compare it before re-copying;crates/dodo-cleaner/AGENTS.mdhas the worked pattern and the measurements. Relatedly: never use a prepaint callback to mutate andnotifya view for the next frame —WindowInvalidator::invalidate_viewschedules a redraw only inDrawPhase::None, so a notify during layout/prepaint records state without dirtying the window and waits for an unrelated event. -
A platform-conditional answer is a value chosen by
HostOsorcfg!, not an item behind#[cfg], wherever that is possible. Two of dodo's four release targets cannot be built from a Mac at all, so an answer expressed as a#[cfg]item is an answer nobody here can compile or test; expressed as aconst fnovercfg!or as a pure function taking aHostOs, every platform's answer is asserted from any machine. A gate in front of a genuinely platform-only API still has to be an attribute — that is the line. -
cargo fmt --allandcargo clippy --all-targets --locked -- -D warningsare blocking CI jobs. Run both before committing.cargo buildalone does not prove the tree is green, and there is no crate-levelallowin dodo;dodo-build-release-internalsowns the suppression rules. -
Every commit subject must follow Conventional Commits:
type(optional-scope)!?: description(feat fix perf refactor docs test build ci chore style revert). It is a blocking CI job (.github/workflows/commit-lint.yml), andfeat/fix/perfsubjects are whatrelease.ymlturns into the release notes' "What's New" section — a non-conforming subject fails CI and would otherwise be dropped from the changelog. -
The pinned
gpui-componentsource is the reference for every widget question, at~/.cargo/git/checkouts/gpui-component-*/<rev>/crates/ui/src(rev fromCargo.lock). Its<checkout>/skills/directory holds the upstream authors' own guidance, which is excellent on GPUI fundamentals and stale in a few places —gpui-component-recipesrecords which.
Router
One index, covering the skills (.claude/skills/<name>/SKILL.md, invoked by name), the crate-local
AGENTS.md files, and docs/. Load a row when its trigger fires, and not before.
| When you are… | Load |
|---|---|
Running cargo for the first time this session; adding tests; a build or cargo test failing oddly; asked whether a UI change actually works |
skill dodo-build-validate |
Writing or changing any text a user reads — label, title, placeholder, description, error, dropdown option — or an i18n / i18n_lint test fails |
skill dodo-i18n-text |
Writing or editing a render / new that builds a gpui-component widget, adding a key binding, or a widget will not compile / builds but does not appear |
skill gpui-component-recipes |
| Adding, renaming, reordering or removing a sidebar tool; a new sidebar entry is blank; a tool page is unreachable at a small window | skill dodo-tool-view |
| Adding or changing a setting, a theme or a language, or a settings change does not apply until restart | skill dodo-theming-settings |
Touching crates/dodo-api-explorer/ |
skill dodo-api-explorer-internals |
Touching crates/dodo-docker/ |
skill dodo-docker-internals |
Touching crates/dodo-database/ |
skill dodo-database-internals |
Touching crates/dodo-flow/ — the Flow Canvas engine behind the Diagram tool, still being built |
crates/dodo-flow/src/lib.rs's doc comment, which carries the phase-by-phase state and the recorded numbers, then whichever of these the change touches: budgets.rs before changing anything that paints, render/plan.rs before adding a painter, geometry/perimeter.rs before anything about where a shape's edge is — it is the one silhouette, the painter's outlines are built from it and a connector endpoint binds to it as a single normalised position round it, so a second copy of a shape's boundary is the failure it exists to prevent, and a corner radius is part of that boundary rather than decoration (which is why set_node_style re-resolves bound endpoints and why NodeStore mirrors the radius into a hot array) — spatial/mod.rs before anything that decides what is drawn, render/lod.rs before anything that decides how much of it is drawn, render/snapshot.rs before anything a frame reads, render/cache.rs before anything cached across frames, render/sketch.rs before anything hand-drawn, render/hatch.rs before anything about a hatched fill, views/palette.rs before adding a control that is not a tool — the palette is where a document-wide authoring choice goes, because the property panel is a view of the selection and has no row for an empty one — properties.rs before anything the property panel offers — the panel is contextual and its section table is data there, stated a second time by the test beside it, so a row that moves has to move twice; the table is indexed by selection kind and by whether the selection carries a label, which is the one place a row depends on an element's content — interaction/state.rs before anything about what a gesture means — the active tool and the tool lock are the machine's state and there is deliberately no second copy on the view — commands/editor.rs before anything that changes a document — every edit goes through one applier and the view is deliberately unable to reach the world directly — and runtime/world.rs before touching the graph. Paint order, vertex accounting, culling, simplification, dirty propagation and the one mutation path are contracts there, and all of them fail silently: over the vertex ceiling the window renders solid black rather than slowly, culling alone cannot bound a graph whose edges cross the document, and an edit that skips the applier shows up as a corrupt undo three steps later. Z-order is a fourth: it is honoured by ordering the planning walk, by moving a body between the renderer's two halves, and — for §10's pictures, which have no outline form to promote — by moving the whole image run to the far side of the paths, so a control that appears to work can still be painting in the wrong order. render/scene.rs's promotes_to_path and images_belong_above_paths, and render/snapshot.rs's place_in_depth_order, are the three places it is decided, and a style field that no painter reads is the failure this crate has now met six times — properties.rs's module doc lists all six, and the last two are the ones that are not style rows: render/registry.rs's shows_label was false for every drawn shape and every linear element, so a label a user typed reached the document and no painter; and a label's own colour, size, face and alignment reached the canvas painter but not the rich half, because a rectangle at working zoom is a GPUI element and views/nodes.rs drew its label from the theme. A label is drawn three times in this crate — by the canvas painter, by the rich element and by the inline editor standing in for it — and all three have to agree about its style and about its box. They agree on the box through one screen constant (LABEL_PADDING_PIXELS) and on whether to draw at all through one field: the snapshot's label_font_size, which render/snapshot.rs withholds for the element a caret is open on. That single gate is why the editor and the committed label are not both drawn, and it is asserted on the frame rather than on the source — render/scene.rs's a_label_being_edited_is_not_also_drawn_by_the_canvas. When a windowless crate cannot see a symptom, look for the data the symptom is made of before settling for a source grep. models/document.rs's Connector before anything about a straight line or arrow — the two ordered endpoints are the authority and the rectangle is derived, so nothing may reorder them through min/max, and an endpoint may be semantically bound to another element rather than merely coincident with it — models/image.rs before anything about a picture — bytes are shared by content hash and a crop is metadata, never a rewrite — and views/images.rs before anything that draws one, because a picture is the one canvas primitive that is a GPUI element and the reason is one pub(crate) in gpui |
Touching crates/dodo-updater/, .github/workflows/, Cargo.toml's dependencies, scripts/, tools/update-manifest/, deny.toml or THIRD-PARTY-NOTICES.md; preparing or debugging a release; the application-icon pipeline; the CI platform matrix and its cross-check traps |
skill dodo-build-release-internals |
Touching crates/dodo-mermaid/ — the Mermaid workspace behind the Mermaid tool |
crates/dodo-mermaid/src/lib.rs's doc comment, which is the map: one GPUI file (view.rs) and five gpui-free ones, and which of the six a change belongs in is the crate's whole design, because view.rs cannot hold a test at all — a #[gpui::test] anywhere in it crashes cargo test at the pinned gpui revision, so a rule left inside the view is a rule nothing can assert. Then whichever module the change touches: render.rs for anything naming mermaid_rs_renderer, workspace.rs for what a layout mode means or what closing a tab leaves behind, templates.rs for the template set and how one is appended into a buffer, zoom.rs for the preview's transform, and theme.rs before anything about how a diagram looks — it owns the preset table, the general fields a user may edit, the merge of overrides onto a preset and the colour validator, and its module doc records that which fields are offered was measured against the renderer rather than assumed, because half the renderer's plausible-sounding general fields move nothing a common diagram draws. A tab's theme is per tab and is half of the render key: the debounced render path skips work when that key is unchanged, so a theme left out of it is a preview that never restyles. Every floating control is a child of the pane it acts on — that, and not a second matches! on the mode, is what stops one being stranded in a pane that is no longer drawn |
Touching crates/dodo-cleaner/ |
crates/dodo-cleaner/AGENTS.md |
Touching crates/dodo-ime-core/ — the Vietnamese engine and the shared key vocabulary |
crates/dodo-ime-core/AGENTS.md |
Touching crates/dodo-input-method/ — Event Tap on macOS or Keyboard Hook on Windows |
crates/dodo-input-method/AGENTS.md |
| Signing or notarisation, on any platform | docs/macos-signing.md |
| Cleaner scanner, safety, privacy or limitation detail beyond the crate file | docs/cleaner/ |
Startup, app lifecycle, window close/quit, or the shape of src/ itself |
docs/architecture/app-shell.md |
Reading or writing anything under data_dir(); adding a persisted file; session restore, window geometry, or the sidebar's tool list |
docs/architecture/persistence.md |
| Adding a crate, extracting a feature out of the binary, or moving code across a crate boundary | docs/architecture/workspace-layout.md |
Quick navigation — pasting into whichever tool can read it, normal mode, Esc |
src/quick_nav/mod.rs and src/quick_nav/models/detect.rs doc comments |
| The menu bar / notification-area item | src/tray/mod.rs, src/tray/icon.rs and src/tray/menu.rs doc comments |
| Binary size, the release profile, or whether a crate split will speed up builds | docs/build-optimization.md |
Maintaining this file
This file is the top tier of four, and the tiering is the point:
- Root
AGENTS.md— global rules, invariants, and the router. Every session pays for it, so it stays small. Nothing crate-specific belongs here. crates/<crate>/AGENTS.md— knowledge local to one crate that its ownlib.rsdocs cannot hold because it spans several files. Only three crates have one; do not add a fourth unless a crate's knowledge is genuinely stranded and no skill covers it.docs/— focused feature, platform and architecture knowledge, loaded on demand..claude/skills/<name>/SKILL.md— procedural knowledge behind a trigger, unchanged in shape.
One owner per fact. Every fact lives in exactly one file and everything else links to it. If a
fact only matters when touching one crate, it belongs in that crate's AGENTS.md, its lib.rs
docs or its skill — never here. If the source already states it, point at the source instead of
copying it: keep the why, the decision and the trap, and drop the what. Prefer rewriting or
pruning an existing entry over appending a new one, and when you add a row to the router, check
that nothing else already claims it.