mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-18 22:33:08 +00:00
Add quick-add feature with improved workflows (#802)
* Changes from feature/quick-add * feat: Clarify system prompt and improve error handling across services. Address PR Feedback * feat: Improve PR description parsing and refactor event handling * feat: Add context options to pipeline orchestrator initialization * fix: Deduplicate React and handle CJS interop for use-sync-external-store Resolve "Cannot read properties of null (reading 'useState')" errors by deduplicating React/react-dom and ensuring use-sync-external-store is bundled together with React to prevent CJS packages from resolving to different React instances.
This commit is contained in:
@@ -188,6 +188,8 @@ interface BranchesResult {
|
||||
hasCommits: boolean;
|
||||
/** The name of the remote that the current branch is tracking (e.g. "origin"), if any */
|
||||
trackingRemote?: string;
|
||||
/** List of remote names that have a branch matching the current branch name */
|
||||
remotesWithBranch?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,6 +248,7 @@ export function useWorktreeBranches(worktreePath: string | undefined, includeRem
|
||||
isGitRepo: true,
|
||||
hasCommits: true,
|
||||
trackingRemote: result.result?.trackingRemote,
|
||||
remotesWithBranch: result.result?.remotesWithBranch,
|
||||
};
|
||||
},
|
||||
enabled: !!worktreePath,
|
||||
|
||||
@@ -120,6 +120,17 @@ export function useAutoMode(worktree?: WorktreeInfo) {
|
||||
return worktreeIsMain ? null : worktreeBranch || null;
|
||||
}, [hasWorktree, worktreeIsMain, worktreeBranch]);
|
||||
|
||||
// Use a ref for branchName inside refreshStatus to prevent the callback identity
|
||||
// from changing on every worktree switch. Without this, switching worktrees causes:
|
||||
// branchName changes → refreshStatus identity changes → useEffect fires →
|
||||
// API call → setAutoModeRunning → store update → re-render cascade → React error #185
|
||||
// On mobile Safari/PWA this cascade is especially problematic as it triggers
|
||||
// "A problem repeatedly occurred" crash loops.
|
||||
const branchNameRef = useRef(branchName);
|
||||
useEffect(() => {
|
||||
branchNameRef.current = branchName;
|
||||
}, [branchName]);
|
||||
|
||||
// Helper to look up project ID from path
|
||||
const getProjectIdFromPath = useCallback(
|
||||
(path: string): string | undefined => {
|
||||
@@ -199,6 +210,11 @@ export function useAutoMode(worktree?: WorktreeInfo) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// refreshStatus uses branchNameRef instead of branchName in its dependency array
|
||||
// to keep a stable callback identity across worktree switches. This prevents the
|
||||
// useEffect([refreshStatus]) from re-firing on every worktree change, which on
|
||||
// mobile Safari/PWA causes a cascading re-render that triggers "A problem
|
||||
// repeatedly occurred" crash loops.
|
||||
const refreshStatus = useCallback(async () => {
|
||||
if (!currentProject) return;
|
||||
|
||||
@@ -206,11 +222,15 @@ export function useAutoMode(worktree?: WorktreeInfo) {
|
||||
// refreshStatus runs before the API call completes and overwrites optimistic state
|
||||
if (isTransitioningRef.current) return;
|
||||
|
||||
// Read branchName from ref to always use the latest value without
|
||||
// adding it to the dependency array (which would destabilize the callback).
|
||||
const currentBranchName = branchNameRef.current;
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode?.status) return;
|
||||
|
||||
const result = await api.autoMode.status(currentProject.path, branchName);
|
||||
const result = await api.autoMode.status(currentProject.path, currentBranchName);
|
||||
if (result.success && result.isAutoLoopRunning !== undefined) {
|
||||
const backendIsRunning = result.isAutoLoopRunning;
|
||||
const backendRunningFeatures = result.runningFeatures ?? [];
|
||||
@@ -231,7 +251,9 @@ export function useAutoMode(worktree?: WorktreeInfo) {
|
||||
backendRunningFeatures.length === 0);
|
||||
|
||||
if (needsSync) {
|
||||
const worktreeDesc = branchName ? `worktree ${branchName}` : 'main worktree';
|
||||
const worktreeDesc = currentBranchName
|
||||
? `worktree ${currentBranchName}`
|
||||
: 'main worktree';
|
||||
if (backendIsRunning !== currentIsRunning) {
|
||||
logger.info(
|
||||
`[AutoMode] Syncing UI state with backend for ${worktreeDesc} in ${currentProject.path}: ${backendIsRunning ? 'ON' : 'OFF'}`
|
||||
@@ -239,18 +261,18 @@ export function useAutoMode(worktree?: WorktreeInfo) {
|
||||
}
|
||||
setAutoModeRunning(
|
||||
currentProject.id,
|
||||
branchName,
|
||||
currentBranchName,
|
||||
backendIsRunning,
|
||||
result.maxConcurrency,
|
||||
backendRunningFeatures
|
||||
);
|
||||
setAutoModeSessionForWorktree(currentProject.path, branchName, backendIsRunning);
|
||||
setAutoModeSessionForWorktree(currentProject.path, currentBranchName, backendIsRunning);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error syncing auto mode state with backend:', error);
|
||||
}
|
||||
}, [branchName, currentProject, setAutoModeRunning]);
|
||||
}, [currentProject, setAutoModeRunning]);
|
||||
|
||||
// On mount (and when refreshStatus identity changes, e.g. project switch),
|
||||
// query backend for current auto loop status and sync UI state.
|
||||
@@ -267,6 +289,18 @@ export function useAutoMode(worktree?: WorktreeInfo) {
|
||||
return () => clearTimeout(timer);
|
||||
}, [refreshStatus]);
|
||||
|
||||
// When the user switches worktrees, re-sync auto mode status for the new branch.
|
||||
// Uses a longer debounce (300ms) than the mount effect (150ms) to let the worktree
|
||||
// switch settle (store update, feature re-filtering, query invalidation) before
|
||||
// triggering another API call. Without this delay, on mobile Safari the cascade of
|
||||
// store mutations from the worktree switch + refreshStatus response overwhelms React's
|
||||
// batching, causing "A problem repeatedly occurred" crash loops.
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => void refreshStatus(), 300);
|
||||
return () => clearTimeout(timer);
|
||||
// branchName is the trigger; refreshStatus is stable (uses ref internally)
|
||||
}, [branchName, refreshStatus]);
|
||||
|
||||
// Periodic polling fallback when WebSocket events are stale.
|
||||
useEffect(() => {
|
||||
if (!currentProject) return;
|
||||
|
||||
@@ -32,6 +32,7 @@ import { useSetupStore } from '@/store/setup-store';
|
||||
import {
|
||||
DEFAULT_OPENCODE_MODEL,
|
||||
DEFAULT_MAX_CONCURRENCY,
|
||||
DEFAULT_PHASE_MODELS,
|
||||
getAllOpencodeModelIds,
|
||||
getAllCursorModelIds,
|
||||
migrateCursorModelIds,
|
||||
@@ -184,6 +185,7 @@ export function parseLocalStorageSettings(): Partial<GlobalSettings> | null {
|
||||
state.enabledDynamicModelIds as GlobalSettings['enabledDynamicModelIds'],
|
||||
disabledProviders: (state.disabledProviders ?? []) as GlobalSettings['disabledProviders'],
|
||||
autoLoadClaudeMd: state.autoLoadClaudeMd as boolean,
|
||||
useClaudeCodeSystemPrompt: state.useClaudeCodeSystemPrompt as boolean,
|
||||
codexAutoLoadAgents: state.codexAutoLoadAgents as GlobalSettings['codexAutoLoadAgents'],
|
||||
codexSandboxMode: state.codexSandboxMode as GlobalSettings['codexSandboxMode'],
|
||||
codexApprovalPolicy: state.codexApprovalPolicy as GlobalSettings['codexApprovalPolicy'],
|
||||
@@ -756,7 +758,7 @@ export function hydrateStoreFromSettings(settings: GlobalSettings): void {
|
||||
showQueryDevtools: settings.showQueryDevtools ?? true,
|
||||
enhancementModel: settings.enhancementModel ?? 'claude-sonnet',
|
||||
validationModel: settings.validationModel ?? 'claude-opus',
|
||||
phaseModels: settings.phaseModels ?? current.phaseModels,
|
||||
phaseModels: { ...DEFAULT_PHASE_MODELS, ...(settings.phaseModels ?? current.phaseModels) },
|
||||
defaultThinkingLevel: settings.defaultThinkingLevel ?? 'none',
|
||||
defaultReasoningEffort: settings.defaultReasoningEffort ?? 'none',
|
||||
enabledCursorModels: allCursorModels, // Always use ALL cursor models
|
||||
@@ -771,6 +773,7 @@ export function hydrateStoreFromSettings(settings: GlobalSettings): void {
|
||||
enableSubagents: settings.enableSubagents ?? true,
|
||||
subagentsSources: settings.subagentsSources ?? ['user', 'project'],
|
||||
autoLoadClaudeMd: settings.autoLoadClaudeMd ?? true,
|
||||
useClaudeCodeSystemPrompt: settings.useClaudeCodeSystemPrompt ?? true,
|
||||
skipSandboxWarning: settings.skipSandboxWarning ?? false,
|
||||
codexAutoLoadAgents: settings.codexAutoLoadAgents ?? false,
|
||||
codexSandboxMode: settings.codexSandboxMode ?? 'workspace-write',
|
||||
@@ -896,6 +899,7 @@ function buildSettingsUpdateFromStore(): Record<string, unknown> {
|
||||
enableSubagents: state.enableSubagents,
|
||||
subagentsSources: state.subagentsSources,
|
||||
autoLoadClaudeMd: state.autoLoadClaudeMd,
|
||||
useClaudeCodeSystemPrompt: state.useClaudeCodeSystemPrompt,
|
||||
skipSandboxWarning: state.skipSandboxWarning,
|
||||
codexAutoLoadAgents: state.codexAutoLoadAgents,
|
||||
codexSandboxMode: state.codexSandboxMode,
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
DEFAULT_COPILOT_MODEL,
|
||||
DEFAULT_MAX_CONCURRENCY,
|
||||
DEFAULT_PHASE_MODELS,
|
||||
getAllOpencodeModelIds,
|
||||
getAllCursorModelIds,
|
||||
getAllGeminiModelIds,
|
||||
@@ -85,6 +86,7 @@ const SETTINGS_FIELDS_TO_SYNC = [
|
||||
'enabledDynamicModelIds',
|
||||
'disabledProviders',
|
||||
'autoLoadClaudeMd',
|
||||
'useClaudeCodeSystemPrompt',
|
||||
'keyboardShortcuts',
|
||||
'mcpServers',
|
||||
'defaultEditorCommand',
|
||||
@@ -100,6 +102,7 @@ const SETTINGS_FIELDS_TO_SYNC = [
|
||||
'subagentsSources',
|
||||
'promptCustomization',
|
||||
'eventHooks',
|
||||
'featureTemplates',
|
||||
'claudeCompatibleProviders',
|
||||
'claudeApiProfiles',
|
||||
'activeClaudeApiProfileId',
|
||||
@@ -727,6 +730,7 @@ export async function refreshSettingsFromServer(): Promise<boolean> {
|
||||
serverSettings.phaseModels.memoryExtractionModel
|
||||
),
|
||||
commitMessageModel: migratePhaseModelEntry(serverSettings.phaseModels.commitMessageModel),
|
||||
prDescriptionModel: migratePhaseModelEntry(serverSettings.phaseModels.prDescriptionModel),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
@@ -785,7 +789,10 @@ export async function refreshSettingsFromServer(): Promise<boolean> {
|
||||
enableRequestLogging: serverSettings.enableRequestLogging ?? true,
|
||||
enhancementModel: serverSettings.enhancementModel,
|
||||
validationModel: serverSettings.validationModel,
|
||||
phaseModels: migratedPhaseModels ?? serverSettings.phaseModels,
|
||||
phaseModels: {
|
||||
...DEFAULT_PHASE_MODELS,
|
||||
...(migratedPhaseModels ?? serverSettings.phaseModels),
|
||||
},
|
||||
enabledCursorModels: allCursorModels, // Always use ALL cursor models
|
||||
cursorDefaultModel: sanitizedCursorDefault,
|
||||
enabledOpencodeModels: sanitizedEnabledOpencodeModels,
|
||||
@@ -797,6 +804,7 @@ export async function refreshSettingsFromServer(): Promise<boolean> {
|
||||
enabledDynamicModelIds: sanitizedDynamicModelIds,
|
||||
disabledProviders: serverSettings.disabledProviders ?? [],
|
||||
autoLoadClaudeMd: serverSettings.autoLoadClaudeMd ?? true,
|
||||
useClaudeCodeSystemPrompt: serverSettings.useClaudeCodeSystemPrompt ?? true,
|
||||
keyboardShortcuts: {
|
||||
...currentAppState.keyboardShortcuts,
|
||||
...(serverSettings.keyboardShortcuts as unknown as Partial<
|
||||
@@ -836,6 +844,8 @@ export async function refreshSettingsFromServer(): Promise<boolean> {
|
||||
recentFolders: serverSettings.recentFolders ?? [],
|
||||
// Event hooks
|
||||
eventHooks: serverSettings.eventHooks ?? [],
|
||||
// Feature templates
|
||||
featureTemplates: serverSettings.featureTemplates ?? [],
|
||||
// Codex CLI Settings
|
||||
codexAutoLoadAgents: serverSettings.codexAutoLoadAgents ?? false,
|
||||
codexSandboxMode: serverSettings.codexSandboxMode ?? 'workspace-write',
|
||||
|
||||
Reference in New Issue
Block a user