mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-16 21:53:07 +00:00
feat: Add settingsService integration for feature defaults and improve worktree handling
This commit is contained in:
@@ -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 ==========');
|
||||
}
|
||||
|
||||
@@ -9,13 +9,16 @@ import { createLogger, atomicWriteJson, DEFAULT_BACKUP_COUNT } from '@automaker/
|
||||
import { getFeaturesDir } from '@automaker/platform';
|
||||
import { extractJsonWithArray } from '../../lib/json-extractor.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');
|
||||
|
||||
export async function parseAndCreateFeatures(
|
||||
projectPath: string,
|
||||
content: string,
|
||||
events: EventEmitter
|
||||
events: EventEmitter,
|
||||
settingsService?: SettingsService
|
||||
): Promise<void> {
|
||||
logger.info('========== parseAndCreateFeatures() started ==========');
|
||||
logger.info(`Content length: ${content.length} chars`);
|
||||
@@ -23,6 +26,37 @@ export async function parseAndCreateFeatures(
|
||||
logger.info(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 {
|
||||
// Extract JSON from response using shared utility
|
||||
logger.info('Extracting JSON from response using extractJsonWithArray...');
|
||||
@@ -61,7 +95,7 @@ export async function parseAndCreateFeatures(
|
||||
const featureDir = path.join(featuresDir, feature.id);
|
||||
await secureFs.mkdir(featureDir, { recursive: true });
|
||||
|
||||
const featureData = {
|
||||
const featureData: Record<string, unknown> = {
|
||||
id: feature.id,
|
||||
category: feature.category || 'Uncategorized',
|
||||
title: feature.title,
|
||||
@@ -70,12 +104,20 @@ export async function parseAndCreateFeatures(
|
||||
priority: feature.priority || 2,
|
||||
complexity: feature.complexity || 'moderate',
|
||||
dependencies: feature.dependencies || [],
|
||||
planningMode: 'skip',
|
||||
requirePlanApproval: false,
|
||||
planningMode: defaultPlanningMode,
|
||||
requirePlanApproval:
|
||||
defaultPlanningMode === 'skip' || defaultPlanningMode === 'lite'
|
||||
? false
|
||||
: defaultRequirePlanApproval,
|
||||
createdAt: 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
|
||||
await atomicWriteJson(path.join(featureDir, 'feature.json'), featureData, {
|
||||
backupCount: DEFAULT_BACKUP_COUNT,
|
||||
|
||||
@@ -193,7 +193,11 @@ export class CodexModelCacheService {
|
||||
* Infer tier from model ID
|
||||
*/
|
||||
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';
|
||||
}
|
||||
if (modelId.includes('mini')) {
|
||||
|
||||
@@ -833,18 +833,11 @@ export function hydrateStoreFromSettings(settings: GlobalSettings): void {
|
||||
)
|
||||
)
|
||||
: current.agentModelBySession,
|
||||
// Sanitize currentWorktreeByProject: only restore entries where path is null
|
||||
// (main branch). Non-null paths point to worktree directories that may have
|
||||
// been deleted while the app was closed. Restoring a stale path causes the
|
||||
// board to render an invalid worktree selection, triggering a crash loop
|
||||
// (error boundary reloads → restores same bad path → crash again).
|
||||
// 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
|
||||
)
|
||||
),
|
||||
// Restore all valid worktree selections (both main branch and feature worktrees).
|
||||
// The validation effect in use-worktrees.ts handles deleted worktrees gracefully
|
||||
// by resetting to main branch when the worktree list loads and the cached
|
||||
// worktree no longer exists.
|
||||
currentWorktreeByProject: sanitizeWorktreeByProject(settings.currentWorktreeByProject),
|
||||
// UI State
|
||||
worktreePanelCollapsed: settings.worktreePanelCollapsed ?? false,
|
||||
lastProjectDir: settings.lastProjectDir ?? '',
|
||||
|
||||
@@ -864,8 +864,8 @@ export async function refreshSettingsFromServer(): Promise<boolean> {
|
||||
)
|
||||
)
|
||||
: currentAppState.agentModelBySession,
|
||||
// Sanitize: only restore entries with path === null (main branch).
|
||||
// Non-null paths may reference deleted worktrees, causing crash loops.
|
||||
// Restore all valid worktree selections (both main branch and feature worktrees).
|
||||
// The validation effect in use-worktrees.ts handles deleted worktrees gracefully.
|
||||
currentWorktreeByProject: sanitizeWorktreeByProject(
|
||||
serverSettings.currentWorktreeByProject ?? currentAppState.currentWorktreeByProject
|
||||
),
|
||||
|
||||
@@ -2512,9 +2512,33 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
|
||||
setSpecCreatingForProject: (projectPath) => set({ specCreatingForProject: projectPath }),
|
||||
isSpecCreatingForProject: (projectPath) => get().specCreatingForProject === projectPath,
|
||||
|
||||
setDefaultPlanningMode: (mode) => set({ defaultPlanningMode: mode }),
|
||||
setDefaultRequirePlanApproval: (require) => set({ defaultRequirePlanApproval: require }),
|
||||
setDefaultFeatureModel: (entry) => set({ defaultFeatureModel: entry }),
|
||||
setDefaultPlanningMode: async (mode) => {
|
||||
set({ defaultPlanningMode: mode });
|
||||
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) => {
|
||||
const currentModel = get().defaultFeatureModel;
|
||||
@@ -2523,14 +2547,23 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
|
||||
|
||||
// Also update defaultFeatureModel's thinkingLevel if compatible
|
||||
if (availableLevels.includes(level)) {
|
||||
const updatedFeatureModel = { ...currentModel, thinkingLevel: level };
|
||||
set({
|
||||
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 {
|
||||
set({ defaultThinkingLevel: level });
|
||||
}
|
||||
|
||||
// Sync to server
|
||||
try {
|
||||
const httpApi = getHttpApiClient();
|
||||
@@ -2538,6 +2571,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
|
||||
} catch (error) {
|
||||
logger.error('Failed to sync defaultThinkingLevel:', error);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
setDefaultReasoningEffort: async (effort) => {
|
||||
|
||||
Reference in New Issue
Block a user