From 7f9880c09719b1484ae2ed7598153fb00455c782 Mon Sep 17 00:00:00 2001 From: Cooper Maruyama Date: Fri, 29 May 2026 15:08:33 -0700 Subject: [PATCH] refactor(frontend): move backend mirrors to viewmodel --- .../03-evolutions-origin-branch/up.sql | 3 - .../widget/controls/bootstrap-config.tsx | 5 +- .../widget/controls/build-head-button.tsx | 4 +- .../components/widget/evolve-flow.stories.tsx | 21 ++- .../widget/history/analyze-history-button.tsx | 3 +- .../history/discard-uncommitted-dialog.tsx | 5 +- .../history/history-restore-item-button.tsx | 4 +- .../widget/layout/debug-overlay.tsx | 7 +- .../widget/layout/git-status-debug.tsx | 4 +- .../src/components/widget/layout/header.tsx | 6 +- .../widget/layout/merge-section.tsx | 3 +- .../external-build-detected.stories.tsx | 12 +- .../notifications/external-build-detected.tsx | 4 +- .../uncommitted-changes-detected.tsx | 3 +- .../unsummarized-changes-detected.tsx | 3 +- .../promptinput/begin-evolve-warning.tsx | 5 +- .../widget/promptinput/homebrew-badge.tsx | 3 +- .../widget/promptinput/prompt-input.tsx | 5 +- .../promptinput/system-defaults-cta.tsx | 12 +- .../components/widget/steps/evolve-step.tsx | 3 +- .../components/widget/steps/history-step.tsx | 4 +- apps/native/src/components/widget/utils.ts | 19 ++- .../src/components/widget/widget.stories.tsx | 6 +- .../src/components/widget/widget.test.tsx | 24 ++-- apps/native/src/components/widget/widget.tsx | 23 ++- apps/native/src/hooks/use-apply.ts | 15 +- apps/native/src/hooks/use-darwin-config.ts | 6 +- apps/native/src/hooks/use-evolve.ts | 19 +-- apps/native/src/hooks/use-git-operations.ts | 12 +- apps/native/src/hooks/use-history-card.ts | 4 +- apps/native/src/hooks/use-history-restore.ts | 7 +- apps/native/src/hooks/use-history.ts | 9 +- apps/native/src/hooks/use-homebrew-diff.ts | 9 +- apps/native/src/hooks/use-queue-summarizer.ts | 47 ------- apps/native/src/hooks/use-rollback.ts | 13 +- apps/native/src/hooks/use-summary.ts | 10 +- apps/native/src/hooks/use-watcher.ts | 67 --------- .../src/hooks/use-widget-initialization.ts | 3 +- .../src/stores/__mocks__/widget-store.ts | 4 +- apps/native/src/stores/ui-state.test.ts | 58 ++++++++ apps/native/src/stores/ui-state.ts | 133 ++++++++++++++++++ apps/native/src/stores/view-model.ts | 103 ++++++++++++++ apps/native/src/stores/widget-store.impl.ts | 55 +------- apps/native/src/stores/widget-store.test.ts | 11 +- apps/native/src/utils/widget-test-helpers.ts | 8 +- apps/native/src/viewmodel/change-map.ts | 17 +++ apps/native/src/viewmodel/evolve.ts | 15 ++ apps/native/src/viewmodel/git.ts | 36 +++++ apps/native/src/viewmodel/history.ts | 11 ++ apps/native/src/viewmodel/index.ts | 35 +++++ apps/native/src/viewmodel/viewmodel.test.ts | 124 ++++++++++++++++ 51 files changed, 728 insertions(+), 294 deletions(-) delete mode 100644 apps/native/src-tauri/migrations/03-evolutions-origin-branch/up.sql delete mode 100644 apps/native/src/hooks/use-queue-summarizer.ts delete mode 100644 apps/native/src/hooks/use-watcher.ts create mode 100644 apps/native/src/stores/ui-state.test.ts create mode 100644 apps/native/src/stores/ui-state.ts create mode 100644 apps/native/src/stores/view-model.ts create mode 100644 apps/native/src/viewmodel/change-map.ts create mode 100644 apps/native/src/viewmodel/evolve.ts create mode 100644 apps/native/src/viewmodel/git.ts create mode 100644 apps/native/src/viewmodel/history.ts create mode 100644 apps/native/src/viewmodel/index.ts create mode 100644 apps/native/src/viewmodel/viewmodel.test.ts diff --git a/apps/native/src-tauri/migrations/03-evolutions-origin-branch/up.sql b/apps/native/src-tauri/migrations/03-evolutions-origin-branch/up.sql deleted file mode 100644 index ef5999d13..000000000 --- a/apps/native/src-tauri/migrations/03-evolutions-origin-branch/up.sql +++ /dev/null @@ -1,3 +0,0 @@ --- Legacy databases created before rusqlite_migration used `evolutions.branch`. --- The Rust hook for this migration performs the conditional column repair. -SELECT 1; diff --git a/apps/native/src/components/widget/controls/bootstrap-config.tsx b/apps/native/src/components/widget/controls/bootstrap-config.tsx index f6c8a4ad7..6a961369a 100644 --- a/apps/native/src/components/widget/controls/bootstrap-config.tsx +++ b/apps/native/src/components/widget/controls/bootstrap-config.tsx @@ -3,6 +3,7 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { useDarwinConfig } from "@/hooks/use-darwin-config"; +import { useViewModel } from "@/stores/view-model"; import { useWidgetStore } from "@/stores/widget-store"; import { tauriAPI } from "@/ipc/api"; import { AlertCircle, GitCommit, Sparkles } from "lucide-react"; @@ -18,7 +19,7 @@ export function BootstrapConfig({ label, onSuccess }: BootstrapConfigProps) { const [localError, setLocalError] = useState(null); const { bootstrap, isBootstrapping } = useDarwinConfig(); const configDir = useWidgetStore((state) => state.configDir); - const gitStatus = useWidgetStore((state) => state.gitStatus); + const gitStatus = useViewModel((state) => state.git); const [flakeExists, setFlakeExists] = useState(false); useEffect(() => { @@ -111,4 +112,4 @@ export function BootstrapConfig({ label, onSuccess }: BootstrapConfigProps) { ); -} \ No newline at end of file +} diff --git a/apps/native/src/components/widget/controls/build-head-button.tsx b/apps/native/src/components/widget/controls/build-head-button.tsx index 16871d888..38803a5f3 100644 --- a/apps/native/src/components/widget/controls/build-head-button.tsx +++ b/apps/native/src/components/widget/controls/build-head-button.tsx @@ -1,7 +1,7 @@ import { Wrench } from "lucide-react"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; -import { useWidgetStore } from "@/stores/widget-store"; +import { useViewModel } from "@/stores/view-model"; import { useApply } from "@/hooks/use-apply"; interface BuildHeadButtonProps { @@ -9,7 +9,7 @@ interface BuildHeadButtonProps { } export function BuildHeadButton({ isRestoring = false }: BuildHeadButtonProps) { - const uncommittedChanges = useWidgetStore((s) => (s.gitStatus?.files?.length ?? 0) > 0); + const uncommittedChanges = useViewModel((s) => (s.git?.files?.length ?? 0) > 0); const { handleHistoryBuild } = useApply(); return ( diff --git a/apps/native/src/components/widget/evolve-flow.stories.tsx b/apps/native/src/components/widget/evolve-flow.stories.tsx index bd99f062e..312d1a427 100644 --- a/apps/native/src/components/widget/evolve-flow.stories.tsx +++ b/apps/native/src/components/widget/evolve-flow.stories.tsx @@ -1,5 +1,6 @@ // @ts-nocheck - Storybook 10 alpha types have inference issues (resolves to `never`) import preview from "#storybook/preview"; +import { useViewModel } from "@/stores/view-model"; import { useWidgetStore } from "@/stores/widget-store"; import type { EvolveEvent } from "@/stores/widget-store"; import type { SemanticChangeMap, EvolveState, GitStatus, Change } from "@/ipc/types"; @@ -158,7 +159,13 @@ const mockEvolveEvents: EvolveEvent[] = [ function WidgetWithState({ storeState }: { storeState: Record }) { useEffect(() => { - useWidgetStore.setState(storeState); + const { evolveState, gitStatus, changeMap, ...widgetState } = storeState; + useWidgetStore.setState(widgetState); + useViewModel.setState({ + evolve: (evolveState as EvolveState | undefined) ?? null, + git: (gitStatus as GitStatus | undefined) ?? null, + changeMap: (changeMap as SemanticChangeMap | undefined) ?? null, + }); }, [storeState]); return ; @@ -168,8 +175,8 @@ function AnimatedEvolveFlow() { const timeoutsRef = useRef[]>([]); useEffect(() => { + useViewModel.setState({ evolve: evolveStateBegin }); useWidgetStore.setState({ - evolveState: evolveStateBegin, evolvePrompt: "Add system monitoring tools like htop and btop", }); @@ -195,19 +202,21 @@ function AnimatedEvolveFlow() { // Phase 2: Evolution complete -> show review step with changes const completionTime = 800 + mockEvolveEvents[mockEvolveEvents.length - 1].timestampMs + 1500; const t2 = setTimeout(() => { + useViewModel.setState({ + evolve: evolveStateEvolve, + git: mockGitStatus, + changeMap: mockChangeMap, + }); useWidgetStore.setState({ isGenerating: false, - evolveState: evolveStateEvolve, - gitStatus: mockGitStatus, - changeMap: mockChangeMap, }); }, completionTime); timeoutsRef.current.push(t2); // Phase 3: Transition to merge step const t3 = setTimeout(() => { + useViewModel.setState({ evolve: evolveStateMerge }); useWidgetStore.setState({ - evolveState: evolveStateMerge, commitMessageSuggestion: "feat: add system monitoring tools (htop, btop, bottom, bandwhich, procs)", }); }, completionTime + 5000); diff --git a/apps/native/src/components/widget/history/analyze-history-button.tsx b/apps/native/src/components/widget/history/analyze-history-button.tsx index 339fc9100..7bd491653 100644 --- a/apps/native/src/components/widget/history/analyze-history-button.tsx +++ b/apps/native/src/components/widget/history/analyze-history-button.tsx @@ -1,12 +1,13 @@ import { Button } from "@/components/ui/button"; import { AnalyzeButton } from "@/components/widget/summaries/analyze-button"; import { useHistory } from "@/hooks/use-history"; +import { useViewModel } from "@/stores/view-model"; import { useWidgetStore } from "@/stores/widget-store"; import { Dna, Square } from "lucide-react"; // Generates commit (if need be) and summary metadata for history items that are missing it. export function AnalyzeHistoryButton() { - const history = useWidgetStore((state) => state.history); + const history = useViewModel((state) => state.history); const analyzingSize = useWidgetStore( (state) => state.analyzingHistoryForHashes.size, ); diff --git a/apps/native/src/components/widget/history/discard-uncommitted-dialog.tsx b/apps/native/src/components/widget/history/discard-uncommitted-dialog.tsx index 7d864d8af..d9c0b6a94 100644 --- a/apps/native/src/components/widget/history/discard-uncommitted-dialog.tsx +++ b/apps/native/src/components/widget/history/discard-uncommitted-dialog.tsx @@ -8,6 +8,7 @@ import { } from "@/components/ui/dialog"; import { ConfigDirBadge } from "@/components/widget/badges/config-dir-badge"; import { useRollback } from "@/hooks/use-rollback"; +import { useViewModel } from "@/stores/view-model"; import { useWidgetStore } from "@/stores/widget-store"; import { toast } from "sonner"; @@ -17,14 +18,14 @@ interface DiscardUncommittedDialogProps { } export function DiscardUncommittedDialog({ open, onOpenChange }: DiscardUncommittedDialogProps) { - const gitStatus = useWidgetStore((s) => s.gitStatus); + const gitStatus = useViewModel((s) => s.git); const configDir = useWidgetStore((s) => s.configDir); const files = gitStatus?.files ?? []; const { handleRollback } = useRollback(); const handleDiscard = async () => { await handleRollback(); - const remaining = useWidgetStore.getState().gitStatus?.files?.length ?? 1; + const remaining = useViewModel.getState().git?.files?.length ?? 1; if (remaining === 0) { toast.success("Changes discarded"); onOpenChange(false); diff --git a/apps/native/src/components/widget/history/history-restore-item-button.tsx b/apps/native/src/components/widget/history/history-restore-item-button.tsx index b7a89f986..b135030a4 100644 --- a/apps/native/src/components/widget/history/history-restore-item-button.tsx +++ b/apps/native/src/components/widget/history/history-restore-item-button.tsx @@ -1,7 +1,7 @@ import { Loader2, RotateCcw } from "lucide-react"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; -import { useWidgetStore } from "@/stores/widget-store"; +import { useViewModel } from "@/stores/view-model"; interface HistoryRestoreItemButtonProps { hash: string; @@ -14,7 +14,7 @@ export function HistoryRestoreItemButton({ isRestoring = false, onRequestRestore, }: HistoryRestoreItemButtonProps) { - const uncommittedChanges = useWidgetStore((s) => (s.gitStatus?.files?.length ?? 0) > 0); + const uncommittedChanges = useViewModel((s) => (s.git?.files?.length ?? 0) > 0); return (