This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Terraphim Editor is a WebAssembly-based Markdown editor built with Rust and vanilla JavaScript. Trunk orchestrates the build: Rust compiles to WASM for Markdown rendering and for the document model, while a contenteditable surface and the Write_On chrome are plain JavaScript and CSS. It implements the "alternative control" requirements (docs/requirements/alternative-control.md): span-anchored alternatives, ghosting and overflow, persisted as a trailing annotation block in the .md file.
- Work is tracked in Gitea at
https://git.terraphim.cloud(terraphim/terraphim-editor) with thegtr(gitea-robot) CLI, not GitHub. Source~/.config/terraphim/gitea.envforGITEA_TOKEN. - Branch
task/IDX-short-title; commits end withRefs #IDX(orFixes #IDX) and the co-author line; open PRs withgtr create-pull. - Every pull request is reviewed with pi-rust and the
structural-pr-reviewskill (read-only tools). An authoring agent's self-review is not the review of record. Verify findings before posting them. - Commit only when tests are green. Check test coverage after implementation.
trunk serveServes at http://127.0.0.1:8080 with hot reload. Output goes to target/trunk-dist (untracked).
Development / production Trunk build:
trunk build # or: trunk build --releaseBoth write to target/trunk-dist; they never modify the committed dist/, so git status stays clean.
Release build (regenerates the committed dist/ on purpose):
scripts/release-dist.shThis runs trunk build --release --dist dist, and the build hook adds the embeddable bundle (terraphim-editor.min.js, terraphim-editor.min.css, example.html). Review git status dist and commit the result. .gitignore ignores dist/* but keeps *.html, *.js, *.css and *.wasm tracked, so git add dist needs no -f.
The scripts/build-dist.sh hook writes to $TRUNK_STAGING_DIR, its first argument, or target/trunk-dist; never to ./dist unprompted. Needs terser and cleancss on PATH.
cargo test --workspace # native unit, golden-file and span-model tests
scripts/browser-tests.sh # browser tests (real Chrome, nothing mocked), one binary at a time, load-guarded
cargo bench --no-run # compile the criterion benches
cargo bench # run them (native only)The browser tests are split across binaries (tests/web.rs, web_chrome.rs, web_persistence.rs, web_bench.rs, plus tokens.rs, sharing tests/support/mod.rs) because wasm-bindgen-test gives each binary one 20 s budget. Native golden files live in tests/fixtures/persistence/ (UPDATE_GOLDENS=1 rewrites them; review the diff).
Run one test: cargo test <name>.
Run the browser binaries locally with scripts/browser-tests.sh (all of them, or scripts/browser-tests.sh web web_chrome), not with a bare wasm-pack test. wasm-bindgen-test gives each binary a fixed 20 s budget. When it runs out, the runner's next WebDriver call gets only the remainder ("execute/sync timed out after 5.0s") and ChromeDriver is killed. Before every binary the script waits for the one-minute load average to drop below BROWSER_TESTS_MAX_LOAD: 20 on Linux, and on macOS three quarters of the logical CPUs, at least 4 (8 on an 11-core laptop). While it waits it prints the load, the limit and the busiest processes every 30 s. After BROWSER_TESTS_LOAD_WAIT seconds (default 300) it stops, lists the binaries it did not run as NOT RUN and exits 4; it never runs them under load. On CI (CI, GITEA_ACTIONS or BROWSER_TESTS_CI, which scripts/ci/browser-tests.sh sets) the guard only warns: one capped wait before the first binary, then every binary runs and the run never exits 4, because the CI runner is fine at loads up to 23. scripts/browser-tests-load-test.sh checks the guard against the real load without starting a browser.
The evidence (issue #110): on the CI runner, over 25 runs (37746–37790, 1,158 binary runs, load average 2.1–23.2), no binary used more than 3.24 s of its 20 s budget:
| Binary | Test time max | Test time median | Wall median (with build) |
|---|---|---|---|
| web_bench | 3.24 s | 2.40 s | 7 s |
| web_bench_logged | 1.41 s | 1.15 s | 5 s |
| web_chrome | 1.29 s | 0.15 s | 4 s |
| every other binary | ≤ 0.91 s | ≤ 0.83 s | 4–5 s |
The 17–21 s runs, timeouts and SIGKILLs seen on a shared laptop came from other processes, typically several agents' headless Chrome sessions pushing the load to 10–17. In that state even an untouched binary went from 5 s to 21 s. Do not split binaries or raise timeouts for local slowness; read the per-binary times (<bin>: ok (Ns wall, finished in Xs, load L)) from the CI job log first.
Coverage: cargo llvm-cov --workspace measures the native tests. The Rust that runs only in the browser (the #[wasm_bindgen] exports) and public/js are measured by bash scripts/ci/wasm-coverage.sh [suite...]: wasm-bindgen-test coverage on nightly for the Rust, V8 precise coverage over the DevTools protocol for the JavaScript. It needs a nightly with the wasm32 target and llvm-tools, and a clang of the same LLVM major version (brew install llvm@N). Output goes to target/wasm-coverage/ (summary.txt, js-summary.txt, coverage.lcov). It does not gate CI.
cargo fmt --all -- --check
cargo clippy --workspaceterraphim_automata and terraphim_types come from the private terraphim Cargo registry (sparse+https://git.terraphim.cloud/api/packages/terraphim/cargo/, configured in ~/.cargo/config.toml). Cargo cannot resolve them without it. If cargo refuses to run in a nested worktree, run from an rsync copy under the scratchpad directory.
terraphim-editor(root): WASM crate andrlib.crates/terraphim_alternatives: the span model, plain Rust (edition 2024), noweb-sys. Spans, alternatives, the ghost layer, overflow, UTF-16 offsets, re-anchoring (throughterraphim_automata::CompiledMatcher) and the trailingterraphim-alternativesfenced JSON block (schema version 1).research/lab-heuristics: deliberately not a workspace member; deterministic weakest-sentence ranking and trim selection prototype (issue #3). Run withcargo run --manifest-path research/lab-heuristics/Cargo.toml.
src/lib.rs:run()(#[wasm_bindgen(start)]), Rinja template render,render_markdown, and the debounced preview (flush_preview,set_preview_delay,preview_delay,preview_pending,preview_render_count; default 120 ms trailing).src/document.rs: persistence bridge.DocumentSession(native, unit-testable) holds one open document; thin#[wasm_bindgen]wrappers (open_document,save_document,export_document,apply_edit,sync_document_body,document_counts,document_body,document_annotations) reach JavaScript aswindow.wasmBindings.<name>. Offsets are UTF-16 code units. Malformed blocks are preserved verbatim; detached or unresolved annotations are set aside and saved, never dropped.src/kg.rs: knowledge-graph alternatives (#13) overterraphim_lsp_core(exact registry version;terraphim_alternativesis patched to the workspace member so there is one copy). Derived KG spans (document_annotations().kg, never saved),kg_swap_alternative,kg_lookup_selection,alt_kg_append(Ctrl+Shift+G, R-8.6). No matching or synonym logic here. Seedocs/design/kg-alternatives.md.src/providers.rs: the pluggable alternatives provider seam (#83, R-8.7):AlternativesProvidertrait, ordered per-thread registry,append_from_providers, and thealt_append_alternatives/alternatives_provider_*exports behindeditor.registerAlternativesProvider. Seedocs/design/provider-seam.md.templates/editor.html: Rinja template ({{ initial_content }},{{ initial_preview|safe }}).
editor.js:EditorSurface(contenteditableplaintext-onlyspan-aware surface: UTF-16 offsets, decorations, its own undo/redo history, canonical DOM sotextContentis the document) andMarkdownEditor(toolbar, shortcuts,/command palette, help dialog, document API wrappersopenDocument/saveDocument/exportDocument/counts/annotations).chrome.js: Write_On mode toggle and corner chrome; dispatcheste:*CustomEvents (seedocs/design/write-on-chrome.md).config.js:EditorConfigwith shortcuts, commands and initial content.editor_config.tomlis not read by the code.terraphim-editor.js: embeddableTeraphimEditorwrapper used by the minified bundle.
tokens.css: Write_On design tokens (--te-*), documented indocs/design/tokens.md. Consume tokens; do not hard-code colours.write-on.css: layout and corner chrome scoped tobody[data-mode="write-on"].
- Trunk serves
index.html; the WASM module loads andrun()executes. run()opens the welcome text in the document session and renders the Rinja template into#app.MarkdownEditorcreates theEditorSurfaceand the chrome after the DOM is ready.- Typing fires a capture-phase
inputthat normalises the DOM; the target-phase handler schedules the debounced Rust preview fromtextContent; edits are mirrored into the span model throughapply_edit. - Save writes
body + annotation block; export returns the clean Markdown body.
- WASM for Markdown and the document model: near-native text processing, shared Rust code with the native tests.
- Contenteditable surface rather than a textarea, so spans can carry decorations.
- Shoelace (CDN, pinned to v2.12.0) for UI components; no npm dependencies. FontAwesome icons are used in the Write_On chrome.
- Trunk for the WASM build; dev output is separated from the committed release copy.
terraphim-editor/
├── src/ # lib.rs (preview, entry), document.rs (bridge)
├── crates/terraphim_alternatives/ # span model, block format, re-anchoring
├── research/lab-heuristics/ # standalone research prototype (not in workspace)
├── templates/editor.html # Rinja template
├── public/js/ # editor.js, chrome.js, config.js, ...
├── public/css/ # tokens.css, write-on.css
├── tests/ # native golden tests, split browser tests, support/
├── benches/ # markdown_bench.rs, document_bench.rs
├── docs/{requirements,design,research}/
├── scripts/ # build-dist.sh (hook), release-dist.sh
├── dist/ # committed release copy (scripts/release-dist.sh)
├── .docs/ # per-file summaries and summary.md
├── index.html, Trunk.toml, Cargo.toml
Rinja (Jinja2-like) templates live in templates/, are referenced with #[template(path = "editor.html")], and take variables from struct fields. Use |safe for pre-rendered HTML.
- Target is
wasm32-unknown-unknown;crate-type = ["cdylib", "rlib"]. - Use
wasm-bindgenfor exports andweb-sysfor browser APIs; closures need.forget()or a held handle to outlive the Rust scope. - Browser tests use
wasm_bindgen_testwithwasm_bindgen_test_configure!(run_in_browser). - criterion does not compile for wasm32 (rayon), so it is a non-wasm dev-dependency only.
Editor behaviour is driven by public/js/config.js.
Adding a keyboard shortcut:
shortcuts: [
{ name: "icon-name", key: "ctrl+x", prefix: "~~", suffix: "~~", desc: "Description" }
]Adding a command palette item:
commands: [
{ name: 'Display Name', icon: 'shoelace-icon', prefix: '**', suffix: '**' }
]- New formatting feature: add the shortcut (and optionally the command) to
public/js/config.js, then try it withtrunk serve. - Template change: edit
templates/editor.html; keepEditorTemplatefields in sync. - Markdown rendering change: edit
src/lib.rs; update unit tests there andtests/web.rs; runcargo test --workspaceandwasm-pack test --headless --chrome. - Span model or block format change: edit
crates/terraphim_alternatives; update its tests and the persistence fixtures; keepdocs/requirements/alternative-control.mdconsistent. - Performance: browser typing-latency benchmark in
tests/web_bench.rs; nativecargo bench. Target under 50 ms average conversion in the browser.
- Update with
cargo update; audit withcargo audit. - JavaScript: Shoelace from CDN (v2.12.0); no npm dependencies. Build tools:
trunk,wasm-pack, andterser/cleancssfor the bundle hook.
- "No window found": the WASM module must load after the DOM; check the console.
- Template not rendering: verify the template path, Rinja syntax and struct fields.
- Shortcuts not working:
EditorConfigmust load beforeeditor.js; ensure the surface has focus. - WASM tests failing: Chrome must be installed; use
--headless; look at the console without it. A loaded host can approach the 20 s per-binary budget. trunk buildchangesdist/: it should not; confirmTrunk.tomlhasdist = "target/trunk-dist".
- Rust:
snake_case.rs; JavaScript:kebab-case.jsorcamelCase.js; templateskebab-case.html; configPascalCase.tomlor lowercase.
- Never use mocks in tests.
- Always commit changes and keep Gitea issues updated with progress (
gtr). - Use tmux for background tasks instead of sleep.
- Never use the
timeoutcommand (it does not exist on macOS). - British English, no emoji.
- Keep
.docs/summaries updated when making significant changes.