mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
Merge branch 'v0.10.0rc' of github.com:AutoMaker-Org/automaker into v0.10.0rc
This commit is contained in:
88
.github/workflows/e2e-tests.yml
vendored
88
.github/workflows/e2e-tests.yml
vendored
@@ -37,7 +37,14 @@ jobs:
|
|||||||
git config --global user.email "ci@example.com"
|
git config --global user.email "ci@example.com"
|
||||||
|
|
||||||
- name: Start backend server
|
- 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:
|
env:
|
||||||
PORT: 3008
|
PORT: 3008
|
||||||
NODE_ENV: test
|
NODE_ENV: test
|
||||||
@@ -53,21 +60,70 @@ jobs:
|
|||||||
- name: Wait for backend server
|
- name: Wait for backend server
|
||||||
run: |
|
run: |
|
||||||
echo "Waiting for backend server to be ready..."
|
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
|
for i in {1..60}; do
|
||||||
if curl -s -f http://localhost:3008/api/health > /dev/null 2>&1; then
|
if curl -s -f http://localhost:3008/api/health > /dev/null 2>&1; then
|
||||||
echo "Backend server is ready!"
|
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
|
exit 0
|
||||||
fi
|
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)"
|
echo "Waiting... ($i/60)"
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
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"
|
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"
|
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"
|
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
|
exit 1
|
||||||
|
|
||||||
- name: Run E2E tests
|
- name: Run E2E tests
|
||||||
@@ -81,6 +137,18 @@ jobs:
|
|||||||
# Keep UI-side login/defaults consistent
|
# Keep UI-side login/defaults consistent
|
||||||
AUTOMAKER_API_KEY: test-api-key-for-e2e-tests
|
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
|
- name: Upload Playwright report
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
if: always()
|
if: always()
|
||||||
@@ -98,3 +166,13 @@ jobs:
|
|||||||
apps/ui/test-results/
|
apps/ui/test-results/
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
if-no-files-found: ignore
|
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
|
||||||
|
|||||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -87,4 +87,11 @@ docker-compose.override.yml
|
|||||||
.claude/hans/
|
.claude/hans/
|
||||||
|
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
yarn.lock
|
yarn.lock
|
||||||
|
|
||||||
|
# Fork-specific workflow files (should never be committed)
|
||||||
|
DEVELOPMENT_WORKFLOW.md
|
||||||
|
check-sync.sh
|
||||||
|
# API key files
|
||||||
|
data/.api-key
|
||||||
|
data/credentials.json
|
||||||
|
|||||||
@@ -597,6 +597,26 @@ const startServer = (port: number) => {
|
|||||||
|
|
||||||
startServer(PORT);
|
startServer(PORT);
|
||||||
|
|
||||||
|
// Global error handlers to prevent crashes from uncaught errors
|
||||||
|
process.on('unhandledRejection', (reason: unknown, _promise: Promise<unknown>) => {
|
||||||
|
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
|
// Graceful shutdown
|
||||||
process.on('SIGTERM', () => {
|
process.on('SIGTERM', () => {
|
||||||
logger.info('SIGTERM received, shutting down...');
|
logger.info('SIGTERM received, shutting down...');
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { spawn } from 'child_process';
|
|||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as pty from 'node-pty';
|
import * as pty from 'node-pty';
|
||||||
import { ClaudeUsage } from '../routes/claude/types.js';
|
import { ClaudeUsage } from '../routes/claude/types.js';
|
||||||
|
import { createLogger } from '@automaker/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Claude Usage Service
|
* Claude Usage Service
|
||||||
@@ -14,6 +15,8 @@ import { ClaudeUsage } from '../routes/claude/types.js';
|
|||||||
* - macOS: Uses 'expect' command for PTY
|
* - macOS: Uses 'expect' command for PTY
|
||||||
* - Windows/Linux: Uses node-pty for PTY
|
* - Windows/Linux: Uses node-pty for PTY
|
||||||
*/
|
*/
|
||||||
|
const logger = createLogger('ClaudeUsage');
|
||||||
|
|
||||||
export class ClaudeUsageService {
|
export class ClaudeUsageService {
|
||||||
private claudeBinary = 'claude';
|
private claudeBinary = 'claude';
|
||||||
private timeout = 30000; // 30 second timeout
|
private timeout = 30000; // 30 second timeout
|
||||||
@@ -164,21 +167,40 @@ export class ClaudeUsageService {
|
|||||||
const shell = this.isWindows ? 'cmd.exe' : '/bin/sh';
|
const shell = this.isWindows ? 'cmd.exe' : '/bin/sh';
|
||||||
const args = this.isWindows ? ['/c', 'claude', '/usage'] : ['-c', 'claude /usage'];
|
const args = this.isWindows ? ['/c', 'claude', '/usage'] : ['-c', 'claude /usage'];
|
||||||
|
|
||||||
const ptyProcess = pty.spawn(shell, args, {
|
let ptyProcess: any = null;
|
||||||
name: 'xterm-256color',
|
|
||||||
cols: 120,
|
try {
|
||||||
rows: 30,
|
ptyProcess = pty.spawn(shell, args, {
|
||||||
cwd: workingDirectory,
|
name: 'xterm-256color',
|
||||||
env: {
|
cols: 120,
|
||||||
...process.env,
|
rows: 30,
|
||||||
TERM: 'xterm-256color',
|
cwd: workingDirectory,
|
||||||
} as Record<string, string>,
|
env: {
|
||||||
});
|
...process.env,
|
||||||
|
TERM: 'xterm-256color',
|
||||||
|
} as Record<string, string>,
|
||||||
|
});
|
||||||
|
} 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(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
if (!settled) {
|
if (!settled) {
|
||||||
settled = true;
|
settled = true;
|
||||||
ptyProcess.kill();
|
if (ptyProcess && !ptyProcess.killed) {
|
||||||
|
ptyProcess.kill();
|
||||||
|
}
|
||||||
// Don't fail if we have data - return it instead
|
// Don't fail if we have data - return it instead
|
||||||
if (output.includes('Current session')) {
|
if (output.includes('Current session')) {
|
||||||
resolve(output);
|
resolve(output);
|
||||||
@@ -188,7 +210,7 @@ export class ClaudeUsageService {
|
|||||||
}
|
}
|
||||||
}, this.timeout);
|
}, this.timeout);
|
||||||
|
|
||||||
ptyProcess.onData((data) => {
|
ptyProcess.onData((data: string) => {
|
||||||
output += data;
|
output += data;
|
||||||
|
|
||||||
// Check if we've seen the usage data (look for "Current session")
|
// Check if we've seen the usage data (look for "Current session")
|
||||||
@@ -196,12 +218,12 @@ export class ClaudeUsageService {
|
|||||||
hasSeenUsageData = true;
|
hasSeenUsageData = true;
|
||||||
// Wait for full output, then send escape to exit
|
// Wait for full output, then send escape to exit
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!settled) {
|
if (!settled && ptyProcess && !ptyProcess.killed) {
|
||||||
ptyProcess.write('\x1b'); // Send escape key
|
ptyProcess.write('\x1b'); // Send escape key
|
||||||
|
|
||||||
// Fallback: if ESC doesn't exit (Linux), use SIGTERM after 2s
|
// Fallback: if ESC doesn't exit (Linux), use SIGTERM after 2s
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!settled) {
|
if (!settled && ptyProcess && !ptyProcess.killed) {
|
||||||
ptyProcess.kill('SIGTERM');
|
ptyProcess.kill('SIGTERM');
|
||||||
}
|
}
|
||||||
}, 2000);
|
}, 2000);
|
||||||
@@ -212,14 +234,14 @@ export class ClaudeUsageService {
|
|||||||
// Fallback: if we see "Esc to cancel" but haven't seen usage data yet
|
// Fallback: if we see "Esc to cancel" but haven't seen usage data yet
|
||||||
if (!hasSeenUsageData && output.includes('Esc to cancel')) {
|
if (!hasSeenUsageData && output.includes('Esc to cancel')) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!settled) {
|
if (!settled && ptyProcess && !ptyProcess.killed) {
|
||||||
ptyProcess.write('\x1b'); // Send escape key
|
ptyProcess.write('\x1b'); // Send escape key
|
||||||
}
|
}
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ptyProcess.onExit(({ exitCode }) => {
|
ptyProcess.onExit(({ exitCode }: { exitCode: number }) => {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
if (settled) return;
|
if (settled) return;
|
||||||
settled = true;
|
settled = true;
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ const PROVIDER_ICON_KEYS = {
|
|||||||
deepseek: 'deepseek',
|
deepseek: 'deepseek',
|
||||||
qwen: 'qwen',
|
qwen: 'qwen',
|
||||||
nova: 'nova',
|
nova: 'nova',
|
||||||
|
meta: 'meta',
|
||||||
|
mistral: 'mistral',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type ProviderIconKey = keyof typeof PROVIDER_ICON_KEYS;
|
type ProviderIconKey = keyof typeof PROVIDER_ICON_KEYS;
|
||||||
@@ -73,6 +75,17 @@ const PROVIDER_ICON_DEFINITIONS: Record<ProviderIconKey, ProviderIconDefinition>
|
|||||||
viewBox: '0 0 33 32',
|
viewBox: '0 0 33 32',
|
||||||
// Official Amazon Nova logo from lobehub/lobe-icons
|
// 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',
|
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;
|
const modelStr = typeof model === 'string' ? model.toLowerCase() : model;
|
||||||
|
|
||||||
// Check for OpenCode models (opencode/, amazon-bedrock/, opencode-*)
|
// 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';
|
return 'opencode';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,6 +366,11 @@ export function getProviderIconForModel(
|
|||||||
gemini: GeminiIcon,
|
gemini: GeminiIcon,
|
||||||
grok: GrokIcon,
|
grok: GrokIcon,
|
||||||
opencode: OpenCodeIcon,
|
opencode: OpenCodeIcon,
|
||||||
|
deepseek: DeepSeekIcon,
|
||||||
|
qwen: QwenIcon,
|
||||||
|
nova: NovaIcon,
|
||||||
|
meta: MetaIcon,
|
||||||
|
mistral: MistralIcon,
|
||||||
};
|
};
|
||||||
|
|
||||||
return iconMap[iconKey] || AnthropicIcon;
|
return iconMap[iconKey] || AnthropicIcon;
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export const OPENCODE_MODELS: ModelOption[] = OPENCODE_MODEL_CONFIGS.map((config
|
|||||||
label: config.label,
|
label: config.label,
|
||||||
description: config.description,
|
description: config.description,
|
||||||
badge: config.tier === 'free' ? 'Free' : config.tier === 'premium' ? 'Premium' : undefined,
|
badge: config.tier === 'free' ? 'Free' : config.tier === 'premium' ? 'Premium' : undefined,
|
||||||
provider: 'opencode' as ModelProvider,
|
provider: config.provider as ModelProvider,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -27,7 +27,17 @@ import {
|
|||||||
REASONING_EFFORT_LABELS,
|
REASONING_EFFORT_LABELS,
|
||||||
} from '@/components/views/board-view/shared/model-constants';
|
} from '@/components/views/board-view/shared/model-constants';
|
||||||
import { Check, ChevronsUpDown, Star, ChevronRight } from 'lucide-react';
|
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 { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
@@ -503,6 +513,28 @@ export function PhaseModelSelector({
|
|||||||
const isSelected = selectedModel === model.id;
|
const isSelected = selectedModel === model.id;
|
||||||
const isFavorite = favoriteModels.includes(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 (
|
return (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={model.id}
|
key={model.id}
|
||||||
@@ -514,7 +546,7 @@ export function PhaseModelSelector({
|
|||||||
className="group flex items-center justify-between py-2"
|
className="group flex items-center justify-between py-2"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3 overflow-hidden">
|
<div className="flex items-center gap-3 overflow-hidden">
|
||||||
<OpenCodeIcon
|
<ProviderIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
'h-4 w-4 shrink-0',
|
'h-4 w-4 shrink-0',
|
||||||
isSelected ? 'text-primary' : 'text-muted-foreground'
|
isSelected ? 'text-primary' : 'text-muted-foreground'
|
||||||
|
|||||||
@@ -368,3 +368,42 @@ export async function authenticateForTests(page: Page): Promise<boolean> {
|
|||||||
const apiKey = process.env.AUTOMAKER_API_KEY || 'test-api-key-for-e2e-tests';
|
const apiKey = process.env.AUTOMAKER_API_KEY || 'test-api-key-for-e2e-tests';
|
||||||
return authenticateWithApiKey(page, apiKey);
|
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<boolean> {
|
||||||
|
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<void> {
|
||||||
|
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.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user