mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
Merge pull request #314 from AutoMaker-Org/feat/enchance-agent-runner
feat: enchance agent runner ui
This commit is contained in:
@@ -9,8 +9,7 @@ import { getErrorMessage, logError } from '../common.js';
|
||||
export function createIndexHandler(autoModeService: AutoModeService) {
|
||||
return async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const runningAgents = autoModeService.getRunningAgents();
|
||||
const status = autoModeService.getStatus();
|
||||
const runningAgents = await autoModeService.getRunningAgents();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
|
||||
@@ -1374,18 +1374,43 @@ Format your response as a structured markdown document.`;
|
||||
/**
|
||||
* Get detailed info about all running agents
|
||||
*/
|
||||
getRunningAgents(): Array<{
|
||||
featureId: string;
|
||||
projectPath: string;
|
||||
projectName: string;
|
||||
isAutoMode: boolean;
|
||||
}> {
|
||||
return Array.from(this.runningFeatures.values()).map((rf) => ({
|
||||
featureId: rf.featureId,
|
||||
projectPath: rf.projectPath,
|
||||
projectName: path.basename(rf.projectPath),
|
||||
isAutoMode: rf.isAutoMode,
|
||||
}));
|
||||
async getRunningAgents(): Promise<
|
||||
Array<{
|
||||
featureId: string;
|
||||
projectPath: string;
|
||||
projectName: string;
|
||||
isAutoMode: boolean;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}>
|
||||
> {
|
||||
const agents = await Promise.all(
|
||||
Array.from(this.runningFeatures.values()).map(async (rf) => {
|
||||
// Try to fetch feature data to get title and description
|
||||
let title: string | undefined;
|
||||
let description: string | undefined;
|
||||
|
||||
try {
|
||||
const feature = await this.featureLoader.get(rf.projectPath, rf.featureId);
|
||||
if (feature) {
|
||||
title = feature.title;
|
||||
description = feature.description;
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently ignore errors - title/description are optional
|
||||
}
|
||||
|
||||
return {
|
||||
featureId: rf.featureId,
|
||||
projectPath: rf.projectPath,
|
||||
projectName: path.basename(rf.projectPath),
|
||||
isAutoMode: rf.isAutoMode,
|
||||
title,
|
||||
description,
|
||||
};
|
||||
})
|
||||
);
|
||||
return agents;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
195
apps/server/tests/unit/routes/running-agents.test.ts
Normal file
195
apps/server/tests/unit/routes/running-agents.test.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import type { Request, Response } from 'express';
|
||||
import { createIndexHandler } from '@/routes/running-agents/routes/index.js';
|
||||
import type { AutoModeService } from '@/services/auto-mode-service.js';
|
||||
import { createMockExpressContext } from '../../utils/mocks.js';
|
||||
|
||||
describe('running-agents routes', () => {
|
||||
let mockAutoModeService: Partial<AutoModeService>;
|
||||
let req: Request;
|
||||
let res: Response;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
mockAutoModeService = {
|
||||
getRunningAgents: vi.fn(),
|
||||
};
|
||||
|
||||
const context = createMockExpressContext();
|
||||
req = context.req;
|
||||
res = context.res;
|
||||
});
|
||||
|
||||
describe('GET / (index handler)', () => {
|
||||
it('should return empty array when no agents are running', async () => {
|
||||
// Arrange
|
||||
vi.mocked(mockAutoModeService.getRunningAgents!).mockResolvedValue([]);
|
||||
|
||||
// Act
|
||||
const handler = createIndexHandler(mockAutoModeService as AutoModeService);
|
||||
await handler(req, res);
|
||||
|
||||
// Assert
|
||||
expect(mockAutoModeService.getRunningAgents).toHaveBeenCalled();
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
runningAgents: [],
|
||||
totalCount: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return running agents with all properties', async () => {
|
||||
// Arrange
|
||||
const runningAgents = [
|
||||
{
|
||||
featureId: 'feature-123',
|
||||
projectPath: '/home/user/project',
|
||||
projectName: 'project',
|
||||
isAutoMode: true,
|
||||
title: 'Implement login feature',
|
||||
description: 'Add user authentication with OAuth',
|
||||
},
|
||||
{
|
||||
featureId: 'feature-456',
|
||||
projectPath: '/home/user/other-project',
|
||||
projectName: 'other-project',
|
||||
isAutoMode: false,
|
||||
title: 'Fix navigation bug',
|
||||
description: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(mockAutoModeService.getRunningAgents!).mockResolvedValue(runningAgents);
|
||||
|
||||
// Act
|
||||
const handler = createIndexHandler(mockAutoModeService as AutoModeService);
|
||||
await handler(req, res);
|
||||
|
||||
// Assert
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
runningAgents,
|
||||
totalCount: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return agents without title/description (backward compatibility)', async () => {
|
||||
// Arrange
|
||||
const runningAgents = [
|
||||
{
|
||||
featureId: 'legacy-feature',
|
||||
projectPath: '/project',
|
||||
projectName: 'project',
|
||||
isAutoMode: true,
|
||||
title: undefined,
|
||||
description: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(mockAutoModeService.getRunningAgents!).mockResolvedValue(runningAgents);
|
||||
|
||||
// Act
|
||||
const handler = createIndexHandler(mockAutoModeService as AutoModeService);
|
||||
await handler(req, res);
|
||||
|
||||
// Assert
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
runningAgents,
|
||||
totalCount: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle errors gracefully and return 500', async () => {
|
||||
// Arrange
|
||||
const error = new Error('Database connection failed');
|
||||
vi.mocked(mockAutoModeService.getRunningAgents!).mockRejectedValue(error);
|
||||
|
||||
// Act
|
||||
const handler = createIndexHandler(mockAutoModeService as AutoModeService);
|
||||
await handler(req, res);
|
||||
|
||||
// Assert
|
||||
expect(res.status).toHaveBeenCalledWith(500);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: 'Database connection failed',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle non-Error exceptions', async () => {
|
||||
// Arrange
|
||||
vi.mocked(mockAutoModeService.getRunningAgents!).mockRejectedValue('String error');
|
||||
|
||||
// Act
|
||||
const handler = createIndexHandler(mockAutoModeService as AutoModeService);
|
||||
await handler(req, res);
|
||||
|
||||
// Assert
|
||||
expect(res.status).toHaveBeenCalledWith(500);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly count multiple running agents', async () => {
|
||||
// Arrange
|
||||
const runningAgents = Array.from({ length: 10 }, (_, i) => ({
|
||||
featureId: `feature-${i}`,
|
||||
projectPath: `/project-${i}`,
|
||||
projectName: `project-${i}`,
|
||||
isAutoMode: i % 2 === 0,
|
||||
title: `Feature ${i}`,
|
||||
description: `Description ${i}`,
|
||||
}));
|
||||
|
||||
vi.mocked(mockAutoModeService.getRunningAgents!).mockResolvedValue(runningAgents);
|
||||
|
||||
// Act
|
||||
const handler = createIndexHandler(mockAutoModeService as AutoModeService);
|
||||
await handler(req, res);
|
||||
|
||||
// Assert
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
runningAgents,
|
||||
totalCount: 10,
|
||||
});
|
||||
});
|
||||
|
||||
it('should include agents from different projects', async () => {
|
||||
// Arrange
|
||||
const runningAgents = [
|
||||
{
|
||||
featureId: 'feature-a',
|
||||
projectPath: '/workspace/project-alpha',
|
||||
projectName: 'project-alpha',
|
||||
isAutoMode: true,
|
||||
title: 'Feature A',
|
||||
description: 'In project alpha',
|
||||
},
|
||||
{
|
||||
featureId: 'feature-b',
|
||||
projectPath: '/workspace/project-beta',
|
||||
projectName: 'project-beta',
|
||||
isAutoMode: false,
|
||||
title: 'Feature B',
|
||||
description: 'In project beta',
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(mockAutoModeService.getRunningAgents!).mockResolvedValue(runningAgents);
|
||||
|
||||
// Act
|
||||
const handler = createIndexHandler(mockAutoModeService as AutoModeService);
|
||||
await handler(req, res);
|
||||
|
||||
// Assert
|
||||
const response = vi.mocked(res.json).mock.calls[0][0];
|
||||
expect(response.runningAgents[0].projectPath).toBe('/workspace/project-alpha');
|
||||
expect(response.runningAgents[1].projectPath).toBe('/workspace/project-beta');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { AutoModeService } from '@/services/auto-mode-service.js';
|
||||
import type { Feature } from '@automaker/types';
|
||||
|
||||
describe('auto-mode-service.ts', () => {
|
||||
let service: AutoModeService;
|
||||
@@ -66,4 +67,252 @@ describe('auto-mode-service.ts', () => {
|
||||
expect(runningCount).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRunningAgents', () => {
|
||||
// Helper to access private runningFeatures Map
|
||||
const getRunningFeaturesMap = (svc: AutoModeService) =>
|
||||
(svc as any).runningFeatures as Map<
|
||||
string,
|
||||
{ featureId: string; projectPath: string; isAutoMode: boolean }
|
||||
>;
|
||||
|
||||
// Helper to get the featureLoader and mock its get method
|
||||
const mockFeatureLoaderGet = (svc: AutoModeService, mockFn: ReturnType<typeof vi.fn>) => {
|
||||
(svc as any).featureLoader = { get: mockFn };
|
||||
};
|
||||
|
||||
it('should return empty array when no agents are running', async () => {
|
||||
const result = await service.getRunningAgents();
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return running agents with basic info when feature data is not available', async () => {
|
||||
// Arrange: Add a running feature to the Map
|
||||
const runningFeaturesMap = getRunningFeaturesMap(service);
|
||||
runningFeaturesMap.set('feature-123', {
|
||||
featureId: 'feature-123',
|
||||
projectPath: '/test/project/path',
|
||||
isAutoMode: true,
|
||||
});
|
||||
|
||||
// Mock featureLoader.get to return null (feature not found)
|
||||
const getMock = vi.fn().mockResolvedValue(null);
|
||||
mockFeatureLoaderGet(service, getMock);
|
||||
|
||||
// Act
|
||||
const result = await service.getRunningAgents();
|
||||
|
||||
// Assert
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual({
|
||||
featureId: 'feature-123',
|
||||
projectPath: '/test/project/path',
|
||||
projectName: 'path',
|
||||
isAutoMode: true,
|
||||
title: undefined,
|
||||
description: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return running agents with title and description when feature data is available', async () => {
|
||||
// Arrange
|
||||
const runningFeaturesMap = getRunningFeaturesMap(service);
|
||||
runningFeaturesMap.set('feature-456', {
|
||||
featureId: 'feature-456',
|
||||
projectPath: '/home/user/my-project',
|
||||
isAutoMode: false,
|
||||
});
|
||||
|
||||
const mockFeature: Partial<Feature> = {
|
||||
id: 'feature-456',
|
||||
title: 'Implement user authentication',
|
||||
description: 'Add login and signup functionality',
|
||||
category: 'auth',
|
||||
};
|
||||
|
||||
const getMock = vi.fn().mockResolvedValue(mockFeature);
|
||||
mockFeatureLoaderGet(service, getMock);
|
||||
|
||||
// Act
|
||||
const result = await service.getRunningAgents();
|
||||
|
||||
// Assert
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual({
|
||||
featureId: 'feature-456',
|
||||
projectPath: '/home/user/my-project',
|
||||
projectName: 'my-project',
|
||||
isAutoMode: false,
|
||||
title: 'Implement user authentication',
|
||||
description: 'Add login and signup functionality',
|
||||
});
|
||||
expect(getMock).toHaveBeenCalledWith('/home/user/my-project', 'feature-456');
|
||||
});
|
||||
|
||||
it('should handle multiple running agents', async () => {
|
||||
// Arrange
|
||||
const runningFeaturesMap = getRunningFeaturesMap(service);
|
||||
runningFeaturesMap.set('feature-1', {
|
||||
featureId: 'feature-1',
|
||||
projectPath: '/project-a',
|
||||
isAutoMode: true,
|
||||
});
|
||||
runningFeaturesMap.set('feature-2', {
|
||||
featureId: 'feature-2',
|
||||
projectPath: '/project-b',
|
||||
isAutoMode: false,
|
||||
});
|
||||
|
||||
const getMock = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
id: 'feature-1',
|
||||
title: 'Feature One',
|
||||
description: 'Description one',
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
id: 'feature-2',
|
||||
title: 'Feature Two',
|
||||
description: 'Description two',
|
||||
});
|
||||
mockFeatureLoaderGet(service, getMock);
|
||||
|
||||
// Act
|
||||
const result = await service.getRunningAgents();
|
||||
|
||||
// Assert
|
||||
expect(result).toHaveLength(2);
|
||||
expect(getMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should silently handle errors when fetching feature data', async () => {
|
||||
// Arrange
|
||||
const runningFeaturesMap = getRunningFeaturesMap(service);
|
||||
runningFeaturesMap.set('feature-error', {
|
||||
featureId: 'feature-error',
|
||||
projectPath: '/project-error',
|
||||
isAutoMode: true,
|
||||
});
|
||||
|
||||
const getMock = vi.fn().mockRejectedValue(new Error('Database connection failed'));
|
||||
mockFeatureLoaderGet(service, getMock);
|
||||
|
||||
// Act - should not throw
|
||||
const result = await service.getRunningAgents();
|
||||
|
||||
// Assert
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual({
|
||||
featureId: 'feature-error',
|
||||
projectPath: '/project-error',
|
||||
projectName: 'project-error',
|
||||
isAutoMode: true,
|
||||
title: undefined,
|
||||
description: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle feature with title but no description', async () => {
|
||||
// Arrange
|
||||
const runningFeaturesMap = getRunningFeaturesMap(service);
|
||||
runningFeaturesMap.set('feature-title-only', {
|
||||
featureId: 'feature-title-only',
|
||||
projectPath: '/project',
|
||||
isAutoMode: false,
|
||||
});
|
||||
|
||||
const getMock = vi.fn().mockResolvedValue({
|
||||
id: 'feature-title-only',
|
||||
title: 'Only Title',
|
||||
// description is undefined
|
||||
});
|
||||
mockFeatureLoaderGet(service, getMock);
|
||||
|
||||
// Act
|
||||
const result = await service.getRunningAgents();
|
||||
|
||||
// Assert
|
||||
expect(result[0].title).toBe('Only Title');
|
||||
expect(result[0].description).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle feature with description but no title', async () => {
|
||||
// Arrange
|
||||
const runningFeaturesMap = getRunningFeaturesMap(service);
|
||||
runningFeaturesMap.set('feature-desc-only', {
|
||||
featureId: 'feature-desc-only',
|
||||
projectPath: '/project',
|
||||
isAutoMode: false,
|
||||
});
|
||||
|
||||
const getMock = vi.fn().mockResolvedValue({
|
||||
id: 'feature-desc-only',
|
||||
description: 'Only description, no title',
|
||||
// title is undefined
|
||||
});
|
||||
mockFeatureLoaderGet(service, getMock);
|
||||
|
||||
// Act
|
||||
const result = await service.getRunningAgents();
|
||||
|
||||
// Assert
|
||||
expect(result[0].title).toBeUndefined();
|
||||
expect(result[0].description).toBe('Only description, no title');
|
||||
});
|
||||
|
||||
it('should extract projectName from nested paths correctly', async () => {
|
||||
// Arrange
|
||||
const runningFeaturesMap = getRunningFeaturesMap(service);
|
||||
runningFeaturesMap.set('feature-nested', {
|
||||
featureId: 'feature-nested',
|
||||
projectPath: '/home/user/workspace/projects/my-awesome-project',
|
||||
isAutoMode: true,
|
||||
});
|
||||
|
||||
const getMock = vi.fn().mockResolvedValue(null);
|
||||
mockFeatureLoaderGet(service, getMock);
|
||||
|
||||
// Act
|
||||
const result = await service.getRunningAgents();
|
||||
|
||||
// Assert
|
||||
expect(result[0].projectName).toBe('my-awesome-project');
|
||||
});
|
||||
|
||||
it('should fetch feature data in parallel for multiple agents', async () => {
|
||||
// Arrange: Add multiple running features
|
||||
const runningFeaturesMap = getRunningFeaturesMap(service);
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
runningFeaturesMap.set(`feature-${i}`, {
|
||||
featureId: `feature-${i}`,
|
||||
projectPath: `/project-${i}`,
|
||||
isAutoMode: i % 2 === 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Track call order
|
||||
const callOrder: string[] = [];
|
||||
const getMock = vi.fn().mockImplementation(async (projectPath: string, featureId: string) => {
|
||||
callOrder.push(featureId);
|
||||
// Simulate async delay to verify parallel execution
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
return { id: featureId, title: `Title for ${featureId}` };
|
||||
});
|
||||
mockFeatureLoaderGet(service, getMock);
|
||||
|
||||
// Act
|
||||
const startTime = Date.now();
|
||||
const result = await service.getRunningAgents();
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
// Assert
|
||||
expect(result).toHaveLength(5);
|
||||
expect(getMock).toHaveBeenCalledTimes(5);
|
||||
// If executed in parallel, total time should be ~10ms (one batch)
|
||||
// If sequential, it would be ~50ms (5 * 10ms)
|
||||
// Allow some buffer for execution overhead
|
||||
expect(duration).toBeLessThan(40);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,6 +23,8 @@ interface AgentOutputModalProps {
|
||||
featureStatus?: string;
|
||||
/** Called when a number key (0-9) is pressed while the modal is open */
|
||||
onNumberKeyPress?: (key: string) => void;
|
||||
/** Project path - if not provided, falls back to window.__currentProject for backward compatibility */
|
||||
projectPath?: string;
|
||||
}
|
||||
|
||||
type ViewMode = 'parsed' | 'raw' | 'changes';
|
||||
@@ -34,6 +36,7 @@ export function AgentOutputModal({
|
||||
featureId,
|
||||
featureStatus,
|
||||
onNumberKeyPress,
|
||||
projectPath: projectPathProp,
|
||||
}: AgentOutputModalProps) {
|
||||
const [output, setOutput] = useState<string>('');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@@ -62,19 +65,19 @@ export function AgentOutputModal({
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
// Get current project path from store (we'll need to pass this)
|
||||
const currentProject = (window as any).__currentProject;
|
||||
if (!currentProject?.path) {
|
||||
// Use projectPath prop if provided, otherwise fall back to window.__currentProject for backward compatibility
|
||||
const resolvedProjectPath = projectPathProp || (window as any).__currentProject?.path;
|
||||
if (!resolvedProjectPath) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
projectPathRef.current = currentProject.path;
|
||||
setProjectPath(currentProject.path);
|
||||
projectPathRef.current = resolvedProjectPath;
|
||||
setProjectPath(resolvedProjectPath);
|
||||
|
||||
// Use features API to get agent output
|
||||
if (api.features) {
|
||||
const result = await api.features.getAgentOutput(currentProject.path, featureId);
|
||||
const result = await api.features.getAgentOutput(resolvedProjectPath, featureId);
|
||||
|
||||
if (result.success) {
|
||||
setOutput(result.content || '');
|
||||
@@ -93,7 +96,7 @@ export function AgentOutputModal({
|
||||
};
|
||||
|
||||
loadOutput();
|
||||
}, [open, featureId]);
|
||||
}, [open, featureId, projectPathProp]);
|
||||
|
||||
// Listen to auto mode events and update output
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Bot, Folder, Loader2, RefreshCw, Square, Activity } from 'lucide-react';
|
||||
import { Bot, Folder, Loader2, RefreshCw, Square, Activity, FileText } from 'lucide-react';
|
||||
import { getElectronAPI, RunningAgent } from '@/lib/electron';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { AgentOutputModal } from './board-view/dialogs/agent-output-modal';
|
||||
|
||||
export function RunningAgentsView() {
|
||||
const [runningAgents, setRunningAgents] = useState<RunningAgent[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [selectedAgent, setSelectedAgent] = useState<RunningAgent | null>(null);
|
||||
const { setCurrentProject, projects } = useAppStore();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -94,6 +96,10 @@ export function RunningAgentsView() {
|
||||
[projects, setCurrentProject, navigate]
|
||||
);
|
||||
|
||||
const handleViewLogs = useCallback((agent: RunningAgent) => {
|
||||
setSelectedAgent(agent);
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
@@ -156,15 +162,25 @@ export function RunningAgentsView() {
|
||||
</div>
|
||||
|
||||
{/* Agent info */}
|
||||
<div className="min-w-0">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium truncate">{agent.featureId}</span>
|
||||
<span className="font-medium truncate" title={agent.title || agent.featureId}>
|
||||
{agent.title || agent.featureId}
|
||||
</span>
|
||||
{agent.isAutoMode && (
|
||||
<span className="px-2 py-0.5 text-[10px] font-medium rounded-full bg-brand-500/10 text-brand-500 border border-brand-500/30">
|
||||
AUTO
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{agent.description && (
|
||||
<p
|
||||
className="text-sm text-muted-foreground truncate max-w-md"
|
||||
title={agent.description}
|
||||
>
|
||||
{agent.description}
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleNavigateToProject(agent)}
|
||||
className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
@@ -177,6 +193,15 @@ export function RunningAgentsView() {
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleViewLogs(agent)}
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<FileText className="h-3.5 w-3.5 mr-1.5" />
|
||||
View Logs
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -199,6 +224,20 @@ export function RunningAgentsView() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Agent Output Modal */}
|
||||
{selectedAgent && (
|
||||
<AgentOutputModal
|
||||
open={true}
|
||||
onClose={() => setSelectedAgent(null)}
|
||||
projectPath={selectedAgent.projectPath}
|
||||
featureDescription={
|
||||
selectedAgent.description || selectedAgent.title || selectedAgent.featureId
|
||||
}
|
||||
featureId={selectedAgent.featureId}
|
||||
featureStatus="running"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -106,6 +106,8 @@ export interface RunningAgent {
|
||||
projectPath: string;
|
||||
projectName: string;
|
||||
isAutoMode: boolean;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface RunningAgentsResult {
|
||||
@@ -2687,6 +2689,8 @@ function createMockRunningAgentsAPI(): RunningAgentsAPI {
|
||||
projectPath: '/mock/project',
|
||||
projectName: 'Mock Project',
|
||||
isAutoMode: mockAutoModeRunning,
|
||||
title: `Mock Feature Title for ${featureId}`,
|
||||
description: 'This is a mock feature description for testing purposes.',
|
||||
}));
|
||||
return {
|
||||
success: true,
|
||||
|
||||
Reference in New Issue
Block a user