Roadmap
From where the editor stands to a 1.0 someone else can use all day.
Originally written from the 2026-09-19 audit. The bug list in
bugs.md is a historical snapshot; #n below refers to it.
Current checkpoint — 2026-09-22
Section titled “Current checkpoint — 2026-09-22”2026-09-24 addendum: the alpha plan is launch-alpha.md.
The syntax palette was redone (43ccdc9) and 3.1 gained the missing half:
open files follow external writes, Save asks on a conflict or a deleted
file, and File > Revert to Saved exists. From 2.8: editor scrollbar, indent
guides, bracket match. Git gutter marks (against HEAD) landed. See the
handoff for validation.
2026-09-23 addendum: 488f88e adds the Cmd-P command mode (> lists and
runs the menu’s commands, with a Git menu and New Terminal) and a scrolling
palette list with a scrollbar (FEAT-012). Terminal: click-to-move-cursor at
the prompt is not implemented.
Latest tested code change: 19b8633 (0.1.0), the fourth of four
commits from 2026-09-23 that add the frame model (1.1), FSEvents project
watching, split panes (4.7) and language servers (4.6): completion,
diagnostics, go to definition and hover. Before them, 5e0b116 stopped
Cmd-Backspace in the palette from reaching Move to Trash (BUG-011), and 3f15671 gave HTTP
responses a native tab with a status pill and Body, Headers and Request
segments (7.0 below), drops the toolbar Git button in favour of the sidebar
switch, restyles the project switcher, and makes New File, New Folder and
Rename an inline name field in the tree (FEAT-007). Before that, e2da205
added .http requests and 1ea5993 completed native hunk staging. The reported native UI
fixes through BUG-007 are complete. The subsequent commits added hover tab
close, shorter tabs, Cmd-P filename ranking, unified find-bar geometry,
Explorer icon actions and file drag moves. The 2026-09-22 follow-up moves the
project dropdown onto the toolbar title and removes the large sidebar card,
gaining 58 pt of content height. A follow-up keeps that switcher visible in a
420 pt window with a compact Find control and measured middle ellipsis for
long project names. Source Control is a sidebar view with a diff
in the editor column.
The installed native app now has a custom toolbar, system-font UI labels, a project title menu, quieter tabs and icons, breadcrumbs, rounded file picker, useful status and accurate pointer/I-beam/ resize cursors. Gutter clipping is fixed. The browser concepts remain design references, with personal sample names and regenerated previews.
Source Control (toolbar or Cmd-Option-G) now provides real branch/status, changed files, staged/working-tree diff previews, whole-file and tracked-text hunk staging/unstaging, and committing staged changes. Git runs on workers with literal pathspecs, NUL-separated filenames and bounded diff previews. Hooks/signing are preserved; no remote operations or login-shell sourcing were introduced. Git reflects saved disk state; Refresh checks external changes.
377 tests, Clippy, formatting, supply-chain checks and the expanded native GUI suite pass. The latest 2026-09-22 100 MiB gates measure 0.885 ms p99 editor and 1.014 ms with native chrome, below 8.333 ms. Long-line gates pass; unchanged-paragraph edit/undo layout is 0.042 ms p99. No new crates were added (still 14). The native GUI suite now covers a blocked Explorer move onto a dangling symlink and an allowed move. See handoff for the specific validation and installation commands.
Immediate priority: the user’s next reports. The 2026-09-21 reports are
recorded in issues.md: BUG-001 to BUG-007 and FEAT-001 to
FEAT-003 are fixed. Two 2026-09-22 audit findings, BUG-008 and BUG-009, are
also fixed: Explorer moves and renames refuse an occupied destination
atomically, and Refresh scans on a worker while Collapse All reads no disk.
FEAT-004 records the user’s compact project-switcher request and its native
implementation. BUG-010 records the narrow-width regression and fix at
f1905a5; PERF-001 records a slow file-open Return frame in a 4,000-file
fixture. Its identified double-draw and early AppKit title update are fixed at
fc6e4f1, with three native repeats below the 8.333 ms budget. Explorer
Refresh itself measured 0.305–0.354 ms in that fixture. Direct popup visual
inspection is still blocked by cmux computer-use onboarding. The final install
and checks are in the handoff.
FEAT-005 adds native Stage/Unstage controls on tracked-text hunk separators at
1ea5993, with fresh-patch validation and no worktree mutation. Other change
types retain whole-file actions. The native GUI suite covers one-hunk actions
in a disposable repository; the Metal hunk frame was inspected.
Markdown prose now renders in the proportional system font with real bold and italic, and the parser no longer eats underscores, asterisks or backslashes; heading size is still capped by the atlas cell height, which is written up in the handoff. Source control is now a sidebar view with a grouped change list, per-row staging and a parsed diff in the editor column, replacing the centred modal. Passing tests still do not supersede hands-on reports from the installed app.
UI labels are centred by measurement (push_ui_text_centered,
push_ui_text_right), not by hand-picked insets. The insets are what made
BUG-002 come back after it was closed; a label of a new length broke it again.
Further feature work: native light appearance, overflow, backing-scale/font zoom and accessibility remain. Git gutter diffs, branch switching, history, conflicts and remote operations remain separate roadmap work. SQLite-backed outline/local history are not implemented by this checkpoint.
The working tree was clean at the start of the 2026-09-22 audit. Task
completion requires checks, handoff updates, installation and a commit under
AGENTS.md. The longer-term estimates below have not been re-estimated.
Estimates are rough and assume the same working pace as the last stretch. They are for ordering work, not for promising dates. The total is about 25 working weeks, which does not fit in what is left of 2026. The cut line is at the end.
Done so far
Section titled “Done so far”The audit’s initial steps are merged into main.
| Step | What | Closes |
|---|---|---|
| 0.1 | Rope boundary safety | #1, #2, #89, crash half of #3 |
| 0.2 | Opening a file opens a tab | #12, #13, #20, #48, #49 |
| 0.3 | Multi-cursor integrity | #4, #5, #6, #11, #30 to #34 |
| 0.4 | Borrow discipline | #7, #8, #56 |
| 0.5 | Syntax state per document | #3, #63, #64 |
| 0.6 | Small crashes and corruptions | #9, #10, #18, #19, #94, #95 |
| 0.7 | Crash recovery | checked end to end in the GUI suite 2026-09-24; prompt names the documents |
| 0.8 | CI hardening | #97 to #103, #105; #104 reduced |
| 1.1 | One layout (layout::Chrome), scroll re-clamp |
#35 to #41, #87, drag origin of #42 |
| 1.2 | Scroll deltas carried between events, pixel-smooth trackpad scrolling | vertical done 2026-09-24; horizontal still moves by columns |
| 1.7 | Home screen, session active index | #51; frame autosave and atomic write still to do |
| 4.2 | Grammars, first batch, with injections | #66 |
| new | File-type icons from a bundled icon font | #54 |
| new | Search controls and background project results | part of 4.4 |
| new | Markdown live editing, copy feedback, and tab controls | parts of 1.8, 2.5, 2.8 |
| new | File encoding/BOM/line-ending preservation | #17, #29; part of 3.2 |
| new | Grapheme-aware movement/deletion | #28; shaping remains |
| new | Directory argument and public.folder app registration |
directory part of #55, #106 |
Every crash identified in the original audit was addressed. Save preserves file metadata, symlinks and hard links, and rejects an overwrite when the opened file changed on disk (#14 to #16). CRLF handling (#17) is now done.
The real AppKit self-test now exercises typing, mouse input, tabs, Markdown, find fields, project search and a directory launch. It does not replace manual testing of input methods, accessibility or large repositories.
Where we are
Section titled “Where we are”The bet from milestone 0 held: keystroke to frame is p99 0.857 ms against an 8.333 ms budget at 120 Hz with a 100 MiB file open. Two footnotes from the audit: the in-app number ends at command-buffer commit, not at present (#93), and files over 2 MiB get no highlighting, so that figure is a plain-text number (#65).
Working today: the rope and undo, multi-cursor on the insert and backspace paths, find and replace, the Metal renderer, the CoreText atlas with fallback, tree-sitter for Rust, sidebar, tab bar, palette, session restore, Markdown preview, CI and the dependency gates.
At the audit, 111 items were found: 11 crashes, 9 ways to lose or corrupt work,
and a long tail of wrong behaviour. The suite was green then with 206 tests,
clippy clean. Almost every bug is in how components are composed: window.rs
computes the text rectangle three different ways, clicks ignore it entirely,
and the tool meant to catch this (frame_dump) re-derives the layout itself
and has drifted from the app.
Supply chain: 14 crates, 1 vendored build script, 0 proc macros. Nothing in phases 0 to 3 adds to that.
Rules for the work
Section titled “Rules for the work”- One small PR per numbered step. Each one names the bug numbers it closes.
- Every fix lands with the test that would have caught it. For text bugs that means non-ASCII, CRLF and multi-cursor cases in the existing oracle tests.
- Every visual fix is verified by a frame dump through the app’s own composition, never by a unit test alone. Step 1.1 makes that possible.
- No new crate without the review in
dependency-review.mdand an explicit yes.
Phase 0. Stop losing work
Section titled “Phase 0. Stop losing work”About a week. Nothing else matters while the editor can abort and take the session with it.
0.1 Rope boundary safety. Chunks and the search functions work on bytes,
or snap to character boundaries, and read_rope hands tree-sitter bytes. One
root cause, three crashes: #1, #2 and the crash half of #3. Bound find_in
properly while there (#89). Half a day.
0.2 Opening a file opens a tab. load_path goes through Documents::open.
Close and quit check any_dirty and prompt once, listing the files. Fixes #12,
#13, #48, #49, #20. One day.
0.3 Multi-cursor integrity. Snapshots carry the extra cursors and
clamp_positions clamps them. Every edit operation that is not multi-aware
collapses to the primary cursor first, which is correct and cheap; making each
one truly per-cursor comes in phase 3. Backspace computes its width per cursor.
Fixes #4, #5, #6, #11, #30 to #34. Two days.
0.4 Borrow discipline. One with_state helper built on try_borrow_mut,
used by every entry point. No Ref held across a call that re-enters. Fixes
#7, #8. Half a day.
0.5 Syntax state belongs to the document. (Tree, pending_edits) moves
into the document, one Highlighter per language is shared, every path drains
edits, tab clicks reparse. Fixes #3, #63, #64. One day.
0.6 Small crashes and corruptions. Preview slice (#9), function keys and
numpad Enter (#19), edit menu respects overlay focus (#18), the SQLite variadic
and the missing ROLLBACK (#94, #95), columnResizeCursor guarded by
respondsToSelector (#10). One day.
0.7 Crash recovery. A panic hook that writes every dirty buffer to
~/Library/Application Support/crc/recovery/ before the abort, and a
prompt on the next launch. The hook runs even with panic = "abort". It cannot
read the state RefCell, which is usually borrowed at the moment of a panic,
so each edit publishes an O(1) rope snapshot of the dirty document to a side
table the hook reads instead. About 120 lines, and it turns every crash still
undiscovered from lost work into an annoyance. One day.
0.8 CI hardening, in its own PR since it changes nothing in the app.
Actions pinned by SHA, permissions: contents: read, a rust-toolchain.toml,
test gated on supply-chain with needs:, an offline build job against the
vendored tree, cargo vendor --locked, and the build-script gate rewritten on
cargo metadata. Record upstream commit and checksum for everything in
third_party/. Fixes #97 to #105. One day.
Phase 1. One geometry, real input
Section titled “Phase 1. One geometry, real input”About two weeks. These are the bugs you feel in the first minute.
1.1 A single layout function. Done in part 2026-09-23. frame_of builds
one list of named hit regions from the functions that draw them; mouse-down,
cursor rects and scripted clicks (click @sidebar.action.0) read it. The
draw path itself still composes in draw_now. The original item:
compose(state, size) -> Frame returns every
rect (sidebar, tab bar, find bar, text, status) and the instance list, and does
not need an NSView. render, grid, handle_key, offset_for_event,
tab_click and menuForEvent all read from it. frame_dump calls it too, so
a dump finally shows what the app shows, and gains arguments for a file path,
preview mode and window size. Add clamp_scroll on resize, after every edit
and after tree mutations. Fixes #35 to #39, #41, #83, #87, and the roadmap’s
old unresolved scroll item (#37). Three days. This is the most valuable single
step in the plan.
1.2 Scrolling. Precise deltas with an accumulator, line deltas scaled properly, then a fractional pixel offset in layout so scrolling is smooth rather than whole lines. Fixes #40. Two days.
1.3 Mouse. Drag origin, autoscroll on a timer, double and triple click, control-click, nearest-boundary hit testing, I-beam cursor, cursor rect invalidation. Fixes #42, #43, #44, #58. Two days.
1.4 NSTextInputClient. interpretKeyEvents:, marked text drawn underlined,
firstRectForCharacterRange so the IME window sits at the caret. This is what
makes ~ and ` typeable on a Portuguese, Spanish or Italian layout, and
it brings press-and-hold accents, the emoji picker and dictation for free. The
Cargo feature is already on; NSTextInputContext is one more feature flag, not
a crate. Fixes #45. Three days.
1.5 Visual columns everywhere. One function for char-to-visual-column,
used by goal column, scroll_to_cursor and the horizontal clamp. Fixes #27.
One day.
1.6 Keys and menus. Move Line goes to Option-Up/Down, which is what every other Mac editor uses, and frees Cmd-Shift-Up/Down (#46). Cmd-1..9 by key code (#47). A Window menu, Help menu, About, Services, Enter Full Screen.
1.7 Session. setFrameAutosaveName replaces the hand-rolled frame
persistence. Welcome is a real empty-state screen, not a buffer. Atomic write.
Fixes #50, #51, #60, #61. One day.
1.8 Preview that works. Its own scroll position in pixels, read-only while showing, Cmd-E only on Markdown. Fixes #74. The parser fixes (#75, #76) follow in 2.5 with the display atlas.
Phase 2. Text that looks right
Section titled “Phase 2. Text that looks right”About three weeks.
2.1 Pixel correctness. Snap glyph origins to device pixels, pad atlas cells
by one texel, fix the wide-glyph table by asking CoreText for the advance
instead of keeping ranges, repair atlas_dump. Fixes #78, #79, #80, #86, part
of #88. Two days. Verified by diffing the same digit on consecutive lines.
2.2 An atlas that can be rebuilt. Atlas::build(font, size, scale),
rebuild on backing-scale change, Cmd-plus and Cmd-minus, a second page when
full with LRU eviction. Fixes #81, #82. Two days.
Current status: atlas construction and up to eight pages with LRU eviction
are implemented. Font zoom and rebuilding for backing-scale changes remain.
2.3 The display font atlas. The old roadmap item 1, now nearly free because 2.2 did the plumbing: a second texture at index 1, one flag bit, bold proportional headings, hashes dropped from the preview. Half a day.
2.4 Instance buffer ring. Three buffers rotated per frame, matching the layer’s drawable count. No block needed. Fixes #85. Half a day.
2.5 Markdown parser. Flanking rules for emphasis, escapes only for punctuation, images, fence length, indented code before block markers, nested quotes and tab-indented lists, wrapping of long words. Parse on edit, not per frame. Fixes #75, #76, #77. Three days.
2.6 Shaping. Lines go through CTLine, cached per line by content hash,
glyphs keyed by glyph id rather than by character. Brings ligatures, combining
marks, ZWJ emoji as one cluster and right-to-left runs. Buffer motion and
deletion already move by grapheme cluster; rendering is the remaining part.
Fixes the remaining visual parts of #28 and #54.
The ASCII fast path must stay, and the latency example is the gate: this step
does not merge if p99 moves. One week.
Current status: full native paragraph shaping through 2 MiB (bounded separately
by 4 Mi expanded UTF-16 units), cancellable workers, indexed grapheme hit testing,
fully prepared primary carets and viewport-pruned selection bands. Caret windows
share and verify against the original paragraph. Explicit bidi controls retain
full native worker preparation. Cold fallback glyph lookup/rasterization is
asynchronous. Unchanged whole-line geometry and pending jobs survive edits,
line shifts and undo using bounded structural rope comparisons. Remaining: large-paragraph native completion time, conservative
control handling, temporary fallback rearrangement and source sizes beyond the
new limits without losing context.
2.7 Word wrap. The old item 5. Introduces the visual-row to logical-line mapping, which code folding and the preview reuse. One week.
2.8 Chrome. Light theme and following the system appearance and accent, scrollbars, tab overflow (#53), indent guides, bracket-match highlight, cursor blink (done 2026-09-24), active line number, status message expiry (#57), layer colorspace. Three days. Current status: native Graphite toolbar/sidebar/tabs/breadcrumbs/status and rounded picker are implemented, with proportional labels, active line number, message expiry, gutter clipping and correct control cursors. Light appearance, overflow, scrollbars and the remaining details above still need work.
Phase 3. Files a professional trusts
Section titled “Phase 3. Files a professional trusts”About two weeks.
3.1 Save properly. Write through the symlink target, preserve mode, owner
and xattrs (fchmod, fcopyfile metadata flags via libc, no crate), fsync
file and directory, random temp name, in-place fallback when the directory is
not writable. Fixes #14, #15. Two days.
3.2 What is on disk. Line-ending detection and preservation (#17), UTF-8/UTF-16 BOM and Windows-1252 round trips (#29) are done. Remaining: final-newline and trailing-whitespace policy, read-only awareness, and a clearer choice when an unmarked legacy encoding is ambiguous.
3.3 External changes. mtime and size at load, checked on activate and
before save, with a reload or keep prompt (#16). Then FSEvents by hand-written
FFI against CoreServices, the same way font.rs and index/ffi.rs do it:
about eight externs, dispatch2 is already in the tree. Sidebar and finder
refresh from it. Three days.
3.4 Undo. Saved-state marker (#25), grouping by word and by operation (#24), a memory cap (#92), selection restored with each step.
3.5 Real multi-cursor. Every motion and edit per cursor, column selection with Option-drag. Replaces the collapse-first stopgap from 0.3. Three days.
3.6 Indentation. Detect tabs or spaces and width per file, .editorconfig,
soft-tab Tab and Backspace. Fixes #26, #21, #22, #23.
3.7 Project tree and finder. The tree, finder and Git panel follow disk
changes since 2026-09-23, through FSEvents (src/project/watch.rs). crc . is done. Walk on a background thread with incremental
results, file list from git ls-files -co --exclude-standard -z when in a
repo (zero crates, honours every ignore rule), optional visibility controls (dotfiles now show by default), symlinks
followed once, better scoring. New folder, rename, move to Trash, with open
tabs re-keyed. Multiple arguments remain. Fixes the remaining part of #55 and #68 to #73.
Three days.
3.8 Long lines. Layout and motion bounded by the visible columns, no whole-line String copies. Fixes #90, #91. Two days. Current status: shaped glyph emission binary-searches visual x. Scalar, UTF-16 and tab-aware rope summaries index fallback lookups. Rope-backed NSString keeps native grapheme context for motion/deletion, including raw CRLF, without copying the line. Source extraction, bounded expansion and maps run on workers; geometry is keyed by document/rope identity/line with shared entry and memory budgets. A visual-bounds tree prunes offscreen selection/search geometry.
All primary-caret positions are prepared; first lookups no longer call CoreText on the foreground. Verified native windows avoid whole-line caret enumeration for ordinary text, while explicit bidi controls retain native worker handling. Full native shaping remains intact on long lines and now supports source through 2 MiB. Cold fallback rasterization uses a bounded bitmap worker and first cold layout is gated. First-caret p99 is below 0.001 ms; whole-line selection on a 1.2 MB fixture is 0.012 ms. Unchanged prefix/suffix paragraphs now reuse completed geometry and pending jobs across snapshots, including line shifts and undo. Changed paragraphs, unchanged interiors between disjoint edits, native pathological context and source sizes beyond the 2 MiB / expansion bounds remain further work.
Phase 4. Understanding code
Section titled “Phase 4. Understanding code”About five weeks. This is where the README’s bets start paying.
4.1 Background parse. A worker parses an O(1) rope snapshot against a
ts_tree_copy, edits arriving mid-parse are queued and replayed, cancellation
through the progress callback. Removes the 2 MiB cap and the undo stall (#65).
Fix the span walker for nested captures (#66). Three days.
4.2 Grammars. In order of what you open: TypeScript and TSX, JavaScript,
Svelte, Go, Python, JSON, TOML, YAML, Markdown, HTML, CSS, SQL, Bash,
Dockerfile. Each is a vendored parser.c with its upstream commit and
checksum recorded. Injections (set_included_ranges, injections.scm) for
Svelte, Markdown fences and HTML. The regex subset learns to refuse what it
cannot compile (#67). Language detection by filename and shebang. Expect about
40 MB of generated C in the repo and 10 to 15 MB in the binary; that is the
cost of zero crates here and it is worth stating in the README. One week.
4.3 The SQLite index. The old item 2, unchanged: schema, tags.scm
extraction, one writer thread, WAL, invalidation from the FSEvents stream in
3.3. Ships with two consumers: a symbol outline and go-to-symbol in the
palette. Fix #96 first. Four days.
4.4 Project-wide search. Literal, case-insensitive, whole-word and regex search with clickable results runs on a worker now. Replace across files and a fuller results panel remain. Revisit the search engine after 4.2.
4.5 Git and history grew into its own phase, see phase 6.
4.6 LSP. Implemented 2026-09-23 at 19b8633. Completion, diagnostics,
go to definition and hover, from src/lsp/: a registry of one server per
language found in the usual install locations, a stdio transport with a
reader thread that wakes the main thread through dispatch_async_f, and a
client that owns the lifecycle, open documents (full sync after a 150 ms
pause), and requests in flight. Servers: rust-analyzer, gopls, pyright,
typescript-language-server, clangd. The GUI self-test drives it through
scripts/fake-lsp.py. Not done: rename, format on save, code actions,
signature help, references, and the login-shell question for finding servers
installed somewhere unusual (a config file is the honest answer). Verified
against rust-analyzer 1.98.0 on 2026-09-23.
4.7 Split panes and folding. Panes done 2026-09-23. Up to four panes share the editor column; the focused pane keeps its documents in the state’s own fields and the others are swapped in on focus, so nothing that edits, saves, finds or draws had to change. A document lives in one pane. Folding still falls out of 2.7.
Phase 5. A Mac app you can hand to someone
Section titled “Phase 5. A Mac app you can hand to someone”About two weeks.
5.1 Accessibility. NSAccessibility text role on the view: value,
selected range, line for index, bounds for range. VoiceOver currently sees an
empty window. Feature flags only. Three days.
5.2 Preferences. A plain config file in Application Support in the same
key=value format as the session, reloaded on change, opened with Cmd-comma.
Font, size, theme, tab width, wrap. No settings UI for 1.0.
5.3 Platform manners. Drag files and folders onto the window, recent
documents, the title-bar proxy icon (public.folder registration is done),
Info.plist stops claiming public.data, role becomes Editor (#106).
5.4 Release pipeline. Developer ID signing inside-out without --deep,
hardened runtime, notarytool, stapler, a DMG, universal binary with
MACOSX_DEPLOYMENT_TARGET=14.0, a tag-triggered workflow with secrets in a
protected environment. Status 2026-09-24: signing, notarization, stapling
and the DMG are done in scripts/bundle.sh --release and verified end to
end; the alpha ships arm64 only under the 30 MB gate, so no universal
binary; the workflow is not written yet. Updates: a small checker that verifies a signature over
a release manifest and opens the download page. Sparkle would be a vendored
framework that auto-updates executables, which is the exact thing this project
exists to be careful about. Three days.
5.5 Docs. README, CONTRIBUTING and the dependency review brought back in line with the code (#108 to #111), and the latency claim reworded or measured at present (#93).
Phase 6. History
Section titled “Phase 6. History”Added 2026-09-19. About six weeks for all of it, about three for the core
(6.1 to 6.4). Git is driven by shelling out to plumbing commands with -z and
porcelain v2 (the initial local panel uses v1), plus one long-lived git cat-file --batch. No git library, no
crates.
The idea that organises the phase: an editor already has three kinds of history and treats them as unrelated features. Undo is keystroke groups, local history is saves, git is commits. They are the same axis at three zoom levels. Here they are one timeline, and because rope snapshots are O(1) and a frame costs under a millisecond, moving along it can be as fast as scrolling. No other editor can do that, since none of them can redraw a file at 120 Hz.
6.1 Diff engine and gutter. Histogram diff over line hashes, ours, about 300 lines, incremental against the index blob so it runs per keystroke. Gutter marks, next and previous hunk, peek the old text inline, stage or revert a hunk from the gutter. Branch and ahead/behind in the status line. Three days.
Current status: branch/status and whole-file Git diff previews are available in the native Source Control panel. Incremental gutter diff/hunk actions and the custom diff engine remain.
6.2 The scrubber. Cmd-Option-Left and Right, or a slider under the tab
bar, walk a file through every commit that touched it. Each version is loaded
once through cat-file --batch into a rope and kept; adjacent versions are
diffed once. Added and removed lines fade in and out as you move. Release on a
version to read it, Return to open it as a read-only tab, Escape to come back
to now. Four days.
6.3 Blame as heat. The gutter is tinted by the age of each line instead of printing names. The status line shows the commit for the caret’s line. Cmd-click a line to send the scrubber to the commit that introduced it, and again to peel through to the one before that. Unsigned commits are marked, since you run GitHub in vigilant mode and the editor should agree with it. Two days.
6.4 The commit composer. The staging area is a buffer: a diff rendered by
the editor where Space stages a hunk or the selected lines and the message is
written underneath with 50/72 guides and conventional-commit types offered.
Commits go through plain git commit, so signing is whatever git is configured
to do. There is no force-push command anywhere in the app, and amend is
disabled once git branch -r --contains says the commit is pushed. Four days.
Current status: native whole-file stage/unstage, message entry and commit-staged actions are implemented through background Git commands, preserving hooks and signing. Hunk/line staging, message guides, amend and conventional-commit suggestions remain. No remote or force-push commands exist.
6.5 Local history and persistent undo. Every save, and idle edits every few seconds, snapshot the buffer into the SQLite index, content-addressed, compressed with the system libcompression by FFI. Undo survives a restart. The scrubber from 6.2 shows these between the commits, so the timeline runs from the last keystroke to the first commit without a seam. Four days.
6.6 Sessions per branch. Open tabs and cursor positions are stored by branch. Switching branch gives you back what you had open there. Dirty buffers become local-history snapshots instead of a stash you forget about. One day.
6.7 Review mode. Open a range such as main..HEAD as a changeset: the
sidebar becomes the list of changed files, the gutter diffs against the range
base, n and p walk hunks across files. It is reviewing your own branch
before you push it. bun.lock and Cargo.lock changes are summarised in
words: packages added, removed, bumped, and any new install script. Three days.
6.8 History of a symbol. Needs the index (4.3). For every commit touching
a file, parse the blob, find the symbol, store its body hash in a
symbol_versions table. “History of this function” then shows only the
versions where that function changed, and renames are followed by body
similarity. Because it is SQL, churn per function over 90 days is a query any
tool or agent can run. git log -L does a slow regex approximation of this.
Four days.
6.9 Conflicts. Three-way view from zdiff3 markers, with tree-sitter used
to accept both sides automatically when they touch different symbols. Four days.
One design point that needs your decision: an app launched from the Dock does
not see your shell profile, so SSH_AUTH_SOCK will be wrong and signed commits
from 6.4 will fail. The usual fix is to run the login shell once at startup and
adopt its environment, as VS Code does. That is the app reading your profile,
not setting the variable on a command line, but it is close enough to your rule
that I will not do it without a yes.
Phase 7. Run
Section titled “Phase 7. Run”Added 2026-09-19. About four weeks, two for the core (7.1 to 7.3). Bun is the
one JavaScript runtime the editor knows about, and it is driven, never bundled
and never downloaded: the editor finds bun on PATH and says so if it is not
there. The editor itself gains no network code. Anything that needs the
registry is asked of bun, and HTTP requests (7.0) are asked of the system
curl in the same spirit.
7.0 Requests. Implemented 2026-09-22 at e2da205, response view redone
2026-09-23 at 3f15671. A built-in HTTP client, asked for on 2026-09-22
(FEAT-006), done the editor’s way rather than Postman’s: requests are text in
the JetBrains / VS Code REST Client format (### blocks, {{variables}},
@name = value, http-client.env.json plus a private file for tokens,
< file bodies, # @env to pick an environment). Cmd-Return sends the block
under the caret through /usr/bin/curl, driven like git: one subprocess on
a worker, a 30 s timeout, a 4 MiB body cap, stderr shown verbatim. The reply
is a response tab: a strip with the status coloured by class, time, size,
protocol and peer, then Body, Headers and Request segments, each shown in the
tab’s buffer (JSON highlighted by tree-sitter) so find, selection and copy
work. The tab is reused on re-send, Cmd-Return on it re-sends, and it is
clean, so it never prompts on close. src/http/ has the parser, a small JSON
reader and printer, and the curl runner; no crate was added. Still to do, in
order of use: a sidebar list of every request in the project, response
history, a cookie jar, cancelling a request in flight, and an importer for
Postman and Insomnia collection exports. Highlighting for .http source
needs a grammar and is not done.
7.1 Tasks and an output panel. package.json scripts, Cargo.toml
targets and a Makefile show up in the palette. Output goes to a panel drawn
by the same glyph grid, ANSI colours parsed, file:line clickable. Run the
current file with Cmd-R. Three days.
7.2 Run the selection. Cmd-Return evaluates the selected TypeScript
through bun on stdin and shows the value as dimmed text at the end of the
line, errors as a red underline at the right span. A scratch file with live
results as you type, without a single package installed. Two days.
7.3 The dependency lens. package.json and bun.lock are understood, not
just highlighted. Beside each dependency: the exact resolved version, its age,
whether the installed package declares install scripts (read from
node_modules/<pkg>/package.json, no network), and whether it is listed in
trustedDependencies. A caret range is a warning. The editor never runs an
install on its own; an add-dependency command shows the facts first and then
the command it would run, and a configurable vet command (yours is npm-vet)
runs before it. This is your npm policy made visible in the place where the
damage is usually done. Three days.
7.4 Tests in the gutter. bun test, cargo test and go test run in
watch mode, each test gets a pass or fail mark beside its name, a failure
shows its message inline at the assertion. Three days.
7.5 TypeScript without npm. typescript-language-server is an npm package
with a tree behind it. The native TypeScript compiler ships as one binary with
an LSP mode, which fits here. I have not verified its current state, so this
is a spike before it is a plan. Comes with 4.6.
7.6 Project scripts. .caio/tasks/*.ts, run by bun, with the path of
the SQLite index in the environment, so a script can ask the index things
through bun:sqlite. This is the first extension mechanism, and it is honest
about what it is: your code, in your repo, run when you ask. A cloned repo’s
tasks do not run until the folder is trusted, the same model as rtk trust.
It does not replace the WASM host, because code you did not write still needs
declared capabilities. Two days.
7.7 A terminal. The renderer is already a monospace cell grid drawn in one
call, which is most of a terminal’s display side. What is missing is a pty
(forkpty by FFI) and a VT state machine, about 2500 lines. Two weeks, and
the last thing in the phase because 7.1 covers most daily needs.
Deliberately not planned: Bun as the extension host. That would be arbitrary code with full filesystem and network access inside the editor, which is the VS Code model the README exists to argue against.
Phase C. Claude Code in the editor
Section titled “Phase C. Claude Code in the editor”C.1 to C.6 implemented 2026-09-23. Verified against scripts/fake-claude.py
in the GUI self-test; a session with a real claude is still to be recorded
(see next-session.md). Planned the same day. Bet 3 in its
smallest useful form: crc does not ship an agent, it becomes an IDE that
Claude Code connects to, the way VS Code and JetBrains are. Claude sees the
open file, the selection and the language server diagnostics, and its edits
arrive in crc as a diff to accept or reject before anything is written.
Zero new crates. About eight working days.
The protocol is not formally specified. What follows comes from the Claude
Code docs (code.claude.com/docs/en/vs-code, “The built-in IDE MCP server”)
and two open-source editors that implement it, coder/claudecode.nvim (its
PROTOCOL.md) and manzaltu/claude-code-ide.el, cross-checked against Claude
Code 2.1.280 on this machine. Treat it as version-dependent and keep the
self-test honest about which parts a real claude exercised.
The shape. crc listens on 127.0.0.1, random port 10000 to 65535, plain
WebSocket, and writes ~/.claude/ide/<port>.lock ($CLAUDE_CONFIG_DIR/ide
when set; directory 0700, file 0600, temp file then rename) holding pid,
canonical workspaceFolders, ideName: "crc", transport: "ws" and a 32 hex
character authToken from /dev/urandom. The client presents the token in
X-Claude-Code-Ide-Authorization on the upgrade; anything else gets a 400
and no upgrade. The protocol on top is MCP, JSON-RPC 2.0 in text frames.
claude run anywhere inside the project finds crc through /ide, claude --ide or CLAUDE_CODE_AUTO_CONNECT_IDE=true.
C.1 WebSocket server. src/ide/ws.rs: a listener thread, HTTP upgrade
parsing, constant-time token compare, Sec-WebSocket-Accept through
CommonCrypto’s CC_SHA1 (libSystem, already linked) and a local base64,
masked and fragmented frames, ping/pong/close. One client; a reconnect
replaces the old socket. Messages reach the main thread the way the LSP
transport’s do: a channel plus the dispatch_async_f wake. Tests drive it
over a real socket. Two days.
C.2 Lock file. Written when a project opens, rewritten when the folder
changes, removed on quit, from the recovery path and on drop. On start,
remove only our own stale locks (ideName crc, dead pid). Half a day.
C.3 MCP. initialize (protocol 2024-11-05, capabilities tools, prompts,
logging, each an object, never []), tools/list, prompts/list empty,
tools/call, -32601 for anything unknown. Tools: getDiagnostics from
the language servers (0-based UTF-16 positions, severity names), openFile,
getCurrentSelection, getLatestSelection, getOpenEditors,
getWorkspaceFolders, checkDocumentDirty, saveDocument,
closeAllDiffTabs. executeCode is Jupyter only and is not implemented.
One to two days.
C.4 Context. selection_changed after a 100 ms pause, deduplicated,
0-based, the caret as an empty selection when nothing is selected. A command
sends at_mentioned (file plus 0-based line range, or the whole file) to put
the selection into Claude’s prompt. Half a day.
C.5 Diff review. openDiff opens a review tab: the current file against
Claude’s proposal, drawn with the hunk styling source control already uses.
Accept replies FILE_SAVED plus the proposed text; reject, Escape or closing
the tab replies DIFF_REJECTED. crc never writes the file, the client does.
The request stays pending until the user decides, possibly minutes; the tab
stays until close_tab arrives; a disconnect rejects and closes everything
pending. Replies keep the file’s own line endings. Per-hunk accept is the
obvious next step and is not in v1. Three days.
C.6 Presence and proof. A status line item for “Claude connected”. The GUI
self-test gains scripts/fake-claude.py, standard library only like
fake-lsp.py, which reads the lock file, connects, runs the handshake, reads
diagnostics, and accepts one diff and rejects another. Then one manual run
against a real claude, recorded in next-session.md with its version.
One day.
Security: localhost only, token checked before the upgrade, lock file
readable by you alone, one client at a time. A connected client can read
open buffers and selections, which is the same trust you already give
claude in that folder. crc itself sends nothing over the network.
Phase T. Terminal, with Claude Code in it
Section titled “Phase T. Terminal, with Claude Code in it”T.1 to T.5 implemented 2026-09-23, asked for after the user found
Claude Code had nowhere to run inside crc. src/term/ is an xterm-compatible
emulator (mod.rs), a pseudo-terminal session parsed on its reader thread
(pty.rs) and xterm key encoding (keys.rs); src/platform/terminal.rs is
the panel. The ✻ Claude toolbar button and Cmd-Shift-C run claude
through the login shell with CLAUDE_CODE_SSE_PORT set, so it connects to
the phase C bridge without /ide. Selection, copy and Cmd-click on file references followed the same night.
Remaining: mouse reporting, dead keys and IME in the
terminal, and reflow on resize.
After 1.0
Section titled “After 1.0”The agent protocol (bet 3, beyond the Claude Code bridge in phase C) and the WASM extension host (bet 2). The protocol is cheap once the index exists: a Unix socket, JSON lines, read-only first. I would ship that in 1.1 because in 2026 it is the differentiator. The extension host needs a WASM runtime, which is either hundreds of crates or an interpreter of our own, and that decision deserves its own document.
The cut line for 2026
Section titled “The cut line for 2026”Phases 0 to 5 are about 15 working weeks. History and Run add about 10. From 2026-09-21 that is the end of March 2027 for everything, so the question is what ships first.
- Beta, mid December: phases 0 to 3, then 4.1, 4.2 and 4.4, then the core of History (6.1 to 6.4) and of Run (7.1 to 7.3), then 5.4. About 13 weeks. A fast, correct, good-looking editor with highlighting for your whole stack, search, the scrubber, the composer and the dependency lens, signed and notarized. No LSP, no index, no terminal.
- 1.0, end of March 2027: the index, LSP, panes, the rest of History and Run, accessibility, preferences.
If mid December slips, shaping (2.6) goes first, since ASCII code looks right without it, then real multi-cursor (3.5), since the collapse-first stopgap from 0.3 is correct, only less capable.
Blocked on you
Section titled “Blocked on you”- rust-analyzer. Resolved 2026-09-23: the component is installed and
the editor looks in
~/.rustup/toolchains/*/binbefore the recursing proxy onPATH. 4.6 is verified against it. - Apple Developer ID. Needed for 5.4. Nothing else depends on it.
block2. Same maintainers as the rest of the objc2 family. Without it there are no sheets, noaddPresentedHandlerand no completion handlers. Nothing in this plan needs it; alerts stay app-modal and the latency number stays at commit. My recommendation is not to add it. Say so if you disagree.- The grammar list in 4.2 and whether 40 MB of generated C in the repo is acceptable, or whether grammars live in a second repository.
- The login-shell environment question at the end of phase 6. Blocks signed commits from inside the app, nothing else.
A standing lesson
Section titled “A standing lesson”Three bugs were invisible to a green suite and were found only by dumping a
frame: the atlas created as R8 while being fed RGBA, the tab bar drawn and
then erased by a later clear(), and a proportional font substituted for a
monospace one. The audit added a hundred more, under the same green suite, and
found that the dump tool itself had stopped rendering what the app renders.
Unit tests here check components in isolation, and the bugs are in how the components are composed. Step 1.1 exists to make the composition a single function that both the app and the dump call.
Instrument the real path. Do not reason about it.