mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
feat: implement server logging and event hook features
- Introduced server log level configuration and HTTP request logging settings, allowing users to control the verbosity of server logs and enable or disable request logging at runtime. - Added an Event Hook Service to execute custom actions based on system events, supporting shell commands and HTTP webhooks. - Enhanced the UI with new sections for managing server logging preferences and event hooks, including a dialog for creating and editing hooks. - Updated global settings to include server log level and request logging options, ensuring persistence across sessions. These changes aim to improve debugging capabilities and provide users with customizable event-driven actions within the application.
This commit is contained in:
@@ -17,9 +17,19 @@ import dotenv from 'dotenv';
|
||||
|
||||
import { createEventEmitter, type EventEmitter } from './lib/events.js';
|
||||
import { initAllowedPaths } from '@automaker/platform';
|
||||
import { createLogger } from '@automaker/utils';
|
||||
import { createLogger, setLogLevel, LogLevel } from '@automaker/utils';
|
||||
|
||||
const logger = createLogger('Server');
|
||||
|
||||
/**
|
||||
* Map server log level string to LogLevel enum
|
||||
*/
|
||||
const LOG_LEVEL_MAP: Record<string, LogLevel> = {
|
||||
error: LogLevel.ERROR,
|
||||
warn: LogLevel.WARN,
|
||||
info: LogLevel.INFO,
|
||||
debug: LogLevel.DEBUG,
|
||||
};
|
||||
import { authMiddleware, validateWsConnectionToken, checkRawAuthentication } from './lib/auth.js';
|
||||
import { requireJsonContentType } from './middleware/require-json-content-type.js';
|
||||
import { createAuthRoutes } from './routes/auth/index.js';
|
||||
@@ -68,13 +78,31 @@ import { pipelineService } from './services/pipeline-service.js';
|
||||
import { createIdeationRoutes } from './routes/ideation/index.js';
|
||||
import { IdeationService } from './services/ideation-service.js';
|
||||
import { getDevServerService } from './services/dev-server-service.js';
|
||||
import { eventHookService } from './services/event-hook-service.js';
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
|
||||
const PORT = parseInt(process.env.PORT || '3008', 10);
|
||||
const DATA_DIR = process.env.DATA_DIR || './data';
|
||||
const ENABLE_REQUEST_LOGGING = process.env.ENABLE_REQUEST_LOGGING !== 'false'; // Default to true
|
||||
const ENABLE_REQUEST_LOGGING_DEFAULT = process.env.ENABLE_REQUEST_LOGGING !== 'false'; // Default to true
|
||||
|
||||
// Runtime-configurable request logging flag (can be changed via settings)
|
||||
let requestLoggingEnabled = ENABLE_REQUEST_LOGGING_DEFAULT;
|
||||
|
||||
/**
|
||||
* Enable or disable HTTP request logging at runtime
|
||||
*/
|
||||
export function setRequestLoggingEnabled(enabled: boolean): void {
|
||||
requestLoggingEnabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current request logging state
|
||||
*/
|
||||
export function isRequestLoggingEnabled(): boolean {
|
||||
return requestLoggingEnabled;
|
||||
}
|
||||
|
||||
// Check for required environment variables
|
||||
const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY;
|
||||
@@ -103,22 +131,21 @@ initAllowedPaths();
|
||||
const app = express();
|
||||
|
||||
// Middleware
|
||||
// Custom colored logger showing only endpoint and status code (configurable via ENABLE_REQUEST_LOGGING env var)
|
||||
if (ENABLE_REQUEST_LOGGING) {
|
||||
morgan.token('status-colored', (_req, res) => {
|
||||
const status = res.statusCode;
|
||||
if (status >= 500) return `\x1b[31m${status}\x1b[0m`; // Red for server errors
|
||||
if (status >= 400) return `\x1b[33m${status}\x1b[0m`; // Yellow for client errors
|
||||
if (status >= 300) return `\x1b[36m${status}\x1b[0m`; // Cyan for redirects
|
||||
return `\x1b[32m${status}\x1b[0m`; // Green for success
|
||||
});
|
||||
// Custom colored logger showing only endpoint and status code (dynamically configurable)
|
||||
morgan.token('status-colored', (_req, res) => {
|
||||
const status = res.statusCode;
|
||||
if (status >= 500) return `\x1b[31m${status}\x1b[0m`; // Red for server errors
|
||||
if (status >= 400) return `\x1b[33m${status}\x1b[0m`; // Yellow for client errors
|
||||
if (status >= 300) return `\x1b[36m${status}\x1b[0m`; // Cyan for redirects
|
||||
return `\x1b[32m${status}\x1b[0m`; // Green for success
|
||||
});
|
||||
|
||||
app.use(
|
||||
morgan(':method :url :status-colored', {
|
||||
skip: (req) => req.url === '/api/health', // Skip health check logs
|
||||
})
|
||||
);
|
||||
}
|
||||
app.use(
|
||||
morgan(':method :url :status-colored', {
|
||||
// Skip when request logging is disabled or for health check endpoints
|
||||
skip: (req) => !requestLoggingEnabled || req.url === '/api/health',
|
||||
})
|
||||
);
|
||||
// CORS configuration
|
||||
// When using credentials (cookies), origin cannot be '*'
|
||||
// We dynamically allow the requesting origin for local development
|
||||
@@ -181,8 +208,26 @@ const ideationService = new IdeationService(events, settingsService, featureLoad
|
||||
const devServerService = getDevServerService();
|
||||
devServerService.setEventEmitter(events);
|
||||
|
||||
// Initialize Event Hook Service for custom event triggers
|
||||
eventHookService.initialize(events, settingsService);
|
||||
|
||||
// Initialize services
|
||||
(async () => {
|
||||
// Apply logging settings from saved settings
|
||||
try {
|
||||
const settings = await settingsService.getGlobalSettings();
|
||||
if (settings.serverLogLevel && LOG_LEVEL_MAP[settings.serverLogLevel] !== undefined) {
|
||||
setLogLevel(LOG_LEVEL_MAP[settings.serverLogLevel]);
|
||||
logger.info(`Server log level set to: ${settings.serverLogLevel}`);
|
||||
}
|
||||
// Apply request logging setting (default true if not set)
|
||||
const enableRequestLog = settings.enableRequestLogging ?? true;
|
||||
setRequestLoggingEnabled(enableRequestLog);
|
||||
logger.info(`HTTP request logging: ${enableRequestLog ? 'enabled' : 'disabled'}`);
|
||||
} catch (err) {
|
||||
logger.warn('Failed to load logging settings, using defaults');
|
||||
}
|
||||
|
||||
await agentService.initialize();
|
||||
logger.info('Agent service initialized');
|
||||
|
||||
|
||||
@@ -12,6 +12,18 @@ import type { Request, Response } from 'express';
|
||||
import type { SettingsService } from '../../../services/settings-service.js';
|
||||
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';
|
||||
|
||||
/**
|
||||
* Map server log level string to LogLevel enum
|
||||
*/
|
||||
const LOG_LEVEL_MAP: Record<string, LogLevel> = {
|
||||
error: LogLevel.ERROR,
|
||||
warn: LogLevel.WARN,
|
||||
info: LogLevel.INFO,
|
||||
debug: LogLevel.DEBUG,
|
||||
};
|
||||
|
||||
/**
|
||||
* Create handler factory for PUT /api/settings/global
|
||||
@@ -46,6 +58,23 @@ export function createUpdateGlobalHandler(settingsService: SettingsService) {
|
||||
|
||||
const settings = await settingsService.updateGlobalSettings(updates);
|
||||
|
||||
// Apply server log level if it was updated
|
||||
if ('serverLogLevel' in updates && updates.serverLogLevel) {
|
||||
const level = LOG_LEVEL_MAP[updates.serverLogLevel];
|
||||
if (level !== undefined) {
|
||||
setLogLevel(level);
|
||||
logger.info(`Server log level changed to: ${updates.serverLogLevel}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply request logging setting if it was updated
|
||||
if ('enableRequestLogging' in updates && typeof updates.enableRequestLogging === 'boolean') {
|
||||
setRequestLoggingEnabled(updates.enableRequestLogging);
|
||||
logger.info(
|
||||
`HTTP request logging ${updates.enableRequestLogging ? 'enabled' : 'disabled'}`
|
||||
);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
settings,
|
||||
|
||||
316
apps/server/src/services/event-hook-service.ts
Normal file
316
apps/server/src/services/event-hook-service.ts
Normal file
@@ -0,0 +1,316 @@
|
||||
/**
|
||||
* Event Hook Service - Executes custom actions when system events occur
|
||||
*
|
||||
* Listens to the event emitter and triggers configured hooks:
|
||||
* - Shell commands: Executed with configurable timeout
|
||||
* - HTTP webhooks: POST/GET/PUT/PATCH requests with variable substitution
|
||||
*
|
||||
* Supported events:
|
||||
* - feature_success: Feature completed successfully
|
||||
* - feature_error: Feature failed with an error
|
||||
* - auto_mode_complete: Auto mode finished all features (idle state)
|
||||
* - auto_mode_error: Auto mode encountered a critical error
|
||||
*/
|
||||
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { createLogger } from '@automaker/utils';
|
||||
import type { EventEmitter } from '../lib/events.js';
|
||||
import type { SettingsService } from './settings-service.js';
|
||||
import type {
|
||||
EventHook,
|
||||
EventHookTrigger,
|
||||
EventHookShellAction,
|
||||
EventHookHttpAction,
|
||||
} from '@automaker/types';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
const logger = createLogger('EventHooks');
|
||||
|
||||
/** Default timeout for shell commands (30 seconds) */
|
||||
const DEFAULT_SHELL_TIMEOUT = 30000;
|
||||
|
||||
/** Default timeout for HTTP requests (10 seconds) */
|
||||
const DEFAULT_HTTP_TIMEOUT = 10000;
|
||||
|
||||
/**
|
||||
* Context available for variable substitution in hooks
|
||||
*/
|
||||
interface HookContext {
|
||||
featureId?: string;
|
||||
featureName?: string;
|
||||
projectPath?: string;
|
||||
projectName?: string;
|
||||
error?: string;
|
||||
errorType?: string;
|
||||
timestamp: string;
|
||||
eventType: EventHookTrigger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-mode event payload structure
|
||||
*/
|
||||
interface AutoModeEventPayload {
|
||||
type?: string;
|
||||
featureId?: string;
|
||||
passes?: boolean;
|
||||
message?: string;
|
||||
error?: string;
|
||||
errorType?: string;
|
||||
projectPath?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event Hook Service
|
||||
*
|
||||
* Manages execution of user-configured event hooks in response to system events.
|
||||
*/
|
||||
export class EventHookService {
|
||||
private emitter: EventEmitter | null = null;
|
||||
private settingsService: SettingsService | null = null;
|
||||
private unsubscribe: (() => void) | null = null;
|
||||
|
||||
/**
|
||||
* Initialize the service with event emitter and settings service
|
||||
*/
|
||||
initialize(emitter: EventEmitter, settingsService: SettingsService): void {
|
||||
this.emitter = emitter;
|
||||
this.settingsService = settingsService;
|
||||
|
||||
// Subscribe to auto-mode events
|
||||
this.unsubscribe = emitter.subscribe((type, payload) => {
|
||||
if (type === 'auto-mode:event') {
|
||||
this.handleAutoModeEvent(payload as AutoModeEventPayload);
|
||||
}
|
||||
});
|
||||
|
||||
logger.info('Event hook service initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup subscriptions
|
||||
*/
|
||||
destroy(): void {
|
||||
if (this.unsubscribe) {
|
||||
this.unsubscribe();
|
||||
this.unsubscribe = null;
|
||||
}
|
||||
this.emitter = null;
|
||||
this.settingsService = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle auto-mode events and trigger matching hooks
|
||||
*/
|
||||
private async handleAutoModeEvent(payload: AutoModeEventPayload): Promise<void> {
|
||||
if (!payload.type) return;
|
||||
|
||||
// Map internal event types to hook triggers
|
||||
let trigger: EventHookTrigger | null = null;
|
||||
|
||||
switch (payload.type) {
|
||||
case 'auto_mode_feature_complete':
|
||||
trigger = payload.passes ? 'feature_success' : 'feature_error';
|
||||
break;
|
||||
case 'auto_mode_error':
|
||||
// Feature-level error (has featureId) vs auto-mode level error
|
||||
trigger = payload.featureId ? 'feature_error' : 'auto_mode_error';
|
||||
break;
|
||||
case 'auto_mode_idle':
|
||||
trigger = 'auto_mode_complete';
|
||||
break;
|
||||
default:
|
||||
// Other event types don't trigger hooks
|
||||
return;
|
||||
}
|
||||
|
||||
if (!trigger) return;
|
||||
|
||||
// Build context for variable substitution
|
||||
const context: HookContext = {
|
||||
featureId: payload.featureId,
|
||||
projectPath: payload.projectPath,
|
||||
projectName: payload.projectPath ? this.extractProjectName(payload.projectPath) : undefined,
|
||||
error: payload.error || payload.message,
|
||||
errorType: payload.errorType,
|
||||
timestamp: new Date().toISOString(),
|
||||
eventType: trigger,
|
||||
};
|
||||
|
||||
// Execute matching hooks
|
||||
await this.executeHooksForTrigger(trigger, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute all enabled hooks matching the given trigger
|
||||
*/
|
||||
private async executeHooksForTrigger(
|
||||
trigger: EventHookTrigger,
|
||||
context: HookContext
|
||||
): Promise<void> {
|
||||
if (!this.settingsService) {
|
||||
logger.warn('Settings service not available');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const settings = await this.settingsService.getGlobalSettings();
|
||||
const hooks = settings.eventHooks || [];
|
||||
|
||||
// Filter to enabled hooks matching this trigger
|
||||
const matchingHooks = hooks.filter((hook) => hook.enabled && hook.trigger === trigger);
|
||||
|
||||
if (matchingHooks.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Executing ${matchingHooks.length} hook(s) for trigger: ${trigger}`);
|
||||
|
||||
// Execute hooks in parallel (don't wait for one to finish before starting next)
|
||||
await Promise.allSettled(matchingHooks.map((hook) => this.executeHook(hook, context)));
|
||||
} catch (error) {
|
||||
logger.error('Error executing hooks:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a single hook
|
||||
*/
|
||||
private async executeHook(hook: EventHook, context: HookContext): Promise<void> {
|
||||
const hookName = hook.name || hook.id;
|
||||
|
||||
try {
|
||||
if (hook.action.type === 'shell') {
|
||||
await this.executeShellHook(hook.action, context, hookName);
|
||||
} else if (hook.action.type === 'http') {
|
||||
await this.executeHttpHook(hook.action, context, hookName);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Hook "${hookName}" failed:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a shell command hook
|
||||
*/
|
||||
private async executeShellHook(
|
||||
action: EventHookShellAction,
|
||||
context: HookContext,
|
||||
hookName: string
|
||||
): Promise<void> {
|
||||
const command = this.substituteVariables(action.command, context);
|
||||
const timeout = action.timeout || DEFAULT_SHELL_TIMEOUT;
|
||||
|
||||
logger.info(`Executing shell hook "${hookName}": ${command}`);
|
||||
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(command, {
|
||||
timeout,
|
||||
maxBuffer: 1024 * 1024, // 1MB buffer
|
||||
});
|
||||
|
||||
if (stdout) {
|
||||
logger.debug(`Hook "${hookName}" stdout: ${stdout.trim()}`);
|
||||
}
|
||||
if (stderr) {
|
||||
logger.warn(`Hook "${hookName}" stderr: ${stderr.trim()}`);
|
||||
}
|
||||
|
||||
logger.info(`Shell hook "${hookName}" completed successfully`);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ETIMEDOUT') {
|
||||
logger.error(`Shell hook "${hookName}" timed out after ${timeout}ms`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an HTTP webhook hook
|
||||
*/
|
||||
private async executeHttpHook(
|
||||
action: EventHookHttpAction,
|
||||
context: HookContext,
|
||||
hookName: string
|
||||
): Promise<void> {
|
||||
const url = this.substituteVariables(action.url, context);
|
||||
const method = action.method || 'POST';
|
||||
|
||||
// Substitute variables in headers
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
if (action.headers) {
|
||||
for (const [key, value] of Object.entries(action.headers)) {
|
||||
headers[key] = this.substituteVariables(value, context);
|
||||
}
|
||||
}
|
||||
|
||||
// Substitute variables in body
|
||||
let body: string | undefined;
|
||||
if (action.body) {
|
||||
body = this.substituteVariables(action.body, context);
|
||||
} else if (method !== 'GET') {
|
||||
// Default body with context information
|
||||
body = JSON.stringify({
|
||||
eventType: context.eventType,
|
||||
timestamp: context.timestamp,
|
||||
featureId: context.featureId,
|
||||
projectPath: context.projectPath,
|
||||
projectName: context.projectName,
|
||||
error: context.error,
|
||||
});
|
||||
}
|
||||
|
||||
logger.info(`Executing HTTP hook "${hookName}": ${method} ${url}`);
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), DEFAULT_HTTP_TIMEOUT);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers,
|
||||
body: method !== 'GET' ? body : undefined,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
logger.warn(`HTTP hook "${hookName}" received status ${response.status}`);
|
||||
} else {
|
||||
logger.info(`HTTP hook "${hookName}" completed successfully (status: ${response.status})`);
|
||||
}
|
||||
} catch (error) {
|
||||
if ((error as Error).name === 'AbortError') {
|
||||
logger.error(`HTTP hook "${hookName}" timed out after ${DEFAULT_HTTP_TIMEOUT}ms`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Substitute {{variable}} placeholders in a string
|
||||
*/
|
||||
private substituteVariables(template: string, context: HookContext): string {
|
||||
return template.replace(/\{\{(\w+)\}\}/g, (match, variable) => {
|
||||
const value = context[variable as keyof HookContext];
|
||||
if (value === undefined || value === null) {
|
||||
return '';
|
||||
}
|
||||
return String(value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract project name from path
|
||||
*/
|
||||
private extractProjectName(projectPath: string): string {
|
||||
const parts = projectPath.split(/[/\\]/);
|
||||
return parts[parts.length - 1] || projectPath;
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
export const eventHookService = new EventHookService();
|
||||
@@ -19,6 +19,7 @@ import { WorktreesSection } from './settings-view/worktrees';
|
||||
import { DangerZoneSection } from './settings-view/danger-zone/danger-zone-section';
|
||||
import { AccountSection } from './settings-view/account';
|
||||
import { SecuritySection } from './settings-view/security';
|
||||
import { DeveloperSection } from './settings-view/developer/developer-section';
|
||||
import {
|
||||
ClaudeSettingsTab,
|
||||
CursorSettingsTab,
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
} from './settings-view/providers';
|
||||
import { MCPServersSection } from './settings-view/mcp-servers';
|
||||
import { PromptCustomizationSection } from './settings-view/prompts';
|
||||
import { EventHooksSection } from './settings-view/event-hooks';
|
||||
import type { Project as SettingsProject, Theme } from './settings-view/shared/types';
|
||||
import type { Project as ElectronProject } from '@/lib/electron';
|
||||
|
||||
@@ -183,6 +185,8 @@ export function SettingsView() {
|
||||
return (
|
||||
<AudioSection muteDoneSound={muteDoneSound} onMuteDoneSoundChange={setMuteDoneSound} />
|
||||
);
|
||||
case 'event-hooks':
|
||||
return <EventHooksSection />;
|
||||
case 'defaults':
|
||||
return (
|
||||
<FeatureDefaultsSection
|
||||
@@ -215,6 +219,8 @@ export function SettingsView() {
|
||||
onSkipSandboxWarningChange={setSkipSandboxWarning}
|
||||
/>
|
||||
);
|
||||
case 'developer':
|
||||
return <DeveloperSection />;
|
||||
case 'danger':
|
||||
return (
|
||||
<DangerZoneSection
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
Shield,
|
||||
Cpu,
|
||||
GitBranch,
|
||||
Code2,
|
||||
Webhook,
|
||||
} from 'lucide-react';
|
||||
import { AnthropicIcon, CursorIcon, OpenAIIcon, OpenCodeIcon } from '@/components/ui/provider-icon';
|
||||
import type { SettingsViewId } from '../hooks/use-settings-view';
|
||||
@@ -63,6 +65,7 @@ export const GLOBAL_NAV_GROUPS: NavigationGroup[] = [
|
||||
{ id: 'terminal', label: 'Terminal', icon: SquareTerminal },
|
||||
{ id: 'keyboard', label: 'Keyboard Shortcuts', icon: Settings2 },
|
||||
{ id: 'audio', label: 'Audio', icon: Volume2 },
|
||||
{ id: 'event-hooks', label: 'Event Hooks', icon: Webhook },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -72,6 +75,10 @@ export const GLOBAL_NAV_GROUPS: NavigationGroup[] = [
|
||||
{ id: 'security', label: 'Security', icon: Shield },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Advanced',
|
||||
items: [{ id: 'developer', label: 'Developer', icon: Code2 }],
|
||||
},
|
||||
];
|
||||
|
||||
// Flat list of all global nav items for backwards compatibility
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Code2 } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useAppStore, type ServerLogLevel } from '@/store/app-store';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
const LOG_LEVEL_OPTIONS: { value: ServerLogLevel; label: string; description: string }[] = [
|
||||
{ value: 'error', label: 'Error', description: 'Only show error messages' },
|
||||
{ value: 'warn', label: 'Warning', description: 'Show warnings and errors' },
|
||||
{ value: 'info', label: 'Info', description: 'Show general information (default)' },
|
||||
{ value: 'debug', label: 'Debug', description: 'Show all messages including debug' },
|
||||
];
|
||||
|
||||
export function DeveloperSection() {
|
||||
const { serverLogLevel, setServerLogLevel, enableRequestLogging, setEnableRequestLogging } =
|
||||
useAppStore();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-2xl overflow-hidden',
|
||||
'border border-border/50',
|
||||
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
|
||||
'shadow-sm shadow-black/5'
|
||||
)}
|
||||
>
|
||||
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-purple-500/20 to-purple-600/10 flex items-center justify-center border border-purple-500/20">
|
||||
<Code2 className="w-5 h-5 text-purple-500" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-foreground tracking-tight">Developer</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground/80 ml-12">
|
||||
Advanced settings for debugging and development.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Server Log Level */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-foreground font-medium">Server Log Level</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Control the verbosity of API server logs. Set to "Error" to only see error messages in
|
||||
the server console.
|
||||
</p>
|
||||
<select
|
||||
value={serverLogLevel}
|
||||
onChange={(e) => {
|
||||
setServerLogLevel(e.target.value as ServerLogLevel);
|
||||
toast.success(`Log level changed to ${e.target.value}`, {
|
||||
description: 'Server logging verbosity updated',
|
||||
});
|
||||
}}
|
||||
className={cn(
|
||||
'w-full px-3 py-2 rounded-lg',
|
||||
'bg-accent/30 border border-border/50',
|
||||
'text-foreground text-sm',
|
||||
'focus:outline-none focus:ring-2 focus:ring-purple-500/30'
|
||||
)}
|
||||
>
|
||||
{LOG_LEVEL_OPTIONS.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label} - {option.description}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* HTTP Request Logging */}
|
||||
<div className="flex items-center justify-between pt-4 border-t border-border/30">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-foreground font-medium">HTTP Request Logging</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Log all HTTP requests (method, URL, status) to the server console.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={enableRequestLogging}
|
||||
onCheckedChange={(checked) => {
|
||||
setEnableRequestLogging(checked);
|
||||
toast.success(checked ? 'Request logging enabled' : 'Request logging disabled', {
|
||||
description: 'HTTP request logging updated',
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Terminal, Globe } from 'lucide-react';
|
||||
import type {
|
||||
EventHook,
|
||||
EventHookTrigger,
|
||||
EventHookHttpMethod,
|
||||
EventHookShellAction,
|
||||
EventHookHttpAction,
|
||||
} from '@automaker/types';
|
||||
import { EVENT_HOOK_TRIGGER_LABELS } from '@automaker/types';
|
||||
|
||||
interface EventHookDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
editingHook: EventHook | null;
|
||||
onSave: (hook: EventHook) => void;
|
||||
}
|
||||
|
||||
type ActionType = 'shell' | 'http';
|
||||
|
||||
const TRIGGER_OPTIONS: EventHookTrigger[] = [
|
||||
'feature_success',
|
||||
'feature_error',
|
||||
'auto_mode_complete',
|
||||
'auto_mode_error',
|
||||
];
|
||||
|
||||
const HTTP_METHODS: EventHookHttpMethod[] = ['POST', 'GET', 'PUT', 'PATCH'];
|
||||
|
||||
export function EventHookDialog({ open, onOpenChange, editingHook, onSave }: EventHookDialogProps) {
|
||||
// Form state
|
||||
const [name, setName] = useState('');
|
||||
const [trigger, setTrigger] = useState<EventHookTrigger>('feature_success');
|
||||
const [actionType, setActionType] = useState<ActionType>('shell');
|
||||
|
||||
// Shell action state
|
||||
const [command, setCommand] = useState('');
|
||||
const [timeout, setTimeout] = useState('30000');
|
||||
|
||||
// HTTP action state
|
||||
const [url, setUrl] = useState('');
|
||||
const [method, setMethod] = useState<EventHookHttpMethod>('POST');
|
||||
const [headers, setHeaders] = useState('');
|
||||
const [body, setBody] = useState('');
|
||||
|
||||
// Reset form when dialog opens/closes or editingHook changes
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
if (editingHook) {
|
||||
// Populate form with existing hook data
|
||||
setName(editingHook.name || '');
|
||||
setTrigger(editingHook.trigger);
|
||||
setActionType(editingHook.action.type);
|
||||
|
||||
if (editingHook.action.type === 'shell') {
|
||||
const shellAction = editingHook.action as EventHookShellAction;
|
||||
setCommand(shellAction.command);
|
||||
setTimeout(String(shellAction.timeout || 30000));
|
||||
// Reset HTTP fields
|
||||
setUrl('');
|
||||
setMethod('POST');
|
||||
setHeaders('');
|
||||
setBody('');
|
||||
} else {
|
||||
const httpAction = editingHook.action as EventHookHttpAction;
|
||||
setUrl(httpAction.url);
|
||||
setMethod(httpAction.method);
|
||||
setHeaders(httpAction.headers ? JSON.stringify(httpAction.headers, null, 2) : '');
|
||||
setBody(httpAction.body || '');
|
||||
// Reset shell fields
|
||||
setCommand('');
|
||||
setTimeout('30000');
|
||||
}
|
||||
} else {
|
||||
// Reset to defaults for new hook
|
||||
setName('');
|
||||
setTrigger('feature_success');
|
||||
setActionType('shell');
|
||||
setCommand('');
|
||||
setTimeout('30000');
|
||||
setUrl('');
|
||||
setMethod('POST');
|
||||
setHeaders('');
|
||||
setBody('');
|
||||
}
|
||||
}
|
||||
}, [open, editingHook]);
|
||||
|
||||
const handleSave = () => {
|
||||
const hook: EventHook = {
|
||||
id: editingHook?.id || crypto.randomUUID(),
|
||||
name: name.trim() || undefined,
|
||||
trigger,
|
||||
enabled: editingHook?.enabled ?? true,
|
||||
action:
|
||||
actionType === 'shell'
|
||||
? {
|
||||
type: 'shell',
|
||||
command,
|
||||
timeout: parseInt(timeout, 10) || 30000,
|
||||
}
|
||||
: {
|
||||
type: 'http',
|
||||
url,
|
||||
method,
|
||||
headers: headers.trim() ? JSON.parse(headers) : undefined,
|
||||
body: body.trim() || undefined,
|
||||
},
|
||||
};
|
||||
|
||||
onSave(hook);
|
||||
};
|
||||
|
||||
const isValid = actionType === 'shell' ? command.trim().length > 0 : url.trim().length > 0;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-lg max-h-[85vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingHook ? 'Edit Event Hook' : 'Add Event Hook'}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Configure an action to run when a specific event occurs.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
{/* Name (optional) */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="hook-name">Name (optional)</Label>
|
||||
<Input
|
||||
id="hook-name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="My notification hook"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Trigger selection */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="hook-trigger">Trigger Event</Label>
|
||||
<Select value={trigger} onValueChange={(v) => setTrigger(v as EventHookTrigger)}>
|
||||
<SelectTrigger id="hook-trigger">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{TRIGGER_OPTIONS.map((t) => (
|
||||
<SelectItem key={t} value={t}>
|
||||
{EVENT_HOOK_TRIGGER_LABELS[t]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Action type tabs */}
|
||||
<div className="space-y-2">
|
||||
<Label>Action Type</Label>
|
||||
<Tabs value={actionType} onValueChange={(v) => setActionType(v as ActionType)}>
|
||||
<TabsList className="w-full">
|
||||
<TabsTrigger value="shell" className="flex-1 gap-2">
|
||||
<Terminal className="w-4 h-4" />
|
||||
Shell Command
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="http" className="flex-1 gap-2">
|
||||
<Globe className="w-4 h-4" />
|
||||
HTTP Request
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Shell command form */}
|
||||
<TabsContent value="shell" className="space-y-4 mt-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="shell-command">Command</Label>
|
||||
<Textarea
|
||||
id="shell-command"
|
||||
value={command}
|
||||
onChange={(e) => setCommand(e.target.value)}
|
||||
placeholder='echo "Feature {{featureId}} completed!"'
|
||||
className="font-mono text-sm"
|
||||
rows={3}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Use {'{{variable}}'} syntax for dynamic values
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="shell-timeout">Timeout (ms)</Label>
|
||||
<Input
|
||||
id="shell-timeout"
|
||||
type="number"
|
||||
value={timeout}
|
||||
onChange={(e) => setTimeout(e.target.value)}
|
||||
placeholder="30000"
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* HTTP request form */}
|
||||
<TabsContent value="http" className="space-y-4 mt-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="http-url">URL</Label>
|
||||
<Input
|
||||
id="http-url"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
placeholder="https://api.example.com/webhook"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="http-method">Method</Label>
|
||||
<Select value={method} onValueChange={(v) => setMethod(v as EventHookHttpMethod)}>
|
||||
<SelectTrigger id="http-method">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{HTTP_METHODS.map((m) => (
|
||||
<SelectItem key={m} value={m}>
|
||||
{m}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="http-headers">Headers (JSON, optional)</Label>
|
||||
<Textarea
|
||||
id="http-headers"
|
||||
value={headers}
|
||||
onChange={(e) => setHeaders(e.target.value)}
|
||||
placeholder={'{\n "Authorization": "Bearer {{token}}"\n}'}
|
||||
className="font-mono text-sm"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="http-body">Body (JSON, optional)</Label>
|
||||
<Textarea
|
||||
id="http-body"
|
||||
value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
placeholder={'{\n "feature": "{{featureId}}",\n "status": "{{eventType}}"\n}'}
|
||||
className="font-mono text-sm"
|
||||
rows={4}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Leave empty for default body with all event context
|
||||
</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={!isValid}>
|
||||
{editingHook ? 'Save Changes' : 'Add Hook'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Webhook, Plus, Trash2, Pencil, Terminal, Globe } from 'lucide-react';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import type { EventHook, EventHookTrigger } from '@automaker/types';
|
||||
import { EVENT_HOOK_TRIGGER_LABELS } from '@automaker/types';
|
||||
import { EventHookDialog } from './event-hook-dialog';
|
||||
|
||||
export function EventHooksSection() {
|
||||
const { eventHooks, setEventHooks } = useAppStore();
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [editingHook, setEditingHook] = useState<EventHook | null>(null);
|
||||
|
||||
const handleAddHook = () => {
|
||||
setEditingHook(null);
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleEditHook = (hook: EventHook) => {
|
||||
setEditingHook(hook);
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteHook = (hookId: string) => {
|
||||
setEventHooks(eventHooks.filter((h) => h.id !== hookId));
|
||||
};
|
||||
|
||||
const handleToggleHook = (hookId: string, enabled: boolean) => {
|
||||
setEventHooks(eventHooks.map((h) => (h.id === hookId ? { ...h, enabled } : h)));
|
||||
};
|
||||
|
||||
const handleSaveHook = (hook: EventHook) => {
|
||||
if (editingHook) {
|
||||
// Update existing
|
||||
setEventHooks(eventHooks.map((h) => (h.id === hook.id ? hook : h)));
|
||||
} else {
|
||||
// Add new
|
||||
setEventHooks([...eventHooks, hook]);
|
||||
}
|
||||
setDialogOpen(false);
|
||||
setEditingHook(null);
|
||||
};
|
||||
|
||||
// Group hooks by trigger type for better organization
|
||||
const hooksByTrigger = eventHooks.reduce(
|
||||
(acc, hook) => {
|
||||
if (!acc[hook.trigger]) {
|
||||
acc[hook.trigger] = [];
|
||||
}
|
||||
acc[hook.trigger].push(hook);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<EventHookTrigger, EventHook[]>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-2xl overflow-hidden',
|
||||
'border border-border/50',
|
||||
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
|
||||
'shadow-sm shadow-black/5'
|
||||
)}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
|
||||
<Webhook className="w-5 h-5 text-brand-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-foreground tracking-tight">Event Hooks</h2>
|
||||
<p className="text-sm text-muted-foreground/80">
|
||||
Run custom commands or webhooks when events occur
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={handleAddHook} size="sm" className="gap-2">
|
||||
<Plus className="w-4 h-4" />
|
||||
Add Hook
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6">
|
||||
{eventHooks.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<Webhook className="w-12 h-12 mx-auto mb-3 opacity-30" />
|
||||
<p className="text-sm">No event hooks configured</p>
|
||||
<p className="text-xs mt-1">
|
||||
Add hooks to run commands or send webhooks when features complete
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{/* Group by trigger type */}
|
||||
{Object.entries(hooksByTrigger).map(([trigger, hooks]) => (
|
||||
<div key={trigger} className="space-y-3">
|
||||
<h3 className="text-sm font-medium text-muted-foreground">
|
||||
{EVENT_HOOK_TRIGGER_LABELS[trigger as EventHookTrigger]}
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{hooks.map((hook) => (
|
||||
<HookCard
|
||||
key={hook.id}
|
||||
hook={hook}
|
||||
onEdit={() => handleEditHook(hook)}
|
||||
onDelete={() => handleDeleteHook(hook.id)}
|
||||
onToggle={(enabled) => handleToggleHook(hook.id, enabled)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Variable reference */}
|
||||
<div className="px-6 pb-6">
|
||||
<div className="rounded-lg bg-muted/30 p-4 text-xs text-muted-foreground">
|
||||
<p className="font-medium mb-2">Available variables:</p>
|
||||
<code className="text-[10px] leading-relaxed">
|
||||
{'{{featureId}}'} {'{{featureName}}'} {'{{projectPath}}'} {'{{projectName}}'}{' '}
|
||||
{'{{error}}'} {'{{timestamp}}'} {'{{eventType}}'}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dialog */}
|
||||
<EventHookDialog
|
||||
open={dialogOpen}
|
||||
onOpenChange={setDialogOpen}
|
||||
editingHook={editingHook}
|
||||
onSave={handleSaveHook}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface HookCardProps {
|
||||
hook: EventHook;
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
onToggle: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
function HookCard({ hook, onEdit, onDelete, onToggle }: HookCardProps) {
|
||||
const isShell = hook.action.type === 'shell';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-3 p-3 rounded-lg border',
|
||||
'bg-background/50 hover:bg-background/80 transition-colors',
|
||||
!hook.enabled && 'opacity-60'
|
||||
)}
|
||||
>
|
||||
{/* Type icon */}
|
||||
<div
|
||||
className={cn(
|
||||
'w-8 h-8 rounded-lg flex items-center justify-center',
|
||||
isShell ? 'bg-amber-500/10 text-amber-500' : 'bg-blue-500/10 text-blue-500'
|
||||
)}
|
||||
>
|
||||
{isShell ? <Terminal className="w-4 h-4" /> : <Globe className="w-4 h-4" />}
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium truncate">
|
||||
{hook.name || (isShell ? 'Shell Command' : 'HTTP Webhook')}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground truncate">
|
||||
{isShell
|
||||
? (hook.action as { type: 'shell'; command: string }).command
|
||||
: (hook.action as { type: 'http'; url: string }).url}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch checked={hook.enabled} onCheckedChange={onToggle} />
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={onEdit}>
|
||||
<Pencil className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-destructive hover:text-destructive"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { EventHooksSection } from './event-hooks-section';
|
||||
@@ -15,10 +15,12 @@ export type SettingsViewId =
|
||||
| 'terminal'
|
||||
| 'keyboard'
|
||||
| 'audio'
|
||||
| 'event-hooks'
|
||||
| 'defaults'
|
||||
| 'worktrees'
|
||||
| 'account'
|
||||
| 'security'
|
||||
| 'developer'
|
||||
| 'danger';
|
||||
|
||||
interface UseSettingsViewOptions {
|
||||
|
||||
@@ -567,6 +567,8 @@ export function hydrateStoreFromSettings(settings: GlobalSettings): void {
|
||||
defaultRequirePlanApproval: settings.defaultRequirePlanApproval ?? false,
|
||||
defaultFeatureModel: settings.defaultFeatureModel ?? { model: 'opus' },
|
||||
muteDoneSound: settings.muteDoneSound ?? false,
|
||||
serverLogLevel: settings.serverLogLevel ?? 'info',
|
||||
enableRequestLogging: settings.enableRequestLogging ?? true,
|
||||
enhancementModel: settings.enhancementModel ?? 'sonnet',
|
||||
validationModel: settings.validationModel ?? 'opus',
|
||||
phaseModels: settings.phaseModels ?? current.phaseModels,
|
||||
@@ -626,6 +628,8 @@ function buildSettingsUpdateFromStore(): Record<string, unknown> {
|
||||
defaultPlanningMode: state.defaultPlanningMode,
|
||||
defaultRequirePlanApproval: state.defaultRequirePlanApproval,
|
||||
muteDoneSound: state.muteDoneSound,
|
||||
serverLogLevel: state.serverLogLevel,
|
||||
enableRequestLogging: state.enableRequestLogging,
|
||||
enhancementModel: state.enhancementModel,
|
||||
validationModel: state.validationModel,
|
||||
phaseModels: state.phaseModels,
|
||||
|
||||
@@ -44,6 +44,8 @@ const SETTINGS_FIELDS_TO_SYNC = [
|
||||
'defaultRequirePlanApproval',
|
||||
'defaultFeatureModel',
|
||||
'muteDoneSound',
|
||||
'serverLogLevel',
|
||||
'enableRequestLogging',
|
||||
'enhancementModel',
|
||||
'validationModel',
|
||||
'phaseModels',
|
||||
@@ -58,6 +60,7 @@ const SETTINGS_FIELDS_TO_SYNC = [
|
||||
'mcpServers',
|
||||
'defaultEditorCommand',
|
||||
'promptCustomization',
|
||||
'eventHooks',
|
||||
'projects',
|
||||
'trashedProjects',
|
||||
'currentProjectId', // ID of currently open project
|
||||
@@ -470,6 +473,8 @@ export async function refreshSettingsFromServer(): Promise<boolean> {
|
||||
defaultRequirePlanApproval: serverSettings.defaultRequirePlanApproval,
|
||||
defaultFeatureModel: serverSettings.defaultFeatureModel ?? { model: 'opus' },
|
||||
muteDoneSound: serverSettings.muteDoneSound,
|
||||
serverLogLevel: serverSettings.serverLogLevel ?? 'info',
|
||||
enableRequestLogging: serverSettings.enableRequestLogging ?? true,
|
||||
enhancementModel: serverSettings.enhancementModel,
|
||||
validationModel: serverSettings.validationModel,
|
||||
phaseModels: serverSettings.phaseModels,
|
||||
|
||||
@@ -24,6 +24,8 @@ import type {
|
||||
PipelineStep,
|
||||
PromptCustomization,
|
||||
ModelDefinition,
|
||||
ServerLogLevel,
|
||||
EventHook,
|
||||
} from '@automaker/types';
|
||||
import {
|
||||
getAllCursorModelIds,
|
||||
@@ -43,6 +45,7 @@ export type {
|
||||
PlanningMode,
|
||||
ThinkingLevel,
|
||||
ModelProvider,
|
||||
ServerLogLevel,
|
||||
FeatureTextFilePath,
|
||||
FeatureImagePath,
|
||||
};
|
||||
@@ -564,6 +567,10 @@ export interface AppState {
|
||||
// Audio Settings
|
||||
muteDoneSound: boolean; // When true, mute the notification sound when agents complete (default: false)
|
||||
|
||||
// Server Log Level Settings
|
||||
serverLogLevel: ServerLogLevel; // Log level for the API server (error, warn, info, debug)
|
||||
enableRequestLogging: boolean; // Enable HTTP request logging (Morgan)
|
||||
|
||||
// Enhancement Model Settings
|
||||
enhancementModel: ModelAlias; // Model used for feature enhancement (default: sonnet)
|
||||
|
||||
@@ -630,6 +637,9 @@ export interface AppState {
|
||||
// Prompt Customization
|
||||
promptCustomization: PromptCustomization; // Custom prompts for Auto Mode, Agent, Backlog Plan, Enhancement
|
||||
|
||||
// Event Hooks
|
||||
eventHooks: EventHook[]; // Event hooks for custom commands or webhooks
|
||||
|
||||
// Project Analysis
|
||||
projectAnalysis: ProjectAnalysis | null;
|
||||
isAnalyzing: boolean;
|
||||
@@ -982,6 +992,10 @@ export interface AppActions {
|
||||
// Audio Settings actions
|
||||
setMuteDoneSound: (muted: boolean) => void;
|
||||
|
||||
// Server Log Level actions
|
||||
setServerLogLevel: (level: ServerLogLevel) => void;
|
||||
setEnableRequestLogging: (enabled: boolean) => void;
|
||||
|
||||
// Enhancement Model actions
|
||||
setEnhancementModel: (model: ModelAlias) => void;
|
||||
|
||||
@@ -1039,6 +1053,9 @@ export interface AppActions {
|
||||
// Prompt Customization actions
|
||||
setPromptCustomization: (customization: PromptCustomization) => Promise<void>;
|
||||
|
||||
// Event Hook actions
|
||||
setEventHooks: (hooks: EventHook[]) => void;
|
||||
|
||||
// MCP Server actions
|
||||
addMCPServer: (server: Omit<MCPServerConfig, 'id'>) => void;
|
||||
updateMCPServer: (id: string, updates: Partial<MCPServerConfig>) => void;
|
||||
@@ -1250,6 +1267,8 @@ const initialState: AppState = {
|
||||
worktreesByProject: {},
|
||||
keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS, // Default keyboard shortcuts
|
||||
muteDoneSound: false, // Default to sound enabled (not muted)
|
||||
serverLogLevel: 'info', // Default to info level for server logs
|
||||
enableRequestLogging: true, // Default to enabled for HTTP request logging
|
||||
enhancementModel: 'sonnet', // Default to sonnet for feature enhancement
|
||||
validationModel: 'opus', // Default to opus for GitHub issue validation
|
||||
phaseModels: DEFAULT_PHASE_MODELS, // Phase-specific model configuration
|
||||
@@ -1282,6 +1301,7 @@ const initialState: AppState = {
|
||||
enableSubagents: true, // Subagents enabled by default
|
||||
subagentsSources: ['user', 'project'] as Array<'user' | 'project'>, // Load from both sources by default
|
||||
promptCustomization: {}, // Empty by default - all prompts use built-in defaults
|
||||
eventHooks: [], // No event hooks configured by default
|
||||
projectAnalysis: null,
|
||||
isAnalyzing: false,
|
||||
boardBackgroundByProject: {},
|
||||
@@ -2036,6 +2056,10 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
|
||||
// Audio Settings actions
|
||||
setMuteDoneSound: (muted) => set({ muteDoneSound: muted }),
|
||||
|
||||
// Server Log Level actions
|
||||
setServerLogLevel: (level) => set({ serverLogLevel: level }),
|
||||
setEnableRequestLogging: (enabled) => set({ enableRequestLogging: enabled }),
|
||||
|
||||
// Enhancement Model actions
|
||||
setEnhancementModel: (model) => set({ enhancementModel: model }),
|
||||
|
||||
@@ -2207,6 +2231,9 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
|
||||
await syncSettingsToServer();
|
||||
},
|
||||
|
||||
// Event Hook actions
|
||||
setEventHooks: (hooks) => set({ eventHooks: hooks }),
|
||||
|
||||
// MCP Server actions
|
||||
addMCPServer: (server) => {
|
||||
const id = `mcp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
Reference in New Issue
Block a user