Commits
Restructure the Automerge editor's single livePreview.ts into per-node
CodeMirror extensions composed by livePreview(), mirroring the reams
reference. Extends coverage from 4 node types to the core CommonMark +
GFM set (no widgets):
- headings: ported as-is
- bold / italic / strikethrough: content classes + proximity-revealed
markers, registered atomic so cursor motion skips hidden markers
- inline code + fenced code blocks: content/line styling + fence hiding
- links: fix the broken handler (hide all brackets/paren/URL, style the
label) + cmd/ctrl-click to open externally via a new shell.openExternal
IPC bridge
- blockquotes, horizontal rules, lists: line decorations + prefix hiding
Splits the single cm-formatting-hidden class into idiom-correct hide
classes (per the CM6 cursor-skip contract) and activates the previously
dead CSS. Adds headless builder tests (pure over EditorState) covering
each node type.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drag a file in the sidebar onto a directory (or the vault root) to move it.
- FileSystemProvider gains moveFile(entryId, targetDirId) and a rootId.
Local implementation renames across directories (guards same-location
and name conflicts); the synced provider syncs afterward like rename.
- VaultTree: files are draggable (independent off-screen drag ghost);
every row carries data-drop-dir for its enclosing folder, and the tree
container resolves the drop target via closest() — so it picks the
deepest folder under the cursor, falls through to the root over empty
space / root-level files, and clears reliably. The target directory's
whole subtree (or the whole tree, for the root) is highlighted.
- AppSidebar: handleMove calls provider.moveFile then refreshes the tree.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Split Right / Split Down move from the bottom bar to the top bar as
icon buttons (SplitRightIcon / SplitDownIcon) with hover tooltips, left
of the workspace selector, shown only in tiling mode. Wired through the
tiling handle (App.handleSplitRight/Down).
- Title bar: compact fixed-height buttons (size-5) with header padding so
hover highlights no longer reach the screen edge; tooltip + pointer
cursor on the sidebar toggle, split buttons, and workspace menu.
- Sidebar: fix asymmetric collapse animation — the open state now also
transitions width, so re-opening animates instead of snapping.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drag tab chips to reorder, move between tiles, or split.
- tabsModel.moveTab: reorder within / move across tiles (pure + tested);
creates the destination tile if absent and reports sourceEmptied.
- tabDnd: shared drag payload + computeDropZone (5 zones; pure + tested).
- model.splitLeaf gains `before` so a drop on a left/top edge puts the
new tile first.
- tiling.tsx: TilingTile shows a translucent zone overlay (raised via a
capture overlay so the editor can't swallow drag events); dropping on
an edge splits the tile and moves the tab; center moves it in.
- TileTabStrip: draggable chips with an insertion marker, cleared on
dragleave/dragend; an independent off-screen clone is used as the drag
image so the clipped live chip can't corrupt it. Thin, non-overlay tab
scrollbar (TileTabStrip.css).
Fixes found while testing:
- White-screen crash: read currentTarget synchronously, not inside a
setState updater (React nulls it after the handler).
- Tiles overlapped: split panes now flex-grow to share the space the
fixed splitter leaves, so panes + divider sum to 100% with no
hardcoded splitter width.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each vault's tile layout and tabs now survive an app restart, along with
the selected vault and (pre-existing) workspace kind.
- tilingPersistence: serializeLayout / buildInitialTileTabs / maxIdSuffix
(pure + tested) and load/save to localStorage under
textile.tiling.<vaultId>. Persists the split tree, active tile, and
each tile's documents + active index; not handles or load state.
- useTilingDocumentSlots: accept initialTiles, seed the tab map with
loading tabs, and re-open the documents in a mount-only effect
(cancelled on unmount). Exposes tileTabs for serialization.
- TilingWorkspace: lazy-init tree/activeTile/id-counter from the
persisted layout (keyed by instanceKey = vault provider id) and write
on change (skipping no-op rewrites).
- App: persist + restore selectedProviderId (textile.selectedVault); the
registry loads vaults synchronously so the restored vault survives the
auto-select effects.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tab management via right-click, a shared context-menu provider, and a
sidebar action to open a file in a new tile.
- tabsModel: closeOthers / closeToRight / closeToLeft (anchor survives)
and closeAllOfFile (across tiles, reports emptiedTileIds), all pure +
tested. Shared slotShowsRequest helper.
- TileTabStrip: right-click -> native context menu of close actions
(unified onTabAction dispatcher). useTilingDocumentSlots gains the
matching close methods; tiling.tsx collapses tiles emptied by
close-all-of-file.
- platform/contextMenu: ContextMenuProvider + useContextMenu inject the
context-menu capability (electron impl is the one place touching
window.textile). Provider mounted at the App composition root; TileTabStrip
and the sidebar consume via the hook instead of window.
- Sidebar file menu: "Open in new tile to the right/below" splits the
active tile then opens the file (App.handleOpenInSplit).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire tab/tile actions into the command palette and hotkey system.
- TilingHandle + TilingActionSet gain new-tab (Mod+T), close-tab,
next-tab (Mod+Alt+Right), prev-tab (Mod+Alt+Left). Close-tab has no
default binding since Mod+W is owned by the native window menu.
- tabsModel.cycleActiveTab: pure wrap-around tab cycling (+ tests).
useTilingDocumentSlots exposes cycleTab.
- tiling.tsx: the imperative handle now delegates to a per-render
handleImplRef, so every tiling/tab command runs against the latest
tree / activeTileId / tab state (previously only splits did).
- formatHotkeyForDisplay: render special keys as glyphs (arrows, Enter,
Tab, etc.) so the palette shows arrow icons, not "ARROWLEFT".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Opening a document no longer replaces the active tab. New pure helper
tabsModel.planOpen decides: focus an existing tab showing the same
document (no reopen), else reuse the active empty start tab, else append
a new tab. makeTabId is only called when a tab is actually created.
useTilingDocumentSlots routes opens through planOpen and skips
startDocumentOpen when the document was already open (focus only).
Also make the active-tab highlight switch instantly: tab chips opt out
of the global 320ms color transition (transition-none) so selecting a
tab updates the highlight immediately.
Fix tab switches sometimes showing the previous file: key DocumentPane
by document id in DocumentSlotView so switching to a different document
remounts a fresh editor. The CodeMirror view binds content on mount
only, and the in-place content swap was skippable by the isWriting
echo-suppression lock, so a reused editor could keep showing the old
file even with the new tab marked active.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tab strip UI and per-vault layout persistence on top of the phase-1 tab model.
- TileTabStrip: per-tile tab chips (select/close, middle-click, +). Empty
tabs are not chips — they render TileStartView, a quick-actions start
page whose only action is "New File" (wired to App.handleNewFile:
createFile + open). + reuses an existing empty tab (findEmptyTab).
- tiling.tsx: render the strip + active tab; close-the-last-tab collapses
the split via model.removeLeaf / planTileRemoval (pure, tested).
- Per-vault state: WorkspaceHost mounts one instance per (kind x vault)
and shows only the active one, preserving each vault's tile layout,
tabs, and open documents across vault/mode switches. Inactive instances
are gated by `active` (ignore opens, suppress bottom bar, no split
handle) via WorkspaceProps.active + barSlot WorkspaceActiveProvider.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Foundation for multiple tabs per tile in the tiling workspace. No UI
change yet; behavior is preserved.
- model.ts: add removeLeaf (collapse split into sibling; root removal is
a no-op) and listTileIds, for last-tab close.
- tabsModel.ts: pure tab-state model (TileTabsMap), React-free and
side-effect-free. closeTab reports removedTabIds + tileEmptied so the
hook owns handle release and the caller owns collapse-vs-reseed.
- useTilingDocumentSlots: re-back onto TileTabsMap, key DocumentHandles
by tab id, preserve getSlot/ensureTile, add getTabs/getActiveTabId/
setActiveTab/newTab/closeTab for the upcoming tab UI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- .githooks/pre-commit runs `format:check`, `lint`, and `test`; commits
are blocked unless all pass. Enabled via core.hooksPath, wired by a
`prepare` npm script so `npm install` activates it on fresh clones.
- Ran `prettier --write .` across the repo so it's now Prettier-clean and
the hook's format:check passes going forward.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Textile is now vault-only. Remove the standalone document/filesystem
providers and everything they kept alive:
Providers:
- documents: automerge, habitat, subtext
- filesystem: automerge, habitat, subtext, remoteSync (stub)
- App.tsx: drop staticProviders + isSubtextEnabled; the sidebar lists
only vault providers.
Yjs stack (only the removed providers produced 'yjs' bindings):
- editors/yjs/* + the registry 'yjs' entry; EditorKind is now 'automerge'.
- habitat/docSession.ts (+ test) and habitat/connectionProvider.ts;
libp2pNode now sources HabitatLibp2pNode from automergeConnectionProvider.
- habitatDoc.ts trimmed to the CRDT-agnostic URI helpers (parseDocUri,
editRkeyFor) the vault stack still uses.
- automergeDocSession: dropped the now-subscriber-less title-emit machinery.
Subtext subsystem (fully):
- src/subtext/*, the main-process SubtextTCPClient + IPC handlers, the
preload/vite-env bridge, and the Settings → Subtext pane + icon.
Dependencies orphaned by the above:
- yjs, y-protocols, @tiptap/* (the old TipTap/Yjs editor), protobufjs(+cli)
and the dead proto:gen script, it-byte-stream. Kept lib0 (used by the
automerge connection provider).
Net ~-9.4k lines of source. Lint/tsc/tests clean (56 passing).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 4 (editor binding):
- Port pushwork's text-diff.ts verbatim (diffChars + Automerge.splice) so
vault file edits are character-level and merge cleanly — and the change
ops match pushwork for parity. Adds the `diff` dependency.
- Automerge editor reads/writes content via readDocContent/updateTextContent.
- Text-only guard (isTextFile): binary files don't open in the markdown
editor. Synced vault tabs title by filename.
Remote-vault sync-down:
- New `network.habitat.vault.root` collection for vault roots, so discovery
is a single unfiltered query. Unsynced remote vaults show in the sidebar
selector with a cloud-download icon; a "Sync Remote Vault" modal picks a
directory and runs the initial full clone (awaited, with progress).
Unified vault model:
- Collapse OpenVault + RemoteVaultRoot into a discriminated `Vault` union
(local | synced | remote-unsynced) behind a `VaultRegistry` singleton that
owns local persistence + remote discovery.
- Reduce VaultConfig to the engine's input view ({ type, rootUri }); derive
it via toVaultConfig() at the engine boundary.
Hashing:
- Replace Node `crypto` in content.ts with @noble/hashes (sync, browser+node)
so contentHash works in the Electron renderer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make synced vaults functional end-to-end by driving the push-only sync
engine from the providers and the composition root:
- SyncedVaultFilesystemProvider: disk tree + CRUD (extends
LocalFilesystemProvider), reconciling each mutation through the engine
(pushFile on create/copy, sync on rename/delete).
- SyncedVaultDocumentsProvider: opens files via the live Automerge session
(multiplayer over libp2p), resolving disk path -> Habitat URI from the
snapshot (pushing first if new), pulling latest on open, and mirroring
session edits back to disk. The session factory is injected (DI) so the
module stays free of the libp2p runtime and is testable.
- engine.ts: add noteMirroredFile() so mirrored in-app edits aren't
re-pushed as phantom external changes.
- registry.ts: persist OpenVault.rootUri.
- App.tsx: provision a synced vault's root directory record on first open,
construct the per-vault engine stack, run the first-mount sync(), and
wire the two synced providers (acquireSession = real acquireDocSession).
Testing uses fakes over mocks: extract InMemoryHabitatRepo +
NodeFilesystemApi to src/vault/testing/fakeHabitat.ts and add a
FakeDocSession sharing one in-memory doc store with the repo. The provider
test runs the real engine/detector/snapshot manager against the fakes and
asserts on real state (on-disk files, snapshot, doc store) — no vi.mock.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
loadAutomergeDoc captured entry.doc before awaiting the PDS fetch. A
concurrent applyChange during that await marked the captured reference
stale (Automerge progressDocument sets state.heads), so the resumed
loadIncremental threw "Attempting to change an out of date document".
Fix: load into a fresh Automerge.init({ actor }) and reconcile any
in-flight local changes after the await via Automerge.merge(result.doc,
entry.doc). Applied to initSession, reinitSession, and resyncEntry.
mergeAllEdits now returns the final doc reference (MergeEditsResult) so
loadAutomergeDoc threads through a valid (non-stale) doc.
Adds a TODO for a regression test, and TESTING.md documenting the
Vitest/Automerge WASM config.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the vault core sync algorithms on top of the phase 1 foundation:
- change-detection.ts: split detection into local-only (steady state,
no network) and remote-tree (first-open clone only). Removes the
per-file remote head-comparison.
- engine.ts: VaultSyncEngine. sync() does a one-time remote clone on
first mount, then is push-only. Adds per-file openFile() (pull on
open) and pushFile() (push on save), so steady-state cost is
proportional to what the user touched rather than the whole tree.
- move-detection.ts: rename detection by content similarity.
- habitat-repo.ts: implement create() via createAutomergeDoc.
- types.ts/snapshot.ts: supporting types and snapshot wiring.
- App.tsx: TODO to run a top-level sync on first vault mount.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract docSession module-level state (ydocRegistry, titleListeners,
visibilityListenerInstalled) into a DocSessionRegistry class. Tests now
instantiate a fresh registry per test instead of calling a test-only
reset function exported from production code. The module-level
acquireDocSession/subscribeEntryTitleChanges wrappers delegate to a
default singleton so no callers change.
Also fix Vitest picking @automerge/automerge's browser entry (base64
WASM, OOMs) by externalising the package and passing --conditions=node
to forked workers so Node picks the disk-loading entry instead.
Remove importOriginal() from habitatDocumentsProvider.test.ts to avoid
pulling in libp2p's native addon during mock setup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lays the groundwork for Obsidian-style vaults backed by Habitat PDS +
libp2p gossipsub. Adds types, HabitatRepo/DocHandle adapter over
AutomergeDocSession, VaultSnapshotManager, vault registry, and utility
modules (content hashing, text diff, move detection similarity, directory
traversal). Also pulls forward collection-aware routing in automergeDoc.ts
and adds changeAt to AutomergeDocSession.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Restructure the Automerge editor's single livePreview.ts into per-node
CodeMirror extensions composed by livePreview(), mirroring the reams
reference. Extends coverage from 4 node types to the core CommonMark +
GFM set (no widgets):
- headings: ported as-is
- bold / italic / strikethrough: content classes + proximity-revealed
markers, registered atomic so cursor motion skips hidden markers
- inline code + fenced code blocks: content/line styling + fence hiding
- links: fix the broken handler (hide all brackets/paren/URL, style the
label) + cmd/ctrl-click to open externally via a new shell.openExternal
IPC bridge
- blockquotes, horizontal rules, lists: line decorations + prefix hiding
Splits the single cm-formatting-hidden class into idiom-correct hide
classes (per the CM6 cursor-skip contract) and activates the previously
dead CSS. Adds headless builder tests (pure over EditorState) covering
each node type.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drag a file in the sidebar onto a directory (or the vault root) to move it.
- FileSystemProvider gains moveFile(entryId, targetDirId) and a rootId.
Local implementation renames across directories (guards same-location
and name conflicts); the synced provider syncs afterward like rename.
- VaultTree: files are draggable (independent off-screen drag ghost);
every row carries data-drop-dir for its enclosing folder, and the tree
container resolves the drop target via closest() — so it picks the
deepest folder under the cursor, falls through to the root over empty
space / root-level files, and clears reliably. The target directory's
whole subtree (or the whole tree, for the root) is highlighted.
- AppSidebar: handleMove calls provider.moveFile then refreshes the tree.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Split Right / Split Down move from the bottom bar to the top bar as
icon buttons (SplitRightIcon / SplitDownIcon) with hover tooltips, left
of the workspace selector, shown only in tiling mode. Wired through the
tiling handle (App.handleSplitRight/Down).
- Title bar: compact fixed-height buttons (size-5) with header padding so
hover highlights no longer reach the screen edge; tooltip + pointer
cursor on the sidebar toggle, split buttons, and workspace menu.
- Sidebar: fix asymmetric collapse animation — the open state now also
transitions width, so re-opening animates instead of snapping.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drag tab chips to reorder, move between tiles, or split.
- tabsModel.moveTab: reorder within / move across tiles (pure + tested);
creates the destination tile if absent and reports sourceEmptied.
- tabDnd: shared drag payload + computeDropZone (5 zones; pure + tested).
- model.splitLeaf gains `before` so a drop on a left/top edge puts the
new tile first.
- tiling.tsx: TilingTile shows a translucent zone overlay (raised via a
capture overlay so the editor can't swallow drag events); dropping on
an edge splits the tile and moves the tab; center moves it in.
- TileTabStrip: draggable chips with an insertion marker, cleared on
dragleave/dragend; an independent off-screen clone is used as the drag
image so the clipped live chip can't corrupt it. Thin, non-overlay tab
scrollbar (TileTabStrip.css).
Fixes found while testing:
- White-screen crash: read currentTarget synchronously, not inside a
setState updater (React nulls it after the handler).
- Tiles overlapped: split panes now flex-grow to share the space the
fixed splitter leaves, so panes + divider sum to 100% with no
hardcoded splitter width.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each vault's tile layout and tabs now survive an app restart, along with
the selected vault and (pre-existing) workspace kind.
- tilingPersistence: serializeLayout / buildInitialTileTabs / maxIdSuffix
(pure + tested) and load/save to localStorage under
textile.tiling.<vaultId>. Persists the split tree, active tile, and
each tile's documents + active index; not handles or load state.
- useTilingDocumentSlots: accept initialTiles, seed the tab map with
loading tabs, and re-open the documents in a mount-only effect
(cancelled on unmount). Exposes tileTabs for serialization.
- TilingWorkspace: lazy-init tree/activeTile/id-counter from the
persisted layout (keyed by instanceKey = vault provider id) and write
on change (skipping no-op rewrites).
- App: persist + restore selectedProviderId (textile.selectedVault); the
registry loads vaults synchronously so the restored vault survives the
auto-select effects.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tab management via right-click, a shared context-menu provider, and a
sidebar action to open a file in a new tile.
- tabsModel: closeOthers / closeToRight / closeToLeft (anchor survives)
and closeAllOfFile (across tiles, reports emptiedTileIds), all pure +
tested. Shared slotShowsRequest helper.
- TileTabStrip: right-click -> native context menu of close actions
(unified onTabAction dispatcher). useTilingDocumentSlots gains the
matching close methods; tiling.tsx collapses tiles emptied by
close-all-of-file.
- platform/contextMenu: ContextMenuProvider + useContextMenu inject the
context-menu capability (electron impl is the one place touching
window.textile). Provider mounted at the App composition root; TileTabStrip
and the sidebar consume via the hook instead of window.
- Sidebar file menu: "Open in new tile to the right/below" splits the
active tile then opens the file (App.handleOpenInSplit).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire tab/tile actions into the command palette and hotkey system.
- TilingHandle + TilingActionSet gain new-tab (Mod+T), close-tab,
next-tab (Mod+Alt+Right), prev-tab (Mod+Alt+Left). Close-tab has no
default binding since Mod+W is owned by the native window menu.
- tabsModel.cycleActiveTab: pure wrap-around tab cycling (+ tests).
useTilingDocumentSlots exposes cycleTab.
- tiling.tsx: the imperative handle now delegates to a per-render
handleImplRef, so every tiling/tab command runs against the latest
tree / activeTileId / tab state (previously only splits did).
- formatHotkeyForDisplay: render special keys as glyphs (arrows, Enter,
Tab, etc.) so the palette shows arrow icons, not "ARROWLEFT".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Opening a document no longer replaces the active tab. New pure helper
tabsModel.planOpen decides: focus an existing tab showing the same
document (no reopen), else reuse the active empty start tab, else append
a new tab. makeTabId is only called when a tab is actually created.
useTilingDocumentSlots routes opens through planOpen and skips
startDocumentOpen when the document was already open (focus only).
Also make the active-tab highlight switch instantly: tab chips opt out
of the global 320ms color transition (transition-none) so selecting a
tab updates the highlight immediately.
Fix tab switches sometimes showing the previous file: key DocumentPane
by document id in DocumentSlotView so switching to a different document
remounts a fresh editor. The CodeMirror view binds content on mount
only, and the in-place content swap was skippable by the isWriting
echo-suppression lock, so a reused editor could keep showing the old
file even with the new tab marked active.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tab strip UI and per-vault layout persistence on top of the phase-1 tab model.
- TileTabStrip: per-tile tab chips (select/close, middle-click, +). Empty
tabs are not chips — they render TileStartView, a quick-actions start
page whose only action is "New File" (wired to App.handleNewFile:
createFile + open). + reuses an existing empty tab (findEmptyTab).
- tiling.tsx: render the strip + active tab; close-the-last-tab collapses
the split via model.removeLeaf / planTileRemoval (pure, tested).
- Per-vault state: WorkspaceHost mounts one instance per (kind x vault)
and shows only the active one, preserving each vault's tile layout,
tabs, and open documents across vault/mode switches. Inactive instances
are gated by `active` (ignore opens, suppress bottom bar, no split
handle) via WorkspaceProps.active + barSlot WorkspaceActiveProvider.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Foundation for multiple tabs per tile in the tiling workspace. No UI
change yet; behavior is preserved.
- model.ts: add removeLeaf (collapse split into sibling; root removal is
a no-op) and listTileIds, for last-tab close.
- tabsModel.ts: pure tab-state model (TileTabsMap), React-free and
side-effect-free. closeTab reports removedTabIds + tileEmptied so the
hook owns handle release and the caller owns collapse-vs-reseed.
- useTilingDocumentSlots: re-back onto TileTabsMap, key DocumentHandles
by tab id, preserve getSlot/ensureTile, add getTabs/getActiveTabId/
setActiveTab/newTab/closeTab for the upcoming tab UI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- .githooks/pre-commit runs `format:check`, `lint`, and `test`; commits
are blocked unless all pass. Enabled via core.hooksPath, wired by a
`prepare` npm script so `npm install` activates it on fresh clones.
- Ran `prettier --write .` across the repo so it's now Prettier-clean and
the hook's format:check passes going forward.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Textile is now vault-only. Remove the standalone document/filesystem
providers and everything they kept alive:
Providers:
- documents: automerge, habitat, subtext
- filesystem: automerge, habitat, subtext, remoteSync (stub)
- App.tsx: drop staticProviders + isSubtextEnabled; the sidebar lists
only vault providers.
Yjs stack (only the removed providers produced 'yjs' bindings):
- editors/yjs/* + the registry 'yjs' entry; EditorKind is now 'automerge'.
- habitat/docSession.ts (+ test) and habitat/connectionProvider.ts;
libp2pNode now sources HabitatLibp2pNode from automergeConnectionProvider.
- habitatDoc.ts trimmed to the CRDT-agnostic URI helpers (parseDocUri,
editRkeyFor) the vault stack still uses.
- automergeDocSession: dropped the now-subscriber-less title-emit machinery.
Subtext subsystem (fully):
- src/subtext/*, the main-process SubtextTCPClient + IPC handlers, the
preload/vite-env bridge, and the Settings → Subtext pane + icon.
Dependencies orphaned by the above:
- yjs, y-protocols, @tiptap/* (the old TipTap/Yjs editor), protobufjs(+cli)
and the dead proto:gen script, it-byte-stream. Kept lib0 (used by the
automerge connection provider).
Net ~-9.4k lines of source. Lint/tsc/tests clean (56 passing).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 4 (editor binding):
- Port pushwork's text-diff.ts verbatim (diffChars + Automerge.splice) so
vault file edits are character-level and merge cleanly — and the change
ops match pushwork for parity. Adds the `diff` dependency.
- Automerge editor reads/writes content via readDocContent/updateTextContent.
- Text-only guard (isTextFile): binary files don't open in the markdown
editor. Synced vault tabs title by filename.
Remote-vault sync-down:
- New `network.habitat.vault.root` collection for vault roots, so discovery
is a single unfiltered query. Unsynced remote vaults show in the sidebar
selector with a cloud-download icon; a "Sync Remote Vault" modal picks a
directory and runs the initial full clone (awaited, with progress).
Unified vault model:
- Collapse OpenVault + RemoteVaultRoot into a discriminated `Vault` union
(local | synced | remote-unsynced) behind a `VaultRegistry` singleton that
owns local persistence + remote discovery.
- Reduce VaultConfig to the engine's input view ({ type, rootUri }); derive
it via toVaultConfig() at the engine boundary.
Hashing:
- Replace Node `crypto` in content.ts with @noble/hashes (sync, browser+node)
so contentHash works in the Electron renderer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make synced vaults functional end-to-end by driving the push-only sync
engine from the providers and the composition root:
- SyncedVaultFilesystemProvider: disk tree + CRUD (extends
LocalFilesystemProvider), reconciling each mutation through the engine
(pushFile on create/copy, sync on rename/delete).
- SyncedVaultDocumentsProvider: opens files via the live Automerge session
(multiplayer over libp2p), resolving disk path -> Habitat URI from the
snapshot (pushing first if new), pulling latest on open, and mirroring
session edits back to disk. The session factory is injected (DI) so the
module stays free of the libp2p runtime and is testable.
- engine.ts: add noteMirroredFile() so mirrored in-app edits aren't
re-pushed as phantom external changes.
- registry.ts: persist OpenVault.rootUri.
- App.tsx: provision a synced vault's root directory record on first open,
construct the per-vault engine stack, run the first-mount sync(), and
wire the two synced providers (acquireSession = real acquireDocSession).
Testing uses fakes over mocks: extract InMemoryHabitatRepo +
NodeFilesystemApi to src/vault/testing/fakeHabitat.ts and add a
FakeDocSession sharing one in-memory doc store with the repo. The provider
test runs the real engine/detector/snapshot manager against the fakes and
asserts on real state (on-disk files, snapshot, doc store) — no vi.mock.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
loadAutomergeDoc captured entry.doc before awaiting the PDS fetch. A
concurrent applyChange during that await marked the captured reference
stale (Automerge progressDocument sets state.heads), so the resumed
loadIncremental threw "Attempting to change an out of date document".
Fix: load into a fresh Automerge.init({ actor }) and reconcile any
in-flight local changes after the await via Automerge.merge(result.doc,
entry.doc). Applied to initSession, reinitSession, and resyncEntry.
mergeAllEdits now returns the final doc reference (MergeEditsResult) so
loadAutomergeDoc threads through a valid (non-stale) doc.
Adds a TODO for a regression test, and TESTING.md documenting the
Vitest/Automerge WASM config.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the vault core sync algorithms on top of the phase 1 foundation:
- change-detection.ts: split detection into local-only (steady state,
no network) and remote-tree (first-open clone only). Removes the
per-file remote head-comparison.
- engine.ts: VaultSyncEngine. sync() does a one-time remote clone on
first mount, then is push-only. Adds per-file openFile() (pull on
open) and pushFile() (push on save), so steady-state cost is
proportional to what the user touched rather than the whole tree.
- move-detection.ts: rename detection by content similarity.
- habitat-repo.ts: implement create() via createAutomergeDoc.
- types.ts/snapshot.ts: supporting types and snapshot wiring.
- App.tsx: TODO to run a top-level sync on first vault mount.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract docSession module-level state (ydocRegistry, titleListeners,
visibilityListenerInstalled) into a DocSessionRegistry class. Tests now
instantiate a fresh registry per test instead of calling a test-only
reset function exported from production code. The module-level
acquireDocSession/subscribeEntryTitleChanges wrappers delegate to a
default singleton so no callers change.
Also fix Vitest picking @automerge/automerge's browser entry (base64
WASM, OOMs) by externalising the package and passing --conditions=node
to forked workers so Node picks the disk-loading entry instead.
Remove importOriginal() from habitatDocumentsProvider.test.ts to avoid
pulling in libp2p's native addon during mock setup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lays the groundwork for Obsidian-style vaults backed by Habitat PDS +
libp2p gossipsub. Adds types, HabitatRepo/DocHandle adapter over
AutomergeDocSession, VaultSnapshotManager, vault registry, and utility
modules (content hashing, text diff, move detection similarity, directory
traversal). Also pulls forward collection-aware routing in automergeDoc.ts
and adds changeAt to AutomergeDocSession.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>