The Story So Far — memopop-ai From Empty Bun Workspace to Live Desktop Client
First changelog at the monorepo level. Catching up the eight days that took memopop-native from `bun create tauri-app` to a fullscreen client driving the orchestrator's HTTP API end-to-end.
feat(memopop-native): full Tauri client for the orchestrator API
Catch-up commit covering work since the bun-workspace scaffold and the
three foundational changelogs already in apps/memopop-native/changelog/.
The desktop client now drives the FastAPI sidecar end-to-end:
* Onboarding journey. Anchor orchestrator → outline gallery → outline
detail → firm creation (snake_case slug + idempotent .gitignore
handling) → deal capture form. JourneyBreadcrumbs across the top
tracks position with a thin progress bar.
* Job runtime. Fullscreen JobView with three live panes:
- PhaseChecklist (left): ten ordered phases derived from structured
milestone events streaming from the orchestrator. States: pending,
in-progress (animated spinner), done (green check + relative time),
failed (red X).
- LogStream (center): terminal-dark, JetBrains Mono, blinking purple
cursor at the tail. Capped at 1,000 rendered / 2,000 stored with
rAF-throttled scroll to fix Svelte deep-reactivity GC stalls.
- ArtifactBrowser (right): polls GET /memos/{id}/artifacts every 3s,
groups files by top-level folder, flashes new files green on arrival.
* Sidecar lifecycle. Rust SidecarManager spawns Python from
{repoPath}/.venv/bin/python, polls /healthz, self-heals on dead
sidecars (always probes healthz before forwarding, drops stale handles),
kills cleanly on WindowEvent::CloseRequested|Destroyed. POST
/actions/stop-sidecar wires a Stop button that calls
SidecarManager::shutdown directly — bypasses Python entirely so it
works when the sidecar is wedged.
* Transport seam. LocalTransport routes JSON through one api_dispatch
Tauri command (Rust forwards /memos* to the sidecar via reqwest, strips
the dispatcher-only repoPath field). SSE streams go DIRECT from the
webview to localhost:8765 — bypassing the Rust dispatcher because the
FastAPI CORS allowlist already covers tauri:// and http://localhost
origins, and reimplementing chunked HTTP forwarding in Rust would be
a waste.
Tied to the investment-memo-orchestrator changelogs 2026-04-30_01
(FastAPI sidecar), 2026-05-01_02 (milestones + scaffold), and
2026-05-01_03 (persistence + versioning + lifecycle) which shipped the
API surface this client consumes.Changelog - 2026-05-01_01
The Story So Far — memopop-ai From Empty Bun Workspace to Live Desktop Client
This is the first changelog at the monorepo level for memopop-ai. We've been writing changelogs inside apps/memopop-native/changelog/ since the Tauri shell was scaffolded — three of them, all dated 2026-04-27 — but never one at the top of the workspace itself. So this entry catches up the rest, narrates the arc from "Bun workspace with three placeholder app dirs" to "fullscreen desktop client driving the orchestrator's HTTP API live, with milestones, log streaming, file flashing, stop button, and a self-healing sidecar manager," and gives the next changelog a place to start.
If you want the granular foundation, read those three first:
apps/memopop-native/changelog/2026-04-27_01.md— Tauri 2 + Svelte 5 inside the Bun workspace.apps/memopop-native/changelog/2026-04-27_02.md— Settings + firm picker + the Transport seam.apps/memopop-native/changelog/2026-04-27_03.md— Full onboarding journey: outline gallery, firm creation, deal capture.
Everything below picks up where 2026-04-27_03 ended.
The Arc, in Order
2026-04-27. Three changelogs in one day. Tauri shell stood up, plugins installed, transport interface defined,
LocalTransportcalling a single Rustapi_dispatchcommand, GET /firms wired end-to-end. Then Settings + firm picker. Then full onboarding flow — gallery, outline detail modal, firm creation modal with snake_case slug preview and idempotent.gitignorepatching, deal capture form. Stopped at "Ready to generate" placeholder.2026-04-30. Two things happened in parallel:
JourneyBreadcrumbsshipped (above the gallery / between header and content) — a four-step progress bar (Outline → Firm → Deal → Generate) with backward navigation.The orchestrator gained a FastAPI sidecar at
investment-memo-orchestrator/src/server/. The desktop client now had something to talk to. The sidecar's API mirrorsgenerate_memo()kwargs.
2026-04-30 → 2026-05-01 (overnight). Wired the desktop to the sidecar.
Rust:
Cargo.tomlgainedreqwest+tokio[time]. Newsrc/api/sidecar.rswithSidecarManager(lazy spawn from{repoPath}/.venv/bin/python, healthz polling, child handle inMutex<Option<CommandChild>>,forward()for JSON,shutdown()for SIGKILL). New match arms inrouter.rsfor/memos*(POST + status + list + artifacts) — all routed throughforward_to_sidecarwhich strips the dispatcher-onlyrepoPathfield before forwarding.lib.rsregisters the manager in app state and wires shutdown toWindowEvent::Destroyed.Frontend:
Transportinterface gainssubscribeEvents(jobId, onEvent).LocalTransport.subscribeEventsopens anEventSourcedirect tohttp://127.0.0.1:8765/memos/{id}/events— bypassing the Rust dispatcher entirely. Newrunning_jobflow stage with milestones + events arrays.DealCreationModalnow does a realPOST /memos. NewJobLogViewermodal with three tabs: Milestones, Files, Logs.
2026-05-01 morning. Built
MilestoneTimelineandArtifactBrowser(live polling of artifacts every 3s, files flash green on arrival). The orchestrator'sMilestoneExtractorpublished structuredmilestoneevents; the timeline rendered them as a vertical chronological list.2026-05-01 (the user-feedback inflection). First end-to-end run with an actual memo. Four issues surfaced:
Log viewer ate memory and the screen went black on a 30s cadence.
The checklist updated only at the end (consequence of #1).
Files landed at
output/ChromaDB-v0.0.1/— wrong dir.A second run silently overwrote the first run's directory.
2026-05-01 evening — the redesign. Replaced the tabbed
JobLogViewermodal withJobView: fullscreen, three columns simultaneously visible (PhaseChecklist·LogStream·ArtifactBrowser), no tabs. The user's words: "this will be THE THING THEY ARE TRYING TO DO, WITH IMPORTANCE." Fullscreen made it that. Deleted the oldJobLogViewer.svelteandMilestoneTimeline.svelte— the new components own that space cleanly.2026-05-01 late evening — the hardening pass. Performance fixes (events array capped at 2,000 in store, rendered lines capped at 1,000 with a "earlier lines hidden — full log on disk" banner,
requestAnimationFrame-throttled auto-scroll). Stop button (red⏹ Stopin the JobView header, callsPOST /actions/stop-sidecarwhich routes toSidecarManager::shutdown()directly so it works when Python is wedged). Tauri shell extended to handleWindowEvent::CloseRequestedalongsideDestroyed(Cmd+W and Cmd+Q both reliably terminate the sidecar).SidecarManager::ensure_runningself-heals — always probes/healthzfirst, drops stale handles, spawns fresh.Where we are now (commit time). The user can launch MemoPop, anchor an orchestrator, click an outline, set up a firm, fill a deal form, click Generate, and watch:
The phase checklist tick over in real time.
Log lines stream in the dark center pane with a blinking purple cursor.
Files flash green on the right as they appear in the firm-scoped output dir.
A red Stop button kills the run cleanly mid-flight.
Quitting the window kills the sidecar.
What memopop-ai Is Right Now
memopop-ai/
├── apps/
│ ├── memopop-native/ # The Tauri 2 desktop shell. The substantive app.
│ ├── memopop-site/ # Astro landing page (placeholder; not yet wired into the journey).
│ └── memopop-web-app/ # SvelteKit web app (placeholder; future hosted-mode home).
├── packages/ # Shared packages (currently empty / pre-extraction).
├── changelog/ # ← THIS file lives here. First entry at the monorepo level.
└── context-v/ # Specs, explorations, reminders. (Read context-v/specs/ for designs.)The substantive app: apps/memopop-native/
apps/memopop-native/
├── src-tauri/ # Rust backend
│ ├── src/api/
│ │ ├── router.rs # api_dispatch — match (method, path) → query | action | sidecar-forward
│ │ ├── queries.rs # GET /firms, /outlines, /outlines/{id}
│ │ ├── actions.rs # POST /actions/create-firm
│ │ ├── sidecar.rs # SidecarManager: spawn, healthz, forward, shutdown, self-heal
│ │ └── mod.rs # ApiError shape + helper constructors
│ ├── src/lib.rs # Tauri builder, plugin registration, WindowEvent handler
│ └── Cargo.toml # tauri 2, reqwest, tokio[time], plugins (shell, dialog, store, fs, opener)
└── src/ # Frontend (Svelte 5 runes + SvelteKit static)
├── lib/
│ ├── transport/ # Transport interface + LocalTransport (api_dispatch + EventSource)
│ ├── stores/ # settings.svelte.ts, outlines.svelte.ts, flow.svelte.ts (running_job state)
│ └── components/ # JobView, PhaseChecklist, LogStream, ArtifactBrowser,
│ # OutlineGallery, OutlineCard, OutlineDetail,
│ # FirmCreationModal, DealCreationModal,
│ # AnchorOrchestrator, JourneyBreadcrumbs, Header, Settings
└── routes/
├── +layout.svelte # Header + JourneyBreadcrumbs + content; height: 100vh + min-height: 0
├── +page.svelte # Branches: anchor → running_job (JobView) → gallery + modals
└── settings/+page.svelteHow a request flows
┌─────────────────────────────────────────────────────────────────┐
│ Webview (Svelte runes UI) │
│ │
│ getTransport().request('POST', '/memos', { ... }) │
│ getTransport().subscribeEvents(jobId, onEvent) │
└────┬───────────────────────────────────────────┬────────────────┘
│ JSON via Tauri invoke │ EventSource (HTTP/SSE)
▼ │
┌─────────────────────────────────────────┐ │
│ Rust dispatcher (api_dispatch) │ │
│ - Routes /firms, /outlines, /actions │ │
│ - Forwards /memos* via reqwest → │ │
│ SidecarManager::forward() │ │
└────┬────────────────────────────────────┘ │
│ HTTP localhost:8765 │
▼ │
┌─────────────────────────────────────────┐ │
│ FastAPI sidecar (Python, in venv) │◄─────┘
│ - generate_memo() in a worker thread │
│ - JobEventBus → SSE │
│ - persist_job_logs at terminal │
└─────────────────────────────────────────┘The split is deliberate: the Rust dispatcher is the right place for JSON request/response (uniform error shape, single Tauri command surface, easy to swap for HTTP-mode later). SSE is browser-native — the webview opens an EventSource directly. Reimplementing chunked HTTP forwarding inside api_dispatch would have been wasted code.
Design Decisions Worth Recording
The Transport interface has exactly two methods. request() for JSON request/response, subscribeEvents() for SSE streaming. Resisted the temptation to add a third (watchFiles, subscribeStatus, etc.) — anything periodic can be polled via request() and a setInterval, anything streaming can be expressed as SSE. Two methods stay easy to swap when local mode becomes hosted mode.
The Tauri sidecar lives in the orchestrator's repo, not memopop-ai's. Documented in context-v/explorations/Moving-an-Agent-Orchestrator-to-an-API.md. The FastAPI is a thin import wrapper around generate_memo(); it has to live next to the Python source it imports. The orchestrator gains an HTTP surface for any non-Python client; we gain a memopop-specific UI; nobody owns two copies of the workflow.
SSE goes direct from webview to localhost:8765. Considered routing through Rust for "uniformity," rejected because (a) EventSource is browser-native and works perfectly, (b) the FastAPI CORS allowlist already covers Tauri origins (tauri://localhost, http://tauri.localhost(:port)?), (c) Rust would have to forward chunked HTTP responses, which is real engineering for no real benefit.
Lazy sidecar spawn. SidecarManager doesn't start anything until the first /memos* call. Spawn cost (250ms-1s) is paid once, on the first action that needs it. The user clicking around the gallery and reading outline detail modals never wakes up Python.
Self-healing instead of version-checking. ensure_running always probes /healthz first. If the sidecar is dead, hung, or running stale code (which we can't directly detect without instrumentation), the simplest reliable approach is "if healthz is silent, kill any handle we have and spawn fresh." Future work could add a /healthz-returned version hash for staleness detection; today's bias is "self-heal anything that isn't responding."
Stop bypasses Python. POST /actions/stop-sidecar routes to SidecarManager::shutdown() in Rust directly — does not round-trip through the FastAPI server. The whole point of "Stop" is that Python may be wedged. SIGKILL via stored CommandChild is the only design that works when the sidecar is in a deadlocked retry loop or rate-limited. SIGKILL means cancelled runs lose their structured log file (no time for persist_job_logs to run); for v1 that trade-off is acceptable.
The job UI is a route-level page, not a modal. Earlier iteration was a centered modal capped at 760px. The first user-test feedback was decisive: "most people will likely not have it as a small part of their screen." Replaced with a full-viewport three-column layout. Logs dominate, milestones are glanceable on the left, files flash on the right.
Performance bounds at the store level, not just the view level. Capping rendered lines fixes layout thrash; but the events array itself was the source of GC stalls (each event a deep-reactivity proxy). Cap at the store with slice(-2000) whenever the array exceeds the bound. View-level caps are a secondary defense.
Flow state is a discriminated union, not a stack. idle | outline_detail | create_firm | create_deal | ready_to_run | running_job. One variable; conditional rendering in +page.svelte reads cleanly. Resisted earlier urge to model a modal stack (since the spec's flow is linear, not nested).
Verification
$ bun run check
1 ERROR (vite.config.js — pre-existing, unrelated)
19 WARNINGS (all pre-existing accessibility / unused-CSS)
0 new issues from any of this work.
$ bun run build
✓ built in 1.6s
✓ Wrote site to "build" via @sveltejs/adapter-static
$ cargo check --manifest-path src-tauri/Cargo.toml
Finished `dev` profile [unoptimized + debuginfo] target(s)
$ bun run tauri dev
# Window opens, gallery renders, full journey is operable end-to-end.End-to-end run verified the night of 2026-05-01: ChromaDB on alpha-partners ran a full 17-minute pipeline through the JobView, files appearing live in the right pane, milestones ticking over in the checklist, log streaming smoothly without freezing. Stop button confirmed killing the sidecar via "Stopped by user" milestone. Output landed at io/alpha-partners/deals/ChromaDB/outputs/ChromaDB-v0.0.2/ (the never-overwrite versioning fix doing its job).
Files Worth Highlighting
(Not exhaustive — the full tree is in apps/memopop-native/. These are the load-bearing pieces a future contributor should know about.)
| File | Role |
apps/memopop-native/src-tauri/src/api/router.rs | The dispatcher. One match expression, one source of routing truth. Add new endpoints here. |
apps/memopop-native/src-tauri/src/api/sidecar.rs | SidecarManager — process lifecycle, self-heal, forward. The Python child's parent. |
apps/memopop-native/src/lib/transport/local.ts | Two methods: request() and subscribeEvents(). The whole transport seam. |
apps/memopop-native/src/lib/stores/flow.svelte.ts | The state machine for the user's journey. Discriminated union, ~150 lines. |
apps/memopop-native/src/lib/components/JobView.svelte | The fullscreen runtime view. Three children, one header, one banner. |
apps/memopop-native/src/lib/components/PhaseChecklist.svelte | Maps milestones to ten ordered phases. Pure presentation. |
apps/memopop-native/src/lib/components/LogStream.svelte | Capped-and-throttled tail. The sexy bit. |
apps/memopop-native/src/lib/components/ArtifactBrowser.svelte | Live file-tree with green-flash on arrival. |
What's Next
The journey from outline → memo is closed end-to-end for the local-sidecar mode. Three categories of follow-up, none urgent:
Polish the export layer. The orchestrator produces a 67k-character markdown memo plus a one-pager HTML/PDF plus a Word doc. The desktop client currently shows the output directory path and the file list — but doesn't open files. A "View memo" button that opens
7-{Company}-vX.Y.Z.mdin the system viewer (or a built-in markdown preview) would close the loop visibly.tauri-plugin-shell::open()is the move.Branded export flow. The orchestrator's
export-branded.pyproduces firm-styled HTML/PDF using thebrand-{firm}-config.yamlwe scaffold-stub during firm creation. Today the brand config has onlycompany.conventional_namepopulated. A "Brand setup" wizard (hinted at incontext-v/specs/An-Onboarding-User-Journey-for-Memopop-Native.md) is the natural next feature — promising version is the agent-driven version that auto-fetches colors/fonts/logos from a firm's website URL.Hosted mode. Today's transport is
LocalTransport. Tomorrow's isHttpTransportto a deployed FastAPI. Both implement the same two-method interface; the components don't change. The work between local and hosted is auth, multi-tenant isolation, persistent storage, and a job queue — none of which are needed before the first hosted user.
The memopop-site/ and memopop-web-app/ apps in this workspace are placeholders for the hosted future. They'll get their own changelog entries when there's something to say.
