feat: improve agent output modal with parsed log viewer

Add a parsed log viewer to the agent output modal that groups and colors
log entries by type (tool calls, phases, errors, success messages, etc.)
similar to Coolify logs. Includes:

- New log-parser.ts utility for parsing agent output into structured entries
- New LogViewer component with collapsible entries and colored badges
- Toggle between Parsed and Raw view modes in the modal header
- Type-specific colors (amber for tools, cyan for phases, red for errors)
- Expand/Collapse all buttons for better navigation
- JSON content detection and formatting within entries

Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Kacper
2025-12-09 21:01:55 +01:00
parent 000cae6737
commit 2c01587180
5 changed files with 837 additions and 5 deletions

View File

@@ -1941,3 +1941,184 @@ export async function isElementVisibleInScrollContainer(
elementBox.y + elementBox.height <= containerBox.y + containerBox.height
);
}
// ============ Log Viewer Utilities ============
/**
* Get the log viewer header element (contains type counts and expand/collapse buttons)
*/
export async function getLogViewerHeader(page: Page): Promise<Locator> {
return page.locator('[data-testid="log-viewer-header"]');
}
/**
* Check if the log viewer header is visible
*/
export async function isLogViewerHeaderVisible(page: Page): Promise<boolean> {
const header = page.locator('[data-testid="log-viewer-header"]');
return await header.isVisible().catch(() => false);
}
/**
* Get the log entries container element
*/
export async function getLogEntriesContainer(page: Page): Promise<Locator> {
return page.locator('[data-testid="log-entries-container"]');
}
/**
* Get a log entry by its type
*/
export async function getLogEntryByType(
page: Page,
type: string
): Promise<Locator> {
return page.locator(`[data-testid="log-entry-${type}"]`).first();
}
/**
* Get all log entries of a specific type
*/
export async function getAllLogEntriesByType(
page: Page,
type: string
): Promise<Locator> {
return page.locator(`[data-testid="log-entry-${type}"]`);
}
/**
* Count log entries of a specific type
*/
export async function countLogEntriesByType(
page: Page,
type: string
): Promise<number> {
const entries = page.locator(`[data-testid="log-entry-${type}"]`);
return await entries.count();
}
/**
* Get the log type count badge by type
*/
export async function getLogTypeCountBadge(
page: Page,
type: string
): Promise<Locator> {
return page.locator(`[data-testid="log-type-count-${type}"]`);
}
/**
* Check if a log type count badge is visible
*/
export async function isLogTypeCountBadgeVisible(
page: Page,
type: string
): Promise<boolean> {
const badge = page.locator(`[data-testid="log-type-count-${type}"]`);
return await badge.isVisible().catch(() => false);
}
/**
* Click the expand all button in the log viewer
*/
export async function clickLogExpandAll(page: Page): Promise<void> {
await clickElement(page, "log-expand-all");
}
/**
* Click the collapse all button in the log viewer
*/
export async function clickLogCollapseAll(page: Page): Promise<void> {
await clickElement(page, "log-collapse-all");
}
/**
* Get a log entry badge element
*/
export async function getLogEntryBadge(page: Page): Promise<Locator> {
return page.locator('[data-testid="log-entry-badge"]').first();
}
/**
* Check if any log entry badge is visible
*/
export async function isLogEntryBadgeVisible(page: Page): Promise<boolean> {
const badge = page.locator('[data-testid="log-entry-badge"]').first();
return await badge.isVisible().catch(() => false);
}
/**
* Get the view mode toggle button (parsed/raw)
*/
export async function getViewModeButton(
page: Page,
mode: "parsed" | "raw"
): Promise<Locator> {
return page.locator(`[data-testid="view-mode-${mode}"]`);
}
/**
* Click a view mode toggle button
*/
export async function clickViewModeButton(
page: Page,
mode: "parsed" | "raw"
): Promise<void> {
await clickElement(page, `view-mode-${mode}`);
}
/**
* Check if a view mode button is active (selected)
*/
export async function isViewModeActive(
page: Page,
mode: "parsed" | "raw"
): Promise<boolean> {
const button = page.locator(`[data-testid="view-mode-${mode}"]`);
const classes = await button.getAttribute("class");
return classes?.includes("text-purple-300") ?? false;
}
/**
* Set up a mock project with agent output content in the context file
*/
export async function setupMockProjectWithAgentOutput(
page: Page,
featureId: string,
outputContent: string
): Promise<void> {
await page.addInitScript(
({ featureId, outputContent }: { featureId: string; outputContent: string }) => {
const mockProject = {
id: "test-project-1",
name: "Test Project",
path: "/mock/test-project",
lastOpened: new Date().toISOString(),
};
const mockState = {
state: {
projects: [mockProject],
currentProject: mockProject,
theme: "dark",
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: 3,
},
version: 0,
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
// Set up mock file system with output content for the feature
(window as any).__mockContextFile = {
featureId,
path: `/mock/test-project/.automaker/agents-context/${featureId}.md`,
content: outputContent,
};
},
{ featureId, outputContent }
);
}