From f95b0ae91272a0d035a52e72579d214cacbdac92 Mon Sep 17 00:00:00 2001 From: francy51 Date: Thu, 14 May 2026 21:28:32 -0400 Subject: [PATCH] Implement local SQLite backend and reactive UI --- apps/desktop/src/ipc.ts | 16 +- apps/desktop/src/main.ts | 76 +- apps/desktop/tsdown.config.ts | 8 +- apps/web/src/lib/constants.ts | 51 +- apps/web/src/rpcClient.ts | 19 +- apps/web/src/ui/App.tsx | 1042 +----- apps/web/src/ui/app/AppShell.tsx | 3915 ++++++++++++++++++++ package.json | 9 +- packages/contracts/src/rpc.ts | 6 + packages/contracts/src/rpcSchemas.ts | 12 + packages/shared/package.json | 12 - packages/shared/src/agents/runner.ts | 7 +- packages/shared/src/db/database.test.ts | 51 + packages/shared/src/db/database.ts | 111 +- packages/shared/src/db/queries.test.ts | 34 +- packages/shared/src/db/queries.ts | 466 ++- packages/shared/src/db/schema.ts | 42 +- packages/shared/src/db/seed.ts | 289 +- packages/shared/src/demoData.ts | 162 - packages/shared/src/extraData.ts | 44 - packages/shared/src/mockRpc.ts | 250 -- packages/shared/src/rpc/agentRpc.test.ts | 34 + packages/shared/src/rpc/agentRpc.ts | 72 +- packages/shared/src/rpc/companyRpc.ts | 34 +- packages/shared/src/rpc/exportRpc.ts | 49 +- packages/shared/src/rpc/marketRpc.ts | 45 +- packages/shared/src/rpc/memoRpc.test.ts | 62 +- packages/shared/src/rpc/memoRpc.ts | 58 +- packages/shared/src/rpc/modelRpc.test.ts | 21 +- packages/shared/src/rpc/modelRpc.ts | 52 +- packages/shared/src/rpc/portfolioRpc.ts | 42 +- packages/shared/src/rpc/startupRpc.test.ts | 77 + packages/shared/src/rpc/workspaceRpc.ts | 47 +- pnpm-lock.yaml | 207 ++ scripts/rebuild-better-sqlite3.mjs | 31 + 35 files changed, 5444 insertions(+), 2009 deletions(-) create mode 100644 apps/web/src/ui/app/AppShell.tsx delete mode 100644 packages/shared/src/demoData.ts delete mode 100644 packages/shared/src/extraData.ts delete mode 100644 packages/shared/src/mockRpc.ts create mode 100644 packages/shared/src/rpc/agentRpc.test.ts create mode 100644 packages/shared/src/rpc/startupRpc.test.ts create mode 100644 scripts/rebuild-better-sqlite3.mjs diff --git a/apps/desktop/src/ipc.ts b/apps/desktop/src/ipc.ts index fecc300..bc5c2e1 100644 --- a/apps/desktop/src/ipc.ts +++ b/apps/desktop/src/ipc.ts @@ -12,6 +12,8 @@ export type IpcMethod = { input: z.ZodType; output: z.ZodType; handler: (input: Input) => Promise | Output; + rawArgs?: boolean; + rawResult?: boolean; }; function validationDetail(error: z.ZodError): unknown { @@ -22,10 +24,11 @@ function validationDetail(error: z.ZodError): unknown { } export function registerIpcMethod(method: IpcMethod): void { - ipcMain.handle(method.channel, async (_event, rawInput): Promise> => { + ipcMain.handle(method.channel, async (_event, ...rawArgs): Promise | Output> => { + const rawInput = method.rawArgs ? rawArgs : rawArgs[0]; const parsedInput = method.input.safeParse(rawInput); if (!parsedInput.success) { - return { + const errorResult: IpcResult = { ok: false, error: { code: "VALIDATION_ERROR", @@ -33,29 +36,32 @@ export function registerIpcMethod(method: IpcMethod = { ok: false, error: { code: "INTERNAL_ERROR", message: "Invalid IPC response.", }, }; + return method.rawResult ? errorResult as Output : errorResult; } - return { ok: true, data: parsedOutput.data }; + return method.rawResult ? parsedOutput.data : { ok: true, data: parsedOutput.data }; } catch (error) { - return { + const errorResult: IpcResult = { ok: false, error: { code: "INTERNAL_ERROR", message: error instanceof Error ? error.message : "Unhandled IPC failure.", }, }; + return method.rawResult ? errorResult as Output : errorResult; } }); } diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index bfa60fb..2d4a142 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -8,9 +8,10 @@ import { startAutoSnapshot } from "./snapshotService.js"; import { registerFileHandlers } from "./fileHandler.js"; import type { ClientSettings, RpcMethod, RpcResult } from "@mosaiciq/contracts/rpc"; import { isRpcMethod, parseRpcRequest, parseRpcResponse } from "@mosaiciq/contracts/rpcSchemas"; -import { getDatabase, seedDatabase, getDataDir, updateClientSettings, getClientSettings } from "@mosaiciq/shared/db"; +import { getDatabase, bootstrapDatabase, getDataDir, updateClientSettings, getClientSettings } from "@mosaiciq/shared/db"; import { eventEmitter } from "@mosaiciq/shared/agents"; import { z } from "zod"; +import { registerIpcMethod } from "./ipc.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const isDev = process.env.VITE_DEV_SERVER_URL || !app.isPackaged; @@ -44,34 +45,49 @@ function rpcValidationDetail(error: z.ZodError): unknown { return error.issues.map((issue) => ({ path: issue.path.join("."), message: issue.message })); } -ipcMain.handle("rpc:call", async (_event, method: unknown, payload: unknown): Promise> => { - if (!isRpcMethod(method)) { - return { ok: false, error: { code: "VALIDATION_ERROR", message: "Unknown RPC method." } }; - } +registerIpcMethod>({ + channel: "rpc:call", + input: z.tuple([z.unknown(), z.unknown()]), + output: z.custom>((value) => { + if (!value || typeof value !== "object" || !("ok" in value)) return false; + const result = value as RpcResult; + return result.ok ? "data" in result : "error" in result; + }), + rawArgs: true, + rawResult: true, + handler: async ([method, payload]): Promise> => { + if (!isRpcMethod(method)) { + return { ok: false, error: { code: "VALIDATION_ERROR", message: "Unknown RPC method." } }; + } - let parsedPayload; - try { - parsedPayload = parseRpcRequest(method, payload); - } catch (error) { - return { - ok: false, - error: { - code: "VALIDATION_ERROR", - message: "Invalid RPC payload.", + let parsedPayload; + try { + parsedPayload = parseRpcRequest(method, payload); + } catch (error) { + return { + ok: false, + error: { + code: "VALIDATION_ERROR", + message: "Invalid RPC payload.", + detail: error instanceof z.ZodError ? rpcValidationDetail(error) : undefined, + }, + }; + } + + const result = await handleRpc(method, parsedPayload); + if (!result.ok) return result; + + try { + parseRpcResponse(method, result.data); + return result; + } catch (error) { + console.error("[RPC] Invalid response:", { + method, detail: error instanceof z.ZodError ? rpcValidationDetail(error) : undefined, - }, - }; - } - - const result = await handleRpc(method, parsedPayload); - if (!result.ok) return result; - - try { - parseRpcResponse(method, result.data); - return result; - } catch { - return { ok: false, error: { code: "INTERNAL_ERROR", message: "Invalid RPC response." } }; - } + }); + return { ok: false, error: { code: "INTERNAL_ERROR", message: "Invalid RPC response." } }; + } + }, }); app.whenReady().then(async () => { @@ -91,11 +107,7 @@ app.whenReady().then(async () => { } } - // Seed demo data if empty (check if any companies exist) - const companyCount = db.prepare("SELECT COUNT(*) as count FROM companies").get() as { count: number }; - if (companyCount.count === 0) { - seedDatabase(db); - } + bootstrapDatabase(db); const win = await createWindow(); diff --git a/apps/desktop/tsdown.config.ts b/apps/desktop/tsdown.config.ts index 982e310..336c113 100644 --- a/apps/desktop/tsdown.config.ts +++ b/apps/desktop/tsdown.config.ts @@ -5,6 +5,9 @@ const shared = { outDir: "dist-electron", sourcemap: true, outExtensions: () => ({ js: ".cjs" }), + deps: { + neverBundle: ["electron"], + }, }; export default defineConfig([ @@ -12,7 +15,10 @@ export default defineConfig([ ...shared, entry: ["src/main.ts"], clean: true, - noExternal: (id) => id.startsWith("@mosaiciq/"), + deps: { + ...shared.deps, + alwaysBundle: (id: string) => id.startsWith("@mosaiciq/"), + }, }, { ...shared, diff --git a/apps/web/src/lib/constants.ts b/apps/web/src/lib/constants.ts index c289ceb..ae83d5b 100644 --- a/apps/web/src/lib/constants.ts +++ b/apps/web/src/lib/constants.ts @@ -1,4 +1,4 @@ -import type { Alert, Catalyst, EarningsSchedule, ExportRecord, Filing, Holding, Risk, Screen } from "@mosaiciq/contracts/rpc"; +import type { Screen } from "@mosaiciq/contracts/rpc"; export const screens: Screen[] = ["home", "workspace", "model", "memo", "agents"]; @@ -26,55 +26,6 @@ export const agentCatalog: Array<[string, string, string, string]> = [ ["qa", "Model QA Agent", "Formula auditing, balance sheet checks, sanity tests", "cross-cutting"] ]; -export const extraHoldings: Holding[] = [ - { ticker: "BJ", name: "BJ's Wholesale Club", price: 84.2, changePct: 0.4, weight: 8 }, - { ticker: "KR", name: "Kroger Co", price: 62.1, changePct: -0.6, weight: 5 }, - { ticker: "DG", name: "Dollar General", price: 78.5, changePct: 1.5, weight: 4 } -]; - -export const demoCatalysts: Catalyst[] = [ - { id: "cat-1", date: "2026-06-05", event: "Q3 FY25 Earnings", impact: "high", thesisRelevance: "supports", source: "[4]" }, - { id: "cat-2", date: "2026-07-15", event: "Executive member update", impact: "medium", thesisRelevance: "supports", source: "[2]" }, - { id: "cat-3", date: "2026-08-01", event: "Annual membership fee review", impact: "high", thesisRelevance: "neutral", source: "[1]" }, - { id: "cat-4", date: "2026-09-10", event: "New warehouse openings (Q4)", impact: "low", thesisRelevance: "supports", source: "[1]" } -]; - -export const demoAlerts: Alert[] = [ - { id: "alert-1", companyId: "cost", timestamp: "2026-05-12T08:30:00Z", type: "earnings_surprise", description: "Q2 FY25 earnings beat (+7.5% comp)", thesisImpact: "positive", status: "new", targetSection: "thesis" }, - { id: "alert-2", companyId: "cost", timestamp: "2026-05-12T06:00:00Z", type: "filing", description: "New 8-K: Executive compensation update", thesisImpact: "neutral", status: "new" }, - { id: "alert-3", companyId: "wmt", timestamp: "2026-05-11T14:00:00Z", type: "peer_event", description: "WMT announces price investment in grocery", thesisImpact: "negative", status: "reviewed" }, - { id: "alert-4", companyId: "cost", timestamp: "2026-05-11T10:00:00Z", type: "price_move", description: "COST +2.1% on heavy volume", thesisImpact: "positive", status: "reviewed" } -]; - -export const demoRisks: Risk[] = [ - { id: "risk-1", companyId: "cost", risk: "Amazon enters warehouse club segment", category: "competitive", severity: "high", likelihood: "low", mitigation: "Costco's 93% renewal rate creates switching costs", status: "open" }, - { id: "risk-2", companyId: "cost", risk: "Wage inflation compresses operating margin", category: "financial", severity: "medium", likelihood: "high", mitigation: "Automation and productivity offset 40-60% of wage pressure", status: "open" }, - { id: "risk-3", companyId: "cost", risk: "Membership fee increase delayed beyond FY26", category: "financial", severity: "medium", likelihood: "medium", mitigation: "Fee income growth from executive tier conversion", status: "mitigated" }, - { id: "risk-4", companyId: "cost", risk: "Regulatory pressure on merchandise sourcing", category: "regulatory", severity: "low", likelihood: "low", mitigation: "Diversified supply chain across 14 countries", status: "accepted" }, - { id: "risk-5", companyId: "cost", risk: "E-commerce disruption of warehouse model", category: "competitive", severity: "medium", likelihood: "medium", mitigation: "Costco.com growth and Instacart partnership", status: "open" }, - { id: "risk-6", companyId: "cost", risk: "Valuation compression on growth deceleration", category: "financial", severity: "high", likelihood: "medium", mitigation: "High-single-digit earnings growth supports premium", status: "open" } -]; - -export const demoEarnings: EarningsSchedule[] = [ - { id: "earn-1", companyId: "cost", quarter: "Q3 FY25", expectedDate: "2026-06-05", timing: "bmo" }, - { id: "earn-2", companyId: "cost", quarter: "Q4 FY25", expectedDate: "2026-09-25", timing: "bmo" }, - { id: "earn-3", companyId: "cost", quarter: "Q2 FY25", expectedDate: "2026-03-06", timing: "bmo", actualRevenue: "$62.5B", expectedRevenue: "$61.2B", actualEps: "$4.28", expectedEps: "$4.05" }, - { id: "earn-4", companyId: "cost", quarter: "Q1 FY25", expectedDate: "2025-12-12", timing: "bmo", actualRevenue: "$60.2B", expectedRevenue: "$59.8B", actualEps: "$4.04", expectedEps: "$3.98" } -]; - -export const demoFilings: Filing[] = [ - { id: "filing-1", companyId: "cost", formType: "10-K", filedDate: "2024-10-10", title: "Annual Report FY2024", keyChanges: "Updated segment reporting, new warehouse commitments ($2.1B)", reviewed: true }, - { id: "filing-2", companyId: "cost", formType: "10-Q", filedDate: "2026-03-10", title: "Quarterly Report Q2 FY25", keyChanges: "Membership fee income growth, margin expansion", reviewed: true }, - { id: "filing-3", companyId: "cost", formType: "8-K", filedDate: "2026-05-12", title: "Executive Compensation Update", keyChanges: "New CEO compensation structure", reviewed: false } -]; - -export const demoExports: ExportRecord[] = [ - { id: "export-1", type: "excel", title: "COST Model — Revenue Build FY25-FY27", companyId: "cost", format: "Excel", fileSize: "2.4 MB", status: "complete", createdAt: "2026-05-10T14:30:00Z" }, - { id: "export-2", type: "pdf", title: "COST Investment Memo — Draft", companyId: "cost", format: "PDF", fileSize: "1.8 MB", status: "complete", createdAt: "2026-05-09T16:00:00Z" }, - { id: "export-3", type: "ppt", title: "COST IC Presentation", companyId: "cost", format: "PowerPoint", fileSize: "4.2 MB", status: "processing", createdAt: "2026-05-12T09:00:00Z" }, - { id: "export-4", type: "pdf", title: "Peer Comparison Report", companyId: "cost", format: "PDF", fileSize: "980 KB", status: "complete", createdAt: "2026-05-08T11:00:00Z" } -]; - export const keyboardShortcuts: Array<[string, string, string]> = [ ["⌘K", "Open agent chat / command bar", "global"], ["⌘F", "Focus search bar", "global"], diff --git a/apps/web/src/rpcClient.ts b/apps/web/src/rpcClient.ts index 9bd2140..96f159c 100644 --- a/apps/web/src/rpcClient.ts +++ b/apps/web/src/rpcClient.ts @@ -1,18 +1,17 @@ import type { RpcClient, RpcMethod, RpcRequestMap } from "../../../packages/contracts/src/rpc"; -import { parseRpcResponse } from "../../../packages/contracts/src/rpcSchemas"; -import { handleMockRpc } from "../../../packages/shared/src/mockRpc"; export const rpc: RpcClient = { async call(method: T, payload: RpcRequestMap[T]) { - const useMockRpc = !window.mosaic; - if (useMockRpc) { - const result = await handleMockRpc(method, payload); - if (process.env.NODE_ENV !== "production" && result.ok) { - parseRpcResponse(method, result.data); - } - return result; + if (!window.mosaic) { + return { + ok: false, + error: { + code: "INTERNAL_ERROR", + message: "Desktop backend unavailable. Run MosaicIQ in the Electron app to use the local SQLite backend." + } + }; } - return window.mosaic!.call(method, payload); + return window.mosaic.call(method, payload); } }; diff --git a/apps/web/src/ui/App.tsx b/apps/web/src/ui/App.tsx index c2535e3..59488f9 100644 --- a/apps/web/src/ui/App.tsx +++ b/apps/web/src/ui/App.tsx @@ -1,1041 +1 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import type { Agent, ClientSettings, Company, ExportRecord, Holding, MemoAnnotation, MemoSection, MemoSectionReview, ModelRow, RpcResponseMap, Screen } from "@mosaiciq/contracts/rpc"; -import { rpc } from "../rpcClient"; -import { cx } from "../lib/cn"; -import { capitalize, formatPct, formatTime, toneText } from "../lib/format"; -import { editableHtmlToMarkdown, getAnnotationSignature, getCaretTextOffset, getSelectionWithin, insertParagraphBreak, insertPlainText, markdownToEditableHtml, normalizeEditableMarkdown, restoreCaretTextOffset } from "../lib/markdown"; -import { agentCatalog, demoCatalysts, demoAlerts, demoRisks, demoEarnings, demoFilings, demoExports, extraHoldings, keyboardShortcuts, modelTabs, screens, workspaceGroups } from "../lib/constants"; -import { ui } from "../lib/styles"; -import type { Alert, Catalyst, EarningsSchedule, Filing, Risk } from "@mosaiciq/contracts/rpc"; -import { useAllAgentEvents } from "../hooks/useServerEvents"; - -type AppData = { - holdings: Holding[]; - activeCompany: Company | null; - agents: Agent[]; - model: { headers: string[]; rows: ModelRow[] }; - memo: RpcResponseMap["memo.get"]; - catalysts: Catalyst[]; - alerts: Alert[]; - risks: Risk[]; - earnings: EarningsSchedule[]; - filings: Filing[]; - exports: ExportRecord[]; -}; - -type AnnotationMode = "highlight" | "comment" | "strike" | "box"; -type Toast = { id: string; type: "success" | "error" | "warning" | "info"; title: string; desc?: string; action?: string }; -type SettingsPanel = "display" | "data-sources" | "agents" | "export" | "keybindings" | "advanced"; -type Theme = "light" | "dark" | "system"; -type Density = "comfortable" | "compact" | "dense"; - -export function App() { - const [activeScreen, setActiveScreen] = useState("home"); - const [settingsOpen, setSettingsOpen] = useState(false); - const [profileOpen, setProfileOpen] = useState(false); - const [agentFullscreenOpen, setAgentFullscreenOpen] = useState(false); - const [agentFullscreenTab, setAgentFullscreenTab] = useState(0); - const [searchOpen, setSearchOpen] = useState(false); - const [searchQuery, setSearchQuery] = useState(""); - const [theme, setTheme] = useState("light"); - const [density, setDensity] = useState("comfortable"); - const [settingsPanel, setSettingsPanel] = useState("display"); - const [data, setData] = useState({ - holdings: [], activeCompany: null, agents: [], model: { headers: [], rows: [] }, - memo: { status: "draft", sections: [], citations: [], annotations: [], sectionReviews: [] }, - catalysts: demoCatalysts, alerts: demoAlerts, risks: demoRisks, earnings: demoEarnings, - filings: demoFilings, exports: demoExports - }); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [toasts, setToasts] = useState([]); - const toastIdRef = useRef(0); - const [selectedAgentId, setSelectedAgentId] = useState(null); - const [agentChatMessages, setAgentChatMessages] = useState>([]); - const [agentChatInput, setAgentChatInput] = useState(""); - - const addToast = useCallback((toast: Omit) => { - const id = String(++toastIdRef.current); - setToasts((t) => [...t, { ...toast, id }]); - const duration = toast.type === "error" || toast.type === "warning" ? 0 : toast.type === "info" ? 6000 : 4000; - if (duration > 0) setTimeout(() => setToasts((t) => t.filter((x) => x.id !== id)), duration); - }, []); - - const removeToast = useCallback((id: string) => setToasts((t) => t.filter((x) => x.id !== id)), []); - - useEffect(() => { - const root = document.documentElement; - root.setAttribute("data-density", density); - if (theme === "dark") root.setAttribute("data-theme", "dark"); - else if (theme === "light") root.removeAttribute("data-theme"); - else { - const mq = window.matchMedia("(prefers-color-scheme: dark)"); - if (mq.matches) root.setAttribute("data-theme", "dark"); - else root.removeAttribute("data-theme"); - } - }, [theme, density]); - - // Load settings from backend on mount - useEffect(() => { - async function loadSettings() { - const result = await rpc.call("settings.get", { scope: "client" }); - if (result.ok) { - const settings = result.data.settings as ClientSettings; - if (settings.theme) setTheme(settings.theme); - if (settings.density) setDensity(settings.density); - } - } - void loadSettings(); - }, []); - - // Persist theme changes - const handleThemeChange = useCallback(async (newTheme: Theme) => { - setTheme(newTheme); - await rpc.call("settings.update", { scope: "client", changes: { theme: newTheme } }); - }, []); - - // Persist density changes - const handleDensityChange = useCallback(async (newDensity: Density) => { - setDensity(newDensity); - await rpc.call("settings.update", { scope: "client", changes: { density: newDensity } }); - }, []); - - useEffect(() => { - let cancelled = false; - async function load() { - const portfolio = await rpc.call("portfolio.get", undefined); - if (!portfolio.ok) { if (!cancelled) { setError(portfolio.error.message); setLoading(false); } return; } - const cid = portfolio.data.activeCompanyId; - const [company, agents, model, memo, catalysts, alerts, risks, earnings, filings, exports] = await Promise.all([ - rpc.call("company.get", { companyId: cid }), - rpc.call("agent.list", { companyId: cid }), - rpc.call("model.get", { companyId: cid, tab: "operating" }), - rpc.call("memo.get", { companyId: cid }), - rpc.call("catalyst.list", { companyId: cid }), - rpc.call("alert.list", { companyId: cid }), - rpc.call("risk.list", { companyId: cid }), - rpc.call("earnings.getSchedule", { companyId: cid }), - rpc.call("filing.list", { companyId: cid }), - rpc.call("export.list", { companyId: cid }) - ]); - if (cancelled) return; - const failed = [company, agents, model, memo, catalysts, alerts, risks, earnings, filings, exports].find((r) => !r.ok); - if (failed && !failed.ok) { setError(failed.error.message); setLoading(false); return; } - setData({ - holdings: portfolio.data.holdings, - activeCompany: company.ok ? company.data.company : null, - agents: agents.ok ? agents.data.agents : [], - model: model.ok ? model.data : { headers: [], rows: [] }, - memo: memo.ok ? memo.data : { status: "draft", sections: [], citations: [], annotations: [], sectionReviews: [] }, - catalysts: catalysts.ok ? catalysts.data.catalysts : demoCatalysts, - alerts: alerts.ok ? alerts.data.alerts : demoAlerts, - risks: risks.ok ? risks.data.risks : demoRisks, - earnings: earnings.ok ? earnings.data.schedule : demoEarnings, - filings: filings.ok ? filings.data.filings : demoFilings, - exports: exports.ok ? exports.data.exports : demoExports - }); - setLoading(false); - } - void load(); - return () => { cancelled = true; }; - }, []); - - const activeAgents = useMemo(() => data.agents.filter((a) => a.status === "running" || a.status === "paused"), [data.agents]); - - // Subscribe to agent events for real-time updates - useAllAgentEvents(useCallback((agentId: string, update: { status?: string; progress?: number; action?: string }) => { - setData((prev) => ({ - ...prev, - agents: prev.agents.map((a) => { - if (a.id === agentId) { - return { ...a, ...update } as typeof a; - } - return a; - }), - })); - }, [])); - - useEffect(() => { - function handleKey(e: KeyboardEvent) { - const mod = e.metaKey || e.ctrlKey; - if (mod && e.key === "k") { e.preventDefault(); setSearchOpen(true); setSearchQuery(""); } - else if (mod && e.key === "f") { e.preventDefault(); setSearchOpen(true); setSearchQuery(""); } - else if (mod && e.key === "e") { e.preventDefault(); addToast({ type: "info", title: "Export quick-action", desc: "Select export type" }); } - else if (mod && e.key === ".") { e.preventDefault(); setSettingsOpen((v) => !v); } - else if (mod && e.key === "\\") { /* nav toggle handled by screens */ } - else if (mod && e.key >= "1" && e.key <= "5") { e.preventDefault(); setActiveScreen(screens[Number(e.key) - 1]); } - else if (e.key === "Escape") { setSearchOpen(false); setAgentFullscreenOpen(false); setSettingsOpen(false); setProfileOpen(false); } - } - window.addEventListener("keydown", handleKey); - return () => window.removeEventListener("keydown", handleKey); - }, [addToast]); - - const searchResults = useMemo(() => { - if (!searchQuery.trim()) return []; - const q = searchQuery.toLowerCase(); - const results: Array<{ type: string; title: string; sub: string; action?: string }> = []; - data.holdings.forEach((h) => { if (h.ticker.toLowerCase().includes(q) || h.name.toLowerCase().includes(q)) results.push({ type: "company", title: h.ticker, sub: h.name }); }); - data.exports.forEach((ex) => { if (ex.title.toLowerCase().includes(q)) results.push({ type: "export", title: ex.title, sub: ex.format }); }); - keyboardShortcuts.forEach(([key, action]) => { if (action.toLowerCase().includes(q)) results.push({ type: "command", title: action, sub: key }); }); - return results; - }, [searchQuery, data.holdings, data.exports]); - - if (loading) return ; - - return ( -
- setProfileOpen(true)} onScreenChange={setActiveScreen} onSettingsOpen={() => setSettingsOpen(true)} searchOpen={searchOpen} searchQuery={searchQuery} onSearchChange={(q) => { setSearchOpen(true); setSearchQuery(q); }} onSearchToggle={() => setSearchOpen((v) => !v)} onSearchClose={() => { setSearchOpen(false); setSearchQuery(""); }} searchResults={searchResults} /> - {error ? ( -
-

RPC Error

-

{error}

- -
- ) : ( -
- {activeScreen === "home" && } - {activeScreen === "workspace" && } - {activeScreen === "model" && } - {activeScreen === "memo" && } - {activeScreen === "agents" && setAgentFullscreenOpen(true)} addToast={addToast} />} -
- )} - { setAgentFullscreenOpen(true); }} onChatSend={async (msg) => { - setAgentChatMessages((m) => [...m, { role: "analyst", text: msg }]); - const res = await rpc.call("agent.chat", { agentId: activeAgents[0]?.id ?? "fm", message: msg }); - if (res.ok) setAgentChatMessages((m) => [...m, { role: "agent", text: res.data.response }]); - }} chatInput={agentChatInput} onChatInputChange={setAgentChatInput} /> - - setSettingsOpen(false)} panel={settingsPanel} onPanelChange={setSettingsPanel} theme={theme} onThemeChange={handleThemeChange} density={density} onDensityChange={handleDensityChange} /> - setProfileOpen(false)} /> - setAgentFullscreenOpen(false)} agents={data.agents} activeTab={agentFullscreenTab} onTabChange={setAgentFullscreenTab} chatMessages={agentChatMessages} chatInput={agentChatInput} onChatInputChange={setAgentChatInput} onChatSend={async (msg) => { - setAgentChatMessages((m) => [...m, { role: "analyst", text: msg }]); - const res = await rpc.call("agent.chat", { agentId: (data.agents[agentFullscreenTab] ?? data.agents[0])?.id ?? "fm", message: msg }); - if (res.ok) setAgentChatMessages((m) => [...m, { role: "agent", text: res.data.response }]); - }} /> -
{title(activeScreen)}
-
- ); -} - -function LoadingShell() { - return ( -
-
- MosaicIQ -
-
-
-
-
-
-
-
-
- {Array.from({ length: 6 }).map((_, i) =>
)} -
-
- {Array.from({ length: 6 }).map((_, i) =>
)} -
-
-
-
- ); -} - -function ToastContainer({ toasts, onDismiss }: { toasts: Toast[]; onDismiss: (id: string) => void }) { - if (!toasts.length) return null; - return ( -
- {toasts.map((t) => ( -
- {t.type === "success" ? "✓" : t.type === "error" ? "!" : t.type === "warning" ? "⚑" : "i"} -

{t.title}

{t.desc &&

{t.desc}

}
- {t.action && } - -
- ))} -
- ); -} - -function Topbar(props: { activeScreen: Screen; company: Company | null; onProfileOpen: () => void; onScreenChange: (s: Screen) => void; onSettingsOpen: () => void; searchOpen: boolean; searchQuery: string; onSearchChange: (q: string) => void; onSearchToggle: () => void; onSearchClose: () => void; searchResults: Array<{ type: string; title: string; sub: string; action?: string }> }) { - return ( -
- - {props.activeScreen !== "home" && props.company ? ( -
- {props.company.ticker} - {props.company.name} - ${props.company.price.toFixed(2)} - {formatPct(props.company.changePct)} -
- ) : null} - -
- props.onSearchChange(e.target.value)} onFocus={() => { if (!props.searchOpen) props.onSearchToggle(); }} aria-label="Search" /> - {props.searchOpen && (props.searchQuery || props.searchResults.length > 0) ? ( -
- {props.searchResults.length === 0 && props.searchQuery ?
No results for "{props.searchQuery}"
: null} - {props.searchResults.map((r, i) => ( - - ))} -
- ) : null} -
- - -
- ); -} - -function Home(props: { holdings: Holding[]; agents: Agent[]; alerts: Alert[]; onScreenChange: (s: Screen) => void; addToast: (t: Omit) => void }) { - const holdings = [...props.holdings, ...extraHoldings]; - return ( -
-
-

Morning Briefing

Good morning, JD

- -
-
- -
- {holdings.map((h) => ( - - ))} -
-
- - -
NovDecJanFebMarApr
-
- -
-
- -
-
-
-
-
- {[["Value", 62, "positive", "+0.32"], ["Size", 40, "negative", "-0.48"], ["Momentum", 35, "positive", "+0.18"], ["Quality", 72, "positive", "+0.41"], ["Low Vol", 55, "positive", "+0.27"], ["Mkt Beta", 48, "positive", "+0.15"]].map(([l, w, t, v]) => ( -
- {l}
- {v} -
- ))} -
-
-
-
- - {demoEarnings.slice(0, 3).map((e) => )} - - - {props.alerts.slice(0, 3).map((a) => )} - - - - - - - - {props.agents.map((a) => )} - {props.agents.length === 0 && } - -
-
- ); -} - -function Workspace(props: { company: Company | null; onScreenChange: (s: Screen) => void; catalysts: Catalyst[]; alerts: Alert[]; risks: Risk[]; earnings: EarningsSchedule[]; filings: Filing[]; exports: ExportRecord[] }) { - const [activeSection, setActiveSection] = useState("Company Snapshot"); - return ( -
- -
- {activeSection === "Catalyst Tracker" ? : - activeSection === "Thesis Alerts" ? : - activeSection === "Risks & Mitigants" ? : - activeSection === "Earnings Monitor" ? : - activeSection === "Filing Watch" ? : - activeSection === "Export Center" ? : - } -
-
- ); -} - -function WorkspaceMain(props: { company: Company | null; onScreenChange: (s: Screen) => void }) { - return <> -

{props.company?.ticker ?? "COST"} · Nasdaq · {props.company?.sector ?? "Consumer Staples"} · {props.company?.subIndustry ?? "Membership Warehouse Clubs"}

-

{props.company?.name ?? "Costco Wholesale Corporation"}

-

Founded {props.company?.founded ?? "1983"} · {props.company?.headquarters ?? "Issaquah, WA"} · ~{(props.company?.employees ?? 320000).toLocaleString()} employees

-
- {[["Market Cap", "$408.7B"], ["EV", "$401.2B"], ["Price", "$921.40"], ["P/E (TTM)", "54.8x"], ["EV/EBITDA", "31.2x"], ["Dividend Yield", "0.5%"], ["FY24 Revenue", "$254.5B"], ["Gross Margin", "12.6%"], ["Operating Margin", "3.6%"], ["Net Income", "$7.4B"], ["Membership Fee Inc.", "$4.8B"], ["Renewal Rate", "93.0%"]].map(([l, v]) => )} -
-

Business Summary

-

{props.company?.thesis}

-

Scale-driven purchasing power, a curated SKU base, and fee income create a structure where the company can price aggressively while preserving recurring profitability.

-

Segment Revenue Breakdown

- -

Reports & Analysis

- -

Source Materials

- - - - ; -} - -function CatalystSection({ catalysts }: { catalysts: Catalyst[] }) { - return <> -

Analysis · Catalyst Tracker

-

Catalyst Tracker

- - DateEventImpactThesisSource - {catalysts.map((c) => ( - - {c.date} - {c.event} - {capitalize(c.impact)} - {c.thesisRelevance} - {c.source} - - ))} -
- ; -} - -function AlertsSection({ alerts }: { alerts: Alert[] }) { - return <> -

Monitoring · Thesis Alerts

-

Thesis Alerts

-
- {alerts.map((a) => ( -
- {a.timestamp.slice(11, 16)} - {a.description} - {a.thesisImpact === "positive" ? "Thesis +" : a.thesisImpact === "negative" ? "Thesis −" : "Neutral"} - - {capitalize(a.status)} -
- ))} -
- ; -} - -function RiskSection({ risks }: { risks: Risk[] }) { - return <> -

Analysis · Risk Register

-

Risk Register

- - RiskCategorySeverityLikelihoodMitigationStatus - {risks.map((r) => ( - - {r.risk} - {capitalize(r.category)} - {capitalize(r.severity)} - {capitalize(r.likelihood)} - {r.mitigation} - {capitalize(r.status)} - - ))} -
- ; -} - -function EarningsSection({ earnings }: { earnings: EarningsSchedule[] }) { - return <> -

Monitoring · Earnings Monitor

-

Earnings Monitor

-
- {earnings.slice(0, 2).map((e) =>
Next Report{e.expectedDate}{e.quarter} · {e.timing?.toUpperCase()}
)} -
-

Consensus Estimates

- - MetricQ2 FY25AQ3 FY25EFY25E - - Revenue$62.5B$63.8B$242.3B - EPS$4.28$4.35$16.82 - -
- ; -} - -function FilingSection({ filings }: { filings: Filing[] }) { - return <> -

Monitoring · Filing Watch

-

Filing Watch

-
- {filings.map((f) => ( -
-
{f.formType}{f.filedDate}
-
{f.title}
- {f.keyChanges &&
Key changes: {f.keyChanges}
} - -
- ))} -
- ; -} - -function ExportSection({ exports }: { exports: ExportRecord[] }) { - return <> -

Library · Export Center

-

Export Center

-
- {exports.map((ex) => ( -
- {ex.format} - {ex.title} - {ex.fileSize} · {capitalize(ex.status)} - {ex.status === "complete" && } -
- ))} -
- ; -} - -function Model({ headers, rows, addToast }: { headers: string[]; rows: ModelRow[]; addToast: (t: Omit) => void }) { - const modelHeaders = [...headers, "FY2027E"].filter(Boolean); - return ( -
-
- - Revenue Build ($M) -
- - - -
-
- - Line Item{modelHeaders.map((h, i) => = 3}>{h})} - {expandedModelRows(rows).map((row) => ( - - {row.label} - {row.values.map((v, i) => = 3} total={row.kind === "total"}>{v})} - - ))} -
-
-
- ); -} - -function Memo({ memo, company }: { memo: RpcResponseMap["memo.get"]; company: Company | null }) { - const [reviewMode, setReviewMode] = useState(false); - const [annotationMode, setAnnotationMode] = useState(null); - const [activeSectionId, setActiveSectionId] = useState(memo.sections[0]?.id ?? ""); - const [outlineCollapsed, setOutlineCollapsed] = useState(false); - const [rightPanelCollapsed, setRightPanelCollapsed] = useState(false); - const [sectionsDraft, setSectionsDraft] = useState(memo.sections); - const [annotations, setAnnotations] = useState(memo.annotations); - const [sectionReviews, setSectionReviews] = useState(memo.sectionReviews); - const [pendingAnnotation, setPendingAnnotation] = useState<{ sectionId: string; selectedText: string } | null>(null); - const [commentDraft, setCommentDraft] = useState(""); - const [saveStateBySection, setSaveStateBySection] = useState>({}); - const [lastSavedAt, setLastSavedAt] = useState(null); - const [saveError, setSaveError] = useState(null); - const saveTimers = useRef>({}); - const sections = sectionsDraft; - const activeIndex = Math.max(0, sections.findIndex((s) => s.id === activeSectionId)); - const activeSection = sections[activeIndex]; - const activeReview = sectionReviews.find((r) => r.sectionId === activeSectionId); - const allApproved = sections.length > 0 && sections.every((s) => sectionReviews.find((r) => r.sectionId === s.id)?.status === "approved"); - const hasUnsaved = Object.values(saveStateBySection).some((s) => s === "dirty" || s === "saving" || s === "failed"); - const publishDisabled = hasUnsaved || !allApproved; - const publishBlockedReason = hasUnsaved ? "Blocked by unsaved changes" : !allApproved ? "Blocked by unapproved sections" : "Ready to publish"; - const activeSaveState = saveStateBySection[activeSectionId] ?? "saved"; - const saveLabel = activeSaveState === "saving" ? "Saving..." : activeSaveState === "dirty" ? "Unsaved changes" : activeSaveState === "failed" ? "Save failed" : lastSavedAt ? `Saved ${formatTime(lastSavedAt)}` : "Saved"; - - useEffect(() => { setSectionsDraft(memo.sections); setAnnotations(memo.annotations); setSectionReviews(memo.sectionReviews); setSaveStateBySection({}); setSaveError(null); setPendingAnnotation(null); setCommentDraft(""); setActiveSectionId((c) => memo.sections.some((s) => s.id === c) ? c : memo.sections[0]?.id || ""); }, [memo]); - useEffect(() => { return () => { Object.values(saveTimers.current).forEach(clearTimeout); }; }, []); - - function updateSectionDraft(sectionId: string, patch: Pick | Pick) { - setSectionsDraft((d) => d.map((s) => s.id === sectionId ? { ...s, ...patch } : s)); - setActiveSectionId(sectionId); - setSaveStateBySection((s) => ({ ...s, [sectionId]: "dirty" })); - setSaveError(null); - clearTimeout(saveTimers.current[sectionId]); - saveTimers.current[sectionId] = window.setTimeout(() => { const section = sectionsDraftRef.current.find((c) => c.id === sectionId); if (section) void saveSection(section); }, 2000); - } - - const sectionsDraftRef = useRef(sectionsDraft); - useEffect(() => { sectionsDraftRef.current = sectionsDraft; }, [sectionsDraft]); - - async function saveSection(section: MemoSection) { - if (!company) return; - if (section.content.trim().length === 0) { setSaveStateBySection((s) => ({ ...s, [section.id]: "failed" })); setSaveError("Memo section content cannot be empty."); return; } - if (section.title.trim().length === 0) { setSaveStateBySection((s) => ({ ...s, [section.id]: "failed" })); setSaveError("Memo section title cannot be empty."); return; } - setSaveStateBySection((s) => ({ ...s, [section.id]: "saving" })); - const result = await rpc.call("memo.updateSection", { companyId: company.id, sectionId: section.id, title: section.title, content: section.content }); - if (!result.ok) { setSaveStateBySection((s) => ({ ...s, [section.id]: "failed" })); setSaveError(result.error.message); return; } - const latest = sectionsDraftRef.current.find((d) => d.id === section.id); - if (!latest || latest.title !== section.title || latest.content !== section.content) return; - setSectionsDraft((d) => d.map((s) => s.id === section.id ? result.data.section : s)); - setSaveStateBySection((s) => ({ ...s, [section.id]: "saved" })); - setLastSavedAt(result.data.savedAt); - setSaveError(null); - } - - function activateSection(sectionId: string, scroll = false) { setActiveSectionId(sectionId); if (scroll) document.getElementById(`memo-${sectionId}`)?.scrollIntoView({ behavior: "smooth", block: "start" }); } - - async function createAnnotation(sectionId: string, selectedText: string, kind = annotationMode) { - if (!company || kind === null || kind === "box") return; - const norm = selectedText.trim(); if (!norm) return; - setActiveSectionId(sectionId); - const existing = annotations.find((a) => a.status === "open" && a.sectionId === sectionId && a.kind === kind && a.selectedText === norm); - if (existing) { await resolveAnnotation(existing.id); return; } - if (kind === "comment") { setPendingAnnotation({ sectionId, selectedText: norm }); setCommentDraft(""); return; } - const result = await rpc.call("memo.addAnnotation", { companyId: company.id, sectionId, kind, selectedText: norm }); - if (result.ok) setAnnotations((c) => [result.data.annotation, ...c]); else setSaveError(result.error.message); - } - - async function savePendingComment() { - if (!company || !pendingAnnotation) return; - const result = await rpc.call("memo.addAnnotation", { companyId: company.id, sectionId: pendingAnnotation.sectionId, kind: "comment", selectedText: pendingAnnotation.selectedText, comment: commentDraft }); - if (result.ok) { setAnnotations((c) => [result.data.annotation, ...c]); setPendingAnnotation(null); setCommentDraft(""); } else setSaveError(result.error.message); - } - - async function resolveAnnotation(annotationId: string) { - if (!company) return; - const result = await rpc.call("memo.resolveAnnotation", { companyId: company.id, annotationId }); - if (result.ok) setAnnotations((c) => c.map((a) => a.id === annotationId ? result.data.annotation : a)); else setSaveError(result.error.message); - } - - async function updateSectionReview(status: MemoSectionReview["status"]) { - if (!company || !activeSection) return; - const result = await rpc.call("memo.updateSectionReview", { companyId: company.id, sectionId: activeSection.id, status }); - if (result.ok) setSectionReviews((c) => { const e = c.some((r) => r.sectionId === result.data.review.sectionId); return e ? c.map((r) => r.sectionId === result.data.review.sectionId ? result.data.review : r) : [...c, result.data.review]; }); else setSaveError(result.error.message); - } - - const activeCitations = memo.citations.filter((c) => c.sectionId === activeSectionId); - const otherCitations = memo.citations.filter((c) => c.sectionId !== activeSectionId); - const openAnnotations = annotations.filter((a) => a.status === "open"); - const memoGridColumns = cx(outlineCollapsed && rightPanelCollapsed && "grid-cols-[36px_minmax(0,1fr)]", outlineCollapsed && !rightPanelCollapsed && "grid-cols-[36px_minmax(0,1fr)_300px]", !outlineCollapsed && rightPanelCollapsed && "grid-cols-[220px_minmax(0,1fr)]", !outlineCollapsed && !rightPanelCollapsed && "grid-cols-[220px_minmax(0,1fr)_300px]"); - - return ( -
- -
-
- {title(memo.status)} - {saveLabel} - {saveError && {saveError}} - - - - - {publishBlockedReason} - -
-
-

{company?.ticker ?? "COST"} · Nasdaq · {company?.sector ?? "Consumer Staples"}

-

{company?.ticker ?? "COST"} Investment Memo

-

Prepared by JD · May 2026 · Confidential

-
- {sections.map((section, index) => ( -
activateSection(section.id)}> -

updateSectionDraft(section.id, { title: e.currentTarget.textContent ?? "" })} onBlur={(e) => { if (!(e.currentTarget.textContent ?? "").trim()) { e.currentTarget.textContent = section.title; setSaveStateBySection((s) => ({ ...s, [section.id]: "failed" })); setSaveError("Memo section title cannot be empty."); } }} onFocus={() => activateSection(section.id)} suppressContentEditableWarning>{section.title}

- a.sectionId === section.id && a.status === "open")} annotationMode={reviewMode ? annotationMode : null} onActivate={() => activateSection(section.id)} onAnnotate={(text) => void createAnnotation(section.id, text)} onChange={(content) => updateSectionDraft(section.id, { content })} section={section} /> -

[{index + 1}]

- {index === 0 &&
AI suggestion: tighten the fee-increase sentence and quantify renewal-cycle timing.
} - {reviewMode && index === activeIndex &&
{(["highlight", "comment", "strike", "box"] as const).map((m) => ())}
} -
- ))} -
- {rightPanelCollapsed ? : ( -