Skip to content

mesh: restore folder preservation in catalog gossip + Pull#209

Merged
mrjeeves merged 1 commit into
mainfrom
claude/charming-turing-PMIK8
May 28, 2026
Merged

mesh: restore folder preservation in catalog gossip + Pull#209
mrjeeves merged 1 commit into
mainfrom
claude/charming-turing-PMIK8

Conversation

@mrjeeves
Copy link
Copy Markdown
Owner

Yes, folders used to transfer — two regressions

The user reported "didn't folders used to transfer over [in the sidebar]?". Confirmed: two related drops from the daemon migration.

1. Sidebar peer view: flat instead of folder-tree

The legacy mesh-client.svelte.ts::refreshLocalCatalog shipped path on every CatalogEntry. Receivers' src/ui/Sidebar.svelte::buildRemoteTree (still in place, unchanged) reads e.path and groups peer conversations into nested RemoteNodes — that's how the user's Work/Projects/Q4 planning on the source rendered as a folder tree on every other device.

mesh-gossip.ts::snapshotLocalCatalog shipped in PR #203 dropped the field on the map:

return conversations.map((c) => ({
  guid: c.id,
  title: c.title,
  mode: c.mode,
  updated_at: c.updated_at,
})) as CatalogEntry[];                  // ← no path

Every receiver saw every remote conversation at root. A peer's Work/Projects/Q4 planning showed up as a flat Q4 planning row alongside top-level conversations — visually indistinguishable from a root-level chat.

Fix: thread c.path through the snapshot's map. Empty (root) omitted from the wire payload via spread-only-when-truthy, matching the legacy || undefined shape.

2. Pull lands conversations at root regardless of source

pullConversation was calling saveConversation(conversation) with no targetFolder arg. The Conversation JSON the source returns doesn't carry path (filesystem fact, not conversation content), so the receiver had no way to know where on the source the conversation lived → every Pull flattened to root.

Push (moveConversation) was already correct: it looks up c.path from the local listing and ships it as move_take.source_folder, which handleMoveTake forwards into saveConversation(conversation, source_folder). The Pull path was the missing half.

Fix: pullConversation looks the source folder up from the cached catalog of the source peer (now that the catalog carries path again — both fixes need each other). Passes it as the second arg to saveConversation, which calls pathFormkdir({ recursive: true }) to create intermediate folders. Falls back to root when the catalog hasn't caught up.

MoveClient gains a read-only peers accessor (with the minimum shape needed for the lookup: device_pubkey, peer_id, catalog[].guid, catalog[].path).

Validation

  • pnpm run check: 164 files, 0 errors, 0 warnings.
  • pnpm run build: clean.
  • Sidebar tree on peer view: A has Work/Q4 planning; B's sidebar Network section nests Work as a folder containing Q4 planning (was a flat row before).
  • Push folder preservation: push Foo/Bar/baz from A to B → arrives at Foo/Bar/baz on B (already worked, sanity check).
  • Pull folder preservation: pull Foo/Bar/baz from A → arrives at Foo/Bar/baz locally (was landing at root before).
  • Pull when catalog hasn't caught up (e.g. mid-handshake pull): conversation lands at root rather than erroring out. Cosmetic only; the next catalog announce + manual move will resolve.

https://claude.ai/code/session_01RLu1LdTgtxEDdzhybzqFrk


Generated by Claude Code

## Two regressions from the daemon migration

### Sidebar peer view: flat instead of folder-tree

The legacy `mesh-client.svelte.ts::refreshLocalCatalog` included
`path` on every `CatalogEntry` it shipped, so receivers' Sidebar
`buildRemoteTree` could reproduce the host's folder structure
(see `src/ui/Sidebar.svelte:337`, which still reads `e.path` and
builds nested `RemoteNode`s).

`mesh-gossip.ts::snapshotLocalCatalog` shipped in PR #203 dropped
the `path` field on the map:

    return conversations.map((c) => ({
      guid: c.id,
      title: c.title,
      mode: c.mode,
      updated_at: c.updated_at,
    })) as CatalogEntry[];                  // ← no path

Every receiver saw every remote conversation at root. A peer's
`Work/Projects/Q4 planning` showed up as a flat
`Q4 planning` row alongside top-level conversations — visually
indistinguishable from a root-level chat. Reported by the user
as "didn't folders used to transfer over [in the sidebar]?".

Fix: thread `c.path` through the snapshot's map. Empty (root)
omitted from the wire payload via the spread-only-when-truthy
trick, matching the legacy `|| undefined` shape.

### Pull: pulled conversations land at root

`pullConversation` was calling `saveConversation(conversation)`
with no `targetFolder` argument. The conversation JSON itself
doesn't carry path (that's a filesystem fact, not conversation
content), so the receiver had no way to know where on the source
the conversation lived → every Pull flattened to root regardless
of where the conversation was on the source.

Push (`moveConversation`) was already correct: it looks up
`c.path` from the local listing and ships it as
`move_take.source_folder`, which the receiver's `handleMoveTake`
forwards into `saveConversation(conversation, source_folder)`.
The Pull path was the missing half.

Fix: `pullConversation` looks the source folder up from the
cached catalog of the source peer (now that catalog gossip
carries path again — without the gossip fix above, the catalog
entry's `path` would still be undefined). Passes it as the
second arg to `saveConversation`, which calls `pathFor` →
`mkdir({ recursive: true })` to create intermediate folders.

MoveClient gains a read-only `peers` accessor (with the minimum
shape needed for the lookup: `device_pubkey`, `peer_id`,
`catalog[].guid`, `catalog[].path`). Falls back to root when
the catalog hasn't caught up — same disposition as a legitimate
root-hosted conversation.

## Validation

- `pnpm run check`: 164 files, 0 errors, 0 warnings.
- `pnpm run build`: clean.
- Two devices: push `Foo/Bar/baz` from A to B → arrives at
  `Foo/Bar/baz` on B (already worked before this PR, sanity check).
- Two devices: pull `Foo/Bar/baz` from A → arrives at
  `Foo/Bar/baz` locally (was landing at root before).
- Two devices: A has `Work/Q4 planning` chat; B's sidebar
  Network section nests `Work` as a folder containing
  `Q4 planning` (was a flat row before).

https://claude.ai/code/session_01RLu1LdTgtxEDdzhybzqFrk
@mrjeeves mrjeeves merged commit 6ebd643 into main May 28, 2026
4 checks passed
@mrjeeves mrjeeves deleted the claude/charming-turing-PMIK8 branch May 28, 2026 08:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants