From 89ace7f6a440de8e7f455d433644da26d260126d Mon Sep 17 00:00:00 2001 From: Ashish Kumar Singh Date: Thu, 30 Oct 2025 19:43:51 -0400 Subject: [PATCH 01/12] fix: check repo exists before export --- .../controllers/githubExporter/controller.ts | 54 ++++++++++++++----- worker/services/github/GitHubService.ts | 30 +++++++++++ 2 files changed, 72 insertions(+), 12 deletions(-) diff --git a/worker/api/controllers/githubExporter/controller.ts b/worker/api/controllers/githubExporter/controller.ts index cd2cfd1b..ae8057f1 100644 --- a/worker/api/controllers/githubExporter/controller.ts +++ b/worker/api/controllers/githubExporter/controller.ts @@ -48,8 +48,8 @@ export class GitHubExporterController extends BaseController { const { env, agentId, repositoryName, description, isPrivate, token, username, existingRepositoryUrl } = options; try { - let repositoryUrl: string; - let cloneUrl: string; + let repositoryUrl: string | undefined; + let cloneUrl: string | undefined; // Check database for existing repository if not provided let finalExistingRepoUrl = existingRepositoryUrl; @@ -72,17 +72,39 @@ export class GitHubExporterController extends BaseController { // Determine repository details (sync to existing or create new) if (finalExistingRepoUrl) { - this.logger.info('Syncing to existing repository', { agentId, repositoryUrl: finalExistingRepoUrl }); - - repositoryUrl = finalExistingRepoUrl; - - // GitHub clone URL is simply the html_url + .git - cloneUrl = finalExistingRepoUrl.endsWith('.git') - ? finalExistingRepoUrl - : `${finalExistingRepoUrl}.git`; + // Check if repository still exists on GitHub + const exists = await GitHubService.repositoryExists({ + repositoryUrl: finalExistingRepoUrl, + token + }); - this.logger.info('Using existing repository URLs', { repositoryUrl, cloneUrl }); - } else { + if (!exists) { + // Repository doesn't exist - clear from database and create new + this.logger.info('Repository no longer exists, creating new one', { + agentId, + oldUrl: finalExistingRepoUrl, + repositoryName + }); + + try { + const appService = new AppService(env); + await appService.updateGitHubRepository(agentId, '', 'public'); + } catch (clearError) { + this.logger.warn('Failed to clear repository URL', { error: clearError, agentId }); + } + + // Create new repository + finalExistingRepoUrl = undefined; + } else { + // Repository exists, use it + repositoryUrl = finalExistingRepoUrl; + cloneUrl = finalExistingRepoUrl.endsWith('.git') + ? finalExistingRepoUrl + : `${finalExistingRepoUrl}.git`; + } + } + + if (!finalExistingRepoUrl) { this.logger.info('Creating new repository', { agentId, repositoryName }); const createResult = await GitHubService.createUserRepository({ @@ -130,6 +152,14 @@ export class GitHubExporterController extends BaseController { this.logger.info('Repository created', { agentId, repositoryUrl }); } + // Ensure repository URLs are set + if (!repositoryUrl || !cloneUrl) { + return { + success: false, + error: 'Failed to determine repository URLs' + }; + } + // Push files to repository this.logger.info('Pushing files to repository', { agentId, repositoryUrl }); diff --git a/worker/services/github/GitHubService.ts b/worker/services/github/GitHubService.ts index 0e7e52a8..d9f695da 100644 --- a/worker/services/github/GitHubService.ts +++ b/worker/services/github/GitHubService.ts @@ -140,6 +140,36 @@ export class GitHubService { } } + /** + * Check if repository exists on GitHub + */ + static async repositoryExists(options: { + repositoryUrl: string; + token: string; + }): Promise { + const repoInfo = GitHubService.extractRepoInfo(options.repositoryUrl); + + if (!repoInfo) { + return false; + } + + try { + const octokit = GitHubService.createOctokit(options.token); + await octokit.repos.get({ + owner: repoInfo.owner, + repo: repoInfo.repo + }); + + return true; + } catch (error) { + GitHubService.logger.error('Repository existence check failed', { + repositoryUrl: options.repositoryUrl, + error: error instanceof Error ? error.message : 'Unknown error' + }); + return false; + } + } + /** * Parse owner and repo name from GitHub URL */ From 6ed059510b14b3efb0795f05919d850f6f120ae1 Mon Sep 17 00:00:00 2001 From: Ashish Kumar Singh Date: Fri, 31 Oct 2025 01:34:45 -0400 Subject: [PATCH 02/12] cleanup: remove stale code_review websocket handling --- worker/agents/constants.ts | 1 - worker/agents/core/websocket.ts | 39 --------------------------------- 2 files changed, 40 deletions(-) diff --git a/worker/agents/constants.ts b/worker/agents/constants.ts index 58d0644c..4fc6f76f 100644 --- a/worker/agents/constants.ts +++ b/worker/agents/constants.ts @@ -80,7 +80,6 @@ export const WebSocketMessageResponses: Record = { export const WebSocketMessageRequests = { GENERATE_ALL: 'generate_all', GENERATE: 'generate', - CODE_REVIEW: 'code_review', DEPLOY: 'deploy', PREVIEW: 'preview', OVERWRITE: 'overwrite', diff --git a/worker/agents/core/websocket.ts b/worker/agents/core/websocket.ts index 78358c6d..52979b07 100644 --- a/worker/agents/core/websocket.ts +++ b/worker/agents/core/websocket.ts @@ -45,45 +45,6 @@ export function handleWebSocketMessage(agent: SimpleCodeGeneratorAgent, connecti } }); break; - case WebSocketMessageRequests.CODE_REVIEW: - if (agent.isCodeGenerating()) { - sendError(connection, 'Cannot perform code review while generating files'); - return; - } - sendToConnection(connection, WebSocketMessageResponses.CODE_REVIEW, { - message: 'Starting code review' - }); - agent.reviewCode().then(reviewResult => { - if (!reviewResult) { - sendError(connection, 'Failed to perform code review'); - return; - } - sendToConnection(connection, WebSocketMessageResponses.CODE_REVIEW, { - review: reviewResult, - issuesFound: reviewResult.issuesFound, - }); - if (reviewResult.issuesFound && parsedMessage.autoFix === true) { - for (const fileToFix of reviewResult.filesToFix) { - const fileToRegenerate = agent.state.generatedFilesMap[fileToFix.filePath]; - if (!fileToRegenerate) { - logger.warn(`File to fix not found in generated files: ${fileToFix.filePath}`); - continue; - } - agent.regenerateFile( - fileToRegenerate, - fileToFix.issues, - 0 - ).catch((error: unknown) => { - logger.error(`Error regenerating file ${fileToRegenerate.filePath}:`, error); - sendError(connection, `Error regenerating file: ${error instanceof Error ? error.message : String(error)}`); - }); - } - } - }).catch(error => { - logger.error('Error during code review:', error); - sendError(connection, `Error during code review: ${error instanceof Error ? error.message : String(error)}`); - }); - break; case WebSocketMessageRequests.DEPLOY: agent.deployToCloudflare().then((deploymentResult) => { if (!deploymentResult) { From bafe0d9fe63312895f8df4cfd6ae035d9199cf11 Mon Sep 17 00:00:00 2001 From: Ashish Kumar Singh Date: Fri, 31 Oct 2025 12:59:00 -0400 Subject: [PATCH 03/12] feat: remove code-review system in favor of deep-debugger + user prompt --- worker/agents/core/simpleGeneratorAgent.ts | 177 ++++++++------------- worker/agents/core/state.ts | 1 - 2 files changed, 65 insertions(+), 113 deletions(-) diff --git a/worker/agents/core/simpleGeneratorAgent.ts b/worker/agents/core/simpleGeneratorAgent.ts index 3258128b..9e524443 100644 --- a/worker/agents/core/simpleGeneratorAgent.ts +++ b/worker/agents/core/simpleGeneratorAgent.ts @@ -24,7 +24,6 @@ import { DeploymentManager } from '../services/implementations/DeploymentManager import { GenerationContext } from '../domain/values/GenerationContext'; import { IssueReport } from '../domain/values/IssueReport'; import { PhaseImplementationOperation } from '../operations/PhaseImplementation'; -import { CodeReviewOperation } from '../operations/CodeReview'; import { FileRegenerationOperation } from '../operations/FileRegeneration'; import { PhaseGenerationOperation } from '../operations/PhaseGeneration'; import { ScreenshotAnalysisOperation } from '../operations/ScreenshotAnalysis'; @@ -52,9 +51,9 @@ import { DeepDebugResult } from './types'; import { StateMigration } from './stateMigration'; import { generateNanoId } from 'worker/utils/idGenerator'; import { updatePackageJson } from '../utils/packageSyncer'; +import { IdGenerator } from '../utils/idGenerator'; interface Operations { - codeReview: CodeReviewOperation; regenerateFile: FileRegenerationOperation; generateNextPhase: PhaseGenerationOperation; analyzeScreenshot: ScreenshotAnalysisOperation; @@ -104,7 +103,6 @@ export class SimpleCodeGeneratorAgent extends Agent { private currentAbortController?: AbortController; protected operations: Operations = { - codeReview: new CodeReviewOperation(), regenerateFile: new FileRegenerationOperation(), generateNextPhase: new PhaseGenerationOperation(), analyzeScreenshot: new ScreenshotAnalysisOperation(), @@ -512,6 +510,22 @@ export class SimpleCodeGeneratorAgent extends Agent { if (runningHistory.length === 0) { runningHistory = currentConversation; } + + // Remove duplicates + const deduplicateMessages = (messages: ConversationMessage[]): ConversationMessage[] => { + const seen = new Set(); + return messages.filter(msg => { + if (seen.has(msg.conversationId)) { + return false; + } + seen.add(msg.conversationId); + return true; + }); + }; + + runningHistory = deduplicateMessages(runningHistory); + fullHistory = deduplicateMessages(fullHistory); + return { id: id, runningHistory, @@ -531,7 +545,32 @@ export class SimpleCodeGeneratorAgent extends Agent { } } - async saveToDatabase() { + addConversationMessage(message: ConversationMessage) { + const conversationState = this.getConversationState(); + if (!conversationState.runningHistory.find(msg => msg.conversationId === message.conversationId)) { + conversationState.runningHistory.push(message); + } else { + conversationState.runningHistory = conversationState.runningHistory.map(msg => { + if (msg.conversationId === message.conversationId) { + return message; + } + return msg; + }); + } + if (!conversationState.fullHistory.find(msg => msg.conversationId === message.conversationId)) { + conversationState.fullHistory.push(message); + } else { + conversationState.fullHistory = conversationState.fullHistory.map(msg => { + if (msg.conversationId === message.conversationId) { + return message; + } + return msg; + }); + } + this.setConversationState(conversationState); + } + + private async saveToDatabase() { this.logger().info(`Blueprint generated successfully for agent ${this.getAgentId()}`); // Save the app to database (authenticated users only) const appService = new AppService(this.env); @@ -998,12 +1037,10 @@ export class SimpleCodeGeneratorAgent extends Agent { } /** - * Execute review cycle state - run code review and regeneration cycles + * Execute review cycle state - review and cleanup */ async executeReviewCycle(): Promise { - this.logger().info("Executing REVIEWING state"); - - const reviewCycles = 2; + this.logger().info("Executing REVIEWING state - review and cleanup"); if (this.state.reviewingInitiated) { this.logger().info("Reviewing already initiated, skipping"); return CurrentDevState.IDLE; @@ -1012,78 +1049,27 @@ export class SimpleCodeGeneratorAgent extends Agent { ...this.state, reviewingInitiated: true }); - - try { - this.logger().info("Starting code review and improvement cycle..."); - - for (let i = 0; i < reviewCycles; i++) { - // Check if user input came during review - if so, go back to phase generation - if (this.state.pendingUserInputs.length > 0) { - this.logger().info("User input received during review, transitioning back to PHASE_GENERATING"); - return CurrentDevState.PHASE_GENERATING; - } - this.logger().info(`Starting code review cycle ${i + 1}...`); - - const reviewResult = await this.reviewCode(); - - if (!reviewResult) { - this.logger().warn("Code review failed. Skipping fix cycle."); - break; - } - - const issuesFound = reviewResult.issuesFound; - - if (issuesFound) { - this.logger().info(`Issues found in review cycle ${i + 1}`, { issuesFound }); - const promises = []; - - for (const fileToFix of reviewResult.filesToFix) { - if (!fileToFix.require_code_changes) continue; - - const fileToRegenerate = this.fileManager.getGeneratedFile(fileToFix.filePath); - if (!fileToRegenerate) { - this.logger().warn(`File to fix not found in generated files: ${fileToFix.filePath}, skipping`); - continue; - } - - promises.push(this.regenerateFile( - fileToRegenerate, - fileToFix.issues, - 0 - )); - } - - const fileResults = await Promise.allSettled(promises); - const files: FileOutputType[] = fileResults.map(result => result.status === "fulfilled" ? result.value : null).filter((result) => result !== null); - - await this.deployToSandbox(files, false, "fix: Applying code review fixes"); - - // await this.applyDeterministicCodeFixes(); - - this.logger().info("Completed regeneration for review cycle"); - } else { - this.logger().info("Code review found no issues. Review cycles complete."); - break; - } - } - - // Check again for user input before finalizing - if (this.state.pendingUserInputs.length > 0) { - this.logger().info("User input received after review, transitioning back to PHASE_GENERATING"); - return CurrentDevState.PHASE_GENERATING; - } else { - this.logger().info("Review cycles complete, transitioning to IDLE"); - return CurrentDevState.IDLE; - } - - } catch (error) { - this.logger().error("Error during review cycle:", error); - if (error instanceof RateLimitExceededError) { - throw error; + // If issues/errors found, prompt user if they want to review and cleanup + const issues = await this.fetchAllIssues(false); + if (issues.runtimeErrors.length > 0 || issues.staticAnalysis.typecheck.issues.length > 0) { + this.logger().info("Reviewing stage - issues found, prompting user to review and cleanup"); + const message : ConversationMessage = { + role: "assistant", + content: `If the user responds with yes, launch the 'deep_debug' tool with the prompt to fix all the issues in the app\nThere might be some bugs in the app. Do you want me to try to fix them?`, + conversationId: IdGenerator.generateConversationId(), } - return CurrentDevState.IDLE; + // Store the message in the conversation history so user's response can trigger the deep debug tool + this.addConversationMessage(message); + + this.broadcast(WebSocketMessageResponses.CONVERSATION_RESPONSE, { + message: message.content, + conversationId: message.conversationId, + isStreaming: false, + }); } + + return CurrentDevState.IDLE; } /** @@ -1320,7 +1306,7 @@ export class SimpleCodeGeneratorAgent extends Agent { // Deploy generated files if (finalFiles.length > 0) { - await this.deployToSandbox(finalFiles, false, phase.name); + await this.deployToSandbox(finalFiles, false, phase.name, true); if (postPhaseFixing) { await this.applyDeterministicCodeFixes(); if (this.state.inferenceContext.enableFastSmartCodeFix) { @@ -1424,39 +1410,6 @@ export class SimpleCodeGeneratorAgent extends Agent { } } - /** - * Perform comprehensive code review - * Analyzes for runtime errors, static issues, and best practices - */ - async reviewCode() { - const issues = await this.fetchAllIssues(true); - const issueReport = IssueReport.from(issues); - - // Report discovered issues - this.broadcast(WebSocketMessageResponses.CODE_REVIEWING, { - message: "Running code review...", - staticAnalysis: issues.staticAnalysis, - runtimeErrors: issues.runtimeErrors - }); - - const reviewResult = await this.operations.codeReview.execute( - {issues: issueReport}, - this.getOperationOptions() - ); - - // Execute commands if any - if (reviewResult.commands && reviewResult.commands.length > 0) { - await this.executeCommands(reviewResult.commands); - } - // Notify review completion - this.broadcast(WebSocketMessageResponses.CODE_REVIEWED, { - review: reviewResult, - message: "Code review completed" - }); - - return reviewResult; - } - /** * Regenerate a file to fix identified issues * Retries up to 3 times before giving up @@ -1784,7 +1737,7 @@ export class SimpleCodeGeneratorAgent extends Agent { let fileContents = ''; let filePurpose = ''; try { - const fmFile = this.fileManager.getGeneratedFile(path); + const fmFile = this.fileManager.getFile(path); if (fmFile) { fileContents = fmFile.fileContents; filePurpose = fmFile.filePurpose || ''; diff --git a/worker/agents/core/state.ts b/worker/agents/core/state.ts index 8abfe96b..73099971 100644 --- a/worker/agents/core/state.ts +++ b/worker/agents/core/state.ts @@ -22,7 +22,6 @@ export enum CurrentDevState { PHASE_GENERATING, PHASE_IMPLEMENTING, REVIEWING, - FILE_REGENERATING, FINALIZING, } From d8532668ec9ff595522c9fe9169b20a74c55baa7 Mon Sep 17 00:00:00 2001 From: Ashish Kumar Singh Date: Fri, 31 Oct 2025 14:14:09 -0400 Subject: [PATCH 04/12] feat: improve chat message handling and reconnection flow - Updated bootstrap completion logic to only show messages for new chats, not reloaded ones - Enhanced message display to properly show "thinking" states with separate rendering logic - Improved websocket reconnection handling by clearing old messages to prevent duplicates - Reorganized message display order to group thinking states and regular messages separately - Removed unnecessary tool events and status messages for cleaner conversation --- src/routes/chat/chat.tsx | 54 ++++++++++++++----- src/routes/chat/hooks/use-chat.ts | 19 ++++--- .../chat/utils/handle-websocket-message.ts | 12 +++-- src/routes/chat/utils/message-helpers.ts | 1 - 4 files changed, 57 insertions(+), 29 deletions(-) diff --git a/src/routes/chat/chat.tsx b/src/routes/chat/chat.tsx index 0d36540a..c77fe22f 100644 --- a/src/routes/chat/chat.tsx +++ b/src/routes/chat/chat.tsx @@ -410,7 +410,8 @@ export default function Chat() { }, [isGeneratingBlueprint, view]); useEffect(() => { - if (doneStreaming && !isGeneratingBlueprint && !blueprint) { + // Only show bootstrap completion message for NEW chats, not when reloading existing ones + if (doneStreaming && !isGeneratingBlueprint && !blueprint && urlChatId === 'new') { onCompleteBootstrap(); sendAiMessage( createAIMessage( @@ -426,6 +427,7 @@ export default function Chat() { sendAiMessage, blueprint, onCompleteBootstrap, + urlChatId, ]); const isRunning = useMemo(() => { @@ -595,6 +597,27 @@ export default function Chat() { )} + {otherMessages + .filter(message => message.role === 'assistant' && message.ui?.isThinking) + .map((message) => ( +
+ +
+ ))} + + {isThinking && !otherMessages.some(m => m.ui?.isThinking) && ( +
+ +
+ )} + )} - {otherMessages.map((message) => { - if (message.role === 'assistant') { + {otherMessages + .filter(message => !message.ui?.isThinking) + .map((message) => { + if (message.role === 'assistant') { + return ( + + ); + } return ( - ); - } - return ( - - ); - })} + })} + diff --git a/src/routes/chat/hooks/use-chat.ts b/src/routes/chat/hooks/use-chat.ts index 79072dc4..c02973b7 100644 --- a/src/routes/chat/hooks/use-chat.ts +++ b/src/routes/chat/hooks/use-chat.ts @@ -17,7 +17,7 @@ import { logger } from '@/utils/logger'; import { apiClient } from '@/lib/api-client'; import { appEvents } from '@/lib/app-events'; import { createWebSocketMessageHandler, type HandleMessageDeps } from '../utils/handle-websocket-message'; -import { isConversationalMessage, addOrUpdateMessage, createUserMessage, handleRateLimitError, createAIMessage, appendToolEvent, type ChatMessage } from '../utils/message-helpers'; +import { isConversationalMessage, addOrUpdateMessage, createUserMessage, handleRateLimitError, createAIMessage, type ChatMessage } from '../utils/message-helpers'; import { sendWebSocketMessage } from '../utils/websocket-helpers'; import { initialStages as defaultStages, updateStage as updateStageHelper } from '../utils/project-stage-helpers'; import type { ProjectStage } from '../utils/project-stage-helpers'; @@ -274,7 +274,10 @@ export function useChat({ // Send success message to user if (isRetry) { - sendMessage(createAIMessage('websocket_reconnected', '🔌 Connection restored! Continuing with code generation...')); + // Clear old messages on reconnect to prevent duplicates + setMessages(() => [ + createAIMessage('websocket_reconnected', 'Seems we lost connection for a while there. Fixed now!', true) + ]); } // Always request conversation state explicitly (running/full history) @@ -468,13 +471,10 @@ export function useChat({ }); } else if (connectionStatus.current === 'idle') { setIsBootstrapping(false); - // Show fetching indicator as a tool-event style message - setMessages(() => - appendToolEvent([], 'fetching-chat', { - name: 'fetching your latest conversations', - status: 'start', - }), - ); + // Show starting message with thinking indicator + setMessages(() => [ + createAIMessage('fetching-chat', 'Starting from where you left off...', true) + ]); // Fetch existing agent connection details const response = await apiClient.connectToAgent(urlChatId); @@ -487,7 +487,6 @@ export function useChat({ // Set the chatId for existing chat - this enables the chat input setChatId(urlChatId); - sendMessage(createAIMessage('resuming-chat', 'Starting from where you left off...')); logger.debug('connecting from init for existing chatId'); connectWithRetry(response.data.websocketUrl, { diff --git a/src/routes/chat/utils/handle-websocket-message.ts b/src/routes/chat/utils/handle-websocket-message.ts index f07f53a9..1ccac1a2 100644 --- a/src/routes/chat/utils/handle-websocket-message.ts +++ b/src/routes/chat/utils/handle-websocket-message.ts @@ -346,6 +346,8 @@ export function createWebSocketMessageHandler(deps: HandleMessageDeps) { logger.debug('Merging conversation_state with', deduplicated.length, 'messages (', restoredMessages.length - deduplicated.length, 'duplicates removed)'); setMessages(prev => { const hasFetching = prev.some(m => m.role === 'assistant' && m.conversationId === 'fetching-chat'); + const hasReconnect = prev.some(m => m.role === 'assistant' && m.conversationId === 'websocket_reconnected'); + if (hasFetching) { const next = appendToolEvent(prev, 'fetching-chat', { name: 'fetching your latest conversations', @@ -353,6 +355,12 @@ export function createWebSocketMessageHandler(deps: HandleMessageDeps) { }); return [...next, ...deduplicated]; } + + if (hasReconnect) { + // Preserve reconnect message on top when restoring state after reconnect + return [...prev, ...deduplicated]; + } + return deduplicated; }); } @@ -599,10 +607,6 @@ export function createWebSocketMessageHandler(deps: HandleMessageDeps) { } return updated; }); - - if (message.phase.name === 'Finalization and Review') { - sendMessage(createAIMessage('core_app_complete', 'Main app generation completed. Doing code cleanups and resolving any lingering issues. Meanwhile, feel free to ask me anything!')); - } } logger.debug('🔄 Scheduling preview refresh in 1 second after deployment completion'); diff --git a/src/routes/chat/utils/message-helpers.ts b/src/routes/chat/utils/message-helpers.ts index 7c0e461b..eddba260 100644 --- a/src/routes/chat/utils/message-helpers.ts +++ b/src/routes/chat/utils/message-helpers.ts @@ -28,7 +28,6 @@ export function isConversationalMessage(messageId: string): boolean { 'conversation_response', 'fetching-chat', 'chat-not-found', - 'resuming-chat', 'chat-welcome', 'deployment-status', 'code_reviewed', From 79fb1071524cd654750f978332f6acc0d1ac4c2a Mon Sep 17 00:00:00 2001 From: Ashish Kumar Singh Date: Fri, 31 Oct 2025 16:02:16 -0400 Subject: [PATCH 05/12] feat: enhance phase timeline with debugging status and issue tracking --- src/routes/chat/components/phase-timeline.tsx | 121 +++++++++++++++++- 1 file changed, 116 insertions(+), 5 deletions(-) diff --git a/src/routes/chat/components/phase-timeline.tsx b/src/routes/chat/components/phase-timeline.tsx index cc96c412..563d596f 100644 --- a/src/routes/chat/components/phase-timeline.tsx +++ b/src/routes/chat/components/phase-timeline.tsx @@ -186,6 +186,13 @@ interface PhaseTimelineProps { chatId?: string; isDeploying?: boolean; handleDeployToCloudflare?: (instanceId: string) => void; + // Issue tracking and debugging + runtimeErrorCount?: number; + staticIssueCount?: number; + isDebugging?: boolean; + // Activity state + isGenerating?: boolean; + isThinking?: boolean; } // Helper function to truncate long file paths @@ -267,7 +274,12 @@ export function PhaseTimeline({ onViewChange, chatId, isDeploying, - handleDeployToCloudflare + handleDeployToCloudflare, + runtimeErrorCount = 0, + staticIssueCount = 0, + isDebugging = false, + isGenerating = false, + isThinking = false }: PhaseTimelineProps) { const [expandedPhases, setExpandedPhases] = useState>(new Set()); const [showCollapsedBar, setShowCollapsedBar] = useState(false); @@ -651,9 +663,17 @@ export function PhaseTimeline({ > {/* Main Timeline Card */}
+ {/* Calculate if Done/Debugging will show for line extension */} + {(() => { + const allStagesCompleted = projectStages.every(stage => stage.status === 'completed'); + const isAnythingHappening = isDebugging || isGenerating || isThinking || isPreviewDeploying; + const willShowStatusStage = (allStagesCompleted && !isAnythingHappening) || isDebugging; + + return ( + <> {/* Project Stages */} {projectStages.map((stage, index) => ( -
+
@@ -679,6 +699,33 @@ export function PhaseTimeline({ )} + + {/* Subtle inline issue indicator for completed code stage */} + {stage.id === 'code' && stage.status === 'completed' && (runtimeErrorCount > 0 || staticIssueCount > 0) && ( + + + {runtimeErrorCount > 0 && ( + +
+ {runtimeErrorCount} error{runtimeErrorCount > 1 ? 's' : ''} + + )} + {runtimeErrorCount > 0 && staticIssueCount > 0 && ( + , + )} + {staticIssueCount > 0 && ( + +
+ {staticIssueCount} warning{staticIssueCount > 1 ? 's' : ''} + + )} + + )}
{/* Blueprint button */} @@ -700,13 +747,17 @@ export function PhaseTimeline({ {/* Detailed Phase Timeline for code stage */} {stage.id === 'code' && ( -
+
{phaseTimeline.map((phase, phaseIndex) => (
+ {/* Subtle vertical line connecting phases */} + {phaseIndex < phaseTimeline.length - 1 && ( +
+ )} {/* Phase Implementation Header */}
- {index !== projectStages.length - 1 && ( + {/* Vertical connecting line */} + {(index !== projectStages.length - 1 || (index === projectStages.length - 1 && willShowStatusStage)) && (
))} -
+ + + {/* Done stage - shows when everything is complete and nothing is happening */} + {(() => { + const allStagesCompleted = projectStages.every(stage => stage.status === 'completed'); + const isAnythingHappening = isDebugging || isGenerating || isThinking || isPreviewDeploying; + const showDone = allStagesCompleted && !isAnythingHappening; + + if (showDone) { + return ( + + {/* Connecting line from previous stage */} +
+ + + +
+ Done +
+ + ); + } + + // Show debugging status when debugging + if (isDebugging) { + return ( + + {/* Connecting line from previous stage */} +
+ + + +
+ Debugging in progress... +
+ + ); + } + + return null; + })()} + + + ); + })()} +
); From 6605ecdf51aa6f2c7feacc91ad2f3b1cdfb91127 Mon Sep 17 00:00:00 2001 From: Ashish Kumar Singh Date: Fri, 31 Oct 2025 18:27:52 -0400 Subject: [PATCH 06/12] feat: isolated deepdebug window --- src/index.css | 53 +++++ src/routes/chat/chat.tsx | 141 ++++++++----- .../chat/components/debug-session-bubble.tsx | 197 ++++++++++++++++++ src/routes/chat/components/messages.tsx | 68 +++++- src/routes/chat/hooks/use-chat.ts | 23 ++ src/routes/chat/hooks/use-debug-session.ts | 85 ++++++++ .../chat/utils/handle-websocket-message.ts | 52 ++--- .../chat/utils/project-stage-helpers.ts | 4 +- 8 files changed, 526 insertions(+), 97 deletions(-) create mode 100644 src/routes/chat/components/debug-session-bubble.tsx create mode 100644 src/routes/chat/hooks/use-debug-session.ts diff --git a/src/index.css b/src/index.css index 6a13b840..e5036133 100644 --- a/src/index.css +++ b/src/index.css @@ -29,6 +29,59 @@ .chat-edge-throb { animation: none; } } +/* Debug mode pulsing effect - red edges */ +@keyframes debug-pulse { + 0%, 100% { + box-shadow: + inset 4px 0 12px rgba(239, 68, 68, 0.1), + inset -4px 0 12px rgba(239, 68, 68, 0.1); + } + 50% { + box-shadow: + inset 4px 0 20px rgba(239, 68, 68, 0.2), + inset -4px 0 20px rgba(239, 68, 68, 0.2); + } +} + +.animate-debug-pulse { + animation: debug-pulse 2s ease-in-out infinite; +} + +@media (prefers-reduced-motion: reduce) { + .animate-debug-pulse { animation: none; } +} + +/* Custom scrollbar for debug bubble */ +.custom-scrollbar::-webkit-scrollbar { + width: 6px; +} + +.custom-scrollbar::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.05); + border-radius: 3px; +} + +.custom-scrollbar::-webkit-scrollbar-thumb { + background: rgba(239, 68, 68, 0.3); + border-radius: 3px; +} + +.custom-scrollbar::-webkit-scrollbar-thumb:hover { + background: rgba(239, 68, 68, 0.5); +} + +.dark .custom-scrollbar::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05); +} + +.dark .custom-scrollbar::-webkit-scrollbar-thumb { + background: rgba(239, 68, 68, 0.4); +} + +.dark .custom-scrollbar::-webkit-scrollbar-thumb:hover { + background: rgba(239, 68, 68, 0.6); +} + @custom-variant dark (&:is(.dark *)); @plugin '@tailwindcss/typography'; @theme inline { diff --git a/src/routes/chat/chat.tsx b/src/routes/chat/chat.tsx index c77fe22f..e0439814 100644 --- a/src/routes/chat/chat.tsx +++ b/src/routes/chat/chat.tsx @@ -11,6 +11,7 @@ import { useParams, useSearchParams, useNavigate } from 'react-router'; import { MonacoEditor } from '../../components/monaco-editor/monaco-editor'; import { AnimatePresence, motion } from 'framer-motion'; import { Expand, Github, GitBranch, LoaderCircle, RefreshCw, MoreHorizontal, RotateCcw, X } from 'lucide-react'; +import clsx from 'clsx'; import { Blueprint } from './components/blueprint'; import { FileExplorer } from './components/file-explorer'; import { UserMessage, AIMessage } from './components/messages'; @@ -134,6 +135,10 @@ export default function Chat() { shouldRefreshPreview, // Preview deployment state isPreviewDeploying, + // Issue tracking and debugging state + runtimeErrorCount, + staticIssueCount, + isDebugging, } = useChat({ chatId: urlChatId, query: userQuery, @@ -436,17 +441,15 @@ export default function Chat() { ); }, [isBootstrapping, isGeneratingBlueprint]); - // Check if chat input should be disabled (before blueprint completion and agentId assignment) + // Check if chat input should be disabled (before blueprint completion, or during debugging) const isChatDisabled = useMemo(() => { const blueprintStage = projectStages.find( (stage) => stage.id === 'blueprint', ); - const isBlueprintComplete = blueprintStage?.status === 'completed'; - const hasAgentId = !!chatId; + const blueprintNotCompleted = !blueprintStage || blueprintStage.status !== 'completed'; - // Disable until both blueprint is complete AND we have an agentId - return !isBlueprintComplete || !hasAgentId; - }, [projectStages, chatId]); + return blueprintNotCompleted || isDebugging; + }, [projectStages, isDebugging]); const chatFormRef = useRef(null); const { isDragging: isChatDragging, dragHandlers: chatDragHandlers } = useDragDrop({ @@ -528,7 +531,13 @@ export default function Chat() { layout="position" className="flex-1 shrink-0 flex flex-col basis-0 max-w-lg relative z-10 h-full min-h-0" > -
+
{appLoading ? (
@@ -561,41 +570,41 @@ export default function Chat() { )} {mainMessage && ( -
- - {chatId && ( -
- - - - - - { - e.preventDefault(); - setIsResetDialogOpen(true); - }} - > - - Reset conversation - - - -
- )} -
- )} +
+ + {chatId && ( +
+ + + + + + { + e.preventDefault(); + setIsResetDialogOpen(true); + }} + > + + Reset conversation + + + +
+ )} +
+ )} {otherMessages .filter(message => message.role === 'assistant' && message.ui?.isThinking) @@ -637,6 +646,11 @@ export default function Chat() { chatId={chatId} isDeploying={isDeploying} handleDeployToCloudflare={handleDeployToCloudflare} + runtimeErrorCount={runtimeErrorCount} + staticIssueCount={staticIssueCount} + isDebugging={isDebugging} + isGenerating={isGenerating} + isThinking={isThinking} /> {/* Deployment and Generation Controls */} @@ -763,11 +777,13 @@ export default function Chat() { }} disabled={isChatDisabled} placeholder={ - isChatDisabled - ? 'Please wait for blueprint completion...' - : isRunning - ? 'Chat with AI while generating...' - : 'Ask a follow up...' + isDebugging + ? 'Deep debugging in progress... Please abort to continue' + : isChatDisabled + ? 'Please wait for blueprint completion...' + : isRunning + ? 'Chat with AI while generating...' + : 'Chat with AI...' } rows={1} className="w-full bg-bg-2 border border-text-primary/10 rounded-xl px-3 pr-20 py-2 text-sm outline-none focus:border-white/20 drop-shadow-2xl text-text-primary placeholder:!text-text-primary/50 disabled:opacity-50 disabled:cursor-not-allowed resize-none overflow-y-auto no-scrollbar min-h-[36px] max-h-[120px]" @@ -952,14 +968,29 @@ export default function Chat() { {view === 'blueprint' && (
{/* Toolbar */} -
-
- - Blueprint.md - - {previewUrl && ( - - )} +
+
+ +
+ +
+
+ + Blueprint.md + + {previewUrl && ( + + )} +
+
+ +
+ {/* Right side - can add actions here if needed */}
diff --git a/src/routes/chat/components/debug-session-bubble.tsx b/src/routes/chat/components/debug-session-bubble.tsx new file mode 100644 index 00000000..7f60353c --- /dev/null +++ b/src/routes/chat/components/debug-session-bubble.tsx @@ -0,0 +1,197 @@ +import { useState, useRef, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Check, ChevronDown, AlertTriangle, ArrowDown, Loader } from 'lucide-react'; +import clsx from 'clsx'; +import type { ChatMessage } from '../utils/message-helpers'; +import { formatElapsedTime } from '../hooks/use-debug-session'; +import { MessageContentRenderer, ToolStatusIndicator } from './messages'; + +interface DebugSessionBubbleProps { +message: ChatMessage; +isActive: boolean; +elapsedSeconds: number; +toolCallCount: number; +} + +export function DebugSessionBubble({ +message, +isActive, +elapsedSeconds, +toolCallCount, +}: DebugSessionBubbleProps) { +const [isExpanded, setIsExpanded] = useState(true); +const [showScrollButton, setShowScrollButton] = useState(false); +const scrollAreaRef = useRef(null); +const autoScrollRef = useRef(true); + +// Auto-scroll to bottom when content changes +useEffect(() => { +if (autoScrollRef.current && scrollAreaRef.current) { +scrollAreaRef.current.scrollTop = scrollAreaRef.current.scrollHeight; +} +}, [message.content]); + +// Track scroll position +const handleScroll = () => { +const container = scrollAreaRef.current; +if (!container) return; + +const isNearBottom = +container.scrollHeight - container.scrollTop - container.clientHeight < 100; + +autoScrollRef.current = isNearBottom; +setShowScrollButton(!isNearBottom && container.scrollHeight > container.clientHeight); +}; + +const scrollToBottom = () => { +if (scrollAreaRef.current) { +scrollAreaRef.current.scrollTo({ +top: scrollAreaRef.current.scrollHeight, +behavior: 'smooth' +}); +autoScrollRef.current = true; +setShowScrollButton(false); +} +}; + +const debugEvent = message.ui?.toolEvents?.find(e => e.name === 'deep_debug'); +const hasError = debugEvent?.status === 'error'; + +return ( + +
+{/* Header */} + + +{/* Expandable content */} + +{isExpanded && ( + +
+{/* Scrollable content area */} +
+
+{/* Render message content */} + ev.contentLength !== undefined) || []} +/> + +{/* Tool events */} +{message.ui?.toolEvents && message.ui.toolEvents.length > 0 && ( +
+{message.ui.toolEvents +.filter(ev => ev.name !== 'deep_debug' && ev.contentLength === undefined) +.map((event, idx) => ( + +))} +
+)} +
+
+ +{/* Scroll to bottom button */} + +{showScrollButton && ( + + + +)} + +
+
+)} +
+
+
+); +} diff --git a/src/routes/chat/components/messages.tsx b/src/routes/chat/components/messages.tsx index 1082a94c..e0abb916 100644 --- a/src/routes/chat/components/messages.tsx +++ b/src/routes/chat/components/messages.tsx @@ -6,7 +6,8 @@ import rehypeExternalLinks from 'rehype-external-links'; import { LoaderCircle, Check, AlertTriangle, ChevronDown, ChevronRight, MessageSquare } from 'lucide-react'; import type { ToolEvent } from '../utils/message-helpers'; import type { ConversationMessage } from '@/api-types'; -import { useState } from 'react'; +import { useState, useEffect, useRef } from 'react'; +import { DebugSessionBubble } from './debug-session-bubble'; /** * Strip internal system tags that should not be displayed to users @@ -84,7 +85,7 @@ function convertToToolEvent(msg: ConversationMessage, idx: number): ToolEvent | }; } -function MessageContentRenderer({ +export function MessageContentRenderer({ content, toolEvents = [] }: { @@ -181,7 +182,7 @@ function ToolResultRenderer({ result, toolName }: { result: string; toolName: st } } -function ToolStatusIndicator({ event }: { event: ToolEvent }) { +export function ToolStatusIndicator({ event }: { event: ToolEvent }) { const [isExpanded, setIsExpanded] = useState(false); const hasResult = event.status === 'success' && event.result; const isDeepDebug = event.name === 'deep_debug'; @@ -270,6 +271,67 @@ export function AIMessage({ }) { const sanitizedMessage = sanitizeMessageForDisplay(message); + // Check if this is a debug session (active or just completed in this session) + const debugEvent = toolEvents.find(ev => ev.name === 'deep_debug'); + const isActiveDebug = debugEvent?.status === 'start'; + const isCompletedDebug = debugEvent?.status === 'success' || debugEvent?.status === 'error'; + + // Check if this is a live session with actual content + const hasInlineEvents = toolEvents.some(ev => ev.contentLength !== undefined); + const hasSubstantialContent = sanitizedMessage && sanitizedMessage.length > 50; // More than just "Initializing..." + const hasToolCalls = toolEvents.some(ev => ev.name !== 'deep_debug'); + + // Only show bubble if: actively debugging OR (completed/errored with actual content/tool calls and inline events) + const isLiveDebugSession = debugEvent && ( + isActiveDebug || + (isCompletedDebug && hasInlineEvents && (hasSubstantialContent || hasToolCalls)) + ); + + // Calculate elapsed time for active debug sessions + const [elapsedSeconds, setElapsedSeconds] = useState(0); + const startTimeRef = useRef(null); + + useEffect(() => { + if (!isActiveDebug) { + startTimeRef.current = null; + setElapsedSeconds(0); + return; + } + + if (!startTimeRef.current) { + startTimeRef.current = Date.now(); + } + + const interval = setInterval(() => { + if (startTimeRef.current) { + const elapsed = Math.floor((Date.now() - startTimeRef.current) / 1000); + setElapsedSeconds(elapsed); + } + }, 1000); + + return () => clearInterval(interval); + }, [isActiveDebug]); + + // Render debug bubble for live debug sessions (active or just completed) + // Don't show for old messages after page refresh (no inline events) + if (isLiveDebugSession) { + const toolCallCount = toolEvents.filter(e => e.name !== 'deep_debug').length; + + return ( + + ); + } + // Separate: events without contentLength = top (restored), with contentLength = inline (streaming) const topToolEvents = toolEvents.filter(ev => ev.contentLength === undefined); const inlineToolEvents = toolEvents.filter(ev => ev.contentLength !== undefined) diff --git a/src/routes/chat/hooks/use-chat.ts b/src/routes/chat/hooks/use-chat.ts index c02973b7..4194426e 100644 --- a/src/routes/chat/hooks/use-chat.ts +++ b/src/routes/chat/hooks/use-chat.ts @@ -102,6 +102,11 @@ export function useChat({ const [cloudflareDeploymentUrl, setCloudflareDeploymentUrl] = useState(''); const [deploymentError, setDeploymentError] = useState(); + // Issue tracking and debugging state + const [runtimeErrorCount, setRuntimeErrorCount] = useState(0); + const [staticIssueCount, setStaticIssueCount] = useState(0); + const [isDebugging, setIsDebugging] = useState(false); + // Preview deployment state const [isPreviewDeploying, setIsPreviewDeploying] = useState(false); @@ -182,6 +187,9 @@ export function useChat({ setIsGenerationPaused, setIsGenerating, setIsPhaseProgressActive, + setRuntimeErrorCount, + setStaticIssueCount, + setIsDebugging, // Current state isInitialStateRestored, blueprint, @@ -540,6 +548,17 @@ export function useChat({ } }, [edit]); + // Track debugging state based on deep_debug tool events in messages + useEffect(() => { + const hasActiveDebug = messages.some(msg => + msg.role === 'assistant' && + msg.ui?.toolEvents?.some(event => + event.name === 'deep_debug' && event.status === 'start' + ) + ); + setIsDebugging(hasActiveDebug); + }, [messages]); + // Control functions for deployment and generation const handleStopGeneration = useCallback(() => { sendWebSocketMessage(websocket, 'stop_generation'); @@ -634,5 +653,9 @@ export function useChat({ isPreviewDeploying, // Phase progress visual indicator isPhaseProgressActive, + // Issue tracking and debugging state + runtimeErrorCount, + staticIssueCount, + isDebugging, }; } diff --git a/src/routes/chat/hooks/use-debug-session.ts b/src/routes/chat/hooks/use-debug-session.ts new file mode 100644 index 00000000..7c4ee3e7 --- /dev/null +++ b/src/routes/chat/hooks/use-debug-session.ts @@ -0,0 +1,85 @@ +import { useMemo, useState, useEffect, useRef } from 'react'; +import type { ChatMessage } from '../utils/message-helpers'; + +interface DebugSessionInfo { + message: ChatMessage; + isActive: boolean; + startTime: number; + elapsedSeconds: number; + toolCallCount: number; +} + +/** + * Custom hook to extract and manage debug session state + * Reuses existing message/toolEvent infrastructure + */ +export function useDebugSession( + messages: ChatMessage[] +): DebugSessionInfo | null { + const [elapsedSeconds, setElapsedSeconds] = useState(0); + const startTimeRef = useRef(null); + + // Find the message containing deep_debug tool event + const debugMessage = useMemo(() => { + return messages.find(msg => + msg.ui?.toolEvents?.some(event => event.name === 'deep_debug') + ); + }, [messages]); + + // Extract debug event info + const debugInfo = useMemo(() => { + if (!debugMessage) return null; + + const debugEvent = debugMessage.ui?.toolEvents?.find(e => e.name === 'deep_debug'); + if (!debugEvent) return null; + + const toolCallCount = debugMessage.ui?.toolEvents?.filter( + e => e.name !== 'deep_debug' + ).length || 0; + + return { + message: debugMessage, + isActive: debugEvent.status === 'start', + startTime: debugEvent.timestamp, + toolCallCount, + }; + }, [debugMessage]); + + // Timer for elapsed time + useEffect(() => { + if (!debugInfo?.isActive) { + startTimeRef.current = null; + setElapsedSeconds(0); + return; + } + + if (!startTimeRef.current) { + startTimeRef.current = Date.now(); + } + + const interval = setInterval(() => { + if (startTimeRef.current) { + const elapsed = Math.floor((Date.now() - startTimeRef.current) / 1000); + setElapsedSeconds(elapsed); + } + }, 1000); + + return () => clearInterval(interval); + }, [debugInfo?.isActive]); + + if (!debugInfo) return null; + + return { + ...debugInfo, + elapsedSeconds, + }; +} + +/** + * Format elapsed time as MM:SS + */ +export function formatElapsedTime(seconds: number): string { + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${mins}:${secs.toString().padStart(2, '0')}`; +} diff --git a/src/routes/chat/utils/handle-websocket-message.ts b/src/routes/chat/utils/handle-websocket-message.ts index 1ccac1a2..6a650dee 100644 --- a/src/routes/chat/utils/handle-websocket-message.ts +++ b/src/routes/chat/utils/handle-websocket-message.ts @@ -44,6 +44,9 @@ export interface HandleMessageDeps { setIsGenerationPaused: React.Dispatch>; setIsGenerating: React.Dispatch>; setIsPhaseProgressActive: React.Dispatch>; + setRuntimeErrorCount: React.Dispatch>; + setStaticIssueCount: React.Dispatch>; + setIsDebugging: React.Dispatch>; // Current state isInitialStateRestored: boolean; @@ -79,9 +82,6 @@ export interface HandleMessageDeps { } export function createWebSocketMessageHandler(deps: HandleMessageDeps) { - // Track review lifecycle within this handler instance - let lastReviewIssueCount = 0; - let reviewStartAnnounced = false; const extractTextContent = (content: ConversationMessage['content']): string => { if (!content) return ''; if (typeof content === 'string') return content; @@ -243,7 +243,6 @@ export function createWebSocketMessageHandler(deps: HandleMessageDeps) { if (state.generatedFilesMap && Object.keys(state.generatedFilesMap).length > 0) { updateStage('code', { status: 'completed' }); - updateStage('validate', { status: 'completed' }); if (urlChatId !== 'new') { logger.debug('🚀 Requesting preview deployment for existing chat with files'); sendWebSocketMessage(websocket, 'preview'); @@ -269,7 +268,6 @@ export function createWebSocketMessageHandler(deps: HandleMessageDeps) { if (codeStage?.status === 'active' && !isGenerating) { if (state.generatedFilesMap && Object.keys(state.generatedFilesMap).length > 0) { updateStage('code', { status: 'completed' }); - updateStage('validate', { status: 'completed' }); if (!previewUrl) { logger.debug('🚀 Generated files exist but no preview URL - auto-deploying preview'); @@ -403,8 +401,6 @@ export function createWebSocketMessageHandler(deps: HandleMessageDeps) { case 'file_regenerating': { setFiles((prev) => setFileGenerating(prev, message.filePath, 'File being regenerated...')); setPhaseTimeline((prev) => updatePhaseFileStatus(prev, message.filePath, 'generating')); - // Activates fixing stage only when actual regenerations begin - updateStage('fix', { status: 'active', metadata: lastReviewIssueCount > 0 ? `Fixing ${lastReviewIssueCount} issues` : 'Fixing issues' }); break; } @@ -412,18 +408,13 @@ export function createWebSocketMessageHandler(deps: HandleMessageDeps) { updateStage('code', { status: 'active' }); setTotalFiles(message.totalFiles); setIsGenerating(true); - // Reset review tracking for a new generation run - lastReviewIssueCount = 0; - reviewStartAnnounced = false; break; } case 'generation_complete': { setIsRedeployReady(true); setFiles((prev) => setAllFilesCompleted(prev)); - setProjectStages((prev) => completeStages(prev, ['code', 'validate', 'fix'])); - // Ensure fix stage metadata is cleared on final completion - updateStage('fix', { status: 'completed', metadata: undefined }); + setProjectStages((prev) => completeStages(prev, ['code'])); sendMessage(createAIMessage('generation-complete', 'Code generation has been completed.')); @@ -456,8 +447,6 @@ export function createWebSocketMessageHandler(deps: HandleMessageDeps) { const totalIssues = reviewData?.filesToFix?.reduce((count: number, file: any) => count + file.issues.length, 0) || 0; - lastReviewIssueCount = totalIssues; - let reviewMessage = 'Code review complete'; if (reviewData?.issuesFound) { reviewMessage = `Code review complete - ${totalIssues} issue${totalIssues !== 1 ? 's' : ''} found across ${reviewData.filesToFix?.length || 0} file${reviewData.filesToFix?.length !== 1 ? 's' : ''}`; @@ -465,8 +454,6 @@ export function createWebSocketMessageHandler(deps: HandleMessageDeps) { reviewMessage = 'Code review complete - no issues found'; } - // Mark validation as completed at the end of review - updateStage('validate', { status: 'completed' }); sendMessage(createAIMessage('code_reviewed', reviewMessage)); break; } @@ -474,6 +461,9 @@ export function createWebSocketMessageHandler(deps: HandleMessageDeps) { case 'runtime_error_found': { logger.info('Runtime error found in sandbox', message.errors); + // Update runtime error count + deps.setRuntimeErrorCount(message.count || message.errors?.length || 0); + onDebugMessage?.('error', `Runtime Error (${message.count} errors)`, message.errors.map((e: any) => `${e.message}\nStack: ${e.stack || 'N/A'}`).join('\n\n'), @@ -483,23 +473,17 @@ export function createWebSocketMessageHandler(deps: HandleMessageDeps) { } case 'code_reviewing': { - const totalIssues = - (message.staticAnalysis?.lint?.issues?.length || 0) + - (message.staticAnalysis?.typecheck?.issues?.length || 0) + - (message.runtimeErrors.length || 0); + const lintIssues = message.staticAnalysis?.lint?.issues?.length || 0; + const typecheckIssues = message.staticAnalysis?.typecheck?.issues?.length || 0; + const runtimeErrors = message.runtimeErrors?.length || 0; + const totalIssues = lintIssues + typecheckIssues + runtimeErrors; - lastReviewIssueCount = totalIssues; - - // Announce review start once, right after main code gen - if (!reviewStartAnnounced) { - sendMessage(createAIMessage('review_start', 'App generation complete, now reviewing code indepth')); - reviewStartAnnounced = true; - } + // Update issue counts + deps.setStaticIssueCount(lintIssues + typecheckIssues); + deps.setRuntimeErrorCount(runtimeErrors); - // Only show reviewing as active; do not activate fix until regeneration actually starts - updateStage('validate', { status: 'active' }); - // Show identified issues count while review runs, but keep fix stage pending - updateStage('fix', { status: 'pending', metadata: totalIssues > 0 ? `Identified ${totalIssues} issues` : undefined }); + // Show review start message + sendMessage(createAIMessage('review_start', 'App generation complete, now reviewing code indepth')); if (totalIssues > 0) { const errorDetails = [ @@ -518,8 +502,6 @@ export function createWebSocketMessageHandler(deps: HandleMessageDeps) { } case 'phase_generating': { - updateStage('validate', { status: 'completed' }); - updateStage('fix', { status: 'completed', metadata: undefined }); sendMessage(createAIMessage('phase_generating', message.message)); setIsThinking(true); setIsPhaseProgressActive(true); @@ -567,7 +549,6 @@ export function createWebSocketMessageHandler(deps: HandleMessageDeps) { case 'phase_validating': { sendMessage(createAIMessage('phase_validating', message.message)); - updateStage('validate', { status: 'active' }); setPhaseTimeline(prev => { const updated = [...prev]; @@ -585,7 +566,6 @@ export function createWebSocketMessageHandler(deps: HandleMessageDeps) { case 'phase_validated': { sendMessage(createAIMessage('phase_validated', message.message)); - updateStage('validate', { status: 'completed' }); break; } diff --git a/src/routes/chat/utils/project-stage-helpers.ts b/src/routes/chat/utils/project-stage-helpers.ts index 5d6172e9..94becd8c 100644 --- a/src/routes/chat/utils/project-stage-helpers.ts +++ b/src/routes/chat/utils/project-stage-helpers.ts @@ -1,5 +1,5 @@ export interface ProjectStage { - id: 'bootstrap' | 'blueprint' | 'code' | 'validate' | 'fix'; + id: 'bootstrap' | 'blueprint' | 'code'; title: string; status: 'pending' | 'active' | 'completed' | 'error'; metadata?: string; @@ -17,8 +17,6 @@ export const initialStages: ProjectStage[] = [ status: 'pending', }, { id: 'code', title: 'Generating code', status: 'pending' }, - { id: 'validate', title: 'Reviewing & fixing code', status: 'pending' }, - { id: 'fix', title: 'Fixing issues', status: 'pending' }, ]; /** From 472688dd38f406bad1e6784901ec7124910a11c5 Mon Sep 17 00:00:00 2001 From: Ashish Kumar Singh Date: Sat, 1 Nov 2025 14:18:19 -0400 Subject: [PATCH 07/12] feat: improve error handling and message display for debugging - Added error recovery for aborted inference calls by showing partial response transcript - Updated debug message bubble display logic to only show when there are tool calls - Fixed stop generation button to properly reset debugging state - Enhanced error handling in code debugger to capture partial responses from InferError - Added serializeCallChain utility to format partial response transcripts with last 5 messages - Modified inference --- src/routes/chat/chat.tsx | 2 +- src/routes/chat/components/messages.tsx | 3 +- .../chat/utils/handle-websocket-message.ts | 2 + worker/agents/assistants/codeDebugger.ts | 50 ++++++++++------ worker/agents/inferutils/core.ts | 59 ++++++++++++++++--- worker/agents/inferutils/infer.ts | 10 ++-- .../operations/UserConversationProcessor.ts | 40 ++++++++----- 7 files changed, 116 insertions(+), 50 deletions(-) diff --git a/src/routes/chat/chat.tsx b/src/routes/chat/chat.tsx index e0439814..71803cea 100644 --- a/src/routes/chat/chat.tsx +++ b/src/routes/chat/chat.tsx @@ -801,7 +801,7 @@ export default function Chat() { }} />
- {(isGenerating || isGeneratingBlueprint) && ( + {(isGenerating || isGeneratingBlueprint || isDebugging) && (