From ef43597bc383bef97f04b824d0e256969b07d700 Mon Sep 17 00:00:00 2001 From: CasLinden Date: Fri, 29 May 2026 14:33:05 +0900 Subject: [PATCH 1/7] chore(store): move types and re-exports out of widget store --- .../widget/controls/confirm-button.tsx | 3 +- .../components/widget/evolve-flow.stories.tsx | 9 +- .../external-build-detected.stories.tsx | 2 +- .../widget/overlays/evolve-progress.tsx | 2 +- .../rebuild-overlay-panel.stories.tsx | 7 +- .../widget/overlays/rebuild-overlay-panel.tsx | 3 +- .../widget/settings/settings-dialog.tsx | 3 +- apps/native/src/components/widget/utils.ts | 2 +- .../src/components/widget/widget.stories.tsx | 3 +- apps/native/src/hooks/use-prefs.ts | 3 +- apps/native/src/hooks/use-rebuild-stream.ts | 3 +- .../src/stores/__mocks__/widget-store.ts | 29 +- apps/native/src/stores/widget-store.impl.ts | 282 +----------------- apps/native/src/stores/widget-store.test.ts | 7 +- apps/native/src/types/ui.ts | 275 +++++++++++++++++ 15 files changed, 311 insertions(+), 322 deletions(-) create mode 100644 apps/native/src/types/ui.ts diff --git a/apps/native/src/components/widget/controls/confirm-button.tsx b/apps/native/src/components/widget/controls/confirm-button.tsx index 9af723188..2e4bff88e 100644 --- a/apps/native/src/components/widget/controls/confirm-button.tsx +++ b/apps/native/src/components/widget/controls/confirm-button.tsx @@ -4,7 +4,8 @@ import { Button } from "@/components/ui/button"; import { CheckConfirmationOff } from "@/components/widget/controls/check-confirmation-off"; import { ConfirmationDialog } from "@/components/widget/controls/confirmation-dialog"; import { usePrefs } from "@/hooks/use-prefs"; -import { useWidgetStore, type ConfirmPrefKey } from "@/stores/widget-store"; +import { useWidgetStore } from "@/stores/widget-store"; +import type { ConfirmPrefKey } from "@/types/ui"; import type { ComponentProps } from "react"; import { useState } from "react"; diff --git a/apps/native/src/components/widget/evolve-flow.stories.tsx b/apps/native/src/components/widget/evolve-flow.stories.tsx index bd99f062e..eb93e4c49 100644 --- a/apps/native/src/components/widget/evolve-flow.stories.tsx +++ b/apps/native/src/components/widget/evolve-flow.stories.tsx @@ -1,8 +1,13 @@ // @ts-nocheck - Storybook 10 alpha types have inference issues (resolves to `never`) import preview from "#storybook/preview"; import { useWidgetStore } from "@/stores/widget-store"; -import type { EvolveEvent } from "@/stores/widget-store"; -import type { SemanticChangeMap, EvolveState, GitStatus, Change } from "@/ipc/types"; +import type { + Change, + EvolveEvent, + EvolveState, + GitStatus, + SemanticChangeMap, +} from "@/ipc/types"; import { useEffect, useRef } from "react"; import { DarwinWidget } from "./widget"; diff --git a/apps/native/src/components/widget/notifications/external-build-detected.stories.tsx b/apps/native/src/components/widget/notifications/external-build-detected.stories.tsx index 91490b18e..f7c0c0e7e 100644 --- a/apps/native/src/components/widget/notifications/external-build-detected.stories.tsx +++ b/apps/native/src/components/widget/notifications/external-build-detected.stories.tsx @@ -1,6 +1,6 @@ // @ts-nocheck - Storybook 10 alpha types have inference issues (resolves to `never`) import preview from "#storybook/preview"; -import type { EvolveState } from "@/stores/widget-store"; +import type { EvolveState } from "@/ipc/types"; import { useWidgetStore } from "@/stores/widget-store"; import { fn } from "storybook/test"; import { useEffect } from "react"; diff --git a/apps/native/src/components/widget/overlays/evolve-progress.tsx b/apps/native/src/components/widget/overlays/evolve-progress.tsx index c0f7b8e33..fd672a5f8 100644 --- a/apps/native/src/components/widget/overlays/evolve-progress.tsx +++ b/apps/native/src/components/widget/overlays/evolve-progress.tsx @@ -23,7 +23,7 @@ import { import { useEffect, useRef, useState } from "react"; import { cn } from "@/lib/utils"; import { tauriAPI } from "@/ipc/api"; -import type { EvolveEvent, EvolveEventType } from "@/stores/widget-store"; +import type { EvolveEvent, EvolveEventType } from "@/ipc/types"; // ============================================================================= // Types diff --git a/apps/native/src/components/widget/overlays/rebuild-overlay-panel.stories.tsx b/apps/native/src/components/widget/overlays/rebuild-overlay-panel.stories.tsx index 3403829bf..cde8b8749 100644 --- a/apps/native/src/components/widget/overlays/rebuild-overlay-panel.stories.tsx +++ b/apps/native/src/components/widget/overlays/rebuild-overlay-panel.stories.tsx @@ -1,11 +1,8 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import React, { useEffect } from "react"; import { RebuildOverlayPanel } from "@/components/widget/overlays/rebuild-overlay-panel"; -import { - useWidgetStore, - type RebuildLine, - type RebuildState, -} from "@/stores/widget-store"; +import { useWidgetStore } from "@/stores/widget-store"; +import type { RebuildLine, RebuildState } from "@/types/ui"; /** * Decorator that sets the widget store's rebuild state before rendering. diff --git a/apps/native/src/components/widget/overlays/rebuild-overlay-panel.tsx b/apps/native/src/components/widget/overlays/rebuild-overlay-panel.tsx index 8390bd326..0105043fd 100644 --- a/apps/native/src/components/widget/overlays/rebuild-overlay-panel.tsx +++ b/apps/native/src/components/widget/overlays/rebuild-overlay-panel.tsx @@ -2,7 +2,8 @@ import { Button } from "@/components/ui/button"; import { useRebuildStream } from "@/hooks/use-rebuild-stream"; import { useRollback } from "@/hooks/use-rollback"; import { cn } from "@/lib/utils"; -import { useWidgetStore, type RebuildErrorType, type RebuildLine } from "@/stores/widget-store"; +import { useWidgetStore } from "@/stores/widget-store"; +import type { RebuildErrorType, RebuildLine } from "@/types/ui"; import { AlertTriangle, Brain, diff --git a/apps/native/src/components/widget/settings/settings-dialog.tsx b/apps/native/src/components/widget/settings/settings-dialog.tsx index e0b06abf2..fdfd9b243 100644 --- a/apps/native/src/components/widget/settings/settings-dialog.tsx +++ b/apps/native/src/components/widget/settings/settings-dialog.tsx @@ -1,7 +1,8 @@ import { Button } from "@/components/ui/button"; import { useDarwinConfig } from "@/hooks/use-darwin-config"; import { cn } from "@/lib/utils"; -import { type SettingsTab, useWidgetStore } from "@/stores/widget-store"; +import { useWidgetStore } from "@/stores/widget-store"; +import type { SettingsTab } from "@/types/ui"; import { DEFAULT_MAX_ITERATIONS } from "@/lib/constants"; import { tauriAPI } from "@/ipc/api"; import { useForm } from "@tanstack/react-form"; diff --git a/apps/native/src/components/widget/utils.ts b/apps/native/src/components/widget/utils.ts index 001261b27..fc9c851e6 100644 --- a/apps/native/src/components/widget/utils.ts +++ b/apps/native/src/components/widget/utils.ts @@ -1,5 +1,5 @@ import { filesystemViewEnabled } from "@/lib/flags"; -import type { WidgetState, WidgetStep } from "@/stores/widget-store"; +import type { WidgetState, WidgetStep } from "@/types/ui"; import { FilePen, FilePlus, FileX, FileCode, type LucideIcon } from "lucide-react"; export function computeCurrentStep(state: WidgetState): WidgetStep { diff --git a/apps/native/src/components/widget/widget.stories.tsx b/apps/native/src/components/widget/widget.stories.tsx index 4333bed0d..374ddb862 100644 --- a/apps/native/src/components/widget/widget.stories.tsx +++ b/apps/native/src/components/widget/widget.stories.tsx @@ -1,8 +1,7 @@ // @ts-nocheck - Storybook 10 alpha types have inference issues (resolves to `never`) import preview from "#storybook/preview"; -import type { EvolveEvent, GitStatus } from "@/stores/widget-store"; import { useWidgetStore } from "@/stores/widget-store"; -import type { SemanticChangeMap } from "@/ipc/types"; +import type { EvolveEvent, GitStatus, SemanticChangeMap } from "@/ipc/types"; import type React from "react"; import { useEffect } from "react"; import { DarwinWidget } from "./widget"; diff --git a/apps/native/src/hooks/use-prefs.ts b/apps/native/src/hooks/use-prefs.ts index 1e51d7e69..e0723c5d3 100644 --- a/apps/native/src/hooks/use-prefs.ts +++ b/apps/native/src/hooks/use-prefs.ts @@ -1,4 +1,5 @@ -import { useWidgetStore, type BoolPrefKey } from "@/stores/widget-store"; +import { useWidgetStore } from "@/stores/widget-store"; +import type { BoolPrefKey } from "@/types/ui"; import { tauriAPI } from "@/ipc/api"; export function usePrefs() { diff --git a/apps/native/src/hooks/use-rebuild-stream.ts b/apps/native/src/hooks/use-rebuild-stream.ts index c755836b3..292d4055c 100644 --- a/apps/native/src/hooks/use-rebuild-stream.ts +++ b/apps/native/src/hooks/use-rebuild-stream.ts @@ -1,4 +1,5 @@ -import { useWidgetStore, type RebuildContext } from "@/stores/widget-store"; +import { useWidgetStore } from "@/stores/widget-store"; +import type { RebuildContext } from "@/types/ui"; import { tauriAPI, ipcRenderer } from "@/ipc/api"; import type { DarwinApplyDataEvent, diff --git a/apps/native/src/stores/__mocks__/widget-store.ts b/apps/native/src/stores/__mocks__/widget-store.ts index 28ad5c1cb..2045454e4 100644 --- a/apps/native/src/stores/__mocks__/widget-store.ts +++ b/apps/native/src/stores/__mocks__/widget-store.ts @@ -11,31 +11,8 @@ // Inside `__mocks__`, the relative import resolves to the un-mocked // original module — that's the manual-mock contract Storybook // inherits from Vitest. -import { createWidgetStore as createRealWidgetStore, type WidgetState } from "@/stores/widget-store.impl"; - -export type { - EvolveEvent, - EvolveEventType, - EvolveState, - GitFileStatus, - GitStatus, - PermissionsState, - UpdateChannel, -} from "@/stores/widget-store.impl"; - -export type { - BoolPrefKey, - ConfirmPrefKey, - RebuildContext, - RebuildErrorType, - RebuildLine, - RebuildState, - SettingsTab, - WidgetState, - WidgetStep, -} from "@/stores/widget-store.impl"; - -export { initialRebuildState } from "@/stores/widget-store.impl"; +import { createWidgetStore as createRealWidgetStore } from "@/stores/widget-store.impl"; +import type { WidgetState } from "@/types/ui"; // ============================================================================= // Bypass invariants — these never drift from "all granted, all installed." @@ -118,7 +95,7 @@ export function createWidgetStore(initialState?: Partial) { export const useWidgetStore = createWidgetStore(); import { computeCurrentStep } from "@/components/widget/utils"; -import type { WidgetStep } from "@/stores/widget-store.impl"; +import type { WidgetStep } from "@/types/ui"; export function useCurrentStep(): WidgetStep { return useWidgetStore((state) => computeCurrentStep(state)); diff --git a/apps/native/src/stores/widget-store.impl.ts b/apps/native/src/stores/widget-store.impl.ts index c8cd06522..dca64ec7c 100644 --- a/apps/native/src/stores/widget-store.impl.ts +++ b/apps/native/src/stores/widget-store.impl.ts @@ -1,285 +1,19 @@ import { computeCurrentStep } from "@/components/widget/utils"; -import { FeedbackType } from "@/types/feedback"; import type { - EvolutionTelemetry, - EvolveEvent, - EvolveState, - FileDiffContents, - GitStatus, - HistoryItem, - PermissionsState, - RecommendedPrompt, - SemanticChangeMap, - UpdateChannel, -} from "@/ipc/types"; + BoolPrefKey, + ConfirmPrefKey, + WidgetState, + WidgetStep, + WidgetStore, +} from "@/types/ui"; +import { initialRebuildState } from "@/types/ui"; import { create } from "zustand"; import { devtools } from "zustand/middleware"; -export type { - EvolveEvent, - EvolveEventType, - EvolveState, - GitFileStatus, - GitStatus, - PermissionsState, - UpdateChannel, -} from "@/ipc/types"; - -// ============================================================================= -// Types -// ============================================================================= - -/** - * Widget step state - updated by useEffect based on app state. - */ -export type SettingsTab = "general" | "api-keys" | "ai-models" | "preferences" | "developer"; -export type WidgetStep = "permissions" | "nix-setup" | "setup" | "begin" | "evolve" | "commit" | "manualEvolve" | "manualCommit" | "history" | "filesystem"; -type ProcessingAction = "evolve" | "apply" | "merge" | "cancel" | null; -export type ConfirmPrefKey = "confirmBuild" | "confirmClear" | "confirmRollback"; -export type BoolPrefKey = ConfirmPrefKey | "autoSummarizeOnFocus" | "scanHomebrewOnStartup" | "defaultToDiffTab"; - -// Rebuild state for showing progress inline in the widget -export type RebuildErrorType = - | "infinite_recursion" - | "evaluation_error" - | "build_error" - | "full_disk_access" - | "user_cancelled" - | "authorization_denied" - | "generic_error"; - -export interface RebuildLine { - id: number; - text: string; - type: "stdout" | "stderr" | "info"; -} - -export type RebuildContext = "rollback" | "apply"; - -export interface RebuildState { - isRunning: boolean; - context: RebuildContext; - lines: RebuildLine[]; - rawLines: string[]; - exitCode?: number; - success?: boolean; - errorType?: RebuildErrorType; - errorMessage?: string; -} - -export interface WidgetState { - // Permissions (checked on startup) - permissionsState: PermissionsState | null; - permissionsChecked: boolean; - - // Config (from backend) - configDir: string; - hosts: string[]; - host: string; - - // Bootstrap (creating default config) - isBootstrapping: boolean; - - // Nix installation - nixInstalled: boolean | null; // null = not checked yet - nixInstalling: boolean; - nixInstallPhase: "downloading" | "waiting-for-installer" | "prefetching" | null; - nixDownloadProgress: { downloaded: number; total: number } | null; - - // nix-darwin (darwin-rebuild availability) - darwinRebuildAvailable: boolean | null; // null = not checked yet - darwinRebuildPrefetching: boolean; - - // Evolve state derived from backend source of truth - evolveState: EvolveState | null; - externalBuildDetected: boolean; - - // Git (from backend) - gitStatus: GitStatus | null; - fileDiffContents: Record; - // Evolution - evolvePrompt: string; - isProcessing: boolean; - processingAction: ProcessingAction; - evolveEvents: EvolveEvent[]; - promptHistory: string[]; - conversationalResponse: string | null; - evolutionTelemetry: EvolutionTelemetry | null; - - changeMap: SemanticChangeMap | null; - - // Commit message suggestion (generated on merge screen) - commitMessageSuggestion: string | null; - - // Rebuild state (for inline rebuild progress) - rebuild: RebuildState; - - // Console - consoleLogs: string; - - // History - history: HistoryItem[]; - historyLoading: boolean; - analyzingHistoryForHashes: Set; - - // UI - summaryAvailable: boolean; - isSummarizing: boolean; - isGenerating: boolean; - settingsOpen: boolean; - settingsActiveTab: SettingsTab | null; - prefsLoaded: boolean; - showHistory: boolean; - showFilesystem: boolean; - /** - * Optional initial section to focus when the Filesystem view opens - * (e.g. when "View" on the Untracked banner is clicked, this is set - * to "manage"). The view consumes and clears it on mount. `null` - * means "use the view's default." - */ - filesystemTargetSection: string | null; - feedbackOpen: boolean; - feedbackTypeOverride: FeedbackType | null; - feedbackInitialText: string | null; - panicDetails: { - message: string; - location?: string; - backtrace?: string; - timestamp: string; - } | null; - error: string | null; - // `undefined` means "stale/unfetched", while `null` means "fetched and none found". - recommendedPrompt: RecommendedPrompt | null | undefined; - - // Confirmation preferences - confirmBuild: boolean; - confirmClear: boolean; - confirmRollback: boolean; - - // Summarization preferences - autoSummarizeOnFocus: boolean; - - // Startup scanning preferences - scanHomebrewOnStartup: boolean; - - // Default-tab preference - defaultToDiffTab: boolean; - - // Developer mode (hidden settings panel for bisecting / pinning to a past release) - developerMode: boolean; - pinnedVersion: string | null; - updateChannel: UpdateChannel; - - // Editor - editingFile: string | null; -} - -interface WidgetActions { - // Permissions - setPermissionsState: (state: PermissionsState | null) => void; - setPermissionsChecked: (checked: boolean) => void; - - // Setters - setConfigDir: (dir: string) => void; - setHosts: (hosts: string[]) => void; - setHost: (host: string) => void; - setBootstrapping: (isBootstrapping: boolean) => void; - setNixInstalled: (installed: boolean | null) => void; - setNixInstalling: (installing: boolean) => void; - setNixInstallPhase: ( - phase: "downloading" | "waiting-for-installer" | "prefetching" | null, - ) => void; - setNixDownloadProgress: (progress: { downloaded: number; total: number } | null) => void; - setDarwinRebuildAvailable: (available: boolean | null) => void; - setDarwinRebuildPrefetching: (prefetching: boolean) => void; - setEvolveState: (state: EvolveState | null) => void; - setExternalBuildDetected: (detected: boolean) => void; - setGitStatus: (status: GitStatus | null) => void; - setFileDiffContents: (contents: Record) => void; - setEvolvePrompt: (prompt: string) => void; - setProcessing: (isProcessing: boolean, action?: ProcessingAction) => void; - setChangeMap: (map: SemanticChangeMap | null) => void; - setSettingsOpen: (open: boolean, tab?: SettingsTab | null) => void; - setPrefsLoaded: (loaded: boolean) => void; - setShowHistory: (show: boolean) => void; - /** - * @param section optional initial section id; when omitted on a - * `show=true` call the view falls back to its default section. - */ - setShowFilesystem: (show: boolean, section?: string | null) => void; - setFeedbackOpen: (open: boolean) => void; - setError: (error: string | null) => void; - setPanicDetails: ( - details: { message: string; location?: string; backtrace?: string; timestamp: string } | null, - ) => void; - setPromptHistory: (history: string[]) => void; - setSummaryAvailable: (available: boolean) => void; - setRecommendedPrompt: (prompt: RecommendedPrompt | null | undefined) => void; - - // History - setHistory: (history: HistoryItem[]) => void; - setHistoryLoading: (loading: boolean) => void; - addAnalyzingHistoryHash: (hash: string) => void; - removeAnalyzingHistoryHash: (hash: string) => void; - - // Boolean preferences - setBoolPref: (key: BoolPrefKey, value: boolean) => void; - initConfirmPrefs: (prefs: Partial>) => void; - - // Summarization preferences - setAutoSummarizeOnFocus: (value: boolean) => void; - - // Developer mode - setDeveloperMode: (value: boolean) => void; - setPinnedVersion: (value: string | null) => void; - setUpdateChannel: (value: UpdateChannel) => void; - - // Client-side state (NOT from server) - setSummarizing: (summarizing: boolean) => void; - setGenerating: (generating: boolean) => void; - clearPreview: () => void; - setFeedbackTypeOverride: (type: FeedbackType | null) => void; - openFeedback: (type?: FeedbackType, initialText?: string) => void; - - // Console - appendLog: (text: string) => void; - clearLogs: () => void; - - // Evolve events - appendEvolveEvent: (event: EvolveEvent) => void; - clearEvolveEvents: () => void; - setEvolutionTelemetry: (telemetry: EvolutionTelemetry | null) => void; - - setConversationalResponse: (response: string | null) => void; - - // Commit message suggestion - setCommitMessageSuggestion: (msg: string | null) => void; - - // Rebuild state - startRebuild: (context: RebuildContext) => void; - appendRebuildLine: (line: RebuildLine) => void; - appendRawLine: (line: string) => void; - setRebuildError: (errorType: RebuildErrorType, errorMessage: string) => void; - setRebuildComplete: (success: boolean, exitCode?: number) => void; - clearRebuild: () => void; -} - -type WidgetStore = WidgetState & WidgetActions; // ============================================================================= // Initial State // ============================================================================= -export const initialRebuildState: RebuildState = { - isRunning: false, - context: "apply", - lines: [], - rawLines: [], - exitCode: undefined, - success: undefined, - errorType: undefined, - errorMessage: undefined, -}; - const initialWidgetState: WidgetState = { // Permissions permissionsState: null, @@ -410,7 +144,7 @@ export function createWidgetStore(initialState?: Partial) { setChangeMap: (changeMap) => set({ changeMap }), setSummaryAvailable: (summaryAvailable) => set({ summaryAvailable }), setBoolPref: (key: BoolPrefKey, value: boolean) => set({ [key]: value }), - initConfirmPrefs: (prefs) => + initConfirmPrefs: (prefs: Partial>) => set({ confirmBuild: prefs.confirmBuild ?? true, confirmClear: prefs.confirmClear ?? true, diff --git a/apps/native/src/stores/widget-store.test.ts b/apps/native/src/stores/widget-store.test.ts index c8e074a9f..17850639b 100644 --- a/apps/native/src/stores/widget-store.test.ts +++ b/apps/native/src/stores/widget-store.test.ts @@ -1,10 +1,7 @@ import type { EvolveEvent, GitStatus } from "@/ipc/types"; +import { initialRebuildState, type RebuildLine } from "@/types/ui"; import { describe, expect, it } from "vitest"; -import { - createWidgetStore, - initialRebuildState, - type RebuildLine, -} from "./widget-store"; +import { createWidgetStore } from "./widget-store"; // --------------------------------------------------------------------------- // createWidgetStore() — factory + initial state diff --git a/apps/native/src/types/ui.ts b/apps/native/src/types/ui.ts new file mode 100644 index 000000000..3ac9a13ab --- /dev/null +++ b/apps/native/src/types/ui.ts @@ -0,0 +1,275 @@ +import type { + EvolutionTelemetry, + EvolveEvent, + EvolveState, + FileDiffContents, + GitStatus, + HistoryItem, + PermissionsState, + RecommendedPrompt, + SemanticChangeMap, + UpdateChannel, +} from "@/ipc/types"; +import type { FeedbackType } from "@/types/feedback"; + +export type SettingsTab = "general" | "api-keys" | "ai-models" | "preferences" | "developer"; + +export type WidgetStep = + | "permissions" + | "nix-setup" + | "setup" + | "begin" + | "evolve" + | "commit" + | "manualEvolve" + | "manualCommit" + | "history" + | "filesystem"; + +type ProcessingAction = "evolve" | "apply" | "merge" | "cancel" | null; + +export type ConfirmPrefKey = "confirmBuild" | "confirmClear" | "confirmRollback"; +export type BoolPrefKey = + | ConfirmPrefKey + | "autoSummarizeOnFocus" + | "scanHomebrewOnStartup" + | "defaultToDiffTab"; + +export type RebuildErrorType = + | "infinite_recursion" + | "evaluation_error" + | "build_error" + | "full_disk_access" + | "user_cancelled" + | "authorization_denied" + | "generic_error"; + +export interface RebuildLine { + id: number; + text: string; + type: "stdout" | "stderr" | "info"; +} + +export type RebuildContext = "rollback" | "apply"; + +export interface RebuildState { + isRunning: boolean; + context: RebuildContext; + lines: RebuildLine[]; + rawLines: string[]; + exitCode?: number; + success?: boolean; + errorType?: RebuildErrorType; + errorMessage?: string; +} + +export const initialRebuildState: RebuildState = { + isRunning: false, + context: "apply", + lines: [], + rawLines: [], + exitCode: undefined, + success: undefined, + errorType: undefined, + errorMessage: undefined, +}; + +export interface WidgetState { + // Permissions (checked on startup) + permissionsState: PermissionsState | null; + permissionsChecked: boolean; + + // Config (from backend) + configDir: string; + hosts: string[]; + host: string; + + // Bootstrap (creating default config) + isBootstrapping: boolean; + + // Nix installation + nixInstalled: boolean | null; // null = not checked yet + nixInstalling: boolean; + nixInstallPhase: "downloading" | "waiting-for-installer" | "prefetching" | null; + nixDownloadProgress: { downloaded: number; total: number } | null; + + // nix-darwin (darwin-rebuild availability) + darwinRebuildAvailable: boolean | null; // null = not checked yet + darwinRebuildPrefetching: boolean; + + // Evolve state derived from backend source of truth + evolveState: EvolveState | null; + externalBuildDetected: boolean; + + // Git (from backend) + gitStatus: GitStatus | null; + fileDiffContents: Record; + + // Evolution + evolvePrompt: string; + isProcessing: boolean; + processingAction: ProcessingAction; + evolveEvents: EvolveEvent[]; + promptHistory: string[]; + conversationalResponse: string | null; + evolutionTelemetry: EvolutionTelemetry | null; + + changeMap: SemanticChangeMap | null; + + // Commit message suggestion (generated on merge screen) + commitMessageSuggestion: string | null; + + // Rebuild state (for inline rebuild progress) + rebuild: RebuildState; + + // Console + consoleLogs: string; + + // History + history: HistoryItem[]; + historyLoading: boolean; + analyzingHistoryForHashes: Set; + + // UI + summaryAvailable: boolean; + isSummarizing: boolean; + isGenerating: boolean; + settingsOpen: boolean; + settingsActiveTab: SettingsTab | null; + prefsLoaded: boolean; + showHistory: boolean; + showFilesystem: boolean; + /** + * Optional initial section to focus when the Filesystem view opens + * (e.g. when "View" on the Untracked banner is clicked, this is set + * to "manage"). The view consumes and clears it on mount. `null` + * means "use the view's default." + */ + filesystemTargetSection: string | null; + feedbackOpen: boolean; + feedbackTypeOverride: FeedbackType | null; + feedbackInitialText: string | null; + panicDetails: { + message: string; + location?: string; + backtrace?: string; + timestamp: string; + } | null; + error: string | null; + // `undefined` means "stale/unfetched", while `null` means "fetched and none found". + recommendedPrompt: RecommendedPrompt | null | undefined; + + // Confirmation preferences + confirmBuild: boolean; + confirmClear: boolean; + confirmRollback: boolean; + + // Summarization preferences + autoSummarizeOnFocus: boolean; + + // Startup scanning preferences + scanHomebrewOnStartup: boolean; + + // Default-tab preference + defaultToDiffTab: boolean; + + // Developer mode (hidden settings panel for bisecting / pinning to a past release) + developerMode: boolean; + pinnedVersion: string | null; + updateChannel: UpdateChannel; + + // Editor + editingFile: string | null; +} + +export interface WidgetActions { + // Permissions + setPermissionsState: (state: PermissionsState | null) => void; + setPermissionsChecked: (checked: boolean) => void; + + // Setters + setConfigDir: (dir: string) => void; + setHosts: (hosts: string[]) => void; + setHost: (host: string) => void; + setBootstrapping: (isBootstrapping: boolean) => void; + setNixInstalled: (installed: boolean | null) => void; + setNixInstalling: (installing: boolean) => void; + setNixInstallPhase: ( + phase: "downloading" | "waiting-for-installer" | "prefetching" | null, + ) => void; + setNixDownloadProgress: (progress: { downloaded: number; total: number } | null) => void; + setDarwinRebuildAvailable: (available: boolean | null) => void; + setDarwinRebuildPrefetching: (prefetching: boolean) => void; + setEvolveState: (state: EvolveState | null) => void; + setExternalBuildDetected: (detected: boolean) => void; + setGitStatus: (status: GitStatus | null) => void; + setFileDiffContents: (contents: Record) => void; + setEvolvePrompt: (prompt: string) => void; + setProcessing: (isProcessing: boolean, action?: ProcessingAction) => void; + setChangeMap: (map: SemanticChangeMap | null) => void; + setSettingsOpen: (open: boolean, tab?: SettingsTab | null) => void; + setPrefsLoaded: (loaded: boolean) => void; + setShowHistory: (show: boolean) => void; + /** + * @param section optional initial section id; when omitted on a + * `show=true` call the view falls back to its default section. + */ + setShowFilesystem: (show: boolean, section?: string | null) => void; + setFeedbackOpen: (open: boolean) => void; + setError: (error: string | null) => void; + setPanicDetails: ( + details: { message: string; location?: string; backtrace?: string; timestamp: string } | null, + ) => void; + setPromptHistory: (history: string[]) => void; + setSummaryAvailable: (available: boolean) => void; + setRecommendedPrompt: (prompt: RecommendedPrompt | null | undefined) => void; + + // History + setHistory: (history: HistoryItem[]) => void; + setHistoryLoading: (loading: boolean) => void; + addAnalyzingHistoryHash: (hash: string) => void; + removeAnalyzingHistoryHash: (hash: string) => void; + + // Boolean preferences + setBoolPref: (key: BoolPrefKey, value: boolean) => void; + initConfirmPrefs: (prefs: Partial>) => void; + + // Summarization preferences + setAutoSummarizeOnFocus: (value: boolean) => void; + + // Developer mode + setDeveloperMode: (value: boolean) => void; + setPinnedVersion: (value: string | null) => void; + setUpdateChannel: (value: UpdateChannel) => void; + + // Client-side state (NOT from server) + setSummarizing: (summarizing: boolean) => void; + setGenerating: (generating: boolean) => void; + clearPreview: () => void; + setFeedbackTypeOverride: (type: FeedbackType | null) => void; + openFeedback: (type?: FeedbackType, initialText?: string) => void; + + // Console + appendLog: (text: string) => void; + clearLogs: () => void; + + // Evolve events + appendEvolveEvent: (event: EvolveEvent) => void; + clearEvolveEvents: () => void; + setEvolutionTelemetry: (telemetry: EvolutionTelemetry | null) => void; + + setConversationalResponse: (response: string | null) => void; + + // Commit message suggestion + setCommitMessageSuggestion: (msg: string | null) => void; + + // Rebuild state + startRebuild: (context: RebuildContext) => void; + appendRebuildLine: (line: RebuildLine) => void; + appendRawLine: (line: string) => void; + setRebuildError: (errorType: RebuildErrorType, errorMessage: string) => void; + setRebuildComplete: (success: boolean, exitCode?: number) => void; + clearRebuild: () => void; +} + +export type WidgetStore = WidgetState & WidgetActions; From 10426d4093b0ef546b7b100a4c29b95eb3e77dde Mon Sep 17 00:00:00 2001 From: CasLinden Date: Fri, 29 May 2026 14:58:25 +0900 Subject: [PATCH 2/7] chore(store): deprecate unused, orphaned or unneeded fields --- .../widget/settings/developer-tab.tsx | 2 +- apps/native/src/hooks/use-evolve.ts | 6 +--- apps/native/src/hooks/use-git-operations.ts | 1 - apps/native/src/hooks/use-nix-install.ts | 30 +------------------ apps/native/src/hooks/use-summary.ts | 6 ++-- apps/native/src/ipc/api.ts | 1 - apps/native/src/stores/widget-store.impl.ts | 9 ------ apps/native/src/types/ui.ts | 5 ---- 8 files changed, 5 insertions(+), 55 deletions(-) diff --git a/apps/native/src/components/widget/settings/developer-tab.tsx b/apps/native/src/components/widget/settings/developer-tab.tsx index ee994f434..d81d444ef 100644 --- a/apps/native/src/components/widget/settings/developer-tab.tsx +++ b/apps/native/src/components/widget/settings/developer-tab.tsx @@ -122,7 +122,7 @@ export function DeveloperTab() { const store = useWidgetStore.getState(); store.clearLogs(); store.clearEvolveEvents(); - store.clearPreview(); + store.setChangeMap(null); store.clearRebuild(); store.setConversationalResponse(null); store.setCommitMessageSuggestion(null); diff --git a/apps/native/src/hooks/use-evolve.ts b/apps/native/src/hooks/use-evolve.ts index 6c631e48c..2f59a8852 100644 --- a/apps/native/src/hooks/use-evolve.ts +++ b/apps/native/src/hooks/use-evolve.ts @@ -37,12 +37,11 @@ const refreshPromptHistory = async (prompt?: string) => { }; const findChangeMap = async (): Promise => { - const { setChangeMap, setSummaryAvailable } = useWidgetStore.getState(); + const { setChangeMap } = useWidgetStore.getState(); try { const map = await tauriAPI.summarizedChanges.findChangeMap(); if (map) { setChangeMap(map); - setSummaryAvailable(map.groups.length > 0 || map.singles.length > 0); } } catch (e) { console.error("[SemanticChangeMap] error", e); @@ -64,7 +63,6 @@ const handleEvolve = async () => { store.setExternalBuildDetected(false); store.clearEvolveEvents(); store.clearLogs(); - store.clearPreview(); store.setConversationalResponse(null); store.setEvolutionTelemetry(null); store.appendLog(`\n> Evolving: "${store.evolvePrompt}"\n`); @@ -97,8 +95,6 @@ const handleEvolve = async () => { if (isConversational) { useWidgetStore.getState().setConversationalResponse(result.conversationalResponse ?? null); - } else { - store.setSummaryAvailable(true); } if (result?.gitStatus) { useWidgetStore.getState().setGitStatus(result.gitStatus); diff --git a/apps/native/src/hooks/use-git-operations.ts b/apps/native/src/hooks/use-git-operations.ts index ad0e2fb95..915fa5bc3 100644 --- a/apps/native/src/hooks/use-git-operations.ts +++ b/apps/native/src/hooks/use-git-operations.ts @@ -85,7 +85,6 @@ const handleCommit = async ({ message }: { message: string }) => { useWidgetStore.getState().appendLog("✓ Committed successfully\n"); useWidgetStore.getState().setError(null); toast.success("Committed successfully"); - useWidgetStore.getState().clearPreview(); useWidgetStore.getState().setChangeMap(null); useWidgetStore.getState().setEvolveState(result.evolveState); await refreshGitStatus(); diff --git a/apps/native/src/hooks/use-nix-install.ts b/apps/native/src/hooks/use-nix-install.ts index 0bca5c312..b22eeedad 100644 --- a/apps/native/src/hooks/use-nix-install.ts +++ b/apps/native/src/hooks/use-nix-install.ts @@ -1,7 +1,6 @@ import { useWidgetStore } from "@/stores/widget-store"; import { tauriAPI, ipcRenderer } from "@/ipc/api"; import type { - NixDarwinRebuildEndEvent, NixInstallEndEvent, NixInstallProgressEvent, } from "@/ipc/types"; @@ -66,33 +65,6 @@ const installNix = async () => { } }; -const prefetchDarwinRebuild = async () => { - const store = useWidgetStore.getState(); - store.setDarwinRebuildPrefetching(true); - store.setError(null); - - const unlistenEnd = await ipcRenderer.on("nix:darwin-rebuild:end", (event) => { - const current = useWidgetStore.getState(); - current.setDarwinRebuildPrefetching(false); - current.setDarwinRebuildAvailable(event.payload.ok); - - if (!event.payload.ok) { - current.setError(event.payload.error ?? "Failed to set up nix-darwin."); - } - - unlistenEnd(); - }); - - try { - await tauriAPI.nix.prefetchDarwinRebuild(); - } catch (e: unknown) { - const msg = (e as Error)?.message || String(e); - store.setDarwinRebuildPrefetching(false); - store.setError(msg); - unlistenEnd(); - } -}; - export function useNixInstall() { - return { checkNix, installNix, prefetchDarwinRebuild }; + return { checkNix, installNix }; } diff --git a/apps/native/src/hooks/use-summary.ts b/apps/native/src/hooks/use-summary.ts index e12cc0779..98bb0eb9b 100644 --- a/apps/native/src/hooks/use-summary.ts +++ b/apps/native/src/hooks/use-summary.ts @@ -5,12 +5,11 @@ import { tauriAPI } from "@/ipc/api"; * Hook for fetching and managing the AI-generated summary of changes. */ const findChangeMap = async (): Promise => { - const { setChangeMap, setSummaryAvailable } = useWidgetStore.getState(); + const { setChangeMap } = useWidgetStore.getState(); try { const map = await tauriAPI.summarizedChanges.findChangeMap(); if (map) { setChangeMap(map); - setSummaryAvailable(map.groups.length > 0 || map.singles.length > 0); } } catch (e) { console.error("[SemanticChangeMap] error", e); @@ -29,12 +28,11 @@ const generateCommitMessage = async () => { }; const generateCurrentSummary = async () => { - const { setSummarizing, setChangeMap, setSummaryAvailable } = useWidgetStore.getState(); + const { setSummarizing, setChangeMap } = useWidgetStore.getState(); setSummarizing(true); try { const map = await tauriAPI.summarizedChanges.summarizeCurrent(); setChangeMap(map); - setSummaryAvailable(map.groups.length > 0 || map.singles.length > 0); } finally { setSummarizing(false); } diff --git a/apps/native/src/ipc/api.ts b/apps/native/src/ipc/api.ts index 0256fbc5b..bdfe3618e 100644 --- a/apps/native/src/ipc/api.ts +++ b/apps/native/src/ipc/api.ts @@ -72,7 +72,6 @@ export const tauriAPI = { nix: { check: () => invoke("nix_check"), installStart: () => invoke("nix_install_start"), - prefetchDarwinRebuild: () => invoke("darwin_rebuild_prefetch"), }, flake: { listHosts: () => invoke("flake_list_hosts"), diff --git a/apps/native/src/stores/widget-store.impl.ts b/apps/native/src/stores/widget-store.impl.ts index dca64ec7c..bee02e939 100644 --- a/apps/native/src/stores/widget-store.impl.ts +++ b/apps/native/src/stores/widget-store.impl.ts @@ -32,7 +32,6 @@ const initialWidgetState: WidgetState = { // nix-darwin darwinRebuildAvailable: null, - darwinRebuildPrefetching: false, // Routing state evolveState: null, @@ -57,7 +56,6 @@ const initialWidgetState: WidgetState = { analyzingHistoryForHashes: new Set(), changeMap: null, - summaryAvailable: false, // Commit message suggestion commitMessageSuggestion: null, @@ -142,7 +140,6 @@ export function createWidgetStore(initialState?: Partial) { processingAction: isProcessing ? action : null, }), setChangeMap: (changeMap) => set({ changeMap }), - setSummaryAvailable: (summaryAvailable) => set({ summaryAvailable }), setBoolPref: (key: BoolPrefKey, value: boolean) => set({ [key]: value }), initConfirmPrefs: (prefs: Partial>) => set({ @@ -192,14 +189,8 @@ export function createWidgetStore(initialState?: Partial) { setNixInstallPhase: (nixInstallPhase) => set({ nixInstallPhase }), setNixDownloadProgress: (nixDownloadProgress) => set({ nixDownloadProgress }), setDarwinRebuildAvailable: (darwinRebuildAvailable) => set({ darwinRebuildAvailable }), - setDarwinRebuildPrefetching: (darwinRebuildPrefetching) => set({ darwinRebuildPrefetching }), setSummarizing: (isSummarizing) => set({ isSummarizing }), setGenerating: (isGenerating) => set({ isGenerating }), - clearPreview: () => - set({ - changeMap: null, - summaryAvailable: false, - }), // Console appendLog: (text) => set((state) => ({ consoleLogs: state.consoleLogs + text })), diff --git a/apps/native/src/types/ui.ts b/apps/native/src/types/ui.ts index 3ac9a13ab..f4f4d92be 100644 --- a/apps/native/src/types/ui.ts +++ b/apps/native/src/types/ui.ts @@ -95,7 +95,6 @@ export interface WidgetState { // nix-darwin (darwin-rebuild availability) darwinRebuildAvailable: boolean | null; // null = not checked yet - darwinRebuildPrefetching: boolean; // Evolve state derived from backend source of truth evolveState: EvolveState | null; @@ -131,7 +130,6 @@ export interface WidgetState { analyzingHistoryForHashes: Set; // UI - summaryAvailable: boolean; isSummarizing: boolean; isGenerating: boolean; settingsOpen: boolean; @@ -199,7 +197,6 @@ export interface WidgetActions { ) => void; setNixDownloadProgress: (progress: { downloaded: number; total: number } | null) => void; setDarwinRebuildAvailable: (available: boolean | null) => void; - setDarwinRebuildPrefetching: (prefetching: boolean) => void; setEvolveState: (state: EvolveState | null) => void; setExternalBuildDetected: (detected: boolean) => void; setGitStatus: (status: GitStatus | null) => void; @@ -221,7 +218,6 @@ export interface WidgetActions { details: { message: string; location?: string; backtrace?: string; timestamp: string } | null, ) => void; setPromptHistory: (history: string[]) => void; - setSummaryAvailable: (available: boolean) => void; setRecommendedPrompt: (prompt: RecommendedPrompt | null | undefined) => void; // History @@ -245,7 +241,6 @@ export interface WidgetActions { // Client-side state (NOT from server) setSummarizing: (summarizing: boolean) => void; setGenerating: (generating: boolean) => void; - clearPreview: () => void; setFeedbackTypeOverride: (type: FeedbackType | null) => void; openFeedback: (type?: FeedbackType, initialText?: string) => void; From bcbc56914700dfd4ea12dfd34cd4183e00591512 Mon Sep 17 00:00:00 2001 From: CasLinden Date: Fri, 29 May 2026 15:45:22 +0900 Subject: [PATCH 3/7] chore(commands): remove unread state, orphaned setter and rust types and commands downsteam --- apps/native/.storybook/mocks/tauri-runtime.ts | 4 -- .../src-tauri/examples/specta_gen_ts.rs | 1 - apps/native/src-tauri/src/commands/apply.rs | 7 --- apps/native/src-tauri/src/main.rs | 1 - .../src-tauri/src/shared_types/events.rs | 10 ---- apps/native/src-tauri/src/system/nix.rs | 47 ------------------- apps/native/src/ipc/types.ts | 13 ----- 7 files changed, 83 deletions(-) diff --git a/apps/native/.storybook/mocks/tauri-runtime.ts b/apps/native/.storybook/mocks/tauri-runtime.ts index 88bb3264d..9be4410df 100644 --- a/apps/native/.storybook/mocks/tauri-runtime.ts +++ b/apps/native/.storybook/mocks/tauri-runtime.ts @@ -307,10 +307,6 @@ export const storybookTauriAPI = { emit("nix:install:end", { ok: true, code: 0, darwin_rebuild_available: true }); return okResult(); }, - prefetchDarwinRebuild: async () => { - emit("nix:darwin-rebuild:end", { ok: true }); - return okResult(); - }, }, flake: { listHosts: async () => [...defaultHosts], diff --git a/apps/native/src-tauri/examples/specta_gen_ts.rs b/apps/native/src-tauri/examples/specta_gen_ts.rs index e865e562f..f57a6243c 100644 --- a/apps/native/src-tauri/examples/specta_gen_ts.rs +++ b/apps/native/src-tauri/examples/specta_gen_ts.rs @@ -90,7 +90,6 @@ fn main() { .register::() .register::() .register::() - .register::() .register::() .register::() .register::() diff --git a/apps/native/src-tauri/src/commands/apply.rs b/apps/native/src-tauri/src/commands/apply.rs index 7ce159557..f87394066 100644 --- a/apps/native/src-tauri/src/commands/apply.rs +++ b/apps/native/src-tauri/src/commands/apply.rs @@ -91,13 +91,6 @@ pub async fn nix_check() -> Result { }) } -#[tauri::command] -pub async fn darwin_rebuild_prefetch(app: AppHandle) -> Result { - nix::prefetch_darwin_rebuild_stream(&app) - .map_err(|e| capture_err("darwin_rebuild_prefetch", e))?; - Ok(shared_types::OkResult::yes()) -} - #[tauri::command] pub async fn nix_install_start(app: AppHandle) -> Result { nix::install_nix_stream(&app).map_err(|e| capture_err("nix_install_start", e))?; diff --git a/apps/native/src-tauri/src/main.rs b/apps/native/src-tauri/src/main.rs index e178f7add..2f06e5a03 100644 --- a/apps/native/src-tauri/src/main.rs +++ b/apps/native/src-tauri/src/main.rs @@ -514,7 +514,6 @@ fn run_gui_mode( commands::evolve_state::routing_state_clear, commands::apply::nix_check, commands::apply::nix_install_start, - commands::apply::darwin_rebuild_prefetch, commands::apply::flake_list_hosts, commands::config::flake_exists, commands::config::bootstrap_default_config, diff --git a/apps/native/src-tauri/src/shared_types/events.rs b/apps/native/src-tauri/src/shared_types/events.rs index c7cb756fe..808dfaf0e 100644 --- a/apps/native/src-tauri/src/shared_types/events.rs +++ b/apps/native/src-tauri/src/shared_types/events.rs @@ -63,16 +63,6 @@ pub struct NixInstallEndEvent { pub error: Option, } -/// Payload for `nix:darwin-rebuild:end`. -#[derive(Debug, Clone, Serialize, Deserialize, Type)] -#[serde(rename_all = "camelCase")] -pub struct NixDarwinRebuildEndEvent { - /// Whether nix-darwin setup completed successfully. - pub ok: bool, - /// Human-readable failure message. - pub error: Option, -} - /// Known rebuild/activation failure categories. #[derive(Debug, Clone, Serialize, Deserialize, Type)] #[serde(rename_all = "snake_case")] diff --git a/apps/native/src-tauri/src/system/nix.rs b/apps/native/src-tauri/src/system/nix.rs index 10056b0ab..f9c13598a 100644 --- a/apps/native/src-tauri/src/system/nix.rs +++ b/apps/native/src-tauri/src/system/nix.rs @@ -162,53 +162,6 @@ pub fn get_nix_version() -> Option { } } -/// Prefetches darwin-rebuild by running `nix build --no-link nix-darwin/master#darwin-rebuild`. -/// This caches the derivation in the nix store so the `nix run` fallback in darwin.rs is fast. -/// Emits `nix:darwin-rebuild:end` with `{ ok: bool, error?: string }` on completion. -pub fn prefetch_darwin_rebuild_stream(app: &AppHandle) -> Result<()> { - info!("[nix] prefetch_darwin_rebuild_stream called"); - - let app_handle = app.clone(); - - // All emit calls below are fire-and-forget: background thread; window may not be - // listening. Tauri emit returns Err only when no listeners are registered. - std::thread::spawn(move || { - let result = Command::new("nix") - .args(["build", "--no-link", "nix-darwin/master#darwin-rebuild"]) - .env("PATH", get_nix_path_with_login_shell()) - .env("NIX_CONFIG", "experimental-features = nix-command flakes") - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output(); - - match result { - Ok(output) if output.status.success() => { - info!("[nix] darwin-rebuild prefetch succeeded"); - let _ = - app_handle.emit("nix:darwin-rebuild:end", serde_json::json!({ "ok": true })); - } - Ok(output) => { - let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - error!("[nix] darwin-rebuild prefetch failed: {}", stderr); - let _ = app_handle.emit( - "nix:darwin-rebuild:end", - serde_json::json!({ "ok": false, "error": stderr }), - ); - } - Err(e) => { - error!("[nix] darwin-rebuild prefetch error: {}", e); - let _ = app_handle.emit( - "nix:darwin-rebuild:end", - serde_json::json!({ "ok": false, "error": e.to_string() }), - ); - } - } - }); - - info!("[nix] prefetch_darwin_rebuild_stream started background thread"); - Ok(()) -} - pub fn install_nix_stream(app: &AppHandle) -> Result<()> { info!("[nix] install_nix_stream called"); diff --git a/apps/native/src/ipc/types.ts b/apps/native/src/ipc/types.ts index a992464c5..f2845751c 100644 --- a/apps/native/src/ipc/types.ts +++ b/apps/native/src/ipc/types.ts @@ -949,19 +949,6 @@ version: string | null; */ darwinRebuildAvailable: boolean } -/** - * Payload for `nix:darwin-rebuild:end`. - */ -export type NixDarwinRebuildEndEvent = { -/** - * Whether nix-darwin setup completed successfully. - */ -ok: boolean; -/** - * Human-readable failure message. - */ -error: string | null } - /** * Payload for `nix:install:end`. */ From 6f6a495afe024fa765986611736fd44f710081cf Mon Sep 17 00:00:00 2001 From: CasLinden Date: Fri, 29 May 2026 19:08:37 +0900 Subject: [PATCH 4/7] refactor(store): slice up widget store and extract fedback prefs and ui stores --- apps/native/src/components/widget/utils.ts | 21 +- apps/native/src/stores/feedback-store.ts | 62 +++++ apps/native/src/stores/pref-store.ts | 80 ++++++ apps/native/src/stores/slices/config.ts | 35 +++ apps/native/src/stores/slices/console.ts | 30 ++ apps/native/src/stores/slices/evolve.ts | 54 ++++ apps/native/src/stores/slices/git.ts | 29 ++ apps/native/src/stores/slices/history.ts | 49 ++++ apps/native/src/stores/slices/rebuild.ts | 120 ++++++++ apps/native/src/stores/slices/setup.ts | 61 +++++ apps/native/src/stores/slices/summary.ts | 41 +++ apps/native/src/stores/ui-store.ts | 88 ++++++ apps/native/src/stores/widget-store.impl.ts | 288 +++----------------- apps/native/src/types/ui.ts | 270 ------------------ 14 files changed, 701 insertions(+), 527 deletions(-) create mode 100644 apps/native/src/stores/feedback-store.ts create mode 100644 apps/native/src/stores/pref-store.ts create mode 100644 apps/native/src/stores/slices/config.ts create mode 100644 apps/native/src/stores/slices/console.ts create mode 100644 apps/native/src/stores/slices/evolve.ts create mode 100644 apps/native/src/stores/slices/git.ts create mode 100644 apps/native/src/stores/slices/history.ts create mode 100644 apps/native/src/stores/slices/rebuild.ts create mode 100644 apps/native/src/stores/slices/setup.ts create mode 100644 apps/native/src/stores/slices/summary.ts create mode 100644 apps/native/src/stores/ui-store.ts delete mode 100644 apps/native/src/types/ui.ts diff --git a/apps/native/src/components/widget/utils.ts b/apps/native/src/components/widget/utils.ts index fc9c851e6..ff3fb6a2b 100644 --- a/apps/native/src/components/widget/utils.ts +++ b/apps/native/src/components/widget/utils.ts @@ -1,8 +1,25 @@ import { filesystemViewEnabled } from "@/lib/flags"; -import type { WidgetState, WidgetStep } from "@/types/ui"; +import type { ConfigState } from "@/stores/slices/config"; +import type { EvolveSliceState } from "@/stores/slices/evolve"; +import type { SetupState } from "@/stores/slices/setup"; +import type { UiState } from "@/stores/ui-store"; import { FilePen, FilePlus, FileX, FileCode, type LucideIcon } from "lucide-react"; -export function computeCurrentStep(state: WidgetState): WidgetStep { +export type WidgetStep = + | "permissions" + | "nix-setup" + | "setup" + | "begin" + | "evolve" + | "commit" + | "manualEvolve" + | "manualCommit" + | "history" + | "filesystem"; + +type ComputeStepInput = ConfigState & SetupState & UiState & EvolveSliceState; + +export function computeCurrentStep(state: ComputeStepInput): WidgetStep { const hasConfigDir = !!state.configDir; const hasHost = !!state.host && state.hosts.includes(state.host); const permissionsCheckedAndIncomplete = diff --git a/apps/native/src/stores/feedback-store.ts b/apps/native/src/stores/feedback-store.ts new file mode 100644 index 000000000..c2aa54ab3 --- /dev/null +++ b/apps/native/src/stores/feedback-store.ts @@ -0,0 +1,62 @@ +import { FeedbackType } from "@/types/feedback"; +import { create } from "zustand"; +import { devtools } from "zustand/middleware"; + +type PanicDetails = { + message: string; + location?: string; + backtrace?: string; + timestamp: string; +} | null; + +export type FeedbackState = { + error: string | null; + feedbackOpen: boolean; + feedbackTypeOverride: FeedbackType | null; + feedbackInitialText: string | null; + panicDetails: PanicDetails; +}; + +export type FeedbackActions = { + setError: (error: string | null) => void; + setFeedbackOpen: (open: boolean) => void; + setFeedbackTypeOverride: (type: FeedbackType | null) => void; + openFeedback: (type?: FeedbackType, initialText?: string) => void; + setPanicDetails: (details: PanicDetails) => void; +}; + +export type FeedbackStore = FeedbackState & FeedbackActions; + +const initialErrorFeedbackState: FeedbackState = { + error: null, + feedbackOpen: false, + feedbackTypeOverride: null, + feedbackInitialText: null, + panicDetails: null, +}; + +export function createFeedbackStore(initial?: Partial) { + return create()( + devtools( + (set) => ({ + ...initialErrorFeedbackState, + ...initial, + + setError: (error) => set({ error }), + setFeedbackOpen: (feedbackOpen) => set({ feedbackOpen }), + setFeedbackTypeOverride: (feedbackTypeOverride) => + set({ feedbackTypeOverride }), + openFeedback: (type, initialText) => + set({ + feedbackOpen: true, + feedbackTypeOverride: type ?? null, + feedbackInitialText: initialText ?? null, + }), + setPanicDetails: (panicDetails) => set({ panicDetails }), + }), + { name: "feedback-store", enabled: import.meta.env.DEV }, + ), + ); +} + +export const useFeedbackStore = createFeedbackStore(); diff --git a/apps/native/src/stores/pref-store.ts b/apps/native/src/stores/pref-store.ts new file mode 100644 index 000000000..5a8f12a75 --- /dev/null +++ b/apps/native/src/stores/pref-store.ts @@ -0,0 +1,80 @@ +import type { UpdateChannel } from "@/ipc/types"; +import { create } from "zustand"; +import { devtools } from "zustand/middleware"; + +export type ConfirmPrefKey = + | "confirmBuild" + | "confirmClear" + | "confirmRollback"; +export type BoolPrefKey = + | ConfirmPrefKey + | "autoSummarizeOnFocus" + | "scanHomebrewOnStartup" + | "defaultToDiffTab"; + +export type PrefState = { + confirmBuild: boolean; + confirmClear: boolean; + confirmRollback: boolean; + autoSummarizeOnFocus: boolean; + scanHomebrewOnStartup: boolean; + defaultToDiffTab: boolean; + developerMode: boolean; + pinnedVersion: string | null; + updateChannel: UpdateChannel; + prefsLoaded: boolean; +}; + +export type PrefActions = { + setBoolPref: (key: BoolPrefKey, value: boolean) => void; + initConfirmPrefs: (prefs: Partial>) => void; + setAutoSummarizeOnFocus: (value: boolean) => void; + setDeveloperMode: (value: boolean) => void; + setPinnedVersion: (value: string | null) => void; + setUpdateChannel: (value: UpdateChannel) => void; + setPrefsLoaded: (loaded: boolean) => void; +}; + +export type PrefStore = PrefState & PrefActions; + +const initialPreferencesState: PrefState = { + confirmBuild: true, + confirmClear: true, + confirmRollback: true, + autoSummarizeOnFocus: false, + scanHomebrewOnStartup: true, + defaultToDiffTab: false, + developerMode: false, + pinnedVersion: null, + updateChannel: "stable", + prefsLoaded: false, +}; + +export function createPrefStore(initial?: Partial) { + return create()( + devtools( + (set) => ({ + ...initialPreferencesState, + ...initial, + + setBoolPref: (key, value) => + set({ [key]: value } as Partial), + initConfirmPrefs: (prefs) => + set({ + confirmBuild: prefs.confirmBuild ?? true, + confirmClear: prefs.confirmClear ?? true, + confirmRollback: prefs.confirmRollback ?? true, + }), + setAutoSummarizeOnFocus: (value) => + set({ autoSummarizeOnFocus: value }), + setDeveloperMode: (value) => set({ developerMode: value }), + setPinnedVersion: (value) => set({ pinnedVersion: value }), + setUpdateChannel: (value) => set({ updateChannel: value }), + setPrefsLoaded: (prefsLoaded) => set({ prefsLoaded }), + }), + { name: "pref-store", enabled: import.meta.env.DEV }, + ), + ); +} + +export const usePrefStore = createPrefStore(); diff --git a/apps/native/src/stores/slices/config.ts b/apps/native/src/stores/slices/config.ts new file mode 100644 index 000000000..2b11f0d57 --- /dev/null +++ b/apps/native/src/stores/slices/config.ts @@ -0,0 +1,35 @@ +import type { WidgetStore } from "@/stores/widget-store.impl"; +import type { StateCreator } from "zustand"; + +export const createConfigSlice: StateCreator< + WidgetStore, + [], + [], + ConfigSlice +> = (set) => ({ + ...initialConfigState, + + setConfigDir: (configDir) => set({ configDir }), + setHosts: (hosts) => set({ hosts }), + setHost: (host) => set({ host }), +}); + +export type ConfigState = { + configDir: string; + hosts: string[]; + host: string; +}; + +export type ConfigActions = { + setConfigDir: (dir: string) => void; + setHosts: (hosts: string[]) => void; + setHost: (host: string) => void; +}; + +export type ConfigSlice = ConfigState & ConfigActions; + +const initialConfigState: ConfigState = { + configDir: "", + hosts: [], + host: "", +}; diff --git a/apps/native/src/stores/slices/console.ts b/apps/native/src/stores/slices/console.ts new file mode 100644 index 000000000..431e77bbb --- /dev/null +++ b/apps/native/src/stores/slices/console.ts @@ -0,0 +1,30 @@ +import type { WidgetStore } from "@/stores/widget-store.impl"; +import type { StateCreator } from "zustand"; + +export const createConsoleSlice: StateCreator< + WidgetStore, + [], + [], + ConsoleSlice +> = (set) => ({ + ...initialConsoleState, + + appendLog: (text) => + set((state) => ({ consoleLogs: state.consoleLogs + text })), + clearLogs: () => set({ consoleLogs: "" }), +}); + +export type ConsoleState = { + consoleLogs: string; +}; + +export type ConsoleActions = { + appendLog: (text: string) => void; + clearLogs: () => void; +}; + +export type ConsoleSlice = ConsoleState & ConsoleActions; + +const initialConsoleState: ConsoleState = { + consoleLogs: "", +}; diff --git a/apps/native/src/stores/slices/evolve.ts b/apps/native/src/stores/slices/evolve.ts new file mode 100644 index 000000000..58a9d7815 --- /dev/null +++ b/apps/native/src/stores/slices/evolve.ts @@ -0,0 +1,54 @@ +import type { + EvolutionTelemetry, + EvolveEvent, + EvolveState, + RecommendedPrompt, +} from "@/ipc/types"; +import type { WidgetStore } from "@/stores/widget-store.impl"; +import type { StateCreator } from "zustand"; + +export const createEvolveSlice: StateCreator< + WidgetStore, + [], + [], + EvolveSlice +> = (set) => ({ + ...initialEvolveState, + + setEvolveState: (evolveState) => set({ evolveState }), + appendEvolveEvent: (event) => + set((state) => ({ evolveEvents: [...state.evolveEvents, event] })), + clearEvolveEvents: () => set({ evolveEvents: [] }), + setConversationalResponse: (conversationalResponse) => + set({ conversationalResponse }), + setRecommendedPrompt: (recommendedPrompt) => set({ recommendedPrompt }), + setEvolutionTelemetry: (evolutionTelemetry) => set({ evolutionTelemetry }), +}); + +export type EvolveSliceState = { + evolveState: EvolveState | null; + evolveEvents: EvolveEvent[]; + conversationalResponse: string | null; + // `undefined` means "stale/unfetched", while `null` means "fetched and none found". + recommendedPrompt: RecommendedPrompt | null | undefined; + evolutionTelemetry: EvolutionTelemetry | null; +}; + +export type EvolveActions = { + setEvolveState: (state: EvolveState | null) => void; + appendEvolveEvent: (event: EvolveEvent) => void; + clearEvolveEvents: () => void; + setConversationalResponse: (response: string | null) => void; + setRecommendedPrompt: (prompt: RecommendedPrompt | null | undefined) => void; + setEvolutionTelemetry: (telemetry: EvolutionTelemetry | null) => void; +}; + +export type EvolveSlice = EvolveSliceState & EvolveActions; + +const initialEvolveState: EvolveSliceState = { + evolveState: null, + evolveEvents: [], + conversationalResponse: null, + recommendedPrompt: undefined, + evolutionTelemetry: null, +}; diff --git a/apps/native/src/stores/slices/git.ts b/apps/native/src/stores/slices/git.ts new file mode 100644 index 000000000..0ec165e14 --- /dev/null +++ b/apps/native/src/stores/slices/git.ts @@ -0,0 +1,29 @@ +import type { FileDiffContents, GitStatus } from "@/ipc/types"; +import type { WidgetStore } from "@/stores/widget-store.impl"; +import type { StateCreator } from "zustand"; + +export const createGitSlice: StateCreator = ( + set, +) => ({ + ...initialGitState, + + setGitStatus: (gitStatus) => set({ gitStatus }), + setFileDiffContents: (fileDiffContents) => set({ fileDiffContents }), +}); + +export type GitState = { + gitStatus: GitStatus | null; + fileDiffContents: Record; +}; + +export type GitActions = { + setGitStatus: (status: GitStatus | null) => void; + setFileDiffContents: (contents: Record) => void; +}; + +export type GitSlice = GitState & GitActions; + +const initialGitState: GitState = { + gitStatus: null, + fileDiffContents: {}, +}; diff --git a/apps/native/src/stores/slices/history.ts b/apps/native/src/stores/slices/history.ts new file mode 100644 index 000000000..42b9b8eca --- /dev/null +++ b/apps/native/src/stores/slices/history.ts @@ -0,0 +1,49 @@ +import type { HistoryItem } from "@/ipc/types"; +import type { WidgetStore } from "@/stores/widget-store.impl"; +import type { StateCreator } from "zustand"; + +export const createHistorySlice: StateCreator< + WidgetStore, + [], + [], + HistorySlice +> = (set) => ({ + ...initialHistoryState, + + setHistory: (history) => set({ history }), + setHistoryLoading: (historyLoading) => set({ historyLoading }), + addAnalyzingHistoryHash: (hash) => + set((state) => ({ + analyzingHistoryForHashes: new Set([ + ...state.analyzingHistoryForHashes, + hash, + ]), + })), + removeAnalyzingHistoryHash: (hash) => + set((state) => { + const next = new Set(state.analyzingHistoryForHashes); + next.delete(hash); + return { analyzingHistoryForHashes: next }; + }), +}); + +export type HistoryState = { + history: HistoryItem[]; + historyLoading: boolean; + analyzingHistoryForHashes: Set; +}; + +export type HistoryActions = { + setHistory: (history: HistoryItem[]) => void; + setHistoryLoading: (loading: boolean) => void; + addAnalyzingHistoryHash: (hash: string) => void; + removeAnalyzingHistoryHash: (hash: string) => void; +}; + +export type HistorySlice = HistoryState & HistoryActions; + +const initialHistoryState: HistoryState = { + history: [], + historyLoading: false, + analyzingHistoryForHashes: new Set(), +}; diff --git a/apps/native/src/stores/slices/rebuild.ts b/apps/native/src/stores/slices/rebuild.ts new file mode 100644 index 000000000..0bfe397ca --- /dev/null +++ b/apps/native/src/stores/slices/rebuild.ts @@ -0,0 +1,120 @@ +import type { WidgetStore } from "@/stores/widget-store.impl"; +import type { StateCreator } from "zustand"; + +export type RebuildErrorType = + | "infinite_recursion" + | "evaluation_error" + | "build_error" + | "full_disk_access" + | "user_cancelled" + | "authorization_denied" + | "generic_error"; + +export type RebuildLine = { + id: number; + text: string; + type: "stdout" | "stderr" | "info"; +}; + +export type RebuildContext = "rollback" | "apply"; + +export type RebuildState = { + isRunning: boolean; + context: RebuildContext; + lines: RebuildLine[]; + rawLines: string[]; + exitCode?: number; + success?: boolean; + errorType?: RebuildErrorType; + errorMessage?: string; +}; + +export const createRebuildSlice: StateCreator< + WidgetStore, + [], + [], + RebuildSlice +> = (set) => ({ + ...initialRebuildSliceState, + + startRebuild: (context) => + set({ + rebuild: { + isRunning: true, + context, + lines: [{ id: 0, text: "Preparing rebuild...", type: "info" }], + rawLines: [], + exitCode: undefined, + success: undefined, + errorType: undefined, + errorMessage: undefined, + }, + }), + appendRebuildLine: (line) => + set((state) => ({ + rebuild: { + ...state.rebuild, + lines: [...state.rebuild.lines, line].slice(-50), // Keep last 50 lines + }, + })), + appendRawLine: (line) => + set((state) => ({ + rebuild: { + ...state.rebuild, + rawLines: [...state.rebuild.rawLines, line].slice(-500), // Keep last 500 raw lines + }, + })), + setRebuildError: (errorType, errorMessage) => + set((state) => ({ + rebuild: { + ...state.rebuild, + errorType, + errorMessage, + }, + })), + setRebuildComplete: (success, exitCode) => + set((state) => ({ + rebuild: { + ...state.rebuild, + isRunning: false, + success, + exitCode, + }, + })), + clearRebuild: () => set({ rebuild: initialRebuildSubstate }), + setExternalBuildDetected: (externalBuildDetected) => + set({ externalBuildDetected }), +}); + +export type RebuildSliceState = { + rebuild: RebuildState; + externalBuildDetected: boolean; +}; + +export type RebuildActions = { + startRebuild: (context: RebuildContext) => void; + appendRebuildLine: (line: RebuildLine) => void; + appendRawLine: (line: string) => void; + setRebuildError: (errorType: RebuildErrorType, errorMessage: string) => void; + setRebuildComplete: (success: boolean, exitCode?: number) => void; + clearRebuild: () => void; + setExternalBuildDetected: (detected: boolean) => void; +}; + +export type RebuildSlice = RebuildSliceState & RebuildActions; + +export const initialRebuildSubstate: RebuildState = { + isRunning: false, + context: "apply", + lines: [], + rawLines: [], + exitCode: undefined, + success: undefined, + errorType: undefined, + errorMessage: undefined, +}; + +const initialRebuildSliceState: RebuildSliceState = { + rebuild: initialRebuildSubstate, + externalBuildDetected: false, +}; diff --git a/apps/native/src/stores/slices/setup.ts b/apps/native/src/stores/slices/setup.ts new file mode 100644 index 000000000..8df6a3767 --- /dev/null +++ b/apps/native/src/stores/slices/setup.ts @@ -0,0 +1,61 @@ +import type { PermissionsState } from "@/ipc/types"; +import type { WidgetStore } from "@/stores/widget-store.impl"; +import type { StateCreator } from "zustand"; + +export const createSetupSlice: StateCreator = ( + set, +) => ({ + ...initialSetupState, + + setPermissionsState: (permissionsState) => set({ permissionsState }), + setPermissionsChecked: (permissionsChecked) => set({ permissionsChecked }), + setBootstrapping: (isBootstrapping) => set({ isBootstrapping }), + setNixInstalled: (nixInstalled) => set({ nixInstalled }), + setNixInstalling: (nixInstalling) => set({ nixInstalling }), + setNixInstallPhase: (nixInstallPhase) => set({ nixInstallPhase }), + setNixDownloadProgress: (nixDownloadProgress) => set({ nixDownloadProgress }), + setDarwinRebuildAvailable: (darwinRebuildAvailable) => + set({ darwinRebuildAvailable }), +}); + +type NixInstallPhase = + | "downloading" + | "waiting-for-installer" + | "prefetching" + | null; +type NixDownloadProgress = { downloaded: number; total: number } | null; + +export type SetupState = { + permissionsState: PermissionsState | null; + permissionsChecked: boolean; + isBootstrapping: boolean; + nixInstalled: boolean | null; + nixInstalling: boolean; + nixInstallPhase: NixInstallPhase; + nixDownloadProgress: NixDownloadProgress; + darwinRebuildAvailable: boolean | null; +}; + +export type SetupActions = { + setPermissionsState: (state: PermissionsState | null) => void; + setPermissionsChecked: (checked: boolean) => void; + setBootstrapping: (isBootstrapping: boolean) => void; + setNixInstalled: (installed: boolean | null) => void; + setNixInstalling: (installing: boolean) => void; + setNixInstallPhase: (phase: NixInstallPhase) => void; + setNixDownloadProgress: (progress: NixDownloadProgress) => void; + setDarwinRebuildAvailable: (available: boolean | null) => void; +}; + +export type SetupSlice = SetupState & SetupActions; + +const initialSetupState: SetupState = { + permissionsState: null, + permissionsChecked: false, + isBootstrapping: false, + nixInstalled: null, + nixInstalling: false, + nixInstallPhase: null, + nixDownloadProgress: null, + darwinRebuildAvailable: null, +}; diff --git a/apps/native/src/stores/slices/summary.ts b/apps/native/src/stores/slices/summary.ts new file mode 100644 index 000000000..51a375caa --- /dev/null +++ b/apps/native/src/stores/slices/summary.ts @@ -0,0 +1,41 @@ +import type { SemanticChangeMap } from "@/ipc/types"; +import type { WidgetStore } from "@/stores/widget-store.impl"; +import type { StateCreator } from "zustand"; + +export const createSummarySlice: StateCreator< + WidgetStore, + [], + [], + SummarySlice +> = (set) => ({ + ...initialSummaryState, + + setChangeMap: (changeMap) => set({ changeMap }), + setCommitMessageSuggestion: (commitMessageSuggestion) => + set({ commitMessageSuggestion }), + setSummarizing: (isSummarizing) => set({ isSummarizing }), + setGenerating: (isGenerating) => set({ isGenerating }), +}); + +export type SummaryState = { + changeMap: SemanticChangeMap | null; + commitMessageSuggestion: string | null; + isSummarizing: boolean; + isGenerating: boolean; +}; + +export type SummaryActions = { + setChangeMap: (map: SemanticChangeMap | null) => void; + setCommitMessageSuggestion: (msg: string | null) => void; + setSummarizing: (summarizing: boolean) => void; + setGenerating: (generating: boolean) => void; +}; + +export type SummarySlice = SummaryState & SummaryActions; + +const initialSummaryState: SummaryState = { + changeMap: null, + commitMessageSuggestion: null, + isSummarizing: false, + isGenerating: false, +}; diff --git a/apps/native/src/stores/ui-store.ts b/apps/native/src/stores/ui-store.ts new file mode 100644 index 000000000..8004fd3df --- /dev/null +++ b/apps/native/src/stores/ui-store.ts @@ -0,0 +1,88 @@ +import { create } from "zustand"; +import { devtools } from "zustand/middleware"; + +export type SettingsTab = + | "general" + | "api-keys" + | "ai-models" + | "preferences" + | "developer"; + +type ProcessingAction = "evolve" | "apply" | "merge" | "cancel" | null; + +export type UiState = { + evolvePrompt: string; + isProcessing: boolean; + processingAction: ProcessingAction; + settingsOpen: boolean; + settingsActiveTab: SettingsTab | null; + showHistory: boolean; + showFilesystem: boolean; + /** + * Optional initial section to focus when the Filesystem view opens + * (e.g. when "View" on the Untracked banner is clicked, this is set + * to "manage"). The view consumes and clears it on mount. `null` + * means "use the view's default." + */ + filesystemTargetSection: string | null; + editingFile: string | null; + promptHistory: string[]; +}; + +export type UiActions = { + setEvolvePrompt: (prompt: string) => void; + setProcessing: (isProcessing: boolean, action?: ProcessingAction) => void; + setSettingsOpen: (open: boolean, tab?: SettingsTab | null) => void; + setShowHistory: (show: boolean) => void; + /** + * @param section optional initial section id; when omitted on a + * `show=true` call the view falls back to its default section. + */ + setShowFilesystem: (show: boolean, section?: string | null) => void; + setPromptHistory: (history: string[]) => void; +}; + +export type UiStore = UiState & UiActions; + +const initialUiState: UiState = { + evolvePrompt: "", + isProcessing: false, + processingAction: null, + settingsOpen: false, + settingsActiveTab: null, + showHistory: false, + showFilesystem: false, + filesystemTargetSection: null, + editingFile: null, + promptHistory: [], +}; + +export function createUiStore(initial?: Partial) { + return create()( + devtools( + (set) => ({ + ...initialUiState, + ...initial, + + setEvolvePrompt: (evolvePrompt) => set({ evolvePrompt }), + setProcessing: (isProcessing, action = null) => + set({ + isProcessing, + processingAction: isProcessing ? action : null, + }), + setSettingsOpen: (settingsOpen, tab) => + set({ settingsOpen, settingsActiveTab: tab ?? null }), + setShowHistory: (showHistory) => set({ showHistory }), + setShowFilesystem: (showFilesystem, section = null) => + set({ + showFilesystem, + filesystemTargetSection: showFilesystem ? section : null, + }), + setPromptHistory: (promptHistory) => set({ promptHistory }), + }), + { name: "ui-store", enabled: import.meta.env.DEV }, + ), + ); +} + +export const useUiStore = createUiStore(); diff --git a/apps/native/src/stores/widget-store.impl.ts b/apps/native/src/stores/widget-store.impl.ts index bee02e939..bdd1821f1 100644 --- a/apps/native/src/stores/widget-store.impl.ts +++ b/apps/native/src/stores/widget-store.impl.ts @@ -1,259 +1,39 @@ +import type { WidgetStep } from "@/components/widget/utils"; import { computeCurrentStep } from "@/components/widget/utils"; -import type { - BoolPrefKey, - ConfirmPrefKey, - WidgetState, - WidgetStep, - WidgetStore, -} from "@/types/ui"; -import { initialRebuildState } from "@/types/ui"; +import { createConfigSlice, type ConfigSlice } from "@/stores/slices/config"; +import { createConsoleSlice, type ConsoleSlice } from "@/stores/slices/console"; +import { createEvolveSlice, type EvolveSlice } from "@/stores/slices/evolve"; +import { createGitSlice, type GitSlice } from "@/stores/slices/git"; +import { createHistorySlice, type HistorySlice } from "@/stores/slices/history"; +import { createRebuildSlice, type RebuildSlice } from "@/stores/slices/rebuild"; +import { createSetupSlice, type SetupSlice } from "@/stores/slices/setup"; +import { createSummarySlice, type SummarySlice } from "@/stores/slices/summary"; +import { useUiStore } from "@/stores/ui-store"; import { create } from "zustand"; import { devtools } from "zustand/middleware"; -// ============================================================================= -// Initial State -// ============================================================================= +export type WidgetStore = SetupSlice & + ConfigSlice & + EvolveSlice & + GitSlice & + RebuildSlice & + HistorySlice & + SummarySlice & + ConsoleSlice; -const initialWidgetState: WidgetState = { - // Permissions - permissionsState: null, - permissionsChecked: false, - - // Config - configDir: "", - hosts: [], - host: "", - - // Nix - nixInstalled: null, - nixInstalling: false, - nixInstallPhase: null, - nixDownloadProgress: null, - - // nix-darwin - darwinRebuildAvailable: null, - - // Routing state - evolveState: null, - externalBuildDetected: false, - - // Git - gitStatus: null, - fileDiffContents: {}, - - // Evolution - evolvePrompt: "", - isProcessing: false, - processingAction: null, - evolveEvents: [], - promptHistory: [], - conversationalResponse: null, - evolutionTelemetry: null, - - // History - history: [], - historyLoading: false, - analyzingHistoryForHashes: new Set(), - - changeMap: null, - - // Commit message suggestion - commitMessageSuggestion: null, - - // Rebuild - rebuild: initialRebuildState, - - // Console - consoleLogs: "", - - // UI - isBootstrapping: false, - isSummarizing: false, - isGenerating: false, - settingsOpen: false, - settingsActiveTab: null, - prefsLoaded: false, - showHistory: false, - showFilesystem: false, - filesystemTargetSection: null, - feedbackOpen: false, - feedbackTypeOverride: null, - feedbackInitialText: null, - panicDetails: null, - error: null, - recommendedPrompt: undefined, - - // Confirmation preferences - confirmBuild: true, - confirmClear: true, - confirmRollback: true, - - // Summarization preferences - autoSummarizeOnFocus: false, - - // Startup scanning preferences - scanHomebrewOnStartup: true, - - // Default-tab preference - defaultToDiffTab: false, - - // Developer mode - developerMode: false, - pinnedVersion: null, - updateChannel: "stable", - - // Editor - editingFile: null, -}; - -// ============================================================================= -// Store Factory -// ============================================================================= - -/** - * Create a widget store with optional initial state. - * This factory pattern allows creating isolated stores for testing/Storybook. - */ -export function createWidgetStore(initialState?: Partial) { +export function createWidgetStore(initialState?: Partial) { return create()( devtools( - (set, _get) => ({ - ...initialWidgetState, - ...initialState, - - // Permissions - setPermissionsState: (permissionsState) => set({ permissionsState }), - setPermissionsChecked: (permissionsChecked) => set({ permissionsChecked }), - - // Setters - setConfigDir: (configDir) => set({ configDir }), - setHosts: (hosts) => set({ hosts }), - setHost: (host) => set({ host }), - setEvolveState: (evolveState) => set({ evolveState: evolveState }), - setExternalBuildDetected: (externalBuildDetected) => set({ externalBuildDetected }), - setGitStatus: (gitStatus) => set({ gitStatus }), - setFileDiffContents: (fileDiffContents) => set({ fileDiffContents }), - setEvolvePrompt: (evolvePrompt) => set({ evolvePrompt }), - setProcessing: (isProcessing, action = null) => - set({ - isProcessing, - processingAction: isProcessing ? action : null, - }), - setChangeMap: (changeMap) => set({ changeMap }), - setBoolPref: (key: BoolPrefKey, value: boolean) => set({ [key]: value }), - initConfirmPrefs: (prefs: Partial>) => - set({ - confirmBuild: prefs.confirmBuild ?? true, - confirmClear: prefs.confirmClear ?? true, - confirmRollback: prefs.confirmRollback ?? true, - }), - setAutoSummarizeOnFocus: (value) => set({ autoSummarizeOnFocus: value }), - setDeveloperMode: (value) => set({ developerMode: value }), - setPinnedVersion: (value) => set({ pinnedVersion: value }), - setUpdateChannel: (value) => set({ updateChannel: value }), - setHistory: (history) => set({ history }), - setHistoryLoading: (historyLoading) => set({ historyLoading }), - addAnalyzingHistoryHash: (hash) => - set((state) => ({ - analyzingHistoryForHashes: new Set([...state.analyzingHistoryForHashes, hash]), - })), - removeAnalyzingHistoryHash: (hash) => - set((state) => { - const next = new Set(state.analyzingHistoryForHashes); - next.delete(hash); - return { analyzingHistoryForHashes: next }; - }), - setSettingsOpen: (settingsOpen, tab) => - set({ settingsOpen, settingsActiveTab: tab ?? null }), - setPrefsLoaded: (prefsLoaded) => set({ prefsLoaded }), - setShowHistory: (showHistory) => set({ showHistory }), - setShowFilesystem: (showFilesystem, section = null) => - set({ showFilesystem, filesystemTargetSection: showFilesystem ? section : null }), - setFeedbackOpen: (feedbackOpen) => set({ feedbackOpen }), - setFeedbackTypeOverride: (feedbackTypeOverride) => set({ feedbackTypeOverride }), - openFeedback: (type, initialText) => - set({ - feedbackOpen: true, - feedbackTypeOverride: type ?? null, - feedbackInitialText: initialText ?? null, - }), - setError: (error) => set({ error }), - setPanicDetails: (panicDetails) => set({ panicDetails }), - setPromptHistory: (promptHistory) => set({ promptHistory }), - setRecommendedPrompt: (recommendedPrompt) => set({ recommendedPrompt }), - - // Client-side UI state (NOT from server) - setBootstrapping: (isBootstrapping) => set({ isBootstrapping }), - setNixInstalled: (nixInstalled) => set({ nixInstalled }), - setNixInstalling: (nixInstalling) => set({ nixInstalling }), - setNixInstallPhase: (nixInstallPhase) => set({ nixInstallPhase }), - setNixDownloadProgress: (nixDownloadProgress) => set({ nixDownloadProgress }), - setDarwinRebuildAvailable: (darwinRebuildAvailable) => set({ darwinRebuildAvailable }), - setSummarizing: (isSummarizing) => set({ isSummarizing }), - setGenerating: (isGenerating) => set({ isGenerating }), - - // Console - appendLog: (text) => set((state) => ({ consoleLogs: state.consoleLogs + text })), - clearLogs: () => set({ consoleLogs: "" }), - - // Evolve events - appendEvolveEvent: (event) => - set((state) => ({ evolveEvents: [...state.evolveEvents, event] })), - clearEvolveEvents: () => set({ evolveEvents: [] }), - setEvolutionTelemetry: (evolutionTelemetry) => set({ evolutionTelemetry }), - - // Conversational response - setConversationalResponse: (conversationalResponse) => set({ conversationalResponse }), - - // Commit message suggestion - setCommitMessageSuggestion: (commitMessageSuggestion) => set({ commitMessageSuggestion }), - - // Rebuild state - startRebuild: (context) => - set({ - rebuild: { - isRunning: true, - context, - lines: [{ id: 0, text: "Preparing rebuild...", type: "info" }], - rawLines: [], - exitCode: undefined, - success: undefined, - errorType: undefined, - errorMessage: undefined, - }, - }), - appendRebuildLine: (line) => - set((state) => ({ - rebuild: { - ...state.rebuild, - lines: [...state.rebuild.lines, line].slice(-50), // Keep last 50 lines - }, - })), - appendRawLine: (line) => - set((state) => ({ - rebuild: { - ...state.rebuild, - rawLines: [...state.rebuild.rawLines, line].slice(-500), // Keep last 500 raw lines - }, - })), - setRebuildError: (errorType, errorMessage) => - set((state) => ({ - rebuild: { - ...state.rebuild, - errorType, - errorMessage, - }, - })), - setRebuildComplete: (success, exitCode) => - set((state) => ({ - rebuild: { - ...state.rebuild, - isRunning: false, - success, - exitCode, - }, - })), - clearRebuild: () => set({ rebuild: initialRebuildState }), + (set, get, api) => ({ + ...createSetupSlice(set, get, api), + ...createConfigSlice(set, get, api), + ...createEvolveSlice(set, get, api), + ...createGitSlice(set, get, api), + ...createRebuildSlice(set, get, api), + ...createHistorySlice(set, get, api), + ...createSummarySlice(set, get, api), + ...createConsoleSlice(set, get, api), + ...initialState, }), { name: "widget-store", @@ -263,10 +43,6 @@ export function createWidgetStore(initialState?: Partial) { ); } -// ============================================================================= -// Default Store Instance -// ============================================================================= - /** * Default store instance for the main app. * Use createWidgetStore() for isolated testing instances. @@ -275,8 +51,10 @@ export const useWidgetStore = createWidgetStore(); /** * Hook to get the current widget step. - * Uses a selector so components only re-render when the step actually changes. + * Reads from the ViewModel store + the peer UI store (showHistory/showFilesystem live there). */ export function useCurrentStep(): WidgetStep { - return useWidgetStore((state) => computeCurrentStep(state)); + const widgetState = useWidgetStore(); + const uiState = useUiStore(); + return computeCurrentStep({ ...widgetState, ...uiState }); } diff --git a/apps/native/src/types/ui.ts b/apps/native/src/types/ui.ts deleted file mode 100644 index f4f4d92be..000000000 --- a/apps/native/src/types/ui.ts +++ /dev/null @@ -1,270 +0,0 @@ -import type { - EvolutionTelemetry, - EvolveEvent, - EvolveState, - FileDiffContents, - GitStatus, - HistoryItem, - PermissionsState, - RecommendedPrompt, - SemanticChangeMap, - UpdateChannel, -} from "@/ipc/types"; -import type { FeedbackType } from "@/types/feedback"; - -export type SettingsTab = "general" | "api-keys" | "ai-models" | "preferences" | "developer"; - -export type WidgetStep = - | "permissions" - | "nix-setup" - | "setup" - | "begin" - | "evolve" - | "commit" - | "manualEvolve" - | "manualCommit" - | "history" - | "filesystem"; - -type ProcessingAction = "evolve" | "apply" | "merge" | "cancel" | null; - -export type ConfirmPrefKey = "confirmBuild" | "confirmClear" | "confirmRollback"; -export type BoolPrefKey = - | ConfirmPrefKey - | "autoSummarizeOnFocus" - | "scanHomebrewOnStartup" - | "defaultToDiffTab"; - -export type RebuildErrorType = - | "infinite_recursion" - | "evaluation_error" - | "build_error" - | "full_disk_access" - | "user_cancelled" - | "authorization_denied" - | "generic_error"; - -export interface RebuildLine { - id: number; - text: string; - type: "stdout" | "stderr" | "info"; -} - -export type RebuildContext = "rollback" | "apply"; - -export interface RebuildState { - isRunning: boolean; - context: RebuildContext; - lines: RebuildLine[]; - rawLines: string[]; - exitCode?: number; - success?: boolean; - errorType?: RebuildErrorType; - errorMessage?: string; -} - -export const initialRebuildState: RebuildState = { - isRunning: false, - context: "apply", - lines: [], - rawLines: [], - exitCode: undefined, - success: undefined, - errorType: undefined, - errorMessage: undefined, -}; - -export interface WidgetState { - // Permissions (checked on startup) - permissionsState: PermissionsState | null; - permissionsChecked: boolean; - - // Config (from backend) - configDir: string; - hosts: string[]; - host: string; - - // Bootstrap (creating default config) - isBootstrapping: boolean; - - // Nix installation - nixInstalled: boolean | null; // null = not checked yet - nixInstalling: boolean; - nixInstallPhase: "downloading" | "waiting-for-installer" | "prefetching" | null; - nixDownloadProgress: { downloaded: number; total: number } | null; - - // nix-darwin (darwin-rebuild availability) - darwinRebuildAvailable: boolean | null; // null = not checked yet - - // Evolve state derived from backend source of truth - evolveState: EvolveState | null; - externalBuildDetected: boolean; - - // Git (from backend) - gitStatus: GitStatus | null; - fileDiffContents: Record; - - // Evolution - evolvePrompt: string; - isProcessing: boolean; - processingAction: ProcessingAction; - evolveEvents: EvolveEvent[]; - promptHistory: string[]; - conversationalResponse: string | null; - evolutionTelemetry: EvolutionTelemetry | null; - - changeMap: SemanticChangeMap | null; - - // Commit message suggestion (generated on merge screen) - commitMessageSuggestion: string | null; - - // Rebuild state (for inline rebuild progress) - rebuild: RebuildState; - - // Console - consoleLogs: string; - - // History - history: HistoryItem[]; - historyLoading: boolean; - analyzingHistoryForHashes: Set; - - // UI - isSummarizing: boolean; - isGenerating: boolean; - settingsOpen: boolean; - settingsActiveTab: SettingsTab | null; - prefsLoaded: boolean; - showHistory: boolean; - showFilesystem: boolean; - /** - * Optional initial section to focus when the Filesystem view opens - * (e.g. when "View" on the Untracked banner is clicked, this is set - * to "manage"). The view consumes and clears it on mount. `null` - * means "use the view's default." - */ - filesystemTargetSection: string | null; - feedbackOpen: boolean; - feedbackTypeOverride: FeedbackType | null; - feedbackInitialText: string | null; - panicDetails: { - message: string; - location?: string; - backtrace?: string; - timestamp: string; - } | null; - error: string | null; - // `undefined` means "stale/unfetched", while `null` means "fetched and none found". - recommendedPrompt: RecommendedPrompt | null | undefined; - - // Confirmation preferences - confirmBuild: boolean; - confirmClear: boolean; - confirmRollback: boolean; - - // Summarization preferences - autoSummarizeOnFocus: boolean; - - // Startup scanning preferences - scanHomebrewOnStartup: boolean; - - // Default-tab preference - defaultToDiffTab: boolean; - - // Developer mode (hidden settings panel for bisecting / pinning to a past release) - developerMode: boolean; - pinnedVersion: string | null; - updateChannel: UpdateChannel; - - // Editor - editingFile: string | null; -} - -export interface WidgetActions { - // Permissions - setPermissionsState: (state: PermissionsState | null) => void; - setPermissionsChecked: (checked: boolean) => void; - - // Setters - setConfigDir: (dir: string) => void; - setHosts: (hosts: string[]) => void; - setHost: (host: string) => void; - setBootstrapping: (isBootstrapping: boolean) => void; - setNixInstalled: (installed: boolean | null) => void; - setNixInstalling: (installing: boolean) => void; - setNixInstallPhase: ( - phase: "downloading" | "waiting-for-installer" | "prefetching" | null, - ) => void; - setNixDownloadProgress: (progress: { downloaded: number; total: number } | null) => void; - setDarwinRebuildAvailable: (available: boolean | null) => void; - setEvolveState: (state: EvolveState | null) => void; - setExternalBuildDetected: (detected: boolean) => void; - setGitStatus: (status: GitStatus | null) => void; - setFileDiffContents: (contents: Record) => void; - setEvolvePrompt: (prompt: string) => void; - setProcessing: (isProcessing: boolean, action?: ProcessingAction) => void; - setChangeMap: (map: SemanticChangeMap | null) => void; - setSettingsOpen: (open: boolean, tab?: SettingsTab | null) => void; - setPrefsLoaded: (loaded: boolean) => void; - setShowHistory: (show: boolean) => void; - /** - * @param section optional initial section id; when omitted on a - * `show=true` call the view falls back to its default section. - */ - setShowFilesystem: (show: boolean, section?: string | null) => void; - setFeedbackOpen: (open: boolean) => void; - setError: (error: string | null) => void; - setPanicDetails: ( - details: { message: string; location?: string; backtrace?: string; timestamp: string } | null, - ) => void; - setPromptHistory: (history: string[]) => void; - setRecommendedPrompt: (prompt: RecommendedPrompt | null | undefined) => void; - - // History - setHistory: (history: HistoryItem[]) => void; - setHistoryLoading: (loading: boolean) => void; - addAnalyzingHistoryHash: (hash: string) => void; - removeAnalyzingHistoryHash: (hash: string) => void; - - // Boolean preferences - setBoolPref: (key: BoolPrefKey, value: boolean) => void; - initConfirmPrefs: (prefs: Partial>) => void; - - // Summarization preferences - setAutoSummarizeOnFocus: (value: boolean) => void; - - // Developer mode - setDeveloperMode: (value: boolean) => void; - setPinnedVersion: (value: string | null) => void; - setUpdateChannel: (value: UpdateChannel) => void; - - // Client-side state (NOT from server) - setSummarizing: (summarizing: boolean) => void; - setGenerating: (generating: boolean) => void; - setFeedbackTypeOverride: (type: FeedbackType | null) => void; - openFeedback: (type?: FeedbackType, initialText?: string) => void; - - // Console - appendLog: (text: string) => void; - clearLogs: () => void; - - // Evolve events - appendEvolveEvent: (event: EvolveEvent) => void; - clearEvolveEvents: () => void; - setEvolutionTelemetry: (telemetry: EvolutionTelemetry | null) => void; - - setConversationalResponse: (response: string | null) => void; - - // Commit message suggestion - setCommitMessageSuggestion: (msg: string | null) => void; - - // Rebuild state - startRebuild: (context: RebuildContext) => void; - appendRebuildLine: (line: RebuildLine) => void; - appendRawLine: (line: string) => void; - setRebuildError: (errorType: RebuildErrorType, errorMessage: string) => void; - setRebuildComplete: (success: boolean, exitCode?: number) => void; - clearRebuild: () => void; -} - -export type WidgetStore = WidgetState & WidgetActions; From 987a8e95011d30f4c49454b0669e3c62b12302f1 Mon Sep 17 00:00:00 2001 From: CasLinden Date: Fri, 29 May 2026 19:10:35 +0900 Subject: [PATCH 5/7] chore(imports): update store imports in hooks, components, utils --- .../widget/controls/bootstrap-config.tsx | 27 ++- .../widget/controls/confirm-button.tsx | 5 +- .../widget/feedback/feedback-dialog.tsx | 173 +++++++++++++----- .../widget/feedback/report-issue-button.tsx | 4 +- .../widget/filesystem/filesystem-step.tsx | 10 +- .../widget/layout/error-message.tsx | 15 +- .../src/components/widget/layout/header.tsx | 42 +++-- .../widget/layout/merge-section.tsx | 7 +- .../overlays/config-edit-overlay-panel.tsx | 5 +- .../widget/overlays/editor-panel.tsx | 6 +- .../widget/overlays/rebuild-overlay-panel.tsx | 5 +- .../promptinput/begin-evolve-warning.tsx | 3 +- .../widget/promptinput/homebrew-badge.tsx | 17 +- .../promptinput/mac-recommendation-chip.tsx | 4 +- .../promptinput/prompt-history-badge.tsx | 20 +- .../widget/promptinput/prompt-input.tsx | 13 +- .../promptinput/system-defaults-cta.tsx | 6 +- .../widget/settings/developer-tab.tsx | 98 ++++++---- .../widget/settings/general-tab.tsx | 54 ++++-- .../widget/settings/preferences-tab.tsx | 54 ++++-- .../widget/settings/settings-dialog.tsx | 81 +++++--- .../components/widget/steps/begin-step.tsx | 6 +- .../widget/steps/nix-setup-step.tsx | 85 +++++++-- .../widget/summaries/collapsible-diff.tsx | 4 +- .../widget/summaries/summary-or-diff.tsx | 14 +- apps/native/src/components/widget/widget.tsx | 64 ++++--- apps/native/src/hooks/use-apply.ts | 7 +- apps/native/src/hooks/use-darwin-config.ts | 20 +- apps/native/src/hooks/use-evolve.ts | 51 +++--- .../src/hooks/use-feedback-on-recovery.ts | 16 +- apps/native/src/hooks/use-git-operations.ts | 20 +- apps/native/src/hooks/use-history-restore.ts | 3 +- apps/native/src/hooks/use-history.ts | 9 +- apps/native/src/hooks/use-homebrew-diff.ts | 5 +- apps/native/src/hooks/use-nix-install.ts | 90 +++++---- apps/native/src/hooks/use-panic-handler.ts | 16 +- apps/native/src/hooks/use-prefs.ts | 34 ++-- apps/native/src/hooks/use-prompt-history.ts | 4 +- apps/native/src/hooks/use-queue-summarizer.ts | 3 +- apps/native/src/hooks/use-rebuild-stream.ts | 89 +++++---- apps/native/src/hooks/use-rollback.ts | 16 +- apps/native/src/hooks/use-summary.ts | 12 +- apps/native/src/hooks/use-tray-events.ts | 10 +- apps/native/src/hooks/use-updater.ts | 29 ++- apps/native/src/hooks/use-watcher.ts | 25 ++- apps/native/src/utils/widget-test-helpers.ts | 14 +- 46 files changed, 849 insertions(+), 446 deletions(-) diff --git a/apps/native/src/components/widget/controls/bootstrap-config.tsx b/apps/native/src/components/widget/controls/bootstrap-config.tsx index f6c8a4ad7..e9c3b89db 100644 --- a/apps/native/src/components/widget/controls/bootstrap-config.tsx +++ b/apps/native/src/components/widget/controls/bootstrap-config.tsx @@ -3,8 +3,9 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { useDarwinConfig } from "@/hooks/use-darwin-config"; -import { useWidgetStore } from "@/stores/widget-store"; import { tauriAPI } from "@/ipc/api"; +import { useFeedbackStore } from "@/stores/feedback-store"; +import { useWidgetStore } from "@/stores/widget-store"; import { AlertCircle, GitCommit, Sparkles } from "lucide-react"; import { useEffect, useState } from "react"; @@ -27,16 +28,24 @@ export function BootstrapConfig({ label, onSuccess }: BootstrapConfigProps) { setFlakeExists(false); return; } - tauriAPI.flake.existsAt(configDir).then(setFlakeExists).catch(() => setFlakeExists(false)); + tauriAPI.flake + .existsAt(configDir) + .then(setFlakeExists) + .catch(() => setFlakeExists(false)); }, [configDir]); - const needsInitialCommit = flakeExists && (gitStatus === null || gitStatus.headCommitHash === ""); + const needsInitialCommit = + flakeExists && (gitStatus === null || gitStatus.headCommitHash === ""); const message = needsInitialCommit ? "flake.nix found but not committed — Nix needs a git commit to evaluate your flake" : "No nix-darwin configuration found in this directory"; - const buttonLabel = needsInitialCommit ? "Make initial commit" : "Create Default Configuration"; - const loadingLabel = needsInitialCommit ? "Committing..." : "Creating Configuration..."; + const buttonLabel = needsInitialCommit + ? "Make initial commit" + : "Create Default Configuration"; + const loadingLabel = needsInitialCommit + ? "Committing..." + : "Creating Configuration..."; const helpText = needsInitialCommit ? "Stages all files and creates the first commit" : "This will create a basic nix-darwin flake in the directory"; @@ -44,7 +53,7 @@ export function BootstrapConfig({ label, onSuccess }: BootstrapConfigProps) { const handleBootstrap = async (): Promise => { setLocalError(null); await bootstrap(needsInitialCommit ? "" : hostname); - const storeError = useWidgetStore.getState().error; + const storeError = useFeedbackStore.getState().error; if (storeError) { setLocalError(storeError); } else { @@ -86,7 +95,9 @@ export function BootstrapConfig({ label, onSuccess }: BootstrapConfigProps) { onClick={handleBootstrap} className="w-full" data-testid="create-default-config-button" - disabled={(!needsInitialCommit && !hostname.trim()) || isBootstrapping} + disabled={ + (!needsInitialCommit && !hostname.trim()) || isBootstrapping + } > {isBootstrapping ? ( <> @@ -111,4 +122,4 @@ export function BootstrapConfig({ label, onSuccess }: BootstrapConfigProps) { ); -} \ No newline at end of file +} diff --git a/apps/native/src/components/widget/controls/confirm-button.tsx b/apps/native/src/components/widget/controls/confirm-button.tsx index 2e4bff88e..5cd0d6c60 100644 --- a/apps/native/src/components/widget/controls/confirm-button.tsx +++ b/apps/native/src/components/widget/controls/confirm-button.tsx @@ -4,8 +4,7 @@ import { Button } from "@/components/ui/button"; import { CheckConfirmationOff } from "@/components/widget/controls/check-confirmation-off"; import { ConfirmationDialog } from "@/components/widget/controls/confirmation-dialog"; import { usePrefs } from "@/hooks/use-prefs"; -import { useWidgetStore } from "@/stores/widget-store"; -import type { ConfirmPrefKey } from "@/types/ui"; +import { usePrefStore, type ConfirmPrefKey } from "@/stores/pref-store"; import type { ComponentProps } from "react"; import { useState } from "react"; @@ -24,7 +23,7 @@ export function ConfirmButton({ children, ...buttonProps }: ConfirmButtonProps) { - const confirm = useWidgetStore((s) => s[confirmPrefKey]); + const confirm = usePrefStore((s) => s[confirmPrefKey]); const { setPref } = usePrefs(); const [open, setOpen] = useState(false); diff --git a/apps/native/src/components/widget/feedback/feedback-dialog.tsx b/apps/native/src/components/widget/feedback/feedback-dialog.tsx index df08c3245..149a83c8a 100644 --- a/apps/native/src/components/widget/feedback/feedback-dialog.tsx +++ b/apps/native/src/components/widget/feedback/feedback-dialog.tsx @@ -1,7 +1,8 @@ -import { useEffect, useState } from "react"; import * as Sentry from "@sentry/react"; +import { useEffect, useState } from "react"; -import { useCurrentStep, useWidgetStore } from "@/stores/widget-store"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, @@ -10,12 +11,9 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; -import { Checkbox } from "@/components/ui/checkbox"; -import { Textarea } from "@/components/ui/textarea"; -import { Input } from "@/components/ui/input"; import { Select, SelectContent, @@ -23,10 +21,21 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { Lightbulb, Bug, MessageCircle, Info, Loader2 } from "lucide-react"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; -import { Feedback as FeedbackModel, FeedbackType, ShareOptions } from "@/types/feedback"; +import { Textarea } from "@/components/ui/textarea"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { tauriAPI } from "@/ipc/api"; +import { useFeedbackStore } from "@/stores/feedback-store"; +import { useCurrentStep } from "@/stores/widget-store"; +import { + Feedback as FeedbackModel, + FeedbackType, + ShareOptions, +} from "@/types/feedback"; +import { Bug, Info, Lightbulb, Loader2, MessageCircle } from "lucide-react"; import { toast } from "sonner"; const DEFAULT_SHARE_OPTIONS: ShareOptions = { @@ -274,22 +283,28 @@ function shouldShowAppLogs( } export function FeedbackDialog() { - const feedbackOpen = useWidgetStore((s) => s.feedbackOpen); - const setFeedbackOpen = useWidgetStore((s) => s.setFeedbackOpen); - const feedbackTypeOverride = useWidgetStore((s) => s.feedbackTypeOverride); - const feedbackInitialText = useWidgetStore((s) => s.feedbackInitialText); - const panicDetails = useWidgetStore((s) => s.panicDetails); - const setFeedbackTypeOverride = useWidgetStore((s) => s.setFeedbackTypeOverride); + const feedbackOpen = useFeedbackStore((s) => s.feedbackOpen); + const setFeedbackOpen = useFeedbackStore((s) => s.setFeedbackOpen); + const feedbackTypeOverride = useFeedbackStore((s) => s.feedbackTypeOverride); + const feedbackInitialText = useFeedbackStore((s) => s.feedbackInitialText); + const panicDetails = useFeedbackStore((s) => s.panicDetails); + const setFeedbackTypeOverride = useFeedbackStore( + (s) => s.setFeedbackTypeOverride, + ); const step = useCurrentStep(); - const mainWindowError = useWidgetStore((s) => s.error) ?? undefined; + const mainWindowError = useFeedbackStore((s) => s.error) ?? undefined; - const [feedbackType, setFeedbackType] = useState(FeedbackType.Suggestion); + const [feedbackType, setFeedbackType] = useState( + FeedbackType.Suggestion, + ); const [feedbackText, setFeedbackText] = useState(""); const [expectedText, setExpectedText] = useState(""); const [email, setEmail] = useState(""); const [promptHistory, setPromptHistory] = useState([]); const [relatedPrompt, setRelatedPrompt] = useState(""); - const [shareOptions, setShareOptions] = useState(DEFAULT_SHARE_OPTIONS); + const [shareOptions, setShareOptions] = useState( + DEFAULT_SHARE_OPTIONS, + ); const [submitting, setSubmitting] = useState(false); const showSentryDebugButton = import.meta.env.DEV; @@ -330,7 +345,10 @@ export function FeedbackDialog() { useEffect(() => { if (!feedbackOpen) return; - if (feedbackType === FeedbackType.Suggestion || feedbackType === FeedbackType.General) { + if ( + feedbackType === FeedbackType.Suggestion || + feedbackType === FeedbackType.General + ) { setShareOptions((prev) => ({ ...prev, evolutionLog: false })); } else if ( feedbackType === FeedbackType.Bug || @@ -351,7 +369,10 @@ export function FeedbackDialog() { setRelatedPrompt(""); setShareOptions(DEFAULT_SHARE_OPTIONS); setFeedbackTypeOverride(null); - useWidgetStore.setState({ feedbackInitialText: null, panicDetails: null }); + useFeedbackStore.setState({ + feedbackInitialText: null, + panicDetails: null, + }); }; const handleSubmit = async () => { @@ -360,9 +381,14 @@ export function FeedbackDialog() { let sentSuccessfully = false; try { - let metadata: Awaited> | null = null; + let metadata: Awaited< + ReturnType + > | null = null; try { - metadata = await tauriAPI.feedback.gatherMetadata(feedbackType, shareOptions); + metadata = await tauriAPI.feedback.gatherMetadata( + feedbackType, + shareOptions, + ); } catch (err) { // eslint-disable-next-line no-console console.warn("Failed to gather feedback metadata:", err); @@ -371,17 +397,21 @@ export function FeedbackDialog() { // Build a typed Feedback model and log it (replace with submission later) const modelType = feedbackType; const relatedPromptText = - (feedbackType === FeedbackType.Issue || feedbackType === FeedbackType.Error) && + (feedbackType === FeedbackType.Issue || + feedbackType === FeedbackType.Error) && relatedPrompt ? relatedPrompt : undefined; - const selectedPromptText = shareOptions.lastPrompt ? relatedPromptText : undefined; + const selectedPromptText = shareOptions.lastPrompt + ? relatedPromptText + : undefined; const feedbackModel = new FeedbackModel({ type: modelType, text: feedbackText, email: email || undefined, - expectedText: feedbackType === FeedbackType.Bug ? expectedText : undefined, + expectedText: + feedbackType === FeedbackType.Bug ? expectedText : undefined, share: shareOptions, // artifact fields left empty for now; will be populated by caller when collecting logs lastPromptText: selectedPromptText, @@ -394,7 +424,10 @@ export function FeedbackDialog() { buildErrorOutput: metadata?.buildErrorOutput ?? undefined, flakeInputsSnapshot: metadata?.flakeInputsSnapshot ?? undefined, appLogsContent: metadata?.appLogsContent ?? undefined, - panicDetails: feedbackType === FeedbackType.Error ? (panicDetails ?? undefined) : undefined, + panicDetails: + feedbackType === FeedbackType.Error + ? (panicDetails ?? undefined) + : undefined, }); const validation = feedbackModel.validate(); @@ -408,7 +441,9 @@ export function FeedbackDialog() { if (sent) { toast.success("Thanks — feedback sent"); } else { - toast.info("Failed to send, we'll try again next time you open the app."); + toast.info( + "Failed to send, we'll try again next time you open the app.", + ); } sentSuccessfully = true; } finally { @@ -449,7 +484,9 @@ export function FeedbackDialog() { console.warn("[debug_sentry_event] Failed to invoke Rust command:", err); } - toast.success(`Sent debug Sentry events${eventId ? ` (frontend: ${eventId})` : ""}`); + toast.success( + `Sent debug Sentry events${eventId ? ` (frontend: ${eventId})` : ""}`, + ); }; const getTextboxLabel = () => { @@ -471,7 +508,11 @@ export function FeedbackDialog() { const isIssue = feedbackType === FeedbackType.Issue; const isError = feedbackType === FeedbackType.Error; const isReportMode = isIssue || isError; - const dialogTitle = isIssue ? "Report an issue" : isError ? "Report an error" : "Give feedback"; + const dialogTitle = isIssue + ? "Report an issue" + : isError + ? "Report an error" + : "Give feedback"; const hasAutoFilledError = feedbackInitialText && isError; const dialogDescription = hasAutoFilledError ? "An error was detected. The details have been pre-filled below. Please review and submit to help us fix this issue." @@ -490,7 +531,9 @@ export function FeedbackDialog() { > - + {hasAutoFilledError && ⚠️} {dialogTitle} @@ -504,23 +547,33 @@ export function FeedbackDialog() { setFeedbackType(value as FeedbackType)} + onValueChange={(value: string) => + setFeedbackType(value as FeedbackType) + } className="grid grid-cols-3 gap-4" >