From bfc0934ce992b3e619978f7241181e6e9e30f4b2 Mon Sep 17 00:00:00 2001 From: Kacper Date: Tue, 9 Dec 2025 22:25:20 +0100 Subject: [PATCH 1/7] feat: implement follow-up and commit features for waiting_approval status - Added functionality to allow users to send follow-up prompts for features in the waiting_approval status, enabling continued work with additional instructions. - Implemented a commit feature that allows users to mark waiting_approval features as verified and commit changes directly. - Updated the UI to include buttons for follow-up and commit actions on Kanban cards and integrated dialogs for user interaction. - Enhanced the feature loader and executor to handle the new status and actions appropriately. This update improves the workflow for managing features that require manual review and enhances user experience in the auto mode. --- app/electron/auto-mode-service.js | 203 +++++++++++++++- app/electron/main.js | 51 +++++ app/electron/preload.js | 12 + app/electron/services/feature-executor.js | 144 +++++++++++- app/electron/services/feature-loader.js | 35 ++- app/electron/services/feature-verifier.js | 6 +- app/electron/services/mcp-server-factory.js | 28 ++- app/electron/services/prompt-builder.js | 140 ++++++++---- .../components/views/agent-output-modal.tsx | 9 +- app/src/components/views/board-view.tsx | 199 +++++++++++++++- app/src/components/views/kanban-card.tsx | 51 +++++ app/src/lib/electron.ts | 52 +++++ app/src/store/app-store.ts | 2 +- app/src/types/electron.d.ts | 11 + app/tests/utils.ts | 216 ++++++++++++++++++ 15 files changed, 1079 insertions(+), 80 deletions(-) diff --git a/app/electron/auto-mode-service.js b/app/electron/auto-mode-service.js index 601683b2..f010e7a1 100644 --- a/app/electron/auto-mode-service.js +++ b/app/electron/auto-mode-service.js @@ -138,10 +138,16 @@ class AutoModeService { const result = await featureExecutor.implementFeature(feature, projectPath, sendToRenderer, execution); // Update feature status based on result - const newStatus = result.passes ? "verified" : "backlog"; + // 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); } @@ -321,10 +327,16 @@ class AutoModeService { } // Update feature status based on final result - const newStatus = finalResult.passes ? "verified" : "in_progress"; + // 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); } @@ -400,10 +412,16 @@ class AutoModeService { const result = await featureExecutor.implementFeature(nextFeature, projectPath, sendToRenderer, execution); // Update feature status based on result - const newStatus = result.passes ? "verified" : "backlog"; + // 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); } @@ -500,6 +518,179 @@ 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); + + 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, + }); + + return { success: true, passes: result.passes }; + } catch (error) { + console.error("[AutoMode] Error in follow-up:", error); + sendToRenderer({ + type: "auto_mode_error", + error: error.message, + featureId: featureId, + }); + throw error; + } 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..f3d99a57 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,138 @@ class FeatureExecutor { throw error; } } + + /** + * Commit changes for a feature without doing additional work + * Just runs git add and git commit with the feature description + */ + 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: "Committing changes to git...", + }); + + 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 simple commit task + systemPrompt: `You are a git assistant. Your only task is to commit the current changes with a proper commit message. + +IMPORTANT RULES: +- DO NOT modify any code +- DO NOT write tests +- DO NOT do anything except committing the existing changes +- Use the git command line tools via Bash`, + maxTurns: 10, // Short limit for simple task + cwd: projectPath, + mcpServers: { + "automaker-tools": featureToolsServer + }, + allowedTools: ["Bash", "mcp__automaker-tools__UpdateFeatureStatus"], + permissionMode: "acceptEdits", + sandbox: { + enabled: false, // Need to run git commands + }, + abortController: abortController, + }; + + // Simple commit prompt + const prompt = `Please commit the current changes with this commit message: + +"${feature.category}: ${feature.description}" + +Steps: +1. Run \`git add .\` to stage all changes +2. Run \`git commit -m "message"\` with the provided message +3. Report success + +Do NOT modify any code or run tests. Just 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..5ad27319 100644 --- a/app/electron/services/feature-loader.js +++ b/app/electron/services/feature-loader.js @@ -51,13 +51,29 @@ class FeatureLoader { ".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; + } + return featureData; + }); await fs.writeFile(featuresPath, JSON.stringify(toSave, null, 2), "utf-8"); console.log(`[FeatureLoader] Updated feature ${featureId}: status=${status}`); @@ -65,11 +81,12 @@ class FeatureLoader { /** * 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..65102729 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,41 @@ 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.", { 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.") }, async (args) => { try { console.log(`[McpServerFactory] UpdateFeatureStatus tool called: featureId=${args.featureId}, status=${args.status}`); + // 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 - await updateFeatureStatusCallback(args.featureId, args.status, projectPath); + await updateFeatureStatusCallback(args.featureId, finalStatus, projectPath); + + const statusMessage = finalStatus !== args.status + ? `Successfully updated feature ${args.featureId} to status "${finalStatus}" (converted from "${args.status}" because skipTests=true)` + : `Successfully updated feature ${args.featureId} to status "${finalStatus}"`; 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..8f6389bd 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,34 @@ ${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" - **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 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 +${feature.skipTests + ? "- **DO NOT commit changes** - user will review and commit manually" + : "- Make a git commit when complete"} **Testing Utilities (CRITICAL):** @@ -75,6 +83,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 +95,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 +103,9 @@ ${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 +115,19 @@ ${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" - **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 **Testing Utilities:** - Check if tests/utils.ts exists and is being used @@ -126,13 +142,10 @@ 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 Begin by reading the project structure and understanding what needs to be implemented or fixed.`; } @@ -141,6 +154,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 +165,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 +179,34 @@ 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" - **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 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 +${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 +300,27 @@ 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 +- 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: +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 and status="verified" - **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 **Testing Utilities (CRITICAL):** - **Create and maintain tests/utils.ts** with helper functions for finding elements and common operations @@ -327,21 +358,30 @@ 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 +- **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: +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 and status="verified" - **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 **Testing Utilities:** - Check if tests/utils.ts needs updates based on code changes 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/board-view.tsx b/app/src/components/views/board-view.tsx index 46594c79..705030cf 100644 --- a/app/src/components/views/board-view.tsx +++ b/app/src/components/views/board-view.tsx @@ -44,7 +44,7 @@ import { KanbanColumn } from "./kanban-column"; import { KanbanCard } from "./kanban-card"; import { AutoModeLog } from "./auto-mode-log"; import { AgentOutputModal } from "./agent-output-modal"; -import { Plus, RefreshCw, Play, StopCircle, Loader2, ChevronUp, ChevronDown, Users, Trash2, FastForward, FlaskConical, CheckCircle2 } from "lucide-react"; +import { Plus, RefreshCw, Play, StopCircle, Loader2, ChevronUp, ChevronDown, Users, Trash2, FastForward, FlaskConical, CheckCircle2, MessageSquare, GitCommit } from "lucide-react"; import { toast } from "sonner"; import { Slider } from "@/components/ui/slider"; import { Checkbox } from "@/components/ui/checkbox"; @@ -60,6 +60,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" }, ]; @@ -95,6 +96,10 @@ export function BoardView() { const [featuresWithContext, setFeaturesWithContext] = useState>(new Set()); 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([]); // Make current project available globally for modal useEffect(() => { @@ -688,6 +693,125 @@ 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; + + console.log("[Board] Sending follow-up prompt for feature:", { + id: followUpFeature.id, + prompt: followUpPrompt, + imagePaths: followUpImagePaths + }); + + try { + 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(followUpFeature.id, { status: "in_progress", startedAt: new Date().toISOString() }); + + // Call the API to send follow-up prompt + const result = await api.autoMode.followUpFeature( + currentProject.path, + followUpFeature.id, + followUpPrompt, + followUpImagePaths.map(img => img.path) + ); + + if (result.success) { + console.log("[Board] Follow-up started successfully"); + toast.success("Follow-up started", { + description: `Continuing work on: ${followUpFeature.description.slice(0, 50)}${followUpFeature.description.length > 50 ? "..." : ""}`, + }); + setShowFollowUpDialog(false); + setFollowUpFeature(null); + setFollowUpPrompt(""); + setFollowUpImagePaths([]); + } else { + console.error("[Board] Failed to send follow-up:", result.error); + toast.error("Failed to send follow-up", { + description: result.error || "An error occurred", + }); + await loadFeatures(); + } + } 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", + }); + await 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; @@ -1000,6 +1124,8 @@ export function BoardView() { onForceStop={() => handleForceStopFeature(feature)} onManualVerify={() => handleManualVerify(feature)} onMoveBackToInProgress={() => handleMoveBackToInProgress(feature)} + onFollowUp={() => handleOpenFollowUp(feature)} + onCommit={() => handleCommitFeature(feature)} hasContext={featuresWithContext.has(feature.id)} isCurrentAutoTask={runningAutoTasks.includes(feature.id)} shortcutKey={shortcutKey} @@ -1318,6 +1444,77 @@ 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/kanban-card.tsx b/app/src/components/views/kanban-card.tsx index da3c16ff..d6943206 100644 --- a/app/src/components/views/kanban-card.tsx +++ b/app/src/components/views/kanban-card.tsx @@ -34,6 +34,8 @@ import { StopCircle, FlaskConical, ArrowLeft, + MessageSquare, + GitCommit, } from "lucide-react"; import { CountUpTimer } from "@/components/ui/count-up-timer"; @@ -47,6 +49,8 @@ interface KanbanCardProps { onForceStop?: () => void; onManualVerify?: () => void; onMoveBackToInProgress?: () => void; + onFollowUp?: () => void; + onCommit?: () => void; hasContext?: boolean; isCurrentAutoTask?: boolean; shortcutKey?: string; @@ -62,6 +66,8 @@ export function KanbanCard({ onForceStop, onManualVerify, onMoveBackToInProgress, + onFollowUp, + onCommit, hasContext, isCurrentAutoTask, shortcutKey, @@ -364,6 +370,51 @@ export function KanbanCard({ )} + {!isCurrentAutoTask && feature.status === "waiting_approval" && ( + <> + {/* Follow-up prompt button */} + {onFollowUp && ( + + )} + {/* Commit and verify button */} + {onCommit && ( + + )} + + + )} {!isCurrentAutoTask && feature.status === "backlog" && ( <>
)} + {/* Agent Info Panel - shows for in_progress, waiting_approval, verified */} + {feature.status !== "backlog" && agentInfo && ( +
+ {/* Model & Progress Bar */} +
+
+ + {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 && ( @@ -471,6 +670,40 @@ export function KanbanCard({ + + {/* Summary Modal */} + + + + + + Implementation Summary + + + {feature.description} + + +
+
+
+                {feature.summary || summary || agentInfo?.summary || "No summary available"}
+              
+
+
+ + + +
+
); } diff --git a/app/src/lib/agent-context-parser.ts b/app/src/lib/agent-context-parser.ts new file mode 100644 index 00000000..d77ab166 --- /dev/null +++ b/app/src/lib/agent-context-parser.ts @@ -0,0 +1,246 @@ +/** + * Agent Context Parser + * Extracts useful information from agent context files for display in kanban cards + */ + +export interface AgentTaskInfo { + // Task list extracted from TodoWrite tool calls + todos: { + content: string; + status: "pending" | "in_progress" | "completed"; + }[]; + + // Progress stats + toolCallCount: number; + lastToolUsed?: string; + + // Phase info + currentPhase?: "planning" | "action" | "verification"; + + // Summary (if feature is completed) + summary?: string; + + // Estimated progress percentage based on phase and tool calls + progressPercentage: number; +} + +/** + * Default model used by the feature executor + */ +export const DEFAULT_MODEL = "claude-opus-4-5-20251101"; + +/** + * Formats a model name for display + */ +export function formatModelName(model: string): string { + if (model.includes("opus")) return "Opus 4.5"; + if (model.includes("sonnet")) return "Sonnet 4"; + if (model.includes("haiku")) return "Haiku 3.5"; + return model.split("-").slice(1, 3).join(" "); +} + +/** + * Extracts todos from the context content + * Looks for TodoWrite tool calls in the format: + * TodoWrite: [{"content": "...", "status": "..."}] + */ +function extractTodos(content: string): AgentTaskInfo["todos"] { + const todos: AgentTaskInfo["todos"] = []; + + // Look for TodoWrite tool inputs + const todoMatches = content.matchAll(/TodoWrite.*?(?:"todos"\s*:\s*)?(\[[\s\S]*?\](?=\s*(?:\}|$|šŸ”§|šŸ“‹|⚔|āœ…|āŒ)))/g); + + for (const match of todoMatches) { + try { + // Try to find JSON array in the match + const jsonStr = match[1] || match[0]; + const arrayMatch = jsonStr.match(/\[[\s\S]*?\]/); + if (arrayMatch) { + const parsed = JSON.parse(arrayMatch[0]); + if (Array.isArray(parsed)) { + for (const item of parsed) { + if (item.content && item.status) { + // Check if this todo already exists (avoid duplicates) + if (!todos.some(t => t.content === item.content)) { + todos.push({ + content: item.content, + status: item.status, + }); + } + } + } + } + } + } catch { + // Ignore parse errors + } + } + + // Also try to extract from markdown task lists + const markdownTodos = content.matchAll(/- \[([ xX])\] (.+)/g); + for (const match of markdownTodos) { + const isCompleted = match[1].toLowerCase() === "x"; + const content = match[2].trim(); + if (!todos.some(t => t.content === content)) { + todos.push({ + content, + status: isCompleted ? "completed" : "pending", + }); + } + } + + return todos; +} + +/** + * Counts tool calls in the content + */ +function countToolCalls(content: string): number { + const matches = content.match(/šŸ”§\s*Tool:/g); + return matches?.length || 0; +} + +/** + * Gets the last tool used + */ +function getLastToolUsed(content: string): string | undefined { + const matches = [...content.matchAll(/šŸ”§\s*Tool:\s*(\S+)/g)]; + if (matches.length > 0) { + return matches[matches.length - 1][1]; + } + return undefined; +} + +/** + * Determines the current phase from the content + */ +function getCurrentPhase(content: string): "planning" | "action" | "verification" | undefined { + // Find the last phase marker + const planningIndex = content.lastIndexOf("šŸ“‹"); + const actionIndex = content.lastIndexOf("⚔"); + const verificationIndex = content.lastIndexOf("āœ…"); + + const maxIndex = Math.max(planningIndex, actionIndex, verificationIndex); + + if (maxIndex === -1) return undefined; + if (maxIndex === verificationIndex) return "verification"; + if (maxIndex === actionIndex) return "action"; + return "planning"; +} + +/** + * Extracts a summary from completed feature context + */ +function extractSummary(content: string): string | undefined { + // Look for completion markers and extract surrounding text + const completionMatch = content.match(/āœ“ (?:Feature|Verification|Task) (?:successfully|completed|verified)[^\n]*(?:\n[^\n]{1,200})?/i); + if (completionMatch) { + return completionMatch[0].trim(); + } + + // Look for summary sections + const summaryMatch = content.match(/## Summary[^\n]*\n([\s\S]{1,500}?)(?=\n##|\nšŸ”§|$)/i); + if (summaryMatch) { + return summaryMatch[1].trim(); + } + + // Look for "What was done" type sections + const whatWasDoneMatch = content.match(/(?:What was done|Changes made|Implemented)[^\n]*\n([\s\S]{1,500}?)(?=\n##|\nšŸ”§|$)/i); + if (whatWasDoneMatch) { + return whatWasDoneMatch[1].trim(); + } + + return undefined; +} + +/** + * Calculates progress percentage based on phase and context + * Uses a more dynamic approach that better reflects actual progress + */ +function calculateProgress(phase: AgentTaskInfo["currentPhase"], toolCallCount: number, todos: AgentTaskInfo["todos"]): number { + // If we have todos, primarily use them for progress calculation + if (todos.length > 0) { + const completedCount = todos.filter(t => t.status === "completed").length; + const inProgressCount = todos.filter(t => t.status === "in_progress").length; + + // Weight: completed = 1, in_progress = 0.5, pending = 0 + const progress = ((completedCount + inProgressCount * 0.5) / todos.length) * 90; + + // Add a small base amount and cap at 95% + return Math.min(5 + progress, 95); + } + + // Fallback: use phase-based progress with tool call scaling + let phaseProgress = 0; + switch (phase) { + case "planning": + // Planning phase: 5-25% + phaseProgress = 5 + Math.min(toolCallCount * 1, 20); + break; + case "action": + // Action phase: 25-75% based on tool calls (logarithmic scaling) + phaseProgress = 25 + Math.min(Math.log2(toolCallCount + 1) * 10, 50); + break; + case "verification": + // Verification phase: 75-95% + phaseProgress = 75 + Math.min(toolCallCount * 0.5, 20); + break; + default: + // Starting: just use tool calls + phaseProgress = Math.min(toolCallCount * 0.5, 10); + } + + return Math.min(Math.round(phaseProgress), 95); +} + +/** + * Parses agent context content and extracts useful information + */ +export function parseAgentContext(content: string): AgentTaskInfo { + if (!content || !content.trim()) { + return { + todos: [], + toolCallCount: 0, + progressPercentage: 0, + }; + } + + const todos = extractTodos(content); + const toolCallCount = countToolCalls(content); + const lastToolUsed = getLastToolUsed(content); + const currentPhase = getCurrentPhase(content); + const summary = extractSummary(content); + const progressPercentage = calculateProgress(currentPhase, toolCallCount, todos); + + return { + todos, + toolCallCount, + lastToolUsed, + currentPhase, + summary, + progressPercentage, + }; +} + +/** + * Quick stats for display in card badges + */ +export interface QuickStats { + toolCalls: number; + completedTasks: number; + totalTasks: number; + phase?: string; +} + +/** + * Extracts quick stats from context for compact display + */ +export function getQuickStats(content: string): QuickStats { + const info = parseAgentContext(content); + return { + toolCalls: info.toolCallCount, + completedTasks: info.todos.filter(t => t.status === "completed").length, + totalTasks: info.todos.length, + phase: info.currentPhase, + }; +} diff --git a/app/src/store/app-store.ts b/app/src/store/app-store.ts index 1ce968e4..4a7c0c38 100644 --- a/app/src/store/app-store.ts +++ b/app/src/store/app-store.ts @@ -61,6 +61,7 @@ export interface Feature { imagePaths?: FeatureImagePath[]; // Paths to temp files for agent context startedAt?: string; // ISO timestamp for when the card moved to in_progress skipTests?: boolean; // When true, skip TDD approach and require manual verification + summary?: string; // Summary of what was done/modified by the agent } export interface AppState { From 6f9d90b199b11f5a37824d39a0f361341e34081e Mon Sep 17 00:00:00 2001 From: Kacper Date: Tue, 9 Dec 2025 23:49:19 +0100 Subject: [PATCH 5/7] feat(markdown): integrate Markdown component for enhanced text rendering - Added a reusable Markdown component to render markdown content with styled typography for dark mode. - Updated KanbanCard to utilize the new Markdown component for displaying feature summaries, improving readability and presentation. - Included the `react-markdown` library as a dependency for markdown parsing. Modified: package.json, package-lock.json, markdown.tsx, kanban-card.tsx --- app/package-lock.json | 1168 +++++++++++++++++++++- app/package.json | 1 + app/src/components/ui/markdown.tsx | 48 + app/src/components/views/kanban-card.tsx | 19 +- app/src/lib/agent-context-parser.ts | 15 +- 5 files changed, 1228 insertions(+), 23 deletions(-) create mode 100644 app/src/components/ui/markdown.tsx 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/kanban-card.tsx b/app/src/components/views/kanban-card.tsx index 169cc190..93154200 100644 --- a/app/src/components/views/kanban-card.tsx +++ b/app/src/components/views/kanban-card.tsx @@ -50,6 +50,7 @@ import { formatModelName, DEFAULT_MODEL, } from "@/lib/agent-context-parser"; +import { Markdown } from "@/components/ui/markdown"; interface KanbanCardProps { feature: Feature; @@ -674,7 +675,7 @@ export function KanbanCard({ {/* Summary Modal */} @@ -682,16 +683,16 @@ export function KanbanCard({ Implementation Summary - - {feature.description} + + {feature.description.length > 100 + ? `${feature.description.slice(0, 100)}...` + : feature.description} -
-
-
-                {feature.summary || summary || agentInfo?.summary || "No summary available"}
-              
-
+
+ + {feature.summary || summary || agentInfo?.summary || "No summary available"} +
- {/* 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) => (
+
+
+
+
+ {Math.round(agentInfo.progressPercentage)}% +
+
+ )} + + {/* Detailed mode: Show all agent info */} + {showAgentInfo && feature.status !== "backlog" && agentInfo && (
- {/* Model & Progress Bar */} + {/* Model & Phase */}
diff --git a/app/src/components/views/settings-view.tsx b/app/src/components/views/settings-view.tsx index 295cbd42..ef43f607 100644 --- a/app/src/components/views/settings-view.tsx +++ b/app/src/components/views/settings-view.tsx @@ -6,10 +6,10 @@ 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, Eye, EyeOff, CheckCircle2, AlertCircle, Loader2, Zap, Sun, Moon, Palette } from "lucide-react"; +import { Settings, Key, Eye, EyeOff, CheckCircle2, AlertCircle, Loader2, Zap, Sun, Moon, Palette, 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); @@ -353,6 +353,70 @@ 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 */}