Fix: Dev server detection bug fixes. Settings sync bug fixes. Cli provider fixes. Terminal background/foreground colors (#791)

* Changes from fix/dev-server-state-bug

* feat: Add configurable max turns setting with user overrides. Address pr comments

* fix: Update default behaviors and improve state management across server and UI

* feat: Extract branch sync logic to separate service. Fix settings sync bug. Address pr comments

* refactor: Extract magic numbers to named constants and improve branch tracking logic

- Add DEFAULT_MAX_TURNS (1000) and MAX_ALLOWED_TURNS (2000) constants to settings-helpers
- Replace hardcoded 1000 values with DEFAULT_MAX_TURNS constant throughout codebase
- Improve max turns validation with explicit Number.isFinite check
- Update getTrackingBranch to split on first slash instead of last for better remote parsing
- Change isBranchCheckedOut return type from boolean to string|null to return worktree path
- Add comments explaining skipFetch parameter in worktree creation
- Fix cleanup order in AgentExecutor finally block to run before logging
```

* feat: Add comment refresh and improve model sync in PR dialog
This commit is contained in:
gsxdsm
2026-02-21 08:57:04 -08:00
committed by GitHub
parent c81ea768a7
commit 3ddf26f666
41 changed files with 2705 additions and 274 deletions

View File

@@ -108,10 +108,17 @@ export function useAutoMode(worktree?: WorktreeInfo) {
// Derive branchName from worktree:
// If worktree is provided, use its branch name (even for main worktree, as it might be on a feature branch)
// If not provided, default to null (main worktree default)
// IMPORTANT: Depend on primitive values (isMain, branch) instead of the worktree object
// reference to avoid re-computing when the parent passes a new object with the same values.
// This prevents a cascading re-render loop: new worktree ref → new branchName useMemo →
// new refreshStatus callback → effect re-fires → store update → re-render → React error #185.
const worktreeIsMain = worktree?.isMain;
const worktreeBranch = worktree?.branch;
const hasWorktree = worktree !== undefined;
const branchName = useMemo(() => {
if (!worktree) return null;
return worktree.isMain ? null : worktree.branch || null;
}, [worktree]);
if (!hasWorktree) return null;
return worktreeIsMain ? null : worktreeBranch || null;
}, [hasWorktree, worktreeIsMain, worktreeBranch]);
// Helper to look up project ID from path
const getProjectIdFromPath = useCallback(

View File

@@ -26,7 +26,6 @@ export function useProjectSettingsLoader() {
(state) => state.setAutoDismissInitScriptIndicator
);
const setWorktreeCopyFiles = useAppStore((state) => state.setWorktreeCopyFiles);
const setCurrentProject = useAppStore((state) => state.setCurrentProject);
const appliedProjectRef = useRef<{ path: string; dataUpdatedAt: number } | null>(null);
@@ -116,30 +115,39 @@ export function useProjectSettingsLoader() {
// Check if we need to update the project
const storeState = useAppStore.getState();
const updatedProject = storeState.currentProject;
if (updatedProject && updatedProject.path === projectPath) {
// snapshotProject is the store's current value at this point in time;
// it is distinct from updatedProjectData which is the new value we build below.
const snapshotProject = storeState.currentProject;
if (snapshotProject && snapshotProject.path === projectPath) {
const needsUpdate =
(activeClaudeApiProfileId !== undefined &&
updatedProject.activeClaudeApiProfileId !== activeClaudeApiProfileId) ||
snapshotProject.activeClaudeApiProfileId !== activeClaudeApiProfileId) ||
(phaseModelOverrides !== undefined &&
JSON.stringify(updatedProject.phaseModelOverrides) !==
JSON.stringify(snapshotProject.phaseModelOverrides) !==
JSON.stringify(phaseModelOverrides));
if (needsUpdate) {
const updatedProjectData = {
...updatedProject,
...snapshotProject,
...(activeClaudeApiProfileId !== undefined && { activeClaudeApiProfileId }),
...(phaseModelOverrides !== undefined && { phaseModelOverrides }),
};
// Update currentProject
setCurrentProject(updatedProjectData);
// Also update the project in the projects array to keep them in sync
// Update both currentProject and projects array in a single setState call
// to avoid two separate re-renders that can cascade during initialization
// and contribute to React error #185 (maximum update depth exceeded).
const updatedProjects = storeState.projects.map((p) =>
p.id === updatedProject.id ? updatedProjectData : p
p.id === snapshotProject.id ? updatedProjectData : p
);
useAppStore.setState({ projects: updatedProjects });
// NOTE: Intentionally bypasses setCurrentProject() to avoid a second
// render cycle that can trigger React error #185 (maximum update depth
// exceeded). This means persistEffectiveThemeForProject() is skipped,
// which is safe because only activeClaudeApiProfileId and
// phaseModelOverrides are mutated here — not the project theme.
useAppStore.setState({
currentProject: updatedProjectData,
projects: updatedProjects,
});
}
}
}, [
@@ -159,6 +167,5 @@ export function useProjectSettingsLoader() {
setDefaultDeleteBranch,
setAutoDismissInitScriptIndicator,
setWorktreeCopyFiles,
setCurrentProject,
]);
}

View File

@@ -213,6 +213,12 @@ export function parseLocalStorageSettings(): Partial<GlobalSettings> | null {
// Claude Compatible Providers (new system)
claudeCompatibleProviders:
(state.claudeCompatibleProviders as GlobalSettings['claudeCompatibleProviders']) ?? [],
// Settings that were previously missing from migration (added for sync parity)
enableAiCommitMessages: state.enableAiCommitMessages as boolean | undefined,
enableSkills: state.enableSkills as boolean | undefined,
skillsSources: state.skillsSources as GlobalSettings['skillsSources'] | undefined,
enableSubagents: state.enableSubagents as boolean | undefined,
subagentsSources: state.subagentsSources as GlobalSettings['subagentsSources'] | undefined,
};
} catch (error) {
logger.error('Failed to parse localStorage settings:', error);
@@ -357,6 +363,27 @@ export function mergeSettings(
merged.claudeCompatibleProviders = localSettings.claudeCompatibleProviders;
}
// Preserve new settings fields from localStorage if server has defaults
// Use nullish coalescing to accept stored falsy values (e.g. false)
if (localSettings.enableAiCommitMessages != null && merged.enableAiCommitMessages == null) {
merged.enableAiCommitMessages = localSettings.enableAiCommitMessages;
}
if (localSettings.enableSkills != null && merged.enableSkills == null) {
merged.enableSkills = localSettings.enableSkills;
}
if (localSettings.skillsSources && (!merged.skillsSources || merged.skillsSources.length === 0)) {
merged.skillsSources = localSettings.skillsSources;
}
if (localSettings.enableSubagents != null && merged.enableSubagents == null) {
merged.enableSubagents = localSettings.enableSubagents;
}
if (
localSettings.subagentsSources &&
(!merged.subagentsSources || merged.subagentsSources.length === 0)
) {
merged.subagentsSources = localSettings.subagentsSources;
}
return merged;
}
@@ -728,7 +755,12 @@ export function hydrateStoreFromSettings(settings: GlobalSettings): void {
opencodeDefaultModel: sanitizedOpencodeDefaultModel,
enabledDynamicModelIds: sanitizedDynamicModelIds,
disabledProviders: settings.disabledProviders ?? [],
autoLoadClaudeMd: settings.autoLoadClaudeMd ?? false,
enableAiCommitMessages: settings.enableAiCommitMessages ?? true,
enableSkills: settings.enableSkills ?? true,
skillsSources: settings.skillsSources ?? ['user', 'project'],
enableSubagents: settings.enableSubagents ?? true,
subagentsSources: settings.subagentsSources ?? ['user', 'project'],
autoLoadClaudeMd: settings.autoLoadClaudeMd ?? true,
skipSandboxWarning: settings.skipSandboxWarning ?? false,
codexAutoLoadAgents: settings.codexAutoLoadAgents ?? false,
codexSandboxMode: settings.codexSandboxMode ?? 'workspace-write',
@@ -763,11 +795,25 @@ export function hydrateStoreFromSettings(settings: GlobalSettings): void {
editorFontFamily: settings.editorFontFamily ?? 'default',
editorAutoSave: settings.editorAutoSave ?? false,
editorAutoSaveDelay: settings.editorAutoSaveDelay ?? 1000,
// Terminal font (nested in terminalState)
...(settings.terminalFontFamily && {
// Terminal settings (nested in terminalState)
...((settings.terminalFontFamily ||
(settings as unknown as Record<string, unknown>).terminalCustomBackgroundColor !==
undefined ||
(settings as unknown as Record<string, unknown>).terminalCustomForegroundColor !==
undefined) && {
terminalState: {
...current.terminalState,
fontFamily: settings.terminalFontFamily,
...(settings.terminalFontFamily && { fontFamily: settings.terminalFontFamily }),
...((settings as unknown as Record<string, unknown>).terminalCustomBackgroundColor !==
undefined && {
customBackgroundColor: (settings as unknown as Record<string, unknown>)
.terminalCustomBackgroundColor as string | null,
}),
...((settings as unknown as Record<string, unknown>).terminalCustomForegroundColor !==
undefined && {
customForegroundColor: (settings as unknown as Record<string, unknown>)
.terminalCustomForegroundColor as string | null,
}),
},
}),
});
@@ -827,6 +873,11 @@ function buildSettingsUpdateFromStore(): Record<string, unknown> {
defaultReasoningEffort: state.defaultReasoningEffort,
enabledDynamicModelIds: state.enabledDynamicModelIds,
disabledProviders: state.disabledProviders,
enableAiCommitMessages: state.enableAiCommitMessages,
enableSkills: state.enableSkills,
skillsSources: state.skillsSources,
enableSubagents: state.enableSubagents,
subagentsSources: state.subagentsSources,
autoLoadClaudeMd: state.autoLoadClaudeMd,
skipSandboxWarning: state.skipSandboxWarning,
codexAutoLoadAgents: state.codexAutoLoadAgents,
@@ -858,6 +909,8 @@ function buildSettingsUpdateFromStore(): Record<string, unknown> {
editorAutoSave: state.editorAutoSave,
editorAutoSaveDelay: state.editorAutoSaveDelay,
terminalFontFamily: state.terminalState.fontFamily,
terminalCustomBackgroundColor: state.terminalState.customBackgroundColor,
terminalCustomForegroundColor: state.terminalState.customForegroundColor,
};
}

View File

@@ -49,6 +49,8 @@ const SETTINGS_FIELDS_TO_SYNC = [
'fontFamilyMono',
'terminalFontFamily', // Maps to terminalState.fontFamily
'openTerminalMode', // Maps to terminalState.openTerminalMode
'terminalCustomBackgroundColor', // Maps to terminalState.customBackgroundColor
'terminalCustomForegroundColor', // Maps to terminalState.customForegroundColor
'sidebarOpen',
'sidebarStyle',
'collapsedNavSections',
@@ -90,8 +92,14 @@ const SETTINGS_FIELDS_TO_SYNC = [
'editorAutoSave',
'editorAutoSaveDelay',
'defaultTerminalId',
'enableAiCommitMessages',
'enableSkills',
'skillsSources',
'enableSubagents',
'subagentsSources',
'promptCustomization',
'eventHooks',
'claudeCompatibleProviders',
'claudeApiProfiles',
'activeClaudeApiProfileId',
'projects',
@@ -109,6 +117,8 @@ const SETTINGS_FIELDS_TO_SYNC = [
'codexEnableImages',
'codexAdditionalDirs',
'codexThreadId',
// Max Turns Setting
'defaultMaxTurns',
// UI State (previously in localStorage)
'worktreePanelCollapsed',
'lastProjectDir',
@@ -143,6 +153,12 @@ function getSettingsFieldValue(
if (field === 'openTerminalMode') {
return appState.terminalState.openTerminalMode;
}
if (field === 'terminalCustomBackgroundColor') {
return appState.terminalState.customBackgroundColor;
}
if (field === 'terminalCustomForegroundColor') {
return appState.terminalState.customForegroundColor;
}
if (field === 'autoModeByWorktree') {
// Only persist settings (maxConcurrency), not runtime state (isRunning, runningTasks)
const autoModeByWorktree = appState.autoModeByWorktree;
@@ -186,6 +202,16 @@ function hasSettingsFieldChanged(
if (field === 'openTerminalMode') {
return newState.terminalState.openTerminalMode !== prevState.terminalState.openTerminalMode;
}
if (field === 'terminalCustomBackgroundColor') {
return (
newState.terminalState.customBackgroundColor !== prevState.terminalState.customBackgroundColor
);
}
if (field === 'terminalCustomForegroundColor') {
return (
newState.terminalState.customForegroundColor !== prevState.terminalState.customForegroundColor
);
}
const key = field as keyof typeof newState;
return newState[key] !== prevState[key];
}
@@ -731,6 +757,7 @@ export async function refreshSettingsFromServer(): Promise<boolean> {
? migratePhaseModelEntry(serverSettings.defaultFeatureModel)
: { model: 'claude-opus' },
muteDoneSound: serverSettings.muteDoneSound,
defaultMaxTurns: serverSettings.defaultMaxTurns ?? 1000,
disableSplashScreen: serverSettings.disableSplashScreen ?? false,
serverLogLevel: serverSettings.serverLogLevel ?? 'info',
enableRequestLogging: serverSettings.enableRequestLogging ?? true,
@@ -747,7 +774,7 @@ export async function refreshSettingsFromServer(): Promise<boolean> {
copilotDefaultModel: sanitizedCopilotDefaultModel,
enabledDynamicModelIds: sanitizedDynamicModelIds,
disabledProviders: serverSettings.disabledProviders ?? [],
autoLoadClaudeMd: serverSettings.autoLoadClaudeMd ?? false,
autoLoadClaudeMd: serverSettings.autoLoadClaudeMd ?? true,
keyboardShortcuts: {
...currentAppState.keyboardShortcuts,
...(serverSettings.keyboardShortcuts as unknown as Partial<
@@ -786,7 +813,12 @@ export async function refreshSettingsFromServer(): Promise<boolean> {
codexAdditionalDirs: serverSettings.codexAdditionalDirs ?? [],
codexThreadId: serverSettings.codexThreadId,
// Terminal settings (nested in terminalState)
...((serverSettings.terminalFontFamily || serverSettings.openTerminalMode) && {
...((serverSettings.terminalFontFamily ||
serverSettings.openTerminalMode ||
(serverSettings as unknown as Record<string, unknown>).terminalCustomBackgroundColor !==
undefined ||
(serverSettings as unknown as Record<string, unknown>).terminalCustomForegroundColor !==
undefined) && {
terminalState: {
...currentAppState.terminalState,
...(serverSettings.terminalFontFamily && {
@@ -795,6 +827,16 @@ export async function refreshSettingsFromServer(): Promise<boolean> {
...(serverSettings.openTerminalMode && {
openTerminalMode: serverSettings.openTerminalMode,
}),
...((serverSettings as unknown as Record<string, unknown>)
.terminalCustomBackgroundColor !== undefined && {
customBackgroundColor: (serverSettings as unknown as Record<string, unknown>)
.terminalCustomBackgroundColor as string | null,
}),
...((serverSettings as unknown as Record<string, unknown>)
.terminalCustomForegroundColor !== undefined && {
customForegroundColor: (serverSettings as unknown as Record<string, unknown>)
.terminalCustomForegroundColor as string | null,
}),
},
}),
});