mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-19 10:43:08 +00:00
Improve pull request flow, add branch selection for worktree creation, fix auto-mode concurrency count (#787)
* Changes from fix/fetch-before-pull-fetch * feat: Improve pull request flow, add branch selection for worktree creation, fix for automode concurrency count * feat: Add validation for remote names and improve error handling * Address PR comments and mobile layout fixes * ``` refactor: Extract PR target resolution logic into dedicated service ``` * feat: Add app shell UI and improve service imports. Address PR comments * fix: Improve security validation and cache handling in git operations * feat: Add GET /list endpoint and improve parameter handling * chore: Improve validation, accessibility, and error handling across apps * chore: Format vite server port configuration * fix: Add error handling for gh pr list command and improve offline fallbacks * fix: Preserve existing PR creation time and improve remote handling
This commit is contained in:
@@ -217,6 +217,52 @@ export { defaultBackgroundSettings, defaultTerminalState, MAX_INIT_OUTPUT_LINES
|
||||
// Type definitions are imported from ./types/state-types.ts
|
||||
// AppActions interface is defined in ./types/state-types.ts
|
||||
|
||||
/**
|
||||
* Pre-populate sidebar/UI state from the UI cache at module load time.
|
||||
* This runs synchronously before createRoot().render(), so the very first
|
||||
* React render uses the correct sidebar width — eliminating the layout shift
|
||||
* (wide sidebar → collapsed) that was visible when auth was pre-populated
|
||||
* but sidebar state wasn't.
|
||||
*/
|
||||
function getInitialUIState(): {
|
||||
sidebarOpen: boolean;
|
||||
sidebarStyle: 'unified' | 'discord';
|
||||
collapsedNavSections: Record<string, boolean>;
|
||||
} {
|
||||
try {
|
||||
const raw = localStorage.getItem('automaker-ui-cache');
|
||||
if (raw) {
|
||||
const wrapper = JSON.parse(raw);
|
||||
// zustand/persist wraps state under a "state" key
|
||||
const cache = wrapper?.state;
|
||||
if (cache) {
|
||||
return {
|
||||
sidebarOpen:
|
||||
typeof cache.cachedSidebarOpen === 'boolean' ? cache.cachedSidebarOpen : true,
|
||||
sidebarStyle: cache.cachedSidebarStyle === 'discord' ? 'discord' : 'unified',
|
||||
collapsedNavSections: (() => {
|
||||
const raw = cache.cachedCollapsedNavSections;
|
||||
if (
|
||||
raw &&
|
||||
typeof raw === 'object' &&
|
||||
!Array.isArray(raw) &&
|
||||
Object.getOwnPropertyNames(raw).every((k) => typeof raw[k] === 'boolean')
|
||||
) {
|
||||
return raw as Record<string, boolean>;
|
||||
}
|
||||
return {};
|
||||
})(),
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// fall through to defaults
|
||||
}
|
||||
return { sidebarOpen: true, sidebarStyle: 'unified', collapsedNavSections: {} };
|
||||
}
|
||||
|
||||
const cachedUI = getInitialUIState();
|
||||
|
||||
const initialState: AppState = {
|
||||
projects: [],
|
||||
currentProject: null,
|
||||
@@ -224,9 +270,9 @@ const initialState: AppState = {
|
||||
projectHistory: [],
|
||||
projectHistoryIndex: -1,
|
||||
currentView: 'welcome',
|
||||
sidebarOpen: true,
|
||||
sidebarStyle: 'unified',
|
||||
collapsedNavSections: {},
|
||||
sidebarOpen: cachedUI.sidebarOpen,
|
||||
sidebarStyle: cachedUI.sidebarStyle,
|
||||
collapsedNavSections: cachedUI.collapsedNavSections,
|
||||
mobileSidebarHidden: false,
|
||||
lastSelectedSessionByProject: {},
|
||||
theme: getStoredTheme() || 'dark',
|
||||
@@ -942,6 +988,13 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
|
||||
runningTasks: [],
|
||||
branchName,
|
||||
};
|
||||
// Prevent duplicate entries - the same feature can trigger multiple
|
||||
// auto_mode_feature_start events (e.g., from execution-service and
|
||||
// pipeline-orchestrator), so we must guard against adding the same
|
||||
// taskId more than once.
|
||||
if (current.runningTasks.includes(taskId)) {
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
autoModeByWorktree: {
|
||||
...state.autoModeByWorktree,
|
||||
|
||||
@@ -14,21 +14,63 @@ interface AuthActions {
|
||||
resetAuth: () => void;
|
||||
}
|
||||
|
||||
const initialState: AuthState = {
|
||||
authChecked: false,
|
||||
isAuthenticated: false,
|
||||
settingsLoaded: false,
|
||||
};
|
||||
/**
|
||||
* Pre-flight check: if localStorage has cached settings with projects AND setup is
|
||||
* complete, we can optimistically mark auth as complete on the very first render,
|
||||
* skipping the spinner entirely. The background verify in __root.tsx will correct
|
||||
* this if the session is invalid.
|
||||
*
|
||||
* This runs synchronously at module load time — before createRoot().render() —
|
||||
* so the first React render never shows the !authChecked spinner for returning users.
|
||||
*
|
||||
* We only set settingsLoaded=true when setupComplete is also true in the cache.
|
||||
* If setupComplete is false, settingsLoaded stays false so the routing effect in
|
||||
* __root.tsx doesn't immediately redirect to /setup before the setup store is hydrated.
|
||||
* In practice, returning users who completed setup have both flags in their cache.
|
||||
*
|
||||
* Intentionally minimal: only checks for the key existence and basic structure.
|
||||
* Full hydration (project data, settings) is handled by __root.tsx after mount.
|
||||
*/
|
||||
function getInitialAuthState(): AuthState {
|
||||
try {
|
||||
const raw = localStorage.getItem('automaker-settings-cache');
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw) as {
|
||||
projects?: unknown[];
|
||||
setupComplete?: boolean;
|
||||
};
|
||||
if (parsed?.projects && Array.isArray(parsed.projects) && parsed.projects.length > 0) {
|
||||
// Returning user with cached settings — optimistically mark as authenticated.
|
||||
// Only mark settingsLoaded=true when setupComplete is confirmed in cache,
|
||||
// preventing premature /setup redirects before the setup store is hydrated.
|
||||
// Background verify in __root.tsx will fix isAuthenticated if session expired.
|
||||
const setupDone = parsed.setupComplete === true;
|
||||
return {
|
||||
authChecked: true,
|
||||
isAuthenticated: true,
|
||||
settingsLoaded: setupDone,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Corrupted localStorage or JSON parse error — fall through to cold start
|
||||
}
|
||||
return { authChecked: false, isAuthenticated: false, settingsLoaded: false };
|
||||
}
|
||||
|
||||
const initialState: AuthState = getInitialAuthState();
|
||||
|
||||
/**
|
||||
* Web authentication state.
|
||||
*
|
||||
* Intentionally NOT persisted: source of truth is server session cookie.
|
||||
* Initial state is optimistically set from localStorage cache for returning users,
|
||||
* then verified against the server in the background by __root.tsx.
|
||||
*/
|
||||
export const useAuthStore = create<AuthState & AuthActions>((set) => ({
|
||||
...initialState,
|
||||
setAuthState: (state) => {
|
||||
set({ ...state });
|
||||
},
|
||||
resetAuth: () => set(initialState),
|
||||
resetAuth: () => set({ authChecked: false, isAuthenticated: false, settingsLoaded: false }),
|
||||
}));
|
||||
|
||||
@@ -270,10 +270,39 @@ const initialInstallProgress: InstallProgress = {
|
||||
// Check if setup should be skipped (for E2E testing)
|
||||
const shouldSkipSetup = import.meta.env.VITE_SKIP_SETUP === 'true';
|
||||
|
||||
/**
|
||||
* Pre-flight check: read setupComplete from localStorage settings cache so that
|
||||
* the routing effect in __root.tsx doesn't flash /setup for returning users.
|
||||
*
|
||||
* The setup store is intentionally NOT persisted (settings sync via API), but on
|
||||
* first render the routing check fires before the initAuth useEffect can call
|
||||
* hydrateStoreFromSettings(). If setupComplete starts as false, returning users
|
||||
* who have completed setup see a /setup redirect flash.
|
||||
*
|
||||
* Reading from localStorage here is safe: it's the same key used by
|
||||
* parseLocalStorageSettings() and written by the settings sync hook.
|
||||
* On first-ever visit (no cache), this returns false as expected.
|
||||
*/
|
||||
function getInitialSetupComplete(): boolean {
|
||||
if (shouldSkipSetup) return true;
|
||||
try {
|
||||
const raw = localStorage.getItem('automaker-settings-cache');
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw) as { setupComplete?: boolean };
|
||||
if (parsed?.setupComplete === true) return true;
|
||||
}
|
||||
} catch {
|
||||
// localStorage unavailable or JSON invalid — fall through
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const initialSetupComplete = getInitialSetupComplete();
|
||||
|
||||
const initialState: SetupState = {
|
||||
isFirstRun: !shouldSkipSetup,
|
||||
setupComplete: shouldSkipSetup,
|
||||
currentStep: shouldSkipSetup ? 'complete' : 'welcome',
|
||||
isFirstRun: !shouldSkipSetup && !initialSetupComplete,
|
||||
setupComplete: initialSetupComplete,
|
||||
currentStep: initialSetupComplete ? 'complete' : 'welcome',
|
||||
|
||||
claudeCliStatus: null,
|
||||
claudeAuthStatus: null,
|
||||
@@ -316,7 +345,11 @@ export const useSetupStore = create<SetupState & SetupActions>()((set, get) => (
|
||||
resetSetup: () =>
|
||||
set({
|
||||
...initialState,
|
||||
isFirstRun: false, // Don't reset first run flag
|
||||
// Explicitly override runtime-critical fields that may be stale in the
|
||||
// module-level initialState (captured at import time from localStorage).
|
||||
setupComplete: false,
|
||||
currentStep: 'welcome',
|
||||
isFirstRun: false, // Don't reset first run flag — user has visited before
|
||||
}),
|
||||
|
||||
setIsFirstRun: (isFirstRun) => set({ isFirstRun }),
|
||||
|
||||
Reference in New Issue
Block a user