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. |
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
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).