Skip to content

Add me serve command: local web UI for memories#47

Merged
murrayju merged 20 commits intomainfrom
murrayju/serve
Apr 24, 2026
Merged

Add me serve command: local web UI for memories#47
murrayju merged 20 commits intomainfrom
murrayju/serve

Conversation

@murrayju
Copy link
Copy Markdown
Member

@murrayju murrayju commented Apr 24, 2026

me serve — local web UI for memories

Adds a new me serve command that starts a local web server on 127.0.0.1:3000 (auto-incrementing if busy) and opens a browser to a React UI for browsing and editing memories in the currently-configured engine. The global --server flag selects which engine to target; the CLI stores no additional credentials.

me serve [--port <n>] [--host <addr>] [--no-open]

What the UI does

Layout

  • Left: resizable tree sidebar (drag handle between panes; width persists across reloads, keyboard-accessible with arrow keys / Home / End).
  • Right: markdown viewer + Monaco editor for the selected memory, with a metadata panel below showing read-only fields (id, embedding status, createdAt, updatedAt, createdBy).
  • Top: collapsible search pane that summarizes active filters as chips when hidden.

Tree

  • Full path hierarchy from memory.tree (no content, no row cap) — every path is visible without pagination.
  • Top-level paths expand by default; leaves load lazily when a path opens and only when it has direct memories.
  • Path rows show aggregate counts; leaves render newest-first by temporal start.
  • Expansion state is tracked separately per mode (browse vs. search) so applying and clearing a filter preserves the previous view.
  • Right-click context menus: Delete… on memories, Delete subtree… on paths (the subtree dialog confirms with an exact dry-run count).

Search

  • Simple: one hybrid-search input (semantic + fulltext).
  • Advanced: every memory.search parameter — semantic, fulltext, grep, tree (ltree filter), meta (JSON), temporal (contains / overlaps / within), limit, candidateLimit, weights, orderBy. Meta JSON is validated live; invalid JSON is excluded from the RPC and flagged inline.
  • Filter state is URL-synced — links like ?q=typescript&selected=<uuid> restore the full view.
  • The advanced pane collapses to a compact summary-chip row; filters stay applied while collapsed and Clear remains available.

Viewer & editor

  • Markdown rendering with GFM + syntax highlighting (react-markdown + rehype-highlight).
  • Memory frontmatter (tree, meta, temporal) preview above the body.
  • Monaco editor with markdown syntax, lazy-loaded so it only arrives when the user enters edit mode. YAML frontmatter edits route to the correct fields in memory.update; invalid YAML shows an inline parse error.
  • Save button enables only when dirty; success/failure toasts.
  • Switching memories while dirty shows a discard-changes prompt; beforeunload covers tab close.

How it's wired

  • Bun HTTP server in packages/cli/serve/. /rpc forwards JSON-RPC bodies byte-for-byte to the configured engine with the stored API key injected server-side; /healthz returns { ok: true }; everything else serves the embedded Vite build (hashed assets get immutable cache headers, index.html gets no-cache, SPA fallback handled).
  • New packages/web workspace: React 19, Vite 7, Tailwind v4, TanStack Query for RPC, zustand for UI state (selection, filter, editor-dirty, layout, UI dialogs), zustand persist middleware for cross-reload preferences.
  • build:web in packages/cli runs the Vite build and scripts/bundle-web-assets.ts base64-encodes every file into packages/cli/serve/web-assets.generated.ts so the compiled binary is self-contained. The generated file is gitignored.
  • No new auth surface: the server binds to localhost by default and trusts any connection; credentials come from the existing resolveCredentials(--server) flow.

Docs

  • New: docs/cli/me-serve.md (command reference, flags, UI overview, security posture, examples).
  • Updated: docs/getting-started.md (new "Browse in the web UI" section) and mkdocs.yml nav.

Testing

./bun run check green: 693 unit tests passing. New coverage includes the tree-building logic (browse vs. search path grouping, aggregate/direct counts, synthetic root bucket, leaf sort order), the URL round-trip, frontmatter parse/serialize, the HTTP server (healthz / proxy / SPA fallback / auth injection / upstream-unreachable), and the filter-summary helper. End-to-end smoke: me serve --no-open --port <n> with a dummy API key serves the embedded bundle correctly.

Screenshots

image image image

murrayju added 19 commits April 24, 2026 15:48
Split expandedPaths into expandedBrowse (default collapsed) and
collapsedSearch (default expanded). Applying or clearing a filter no
longer perturbs browse-mode expansion — pruning is scoped to the
corresponding bucket. A selectIsExpanded helper centralizes the
asymmetric membership semantics.
Drop the forceOpen override that pinned every search-matched path open.
Rows now read straight from the store: in search context the default is
expanded (membership in collapsedSearch flips a path closed), so new
matches are still visible without any pre-seeding, and user clicks land
in the search bucket where they're kept separate from browse state.
Replace the \u25b8 unicode glyph (small, font-dependent baseline) with a
pixel-consistent inline SVG chevron in a 16\u00d716 box. The leaf bullet gets
the same flex-centered 16\u00d716 wrapper so it lands in the same column as
the caret above and stays centered regardless of line height.
Flex items default to min-width: auto, so a long label forced the count
(or trailing edge) to overflow past the button's padding-right. Add
min-w-0 flex-1 to the title spans so truncate actually kicks in, and
shrink-0 to the count so it never gets squeezed out of its column.
Right-clicking a MemoryRow landed on the outer PathRow handler too,
which overwrote the context-menu target with { kind: 'path' } and
rendered the wrong menu items. Call stopPropagation in both handlers so
the innermost row determines the menu.
A fresh checkout fails with 'Could not resolve ./web-assets.generated.ts'
because scripts/build-all.ts calls `bun build --compile` directly,
bypassing the CLI package's `build` script (which chains build:web).
Run build:web up front so the generated assets exist when every platform
compile starts.
web-assets.generated.ts is gitignored and transitively imported by
packages/cli/serve. Running scripts/bundle-web-assets.ts with no web
dist/ emits a valid empty-map stub, which lets CI's lint/typecheck/tests
resolve the module without the multi-second Vite build.
@murrayju murrayju merged commit 7ec5f43 into main Apr 24, 2026
3 checks passed
@murrayju murrayju deleted the murrayju/serve branch April 24, 2026 19:57
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