mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
Merge main into massive-terminal-upgrade
Resolves merge conflicts: - apps/server/src/routes/terminal/common.ts: Keep randomBytes import, use @automaker/utils for createLogger - apps/ui/eslint.config.mjs: Use main's explicit globals list with XMLHttpRequest and MediaQueryListEvent additions - apps/ui/src/components/views/terminal-view.tsx: Keep our terminal improvements (killAllSessions, beforeunload, better error handling) - apps/ui/src/config/terminal-themes.ts: Keep our search highlight colors for all themes - apps/ui/src/store/app-store.ts: Keep our terminal settings persistence improvements (merge function) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,13 +2,10 @@
|
||||
* Common utilities and state for suggestions routes
|
||||
*/
|
||||
|
||||
import { createLogger } from "../../lib/logger.js";
|
||||
import {
|
||||
getErrorMessage as getErrorMessageShared,
|
||||
createLogError,
|
||||
} from "../common.js";
|
||||
import { createLogger } from '@automaker/utils';
|
||||
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
|
||||
|
||||
const logger = createLogger("Suggestions");
|
||||
const logger = createLogger('Suggestions');
|
||||
|
||||
// Shared state for tracking generation status - private
|
||||
let isRunning = false;
|
||||
@@ -27,10 +24,7 @@ export function getSuggestionsStatus(): {
|
||||
/**
|
||||
* Set the running state and abort controller
|
||||
*/
|
||||
export function setRunningState(
|
||||
running: boolean,
|
||||
controller: AbortController | null = null
|
||||
): void {
|
||||
export function setRunningState(running: boolean, controller: AbortController | null = null): void {
|
||||
isRunning = running;
|
||||
currentAbortController = controller;
|
||||
}
|
||||
|
||||
@@ -2,43 +2,43 @@
|
||||
* Business logic for generating suggestions
|
||||
*/
|
||||
|
||||
import { query } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { EventEmitter } from "../../lib/events.js";
|
||||
import { createLogger } from "../../lib/logger.js";
|
||||
import { createSuggestionsOptions } from "../../lib/sdk-options.js";
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
import type { EventEmitter } from '../../lib/events.js';
|
||||
import { createLogger } from '@automaker/utils';
|
||||
import { createSuggestionsOptions } from '../../lib/sdk-options.js';
|
||||
|
||||
const logger = createLogger("Suggestions");
|
||||
const logger = createLogger('Suggestions');
|
||||
|
||||
/**
|
||||
* JSON Schema for suggestions output
|
||||
*/
|
||||
const suggestionsSchema = {
|
||||
type: "object",
|
||||
type: 'object',
|
||||
properties: {
|
||||
suggestions: {
|
||||
type: "array",
|
||||
type: 'array',
|
||||
items: {
|
||||
type: "object",
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
category: { type: "string" },
|
||||
description: { type: "string" },
|
||||
id: { type: 'string' },
|
||||
category: { type: 'string' },
|
||||
description: { type: 'string' },
|
||||
steps: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
priority: {
|
||||
type: "number",
|
||||
priority: {
|
||||
type: 'number',
|
||||
minimum: 1,
|
||||
maximum: 3,
|
||||
},
|
||||
reasoning: { type: "string" },
|
||||
reasoning: { type: 'string' },
|
||||
},
|
||||
required: ["category", "description", "steps", "priority", "reasoning"],
|
||||
required: ['category', 'description', 'steps', 'priority', 'reasoning'],
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["suggestions"],
|
||||
required: ['suggestions'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
@@ -49,13 +49,10 @@ export async function generateSuggestions(
|
||||
abortController: AbortController
|
||||
): Promise<void> {
|
||||
const typePrompts: Record<string, string> = {
|
||||
features:
|
||||
"Analyze this project and suggest new features that would add value.",
|
||||
refactoring: "Analyze this project and identify refactoring opportunities.",
|
||||
security:
|
||||
"Analyze this project for security vulnerabilities and suggest fixes.",
|
||||
performance:
|
||||
"Analyze this project for performance issues and suggest optimizations.",
|
||||
features: 'Analyze this project and suggest new features that would add value.',
|
||||
refactoring: 'Analyze this project and identify refactoring opportunities.',
|
||||
security: 'Analyze this project for security vulnerabilities and suggest fixes.',
|
||||
performance: 'Analyze this project for performance issues and suggest optimizations.',
|
||||
};
|
||||
|
||||
const prompt = `${typePrompts[suggestionType] || typePrompts.features}
|
||||
@@ -71,8 +68,8 @@ For each suggestion, provide:
|
||||
|
||||
The response will be automatically formatted as structured JSON.`;
|
||||
|
||||
events.emit("suggestions:event", {
|
||||
type: "suggestions_progress",
|
||||
events.emit('suggestions:event', {
|
||||
type: 'suggestions_progress',
|
||||
content: `Starting ${suggestionType} analysis...\n`,
|
||||
});
|
||||
|
||||
@@ -80,48 +77,48 @@ The response will be automatically formatted as structured JSON.`;
|
||||
cwd: projectPath,
|
||||
abortController,
|
||||
outputFormat: {
|
||||
type: "json_schema",
|
||||
type: 'json_schema',
|
||||
schema: suggestionsSchema,
|
||||
},
|
||||
});
|
||||
|
||||
const stream = query({ prompt, options });
|
||||
let responseText = "";
|
||||
let responseText = '';
|
||||
let structuredOutput: { suggestions: Array<Record<string, unknown>> } | null = null;
|
||||
|
||||
for await (const msg of stream) {
|
||||
if (msg.type === "assistant" && msg.message.content) {
|
||||
if (msg.type === 'assistant' && msg.message.content) {
|
||||
for (const block of msg.message.content) {
|
||||
if (block.type === "text") {
|
||||
if (block.type === 'text') {
|
||||
responseText += block.text;
|
||||
events.emit("suggestions:event", {
|
||||
type: "suggestions_progress",
|
||||
events.emit('suggestions:event', {
|
||||
type: 'suggestions_progress',
|
||||
content: block.text,
|
||||
});
|
||||
} else if (block.type === "tool_use") {
|
||||
events.emit("suggestions:event", {
|
||||
type: "suggestions_tool",
|
||||
} else if (block.type === 'tool_use') {
|
||||
events.emit('suggestions:event', {
|
||||
type: 'suggestions_tool',
|
||||
tool: block.name,
|
||||
input: block.input,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (msg.type === "result" && msg.subtype === "success") {
|
||||
} else if (msg.type === 'result' && msg.subtype === 'success') {
|
||||
// Check for structured output
|
||||
const resultMsg = msg as any;
|
||||
if (resultMsg.structured_output) {
|
||||
structuredOutput = resultMsg.structured_output as {
|
||||
suggestions: Array<Record<string, unknown>>;
|
||||
};
|
||||
logger.debug("Received structured output:", structuredOutput);
|
||||
logger.debug('Received structured output:', structuredOutput);
|
||||
}
|
||||
} else if (msg.type === "result") {
|
||||
} else if (msg.type === 'result') {
|
||||
const resultMsg = msg as any;
|
||||
if (resultMsg.subtype === "error_max_structured_output_retries") {
|
||||
logger.error("Failed to produce valid structured output after retries");
|
||||
throw new Error("Could not produce valid suggestions output");
|
||||
} else if (resultMsg.subtype === "error_max_turns") {
|
||||
logger.error("Hit max turns limit before completing suggestions generation");
|
||||
if (resultMsg.subtype === 'error_max_structured_output_retries') {
|
||||
logger.error('Failed to produce valid structured output after retries');
|
||||
throw new Error('Could not produce valid suggestions output');
|
||||
} else if (resultMsg.subtype === 'error_max_turns') {
|
||||
logger.error('Hit max turns limit before completing suggestions generation');
|
||||
logger.warn(`Response text length: ${responseText.length} chars`);
|
||||
// Still try to parse what we have
|
||||
}
|
||||
@@ -132,49 +129,44 @@ The response will be automatically formatted as structured JSON.`;
|
||||
try {
|
||||
if (structuredOutput && structuredOutput.suggestions) {
|
||||
// Use structured output directly
|
||||
events.emit("suggestions:event", {
|
||||
type: "suggestions_complete",
|
||||
suggestions: structuredOutput.suggestions.map(
|
||||
(s: Record<string, unknown>, i: number) => ({
|
||||
...s,
|
||||
id: s.id || `suggestion-${Date.now()}-${i}`,
|
||||
})
|
||||
),
|
||||
events.emit('suggestions:event', {
|
||||
type: 'suggestions_complete',
|
||||
suggestions: structuredOutput.suggestions.map((s: Record<string, unknown>, i: number) => ({
|
||||
...s,
|
||||
id: s.id || `suggestion-${Date.now()}-${i}`,
|
||||
})),
|
||||
});
|
||||
} else {
|
||||
// Fallback: try to parse from text (for backwards compatibility)
|
||||
logger.warn("No structured output received, attempting to parse from text");
|
||||
logger.warn('No structured output received, attempting to parse from text');
|
||||
const jsonMatch = responseText.match(/\{[\s\S]*"suggestions"[\s\S]*\}/);
|
||||
if (jsonMatch) {
|
||||
const parsed = JSON.parse(jsonMatch[0]);
|
||||
events.emit("suggestions:event", {
|
||||
type: "suggestions_complete",
|
||||
suggestions: parsed.suggestions.map(
|
||||
(s: Record<string, unknown>, i: number) => ({
|
||||
...s,
|
||||
id: s.id || `suggestion-${Date.now()}-${i}`,
|
||||
})
|
||||
),
|
||||
events.emit('suggestions:event', {
|
||||
type: 'suggestions_complete',
|
||||
suggestions: parsed.suggestions.map((s: Record<string, unknown>, i: number) => ({
|
||||
...s,
|
||||
id: s.id || `suggestion-${Date.now()}-${i}`,
|
||||
})),
|
||||
});
|
||||
} else {
|
||||
throw new Error("No valid JSON found in response");
|
||||
throw new Error('No valid JSON found in response');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Log the parsing error for debugging
|
||||
logger.error("Failed to parse suggestions JSON from AI response:", error);
|
||||
logger.error('Failed to parse suggestions JSON from AI response:', error);
|
||||
// Return generic suggestions if parsing fails
|
||||
events.emit("suggestions:event", {
|
||||
type: "suggestions_complete",
|
||||
events.emit('suggestions:event', {
|
||||
type: 'suggestions_complete',
|
||||
suggestions: [
|
||||
{
|
||||
id: `suggestion-${Date.now()}-0`,
|
||||
category: "Analysis",
|
||||
description: "Review the AI analysis output for insights",
|
||||
steps: ["Review the generated analysis"],
|
||||
category: 'Analysis',
|
||||
description: 'Review the AI analysis output for insights',
|
||||
steps: ['Review the generated analysis'],
|
||||
priority: 1,
|
||||
reasoning:
|
||||
"The AI provided analysis but suggestions need manual review",
|
||||
reasoning: 'The AI provided analysis but suggestions need manual review',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -2,18 +2,19 @@
|
||||
* Suggestions routes - HTTP API for AI-powered feature suggestions
|
||||
*/
|
||||
|
||||
import { Router } from "express";
|
||||
import type { EventEmitter } from "../../lib/events.js";
|
||||
import { createGenerateHandler } from "./routes/generate.js";
|
||||
import { createStopHandler } from "./routes/stop.js";
|
||||
import { createStatusHandler } from "./routes/status.js";
|
||||
import { Router } from 'express';
|
||||
import type { EventEmitter } from '../../lib/events.js';
|
||||
import { validatePathParams } from '../../middleware/validate-paths.js';
|
||||
import { createGenerateHandler } from './routes/generate.js';
|
||||
import { createStopHandler } from './routes/stop.js';
|
||||
import { createStatusHandler } from './routes/status.js';
|
||||
|
||||
export function createSuggestionsRoutes(events: EventEmitter): Router {
|
||||
const router = Router();
|
||||
|
||||
router.post("/generate", createGenerateHandler(events));
|
||||
router.post("/stop", createStopHandler());
|
||||
router.get("/status", createStatusHandler());
|
||||
router.post('/generate', validatePathParams('projectPath'), createGenerateHandler(events));
|
||||
router.post('/stop', createStopHandler());
|
||||
router.get('/status', createStatusHandler());
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -2,29 +2,24 @@
|
||||
* POST /generate endpoint - Generate suggestions
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import type { EventEmitter } from "../../../lib/events.js";
|
||||
import { createLogger } from "../../../lib/logger.js";
|
||||
import {
|
||||
getSuggestionsStatus,
|
||||
setRunningState,
|
||||
getErrorMessage,
|
||||
logError,
|
||||
} from "../common.js";
|
||||
import { generateSuggestions } from "../generate-suggestions.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import type { EventEmitter } from '../../../lib/events.js';
|
||||
import { createLogger } from '@automaker/utils';
|
||||
import { getSuggestionsStatus, setRunningState, getErrorMessage, logError } from '../common.js';
|
||||
import { generateSuggestions } from '../generate-suggestions.js';
|
||||
|
||||
const logger = createLogger("Suggestions");
|
||||
const logger = createLogger('Suggestions');
|
||||
|
||||
export function createGenerateHandler(events: EventEmitter) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { projectPath, suggestionType = "features" } = req.body as {
|
||||
const { projectPath, suggestionType = 'features' } = req.body as {
|
||||
projectPath: string;
|
||||
suggestionType?: string;
|
||||
};
|
||||
|
||||
if (!projectPath) {
|
||||
res.status(400).json({ success: false, error: "projectPath required" });
|
||||
res.status(400).json({ success: false, error: 'projectPath required' });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -32,7 +27,7 @@ export function createGenerateHandler(events: EventEmitter) {
|
||||
if (isRunning) {
|
||||
res.json({
|
||||
success: false,
|
||||
error: "Suggestions generation is already running",
|
||||
error: 'Suggestions generation is already running',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -44,9 +39,9 @@ export function createGenerateHandler(events: EventEmitter) {
|
||||
// Start generation in background
|
||||
generateSuggestions(projectPath, suggestionType, events, abortController)
|
||||
.catch((error) => {
|
||||
logError(error, "Generate suggestions failed (background)");
|
||||
events.emit("suggestions:event", {
|
||||
type: "suggestions_error",
|
||||
logError(error, 'Generate suggestions failed (background)');
|
||||
events.emit('suggestions:event', {
|
||||
type: 'suggestions_error',
|
||||
error: getErrorMessage(error),
|
||||
});
|
||||
})
|
||||
@@ -56,7 +51,7 @@ export function createGenerateHandler(events: EventEmitter) {
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
logError(error, "Generate suggestions failed");
|
||||
logError(error, 'Generate suggestions failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user