Repository navigation
Feature proposal: editor tabs (multiple documents per window) #29
Description
I've been running a local branch that adds editor tabs — several documents in one window — and it's reached a state where it might be worth upstreaming. Before opening a PR I wanted to ask whether you'd want the feature at all, and if so in what shape, since it touches the document model.
Branch: https://github.com/Systemofdumb/domd/tree/pr/editor-tabs (on top of v0.8.1)
What it does
A tab strip on the desktop editor: open several documents in one window, drag to reorder, Cmd+Shift+[/] and Cmd+1..9 to navigate, close with the tab's ⨯ or middle-click. Files opened from Finder or the CLI land in a tab of the focused window, and re-opening a file already open activates its tab instead of duplicating it. Web is unchanged.
Approach
The part I'd most like your opinion on.
My first attempt was a second editor shell for desktop that mirrored the existing one. Upgrading it from v0.5.0 to v0.8.1 showed why that doesn't hold — the per-document tree had gained ModeController, TitlebarBridge, LocalAiBridge and AiCollab, and mirroring them would have to happen every release, with anything missed silently absent from the tabbed build.
So instead there is still one editor shell, and tabs sit underneath it. A tab store becomes the document source that useDocumentLoaders reads from; its public API is unchanged, so editor-app is untouched apart from mounting the tab bar and passing embedded to Editor. This leans on something the editor already does: key={version} remounts the kernel when a document is replaced, and a tab switch is that same event with a different document arriving. Tabs therefore inherit the existing tree whole — collaboration, disk reconciliation, the outline panel, find & replace, AI — and web is simply the case where nothing opens a second tab.
Net effect on your files is small: editor-app.tsx +42 lines, editor.tsx +43. Everything else is new files plus the Rust side.
Per-document subsystems
Only the active tab has a mounted editor, so:
- Collaboration binds to the active tab's
docId, and a tab switch runs the same teardown as loading a different file into the window. The room record survives and resumes when its tab returns. - Disk reconciliation stays with the active editor. For background tabs the watcher records staleness only; on activation a clean tab re-reads from disk and a dirty one gets a forced reconcile, so an external edit merges rather than overwrites.
- Documents load through the existing frontmatter/
domd-idpipeline, so tabs keep document identity.
Not losing work
- The outgoing editor flushes a save on tab switch — autosave is debounced 600ms and dies with the unmount.
- Closing a dirty, never-saved tab asks first.
- The window's assigned path follows the active tab (new
clear_window_pathcommand), so the native "save changes?" sheet still speaks for whatever is on screen.
One thing I got wrong and want to flag, since it's a trap: I briefly registered a JS onCloseRequested listener to prompt for background tabs. That moves window destruction into JavaScript (if (!evt.isPreventDefault()) await this.destroy()) while the Rust close handler is running its own flow, and it left the close button dead. Window close is left entirely to the existing Rust handler now. The residual gap is a background tab holding never-saved work when the window closes — the native sheet can only speak for one document.
Rust
WindowTabs tracks each window's open paths. open_or_reuse routes a file into a tab of the focused window and activates an existing tab when the file is already open. Cmd+N / Cmd+W act on tabs. The file watcher polls every open tab rather than only each window's last-assigned document.
State
Frontend and Rust build clean, and eslint output is identical to v0.8.1 — zero added or removed issues. Verified on macOS: two files opening as tabs in one window, re-opening activating the existing tab, and a background tab picking up an external edit on activation. Only macOS has been exercised.
Happy to open this as a PR and sign the CLA if you're interested, and equally happy to rework the approach or drop it if tabs aren't a direction you want. Understood either way — it's a decent chunk of surface area for a single-maintainer project.
Activity
do-md commentedon Sep 7, 2026
Thanks for asking first, and for the thoughtful write-up. Yes — I’d like tabs, and I think keeping a single editor shell is the right direction. The part I’d change is what survives underneath that shell when switching tabs.
There’s one fact about the kernel that isn’t documented anywhere and may change how you approach that part of the implementation.
The kernel store runs headless
EditorStorehas no DOM dependency. You can construct one in bare Node and it works —toMarkdown(),replaceRanges(), undo/redo, and the whole model layer. The DOM only enters at the render surface.Some numbers from a 20k-line / 595 KB document: the constructor returns in ~8 ms, the document finishes loading ~780 ms later through idle-scheduled chunking, and it costs ~42 MB of heap, scaling linearly.
The change I’d like: one store per tab
Rather than having only one live document at a time, I’d like each tab to own a long-lived
EditorStore— the document runtime. It holds the model, undo stack, and dirty state, and can later own or reference the collaboration session as well.The store is created when a tab opens and destroyed when the tab closes, never when switching tabs. The single mounted editor then becomes a view over the active tab’s runtime.
Most of the behavior follows naturally from that:
So I agree with keeping one editor shell. The difference is that a tab switch should hand the Provider another tab’s existing store rather than hand it another document to parse into a newly constructed store.
Detaching and reattaching the view is the kernel’s responsibility, and neither store should be recreated or reset in the process. I’ll confirm the exact mechanics once the relevant kernel interface lands, so there’s no need to depend on a particular teardown behavior for now.
What I’ll add to the kernel first
There’s currently no way to hand an existing store to the editor:
DOMDProvideronly acceptsinitMd, and the underlying store Provider always constructs its own store.I’ll add an optional
storeto both. If one is passed, it will be used as-is; if not, the current behavior will remain unchanged.Alongside that, I’ll add view-state restoration to the kernel — caret, focus, and scroll position, anchored to the document in the same way the cursor already is. Attaching a view to a store will restore all three, so the tab implementation won’t need to manage them itself.
Both packages are checked into this repository, so once the change is on
main, you’ll be able to build against it without waiting for an npm release. I’ll comment here when it’s ready.What I’d like this PR to cover
The runtime registry and tab lifecycle, as described above, including view attachment and detachment when switching tabs.
Tab bar UI. With a single tab, it should render nothing at all — no strip, no border, and no reserved height. A one-document window should look exactly as it does today.
Per-tab dirty and untitled state reported to Rust, so the existing three-button Save / Don’t Save / Cancel sheet and the ⌘Q review can account for tabs that aren’t currently visible. This is the part I care most about getting right: closing a window must not be able to discard unsaved work in a background tab.
Shortcuts and menus.
⌘N/Ctrl+Nshould open a new tab in the current window;⇧⌘N/Ctrl+Shift+Nshould open a new window. I’d like both actions available from the menu, with matching en/zh/ja labels.⌘⇧[/⌘⇧]for tab navigation. One thing to be aware of is that⌘1–⌘6and⌘0already belong to the heading/body commands in@do-md/commands(keymap.ts), while⌘Tinserts a table. Both keymap systems listen onwindowduring the bubble phase, so collisions are currently resolved by component mount order rather than by a deterministic rule.metaKey.⌘Wshould close the active tab. With only one tab remaining, it should close the window. This should continue through the existing Rust close flow so the native confirmation sheet remains the single authority for close confirmation.Rust. The per-
(label, path)handling you introduced infile_watch.rsis structurally what I want, so please keep it. Two nearby details are worth watching:open_or_reuseshould deterministically choose the currently focused window; the iteration order ofall_windows()is arbitrary.benchmark_mark_readyfires every time the editor mounts, not only on the first mount. The window’s readiness state therefore needs to be cleared when a file is opened into a tab. Otherwise, an immediateinsertfollowingdomd-cli openmay run against the previous document.Find and outline state do not need special per-tab handling. The current Provider boundaries already keep panel open/closed state at the window level, while the panel contents rebuild from the active document.
An optional question about background sync
Separately — and this is not a requirement for the tabs PR — I’d like your take on whether the background-live work fits naturally into this architecture.
Would you be interested in extending it so every open tab remains live in the background?
The behavior I have in mind is:
EditorStore, whether or not the tab is visible.The benefit is that once a document has been shared, it stays current regardless of whether its tab is active. Whether a change comes from the local editor, the filesystem, or another collaborator, everyone sees it promptly without requiring someone to switch back to that tab first.
If this feels like too much scope for the PR, that’s completely fine. The core tab work doesn’t need to depend on it, and I can add the background watching and multi-store WebRTC support afterward. I’d mainly like your opinion on whether it fits naturally into the architecture you’re building now.
Please push back
This is my current view of the design, not a fixed specification. If you see a better decomposition or think any part is more elaborate than tabs actually require, please say so.
You’ve spent more time in this area recently than I have, and I think the design will benefit from that discussion.
The same applies if something turns out to be significantly harder than it appears once you’re implementing it. I’d rather revisit the design than have you work around it.
do-md commentedon Sep 7, 2026
One additional point: I’m aware that keeping a long-lived store for every open tab may become a memory concern when many large documents are open. This is already part of the longer-term plan. One direction is to safely hibernate or evict the stores of tabs that have been inactive for a long time and reconstruct them when needed. Another is to move away from each store eagerly loading the entire document: stores will eventually load document content by chunk and keep their memory usage bounded. So the current per-store memory cost isn’t the intended long-term scaling model, although it’s acceptable as a first step.
Systemofdumb commentedon Sep 7, 2026
Thanks — that's a much better decomposition than what I had, and the kernel detail changes the picture considerably.
Store per tab
Agreed, and I'd go further: it's simpler than what I built, not more elaborate.
The weakness in my version that I should have called out and didn't — undo history doesn't survive a tab switch. The bridge serializes to markdown, the Provider remounts on
key={version}, and the new store starts with an empty undo stack. Caret position goes the same way; I restored scroll offset and nothing else. That's not a rough edge, it's a defect, and it's inherent to "a tab switch is a document swap" rather than something I could have patched around.Once each tab owns a long-lived store, most of my machinery stops having a reason to exist:
docEpochgeneration counter guarding write-back against a stale buffer — unnecessary, there's no stale bufferSo the registry replaces more code than it adds, and the ~780 ms re-parse disappears with it. Happy to build against
storeonDOMDProviderplus view-state restoration once they're onmain— and glad view state is landing in the kernel, because anchoring caret restoration from the tab layer is exactly the kind of thing that ends up subtly wrong.The two Rust issues
Both correct, both fixed on the branch (
fe9d718).benchmark_mark_ready. You're right that it fires on every mount. My code had the opposite assumption written into a comment — that adding a tab wouldn't remount the editor, so clearing the flag would strand the CLI.open_or_reusenow clearsWindowReadybefore emittingopen-file-in-tab. Verified with your exact case,openimmediately followed byinsertand no delay between them: the text lands in the newly opened document and the previous file is untouched on disk. It also explains a symptom I'd been misreading as unrelated —domd-cli openintermittently returningnot_ready— which is gone now that readiness is cleared and the remount re-marks it.Window selection. Fixed: labels are sorted by their
w<N>creation index, and both the focused lookup and the fallback walk that order, so the fallback is "most recently opened window" rather than a hash ordering. That also removes the ambiguity if more than one window ever reports focus.A third instance I left alone deliberately. The same race exists on the
activate-tabpath when the requested file is already open in a background tab: that switch remounts the editor too, so readiness is briefly stale. I didn't fix it because Rust can't distinguish "switching to a background tab" from "re-activating the tab that's already active", and clearing in the second case would leave the CLI waiting five seconds for a mark that never comes. It seems to want a signal from the frontend, which is new command surface I'd rather you shape than have me invent. Worth folding into the readiness story when the store registry lands, since attach/detach is the natural place for it.Keymap arbitration
Dropping
⌘1–⌘9for tab navigation is fine —⌘⇧[/⌘⇧]only, and I'll make the shortcuts desktop-only and platform-correct rather than testingmetaKey.But I'd push back on treating the collision as a tabs problem. If both keymap systems listen on
windowduring the bubble phase and conflicts resolve by component mount order, that's non-deterministic today and tabs are only the first consumer to make it visible. Working around it means the next feature that wants a binding rediscovers the same thing. A deterministic priority or ownership rule seems worth having on its own; I'm happy to avoid the contested keys either way, but it felt worth naming rather than quietly routing around.Close flow
Understood, and this corrects something in my branch: closing the last tab currently calls
force_close_window, which destroys the window and bypasses the sheet entirely. Under your model that becomes an ordinary window close with the sheet as the single authority, and per-tab dirty/untitled state reported to Rust is what lets the sheet and the ⌘Q review account for tabs that aren't visible.Related, and the reason I agree this is the part to get right: I briefly had a JS
onCloseRequestedlistener to prompt for background tabs, and it broke the close button outright. Registering one moves window destruction into JavaScript —if (!evt.isPreventDefault()) await this.destroy()— while the Rust handler is running its own flow, so a handler that prevents or throws leaves nobody to close the window. Reporting state to Rust and leaving the decision there is clearly the right shape.Background sync
Architecturally it fits, and it's specifically the store registry that makes it possible: a background document needs a live runtime to receive a disk change and hand it to its collaboration layer at all. Under my current design there's nothing there to receive it.
I'd keep it out of this PR though. Multi-store session routing is a different problem from tab lifecycle, and I'd rather the tab work be reviewable on its own than have both land together. If the registry exposes the per-tab stores cleanly it should be additive afterwards.
Sequencing
The registry is blocked on your kernel change, but a few things aren't, so I can make progress meanwhile:
Then the registry and view attach/detach once
storeand view-state restoration are onmain.Two caveats worth stating plainly: only macOS has been exercised, and I don't have a Windows or Linux machine to check the non-mac paths. And on memory — your follow-up matches how I'd think about it; a store per open tab seems a reasonable first step given eviction and chunked loading are on the roadmap, and it's bounded by how many documents a person actually keeps open.
Complete the view attachment protocol: restore focus and selection
do-md commentedon Sep 8, 2026
The kernel changes are on
main, in the checked-in packages.What I added
storeon the Provider.DOMDProviderand the underlying store Provider now take an optionalstore. When given, it is used as-is and every construction-time prop (initMd,placeholder, tokenizers, inline rules, …) is ignored — the store already carries them. The Provider never constructs, resets, or destroys a store it was handed. Without the prop, behavior is unchanged, byte for byte.View-state restoration. Detach and re-attach is now an explicit kernel protocol — that settles the caveat I gave earlier about not depending on teardown details. Detach unbinds and nothing else; attach re-binds and restores. The store carries three axes of persistent view-state, and a view mounting over a store restores all of them:
store.scrollAnchor/setScrollAnchor.The three axes are independent: the anchor owns the viewport and the selection replay carries no scroll command, so a caret that was off-screen at detach stays off-screen, exactly as the user left it.
Restoration only runs when the store has state to restore — a fresh store's first mount is unchanged. Repeated detach/attach cycles, including with blur/focus history in between, are exercised and hold up.
The two open threads
activate-tabrace wants a frontend signal, and attach/detach now exists as a concrete kernel moment. I'll pick up the command-surface discussion in your PR.Systemofdumb commentedon Sep 8, 2026
The registry is built against the new kernel seams and working. Branch updated: https://github.com/Systemofdumb/domd/tree/pr/editor-tabs (rebased onto
main, three commits).Store per tab
Each tab owns an
EditorStorefor as long as it is open — created when the tab opens, dropped when it closes, never on a switch. Switching remounts the view over the other tab's existing store via thestoreprop.The registry is a plain
Mapon the tab store rather than part of its state: that state goes through immer, which freezes, and a frozenEditorStoreis an inert one. Nothing renders from the map directly, so it does not need to be reactive.It removed considerably more than it added, as you expected:
docEpochgeneration counter that guarded write-back against a stale bufferTabis down to id, meta, dirty, and two disk-staleness fields. The mount key is the tab id alone: replacing the document inside a tab resets the runtime in place rather than remounting, since remounting would discard the view state the attach protocol just restored.Scroll and selection restoration work exactly as advertised, and undo now survives a switch.
The two Rust issues
Both fixed and verified.
benchmark_mark_ready— cleared inopen_or_reusebefore routing a file. Verified with your exact case,openimmediately followed byinsert: the text lands in the newly opened document and the previous file is untouched on disk.Window selection — labels sorted by their
w<N>creation index, with both the focused lookup and the fallback walking that order, so the fallback is the most recently opened window rather than a hash ordering.Readiness needs a frontend signal after all — and here is why
This turned out to be sharper than the
activate-tabcase we discussed. Reusing the lone blank tab no longer remounts anything: the runtime is reset in place, precisely so view state survives. So clearing readiness inopen_or_reuseleft nothing to re-mark it, andopenstarted returningnot_ready.The tab layer now re-arms readiness once the document is actually in place, because it is the only party that knows — it is true for both the reuse and the new-tab branch. That is
invoke("benchmark_mark_ready")from the frontend, no new command surface, but it does mean readiness is now a thing the host asserts rather than something that falls out of a mount. Worth deciding whether that is the shape you want before it hardens, and theactivate-tabcase can then use the same signal.One observation from the attach protocol
Restoring focus is the host's job — agreed, and the tab layer does it, since selecting a tab means "I want to work in this document" and the kernel binds its key handling to the editable root.
The part worth reporting: doing it through
store.focus()was not sufficient across a view remount. After a switch,document.activeElementwas the editor root andstore.focused_was true, a synthetic keydown at that root undid correctly — and real keystrokes still did nothing until something called.focus()on the element itself. Calling it on the element (throughuseEditorDom) fixed it.I am not claiming a kernel bug: in this path the store was legitimately blurred at detach, since clicking the tab bar moves focus there, so restoration correctly declined to steal it. But the gap between "model says focused" and "the browser will deliver keys here" is worth knowing about if other hosts start attaching views.
Separately: undo was never reachable on desktop
While chasing the above I found that
⌘Z,⇧⌘Zand⌘Ado nothing in the desktop app, and so do the matching Edit menu entries — in an ordinary single-document window, nothing to do with tabs. The predefined Edit menu items carry the standard key equivalents, so macOS claims the keys and sendsundo:/selectAll:down the responder chain instead of letting them reach the editable root.Filed as #30 with a fix branch, since it affects every desktop user independently of whether tabs land.
Still open from your list
Not started yet, all independent of the kernel: tab bar rendering nothing at all with a single tab; per-tab dirty and untitled state reported to Rust, with
⌘W-on-last-tab going through the normal close flow; and the⌘N/⇧⌘Nmenu work with the en/zh/ja labels.One caveat to repeat: everything here is macOS-only testing. I have no Windows or Linux machine, so the non-mac paths in this branch are unexercised.
Systemofdumb commentedon Sep 8, 2026
The four items are done and on the branch: https://github.com/Systemofdumb/domd/tree/pr/editor-tabs (five commits on
main).Tab bar
Renders nothing at all with a single tab — not hidden, absent, so the column never allots the row. A one-document window is identical to before tabs existed.
Per-tab dirty and untitled state
update_tabsnow carries path, dirty, and content — the last only for tabs that are dirty AND never saved, since those are the only ones the sheet could be asked to review and the only ones whose text Rust has any reason to hold.One helper answers "does this window hold work no autosave will ever write, and what should the sheet show?", and all three gates consult it:
CloseRequested, the queuedirty_untitled_labelsbuilds, and the per-window recheck inside the quit review. Tab state is authoritative once the frontend has reported it; the old per-window snapshot stays as the fallback for a window that has not reported yet.Verified end to end: a window whose visible tab is a clean saved file now refuses to close while a hidden tab holds unsaved work, and closing the same window with force still succeeds.
Close flow
⌘Wwith two or more tabs closes the active tab. On the last tab it goes through the ordinary window close rather thanforce_close_window, so the native sheet stays the single authority — and it now sees every tab, not just the visible one.Shortcuts and menus
⌘Nopens a tab,⇧⌘Na new window, both as menu entries with en/zh/ja labels. Navigation is⌘⇧[/⌘⇧]only;⌘1–⌘9are gone, and nothing here claims⌘T.Two things worth reporting from that work:
The navigation shortcuts could never have fired. They tested
event.key === "[", but with Shift held the bracket keys report{and}. They match onevent.codenow, with a platform-appropriate modifier rather thanmetaKeyonly, and the hook is desktop-gated.domd-cli closecould discard unsaved work. It checked the window-level dirty flag — which under tabs reflects only the active document — and then callsdestroy(), deliberately bypassingCloseRequested. So a never-saved document in a background tab could be thrown away by a close the caller believed was safe. It uses the same gate now;--forceis unchanged.One interaction decision I would like your view on
Closing a single tab among several has no native counterpart — the sheet speaks for a window — so that path asks in-app. Tauri's
askgives two buttons, so the prompt is currently Save / Don't save with no Cancel: once you have triggered the tab close you cannot abort it.That is a deviation from the three-button sheet everywhere else, and from what you would expect on macOS. The fix is a small in-app modal rather than the plugin dialog, but it is your product's interaction design, so I would rather ask than pick. Window close and quit are unaffected — both still go through the native sheet.
State
Frontend and Rust build clean, and eslint output is identical to
mainwith zero added or removed issues. Tested on macOS: tab bar absent at one tab,⌘Wclosing the active tab and prompting for a never-saved one, the close gate refusing over a hidden tab's unsaved work, and undo/selection/scroll surviving a switch.Still only macOS — no Windows or Linux machine here, so those paths are unexercised.
Separately, #30 (the Edit menu swallowing
⌘Z/⇧⌘Z/⌘A) is not on this branch: it is not tab-related and stands on its own.do-md commentedon Sep 8, 2026
Thanks for taking it this far, and for the reports along the way. The navigation
shortcuts never being able to fire, and
domd-cli closebypassing the tab gate,are both things I wouldn't have caught.
On the close prompt: your read is right, and I'd go with the in-app modal. Three
buttons — Save / Don't Save / Cancel — with Escape cancelling and Enter defaulting
to Save. A destructive prompt you can't back out of is worse than one that isn't
quite native chrome, and matching the sheet's semantics matters more here than
matching its looks.
On #30: I'm working on it. Thanks for splitting it out — you're right that it
stands on its own.
Systemofdumb commentedon Sep 9, 2026
The close prompt is done as you specified, and the branch is updated: https://github.com/Systemofdumb/domd/tree/pr/editor-tabs (six commits on
main).The modal
Three buttons — Don't Save / Cancel / Save — with Escape cancelling and Enter defaulting to Save, and the native sheet's grouping: the irreversible choice set apart on the left, away from the cluster your hand is already heading for. Window close and quit are unchanged; both still go through the sheet.
Enter is not bound by hand. The default button takes focus on mount, so Enter-saves falls out of ordinary button behaviour and Tab order and assistive tech get the right answer without special cases.
Two details from wiring an async close flow to a rendered component. The resolver parks in a ref, so a second close request while a prompt is open is ignored rather than stranding the first one's resolver, and unmounting with a prompt open resolves it as cancel — the only outcome that touches neither the document nor the disk.
Cancel restores focus to whatever had it before the prompt. It has to: the editor stays mounted, focus is sitting on a button that is about to disappear, and the kernel binds its key handling to the editable root — so without it the next keystroke goes to
<body>and typing silently does nothing until you click back in. Save and discard deliberately skip it, since both close the tab and focus for the document that comes forward belongs to the switch path.Verified in both WebKit and Chromium, 18 assertions: Don't Save closes the tab, Cancel keeps it and returns focus to the editor, Escape cancels, and repeated prompt/cancel cycles leave exactly one editor shell.
One trap worth recording
I shipped this with the modal keyed by tab id and it corrupted the window: closing a tab left the old document on screen above the new one, permanently, and the app kept working around the corpse.
DOMDProvideris keyed byversion, which is the active tab id. Keying the modal by the tab being closed therefore puts two siblings under the same key whenever you close the active tab — which is the common case. React then mounts a second editor shell without removing the first.What made it expensive to find is that it is completely silent in a production build: the duplicate-key warning is development-only, so there is no error, no exception, and nothing in any log. I spent several rounds on plausible wrong theories — that the editor's unmount was unsafe in WKWebView, then that it was a repaint artifact — before instrumenting the running app to count editable roots in the DOM and finding two live editor shells under the app's root column, with zero errors recorded.
The modal is unkeyed now, like the other modals in that file, with a comment saying why. Flagging it because the tab id is exactly what anyone would reach for next, and this file already has a keyed provider sitting next to a list of conditionally-rendered modals.
#33 will shape how tabs look to you
Please take #33 before reviewing this branch, or it will look like the tab work is broken.
Backspace as the first keystroke in an empty document permanently kills the editor — no caret, no placeholder, nothing typed afterwards is ever inserted, and no error. It reproduces on
mainin WebKit with no tabs in the path; Chromium is unaffected, which is why it is invisible in a browser and reliable in the shipped macOS app. A new window opens on exactly that state, so it is "new window, hit Backspace by reflex".It matters here because tabs remount the editor on every switch, and unmounting an editor in that state throws
removeChild— which aborts React's commit and leaves the old tree stranded on screen. So upstream the bug is a quietly dead editor; with tabs it is a visibly wrecked window within seconds. Same bug, and the reproduction script in #33 needs none of this branch.Also outstanding
Both independent of tabs, both with branches: #31 (letter shortcuts matched by physical key position, so
⌘Wruns Undo on any non-QWERTY layout) and #32 (set_localerebuilding the menu bar with an identical copy, leaving the discarded menu's key equivalents live).Still macOS-only testing here — no Windows or Linux machine, so those paths remain unexercised.
do-md commentedon Sep 11, 2026
#33 is fixed and on main — run your harness, the empty-doc backspace should be gone. #30/#31/#32 are all merged too, so the ground's clear to review the branch properly.
Readiness as a host-asserted signal works for me; use the same invoke(benchmark_mark_ready) for activate-tab. Background sync — agreed, keep it out, it's additive later.
Open it as a PR and I'll do a full review pass.
do-md commentedon Sep 13, 2026
Tabs are on
mainand shipped in v0.9.0 — thank you for carrying this through every round of spec changes. They went throughfeat/editor-tabsfirst so I could work through my review on a branch beforemainauto-deployed it.The review turned up real problems, fixed in dbb5c9a — the commit message carries the full list. The class that mattered was unsaved work:
update_tabsonly re-ran when the tabs array changed identity, and re-marking a dirty tab is an immer no-op that notifies nobody — so the content Rust held for the save sheet froze at the first keystroke, and a quit-time Save wrote that near-empty snapshot over the document.first_unsaved_contentreviewed one tab and the flow then destroyed the window; the other never-saved tabs died unprompted.resolveUnsavedandhandle_closestill reasoned "autosave already wrote it". AndreconcileEpochwas read by nothing — same shape as the dead reconcile pass in the first branch.Worth naming the pattern: every one of these lived in a seam — immer's same-value short-circuit, React's commit timing (
markReadyraced the remount),destroy()bypassingCloseRequested. The harnesses prove the parts in isolation; none of them touched tab state, which is what your cold-start Finder regression was telling us from the other direction.scripts/verify-tab-storestarts covering that layer.The architecture held up: one runtime per tab is exactly why every fix stayed local. Background sync stays open as discussed.