From 427832e72e96f21ea402257019631cc83cdbe4a0 Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 11 Jan 2026 02:18:55 +0530 Subject: [PATCH 1/3] fix: Display correct provider icons for all OpenCode/Bedrock models MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The issue was that ALL OpenCode models were showing the OpenCode icon, regardless of their actual underlying provider. This fix ensures each model shows its authentic brand icon. Changes: 1. **model-constants.ts** - Fixed provider field assignment - Changed provider from hardcoded 'opencode' to actual config.provider - Now correctly maps: opencode/big-pickle, amazon-bedrock/anthropic.*, etc. 2. **phase-model-selector.tsx** - Added provider-specific icon logic - Added imports for DeepSeekIcon, NovaIcon, QwenIcon, MistralIcon, MetaIcon - Added ProviderIcon selector based on model.provider field - Each model type now displays its correct provider icon 3. **provider-icon.tsx** - Updated icon detection and mapping - Enhanced getUnderlyingModelIcon() to detect specific Bedrock providers: * amazon-bedrock/anthropic.* → anthropic icon * amazon-bedrock/deepseek.* → deepseek icon * amazon-bedrock/nova.* → nova icon * amazon-bedrock/meta.* or llama → meta icon * amazon-bedrock/mistral.* → mistral icon * amazon-bedrock/qwen.* → qwen icon * opencode/* → opencode icon - Added meta and mistral to PROVIDER_ICON_KEYS - Added placeholder definitions for meta/mistral in PROVIDER_ICON_DEFINITIONS - Updated iconMap to include all provider icons - Set OpenCode icon to official brand color (#6366F1 indigo) Result: All model selectors and kanban cards now show correct brand icons for each OpenCode model (DeepSeek whale, Amazon Nova sparkle, Qwen star, etc.) --- apps/ui/src/components/ui/provider-icon.tsx | 45 ++++++++++++++++++- .../board-view/shared/model-constants.ts | 2 +- .../model-defaults/phase-model-selector.tsx | 36 ++++++++++++++- 3 files changed, 79 insertions(+), 4 deletions(-) diff --git a/apps/ui/src/components/ui/provider-icon.tsx b/apps/ui/src/components/ui/provider-icon.tsx index 4d978305..9a53249d 100644 --- a/apps/ui/src/components/ui/provider-icon.tsx +++ b/apps/ui/src/components/ui/provider-icon.tsx @@ -13,6 +13,8 @@ const PROVIDER_ICON_KEYS = { deepseek: 'deepseek', qwen: 'qwen', nova: 'nova', + meta: 'meta', + mistral: 'mistral', } as const; type ProviderIconKey = keyof typeof PROVIDER_ICON_KEYS; @@ -73,6 +75,17 @@ const PROVIDER_ICON_DEFINITIONS: Record viewBox: '0 0 33 32', // Official Amazon Nova logo from lobehub/lobe-icons path: 'm17.865 23.28 1.533 1.543c.07.07.092.175.055.267l-2.398 6.118A1.24 1.24 0 0 1 15.9 32c-.51 0-.969-.315-1.155-.793l-3.451-8.804-5.582 5.617a.246.246 0 0 1-.35 0l-1.407-1.415a.25.25 0 0 1 0-.352l6.89-6.932a1.3 1.3 0 0 1 .834-.398 1.25 1.25 0 0 1 1.232.79l2.992 7.63 1.557-3.977a.248.248 0 0 1 .408-.085zm8.224-19.3-5.583 5.617-3.45-8.805a1.24 1.24 0 0 0-1.43-.762c-.414.092-.744.407-.899.805l-2.38 6.072a.25.25 0 0 0 .055.267l1.533 1.543c.127.127.34.082.407-.085L15.9 4.655l2.991 7.629a1.24 1.24 0 0 0 2.035.425l6.922-6.965a.25.25 0 0 0 0-.352L26.44 3.977a.246.246 0 0 0-.35 0zM8.578 17.566l-3.953-1.567 7.582-3.01c.49-.195.815-.685.785-1.24a1.3 1.3 0 0 0-.395-.84l-6.886-6.93a.246.246 0 0 0-.35 0L3.954 5.395a.25.25 0 0 0 0 .353l5.583 5.617-8.75 3.472a1.25 1.25 0 0 0 0 2.325l6.079 2.412a.24.24 0 0 0 .266-.055l1.533-1.542a.25.25 0 0 0-.085-.41zm22.434-2.73-6.08-2.412a.24.24 0 0 0-.265.055l-1.533 1.542a.25.25 0 0 0 .084.41L27.172 16l-7.583 3.01a1.255 1.255 0 0 0-.785 1.24c.018.317.172.614.395.84l6.89 6.931a.246.246 0 0 0 .35 0l1.406-1.415a.25.25 0 0 0 0-.352l-5.582-5.617 8.75-3.472a1.25 1.25 0 0 0 0-2.325z', + fill: '#FF9900', + }, + // Meta and Mistral use custom standalone SVG components + // These placeholder entries prevent TypeScript errors + meta: { + viewBox: '0 0 24 24', + path: '', + }, + mistral: { + viewBox: '0 0 24 24', + path: '', }, }; @@ -287,7 +300,32 @@ function getUnderlyingModelIcon(model?: AgentModel | string): ProviderIconKey { const modelStr = typeof model === 'string' ? model.toLowerCase() : model; // Check for OpenCode models (opencode/, amazon-bedrock/, opencode-*) - if (modelStr.includes('opencode') || modelStr.includes('amazon-bedrock')) { + if (modelStr.includes('opencode')) { + // For OpenCode models, check which specific provider + if (modelStr.includes('amazon-bedrock')) { + // Bedrock-hosted models - detect the specific provider + if (modelStr.includes('anthropic') || modelStr.includes('claude')) { + return 'anthropic'; + } + if (modelStr.includes('deepseek')) { + return 'deepseek'; + } + if (modelStr.includes('nova')) { + return 'nova'; + } + if (modelStr.includes('meta') || modelStr.includes('llama')) { + return 'meta'; + } + if (modelStr.includes('mistral')) { + return 'mistral'; + } + if (modelStr.includes('qwen')) { + return 'qwen'; + } + // Default for Bedrock + return 'opencode'; + } + // Native OpenCode models (opencode/big-pickle, etc.) return 'opencode'; } @@ -328,6 +366,11 @@ export function getProviderIconForModel( gemini: GeminiIcon, grok: GrokIcon, opencode: OpenCodeIcon, + deepseek: DeepSeekIcon, + qwen: QwenIcon, + nova: NovaIcon, + meta: MetaIcon, + mistral: MistralIcon, }; return iconMap[iconKey] || AnthropicIcon; diff --git a/apps/ui/src/components/views/board-view/shared/model-constants.ts b/apps/ui/src/components/views/board-view/shared/model-constants.ts index 6db117ad..21ff8ea6 100644 --- a/apps/ui/src/components/views/board-view/shared/model-constants.ts +++ b/apps/ui/src/components/views/board-view/shared/model-constants.ts @@ -110,7 +110,7 @@ export const OPENCODE_MODELS: ModelOption[] = OPENCODE_MODEL_CONFIGS.map((config label: config.label, description: config.description, badge: config.tier === 'free' ? 'Free' : config.tier === 'premium' ? 'Premium' : undefined, - provider: 'opencode' as ModelProvider, + provider: config.provider as ModelProvider, })); /** diff --git a/apps/ui/src/components/views/settings-view/model-defaults/phase-model-selector.tsx b/apps/ui/src/components/views/settings-view/model-defaults/phase-model-selector.tsx index 28d2ed1d..c14a6370 100644 --- a/apps/ui/src/components/views/settings-view/model-defaults/phase-model-selector.tsx +++ b/apps/ui/src/components/views/settings-view/model-defaults/phase-model-selector.tsx @@ -27,7 +27,17 @@ import { REASONING_EFFORT_LABELS, } from '@/components/views/board-view/shared/model-constants'; import { Check, ChevronsUpDown, Star, ChevronRight } from 'lucide-react'; -import { AnthropicIcon, CursorIcon, OpenAIIcon, OpenCodeIcon } from '@/components/ui/provider-icon'; +import { + AnthropicIcon, + CursorIcon, + OpenAIIcon, + OpenCodeIcon, + DeepSeekIcon, + NovaIcon, + QwenIcon, + MistralIcon, + MetaIcon, +} from '@/components/ui/provider-icon'; import { Button } from '@/components/ui/button'; import { Command, @@ -503,6 +513,28 @@ export function PhaseModelSelector({ const isSelected = selectedModel === model.id; const isFavorite = favoriteModels.includes(model.id); + // Get the appropriate icon based on provider + const ProviderIcon = (() => { + switch (model.provider) { + case 'opencode': + return OpenCodeIcon; + case 'amazon-bedrock-anthropic': + return AnthropicIcon; + case 'amazon-bedrock-deepseek': + return DeepSeekIcon; + case 'amazon-bedrock-amazon': + return NovaIcon; + case 'amazon-bedrock-meta': + return MetaIcon; + case 'amazon-bedrock-mistral': + return MistralIcon; + case 'amazon-bedrock-qwen': + return QwenIcon; + default: + return OpenCodeIcon; + } + })(); + return (
- Date: Sun, 11 Jan 2026 02:58:56 +0530 Subject: [PATCH 2/3] fix: Improve E2E test workflow for better backend debugging Enhanced backend server startup in CI: - Track server PID and process status - Save logs to backend.log for debugging - Better error detection with process monitoring - Added cleanup step to kill server process - Print backend logs on test failure Improves reliability of E2E tests by providing better diagnostics when backend fails to start --- .github/workflows/e2e-tests.yml | 88 +++++++++++++++++++++++++++++++-- .gitignore | 6 ++- 2 files changed, 88 insertions(+), 6 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index abc5a867..917672b5 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -37,7 +37,14 @@ jobs: git config --global user.email "ci@example.com" - name: Start backend server - run: npm run start --workspace=apps/server & + run: | + echo "Starting backend server..." + # Start server in background and save PID + npm run start --workspace=apps/server > backend.log 2>&1 & + SERVER_PID=$! + echo "Server started with PID: $SERVER_PID" + echo "SERVER_PID=$SERVER_PID" >> $GITHUB_ENV + env: PORT: 3008 NODE_ENV: test @@ -53,21 +60,70 @@ jobs: - name: Wait for backend server run: | echo "Waiting for backend server to be ready..." + + # Check if server process is running + if [ -z "$SERVER_PID" ]; then + echo "ERROR: Server PID not found in environment" + cat backend.log 2>/dev/null || echo "No backend log found" + exit 1 + fi + + # Check if process is actually running + if ! kill -0 $SERVER_PID 2>/dev/null; then + echo "ERROR: Server process $SERVER_PID is not running!" + echo "=== Backend logs ===" + cat backend.log + echo "" + echo "=== Recent system logs ===" + dmesg 2>/dev/null | tail -20 || echo "No dmesg available" + exit 1 + fi + + # Wait for health endpoint for i in {1..60}; do if curl -s -f http://localhost:3008/api/health > /dev/null 2>&1; then echo "Backend server is ready!" - curl -s http://localhost:3008/api/health | jq . 2>/dev/null || echo "Health check response: $(curl -s http://localhost:3008/api/health 2>/dev/null || echo 'No response')" + echo "=== Backend logs ===" + cat backend.log + echo "" + echo "Health check response:" + curl -s http://localhost:3008/api/health | jq . 2>/dev/null || echo "Health check: $(curl -s http://localhost:3008/api/health 2>/dev/null || echo 'No response')" exit 0 fi + + # Check if server process is still running + if ! kill -0 $SERVER_PID 2>/dev/null; then + echo "ERROR: Server process died during wait!" + echo "=== Backend logs ===" + cat backend.log + exit 1 + fi + echo "Waiting... ($i/60)" sleep 1 done - echo "Backend server failed to start!" - echo "Checking server status..." + + echo "ERROR: Backend server failed to start within 60 seconds!" + echo "=== Backend logs ===" + cat backend.log + echo "" + echo "=== Process status ===" ps aux | grep -E "(node|tsx)" | grep -v grep || echo "No node processes found" + echo "" + echo "=== Port status ===" netstat -tlnp 2>/dev/null | grep :3008 || echo "Port 3008 not listening" - echo "Testing health endpoint..." + lsof -i :3008 2>/dev/null || echo "lsof not available or port not in use" + echo "" + echo "=== Health endpoint test ===" curl -v http://localhost:3008/api/health 2>&1 || echo "Health endpoint failed" + + # Kill the server process if it's still hanging + if kill -0 $SERVER_PID 2>/dev/null; then + echo "" + echo "Killing stuck server process..." + kill -9 $SERVER_PID 2>/dev/null || true + fi + exit 1 - name: Run E2E tests @@ -81,6 +137,18 @@ jobs: # Keep UI-side login/defaults consistent AUTOMAKER_API_KEY: test-api-key-for-e2e-tests + - name: Print backend logs on failure + if: failure() + run: | + echo "=== E2E Tests Failed - Backend Logs ===" + cat backend.log 2>/dev/null || echo "No backend log found" + echo "" + echo "=== Process status at failure ===" + ps aux | grep -E "(node|tsx)" | grep -v grep || echo "No node processes found" + echo "" + echo "=== Port status ===" + netstat -tlnp 2>/dev/null | grep :3008 || echo "Port 3008 not listening" + - name: Upload Playwright report uses: actions/upload-artifact@v4 if: always() @@ -98,3 +166,13 @@ jobs: apps/ui/test-results/ retention-days: 7 if-no-files-found: ignore + + - name: Cleanup - Kill backend server + if: always() + run: | + if [ -n "$SERVER_PID" ]; then + echo "Cleaning up backend server (PID: $SERVER_PID)..." + kill $SERVER_PID 2>/dev/null || true + kill -9 $SERVER_PID 2>/dev/null || true + echo "Backend server cleanup complete" + fi diff --git a/.gitignore b/.gitignore index 91571307..55ba86b2 100644 --- a/.gitignore +++ b/.gitignore @@ -87,4 +87,8 @@ docker-compose.override.yml .claude/hans/ pnpm-lock.yaml -yarn.lock \ No newline at end of file +yarn.lock + +# Fork-specific workflow files (should never be committed) +DEVELOPMENT_WORKFLOW.md +check-sync.sh \ No newline at end of file From a92457b8715d7b2d772a1d1d1ed4a1df27fc3825 Mon Sep 17 00:00:00 2001 From: DhanushSantosh Date: Sun, 11 Jan 2026 03:22:43 +0530 Subject: [PATCH 3/3] fix: Handle Claude CLI unavailability gracefully in CI - Add try-catch around pty.spawn() to prevent crashes when PTY unavailable - Add unhandledRejection/uncaughtException handlers for graceful degradation - Add checkBackendHealth/waitForBackendHealth utilities for tests - Add data/.api-key and data/credentials.json to .gitignore --- .gitignore | 5 +- apps/server/src/index.ts | 20 +++++++ .../src/services/claude-usage-service.ts | 54 +++++++++++++------ apps/ui/tests/utils/api/client.ts | 39 ++++++++++++++ 4 files changed, 101 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index 55ba86b2..2904e438 100644 --- a/.gitignore +++ b/.gitignore @@ -91,4 +91,7 @@ yarn.lock # Fork-specific workflow files (should never be committed) DEVELOPMENT_WORKFLOW.md -check-sync.sh \ No newline at end of file +check-sync.sh +# API key files +data/.api-key +data/credentials.json diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index caa4dd6a..59cc6f57 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -597,6 +597,26 @@ const startServer = (port: number) => { startServer(PORT); +// Global error handlers to prevent crashes from uncaught errors +process.on('unhandledRejection', (reason: unknown, _promise: Promise) => { + logger.error('Unhandled Promise Rejection:', { + reason: reason instanceof Error ? reason.message : String(reason), + stack: reason instanceof Error ? reason.stack : undefined, + }); + // Don't exit - log the error and continue running + // This prevents the server from crashing due to unhandled rejections +}); + +process.on('uncaughtException', (error: Error) => { + logger.error('Uncaught Exception:', { + message: error.message, + stack: error.stack, + }); + // Exit on uncaught exceptions to prevent undefined behavior + // The process is in an unknown state after an uncaught exception + process.exit(1); +}); + // Graceful shutdown process.on('SIGTERM', () => { logger.info('SIGTERM received, shutting down...'); diff --git a/apps/server/src/services/claude-usage-service.ts b/apps/server/src/services/claude-usage-service.ts index 098ce29c..64ace35d 100644 --- a/apps/server/src/services/claude-usage-service.ts +++ b/apps/server/src/services/claude-usage-service.ts @@ -2,6 +2,7 @@ import { spawn } from 'child_process'; import * as os from 'os'; import * as pty from 'node-pty'; import { ClaudeUsage } from '../routes/claude/types.js'; +import { createLogger } from '@automaker/utils'; /** * Claude Usage Service @@ -14,6 +15,8 @@ import { ClaudeUsage } from '../routes/claude/types.js'; * - macOS: Uses 'expect' command for PTY * - Windows/Linux: Uses node-pty for PTY */ +const logger = createLogger('ClaudeUsage'); + export class ClaudeUsageService { private claudeBinary = 'claude'; private timeout = 30000; // 30 second timeout @@ -164,21 +167,40 @@ export class ClaudeUsageService { const shell = this.isWindows ? 'cmd.exe' : '/bin/sh'; const args = this.isWindows ? ['/c', 'claude', '/usage'] : ['-c', 'claude /usage']; - const ptyProcess = pty.spawn(shell, args, { - name: 'xterm-256color', - cols: 120, - rows: 30, - cwd: workingDirectory, - env: { - ...process.env, - TERM: 'xterm-256color', - } as Record, - }); + let ptyProcess: any = null; + + try { + ptyProcess = pty.spawn(shell, args, { + name: 'xterm-256color', + cols: 120, + rows: 30, + cwd: workingDirectory, + env: { + ...process.env, + TERM: 'xterm-256color', + } as Record, + }); + } catch (spawnError) { + // pty.spawn() can throw synchronously if the native module fails to load + // or if PTY is not available in the current environment (e.g., containers without /dev/pts) + const errorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError); + logger.error('[executeClaudeUsageCommandPty] Failed to spawn PTY:', errorMessage); + + // Return a user-friendly error instead of crashing + reject( + new Error( + `Unable to access terminal: ${errorMessage}. Claude CLI may not be available or PTY support is limited in this environment.` + ) + ); + return; + } const timeoutId = setTimeout(() => { if (!settled) { settled = true; - ptyProcess.kill(); + if (ptyProcess && !ptyProcess.killed) { + ptyProcess.kill(); + } // Don't fail if we have data - return it instead if (output.includes('Current session')) { resolve(output); @@ -188,7 +210,7 @@ export class ClaudeUsageService { } }, this.timeout); - ptyProcess.onData((data) => { + ptyProcess.onData((data: string) => { output += data; // Check if we've seen the usage data (look for "Current session") @@ -196,12 +218,12 @@ export class ClaudeUsageService { hasSeenUsageData = true; // Wait for full output, then send escape to exit setTimeout(() => { - if (!settled) { + if (!settled && ptyProcess && !ptyProcess.killed) { ptyProcess.write('\x1b'); // Send escape key // Fallback: if ESC doesn't exit (Linux), use SIGTERM after 2s setTimeout(() => { - if (!settled) { + if (!settled && ptyProcess && !ptyProcess.killed) { ptyProcess.kill('SIGTERM'); } }, 2000); @@ -212,14 +234,14 @@ export class ClaudeUsageService { // Fallback: if we see "Esc to cancel" but haven't seen usage data yet if (!hasSeenUsageData && output.includes('Esc to cancel')) { setTimeout(() => { - if (!settled) { + if (!settled && ptyProcess && !ptyProcess.killed) { ptyProcess.write('\x1b'); // Send escape key } }, 3000); } }); - ptyProcess.onExit(({ exitCode }) => { + ptyProcess.onExit(({ exitCode }: { exitCode: number }) => { clearTimeout(timeoutId); if (settled) return; settled = true; diff --git a/apps/ui/tests/utils/api/client.ts b/apps/ui/tests/utils/api/client.ts index c3f18074..abe6ef2f 100644 --- a/apps/ui/tests/utils/api/client.ts +++ b/apps/ui/tests/utils/api/client.ts @@ -368,3 +368,42 @@ export async function authenticateForTests(page: Page): Promise { const apiKey = process.env.AUTOMAKER_API_KEY || 'test-api-key-for-e2e-tests'; return authenticateWithApiKey(page, apiKey); } + +/** + * Check if the backend server is healthy + * Returns true if the server responds with status 200, false otherwise + */ +export async function checkBackendHealth(page: Page, timeout = 5000): Promise { + try { + const response = await page.request.get(`${API_BASE_URL}/api/health`, { + timeout, + }); + return response.ok(); + } catch { + return false; + } +} + +/** + * Wait for the backend to be healthy, with retry logic + * Throws an error if the backend doesn't become healthy within the timeout + */ +export async function waitForBackendHealth( + page: Page, + maxWaitMs = 30000, + checkIntervalMs = 500 +): Promise { + const startTime = Date.now(); + + while (Date.now() - startTime < maxWaitMs) { + if (await checkBackendHealth(page, checkIntervalMs)) { + return; + } + await page.waitForTimeout(checkIntervalMs); + } + + throw new Error( + `Backend did not become healthy within ${maxWaitMs}ms. ` + + `Last health check failed or timed out.` + ); +}