Merge remote-tracking branch 'origin/v0.15.0rc' into feature/bug-startup-warning-ignores-claude-oauth-credenti-fuzx

This commit is contained in:
Shirone
2026-02-15 17:37:17 +01:00
73 changed files with 6064 additions and 647 deletions

View File

@@ -10,14 +10,23 @@ import type { Request, Response } from 'express';
import { createLogger } from '@automaker/utils';
import { resolveModelString } from '@automaker/model-resolver';
import { CLAUDE_MODEL_MAP, type ThinkingLevel } from '@automaker/types';
import { getAppSpecPath } from '@automaker/platform';
import { simpleQuery } from '../../../providers/simple-query-service.js';
import type { SettingsService } from '../../../services/settings-service.js';
import { getPromptCustomization, getProviderByModelId } from '../../../lib/settings-helpers.js';
import { FeatureLoader } from '../../../services/feature-loader.js';
import * as secureFs from '../../../lib/secure-fs.js';
import {
buildUserPrompt,
isValidEnhancementMode,
type EnhancementMode,
} from '../../../lib/enhancement-prompts.js';
import {
extractTechnologyStack,
extractXmlElements,
extractXmlSection,
unescapeXml,
} from '../../../lib/xml-extractor.js';
const logger = createLogger('EnhancePrompt');
@@ -53,6 +62,66 @@ interface EnhanceErrorResponse {
error: string;
}
async function buildProjectContext(projectPath: string): Promise<string | null> {
const contextBlocks: string[] = [];
try {
const appSpecPath = getAppSpecPath(projectPath);
const specContent = (await secureFs.readFile(appSpecPath, 'utf-8')) as string;
const projectName = extractXmlSection(specContent, 'project_name');
const overview = extractXmlSection(specContent, 'overview');
const techStack = extractTechnologyStack(specContent);
const coreSection = extractXmlSection(specContent, 'core_capabilities');
const coreCapabilities = coreSection ? extractXmlElements(coreSection, 'capability') : [];
const summaryLines: string[] = [];
if (projectName) {
summaryLines.push(`Name: ${unescapeXml(projectName.trim())}`);
}
if (overview) {
summaryLines.push(`Overview: ${unescapeXml(overview.trim())}`);
}
if (techStack.length > 0) {
summaryLines.push(`Tech Stack: ${techStack.join(', ')}`);
}
if (coreCapabilities.length > 0) {
summaryLines.push(`Core Capabilities: ${coreCapabilities.slice(0, 10).join(', ')}`);
}
if (summaryLines.length > 0) {
contextBlocks.push(`PROJECT CONTEXT:\n${summaryLines.map((line) => `- ${line}`).join('\n')}`);
}
} catch (error) {
logger.debug('No app_spec.txt context available for enhancement', error);
}
try {
const featureLoader = new FeatureLoader();
const features = await featureLoader.getAll(projectPath);
const featureTitles = features
.map((feature) => feature.title || feature.name || feature.id)
.filter((title) => Boolean(title));
if (featureTitles.length > 0) {
const listed = featureTitles.slice(0, 30).map((title) => `- ${title}`);
contextBlocks.push(
`EXISTING FEATURES (avoid duplicates):\n${listed.join('\n')}${
featureTitles.length > 30 ? '\n- ...' : ''
}`
);
}
} catch (error) {
logger.debug('Failed to load existing features for enhancement context', error);
}
if (contextBlocks.length === 0) {
return null;
}
return contextBlocks.join('\n\n');
}
/**
* Create the enhance request handler
*
@@ -122,6 +191,10 @@ export function createEnhanceHandler(
// Build the user prompt with few-shot examples
const userPrompt = buildUserPrompt(validMode, trimmedText, true);
const projectContext = projectPath ? await buildProjectContext(projectPath) : null;
if (projectContext) {
logger.debug('Including project context in enhancement prompt');
}
// Check if the model is a provider model (like "GLM-4.5-Air")
// If so, get the provider config and resolved Claude model
@@ -156,7 +229,7 @@ export function createEnhanceHandler(
// The system prompt is combined with user prompt since some providers
// don't have a separate system prompt concept
const result = await simpleQuery({
prompt: `${systemPrompt}\n\n${userPrompt}`,
prompt: [systemPrompt, projectContext, userPrompt].filter(Boolean).join('\n\n'),
model: resolvedModel,
cwd: process.cwd(), // Enhancement doesn't need a specific working directory
maxTurns: 1,

View File

@@ -7,6 +7,7 @@ import * as secureFs from '../../../lib/secure-fs.js';
import path from 'path';
import { getErrorMessage, logError } from '../common.js';
import { getImagesDir } from '@automaker/platform';
import { sanitizeFilename } from '@automaker/utils';
export function createSaveImageHandler() {
return async (req: Request, res: Response): Promise<void> => {
@@ -39,7 +40,7 @@ export function createSaveImageHandler() {
// Generate unique filename with timestamp
const timestamp = Date.now();
const ext = path.extname(filename) || '.png';
const baseName = path.basename(filename, ext);
const baseName = sanitizeFilename(path.basename(filename, ext), 'image');
const uniqueFilename = `${baseName}-${timestamp}${ext}`;
const filePath = path.join(imagesDir, uniqueFilename);

View File

@@ -14,6 +14,7 @@ import type { GlobalSettings } from '../../../types/settings.js';
import { getErrorMessage, logError, logger } from '../common.js';
import { setLogLevel, LogLevel } from '@automaker/utils';
import { setRequestLoggingEnabled } from '../../../index.js';
import { getTerminalService } from '../../../services/terminal-service.js';
/**
* Map server log level string to LogLevel enum
@@ -57,6 +58,10 @@ export function createUpdateGlobalHandler(settingsService: SettingsService) {
}, localStorageMigrated=${(updates as any).localStorageMigrated ?? 'n/a'}`
);
// Get old settings to detect theme changes
const oldSettings = await settingsService.getGlobalSettings();
const oldTheme = oldSettings?.theme;
logger.info('[SERVER_SETTINGS_UPDATE] Calling updateGlobalSettings...');
const settings = await settingsService.updateGlobalSettings(updates);
logger.info(
@@ -64,6 +69,37 @@ export function createUpdateGlobalHandler(settingsService: SettingsService) {
settings.projects?.length ?? 0
);
// Handle theme change - regenerate terminal RC files for all projects
if ('theme' in updates && updates.theme && updates.theme !== oldTheme) {
const terminalService = getTerminalService(settingsService);
const newTheme = updates.theme;
logger.info(
`[TERMINAL_CONFIG] Theme changed from ${oldTheme} to ${newTheme}, regenerating RC files`
);
// Regenerate RC files for all projects with terminal config enabled
const projects = settings.projects || [];
for (const project of projects) {
try {
const projectSettings = await settingsService.getProjectSettings(project.path);
// Check if terminal config is enabled (global or project-specific)
const terminalConfigEnabled =
projectSettings.terminalConfig?.enabled !== false &&
settings.terminalConfig?.enabled === true;
if (terminalConfigEnabled) {
await terminalService.onThemeChange(project.path, newTheme);
logger.info(`[TERMINAL_CONFIG] Regenerated RC files for project: ${project.name}`);
}
} catch (error) {
logger.warn(
`[TERMINAL_CONFIG] Failed to regenerate RC files for project ${project.name}: ${error}`
);
}
}
}
// Apply server log level if it was updated
if ('serverLogLevel' in updates && updates.serverLogLevel) {
const level = LOG_LEVEL_MAP[updates.serverLogLevel];

View File

@@ -16,6 +16,21 @@ import { getErrorMessage, logError } from '../common.js';
const execAsync = promisify(exec);
function isUntrackedLine(line: string): boolean {
return line.startsWith('?? ');
}
function isExcludedWorktreeLine(line: string): boolean {
return line.includes('.worktrees/') || line.endsWith('.worktrees');
}
function isBlockingChangeLine(line: string): boolean {
if (!line.trim()) return false;
if (isExcludedWorktreeLine(line)) return false;
if (isUntrackedLine(line)) return false;
return true;
}
/**
* Check if there are uncommitted changes in the working directory
* Excludes .worktrees/ directory which is created by automaker
@@ -23,15 +38,7 @@ const execAsync = promisify(exec);
async function hasUncommittedChanges(cwd: string): Promise<boolean> {
try {
const { stdout } = await execAsync('git status --porcelain', { cwd });
const lines = stdout
.trim()
.split('\n')
.filter((line) => {
if (!line.trim()) return false;
// Exclude .worktrees/ directory (created by automaker)
if (line.includes('.worktrees/') || line.endsWith('.worktrees')) return false;
return true;
});
const lines = stdout.trim().split('\n').filter(isBlockingChangeLine);
return lines.length > 0;
} catch {
return false;
@@ -45,15 +52,7 @@ async function hasUncommittedChanges(cwd: string): Promise<boolean> {
async function getChangesSummary(cwd: string): Promise<string> {
try {
const { stdout } = await execAsync('git status --short', { cwd });
const lines = stdout
.trim()
.split('\n')
.filter((line) => {
if (!line.trim()) return false;
// Exclude .worktrees/ directory
if (line.includes('.worktrees/') || line.endsWith('.worktrees')) return false;
return true;
});
const lines = stdout.trim().split('\n').filter(isBlockingChangeLine);
if (lines.length === 0) return '';
if (lines.length <= 5) return lines.join(', ');
return `${lines.slice(0, 5).join(', ')} and ${lines.length - 5} more files`;