mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-16 21:53:07 +00:00
* Changes from fix/bug-fixes * feat: Refactor worktree iteration and improve error logging across services * feat: Extract URL/port patterns to module level and fix abort condition * fix: Improve IPv6 loopback handling, select component layout, and terminal UI * feat: Add thinking level defaults and adjust list row padding * Update apps/ui/src/store/app-store.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * feat: Add worktree-aware terminal creation and split options, fix npm security issues from audit * feat: Add tracked remote detection to pull dialog flow * feat: Add merge state tracking to git operations * feat: Improve merge detection and add post-merge action preferences * Update apps/ui/src/components/views/board-view/dialogs/git-pull-dialog.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update apps/ui/src/components/views/board-view/dialogs/git-pull-dialog.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * fix: Pass merge detection info to stash reapplication and handle merge state consistently * fix: Call onPulled callback in merge handlers and add validation checks --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
181 lines
6.6 KiB
TypeScript
181 lines
6.6 KiB
TypeScript
/**
|
|
* UI Cache Store - Persisted UI State for Instant Restore
|
|
*
|
|
* This lightweight Zustand store persists critical UI state to localStorage
|
|
* so that after a tab discard, the user sees their previous UI configuration
|
|
* instantly without waiting for the server.
|
|
*
|
|
* This is NOT a replacement for the app-store or the API-first settings sync.
|
|
* It's a fast cache layer that provides instant visual continuity during:
|
|
* - Tab discard recovery
|
|
* - Page reloads
|
|
* - App restarts
|
|
*
|
|
* The app-store remains the source of truth. This cache is reconciled
|
|
* when server settings are loaded (hydrateStoreFromSettings overwrites everything).
|
|
*
|
|
* Only stores UI-visual state that affects what the user sees immediately:
|
|
* - Selected project ID (to restore board context)
|
|
* - Sidebar state (open/closed, style)
|
|
* - View preferences (board view mode, collapsed sections)
|
|
*/
|
|
|
|
import { create } from 'zustand';
|
|
import { persist } from 'zustand/middleware';
|
|
import { useAppStore } from '@/store/app-store';
|
|
|
|
interface UICacheState {
|
|
/** ID of the currently selected project */
|
|
cachedProjectId: string | null;
|
|
/** Whether sidebar is open */
|
|
cachedSidebarOpen: boolean;
|
|
/** Sidebar style (unified or discord) */
|
|
cachedSidebarStyle: 'unified' | 'discord';
|
|
/** Whether worktree panel is collapsed */
|
|
cachedWorktreePanelCollapsed: boolean;
|
|
/** Collapsed nav sections */
|
|
cachedCollapsedNavSections: Record<string, boolean>;
|
|
/** Selected worktree per project (path + branch) for instant restore on PWA reload */
|
|
cachedCurrentWorktreeByProject: Record<string, { path: string | null; branch: string }>;
|
|
}
|
|
|
|
interface UICacheActions {
|
|
/** Update the cached UI state from the main app store */
|
|
updateFromAppStore: (state: Partial<UICacheState>) => void;
|
|
}
|
|
|
|
const STORE_NAME = 'automaker-ui-cache';
|
|
|
|
export const useUICacheStore = create<UICacheState & UICacheActions>()(
|
|
persist(
|
|
(set) => ({
|
|
cachedProjectId: null,
|
|
cachedSidebarOpen: true,
|
|
cachedSidebarStyle: 'unified',
|
|
cachedWorktreePanelCollapsed: false,
|
|
cachedCollapsedNavSections: {},
|
|
cachedCurrentWorktreeByProject: {},
|
|
|
|
updateFromAppStore: (state) => set(state),
|
|
}),
|
|
{
|
|
name: STORE_NAME,
|
|
version: 2,
|
|
partialize: (state) => ({
|
|
cachedProjectId: state.cachedProjectId,
|
|
cachedSidebarOpen: state.cachedSidebarOpen,
|
|
cachedSidebarStyle: state.cachedSidebarStyle,
|
|
cachedWorktreePanelCollapsed: state.cachedWorktreePanelCollapsed,
|
|
cachedCollapsedNavSections: state.cachedCollapsedNavSections,
|
|
cachedCurrentWorktreeByProject: state.cachedCurrentWorktreeByProject,
|
|
}),
|
|
migrate: (persistedState: unknown, version: number) => {
|
|
const state = persistedState as Record<string, unknown>;
|
|
if (version < 2) {
|
|
// Migration from v1: add cachedCurrentWorktreeByProject
|
|
state.cachedCurrentWorktreeByProject = {};
|
|
}
|
|
return state as unknown as UICacheState & UICacheActions;
|
|
},
|
|
}
|
|
)
|
|
);
|
|
|
|
/**
|
|
* Sync critical UI state from the main app store to the UI cache.
|
|
* Call this whenever the app store changes to keep the cache up to date.
|
|
*
|
|
* This is intentionally a function (not a hook) so it can be called
|
|
* from store subscriptions without React.
|
|
*/
|
|
export function syncUICache(appState: {
|
|
currentProject?: { id: string } | null;
|
|
sidebarOpen?: boolean;
|
|
sidebarStyle?: 'unified' | 'discord';
|
|
worktreePanelCollapsed?: boolean;
|
|
collapsedNavSections?: Record<string, boolean>;
|
|
currentWorktreeByProject?: Record<string, { path: string | null; branch: string }>;
|
|
}): void {
|
|
const update: Partial<UICacheState> = {};
|
|
|
|
if ('currentProject' in appState) {
|
|
update.cachedProjectId = appState.currentProject?.id ?? null;
|
|
}
|
|
if ('sidebarOpen' in appState) {
|
|
update.cachedSidebarOpen = appState.sidebarOpen;
|
|
}
|
|
if ('sidebarStyle' in appState) {
|
|
update.cachedSidebarStyle = appState.sidebarStyle;
|
|
}
|
|
if ('worktreePanelCollapsed' in appState) {
|
|
update.cachedWorktreePanelCollapsed = appState.worktreePanelCollapsed;
|
|
}
|
|
if ('collapsedNavSections' in appState) {
|
|
update.cachedCollapsedNavSections = appState.collapsedNavSections;
|
|
}
|
|
if ('currentWorktreeByProject' in appState) {
|
|
update.cachedCurrentWorktreeByProject = appState.currentWorktreeByProject;
|
|
}
|
|
|
|
if (Object.keys(update).length > 0) {
|
|
useUICacheStore.getState().updateFromAppStore(update);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Restore cached UI state into the main app store.
|
|
* Call this early during initialization — before server settings arrive —
|
|
* so the user sees their previous UI layout instantly on tab discard recovery
|
|
* or page reload, instead of a flash of default state.
|
|
*
|
|
* This is reconciled later when hydrateStoreFromSettings() overwrites
|
|
* the app store with authoritative server data.
|
|
*
|
|
* @param appStoreSetState - The setState function from the app store
|
|
*/
|
|
export function restoreFromUICache(
|
|
appStoreSetState: (state: Record<string, unknown>) => void
|
|
): boolean {
|
|
const cache = useUICacheStore.getState();
|
|
|
|
// Only restore if we have meaningful cached data (not just defaults)
|
|
if (cache.cachedProjectId === null) {
|
|
return false;
|
|
}
|
|
|
|
// Attempt to resolve the cached project ID to a full project object.
|
|
// At early startup the projects array may be empty (server data not yet loaded),
|
|
// but if projects are already in the store (e.g. optimistic hydration has run)
|
|
// this will restore the project context immediately so tab-discard recovery
|
|
// does not lose the selected project when cached settings are missing.
|
|
const existingProjects = useAppStore.getState().projects;
|
|
const cachedProject = existingProjects.find((p) => p.id === cache.cachedProjectId) ?? null;
|
|
|
|
const stateUpdate: Record<string, unknown> = {
|
|
sidebarOpen: cache.cachedSidebarOpen,
|
|
sidebarStyle: cache.cachedSidebarStyle,
|
|
worktreePanelCollapsed: cache.cachedWorktreePanelCollapsed,
|
|
collapsedNavSections: cache.cachedCollapsedNavSections,
|
|
};
|
|
|
|
// Restore last selected worktree per project so the board doesn't
|
|
// reset to main branch after PWA memory eviction or tab discard.
|
|
if (
|
|
cache.cachedCurrentWorktreeByProject &&
|
|
Object.keys(cache.cachedCurrentWorktreeByProject).length > 0
|
|
) {
|
|
stateUpdate.currentWorktreeByProject = cache.cachedCurrentWorktreeByProject;
|
|
}
|
|
|
|
// Restore the project context when the project object is available.
|
|
// When projects are not yet loaded (empty array), currentProject remains
|
|
// null and will be properly set later by hydrateStoreFromSettings().
|
|
if (cachedProject !== null) {
|
|
stateUpdate.currentProject = cachedProject;
|
|
}
|
|
|
|
appStoreSetState(stateUpdate);
|
|
|
|
return true;
|
|
}
|