feat: Add settingsService integration for feature defaults and improve worktree handling

This commit is contained in:
gsxdsm
2026-03-02 03:28:37 -08:00
parent 34161ccc08
commit 33a2e04bf0
6 changed files with 105 additions and 32 deletions

View File

@@ -323,7 +323,7 @@ Your entire response should be valid JSON starting with { and ending with }. No
} }
} }
await parseAndCreateFeatures(projectPath, contentForParsing, events); await parseAndCreateFeatures(projectPath, contentForParsing, events, settingsService);
logger.debug('========== generateFeaturesFromSpec() completed =========='); logger.debug('========== generateFeaturesFromSpec() completed ==========');
} }

View File

@@ -9,13 +9,16 @@ import { createLogger, atomicWriteJson, DEFAULT_BACKUP_COUNT } from '@automaker/
import { getFeaturesDir } from '@automaker/platform'; import { getFeaturesDir } from '@automaker/platform';
import { extractJsonWithArray } from '../../lib/json-extractor.js'; import { extractJsonWithArray } from '../../lib/json-extractor.js';
import { getNotificationService } from '../../services/notification-service.js'; import { getNotificationService } from '../../services/notification-service.js';
import type { SettingsService } from '../../services/settings-service.js';
import { resolvePhaseModel } from '@automaker/model-resolver';
const logger = createLogger('SpecRegeneration'); const logger = createLogger('SpecRegeneration');
export async function parseAndCreateFeatures( export async function parseAndCreateFeatures(
projectPath: string, projectPath: string,
content: string, content: string,
events: EventEmitter events: EventEmitter,
settingsService?: SettingsService
): Promise<void> { ): Promise<void> {
logger.info('========== parseAndCreateFeatures() started =========='); logger.info('========== parseAndCreateFeatures() started ==========');
logger.info(`Content length: ${content.length} chars`); logger.info(`Content length: ${content.length} chars`);
@@ -23,6 +26,37 @@ export async function parseAndCreateFeatures(
logger.info(content); logger.info(content);
logger.info('========== END CONTENT =========='); logger.info('========== END CONTENT ==========');
// Load default model and planning settings from settingsService
let defaultModel: string | undefined;
let defaultPlanningMode: string = 'skip';
let defaultRequirePlanApproval = false;
if (settingsService) {
try {
const globalSettings = await settingsService.getGlobalSettings();
const projectSettings = await settingsService.getProjectSettings(projectPath);
const defaultModelEntry =
projectSettings.defaultFeatureModel ?? globalSettings.defaultFeatureModel;
if (defaultModelEntry) {
const resolved = resolvePhaseModel(defaultModelEntry);
defaultModel = resolved.model;
}
defaultPlanningMode = globalSettings.defaultPlanningMode ?? 'skip';
defaultRequirePlanApproval = globalSettings.defaultRequirePlanApproval ?? false;
logger.info(
`[parseAndCreateFeatures] Using defaults: model=${defaultModel ?? 'none'}, planningMode=${defaultPlanningMode}, requirePlanApproval=${defaultRequirePlanApproval}`
);
} catch (settingsError) {
logger.warn(
'[parseAndCreateFeatures] Failed to load settings, using defaults:',
settingsError
);
}
}
try { try {
// Extract JSON from response using shared utility // Extract JSON from response using shared utility
logger.info('Extracting JSON from response using extractJsonWithArray...'); logger.info('Extracting JSON from response using extractJsonWithArray...');
@@ -61,7 +95,7 @@ export async function parseAndCreateFeatures(
const featureDir = path.join(featuresDir, feature.id); const featureDir = path.join(featuresDir, feature.id);
await secureFs.mkdir(featureDir, { recursive: true }); await secureFs.mkdir(featureDir, { recursive: true });
const featureData = { const featureData: Record<string, unknown> = {
id: feature.id, id: feature.id,
category: feature.category || 'Uncategorized', category: feature.category || 'Uncategorized',
title: feature.title, title: feature.title,
@@ -70,12 +104,20 @@ export async function parseAndCreateFeatures(
priority: feature.priority || 2, priority: feature.priority || 2,
complexity: feature.complexity || 'moderate', complexity: feature.complexity || 'moderate',
dependencies: feature.dependencies || [], dependencies: feature.dependencies || [],
planningMode: 'skip', planningMode: defaultPlanningMode,
requirePlanApproval: false, requirePlanApproval:
defaultPlanningMode === 'skip' || defaultPlanningMode === 'lite'
? false
: defaultRequirePlanApproval,
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
}; };
// Apply default model if available from settings
if (defaultModel) {
featureData.model = defaultModel;
}
// Use atomic write with backup support for crash protection // Use atomic write with backup support for crash protection
await atomicWriteJson(path.join(featureDir, 'feature.json'), featureData, { await atomicWriteJson(path.join(featureDir, 'feature.json'), featureData, {
backupCount: DEFAULT_BACKUP_COUNT, backupCount: DEFAULT_BACKUP_COUNT,

View File

@@ -193,7 +193,11 @@ export class CodexModelCacheService {
* Infer tier from model ID * Infer tier from model ID
*/ */
private inferTier(modelId: string): 'premium' | 'standard' | 'basic' { private inferTier(modelId: string): 'premium' | 'standard' | 'basic' {
if (modelId.includes('max') || modelId.includes('gpt-5.2-codex')) { if (
modelId.includes('max') ||
modelId.includes('gpt-5.2-codex') ||
modelId.includes('gpt-5.3-codex')
) {
return 'premium'; return 'premium';
} }
if (modelId.includes('mini')) { if (modelId.includes('mini')) {

View File

@@ -833,18 +833,11 @@ export function hydrateStoreFromSettings(settings: GlobalSettings): void {
) )
) )
: current.agentModelBySession, : current.agentModelBySession,
// Sanitize currentWorktreeByProject: only restore entries where path is null // Restore all valid worktree selections (both main branch and feature worktrees).
// (main branch). Non-null paths point to worktree directories that may have // The validation effect in use-worktrees.ts handles deleted worktrees gracefully
// been deleted while the app was closed. Restoring a stale path causes the // by resetting to main branch when the worktree list loads and the cached
// board to render an invalid worktree selection, triggering a crash loop // worktree no longer exists.
// (error boundary reloads → restores same bad path → crash again). currentWorktreeByProject: sanitizeWorktreeByProject(settings.currentWorktreeByProject),
// The use-worktrees validation effect will re-discover valid worktrees
// from the server once they load.
currentWorktreeByProject: Object.fromEntries(
Object.entries(sanitizeWorktreeByProject(settings.currentWorktreeByProject)).filter(
([, worktree]) => worktree.path === null
)
),
// UI State // UI State
worktreePanelCollapsed: settings.worktreePanelCollapsed ?? false, worktreePanelCollapsed: settings.worktreePanelCollapsed ?? false,
lastProjectDir: settings.lastProjectDir ?? '', lastProjectDir: settings.lastProjectDir ?? '',

View File

@@ -864,8 +864,8 @@ export async function refreshSettingsFromServer(): Promise<boolean> {
) )
) )
: currentAppState.agentModelBySession, : currentAppState.agentModelBySession,
// Sanitize: only restore entries with path === null (main branch). // Restore all valid worktree selections (both main branch and feature worktrees).
// Non-null paths may reference deleted worktrees, causing crash loops. // The validation effect in use-worktrees.ts handles deleted worktrees gracefully.
currentWorktreeByProject: sanitizeWorktreeByProject( currentWorktreeByProject: sanitizeWorktreeByProject(
serverSettings.currentWorktreeByProject ?? currentAppState.currentWorktreeByProject serverSettings.currentWorktreeByProject ?? currentAppState.currentWorktreeByProject
), ),

View File

@@ -2512,9 +2512,33 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
setSpecCreatingForProject: (projectPath) => set({ specCreatingForProject: projectPath }), setSpecCreatingForProject: (projectPath) => set({ specCreatingForProject: projectPath }),
isSpecCreatingForProject: (projectPath) => get().specCreatingForProject === projectPath, isSpecCreatingForProject: (projectPath) => get().specCreatingForProject === projectPath,
setDefaultPlanningMode: (mode) => set({ defaultPlanningMode: mode }), setDefaultPlanningMode: async (mode) => {
setDefaultRequirePlanApproval: (require) => set({ defaultRequirePlanApproval: require }), set({ defaultPlanningMode: mode });
setDefaultFeatureModel: (entry) => set({ defaultFeatureModel: entry }), try {
const httpApi = getHttpApiClient();
await httpApi.settings.updateGlobal({ defaultPlanningMode: mode });
} catch (error) {
logger.error('Failed to sync defaultPlanningMode:', error);
}
},
setDefaultRequirePlanApproval: async (require) => {
set({ defaultRequirePlanApproval: require });
try {
const httpApi = getHttpApiClient();
await httpApi.settings.updateGlobal({ defaultRequirePlanApproval: require });
} catch (error) {
logger.error('Failed to sync defaultRequirePlanApproval:', error);
}
},
setDefaultFeatureModel: async (entry) => {
set({ defaultFeatureModel: entry });
try {
const httpApi = getHttpApiClient();
await httpApi.settings.updateGlobal({ defaultFeatureModel: entry });
} catch (error) {
logger.error('Failed to sync defaultFeatureModel:', error);
}
},
setDefaultThinkingLevel: async (level) => { setDefaultThinkingLevel: async (level) => {
const currentModel = get().defaultFeatureModel; const currentModel = get().defaultFeatureModel;
@@ -2523,14 +2547,23 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
// Also update defaultFeatureModel's thinkingLevel if compatible // Also update defaultFeatureModel's thinkingLevel if compatible
if (availableLevels.includes(level)) { if (availableLevels.includes(level)) {
const updatedFeatureModel = { ...currentModel, thinkingLevel: level };
set({ set({
defaultThinkingLevel: level, defaultThinkingLevel: level,
defaultFeatureModel: { ...currentModel, thinkingLevel: level }, defaultFeatureModel: updatedFeatureModel,
}); });
// Sync to server - include defaultFeatureModel since thinkingLevel is embedded there too
try {
const httpApi = getHttpApiClient();
await httpApi.settings.updateGlobal({
defaultThinkingLevel: level,
defaultFeatureModel: updatedFeatureModel,
});
} catch (error) {
logger.error('Failed to sync defaultThinkingLevel:', error);
}
} else { } else {
set({ defaultThinkingLevel: level }); set({ defaultThinkingLevel: level });
}
// Sync to server // Sync to server
try { try {
const httpApi = getHttpApiClient(); const httpApi = getHttpApiClient();
@@ -2538,6 +2571,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
} catch (error) { } catch (error) {
logger.error('Failed to sync defaultThinkingLevel:', error); logger.error('Failed to sync defaultThinkingLevel:', error);
} }
}
}, },
setDefaultReasoningEffort: async (effort) => { setDefaultReasoningEffort: async (effort) => {