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 ==========');
|
logger.debug('========== generateFeaturesFromSpec() completed ==========');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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')) {
|
||||||
|
|||||||
@@ -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 ?? '',
|
||||||
|
|||||||
@@ -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
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user