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:
gsxdsm
2026-02-22 20:48:09 -08:00
committed by GitHub
parent 9305ecc242
commit e7504b247f
70 changed files with 3141 additions and 560 deletions

View File

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

View File

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

View File

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

View File

@@ -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',