diff --git a/.automaker/feature_list.json b/.automaker/feature_list.json index 65bc0ad6..ab138177 100644 --- a/.automaker/feature_list.json +++ b/.automaker/feature_list.json @@ -51,5 +51,25 @@ "description": "So i want to improve the look of the view of agent output modal its just plain text and im thinking to parse it better and kinda make it look like the last image of coolify logs nice colorded and somehow grouped into some types of info / debug so in our case like prompt / tool call etc", "steps": [], "status": "verified" + }, + { + "id": "feature-1765318148517-715isvwwb", + "category": "Kanban", + "description": "When agent finish work the cards is moved either to waiting approval or into verified one But mostly its include some type of summary at the end i want you to modify our prompts and ui so when its in both states we can see the feature summary of what was done / modified instead of relying on going to code editor to see what got changed etc.", + "steps": [], + "status": "verified", + "startedAt": "2025-12-09T22:09:13.684Z", + "imagePaths": [], + "skipTests": true + }, + { + "id": "feature-1765319491258-x933j6kbq", + "category": "Core", + "description": "When running new feature in skip automated testing once its got finished its moved to waiting approval for us to manual test it / follow up prompt. Once we are satisfied we can click commit button so ai agent can commit it work this is only hapening in this scenerio because if we have unchecked the skip automated testing its do it automaticly and commit already. But the issue is when its going to commit we move it to in progress state where we can use stop button and if user use that button its moved to backlog column and. that kinda break what we are doing becase we have no longer even abbility to move it back to waiting approval or to run commit button / follow up again so if user use manual one and stop the commit i want it to be again moved back to waiting approval state / column", + "steps": [], + "status": "verified", + "startedAt": "2025-12-09T22:31:41.946Z", + "imagePaths": [], + "skipTests": true } ] \ No newline at end of file diff --git a/app/electron/auto-mode-service.js b/app/electron/auto-mode-service.js index 601683b2..412973d9 100644 --- a/app/electron/auto-mode-service.js +++ b/app/electron/auto-mode-service.js @@ -32,7 +32,7 @@ class AutoModeService { query: null, projectPath: null, sendToRenderer: null, - isActive: () => this.runningFeatures.has(featureId) + isActive: () => this.runningFeatures.has(featureId), }; return context; } @@ -126,7 +126,11 @@ class AutoModeService { console.log(`[AutoMode] Running feature: ${feature.description}`); // Update feature status to in_progress - await featureLoader.updateFeatureStatus(featureId, "in_progress", projectPath); + await featureLoader.updateFeatureStatus( + featureId, + "in_progress", + projectPath + ); sendToRenderer({ type: "auto_mode_feature_start", @@ -135,13 +139,28 @@ class AutoModeService { }); // Implement the feature - const result = await featureExecutor.implementFeature(feature, projectPath, sendToRenderer, execution); + const result = await featureExecutor.implementFeature( + feature, + projectPath, + sendToRenderer, + execution + ); // Update feature status based on result - const newStatus = result.passes ? "verified" : "backlog"; - await featureLoader.updateFeatureStatus(feature.id, newStatus, projectPath); + // For skipTests features, go to waiting_approval on success instead of verified + let newStatus; + if (result.passes) { + newStatus = feature.skipTests ? "waiting_approval" : "verified"; + } else { + newStatus = "backlog"; + } + await featureLoader.updateFeatureStatus( + feature.id, + newStatus, + projectPath + ); - // Delete context file if verified + // Delete context file only if verified (not for waiting_approval) if (newStatus === "verified") { await contextManager.deleteContextFile(projectPath, feature.id); } @@ -208,11 +227,20 @@ class AutoModeService { }); // Verify the feature by running tests - const result = await featureVerifier.verifyFeatureTests(feature, projectPath, sendToRenderer, execution); + const result = await featureVerifier.verifyFeatureTests( + feature, + projectPath, + sendToRenderer, + execution + ); // Update feature status based on result const newStatus = result.passes ? "verified" : "in_progress"; - await featureLoader.updateFeatureStatus(featureId, newStatus, projectPath); + await featureLoader.updateFeatureStatus( + featureId, + newStatus, + projectPath + ); // Delete context file if verified if (newStatus === "verified") { @@ -281,10 +309,19 @@ class AutoModeService { }); // Read existing context - const previousContext = await contextManager.readContextFile(projectPath, featureId); + const previousContext = await contextManager.readContextFile( + projectPath, + featureId + ); // Resume implementation with context - const result = await featureExecutor.resumeFeatureWithContext(feature, projectPath, sendToRenderer, previousContext, execution); + const result = await featureExecutor.resumeFeatureWithContext( + feature, + projectPath, + sendToRenderer, + previousContext, + execution + ); // If the agent ends early without finishing, automatically re-run let attempts = 0; @@ -298,11 +335,16 @@ class AutoModeService { if (updatedFeature && updatedFeature.status === "in_progress") { attempts++; - console.log(`[AutoMode] Feature ended early, auto-retrying (attempt ${attempts}/${maxAttempts})...`); + console.log( + `[AutoMode] Feature ended early, auto-retrying (attempt ${attempts}/${maxAttempts})...` + ); // Update context file with retry message - await contextManager.writeToContextFile(projectPath, featureId, - `\n\nšŸ”„ Auto-retry #${attempts} - Continuing implementation...\n\n`); + await contextManager.writeToContextFile( + projectPath, + featureId, + `\n\nšŸ”„ Auto-retry #${attempts} - Continuing implementation...\n\n` + ); sendToRenderer({ type: "auto_mode_progress", @@ -311,20 +353,39 @@ class AutoModeService { }); // Read updated context - const retryContext = await contextManager.readContextFile(projectPath, featureId); + const retryContext = await contextManager.readContextFile( + projectPath, + featureId + ); // Resume again with full context - finalResult = await featureExecutor.resumeFeatureWithContext(feature, projectPath, sendToRenderer, retryContext, execution); + finalResult = await featureExecutor.resumeFeatureWithContext( + feature, + projectPath, + sendToRenderer, + retryContext, + execution + ); } else { break; } } // Update feature status based on final result - const newStatus = finalResult.passes ? "verified" : "in_progress"; - await featureLoader.updateFeatureStatus(featureId, newStatus, projectPath); + // For skipTests features, go to waiting_approval on success instead of verified + let newStatus; + if (finalResult.passes) { + newStatus = feature.skipTests ? "waiting_approval" : "verified"; + } else { + newStatus = "in_progress"; + } + await featureLoader.updateFeatureStatus( + featureId, + newStatus, + projectPath + ); - // Delete context file if verified + // Delete context file only if verified (not for waiting_approval) if (newStatus === "verified") { await contextManager.deleteContextFile(projectPath, featureId); } @@ -377,7 +438,9 @@ class AutoModeService { // Skip if this feature is already running (via manual trigger) if (this.runningFeatures.has(currentFeatureId)) { - console.log(`[AutoMode] Skipping ${currentFeatureId} - already running`); + console.log( + `[AutoMode] Skipping ${currentFeatureId} - already running` + ); await this.sleep(3000); continue; } @@ -397,13 +460,28 @@ class AutoModeService { this.runningFeatures.set(currentFeatureId, execution); // Implement the feature - const result = await featureExecutor.implementFeature(nextFeature, projectPath, sendToRenderer, execution); + const result = await featureExecutor.implementFeature( + nextFeature, + projectPath, + sendToRenderer, + execution + ); // Update feature status based on result - const newStatus = result.passes ? "verified" : "backlog"; - await featureLoader.updateFeatureStatus(nextFeature.id, newStatus, projectPath); + // For skipTests features, go to waiting_approval on success instead of verified + let newStatus; + if (result.passes) { + newStatus = nextFeature.skipTests ? "waiting_approval" : "verified"; + } else { + newStatus = "backlog"; + } + await featureLoader.updateFeatureStatus( + nextFeature.id, + newStatus, + projectPath + ); - // Delete context file if verified + // Delete context file only if verified (not for waiting_approval) if (newStatus === "verified") { await contextManager.deleteContextFile(projectPath, nextFeature.id); } @@ -477,7 +555,12 @@ class AutoModeService { }); // Perform the analysis - const result = await projectAnalyzer.runProjectAnalysis(projectPath, analysisId, sendToRenderer, execution); + const result = await projectAnalyzer.runProjectAnalysis( + projectPath, + analysisId, + sendToRenderer, + execution + ); sendToRenderer({ type: "auto_mode_feature_complete", @@ -500,6 +583,239 @@ class AutoModeService { } } + /** + * Stop a specific feature by ID + */ + async stopFeature({ featureId }) { + if (!this.runningFeatures.has(featureId)) { + return { success: false, error: `Feature ${featureId} is not running` }; + } + + console.log(`[AutoMode] Stopping feature: ${featureId}`); + + const execution = this.runningFeatures.get(featureId); + if (execution && execution.abortController) { + execution.abortController.abort(); + } + + // Clean up + this.runningFeatures.delete(featureId); + + return { success: true }; + } + + /** + * Follow-up on a feature with additional prompt + * This continues work on a feature that's in waiting_approval status + */ + async followUpFeature({ + projectPath, + featureId, + prompt, + imagePaths, + sendToRenderer, + }) { + // Check if this feature is already running + if (this.runningFeatures.has(featureId)) { + throw new Error(`Feature ${featureId} is already running`); + } + + console.log( + `[AutoMode] Follow-up on feature: ${featureId} with prompt: ${prompt}` + ); + + // Register this feature as running + const execution = this.createExecutionContext(featureId); + execution.projectPath = projectPath; + execution.sendToRenderer = sendToRenderer; + this.runningFeatures.set(featureId, execution); + + // Start the async work in the background (don't await) + // This allows the API to return immediately so the modal can close + this.runFollowUpWork({ + projectPath, + featureId, + prompt, + imagePaths, + sendToRenderer, + execution, + }).catch((error) => { + console.error("[AutoMode] Follow-up work error:", error); + this.runningFeatures.delete(featureId); + }); + + // Return immediately so the frontend can close the modal + return { success: true }; + } + + /** + * Internal method to run follow-up work asynchronously + */ + async runFollowUpWork({ + projectPath, + featureId, + prompt, + imagePaths, + sendToRenderer, + execution, + }) { + try { + // Load features + const features = await featureLoader.loadFeatures(projectPath); + const feature = features.find((f) => f.id === featureId); + + if (!feature) { + throw new Error(`Feature ${featureId} not found`); + } + + console.log(`[AutoMode] Following up on feature: ${feature.description}`); + + // Update status to in_progress + await featureLoader.updateFeatureStatus( + featureId, + "in_progress", + projectPath + ); + + sendToRenderer({ + type: "auto_mode_feature_start", + featureId: feature.id, + feature: feature, + }); + + // Read existing context and append follow-up prompt + const previousContext = await contextManager.readContextFile( + projectPath, + featureId + ); + + // Append follow-up prompt to context + const followUpContext = `${previousContext}\n\n## Follow-up Instructions\n\n${prompt}`; + await contextManager.writeToContextFile( + projectPath, + featureId, + `\n\n## Follow-up Instructions\n\n${prompt}` + ); + + // Resume implementation with follow-up context and optional images + const result = await featureExecutor.resumeFeatureWithContext( + { ...feature, followUpPrompt: prompt, followUpImages: imagePaths }, + projectPath, + sendToRenderer, + followUpContext, + execution + ); + + // For skipTests features, go to waiting_approval on success instead of verified + const newStatus = result.passes + ? feature.skipTests + ? "waiting_approval" + : "verified" + : "in_progress"; + + await featureLoader.updateFeatureStatus( + feature.id, + newStatus, + projectPath + ); + + // Delete context file if verified (only for non-skipTests) + if (newStatus === "verified") { + await contextManager.deleteContextFile(projectPath, feature.id); + } + + sendToRenderer({ + type: "auto_mode_feature_complete", + featureId: feature.id, + passes: result.passes, + message: result.message, + }); + } catch (error) { + console.error("[AutoMode] Error in follow-up:", error); + sendToRenderer({ + type: "auto_mode_error", + error: error.message, + featureId: featureId, + }); + } finally { + this.runningFeatures.delete(featureId); + } + } + + /** + * Commit changes for a feature without doing additional work + * This marks the feature as verified and commits the changes + */ + async commitFeature({ projectPath, featureId, sendToRenderer }) { + console.log(`[AutoMode] Committing feature: ${featureId}`); + + // Register briefly as running for the commit operation + const execution = this.createExecutionContext(featureId); + execution.projectPath = projectPath; + execution.sendToRenderer = sendToRenderer; + this.runningFeatures.set(featureId, execution); + + try { + // Load feature to get description for commit message + const features = await featureLoader.loadFeatures(projectPath); + const feature = features.find((f) => f.id === featureId); + + if (!feature) { + throw new Error(`Feature ${featureId} not found`); + } + + sendToRenderer({ + type: "auto_mode_feature_start", + featureId: feature.id, + feature: { ...feature, description: "Committing changes..." }, + }); + + sendToRenderer({ + type: "auto_mode_phase", + featureId, + phase: "action", + message: "Committing changes to git...", + }); + + // Run git commit via the agent + const commitResult = await featureExecutor.commitChangesOnly( + feature, + projectPath, + sendToRenderer, + execution + ); + + // Update status to verified + await featureLoader.updateFeatureStatus( + featureId, + "verified", + projectPath + ); + + // Delete context file + await contextManager.deleteContextFile(projectPath, featureId); + + sendToRenderer({ + type: "auto_mode_feature_complete", + featureId: feature.id, + passes: true, + message: "Changes committed successfully", + }); + + return { success: true }; + } catch (error) { + console.error("[AutoMode] Error committing feature:", error); + sendToRenderer({ + type: "auto_mode_error", + error: error.message, + featureId: featureId, + }); + throw error; + } finally { + this.runningFeatures.delete(featureId); + } + } + /** * Sleep helper */ diff --git a/app/electron/main.js b/app/electron/main.js index e55f3999..75d45d72 100644 --- a/app/electron/main.js +++ b/app/electron/main.js @@ -500,3 +500,54 @@ ipcMain.handle("auto-mode:analyze-project", async (_, { projectPath }) => { return { success: false, error: error.message }; } }); + +/** + * Stop a specific feature + */ +ipcMain.handle("auto-mode:stop-feature", async (_, { featureId }) => { + console.log("[IPC] auto-mode:stop-feature called with:", { featureId }); + try { + return await autoModeService.stopFeature({ featureId }); + } catch (error) { + console.error("[IPC] auto-mode:stop-feature error:", error); + return { success: false, error: error.message }; + } +}); + +/** + * Follow-up on a feature with additional prompt + */ +ipcMain.handle("auto-mode:follow-up-feature", async (_, { projectPath, featureId, prompt, imagePaths }) => { + console.log("[IPC] auto-mode:follow-up-feature called with:", { projectPath, featureId, prompt, imagePaths }); + try { + const sendToRenderer = (data) => { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send("auto-mode:event", data); + } + }; + + return await autoModeService.followUpFeature({ projectPath, featureId, prompt, imagePaths, sendToRenderer }); + } catch (error) { + console.error("[IPC] auto-mode:follow-up-feature error:", error); + return { success: false, error: error.message }; + } +}); + +/** + * Commit changes for a feature (no further work, just commit) + */ +ipcMain.handle("auto-mode:commit-feature", async (_, { projectPath, featureId }) => { + console.log("[IPC] auto-mode:commit-feature called with:", { projectPath, featureId }); + try { + const sendToRenderer = (data) => { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send("auto-mode:event", data); + } + }; + + return await autoModeService.commitFeature({ projectPath, featureId, sendToRenderer }); + } catch (error) { + console.error("[IPC] auto-mode:commit-feature error:", error); + return { success: false, error: error.message }; + } +}); diff --git a/app/electron/preload.js b/app/electron/preload.js index 0b6e03c2..1b5f49e0 100644 --- a/app/electron/preload.js +++ b/app/electron/preload.js @@ -115,6 +115,18 @@ contextBridge.exposeInMainWorld("electronAPI", { analyzeProject: (projectPath) => ipcRenderer.invoke("auto-mode:analyze-project", { projectPath }), + // Stop a specific feature + stopFeature: (featureId) => + ipcRenderer.invoke("auto-mode:stop-feature", { featureId }), + + // Follow-up on a feature with additional prompt + followUpFeature: (projectPath, featureId, prompt, imagePaths) => + ipcRenderer.invoke("auto-mode:follow-up-feature", { projectPath, featureId, prompt, imagePaths }), + + // Commit changes for a feature + commitFeature: (projectPath, featureId) => + ipcRenderer.invoke("auto-mode:commit-feature", { projectPath, featureId }), + // Listen for auto mode events onEvent: (callback) => { const subscription = (_, data) => callback(data); diff --git a/app/electron/services/feature-executor.js b/app/electron/services/feature-executor.js index b5109c8b..239c7518 100644 --- a/app/electron/services/feature-executor.js +++ b/app/electron/services/feature-executor.js @@ -182,10 +182,12 @@ class FeatureExecutor { content: checkingMsg, }); - // Re-load features to check if it was marked as verified + // Re-load features to check if it was marked as verified or waiting_approval (for skipTests) const updatedFeatures = await featureLoader.loadFeatures(projectPath); const updatedFeature = updatedFeatures.find((f) => f.id === feature.id); - const passes = updatedFeature?.status === "verified"; + // For skipTests features, waiting_approval is also considered a success + const passes = updatedFeature?.status === "verified" || + (updatedFeature?.skipTests && updatedFeature?.status === "waiting_approval"); // Send verification result const resultMsg = passes @@ -312,10 +314,12 @@ class FeatureExecutor { execution.query = null; execution.abortController = null; - // Check if feature was marked as verified + // Check if feature was marked as verified or waiting_approval (for skipTests) const updatedFeatures = await featureLoader.loadFeatures(projectPath); const updatedFeature = updatedFeatures.find((f) => f.id === feature.id); - const passes = updatedFeature?.status === "verified"; + // For skipTests features, waiting_approval is also considered a success + const passes = updatedFeature?.status === "verified" || + (updatedFeature?.skipTests && updatedFeature?.status === "waiting_approval"); const finalMsg = passes ? "āœ“ Feature successfully verified and completed\n" @@ -354,6 +358,171 @@ class FeatureExecutor { throw error; } } + + /** + * Commit changes for a feature without doing additional work + * Analyzes changes and creates a proper conventional commit message + */ + async commitChangesOnly(feature, projectPath, sendToRenderer, execution) { + console.log(`[FeatureExecutor] Committing changes for: ${feature.description}`); + + try { + const commitMessage = `\nšŸ“ Committing changes for: ${feature.description}\n`; + await contextManager.writeToContextFile(projectPath, feature.id, commitMessage); + + sendToRenderer({ + type: "auto_mode_progress", + featureId: feature.id, + content: "Analyzing changes and creating commit...", + }); + + const abortController = new AbortController(); + execution.abortController = abortController; + + // Create custom MCP server with UpdateFeatureStatus tool + const featureToolsServer = mcpServerFactory.createFeatureToolsServer( + featureLoader.updateFeatureStatus.bind(featureLoader), + projectPath + ); + + const options = { + model: "claude-sonnet-4-20250514", // Use sonnet for commit task + systemPrompt: `You are a git commit assistant that creates professional conventional commit messages. + +IMPORTANT RULES: +- DO NOT modify any code +- DO NOT write tests +- DO NOT do anything except analyzing changes and committing them +- Use the git command line tools via Bash +- Create proper conventional commit messages based on what was actually changed`, + maxTurns: 15, // Allow some turns to analyze and commit + cwd: projectPath, + mcpServers: { + "automaker-tools": featureToolsServer + }, + allowedTools: ["Bash", "mcp__automaker-tools__UpdateFeatureStatus"], + permissionMode: "acceptEdits", + sandbox: { + enabled: false, // Need to run git commands + }, + abortController: abortController, + }; + + // Prompt that guides the agent to create a proper conventional commit + const prompt = `Please commit the current changes with a proper conventional commit message. + +**Feature Context:** +Category: ${feature.category} +Description: ${feature.description} + +**Your Task:** + +1. First, run \`git status\` to see all untracked and modified files +2. Run \`git diff\` to see the actual changes (both staged and unstaged) +3. Run \`git log --oneline -5\` to see recent commit message styles in this repo +4. Analyze all the changes and draft a proper conventional commit message: + - Use conventional commit format: \`type(scope): description\` + - Types: feat, fix, refactor, style, docs, test, chore + - The description should be concise (under 72 chars) and focus on "what" was done + - Summarize the nature of the changes (new feature, enhancement, bug fix, etc.) + - Make sure the commit message accurately reflects the actual code changes +5. Run \`git add .\` to stage all changes +6. Create the commit with a message ending with: + šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) + + Co-Authored-By: Claude Sonnet 4 + +Use a HEREDOC for the commit message to ensure proper formatting: +\`\`\`bash +git commit -m "$(cat <<'EOF' +type(scope): Short description here + +Optional longer description if needed. + +šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) + +Co-Authored-By: Claude Sonnet 4 +EOF +)" +\`\`\` + +**IMPORTANT:** +- DO NOT use the feature description verbatim as the commit message +- Analyze the actual code changes to determine the appropriate commit message +- The commit message should be professional and follow conventional commit standards +- DO NOT modify any code or run tests - ONLY commit the existing changes`; + + const currentQuery = query({ prompt, options }); + execution.query = currentQuery; + + let responseText = ""; + for await (const msg of currentQuery) { + if (!execution.isActive()) break; + + if (msg.type === "assistant" && msg.message?.content) { + for (const block of msg.message.content) { + if (block.type === "text") { + responseText += block.text; + + await contextManager.writeToContextFile(projectPath, feature.id, block.text); + + sendToRenderer({ + type: "auto_mode_progress", + featureId: feature.id, + content: block.text, + }); + } else if (block.type === "tool_use") { + const toolMsg = `\nšŸ”§ Tool: ${block.name}\n`; + await contextManager.writeToContextFile(projectPath, feature.id, toolMsg); + + sendToRenderer({ + type: "auto_mode_tool", + featureId: feature.id, + tool: block.name, + input: block.input, + }); + } + } + } + } + + execution.query = null; + execution.abortController = null; + + const finalMsg = "āœ“ Changes committed successfully\n"; + await contextManager.writeToContextFile(projectPath, feature.id, finalMsg); + + sendToRenderer({ + type: "auto_mode_progress", + featureId: feature.id, + content: finalMsg, + }); + + return { + passes: true, + message: responseText.substring(0, 500), + }; + } catch (error) { + if (error instanceof AbortError || error?.name === "AbortError") { + console.log("[FeatureExecutor] Commit aborted"); + if (execution) { + execution.abortController = null; + execution.query = null; + } + return { + passes: false, + message: "Commit aborted", + }; + } + + console.error("[FeatureExecutor] Error committing feature:", error); + if (execution) { + execution.abortController = null; + execution.query = null; + } + throw error; + } + } } module.exports = new FeatureExecutor(); diff --git a/app/electron/services/feature-loader.js b/app/electron/services/feature-loader.js index 6d77c77c..e8e1bbbd 100644 --- a/app/electron/services/feature-loader.js +++ b/app/electron/services/feature-loader.js @@ -32,8 +32,12 @@ class FeatureLoader { /** * Update feature status in .automaker/feature_list.json + * @param {string} featureId - The ID of the feature to update + * @param {string} status - The new status + * @param {string} projectPath - Path to the project + * @param {string} [summary] - Optional summary of what was done */ - async updateFeatureStatus(featureId, status, projectPath) { + async updateFeatureStatus(featureId, status, projectPath, summary) { const features = await this.loadFeatures(projectPath); const feature = features.find((f) => f.id === featureId); @@ -45,31 +49,56 @@ class FeatureLoader { // Update the status field feature.status = status; + // Update the summary field if provided + if (summary) { + feature.summary = summary; + } + // Save back to file const featuresPath = path.join( projectPath, ".automaker", "feature_list.json" ); - const toSave = features.map((f) => ({ - id: f.id, - category: f.category, - description: f.description, - steps: f.steps, - status: f.status, - })); + const toSave = features.map((f) => { + const featureData = { + id: f.id, + category: f.category, + description: f.description, + steps: f.steps, + status: f.status, + }; + // Preserve optional fields if they exist + if (f.skipTests !== undefined) { + featureData.skipTests = f.skipTests; + } + if (f.images !== undefined) { + featureData.images = f.images; + } + if (f.imagePaths !== undefined) { + featureData.imagePaths = f.imagePaths; + } + if (f.startedAt !== undefined) { + featureData.startedAt = f.startedAt; + } + if (f.summary !== undefined) { + featureData.summary = f.summary; + } + return featureData; + }); await fs.writeFile(featuresPath, JSON.stringify(toSave, null, 2), "utf-8"); - console.log(`[FeatureLoader] Updated feature ${featureId}: status=${status}`); + console.log(`[FeatureLoader] Updated feature ${featureId}: status=${status}${summary ? `, summary="${summary}"` : ""}`); } /** * Select the next feature to implement - * Prioritizes: earlier features in the list that are not verified + * Prioritizes: earlier features in the list that are not verified or waiting_approval */ selectNextFeature(features) { // Find first feature that is in backlog or in_progress status - return features.find((f) => f.status !== "verified"); + // Skip verified and waiting_approval (which needs user input) + return features.find((f) => f.status !== "verified" && f.status !== "waiting_approval"); } } diff --git a/app/electron/services/feature-verifier.js b/app/electron/services/feature-verifier.js index 1d3b5708..000ee72c 100644 --- a/app/electron/services/feature-verifier.js +++ b/app/electron/services/feature-verifier.js @@ -101,10 +101,12 @@ class FeatureVerifier { execution.query = null; execution.abortController = null; - // Re-load features to check if it was marked as verified + // Re-load features to check if it was marked as verified or waiting_approval (for skipTests) const updatedFeatures = await featureLoader.loadFeatures(projectPath); const updatedFeature = updatedFeatures.find((f) => f.id === feature.id); - const passes = updatedFeature?.status === "verified"; + // For skipTests features, waiting_approval is also considered a success + const passes = updatedFeature?.status === "verified" || + (updatedFeature?.skipTests && updatedFeature?.status === "waiting_approval"); const finalMsg = passes ? "āœ“ Verification successful: All tests passed\n" diff --git a/app/electron/services/mcp-server-factory.js b/app/electron/services/mcp-server-factory.js index c508906b..bbb57d5e 100644 --- a/app/electron/services/mcp-server-factory.js +++ b/app/electron/services/mcp-server-factory.js @@ -1,5 +1,6 @@ const { createSdkMcpServer, tool } = require("@anthropic-ai/claude-agent-sdk"); const { z } = require("zod"); +const featureLoader = require("./feature-loader"); /** * MCP Server Factory - Creates custom MCP servers with tools @@ -18,22 +19,42 @@ class McpServerFactory { tools: [ tool( "UpdateFeatureStatus", - "Update the status of a feature in the feature list. Use this tool instead of directly modifying feature_list.json to safely update feature status.", + "Update the status of a feature in the feature list. Use this tool instead of directly modifying feature_list.json to safely update feature status. IMPORTANT: If the feature has skipTests=true, you should NOT mark it as verified - instead it will automatically go to waiting_approval status for manual review. Always include a summary of what was done.", { featureId: z.string().describe("The ID of the feature to update"), - status: z.enum(["backlog", "in_progress", "verified"]).describe("The new status for the feature") + status: z.enum(["backlog", "in_progress", "verified"]).describe("The new status for the feature. Note: If skipTests=true, verified will be converted to waiting_approval automatically."), + summary: z.string().optional().describe("A brief summary of what was implemented/changed. This will be displayed on the Kanban card. Example: 'Added dark mode toggle. Modified: settings.tsx, theme-provider.tsx'") }, async (args) => { try { - console.log(`[McpServerFactory] UpdateFeatureStatus tool called: featureId=${args.featureId}, status=${args.status}`); + console.log(`[McpServerFactory] UpdateFeatureStatus tool called: featureId=${args.featureId}, status=${args.status}, summary=${args.summary || "(none)"}`); - // Call the provided callback to update feature status - await updateFeatureStatusCallback(args.featureId, args.status, projectPath); + // Load the feature to check skipTests flag + const features = await featureLoader.loadFeatures(projectPath); + const feature = features.find((f) => f.id === args.featureId); + + if (!feature) { + throw new Error(`Feature ${args.featureId} not found`); + } + + // If agent tries to mark as verified but feature has skipTests=true, convert to waiting_approval + let finalStatus = args.status; + if (args.status === "verified" && feature.skipTests === true) { + console.log(`[McpServerFactory] Feature ${args.featureId} has skipTests=true, converting verified -> waiting_approval`); + finalStatus = "waiting_approval"; + } + + // Call the provided callback to update feature status with summary + await updateFeatureStatusCallback(args.featureId, finalStatus, projectPath, args.summary); + + const statusMessage = finalStatus !== args.status + ? `Successfully updated feature ${args.featureId} to status "${finalStatus}" (converted from "${args.status}" because skipTests=true)${args.summary ? ` with summary: "${args.summary}"` : ""}` + : `Successfully updated feature ${args.featureId} to status "${finalStatus}"${args.summary ? ` with summary: "${args.summary}"` : ""}`; return { content: [{ type: "text", - text: `Successfully updated feature ${args.featureId} to status "${args.status}"` + text: statusMessage }] }; } catch (error) { diff --git a/app/electron/services/prompt-builder.js b/app/electron/services/prompt-builder.js index a070bc38..cd75ac36 100644 --- a/app/electron/services/prompt-builder.js +++ b/app/electron/services/prompt-builder.js @@ -6,6 +6,10 @@ class PromptBuilder { * Build the prompt for implementing a specific feature */ buildFeaturePrompt(feature) { + const skipTestsNote = feature.skipTests + ? `\n**āš ļø IMPORTANT - Manual Testing Mode:**\nThis feature has skipTests=true, which means:\n- DO NOT commit changes automatically\n- DO NOT mark as verified - it will automatically go to "waiting_approval" status\n- The user will manually review and commit the changes\n- Just implement the feature and mark it as verified (it will be converted to waiting_approval)\n` + : ""; + return `You are working on a feature implementation task. **Current Feature to Implement:** @@ -13,7 +17,7 @@ class PromptBuilder { ID: ${feature.id} Category: ${feature.category} Description: ${feature.description} - +${skipTestsNote} **Steps to Complete:** ${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")} @@ -21,30 +25,64 @@ ${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")} 1. Read the project files to understand the current codebase structure 2. Implement the feature according to the description and steps -3. Write Playwright tests to verify the feature works correctly -4. Run the tests and ensure they pass -5. **DELETE the test file(s) you created** - tests are only for immediate verification -6. **CRITICAL: Use the UpdateFeatureStatus tool to mark this feature as verified** - DO NOT manually edit .automaker/feature_list.json -7. Commit your changes with git +${ + feature.skipTests + ? "3. Test the implementation manually (no automated tests needed for skipTests features)" + : "3. Write Playwright tests to verify the feature works correctly\n4. Run the tests and ensure they pass\n5. **DELETE the test file(s) you created** - tests are only for immediate verification" +} +${ + feature.skipTests ? "4" : "6" +}. **CRITICAL: Use the UpdateFeatureStatus tool to mark this feature as verified** - DO NOT manually edit .automaker/feature_list.json +${ + feature.skipTests + ? "5. **DO NOT commit changes** - the user will review and commit manually" + : "7. Commit your changes with git" +} **IMPORTANT - Updating Feature Status:** -When you have completed the feature and all tests pass, you MUST use the \`mcp__automaker-tools__UpdateFeatureStatus\` tool to update the feature status: +When you have completed the feature${ + feature.skipTests ? "" : " and all tests pass" + }, you MUST use the \`mcp__automaker-tools__UpdateFeatureStatus\` tool to update the feature status: - Call the tool with: featureId="${feature.id}" and status="verified" +- **You can also include a summary parameter** to describe what was done: summary="Brief summary of changes" - **DO NOT manually edit the .automaker/feature_list.json file** - this can cause race conditions - The UpdateFeatureStatus tool safely updates the feature status without risk of corrupting other data +- **If skipTests=true, the tool will automatically convert "verified" to "waiting_approval"** - this is correct behavior + +**IMPORTANT - Feature Summary (REQUIRED):** + +When calling UpdateFeatureStatus, you MUST include a summary parameter that describes: +- What files were modified/created +- What functionality was added or changed +- Any notable implementation decisions + +Example: +\`\`\` +UpdateFeatureStatus(featureId="${ + feature.id + }", status="verified", summary="Added dark mode toggle to settings. Modified: settings.tsx, theme-provider.tsx. Created new useTheme hook.") +\`\`\` + +The summary will be displayed on the Kanban card so the user can see what was done without checking the code. **Important Guidelines:** - Focus ONLY on implementing this specific feature - Write clean, production-quality code - Add proper error handling -- Write comprehensive Playwright tests -- Ensure all existing tests still pass -- Mark the feature as passing only when all tests are green -- **CRITICAL: Delete test files after verification** - tests accumulate and become brittle +${ + feature.skipTests + ? "- Skip automated testing (skipTests=true) - user will manually verify" + : "- Write comprehensive Playwright tests\n- Ensure all existing tests still pass\n- Mark the feature as passing only when all tests are green\n- **CRITICAL: Delete test files after verification** - tests accumulate and become brittle" +} - **CRITICAL: Use UpdateFeatureStatus tool instead of editing feature_list.json directly** -- Make a git commit when complete +- **CRITICAL: Always include a summary when marking feature as verified** +${ + feature.skipTests + ? "- **DO NOT commit changes** - user will review and commit manually" + : "- Make a git commit when complete" +} **Testing Utilities (CRITICAL):** @@ -75,6 +113,10 @@ Begin by reading the project structure and then implementing the feature.`; * Build the prompt for verifying a specific feature */ buildVerificationPrompt(feature) { + const skipTestsNote = feature.skipTests + ? `\n**āš ļø IMPORTANT - Manual Testing Mode:**\nThis feature has skipTests=true, which means:\n- DO NOT commit changes automatically\n- DO NOT mark as verified - it will automatically go to "waiting_approval" status\n- The user will manually review and commit the changes\n- Just implement the feature and mark it as verified (it will be converted to waiting_approval)\n` + : ""; + return `You are implementing and verifying a feature until it is complete and working correctly. **Feature to Implement/Verify:** @@ -83,7 +125,7 @@ ID: ${feature.id} Category: ${feature.category} Description: ${feature.description} Current Status: ${feature.status} - +${skipTestsNote} **Steps that should be implemented:** ${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")} @@ -91,7 +133,10 @@ ${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")} 1. Read the project files to understand the current implementation 2. If the feature is not fully implemented, continue implementing it -3. Write or update Playwright tests to verify the feature works correctly +${ + feature.skipTests + ? "3. Test the implementation manually (no automated tests needed for skipTests features)" + : `3. Write or update Playwright tests to verify the feature works correctly 4. Run the Playwright tests: npx playwright test tests/[feature-name].spec.ts 5. Check if all tests pass 6. **If ANY tests fail:** @@ -101,17 +146,43 @@ ${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")} - Re-run the tests to verify the fixes - **REPEAT this process until ALL tests pass** 7. **If ALL tests pass:** - - **DELETE the test file(s) for this feature** - tests are only for immediate verification - - **CRITICAL: Use the UpdateFeatureStatus tool to mark this feature as verified** - DO NOT manually edit .automaker/feature_list.json - - Explain what was implemented/fixed and that all tests passed - - Commit your changes with git + - **DELETE the test file(s) for this feature** - tests are only for immediate verification` +} +${ + feature.skipTests ? "4" : "8" +}. **CRITICAL: Use the UpdateFeatureStatus tool to mark this feature as verified** - DO NOT manually edit .automaker/feature_list.json +${ + feature.skipTests + ? "5. **DO NOT commit changes** - the user will review and commit manually" + : "9. Explain what was implemented/fixed and that all tests passed\n10. Commit your changes with git" +} **IMPORTANT - Updating Feature Status:** -When all tests pass, you MUST use the \`mcp__automaker-tools__UpdateFeatureStatus\` tool to update the feature status: +When you have completed the feature${ + feature.skipTests ? "" : " and all tests pass" + }, you MUST use the \`mcp__automaker-tools__UpdateFeatureStatus\` tool to update the feature status: - Call the tool with: featureId="${feature.id}" and status="verified" +- **You can also include a summary parameter** to describe what was done: summary="Brief summary of changes" - **DO NOT manually edit the .automaker/feature_list.json file** - this can cause race conditions - The UpdateFeatureStatus tool safely updates the feature status without risk of corrupting other data +- **If skipTests=true, the tool will automatically convert "verified" to "waiting_approval"** - this is correct behavior + +**IMPORTANT - Feature Summary (REQUIRED):** + +When calling UpdateFeatureStatus, you MUST include a summary parameter that describes: +- What files were modified/created +- What functionality was added or changed +- Any notable implementation decisions + +Example: +\`\`\` +UpdateFeatureStatus(featureId="${ + feature.id + }", status="verified", summary="Added dark mode toggle to settings. Modified: settings.tsx, theme-provider.tsx. Created new useTheme hook.") +\`\`\` + +The summary will be displayed on the Kanban card so the user can see what was done without checking the code. **Testing Utilities:** - Check if tests/utils.ts exists and is being used @@ -126,13 +197,13 @@ rm tests/[feature-name].spec.ts \`\`\` **Important:** -- **CONTINUE IMPLEMENTING until all tests pass** - don't stop at the first failure -- Only mark as "verified" if Playwright tests pass -- **CRITICAL: Delete test files after they pass** - tests should not accumulate +${ + feature.skipTests + ? "- Skip automated testing (skipTests=true) - user will manually verify\n- **DO NOT commit changes** - user will review and commit manually" + : "- **CONTINUE IMPLEMENTING until all tests pass** - don't stop at the first failure\n- Only mark as verified if Playwright tests pass\n- **CRITICAL: Delete test files after they pass** - tests should not accumulate\n- Update test utilities if functionality changed\n- Make a git commit when the feature is complete\n- Be thorough and persistent in fixing issues" +} - **CRITICAL: Use UpdateFeatureStatus tool instead of editing feature_list.json directly** -- Update test utilities if functionality changed -- Make a git commit when the feature is complete -- Be thorough and persistent in fixing issues +- **CRITICAL: Always include a summary when marking feature as verified** Begin by reading the project structure and understanding what needs to be implemented or fixed.`; } @@ -141,6 +212,10 @@ Begin by reading the project structure and understanding what needs to be implem * Build prompt for resuming feature with previous context */ buildResumePrompt(feature, previousContext) { + const skipTestsNote = feature.skipTests + ? `\n**āš ļø IMPORTANT - Manual Testing Mode:**\nThis feature has skipTests=true, which means:\n- DO NOT commit changes automatically\n- DO NOT mark as verified - it will automatically go to "waiting_approval" status\n- The user will manually review and commit the changes\n- Just implement the feature and mark it as verified (it will be converted to waiting_approval)\n` + : ""; + return `You are resuming work on a feature implementation that was previously started. **Current Feature:** @@ -148,7 +223,7 @@ Begin by reading the project structure and understanding what needs to be implem ID: ${feature.id} Category: ${feature.category} Description: ${feature.description} - +${skipTestsNote} **Steps to Complete:** ${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")} @@ -162,29 +237,64 @@ Continue where you left off and complete the feature implementation: 1. Review the previous work context above to understand what has been done 2. Continue implementing the feature according to the description and steps -3. Write Playwright tests to verify the feature works correctly (if not already done) -4. Run the tests and ensure they pass -5. **DELETE the test file(s) you created** - tests are only for immediate verification -6. **CRITICAL: Use the UpdateFeatureStatus tool to mark this feature as verified** - DO NOT manually edit .automaker/feature_list.json -7. Commit your changes with git +${ + feature.skipTests + ? "3. Test the implementation manually (no automated tests needed for skipTests features)" + : "3. Write Playwright tests to verify the feature works correctly (if not already done)\n4. Run the tests and ensure they pass\n5. **DELETE the test file(s) you created** - tests are only for immediate verification" +} +${ + feature.skipTests ? "4" : "6" +}. **CRITICAL: Use the UpdateFeatureStatus tool to mark this feature as verified** - DO NOT manually edit .automaker/feature_list.json +${ + feature.skipTests + ? "5. **DO NOT commit changes** - the user will review and commit manually" + : "7. Commit your changes with git" +} **IMPORTANT - Updating Feature Status:** -When all tests pass, you MUST use the \`mcp__automaker-tools__UpdateFeatureStatus\` tool to update the feature status: +When you have completed the feature${ + feature.skipTests ? "" : " and all tests pass" + }, you MUST use the \`mcp__automaker-tools__UpdateFeatureStatus\` tool to update the feature status: - Call the tool with: featureId="${feature.id}" and status="verified" +- **You can also include a summary parameter** to describe what was done: summary="Brief summary of changes" - **DO NOT manually edit the .automaker/feature_list.json file** - this can cause race conditions - The UpdateFeatureStatus tool safely updates the feature status without risk of corrupting other data +- **If skipTests=true, the tool will automatically convert "verified" to "waiting_approval"** - this is correct behavior + +**IMPORTANT - Feature Summary (REQUIRED):** + +When calling UpdateFeatureStatus, you MUST include a summary parameter that describes: +- What files were modified/created +- What functionality was added or changed +- Any notable implementation decisions + +Example: +\`\`\` +UpdateFeatureStatus(featureId="${ + feature.id + }", status="verified", summary="Added dark mode toggle to settings. Modified: settings.tsx, theme-provider.tsx. Created new useTheme hook.") +\`\`\` + +The summary will be displayed on the Kanban card so the user can see what was done without checking the code. **Important Guidelines:** - Review what was already done in the previous context - Don't redo work that's already complete - continue from where it left off - Focus on completing any remaining tasks -- Write comprehensive Playwright tests if not already done -- Ensure all tests pass before marking as verified -- **CRITICAL: Delete test files after verification** +${ + feature.skipTests + ? "- Skip automated testing (skipTests=true) - user will manually verify" + : "- Write comprehensive Playwright tests if not already done\n- Ensure all tests pass before marking as verified\n- **CRITICAL: Delete test files after verification**" +} - **CRITICAL: Use UpdateFeatureStatus tool instead of editing feature_list.json directly** -- Make a git commit when complete +- **CRITICAL: Always include a summary when marking feature as verified** +${ + feature.skipTests + ? "- **DO NOT commit changes** - user will review and commit manually" + : "- Make a git commit when complete" +} Begin by assessing what's been done and what remains to be completed.`; } @@ -278,18 +388,38 @@ Begin by exploring the project structure.`; Your role is to: - Implement features exactly as specified - Write production-quality code -- Create comprehensive Playwright tests using testing utilities -- Ensure all tests pass before marking features complete -- **DELETE test files after successful verification** - tests are only for immediate feature verification +- Check if feature.skipTests is true - if so, skip automated testing and don't commit +- Create comprehensive Playwright tests using testing utilities (only if skipTests is false) +- Ensure all tests pass before marking features complete (only if skipTests is false) +- **DELETE test files after successful verification** - tests are only for immediate feature verification (only if skipTests is false) - **Use the UpdateFeatureStatus tool to mark features as verified** - NEVER manually edit feature_list.json -- Commit working code to git +- **Always include a summary parameter when calling UpdateFeatureStatus** - describe what was done +- Commit working code to git (only if skipTests is false - skipTests features require manual review) - Be thorough and detail-oriented +**IMPORTANT - Manual Testing Mode (skipTests=true):** +If a feature has skipTests=true: +- DO NOT write automated tests +- DO NOT commit changes - the user will review and commit manually +- Still mark the feature as verified using UpdateFeatureStatus - it will automatically convert to "waiting_approval" for manual review +- The user will manually verify and commit the changes + **IMPORTANT - UpdateFeatureStatus Tool:** -You have access to the \`mcp__automaker-tools__UpdateFeatureStatus\` tool. When all tests pass, use this tool to update the feature status: -- Call with featureId and status="verified" +You have access to the \`mcp__automaker-tools__UpdateFeatureStatus\` tool. When the feature is complete (and all tests pass if skipTests is false), use this tool to update the feature status: +- Call with featureId, status="verified", and summary="Description of what was done" - **DO NOT manually edit .automaker/feature_list.json** - this can cause race conditions and restore old state - The tool safely updates the status without corrupting other feature data +- **If skipTests=true, the tool will automatically convert "verified" to "waiting_approval"** - this is correct + +**IMPORTANT - Feature Summary (REQUIRED):** +When calling UpdateFeatureStatus, you MUST include a summary parameter that describes: +- What files were modified/created +- What functionality was added or changed +- Any notable implementation decisions + +Example: summary="Added dark mode toggle. Modified: settings.tsx, theme-provider.tsx. Created useTheme hook." + +The summary will be displayed on the Kanban card so the user can quickly see what was done. **Testing Utilities (CRITICAL):** - **Create and maintain tests/utils.ts** with helper functions for finding elements and common operations @@ -327,21 +457,41 @@ Focus on one feature at a time and complete it fully before finishing. Always de Your role is to: - **Continue implementing features until they are complete** - don't stop at the first failure -- Write or update code to fix failing tests -- Run Playwright tests to verify feature implementations -- If tests fail, analyze errors and fix the implementation -- If other tests fail, verify if those tests are still accurate or should be updated or deleted -- Continue rerunning tests and fixing issues until ALL tests pass -- **DELETE test files after successful verification** - tests are only for immediate feature verification +- Check if feature.skipTests is true - if so, skip automated testing and don't commit +- Write or update code to fix failing tests (only if skipTests is false) +- Run Playwright tests to verify feature implementations (only if skipTests is false) +- If tests fail, analyze errors and fix the implementation (only if skipTests is false) +- If other tests fail, verify if those tests are still accurate or should be updated or deleted (only if skipTests is false) +- Continue rerunning tests and fixing issues until ALL tests pass (only if skipTests is false) +- **DELETE test files after successful verification** - tests are only for immediate feature verification (only if skipTests is false) - **Use the UpdateFeatureStatus tool to mark features as verified** - NEVER manually edit feature_list.json -- **Update test utilities (tests/utils.ts) if functionality changed** - keep helpers in sync with code -- Commit working code to git +- **Always include a summary parameter when calling UpdateFeatureStatus** - describe what was done +- **Update test utilities (tests/utils.ts) if functionality changed** - keep helpers in sync with code (only if skipTests is false) +- Commit working code to git (only if skipTests is false - skipTests features require manual review) + +**IMPORTANT - Manual Testing Mode (skipTests=true):** +If a feature has skipTests=true: +- DO NOT write automated tests +- DO NOT commit changes - the user will review and commit manually +- Still mark the feature as verified using UpdateFeatureStatus - it will automatically convert to "waiting_approval" for manual review +- The user will manually verify and commit the changes **IMPORTANT - UpdateFeatureStatus Tool:** -You have access to the \`mcp__automaker-tools__UpdateFeatureStatus\` tool. When all tests pass, use this tool to update the feature status: -- Call with featureId and status="verified" +You have access to the \`mcp__automaker-tools__UpdateFeatureStatus\` tool. When the feature is complete (and all tests pass if skipTests is false), use this tool to update the feature status: +- Call with featureId, status="verified", and summary="Description of what was done" - **DO NOT manually edit .automaker/feature_list.json** - this can cause race conditions and restore old state - The tool safely updates the status without corrupting other feature data +- **If skipTests=true, the tool will automatically convert "verified" to "waiting_approval"** - this is correct + +**IMPORTANT - Feature Summary (REQUIRED):** +When calling UpdateFeatureStatus, you MUST include a summary parameter that describes: +- What files were modified/created +- What functionality was added or changed +- Any notable implementation decisions + +Example: summary="Fixed login validation. Modified: auth.ts, login-form.tsx. Added password strength check." + +The summary will be displayed on the Kanban card so the user can quickly see what was done. **Testing Utilities:** - Check if tests/utils.ts needs updates based on code changes @@ -365,7 +515,7 @@ You have access to: - Make git commits - **UpdateFeatureStatus tool** (mcp__automaker-tools__UpdateFeatureStatus) - Use this to update feature status -**CRITICAL:** Be persistent and thorough - keep iterating on the implementation until all tests pass. Don't give up after the first failure. Always delete tests after they pass, use the UpdateFeatureStatus tool, and commit your work.`; +**CRITICAL:** Be persistent and thorough - keep iterating on the implementation until all tests pass. Don't give up after the first failure. Always delete tests after they pass, use the UpdateFeatureStatus tool with a summary, and commit your work.`; } /** diff --git a/app/package-lock.json b/app/package-lock.json index 1152462e..93f82ab5 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "automaker", "version": "0.1.0", + "license": "Unlicense", "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.1.61", "@dnd-kit/core": "^6.3.1", @@ -26,6 +27,7 @@ "next": "16.0.7", "react": "19.2.0", "react-dom": "19.2.0", + "react-markdown": "^10.1.0", "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "zustand": "^5.0.9" @@ -3710,7 +3712,6 @@ "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/ms": "*" @@ -3720,9 +3721,17 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, "node_modules/@types/fs-extra": { "version": "9.0.13", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", @@ -3733,6 +3742,15 @@ "@types/node": "*" } }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", @@ -3764,11 +3782,19 @@ "@types/node": "*" } }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "dev": true, "license": "MIT" }, "node_modules/@types/node": { @@ -3797,7 +3823,6 @@ "version": "19.2.7", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", - "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -3823,6 +3848,12 @@ "@types/node": "*" } }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/@types/verror": { "version": "1.10.11", "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", @@ -4112,6 +4143,12 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, "node_modules/@unrs/resolver-binding-android-arm-eabi": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", @@ -4951,6 +4988,16 @@ "node": ">= 0.4" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -5381,6 +5428,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -5398,6 +5455,46 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -5583,6 +5680,16 @@ "node": ">= 0.8" } }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", @@ -5773,7 +5880,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -5841,7 +5947,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -5855,6 +5960,19 @@ } } }, + "node_modules/decode-named-character-reference": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -5960,6 +6078,15 @@ "node": ">=0.4.0" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -5984,6 +6111,19 @@ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/dir-compare": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", @@ -7045,6 +7185,16 @@ "node": ">=4.0" } }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -7062,6 +7212,12 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -7807,6 +7963,46 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -7857,6 +8053,16 @@ "dev": true, "license": "ISC" }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/http-cache-semantics": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", @@ -8041,6 +8247,12 @@ "dev": true, "license": "ISC" }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -8066,6 +8278,30 @@ "node": ">= 12" } }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -8237,6 +8473,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -8306,6 +8552,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", @@ -8376,6 +8632,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -9102,6 +9370,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -9258,6 +9536,159 @@ "node": ">= 0.4" } }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -9268,6 +9699,448 @@ "node": ">= 8" } }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -9489,7 +10362,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -10003,6 +10875,31 @@ "node": ">=6" } }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -10294,6 +11191,16 @@ "react-is": "^16.13.1" } }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -10384,6 +11291,33 @@ "dev": true, "license": "MIT" }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, "node_modules/react-remove-scroll": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", @@ -10525,6 +11459,39 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -11222,6 +12189,16 @@ "source-map": "^0.6.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -11442,6 +12419,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -11492,6 +12483,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -11824,6 +12833,26 @@ "tree-kill": "cli.js" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/truncate-utf8-bytes": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", @@ -12058,6 +13087,25 @@ "dev": true, "license": "MIT" }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unique-filename": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", @@ -12084,6 +13132,74 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -12243,6 +13359,34 @@ "node": ">=0.6.0" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/wait-on": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.3.tgz", @@ -12562,6 +13706,16 @@ "optional": true } } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/app/package.json b/app/package.json index ff8489ee..fc28c7e3 100644 --- a/app/package.json +++ b/app/package.json @@ -34,6 +34,7 @@ "next": "16.0.7", "react": "19.2.0", "react-dom": "19.2.0", + "react-markdown": "^10.1.0", "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "zustand": "^5.0.9" diff --git a/app/src/components/ui/markdown.tsx b/app/src/components/ui/markdown.tsx new file mode 100644 index 00000000..84473624 --- /dev/null +++ b/app/src/components/ui/markdown.tsx @@ -0,0 +1,48 @@ +"use client"; + +import ReactMarkdown from "react-markdown"; +import { cn } from "@/lib/utils"; + +interface MarkdownProps { + children: string; + className?: string; +} + +/** + * Reusable Markdown component for rendering markdown content + * Styled for dark mode with proper typography + */ +export function Markdown({ children, className }: MarkdownProps) { + return ( +
+ {children} +
+ ); +} diff --git a/app/src/components/views/agent-output-modal.tsx b/app/src/components/views/agent-output-modal.tsx index e38a8799..14bb1787 100644 --- a/app/src/components/views/agent-output-modal.tsx +++ b/app/src/components/views/agent-output-modal.tsx @@ -205,7 +205,7 @@ export function AgentOutputModal({ className="max-w-4xl max-h-[80vh] flex flex-col" data-testid="agent-output-modal" > - +
@@ -238,7 +238,10 @@ export function AgentOutputModal({
- + {featureDescription}
@@ -266,7 +269,7 @@ export function AgentOutputModal({ )} -
+
{autoScrollRef.current ? "Auto-scrolling enabled" : "Scroll to bottom to enable auto-scroll"} diff --git a/app/src/components/views/analysis-view.tsx b/app/src/components/views/analysis-view.tsx new file mode 100644 index 00000000..4a4d3005 --- /dev/null +++ b/app/src/components/views/analysis-view.tsx @@ -0,0 +1,1119 @@ +"use client"; + +import { useCallback, useState } from "react"; +import { useAppStore, FileTreeNode, ProjectAnalysis } from "@/store/app-store"; +import { getElectronAPI } from "@/lib/electron"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { + Folder, + FolderOpen, + File, + ChevronRight, + ChevronDown, + Search, + RefreshCw, + BarChart3, + FileCode, + Loader2, + FileText, + CheckCircle, + AlertCircle, + ListChecks, +} from "lucide-react"; +import { cn } from "@/lib/utils"; + +const IGNORE_PATTERNS = [ + "node_modules", + ".git", + ".next", + "dist", + "build", + ".DS_Store", + "*.log", + ".cache", + "coverage", + "__pycache__", + ".pytest_cache", + ".venv", + "venv", + ".env", +]; + +const shouldIgnore = (name: string) => { + return IGNORE_PATTERNS.some((pattern) => { + if (pattern.startsWith("*")) { + return name.endsWith(pattern.slice(1)); + } + return name === pattern; + }); +}; + +const getExtension = (filename: string): string => { + const parts = filename.split("."); + return parts.length > 1 ? parts.pop() || "" : ""; +}; + +export function AnalysisView() { + const { + currentProject, + projectAnalysis, + isAnalyzing, + setProjectAnalysis, + setIsAnalyzing, + clearAnalysis, + } = useAppStore(); + + const [expandedFolders, setExpandedFolders] = useState>( + new Set() + ); + const [isGeneratingSpec, setIsGeneratingSpec] = useState(false); + const [specGenerated, setSpecGenerated] = useState(false); + const [specError, setSpecError] = useState(null); + const [isGeneratingFeatureList, setIsGeneratingFeatureList] = useState(false); + const [featureListGenerated, setFeatureListGenerated] = useState(false); + const [featureListError, setFeatureListError] = useState(null); + + // Recursively scan directory + const scanDirectory = useCallback( + async (path: string, depth: number = 0): Promise => { + if (depth > 10) return []; // Prevent infinite recursion + + const api = getElectronAPI(); + try { + const result = await api.readdir(path); + if (!result.success || !result.entries) return []; + + const nodes: FileTreeNode[] = []; + const entries = result.entries.filter((e) => !shouldIgnore(e.name)); + + for (const entry of entries) { + const fullPath = `${path}/${entry.name}`; + const node: FileTreeNode = { + name: entry.name, + path: fullPath, + isDirectory: entry.isDirectory, + extension: entry.isFile ? getExtension(entry.name) : undefined, + }; + + if (entry.isDirectory) { + // Recursively scan subdirectories + node.children = await scanDirectory(fullPath, depth + 1); + } + + nodes.push(node); + } + + // Sort: directories first, then files alphabetically + nodes.sort((a, b) => { + if (a.isDirectory && !b.isDirectory) return -1; + if (!a.isDirectory && b.isDirectory) return 1; + return a.name.localeCompare(b.name); + }); + + return nodes; + } catch (error) { + console.error("Failed to scan directory:", path, error); + return []; + } + }, + [] + ); + + // Count files and directories + const countNodes = ( + nodes: FileTreeNode[] + ): { files: number; dirs: number; byExt: Record } => { + let files = 0; + let dirs = 0; + const byExt: Record = {}; + + const traverse = (items: FileTreeNode[]) => { + for (const item of items) { + if (item.isDirectory) { + dirs++; + if (item.children) traverse(item.children); + } else { + files++; + if (item.extension) { + byExt[item.extension] = (byExt[item.extension] || 0) + 1; + } else { + byExt["(no extension)"] = (byExt["(no extension)"] || 0) + 1; + } + } + } + }; + + traverse(nodes); + return { files, dirs, byExt }; + }; + + // Run the analysis + const runAnalysis = useCallback(async () => { + if (!currentProject) return; + + setIsAnalyzing(true); + clearAnalysis(); + + try { + const fileTree = await scanDirectory(currentProject.path); + const counts = countNodes(fileTree); + + const analysis: ProjectAnalysis = { + fileTree, + totalFiles: counts.files, + totalDirectories: counts.dirs, + filesByExtension: counts.byExt, + analyzedAt: new Date().toISOString(), + }; + + setProjectAnalysis(analysis); + } catch (error) { + console.error("Analysis failed:", error); + } finally { + setIsAnalyzing(false); + } + }, [ + currentProject, + setIsAnalyzing, + clearAnalysis, + scanDirectory, + setProjectAnalysis, + ]); + + // Generate app_spec.txt from analysis + const generateSpec = useCallback(async () => { + if (!currentProject || !projectAnalysis) return; + + setIsGeneratingSpec(true); + setSpecError(null); + setSpecGenerated(false); + + try { + const api = getElectronAPI(); + + // Read key files to understand the project better + const fileContents: Record = {}; + const keyFiles = ["package.json", "README.md", "tsconfig.json"]; + + // Collect file paths from analysis + const collectFilePaths = ( + nodes: FileTreeNode[], + maxDepth: number = 3, + currentDepth: number = 0 + ): string[] => { + const paths: string[] = []; + for (const node of nodes) { + if (!node.isDirectory) { + paths.push(node.path); + } else if (node.children && currentDepth < maxDepth) { + paths.push( + ...collectFilePaths(node.children, maxDepth, currentDepth + 1) + ); + } + } + return paths; + }; + + const allFilePaths = collectFilePaths(projectAnalysis.fileTree); + + // Try to read key configuration files + for (const keyFile of keyFiles) { + const filePath = `${currentProject.path}/${keyFile}`; + const exists = await api.exists(filePath); + if (exists) { + const result = await api.readFile(filePath); + if (result.success && result.content) { + fileContents[keyFile] = result.content; + } + } + } + + // Detect project type and tech stack + const detectTechStack = () => { + const stack: string[] = []; + const extensions = projectAnalysis.filesByExtension; + + // Check package.json for dependencies + if (fileContents["package.json"]) { + try { + const pkg = JSON.parse(fileContents["package.json"]); + if (pkg.dependencies?.react || pkg.dependencies?.["react-dom"]) + stack.push("React"); + if (pkg.dependencies?.next) stack.push("Next.js"); + if (pkg.dependencies?.vue) stack.push("Vue"); + if (pkg.dependencies?.angular) stack.push("Angular"); + if (pkg.dependencies?.express) stack.push("Express"); + if (pkg.dependencies?.electron) stack.push("Electron"); + if (pkg.devDependencies?.typescript || pkg.dependencies?.typescript) + stack.push("TypeScript"); + if ( + pkg.devDependencies?.tailwindcss || + pkg.dependencies?.tailwindcss + ) + stack.push("Tailwind CSS"); + if (pkg.devDependencies?.playwright || pkg.dependencies?.playwright) + stack.push("Playwright"); + if (pkg.devDependencies?.jest || pkg.dependencies?.jest) + stack.push("Jest"); + } catch { + // Ignore JSON parse errors + } + } + + // Detect by file extensions + if (extensions["ts"] || extensions["tsx"]) stack.push("TypeScript"); + if (extensions["py"]) stack.push("Python"); + if (extensions["go"]) stack.push("Go"); + if (extensions["rs"]) stack.push("Rust"); + if (extensions["java"]) stack.push("Java"); + if (extensions["css"] || extensions["scss"] || extensions["sass"]) + stack.push("CSS/SCSS"); + + // Remove duplicates + return [...new Set(stack)]; + }; + + // Get project name from package.json or folder name + const getProjectName = () => { + if (fileContents["package.json"]) { + try { + const pkg = JSON.parse(fileContents["package.json"]); + if (pkg.name) return pkg.name; + } catch { + // Ignore JSON parse errors + } + } + // Fall back to folder name + return currentProject.name; + }; + + // Get project description from package.json or README + const getProjectDescription = () => { + if (fileContents["package.json"]) { + try { + const pkg = JSON.parse(fileContents["package.json"]); + if (pkg.description) return pkg.description; + } catch { + // Ignore JSON parse errors + } + } + if (fileContents["README.md"]) { + // Extract first paragraph from README + const lines = fileContents["README.md"].split("\n"); + for (const line of lines) { + const trimmed = line.trim(); + if ( + trimmed && + !trimmed.startsWith("#") && + !trimmed.startsWith("!") && + trimmed.length > 20 + ) { + return trimmed.substring(0, 200); + } + } + } + return "A software project"; + }; + + // Group files by directory for structure analysis + const analyzeStructure = () => { + const structure: string[] = []; + const topLevelDirs = projectAnalysis.fileTree + .filter((n) => n.isDirectory) + .map((n) => n.name); + + for (const dir of topLevelDirs) { + structure.push(` `); + } + return structure.join("\n"); + }; + + const projectName = getProjectName(); + const description = getProjectDescription(); + const techStack = detectTechStack(); + + // Generate the spec content + const specContent = ` + ${projectName} + + + ${description} + + + + +${Object.entries(projectAnalysis.filesByExtension) + .filter(([ext]) => + ["ts", "tsx", "js", "jsx", "py", "go", "rs", "java", "cpp", "c"].includes( + ext + ) + ) + .sort((a, b) => b[1] - a[1]) + .slice(0, 5) + .map(([ext, count]) => ` `) + .join("\n")} + + +${techStack.map((tech) => ` ${tech}`).join("\n")} + + + + + ${projectAnalysis.totalFiles} + ${projectAnalysis.totalDirectories} + +${analyzeStructure()} + + + + +${Object.entries(projectAnalysis.filesByExtension) + .sort((a, b) => b[1] - a[1]) + .slice(0, 10) + .map( + ([ext, count]) => + ` ` + ) + .join("\n")} + + + ${projectAnalysis.analyzedAt} + +`; + + // Write the spec file + const specPath = `${currentProject.path}/app_spec.txt`; + const writeResult = await api.writeFile(specPath, specContent); + + if (writeResult.success) { + setSpecGenerated(true); + } else { + setSpecError(writeResult.error || "Failed to write spec file"); + } + } catch (error) { + console.error("Failed to generate spec:", error); + setSpecError( + error instanceof Error ? error.message : "Failed to generate spec" + ); + } finally { + setIsGeneratingSpec(false); + } + }, [currentProject, projectAnalysis]); + + // Generate .automaker/feature_list.json from analysis + const generateFeatureList = useCallback(async () => { + if (!currentProject || !projectAnalysis) return; + + setIsGeneratingFeatureList(true); + setFeatureListError(null); + setFeatureListGenerated(false); + + try { + const api = getElectronAPI(); + + // Read key files to understand the project + const fileContents: Record = {}; + const keyFiles = ["package.json", "README.md"]; + + // Try to read key configuration files + for (const keyFile of keyFiles) { + const filePath = `${currentProject.path}/${keyFile}`; + const exists = await api.exists(filePath); + if (exists) { + const result = await api.readFile(filePath); + if (result.success && result.content) { + fileContents[keyFile] = result.content; + } + } + } + + // Collect file paths from analysis + const collectFilePaths = (nodes: FileTreeNode[]): string[] => { + const paths: string[] = []; + for (const node of nodes) { + if (!node.isDirectory) { + paths.push(node.path); + } else if (node.children) { + paths.push(...collectFilePaths(node.children)); + } + } + return paths; + }; + + const allFilePaths = collectFilePaths(projectAnalysis.fileTree); + + // Analyze directories and files to detect features + interface DetectedFeature { + category: string; + description: string; + steps: string[]; + passes: boolean; + } + + const detectedFeatures: DetectedFeature[] = []; + + // Detect features based on project structure and files + const detectFeatures = () => { + const extensions = projectAnalysis.filesByExtension; + const topLevelDirs = projectAnalysis.fileTree + .filter((n) => n.isDirectory) + .map((n) => n.name.toLowerCase()); + const topLevelFiles = projectAnalysis.fileTree + .filter((n) => !n.isDirectory) + .map((n) => n.name.toLowerCase()); + + // Check for test directories and files + const hasTests = + topLevelDirs.includes("tests") || + topLevelDirs.includes("test") || + topLevelDirs.includes("__tests__") || + allFilePaths.some( + (p) => p.includes(".spec.") || p.includes(".test.") + ); + + if (hasTests) { + detectedFeatures.push({ + category: "Testing", + description: "Automated test suite", + steps: [ + "Step 1: Tests directory exists", + "Step 2: Test files are present", + "Step 3: Run test suite", + ], + passes: true, + }); + } + + // Check for components directory (UI components) + const hasComponents = + topLevelDirs.includes("components") || + allFilePaths.some((p) => p.toLowerCase().includes("/components/")); + + if (hasComponents) { + detectedFeatures.push({ + category: "UI/Design", + description: "Component-based UI architecture", + steps: [ + "Step 1: Components directory exists", + "Step 2: UI components are defined", + "Step 3: Components are reusable", + ], + passes: true, + }); + } + + // Check for src directory (organized source code) + if (topLevelDirs.includes("src")) { + detectedFeatures.push({ + category: "Project Structure", + description: "Organized source code structure", + steps: [ + "Step 1: Source directory exists", + "Step 2: Code is properly organized", + "Step 3: Follows best practices", + ], + passes: true, + }); + } + + // Check package.json for dependencies and detect features + if (fileContents["package.json"]) { + try { + const pkg = JSON.parse(fileContents["package.json"]); + + // React/Next.js app detection + if (pkg.dependencies?.react || pkg.dependencies?.["react-dom"]) { + detectedFeatures.push({ + category: "Frontend", + description: "React-based user interface", + steps: [ + "Step 1: React is installed", + "Step 2: Components render correctly", + "Step 3: State management works", + ], + passes: true, + }); + } + + if (pkg.dependencies?.next) { + detectedFeatures.push({ + category: "Framework", + description: "Next.js framework integration", + steps: [ + "Step 1: Next.js is configured", + "Step 2: Pages/routes are defined", + "Step 3: Server-side rendering works", + ], + passes: true, + }); + } + + // TypeScript support + if ( + pkg.devDependencies?.typescript || + pkg.dependencies?.typescript || + extensions["ts"] || + extensions["tsx"] + ) { + detectedFeatures.push({ + category: "Developer Experience", + description: "TypeScript type safety", + steps: [ + "Step 1: TypeScript is configured", + "Step 2: Type definitions exist", + "Step 3: Code compiles without errors", + ], + passes: true, + }); + } + + // Tailwind CSS + if ( + pkg.devDependencies?.tailwindcss || + pkg.dependencies?.tailwindcss + ) { + detectedFeatures.push({ + category: "UI/Design", + description: "Tailwind CSS styling", + steps: [ + "Step 1: Tailwind is configured", + "Step 2: Styles are applied", + "Step 3: Responsive design works", + ], + passes: true, + }); + } + + // ESLint/Prettier (code quality) + if (pkg.devDependencies?.eslint || pkg.devDependencies?.prettier) { + detectedFeatures.push({ + category: "Developer Experience", + description: "Code quality tools", + steps: [ + "Step 1: Linter is configured", + "Step 2: Code passes lint checks", + "Step 3: Formatting is consistent", + ], + passes: true, + }); + } + + // Electron (desktop app) + if (pkg.dependencies?.electron || pkg.devDependencies?.electron) { + detectedFeatures.push({ + category: "Platform", + description: "Electron desktop application", + steps: [ + "Step 1: Electron is configured", + "Step 2: Main process runs", + "Step 3: Renderer process loads", + ], + passes: true, + }); + } + + // Playwright testing + if ( + pkg.devDependencies?.playwright || + pkg.devDependencies?.["@playwright/test"] + ) { + detectedFeatures.push({ + category: "Testing", + description: "Playwright end-to-end testing", + steps: [ + "Step 1: Playwright is configured", + "Step 2: E2E tests are defined", + "Step 3: Tests pass successfully", + ], + passes: true, + }); + } + } catch { + // Ignore JSON parse errors + } + } + + // Check for documentation + if ( + topLevelFiles.includes("readme.md") || + topLevelDirs.includes("docs") + ) { + detectedFeatures.push({ + category: "Documentation", + description: "Project documentation", + steps: [ + "Step 1: README exists", + "Step 2: Documentation is comprehensive", + "Step 3: Setup instructions are clear", + ], + passes: true, + }); + } + + // Check for CI/CD configuration + const hasCICD = + topLevelDirs.includes(".github") || + topLevelFiles.includes(".gitlab-ci.yml") || + topLevelFiles.includes(".travis.yml"); + + if (hasCICD) { + detectedFeatures.push({ + category: "DevOps", + description: "CI/CD pipeline configuration", + steps: [ + "Step 1: CI config exists", + "Step 2: Pipeline runs on push", + "Step 3: Automated checks pass", + ], + passes: true, + }); + } + + // Check for API routes (Next.js API or Express) + const hasAPIRoutes = allFilePaths.some( + (p) => + p.includes("/api/") || + p.includes("/routes/") || + p.includes("/endpoints/") + ); + + if (hasAPIRoutes) { + detectedFeatures.push({ + category: "Backend", + description: "API endpoints", + steps: [ + "Step 1: API routes are defined", + "Step 2: Endpoints respond correctly", + "Step 3: Error handling is implemented", + ], + passes: true, + }); + } + + // Check for state management + const hasStateManagement = allFilePaths.some( + (p) => + p.includes("/store/") || + p.includes("/stores/") || + p.includes("/redux/") || + p.includes("/context/") + ); + + if (hasStateManagement) { + detectedFeatures.push({ + category: "Architecture", + description: "State management system", + steps: [ + "Step 1: Store is configured", + "Step 2: State updates correctly", + "Step 3: Components access state", + ], + passes: true, + }); + } + + // Check for configuration files + if ( + topLevelFiles.includes("tsconfig.json") || + topLevelFiles.includes("package.json") + ) { + detectedFeatures.push({ + category: "Configuration", + description: "Project configuration files", + steps: [ + "Step 1: Config files exist", + "Step 2: Configuration is valid", + "Step 3: Build process works", + ], + passes: true, + }); + } + }; + + detectFeatures(); + + // If no features were detected, add a default feature + if (detectedFeatures.length === 0) { + detectedFeatures.push({ + category: "Core", + description: "Basic project structure", + steps: [ + "Step 1: Project directory exists", + "Step 2: Files are present", + "Step 3: Project can be loaded", + ], + passes: true, + }); + } + + // Generate the feature list content + const featureListContent = JSON.stringify(detectedFeatures, null, 2); + + // Write the feature list file + const featureListPath = `${currentProject.path}/feature_list.json`; + const writeResult = await api.writeFile( + featureListPath, + featureListContent + ); + + if (writeResult.success) { + setFeatureListGenerated(true); + } else { + setFeatureListError( + writeResult.error || "Failed to write feature list file" + ); + } + } catch (error) { + console.error("Failed to generate feature list:", error); + setFeatureListError( + error instanceof Error + ? error.message + : "Failed to generate feature list" + ); + } finally { + setIsGeneratingFeatureList(false); + } + }, [currentProject, projectAnalysis]); + + // Toggle folder expansion + const toggleFolder = (path: string) => { + const newExpanded = new Set(expandedFolders); + if (expandedFolders.has(path)) { + newExpanded.delete(path); + } else { + newExpanded.add(path); + } + setExpandedFolders(newExpanded); + }; + + // Render file tree node + const renderNode = (node: FileTreeNode, depth: number = 0) => { + const isExpanded = expandedFolders.has(node.path); + + return ( +
+
{ + if (node.isDirectory) { + toggleFolder(node.path); + } + }} + > + {node.isDirectory ? ( + <> + {isExpanded ? ( + + ) : ( + + )} + {isExpanded ? ( + + ) : ( + + )} + + ) : ( + <> + + + + )} + {node.name} + {node.extension && ( + + .{node.extension} + + )} +
+ {node.isDirectory && isExpanded && node.children && ( +
+ {node.children.map((child) => renderNode(child, depth + 1))} +
+ )} +
+ ); + }; + + if (!currentProject) { + return ( +
+

No project selected

+
+ ); + } + + return ( +
+ {/* Header */} +
+
+ +
+

Project Analysis

+

+ {currentProject.name} +

+
+
+ +
+ + {/* Content */} +
+ {!projectAnalysis && !isAnalyzing ? ( +
+ +

No Analysis Yet

+

+ Click "Analyze Project" to scan your codebase and get + insights about its structure. +

+ +
+ ) : isAnalyzing ? ( +
+ +

Scanning project files...

+
+ ) : projectAnalysis ? ( +
+ {/* Stats Panel */} +
+ + + + + Statistics + + + Analyzed{" "} + {new Date(projectAnalysis.analyzedAt).toLocaleString()} + + + +
+ + Total Files + + + {projectAnalysis.totalFiles} + +
+
+ + Total Directories + + + {projectAnalysis.totalDirectories} + +
+
+
+ + + + + + Files by Extension + + + +
+ {Object.entries(projectAnalysis.filesByExtension) + .sort((a, b) => b[1] - a[1]) + .slice(0, 15) + .map(([ext, count]) => ( +
+ + {ext.startsWith("(") ? ext : `.${ext}`} + + {count} +
+ ))} +
+
+
+ + {/* Generate Spec Card */} + + + + + Generate Specification + + + Create app_spec.txt from analysis + + + +

+ Generate a project specification file based on the analyzed + codebase structure and detected technologies. +

+ + {specGenerated && ( +
+ + app_spec.txt created successfully! +
+ )} + {specError && ( +
+ + {specError} +
+ )} +
+
+ + {/* Generate Feature List Card */} + + + + + Generate Feature List + + + Create .automaker/feature_list.json from analysis + + + +

+ Automatically detect and generate a feature list based on + the analyzed codebase structure, dependencies, and project + configuration. +

+ + {featureListGenerated && ( +
+ + feature_list.json created successfully! +
+ )} + {featureListError && ( +
+ + {featureListError} +
+ )} +
+
+
+ + {/* File Tree */} + + + + + File Tree + + + {projectAnalysis.totalFiles} files in{" "} + {projectAnalysis.totalDirectories} directories + + + +
+ {projectAnalysis.fileTree.map((node) => renderNode(node))} +
+
+
+
+ ) : null} +
+
+ ); +} diff --git a/app/src/components/views/board-view.tsx b/app/src/components/views/board-view.tsx index 9c9b09bf..e0d01fa0 100644 --- a/app/src/components/views/board-view.tsx +++ b/app/src/components/views/board-view.tsx @@ -65,6 +65,8 @@ import { FastForward, FlaskConical, CheckCircle2, + MessageSquare, + GitCommit, } from "lucide-react"; import { toast } from "sonner"; import { Slider } from "@/components/ui/slider"; @@ -81,6 +83,7 @@ type ColumnId = Feature["status"]; const COLUMNS: { id: ColumnId; title: string; color: string }[] = [ { id: "backlog", title: "Backlog", color: "bg-zinc-500" }, { id: "in_progress", title: "In Progress", color: "bg-yellow-500" }, + { id: "waiting_approval", title: "Waiting Approval", color: "bg-orange-500" }, { id: "verified", title: "Verified", color: "bg-green-500" }, ]; @@ -119,6 +122,12 @@ export function BoardView() { const [showDeleteAllVerifiedDialog, setShowDeleteAllVerifiedDialog] = useState(false); const [persistedCategories, setPersistedCategories] = useState([]); + const [showFollowUpDialog, setShowFollowUpDialog] = useState(false); + const [followUpFeature, setFollowUpFeature] = useState(null); + const [followUpPrompt, setFollowUpPrompt] = useState(""); + const [followUpImagePaths, setFollowUpImagePaths] = useState< + DescriptionImagePath[] + >([]); // Make current project available globally for modal useEffect(() => { @@ -428,6 +437,7 @@ export function BoardView() { startedAt: f.startedAt, imagePaths: f.imagePaths, skipTests: f.skipTests, + summary: f.summary, })); await api.writeFile( `${currentProject.path}/.automaker/feature_list.json`, @@ -776,6 +786,142 @@ export function BoardView() { }); }; + // Open follow-up dialog for waiting_approval features + const handleOpenFollowUp = (feature: Feature) => { + console.log("[Board] Opening follow-up dialog for feature:", { + id: feature.id, + description: feature.description, + }); + setFollowUpFeature(feature); + setFollowUpPrompt(""); + setFollowUpImagePaths([]); + setShowFollowUpDialog(true); + }; + + // Handle sending follow-up prompt + const handleSendFollowUp = async () => { + if (!currentProject || !followUpFeature || !followUpPrompt.trim()) return; + + // Save values before clearing state + const featureId = followUpFeature.id; + const featureDescription = followUpFeature.description; + const prompt = followUpPrompt; + const imagePaths = followUpImagePaths.map((img) => img.path); + + console.log("[Board] Sending follow-up prompt for feature:", { + id: featureId, + prompt: prompt, + imagePaths: imagePaths, + }); + + const api = getElectronAPI(); + if (!api?.autoMode?.followUpFeature) { + console.error("Follow-up feature API not available"); + toast.error("Follow-up not available", { + description: "This feature is not available in the current version.", + }); + return; + } + + // Move feature back to in_progress before sending follow-up + updateFeature(featureId, { + status: "in_progress", + startedAt: new Date().toISOString(), + }); + + // Reset follow-up state immediately (close dialog, clear form) + setShowFollowUpDialog(false); + setFollowUpFeature(null); + setFollowUpPrompt(""); + setFollowUpImagePaths([]); + + // Show success toast immediately + toast.success("Follow-up started", { + description: `Continuing work on: ${featureDescription.slice(0, 50)}${ + featureDescription.length > 50 ? "..." : "" + }`, + }); + + // Call the API in the background (don't await - let it run async) + api.autoMode + .followUpFeature(currentProject.path, featureId, prompt, imagePaths) + .catch((error) => { + console.error("[Board] Error sending follow-up:", error); + toast.error("Failed to send follow-up", { + description: + error instanceof Error ? error.message : "An error occurred", + }); + // Reload features to revert status if there was an error + loadFeatures(); + }); + }; + + // Handle commit-only for waiting_approval features (marks as verified and commits) + const handleCommitFeature = async (feature: Feature) => { + if (!currentProject) return; + + console.log("[Board] Committing feature:", { + id: feature.id, + description: feature.description, + }); + + try { + const api = getElectronAPI(); + if (!api?.autoMode?.commitFeature) { + console.error("Commit feature API not available"); + toast.error("Commit not available", { + description: "This feature is not available in the current version.", + }); + return; + } + + // Call the API to commit this feature + const result = await api.autoMode.commitFeature( + currentProject.path, + feature.id + ); + + if (result.success) { + console.log("[Board] Feature committed successfully"); + // Move to verified status + moveFeature(feature.id, "verified"); + toast.success("Feature committed", { + description: `Committed and verified: ${feature.description.slice( + 0, + 50 + )}${feature.description.length > 50 ? "..." : ""}`, + }); + } else { + console.error("[Board] Failed to commit feature:", result.error); + toast.error("Failed to commit feature", { + description: result.error || "An error occurred", + }); + await loadFeatures(); + } + } catch (error) { + console.error("[Board] Error committing feature:", error); + toast.error("Failed to commit feature", { + description: + error instanceof Error ? error.message : "An error occurred", + }); + await loadFeatures(); + } + }; + + // Move feature to waiting_approval (for skipTests features when agent completes) + const handleMoveToWaitingApproval = (feature: Feature) => { + console.log("[Board] Moving feature to waiting_approval:", { + id: feature.id, + description: feature.description, + }); + updateFeature(feature.id, { status: "waiting_approval" }); + toast.info("Feature ready for review", { + description: `Ready for approval: ${feature.description.slice(0, 50)}${ + feature.description.length > 50 ? "..." : "" + }`, + }); + }; + const checkContextExists = async (featureId: string): Promise => { if (!currentProject) return false; @@ -844,12 +990,30 @@ export function BoardView() { const handleForceStopFeature = async (feature: Feature) => { try { await autoMode.stopFeature(feature.id); - // Move the feature back to backlog status after stopping - moveFeature(feature.id, "backlog"); + + // Determine where to move the feature after stopping: + // - If it's a skipTests feature that was in waiting_approval (i.e., during commit operation), + // move it back to waiting_approval so user can try commit again or do follow-up + // - Otherwise, move to backlog + const targetStatus = + feature.skipTests && feature.status === "waiting_approval" + ? "waiting_approval" + : "backlog"; + + if (targetStatus !== feature.status) { + moveFeature(feature.id, targetStatus); + } + toast.success("Agent stopped", { - description: `Stopped working on: ${feature.description.slice(0, 50)}${ - feature.description.length > 50 ? "..." : "" - }`, + description: + targetStatus === "waiting_approval" + ? `Stopped commit - returned to waiting approval: ${feature.description.slice( + 0, + 50 + )}${feature.description.length > 50 ? "..." : ""}` + : `Stopped working on: ${feature.description.slice(0, 50)}${ + feature.description.length > 50 ? "..." : "" + }`, }); } catch (error) { console.error("[Board] Error stopping feature:", error); @@ -1114,6 +1278,8 @@ export function BoardView() { onMoveBackToInProgress={() => handleMoveBackToInProgress(feature) } + onFollowUp={() => handleOpenFollowUp(feature)} + onCommit={() => handleCommitFeature(feature)} hasContext={featuresWithContext.has(feature.id)} isCurrentAutoTask={runningAutoTasks.includes( feature.id @@ -1457,6 +1623,86 @@ export function BoardView() { + + {/* Follow-Up Prompt Dialog */} + { + if (!open) { + setShowFollowUpDialog(false); + setFollowUpFeature(null); + setFollowUpPrompt(""); + setFollowUpImagePaths([]); + } + }} + > + { + if ( + (e.metaKey || e.ctrlKey) && + e.key === "Enter" && + followUpPrompt.trim() + ) { + e.preventDefault(); + handleSendFollowUp(); + } + }} + > + + Follow-Up Prompt + + Send additional instructions to continue working on this feature. + {followUpFeature && ( + + Feature: {followUpFeature.description.slice(0, 100)} + {followUpFeature.description.length > 100 ? "..." : ""} + + )} + + +
+
+ + +
+

+ The agent will continue from where it left off, using the existing + context. You can attach screenshots to help explain the issue. +

+
+ + + + +
+
); } diff --git a/app/src/components/views/code-view.tsx b/app/src/components/views/code-view.tsx new file mode 100644 index 00000000..fcbb74b8 --- /dev/null +++ b/app/src/components/views/code-view.tsx @@ -0,0 +1,301 @@ +"use client"; + +import { useEffect, useState, useCallback } from "react"; +import { useAppStore } from "@/store/app-store"; +import { getElectronAPI } from "@/lib/electron"; +import { Card, CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { + File, + Folder, + FolderOpen, + ChevronRight, + ChevronDown, + RefreshCw, + Code, +} from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface FileTreeNode { + name: string; + path: string; + isDirectory: boolean; + children?: FileTreeNode[]; + isExpanded?: boolean; +} + +const IGNORE_PATTERNS = [ + "node_modules", + ".git", + ".next", + "dist", + "build", + ".DS_Store", + "*.log", +]; + +const shouldIgnore = (name: string) => { + return IGNORE_PATTERNS.some((pattern) => { + if (pattern.startsWith("*")) { + return name.endsWith(pattern.slice(1)); + } + return name === pattern; + }); +}; + +export function CodeView() { + const { currentProject } = useAppStore(); + const [fileTree, setFileTree] = useState([]); + const [selectedFile, setSelectedFile] = useState(null); + const [fileContent, setFileContent] = useState(""); + const [isLoading, setIsLoading] = useState(true); + const [expandedFolders, setExpandedFolders] = useState>( + new Set() + ); + + // Load directory tree + const loadTree = useCallback(async () => { + if (!currentProject) return; + + setIsLoading(true); + try { + const api = getElectronAPI(); + const result = await api.readdir(currentProject.path); + + if (result.success && result.entries) { + const entries = result.entries + .filter((e) => !shouldIgnore(e.name)) + .sort((a, b) => { + // Directories first + if (a.isDirectory && !b.isDirectory) return -1; + if (!a.isDirectory && b.isDirectory) return 1; + return a.name.localeCompare(b.name); + }) + .map((e) => ({ + name: e.name, + path: `${currentProject.path}/${e.name}`, + isDirectory: e.isDirectory, + })); + + setFileTree(entries); + } + } catch (error) { + console.error("Failed to load file tree:", error); + } finally { + setIsLoading(false); + } + }, [currentProject]); + + useEffect(() => { + loadTree(); + }, [loadTree]); + + // Load subdirectory + const loadSubdirectory = async (path: string): Promise => { + try { + const api = getElectronAPI(); + const result = await api.readdir(path); + + if (result.success && result.entries) { + return result.entries + .filter((e) => !shouldIgnore(e.name)) + .sort((a, b) => { + if (a.isDirectory && !b.isDirectory) return -1; + if (!a.isDirectory && b.isDirectory) return 1; + return a.name.localeCompare(b.name); + }) + .map((e) => ({ + name: e.name, + path: `${path}/${e.name}`, + isDirectory: e.isDirectory, + })); + } + } catch (error) { + console.error("Failed to load subdirectory:", error); + } + return []; + }; + + // Load file content + const loadFileContent = async (path: string) => { + try { + const api = getElectronAPI(); + const result = await api.readFile(path); + + if (result.success && result.content) { + setFileContent(result.content); + setSelectedFile(path); + } + } catch (error) { + console.error("Failed to load file:", error); + } + }; + + // Toggle folder expansion + const toggleFolder = async (node: FileTreeNode) => { + const newExpanded = new Set(expandedFolders); + + if (expandedFolders.has(node.path)) { + newExpanded.delete(node.path); + } else { + newExpanded.add(node.path); + + // Load children if not already loaded + if (!node.children) { + const children = await loadSubdirectory(node.path); + // Update the tree with children + const updateTree = (nodes: FileTreeNode[]): FileTreeNode[] => { + return nodes.map((n) => { + if (n.path === node.path) { + return { ...n, children }; + } + if (n.children) { + return { ...n, children: updateTree(n.children) }; + } + return n; + }); + }; + setFileTree(updateTree(fileTree)); + } + } + + setExpandedFolders(newExpanded); + }; + + // Render file tree node + const renderNode = (node: FileTreeNode, depth: number = 0) => { + const isExpanded = expandedFolders.has(node.path); + const isSelected = selectedFile === node.path; + + return ( +
+
{ + if (node.isDirectory) { + toggleFolder(node); + } else { + loadFileContent(node.path); + } + }} + data-testid={`file-tree-item-${node.name}`} + > + {node.isDirectory ? ( + <> + {isExpanded ? ( + + ) : ( + + )} + {isExpanded ? ( + + ) : ( + + )} + + ) : ( + <> + + + + )} + {node.name} +
+ {node.isDirectory && isExpanded && node.children && ( +
+ {node.children.map((child) => renderNode(child, depth + 1))} +
+ )} +
+ ); + }; + + if (!currentProject) { + return ( +
+

No project selected

+
+ ); + } + + if (isLoading) { + return ( +
+ +
+ ); + } + + return ( +
+ {/* Header */} +
+
+ +
+

Code Explorer

+

+ {currentProject.name} +

+
+
+ +
+ + {/* Split View */} +
+ {/* File Tree */} +
+
{fileTree.map((node) => renderNode(node))}
+
+ + {/* Code Preview */} +
+ {selectedFile ? ( +
+
+

+ {selectedFile.replace(currentProject.path, "")} +

+
+ + +
+                    {fileContent}
+                  
+
+
+
+ ) : ( +
+

+ Select a file to view its contents +

+
+ )} +
+
+
+ ); +} diff --git a/app/src/components/views/kanban-card.tsx b/app/src/components/views/kanban-card.tsx index 3ae7219a..b4fe1c74 100644 --- a/app/src/components/views/kanban-card.tsx +++ b/app/src/components/views/kanban-card.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { cn } from "@/lib/utils"; @@ -20,7 +20,7 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { Feature } from "@/store/app-store"; +import { Feature, useAppStore } from "@/store/app-store"; import { GripVertical, Edit, @@ -34,8 +34,23 @@ import { StopCircle, FlaskConical, ArrowLeft, + MessageSquare, + GitCommit, + Cpu, + Wrench, + ListTodo, + Sparkles, + Expand, } from "lucide-react"; import { CountUpTimer } from "@/components/ui/count-up-timer"; +import { getElectronAPI } from "@/lib/electron"; +import { + parseAgentContext, + AgentTaskInfo, + formatModelName, + DEFAULT_MODEL, +} from "@/lib/agent-context-parser"; +import { Markdown } from "@/components/ui/markdown"; interface KanbanCardProps { feature: Feature; @@ -47,9 +62,15 @@ interface KanbanCardProps { onForceStop?: () => void; onManualVerify?: () => void; onMoveBackToInProgress?: () => void; + onFollowUp?: () => void; + onCommit?: () => void; hasContext?: boolean; isCurrentAutoTask?: boolean; shortcutKey?: string; + /** Context content for extracting progress info */ + contextContent?: string; + /** Feature summary from agent completion */ + summary?: string; } export function KanbanCard({ @@ -62,11 +83,71 @@ export function KanbanCard({ onForceStop, onManualVerify, onMoveBackToInProgress, + onFollowUp, + onCommit, hasContext, isCurrentAutoTask, shortcutKey, + contextContent, + summary, }: KanbanCardProps) { const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [isSummaryDialogOpen, setIsSummaryDialogOpen] = useState(false); + const [agentInfo, setAgentInfo] = useState(null); + const { kanbanCardDetailLevel } = useAppStore(); + + // Helper functions to check what should be shown based on detail level + const showSteps = + kanbanCardDetailLevel === "standard" || + kanbanCardDetailLevel === "detailed"; + const showAgentInfo = kanbanCardDetailLevel === "detailed"; + const showProgressBar = + kanbanCardDetailLevel === "standard" || + kanbanCardDetailLevel === "detailed"; + + // Load context file for in_progress, waiting_approval, and verified features + useEffect(() => { + const loadContext = async () => { + // Use provided context or load from file + if (contextContent) { + const info = parseAgentContext(contextContent); + setAgentInfo(info); + return; + } + + // Only load for non-backlog features + if (feature.status === "backlog") { + setAgentInfo(null); + return; + } + + try { + const api = getElectronAPI(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const currentProject = (window as any).__currentProject; + if (!currentProject?.path) return; + + const contextPath = `${currentProject.path}/.automaker/agents-context/${feature.id}.md`; + const result = await api.readFile(contextPath); + + if (result.success && result.content) { + const info = parseAgentContext(result.content); + setAgentInfo(info); + } + } catch { + // Context file might not exist + console.debug("[KanbanCard] No context file for feature:", feature.id); + } + }; + + loadContext(); + + // Reload context periodically while feature is running + if (isCurrentAutoTask) { + const interval = setInterval(loadContext, 3000); + return () => clearInterval(interval); + } + }, [feature.id, feature.status, contextContent, isCurrentAutoTask]); const handleDeleteClick = (e: React.MouseEvent) => { e.stopPropagation(); @@ -189,8 +270,8 @@ export function KanbanCard({
- {/* Steps Preview */} - {feature.steps.length > 0 && ( + {/* Steps Preview - Show in Standard and Detailed modes */} + {showSteps && feature.steps.length > 0 && (
{feature.steps.slice(0, 3).map((step, index) => (
)} + {/* Agent Info Panel - shows for in_progress, waiting_approval, verified */} + {/* Standard mode: Only show progress bar */} + {showProgressBar && + !showAgentInfo && + feature.status !== "backlog" && + agentInfo && + (isCurrentAutoTask || feature.status === "in_progress") && ( +
+
+
+
+
+ {Math.round(agentInfo.progressPercentage)}% +
+
+ )} + + {/* Detailed mode: Show all agent info */} + {showAgentInfo && feature.status !== "backlog" && agentInfo && ( +
+ {/* Model & Phase */} +
+
+ + + {formatModelName(DEFAULT_MODEL)} + +
+ {agentInfo.currentPhase && ( +
+ {agentInfo.currentPhase} +
+ )} +
+ + {/* Progress Indicator */} + {(isCurrentAutoTask || feature.status === "in_progress") && ( +
+
+
+
+
+
+ + + {agentInfo.toolCallCount} tools + + {agentInfo.lastToolUsed && ( + + {agentInfo.lastToolUsed} + + )} +
+ {Math.round(agentInfo.progressPercentage)}% +
+
+ )} + + {/* Task List Progress (if todos found) */} + {agentInfo.todos.length > 0 && ( +
+
+ + + { + agentInfo.todos.filter((t) => t.status === "completed") + .length + } + /{agentInfo.todos.length} tasks + +
+
+ {agentInfo.todos.slice(0, 3).map((todo, idx) => ( +
+ {todo.status === "completed" ? ( + + ) : todo.status === "in_progress" ? ( + + ) : ( + + )} + + {todo.content} + +
+ ))} + {agentInfo.todos.length > 3 && ( +

+ +{agentInfo.todos.length - 3} more +

+ )} +
+
+ )} + + {/* Summary for waiting_approval and verified - prioritize feature.summary from UpdateFeatureStatus */} + {(feature.status === "waiting_approval" || + feature.status === "verified") && ( + <> + {(feature.summary || summary || agentInfo.summary) && ( +
+
+
+ + Summary +
+ +
+

+ {feature.summary || summary || agentInfo.summary} +

+
+ )} + {/* Show tool count even without summary */} + {!feature.summary && + !summary && + !agentInfo.summary && + agentInfo.toolCallCount > 0 && ( +
+ + + {agentInfo.toolCallCount} tool calls + + {agentInfo.todos.length > 0 && ( + + + { + agentInfo.todos.filter( + (t) => t.status === "completed" + ).length + }{" "} + tasks done + + )} +
+ )} + + )} +
+ )} + {/* Actions */}
{isCurrentAutoTask && ( @@ -363,6 +633,51 @@ export function KanbanCard({ )} + {!isCurrentAutoTask && feature.status === "waiting_approval" && ( + <> + {/* Follow-up prompt button */} + {onFollowUp && ( + + )} + {/* Commit and verify button */} + {onCommit && ( + + )} + + + )} {!isCurrentAutoTask && feature.status === "backlog" && ( <> + + + ); } diff --git a/app/src/components/views/settings-view.tsx b/app/src/components/views/settings-view.tsx index 968096ab..6b9cc124 100644 --- a/app/src/components/views/settings-view.tsx +++ b/app/src/components/views/settings-view.tsx @@ -5,13 +5,6 @@ import { useAppStore } from "@/store/app-store"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; import { Settings, Key, @@ -34,11 +27,22 @@ import { Cat, Atom, Radio, + LayoutGrid, + Minimize2, + Square, + Maximize2, } from "lucide-react"; export function SettingsView() { - const { apiKeys, setApiKeys, setCurrentView, theme, setTheme } = - useAppStore(); + const { + apiKeys, + setApiKeys, + setCurrentView, + theme, + setTheme, + kanbanCardDetailLevel, + setKanbanCardDetailLevel, + } = useAppStore(); const [anthropicKey, setAnthropicKey] = useState(apiKeys.anthropic); const [googleKey, setGoogleKey] = useState(apiKeys.google); const [showAnthropicKey, setShowAnthropicKey] = useState(false); @@ -548,6 +552,81 @@ export function SettingsView() {
+ {/* Kanban Card Display Section */} +
+
+
+ +

+ Kanban Card Display +

+
+

+ Control how much information is displayed on Kanban cards. +

+
+
+
+ +
+ + + +
+

+ Minimal: Shows only title and category +
+ Standard: Adds steps preview and progress bar +
+ Detailed: Shows all info including model, + tool calls, task list, and summaries +

+
+
+
+ {/* Save Button */}