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:
gsxdsm
2026-02-22 10:45:45 -08:00
committed by GitHub
parent 2f071a1ba3
commit 9305ecc242
26 changed files with 761 additions and 203 deletions

View File

@@ -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) => ({

View File

@@ -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;

View File

@@ -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;
}