feat: implement project setup dialog and refactor sidebar integration

- Added a new ProjectSetupDialog component to facilitate project specification generation, enhancing user experience by guiding users through project setup.
- Refactored the Sidebar component to integrate the new ProjectSetupDialog, replacing the previous inline dialog implementation for improved code organization and maintainability.
- Updated the sidebar to handle project overview and feature generation options, streamlining the project setup process.
- Removed the old dialog implementation from the Sidebar, reducing code duplication and improving clarity.
This commit is contained in:
Cody Seibert
2025-12-15 01:07:47 -05:00
parent 919e08689a
commit f25d62fe25
13 changed files with 724 additions and 332 deletions

View File

@@ -2,16 +2,19 @@
* Generate features from existing app_spec.txt
*/
import { query, type Options } from "@anthropic-ai/claude-agent-sdk";
import { query } from "@anthropic-ai/claude-agent-sdk";
import path from "path";
import fs from "fs/promises";
import type { EventEmitter } from "../../lib/events.js";
import { createLogger } from "../../lib/logger.js";
import { createFeatureGenerationOptions } from "../../lib/sdk-options.js";
import { logAuthStatus } from "./common.js";
import { parseAndCreateFeatures } from "./parse-and-create-features.js";
const logger = createLogger("SpecRegeneration");
const MAX_FEATURES = 100;
export async function generateFeaturesFromSpec(
projectPath: string,
events: EventEmitter,
@@ -29,6 +32,10 @@ export async function generateFeaturesFromSpec(
try {
spec = await fs.readFile(specPath, "utf-8");
logger.info(`Spec loaded successfully (${spec.length} chars)`);
logger.info(`Spec preview (first 500 chars): ${spec.substring(0, 500)}`);
logger.info(
`Spec preview (last 500 chars): ${spec.substring(spec.length - 500)}`
);
} catch (readError) {
logger.error("❌ Failed to read spec file:", readError);
events.emit("spec-regeneration:event", {
@@ -66,9 +73,16 @@ Format as JSON:
]
}
Generate 5-15 features that build on each other logically.`;
Generate ${MAX_FEATURES} features that build on each other logically.
logger.debug("Prompt length:", `${prompt.length} chars`);
IMPORTANT: Do not ask for clarification. The specification is provided above. Generate the JSON immediately.`;
logger.info("========== PROMPT BEING SENT ==========");
logger.info(`Prompt length: ${prompt.length} chars`);
logger.info(
`Prompt preview (first 1000 chars):\n${prompt.substring(0, 1000)}`
);
logger.info("========== END PROMPT PREVIEW ==========");
events.emit("spec-regeneration:event", {
type: "spec_regeneration_progress",
@@ -76,14 +90,10 @@ Generate 5-15 features that build on each other logically.`;
projectPath: projectPath,
});
const options: Options = {
model: "claude-sonnet-4-20250514",
maxTurns: 5,
const options = createFeatureGenerationOptions({
cwd: projectPath,
allowedTools: ["Read", "Glob"],
permissionMode: "acceptEdits",
abortController,
};
});
logger.debug("SDK Options:", JSON.stringify(options, null, 2));
logger.info("Calling Claude Agent SDK query() for features...");
@@ -120,7 +130,7 @@ Generate 5-15 features that build on each other logically.`;
if (msg.type === "assistant" && msg.message.content) {
for (const block of msg.message.content) {
if (block.type === "text") {
responseText = block.text;
responseText += block.text;
logger.debug(
`Feature text block received (${block.text.length} chars)`
);
@@ -147,6 +157,9 @@ Generate 5-15 features that build on each other logically.`;
logger.info(`Feature stream complete. Total messages: ${messageCount}`);
logger.info(`Feature response length: ${responseText.length} chars`);
logger.info("========== FULL RESPONSE TEXT ==========");
logger.info(responseText);
logger.info("========== END RESPONSE TEXT ==========");
await parseAndCreateFeatures(projectPath, responseText, events);

View File

@@ -2,12 +2,13 @@
* Generate app_spec.txt from project overview
*/
import { query, type Options } from "@anthropic-ai/claude-agent-sdk";
import { query } from "@anthropic-ai/claude-agent-sdk";
import path from "path";
import fs from "fs/promises";
import type { EventEmitter } from "../../lib/events.js";
import { getAppSpecFormatInstruction } from "../../lib/app-spec-format.js";
import { createLogger } from "../../lib/logger.js";
import { createSpecGenerationOptions } from "../../lib/sdk-options.js";
import { logAuthStatus } from "./common.js";
import { generateFeaturesFromSpec } from "./generate-features-from-spec.js";
@@ -21,11 +22,12 @@ export async function generateSpec(
generateFeatures?: boolean,
analyzeProject?: boolean
): Promise<void> {
logger.debug("========== generateSpec() started ==========");
logger.debug("projectPath:", projectPath);
logger.debug("projectOverview length:", `${projectOverview.length} chars`);
logger.debug("generateFeatures:", generateFeatures);
logger.debug("analyzeProject:", analyzeProject);
logger.info("========== generateSpec() started ==========");
logger.info("projectPath:", projectPath);
logger.info("projectOverview length:", `${projectOverview.length} chars`);
logger.info("projectOverview preview:", projectOverview.substring(0, 300));
logger.info("generateFeatures:", generateFeatures);
logger.info("analyzeProject:", analyzeProject);
// Build the prompt based on whether we should analyze the project
let analysisInstructions = "";
@@ -63,21 +65,20 @@ ${analysisInstructions}
${getAppSpecFormatInstruction()}`;
logger.debug("Prompt length:", `${prompt.length} chars`);
logger.info("========== PROMPT BEING SENT ==========");
logger.info(`Prompt length: ${prompt.length} chars`);
logger.info(`Prompt preview (first 500 chars):\n${prompt.substring(0, 500)}`);
logger.info("========== END PROMPT PREVIEW ==========");
events.emit("spec-regeneration:event", {
type: "spec_progress",
content: "Starting spec generation...\n",
});
const options: Options = {
model: "claude-opus-4-5-20251101",
maxTurns: 10,
const options = createSpecGenerationOptions({
cwd: projectPath,
allowedTools: ["Read", "Glob", "Grep"],
permissionMode: "acceptEdits",
abortController,
};
});
logger.debug("SDK Options:", JSON.stringify(options, null, 2));
logger.info("Calling Claude Agent SDK query()...");
@@ -98,45 +99,96 @@ ${getAppSpecFormatInstruction()}`;
let responseText = "";
let messageCount = 0;
logger.debug("Starting to iterate over stream...");
logger.info("Starting to iterate over stream...");
try {
for await (const msg of stream) {
messageCount++;
logger.debug(
`Stream message #${messageCount}:`,
JSON.stringify(
{ type: msg.type, subtype: (msg as any).subtype },
null,
2
)
logger.info(
`Stream message #${messageCount}: type=${msg.type}, subtype=${
(msg as any).subtype
}`
);
if (msg.type === "assistant" && msg.message.content) {
for (const block of msg.message.content) {
if (block.type === "text") {
responseText += block.text;
logger.debug(`Text block received (${block.text.length} chars)`);
events.emit("spec-regeneration:event", {
type: "spec_regeneration_progress",
content: block.text,
projectPath: projectPath,
});
} else if (block.type === "tool_use") {
logger.debug("Tool use:", block.name);
events.emit("spec-regeneration:event", {
type: "spec_tool",
tool: block.name,
input: block.input,
});
if (msg.type === "assistant") {
// Log the full message structure to debug
logger.info(`Assistant msg keys: ${Object.keys(msg).join(", ")}`);
const msgAny = msg as any;
if (msgAny.message) {
logger.info(
`msg.message keys: ${Object.keys(msgAny.message).join(", ")}`
);
if (msgAny.message.content) {
logger.info(
`msg.message.content length: ${msgAny.message.content.length}`
);
for (const block of msgAny.message.content) {
logger.info(
`Block keys: ${Object.keys(block).join(", ")}, type: ${
block.type
}`
);
if (block.type === "text") {
responseText += block.text;
logger.info(
`Text block received (${block.text.length} chars), total now: ${responseText.length} chars`
);
logger.info(`Text preview: ${block.text.substring(0, 200)}...`);
events.emit("spec-regeneration:event", {
type: "spec_regeneration_progress",
content: block.text,
projectPath: projectPath,
});
} else if (block.type === "tool_use") {
logger.info("Tool use:", block.name);
events.emit("spec-regeneration:event", {
type: "spec_tool",
tool: block.name,
input: block.input,
});
}
}
} else {
logger.warn("msg.message.content is falsy");
}
} else {
logger.warn("msg.message is falsy");
// Log full message to see structure
logger.info(
`Full assistant msg: ${JSON.stringify(msg).substring(0, 1000)}`
);
}
} else if (msg.type === "result" && (msg as any).subtype === "success") {
logger.debug("Received success result");
responseText = (msg as any).result || responseText;
logger.info("Received success result");
logger.info(`Result value: "${(msg as any).result}"`);
logger.info(
`Current responseText length before result: ${responseText.length}`
);
// Only use result if it has content, otherwise keep accumulated text
if ((msg as any).result && (msg as any).result.length > 0) {
logger.info("Using result value as responseText");
responseText = (msg as any).result;
} else {
logger.info("Result is empty, keeping accumulated responseText");
}
} else if (msg.type === "result") {
// Handle all result types
const subtype = (msg as any).subtype;
logger.info(`Result message: subtype=${subtype}`);
if (subtype === "error_max_turns") {
logger.error(
"❌ Hit max turns limit! Claude used too many tool calls."
);
logger.info(`responseText so far: ${responseText.length} chars`);
}
} else if ((msg as { type: string }).type === "error") {
logger.error("❌ Received error message from stream:");
logger.error("Error message:", JSON.stringify(msg, null, 2));
} else if (msg.type === "user") {
// Log user messages (tool results)
logger.info(
`User message (tool result): ${JSON.stringify(msg).substring(0, 500)}`
);
}
}
} catch (streamError) {
@@ -147,16 +199,31 @@ ${getAppSpecFormatInstruction()}`;
logger.info(`Stream iteration complete. Total messages: ${messageCount}`);
logger.info(`Response text length: ${responseText.length} chars`);
logger.info("========== FINAL RESPONSE TEXT ==========");
logger.info(responseText || "(empty)");
logger.info("========== END RESPONSE TEXT ==========");
if (!responseText || responseText.trim().length === 0) {
logger.error("❌ WARNING: responseText is empty! Nothing to save.");
}
// Save spec
const specDir = path.join(projectPath, ".automaker");
const specPath = path.join(specDir, "app_spec.txt");
logger.info("Saving spec to:", specPath);
logger.info(`Content to save (${responseText.length} chars)`);
await fs.mkdir(specDir, { recursive: true });
await fs.writeFile(specPath, responseText);
// Verify the file was written
const savedContent = await fs.readFile(specPath, "utf-8");
logger.info(`Verified saved file: ${savedContent.length} chars`);
if (savedContent.length === 0) {
logger.error("❌ File was saved but is empty!");
}
logger.info("Spec saved successfully");
// Emit spec completion event

View File

@@ -14,23 +14,32 @@ export async function parseAndCreateFeatures(
content: string,
events: EventEmitter
): Promise<void> {
logger.debug("========== parseAndCreateFeatures() started ==========");
logger.debug("Content length:", `${content.length} chars`);
logger.info("========== parseAndCreateFeatures() started ==========");
logger.info(`Content length: ${content.length} chars`);
logger.info("========== CONTENT RECEIVED FOR PARSING ==========");
logger.info(content);
logger.info("========== END CONTENT ==========");
try {
// Extract JSON from response
logger.debug("Extracting JSON from response...");
logger.info("Extracting JSON from response...");
logger.info(`Looking for pattern: /{[\\s\\S]*"features"[\\s\\S]*}/`);
const jsonMatch = content.match(/\{[\s\S]*"features"[\s\S]*\}/);
if (!jsonMatch) {
logger.error("❌ No valid JSON found in response");
logger.error("Content preview:", content.substring(0, 500));
logger.error("Full content received:");
logger.error(content);
throw new Error("No valid JSON found in response");
}
logger.debug(`JSON match found (${jsonMatch[0].length} chars)`);
logger.info(`JSON match found (${jsonMatch[0].length} chars)`);
logger.info("========== MATCHED JSON ==========");
logger.info(jsonMatch[0]);
logger.info("========== END MATCHED JSON ==========");
const parsed = JSON.parse(jsonMatch[0]);
logger.info(`Parsed ${parsed.features?.length || 0} features`);
logger.info("Parsed features:", JSON.stringify(parsed.features, null, 2));
const featuresDir = path.join(projectPath, ".automaker", "features");
await fs.mkdir(featuresDir, { recursive: true });

View File

@@ -2,9 +2,10 @@
* Business logic for generating suggestions
*/
import { query, type Options } from "@anthropic-ai/claude-agent-sdk";
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";
const logger = createLogger("Suggestions");
@@ -54,14 +55,10 @@ Format your response as JSON:
content: `Starting ${suggestionType} analysis...\n`,
});
const options: Options = {
model: "claude-opus-4-5-20251101",
maxTurns: 5,
const options = createSuggestionsOptions({
cwd: projectPath,
allowedTools: ["Read", "Glob", "Grep"],
permissionMode: "acceptEdits",
abortController,
};
});
const stream = query({ prompt, options });
let responseText = "";