mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-18 22:33:08 +00:00
Fix: Restore views properly, model selection for commit and pr and speed up some cli models with session resume (#801)
* Changes from fix/restoring-view * feat: Add resume query safety checks and optimize store selectors * feat: Improve session management and model normalization * refactor: Extract prompt building logic and handle file path parsing for renames
This commit is contained in:
@@ -275,6 +275,7 @@ const initialState: AppState = {
|
||||
collapsedNavSections: cachedUI.collapsedNavSections,
|
||||
mobileSidebarHidden: false,
|
||||
lastSelectedSessionByProject: {},
|
||||
agentModelBySession: {},
|
||||
theme: getStoredTheme() || 'dark',
|
||||
fontFamilySans: getStoredFontSans(),
|
||||
fontFamilyMono: getStoredFontMono(),
|
||||
@@ -962,11 +963,15 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
|
||||
),
|
||||
})),
|
||||
deleteChatSession: (sessionId) =>
|
||||
set((state) => ({
|
||||
chatSessions: state.chatSessions.filter((s) => s.id !== sessionId),
|
||||
currentChatSession:
|
||||
state.currentChatSession?.id === sessionId ? null : state.currentChatSession,
|
||||
})),
|
||||
set((state) => {
|
||||
const { [sessionId]: _removed, ...remainingAgentModels } = state.agentModelBySession;
|
||||
return {
|
||||
chatSessions: state.chatSessions.filter((s) => s.id !== sessionId),
|
||||
currentChatSession:
|
||||
state.currentChatSession?.id === sessionId ? null : state.currentChatSession,
|
||||
agentModelBySession: remainingAgentModels,
|
||||
};
|
||||
}),
|
||||
setChatHistoryOpen: (open) => set({ chatHistoryOpen: open }),
|
||||
toggleChatHistory: () => set((state) => ({ chatHistoryOpen: !state.chatHistoryOpen })),
|
||||
|
||||
@@ -1598,6 +1603,16 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
|
||||
})),
|
||||
getLastSelectedSession: (projectPath) => get().lastSelectedSessionByProject[projectPath] ?? null,
|
||||
|
||||
// Agent model selection actions
|
||||
setAgentModelForSession: (sessionId, model) =>
|
||||
set((state) => ({
|
||||
agentModelBySession: {
|
||||
...state.agentModelBySession,
|
||||
[sessionId]: model,
|
||||
},
|
||||
})),
|
||||
getAgentModelForSession: (sessionId) => get().agentModelBySession[sessionId] ?? null,
|
||||
|
||||
// Board Background actions
|
||||
setBoardBackground: (projectPath, imagePath) =>
|
||||
set((state) => ({
|
||||
|
||||
@@ -83,6 +83,8 @@ export interface AppState {
|
||||
|
||||
// Agent Session state (per-project, keyed by project path)
|
||||
lastSelectedSessionByProject: Record<string, string>; // projectPath -> sessionId
|
||||
// Agent model selection (per-session, keyed by sessionId)
|
||||
agentModelBySession: Record<string, PhaseModelEntry>; // sessionId -> model selection
|
||||
|
||||
// Theme
|
||||
theme: ThemeMode;
|
||||
@@ -669,6 +671,9 @@ export interface AppActions {
|
||||
// Agent Session actions
|
||||
setLastSelectedSession: (projectPath: string, sessionId: string | null) => void;
|
||||
getLastSelectedSession: (projectPath: string) => string | null;
|
||||
// Agent model selection actions
|
||||
setAgentModelForSession: (sessionId: string, model: PhaseModelEntry) => void;
|
||||
getAgentModelForSession: (sessionId: string) => PhaseModelEntry | null;
|
||||
|
||||
// Board Background actions
|
||||
setBoardBackground: (projectPath: string, imagePath: string | null) => void;
|
||||
|
||||
@@ -81,6 +81,38 @@ export const useUICacheStore = create<UICacheState & UICacheActions>()(
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Check whether an unknown value is a valid cached worktree entry.
|
||||
* Accepts objects with a non-empty string branch and a path that is null or a string.
|
||||
*/
|
||||
function isValidCachedWorktreeEntry(
|
||||
worktree: unknown
|
||||
): worktree is { path: string | null; branch: string } {
|
||||
return (
|
||||
typeof worktree === 'object' &&
|
||||
worktree !== null &&
|
||||
typeof (worktree as Record<string, unknown>).branch === 'string' &&
|
||||
((worktree as Record<string, unknown>).branch as string).trim().length > 0 &&
|
||||
((worktree as Record<string, unknown>).path === null ||
|
||||
typeof (worktree as Record<string, unknown>).path === 'string')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter a raw worktree map, discarding entries that fail structural validation.
|
||||
*/
|
||||
function sanitizeCachedWorktreeByProject(
|
||||
raw: Record<string, unknown>
|
||||
): Record<string, { path: string | null; branch: string }> {
|
||||
const sanitized: Record<string, { path: string | null; branch: string }> = {};
|
||||
for (const [key, worktree] of Object.entries(raw)) {
|
||||
if (isValidCachedWorktreeEntry(worktree)) {
|
||||
sanitized[key] = worktree;
|
||||
}
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -114,24 +146,14 @@ export function syncUICache(appState: {
|
||||
update.cachedCollapsedNavSections = appState.collapsedNavSections;
|
||||
}
|
||||
if ('currentWorktreeByProject' in appState && appState.currentWorktreeByProject) {
|
||||
// Sanitize on write: only persist entries where path is null (main branch).
|
||||
// Non-null paths point to worktree directories on disk that may be deleted
|
||||
// while the app is not running. Persisting stale paths can cause crash loops
|
||||
// on restore (the board renders with an invalid selection, the error boundary
|
||||
// reloads, which restores the same bad cache). This mirrors the sanitization
|
||||
// in restoreFromUICache() for defense-in-depth.
|
||||
const sanitized: Record<string, { path: string | null; branch: string }> = {};
|
||||
for (const [projectPath, worktree] of Object.entries(appState.currentWorktreeByProject)) {
|
||||
if (
|
||||
typeof worktree === 'object' &&
|
||||
worktree !== null &&
|
||||
'path' in worktree &&
|
||||
worktree.path === null
|
||||
) {
|
||||
sanitized[projectPath] = worktree;
|
||||
}
|
||||
}
|
||||
update.cachedCurrentWorktreeByProject = sanitized;
|
||||
// Persist all valid worktree selections (both main branch and feature worktrees).
|
||||
// Validation against actual worktrees happens at restore time in:
|
||||
// 1. restoreFromUICache() - early restore with validation
|
||||
// 2. use-worktrees.ts - runtime validation that resets to main if deleted
|
||||
// This allows users to have their feature worktree selection persist across refreshes.
|
||||
update.cachedCurrentWorktreeByProject = sanitizeCachedWorktreeByProject(
|
||||
appState.currentWorktreeByProject as Record<string, unknown>
|
||||
);
|
||||
}
|
||||
|
||||
if (Object.keys(update).length > 0) {
|
||||
@@ -178,33 +200,18 @@ export function restoreFromUICache(
|
||||
// Restore last selected worktree per project so the board doesn't
|
||||
// reset to main branch after PWA memory eviction or tab discard.
|
||||
//
|
||||
// IMPORTANT: Only restore entries where path is null (main branch selection).
|
||||
// Non-null paths point to worktree directories on disk that may have been
|
||||
// deleted while the PWA was evicted. Restoring a stale worktree path causes
|
||||
// the board to render with an invalid selection, and if the server can't
|
||||
// validate it fast enough, the app enters an unrecoverable crash loop
|
||||
// (the error boundary reloads, which restores the same bad cache).
|
||||
// Main branch (path=null) is always valid and safe to restore.
|
||||
// Restore all valid worktree selections (both main branch and feature worktrees).
|
||||
// The validation effect in use-worktrees.ts will handle resetting to main branch
|
||||
// if the cached worktree no longer exists when worktree data loads.
|
||||
if (
|
||||
cache.cachedCurrentWorktreeByProject &&
|
||||
Object.keys(cache.cachedCurrentWorktreeByProject).length > 0
|
||||
) {
|
||||
const sanitized: Record<string, { path: string | null; branch: string }> = {};
|
||||
for (const [projectPath, worktree] of Object.entries(cache.cachedCurrentWorktreeByProject)) {
|
||||
if (
|
||||
typeof worktree === 'object' &&
|
||||
worktree !== null &&
|
||||
'path' in worktree &&
|
||||
worktree.path === null
|
||||
) {
|
||||
// Main branch selection — always safe to restore
|
||||
sanitized[projectPath] = worktree;
|
||||
}
|
||||
// Non-null paths are dropped; the app will re-discover actual worktrees
|
||||
// from the server and the validation effect in use-worktrees will handle
|
||||
// resetting to main if the cached worktree no longer exists.
|
||||
// Null/malformed entries are also dropped to prevent crashes.
|
||||
}
|
||||
// Validate structure only - keep both null (main) and non-null (worktree) paths
|
||||
// Runtime validation in use-worktrees.ts handles deleted worktrees gracefully
|
||||
const sanitized = sanitizeCachedWorktreeByProject(
|
||||
cache.cachedCurrentWorktreeByProject as Record<string, unknown>
|
||||
);
|
||||
if (Object.keys(sanitized).length > 0) {
|
||||
stateUpdate.currentWorktreeByProject = sanitized;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user