mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 21:03:08 +00:00
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) {
|
export function createIndexHandler(autoModeService: AutoModeService) {
|
||||||
return async (_req: Request, res: Response): Promise<void> => {
|
return async (_req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const runningAgents = autoModeService.getRunningAgents();
|
const runningAgents = await autoModeService.getRunningAgents();
|
||||||
const status = autoModeService.getStatus();
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -1374,18 +1374,43 @@ Format your response as a structured markdown document.`;
|
|||||||
/**
|
/**
|
||||||
* Get detailed info about all running agents
|
* Get detailed info about all running agents
|
||||||
*/
|
*/
|
||||||
getRunningAgents(): Array<{
|
async getRunningAgents(): Promise<
|
||||||
|
Array<{
|
||||||
featureId: string;
|
featureId: string;
|
||||||
projectPath: string;
|
projectPath: string;
|
||||||
projectName: string;
|
projectName: string;
|
||||||
isAutoMode: boolean;
|
isAutoMode: boolean;
|
||||||
}> {
|
title?: string;
|
||||||
return Array.from(this.runningFeatures.values()).map((rf) => ({
|
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,
|
featureId: rf.featureId,
|
||||||
projectPath: rf.projectPath,
|
projectPath: rf.projectPath,
|
||||||
projectName: path.basename(rf.projectPath),
|
projectName: path.basename(rf.projectPath),
|
||||||
isAutoMode: rf.isAutoMode,
|
isAutoMode: rf.isAutoMode,
|
||||||
}));
|
title,
|
||||||
|
description,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return agents;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
196
apps/server/tests/unit/routes/running-agents.test.ts
Normal file
196
apps/server/tests/unit/routes/running-agents.test.ts
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
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(),
|
||||||
|
getStatus: 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 { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { AutoModeService } from '@/services/auto-mode-service.js';
|
import { AutoModeService } from '@/services/auto-mode-service.js';
|
||||||
|
import type { Feature } from '@automaker/types';
|
||||||
|
|
||||||
describe('auto-mode-service.ts', () => {
|
describe('auto-mode-service.ts', () => {
|
||||||
let service: AutoModeService;
|
let service: AutoModeService;
|
||||||
@@ -66,4 +67,252 @@ describe('auto-mode-service.ts', () => {
|
|||||||
expect(runningCount).toBe(0);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
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 { getElectronAPI, RunningAgent } from '@/lib/electron';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useNavigate } from '@tanstack/react-router';
|
import { useNavigate } from '@tanstack/react-router';
|
||||||
|
import { AgentOutputModal } from './board-view/dialogs/agent-output-modal';
|
||||||
|
|
||||||
export function RunningAgentsView() {
|
export function RunningAgentsView() {
|
||||||
const [runningAgents, setRunningAgents] = useState<RunningAgent[]>([]);
|
const [runningAgents, setRunningAgents] = useState<RunningAgent[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
const [selectedAgent, setSelectedAgent] = useState<RunningAgent | null>(null);
|
||||||
const { setCurrentProject, projects } = useAppStore();
|
const { setCurrentProject, projects } = useAppStore();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@@ -94,6 +96,15 @@ export function RunningAgentsView() {
|
|||||||
[projects, setCurrentProject, navigate]
|
[projects, setCurrentProject, navigate]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleViewLogs = useCallback((agent: RunningAgent) => {
|
||||||
|
// Set the current project context for the modal
|
||||||
|
const project = projects.find((p) => p.path === agent.projectPath);
|
||||||
|
if (project) {
|
||||||
|
(window as any).__currentProject = project;
|
||||||
|
}
|
||||||
|
setSelectedAgent(agent);
|
||||||
|
}, [projects]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 flex items-center justify-center">
|
<div className="flex-1 flex items-center justify-center">
|
||||||
@@ -156,15 +167,22 @@ export function RunningAgentsView() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Agent info */}
|
{/* Agent info */}
|
||||||
<div className="min-w-0">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex items-center gap-2">
|
<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 && (
|
{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">
|
<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
|
AUTO
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{agent.description && (
|
||||||
|
<p className="text-sm text-muted-foreground truncate max-w-md" title={agent.description}>
|
||||||
|
{agent.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => handleNavigateToProject(agent)}
|
onClick={() => handleNavigateToProject(agent)}
|
||||||
className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
@@ -177,6 +195,15 @@ export function RunningAgentsView() {
|
|||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex items-center gap-2 flex-shrink-0">
|
<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
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -199,6 +226,17 @@ export function RunningAgentsView() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Agent Output Modal */}
|
||||||
|
{selectedAgent && (
|
||||||
|
<AgentOutputModal
|
||||||
|
open={true}
|
||||||
|
onClose={() => setSelectedAgent(null)}
|
||||||
|
featureDescription={selectedAgent.description || selectedAgent.title || selectedAgent.featureId}
|
||||||
|
featureId={selectedAgent.featureId}
|
||||||
|
featureStatus="running"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ export interface RunningAgent {
|
|||||||
projectPath: string;
|
projectPath: string;
|
||||||
projectName: string;
|
projectName: string;
|
||||||
isAutoMode: boolean;
|
isAutoMode: boolean;
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RunningAgentsResult {
|
export interface RunningAgentsResult {
|
||||||
@@ -2687,6 +2689,8 @@ function createMockRunningAgentsAPI(): RunningAgentsAPI {
|
|||||||
projectPath: '/mock/project',
|
projectPath: '/mock/project',
|
||||||
projectName: 'Mock Project',
|
projectName: 'Mock Project',
|
||||||
isAutoMode: mockAutoModeRunning,
|
isAutoMode: mockAutoModeRunning,
|
||||||
|
title: `Mock Feature Title for ${featureId}`,
|
||||||
|
description: 'This is a mock feature description for testing purposes.',
|
||||||
}));
|
}));
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user