Desktop · Pear / Bare · v0.14.0

Desktop App

Folder intent

Project Structure

listam-desktop (v0.14.0) is a Pear desktop app. The renderer is vanilla DOM + CSS; the real backend is the shared @listam/backend engine — the same one the mobile worklet and the headless service run — booted in a Pear worker because bare-* modules cannot load in the renderer.

Path Role Why it exists
index.html Pear entry point Minimal shell with an importmap for the shared @listam/* packages; loads src/main.mjs.
src/main.mjs Renderer composition root Detects the Pear runtime, boots the worker backend (or the mock), wires store, i18n, theme, and mounts the UI.
src/backend-boot.mjs / src/backend-worker.mjs Worker bridge Renderer and worker halves of the IPC pipe: newline-delimited JSON frames carry commands, events, and bridge control.
src/store.mjs / src/ui.mjs State + DOM renderer Redux-style store reduced with the shared @listam/domain id-keyed reducer; ui.mjs renders sidebar, panes, dialogs, and animations.
src/leaf-bridge-manager.mjs / src/leaf-bridge-config.mjs Leaf-peer hub Worker-side TCP listener lifecycle for hardware leaf peers (ESP32 mirrors), plus port validation.
src/owner-control.mjs Headless device client Pairs with headless servers over HyperDHT and sends signed, capability-scoped commands.
src/secret-store.mjs, src/prefs.mjs, src/i18n.mjs Persistence helpers File-backed secret store for backend keys; localStorage for theme/view/hints preferences and locale choice.
src/mock-backend.mjs Design preview Fixture backend for ?mock=1 browser preview — UI work without the Pear runtime or real peers.
design-guide/ Design system docs Kinetic Minimalist v2 proposal, tokens, and component specs that app.css implements.
installer/ macOS distribution Appling shell, DMG build script, seeding LaunchAgent — excluded from Pear staging.
test/ Test suite node:test units, a two-process sync acceptance test on a private DHT, and the pear-smoke E2E harness.
Behavior

Runtime Flows

1

Renderer boot

Pear loads index.htmlsrc/main.mjs. If globalThis.Pear exists, the renderer spawns the backend worker with Pear.worker.run('./src/backend-worker.mjs'); otherwise it falls back to the mock backend for browser previews.

src/main.mjs
2

Worker boot

The worker opens the file secret store, prepares the boot key payload, creates the Pear platform adapter (storage namespace desktop), starts @listam/backend, optionally starts the leaf bridge, then sends a ready frame. The renderer aborts with an error after 45 s of silence.

src/backend-worker.mjs
3

List mutation

UI actions send numeric @listam/protocol commands (RPC_ADD, RPC_UPDATE, RPC_DELETE) over the pipe. Backend events come back pre-decoded by @listam/client, are reduced into the store by id, and renderAll() repaints the DOM.

src/store.mjs
4

Replication

The worker's Autobase replicates over Hyperswarm exactly as on mobile and headless. Peer-count events drive the status strip: hollow ring at 0 peers, breathing dot when ≥1 peer is connected.

src/ui.mjs

Invite join sequence

  1. Share opens the invite dialog: the worker mints a z32 BlindPairing invite (RPC_CREATE_INVITE) with copy-to-clipboard; the code rotates on the next Share.
  2. Join accepts a pasted invite; a confirmation dialog warns that the local list will be replaced before anything is torn down.
  3. On confirm, the renderer sends RPC_JOIN_KEY and shows a join overlay that tracks the backend's join phases.
  4. The backend pairs, receives base credentials, replicates, and waits until the base is writable.
  5. Lists merge, the roster updates, and a bad or dead invite surfaces a join-error notice with the app back in a usable ready state.
src/ui.mjs
Renderer layer

Renderer UI

Shell and sidebar IA

A frameless 1280×860 window with draggable titlebar. The left rail layers registry-driven named lists and groups on top of three built-in surfaces — Groceries, Board, and Todo. The active surface is keyed as a (listId, type) pair, so legacy single-list default data always shows. The app opens with no list selected, and every list belongs to a group: general is the mandated, synced, protected default home.

src/ui.mjs @listam/domain/list-registry.mjs

Groups and collapsible rail

Groups and lists are renamable and deletable; deleting a group re-homes its lists to general. Device-local preferences pick which built-ins show (hiddenBuiltins) and which surface opens (defaultSurfaceKey). Rail groups collapse (preferences.collapsedGroups): a collapsed group shows one green badge summing its surfaces' counts and auto-expands when a surface inside it is selected. The old "Live · N peers" status strip is gone — the peer count moved to the Peers & Devices nav badge.

src/store.mjs src/ui.mjs

Lists pane

Add-bar with chain-adding and draft preservation across background re-renders, duplicate rejection (case-insensitive, whitespace-normalized) with a shake animation, inline edit on double-click, list and grid views, category grouping with per-category colored Tabler icons, and a Clear Done sweep with staggered exits.

src/ui.mjs src/icons.mjs

Keyboard-first actions

N// summon the add-bar, G toggles grid, T cycles theme, H toggles the hint bar, ? opens the shortcuts dialog, arrows move row focus, Space/Enter toggle done, Delete removes. Shortcuts go inert while typing or while a dialog is open, and OS reduce-motion swaps animations for instant updates.

src/ui.mjs

Design system and i18n

app.css implements the Kinetic Minimalist v2 tokens (light/dark/system with live OS re-resolution). Locale comes from the shared @listam/i18n catalogs — English, Español, plus en-XA pseudo-accents and en-XL long-strings for layout QA. Dialogs (share, join, confirm, settings, member-remove, recovery) close on Esc/backdrop; notices stack to 4 and auto-dismiss.

app.css design-guide/

Board & editing

Beyond Groceries, a Board surface (status-chip + card view with a ticket-detail / rigor editor) and a text-only Todo surface. Items reorder via Alt+↑/↓ and drag (@listam/domain/ordering). A "Move to…" picker moves items between lists and types and promotes an item to a board (RPC_MOVE = 28, @listam/domain/list-move). Board-ticket markdown fields edit as live WYSIWYG via a contentEditable over @listam/domain/markdown; the stored value stays markdown.

src/ticket.mjs @listam/domain/list-move.mjs @listam/domain/markdown.mjs
Pinned surface

Overview / Day-plan

A pinned Overview surface pairs a Focus list with a Planner, letting you pull anything from across the app into today's plan without copying it.

Focus & flagging

Flag any grocery item (including from the grid and shopping-mode cards), board ticket, or list into today's focus. Marking a focus item done writes through to the source list, so the plan and the underlying lists never drift. Drag-reorder is discoverable within the Focus list.

src/store.mjs

Planner & sync

A 7-day strip with a future-day popover lets you schedule items ahead. The plan is shared via @listam/domain/plan over a __plan__ meta-item channel — no new RPC; it rides an RPC_UPDATE upsert like the other synced meta channels (registry, labels).

src/store.mjs · @listam/domain/plan.mjs
Pear worker layer

Worker Backend

Why a worker

Pear's DOM loader rejects bare-* modules, so Corestore, Autobase, Hyperswarm, and bare-tcp cannot run in the renderer (loading them there was the root cause of silently dead adds during the port). The whole bare module graph lives in one Pear worker; the renderer stays vanilla JS + Web APIs.

src/backend-worker.mjs

NDJSON pipe protocol

Renderer and worker exchange newline-delimited JSON frames over the worker pipe: req/res for commands, event/reply for backend pushes, bridge/bridge-status for leaf-bridge control, and ready/boot-error for lifecycle. Pending requests are matched by id.

src/backend-boot.mjs

Shared engine

The worker boots the same @listam/backend the mobile worklet and the headless service use, through a Pear platform adapter (bare-fs paths, storage namespace desktop, RPC handler, boot secrets). Events arrive already decoded by @listam/client, so the renderer never parses backend payloads itself.

src/backend-worker.mjs

Secrets round-trip

The backend persists base and encryption keys by sending RPC_PERSIST_SECRET back to the secret store, a file-tier @listam/secrets adapter. A corrupt secrets file degrades to a fresh base instead of crashing the app.

src/secret-store.mjs

Interesting implementation details

Local mutation attribution.

The backend echoes your own edits back as events. The renderer keeps a short-lived map of recently typed texts/ids so locally added items animate as "enter" while genuinely remote arrivals pulse as "remote" — peers' edits are visible, yours are instant.

Bridge generation tokens.

Toggling the leaf bridge off and on quickly could let a stale socket-close overwrite the new bridge's status. Each start increments a generation counter and stale callbacks are dropped.

Boot timeout instead of a hang.

If the worker never reports ready within 45 seconds, the renderer rejects the boot promise and shows an error state rather than an apparently frozen window.

Live system-theme resolution.

With theme "system", a prefers-color-scheme media listener re-resolves light/dark the moment the OS scheme flips — no restart, no stale theme.

Voice hosting in the worker.

The Pear/Bare worker can host the full voice pipeline itself (src/voice-host-worker.mjs): a leaf streams 16 kHz audio to the desktop's audio bridge on TCP :9994, the worker runs whisper.cpp STT via bare-subprocess plus intent parsing, and writes items into the desktop's own base — no separate headless leaf-hub or orphan base.

Worker stays alive on crashes.

A Bare.on('uncaughtException') handler keeps the worker running and appends the failure to {Pear storage}/worker-errors.log instead of aborting the whole app — a leaf-connect error no longer takes the backend down.

Connectivity surfaces

Peers & Devices

Peers pane

Live peer count and connection dot, the current invite code (or a "no active invite" empty state), and the member roster as monospace writer keys with Owner / You / Member badges.

src/ui.mjs

Member removal

Remove appears only when this device can administer the base and never for self or owner. A confirmation dialog gates it; on confirm the roster updates and the removed peer loses write access (the C1 re-key epoch on the backend side).

src/ui.mjs

Device & peer names

A Settings Device-name field advertises a self peer-label (@listam/domain/labels), so the roster shows a legible name instead of the raw writer key. Built-in surfaces are renamable too, carried over the same synced surface-labels meta channel.

@listam/domain/labels.mjs

Leaf bridge (hardware peers)

A worker-side TCP listener (default port 9993, via bare-tcp) that ESP32 leaf peers dial to mirror cores, gated by a 64-hex control key shown with a Copy button. Status reads off / waiting / connected(N); the toggle and port persist and the bridge auto-starts on relaunch. A nearby leaf can also be provisioned over Web Bluetooth (navigator.bluetooth, @listam/provisioning) — this writes the bridge control key plus the LAN hubAddr/audioAddr into the leaf, and falls back to copy-paste when navigator.bluetooth is absent. In browser/mock mode the section shows its "requires Pear runtime" state.

src/leaf-bridge-manager.mjs @listam/provisioning
Subsection

Servers (remote headless peers)

A top-level Servers surface pairs with and monitors remote headless peers over owner-control.

Pair & monitor

Paste a pairing code minted on a headless node (control-pair) plus a name → the device pairs over an encrypted HyperDHT session and appears as a status card with its capabilities. Status cards re-poll roughly every 20 s while open; a stopped node times out with a notice instead of hanging the UI. The Pear runtime is required — the ?mock=1 preview is read-only.

src/owner-control.mjs

Signed, capability-scoped controls

Safe controls — refresh/status, mint invite, export, shutdown — travel as Ed25519-signed, capability-scoped envelopes over HyperDHT with monotonic-sequence replay defense. src/owner-control.mjs was hardened from a connect-per-request model to one persistent connection, because HyperDHT will not let you reconnect to a server key once a connection to it has closed.

src/owner-control.mjs
Trust boundary: the leaf control key and owner-control device seed are credentials. They live in the app's storage directory as files today (keychain migration is tracked follow-up work), and the diagnostics log deliberately redacts keys and item payloads.
Persistence

Storage and Local State

Data Storage Owner Notes
List log and materialized view {Pear storage}/desktop/ Worker backend Corestore + encrypted Autobase under the app's private Pear storage directory.
Base / encryption keys listam-desktop-secrets.json Worker backend File-tier @listam/secrets store, loaded into the boot payload at startup.
Owner-control identity owner-control-keys.json Renderer (Pear only) Device key seed plus the paired-servers list (name, capabilities, public key).
UI preferences localStorage listam.desktop.uiPreferences Renderer Theme, grid view, category toggles, hint bar, leaf-bridge toggle and port. Local to this machine — never replicated.
Locale choice localStorage listam.desktop.localeChoice Renderer system, en, es, or the QA pseudo-locales.
Backup & data: Settings exposes Export data (.listam), Import (auto-detects a data archive vs. an instance seed), and Export instance seed (.listamseed) — all password-encrypted. The crypto lives in @listam/backend (Argon2id key derivation + XChaCha20-Poly1305) and rides the protocol commands RPC_EXPORT_DATA = 24, RPC_EXPORT_SEED = 25, and RPC_IMPORT = 26 (import merges last-write-wins). Separately, the Congruency and Activity analytics moved out of the rail into a new Analytics section of the Settings dialog.
Distribution

Installer & Releases

P2P distribution model

The app itself ships over Pear: pear stage uploads the checkout to a channel drive, pear release moves the release pointer consumers follow. The production app key is pear://h1jwexik1m9c75rqng8hico4oxqgmm8xskws684skmjepksq5r3o. The stage ignore list replaces Pear's defaults, so it must re-state .git alongside tests, design docs, and installer sources.

installer/build-macos.sh

macOS appling DMG

installer/build-macos.sh [--release] [--native] stages, generates listam.icns from the canonical icon, builds a Listam.app shell that execs pear run --appling against the app key, and packs a drag-to-/Applications DMG. The native cmake-pear appling owns the Dock tile and splash; both are ad-hoc signed (right-click → Open past Gatekeeper).

installer/build-macos.sh installer/appling/

Seeding

Installs fetch the payload from whoever seeds the drive. installer/seed-agent.sh install registers the ch.saynode.listam.seed LaunchAgent, which runs pear seed production from this checkout at login and logs to ~/Library/Logs/listam-seed.log.

installer/seed-agent.sh

First run on a fresh machine

Without Pear installed, the appling offers the pears.com download; with Pear, an untrusted key needs a one-time approval. Bundle id ch.saynode.listam.desktop, minimum macOS 11. Windows/Linux packaging is scaffolded but not wired to CI yet.

installer/README.md
Dependency roles

Libraries

@listam/backendThe shared P2P engine — the same package the mobile worklet and headless service boot.
@listam/protocol + @listam/clientNumeric command ABI and the event decoder that keeps bare imports out of the renderer.
@listam/domainId-keyed list reducer shared across all apps — duplicate names stay distinct items.
@listam/grocery + @listam/i18nCategory taxonomy with localized names; shared locale catalogs including QA pseudo-locales.
@listam/owner-controlPairing codes, signed command envelopes, and capability checks for headless devices.
@listam/secrets + @listam/loggingFile secret store and redacting logger.
Corestore / AutobaseLocal Hypercore storage and the encrypted multi-writer log, in the worker.
Hyperswarm / BlindPairingPeer discovery, replication, and the invite handshake.
bare-rpc / bare-tcp / hyperdhtWorker RPC framing, the leaf-bridge TCP listener, and the owner-control transport.
package.json
Source map

File Index

Verification

Testing

Automated suites run with the plain node:test runner; manual passes follow the Desktop Checklist on the QA page.

Unit

test/store.test.mjs (id-keyed reduction and event handling), test/secret-store.test.mjs (boot payload, key round-trip, corrupt-file degradation), test/leaf-bridge.test.mjs (start/stop lifecycle, generation-token races, port validation).

Cross-instance acceptance

test/sync.test.mjs spawns two real backend child processes on a private hyperdht testnet, mints an invite on one, joins from the other, mutates on both sides, and asserts id-keyed convergence within a 120 s budget.

test/helpers/backend-driver.mjs

E2E / manual

The test/pear-smoke harness drives the app under the Pear runtime; ?mock=1 serves the UI with fixtures for design passes; release builds run the full manual checklist from the installed appling.

How to run

npm run dev launches via pear run --dev .; npm test runs the suites; npm run ci is lint + tests and is the release gate together with the cross-device harness.

Interaction: desktop rows of the cross-device matrix live in Cross-Device Scenarios: mobile ↔ desktop content parity in both directions, desktop ↔ headless join and owner-control driving, and the ESP32 leaf mirroring through the bridge (tools/cross-device/esp32-leaf.mjs stands in for the board).