Merge branch 'main' of github.com:webdevcody/automaker

This commit is contained in:
Cody Seibert
2025-12-09 15:56:46 -05:00
6 changed files with 930 additions and 6 deletions

View File

@@ -33,5 +33,23 @@
"description": "-o should actually open the select folder prompt. Right now when you click o it goes to like the overview page. That's not the correct experience I'm looking for. Also just clicking on the top left open folder icon should do the same thing of opening the system prompt so they can select a project.",
"steps": [],
"status": "verified"
},
{
"id": "feature-1765305181443-qze22t1hl",
"category": "Other",
"description": "the settings view is not allowing us to scroll to see rest of the content ",
"steps": [
"start the project",
"open Setting view",
"try to scroll "
],
"status": "verified"
},
{
"id": "feature-1765310151816-plx1pxl0z",
"category": "Kanban",
"description": "So i want to improve the look of the view of agent output modal its just plain text and im thinking to parse it better and kinda make it look like the last image of coolify logs nice colorded and somehow grouped into some types of info / debug so in our case like prompt / tool call etc",
"steps": [],
"status": "verified"
}
]

View File

@@ -0,0 +1,269 @@
"use client";
import { useState, useMemo } from "react";
import {
ChevronDown,
ChevronRight,
MessageSquare,
Wrench,
Zap,
AlertCircle,
CheckCircle2,
AlertTriangle,
Bug,
Info,
FileOutput,
} from "lucide-react";
import { cn } from "@/lib/utils";
import {
parseLogOutput,
getLogTypeColors,
type LogEntry,
type LogEntryType,
} from "@/lib/log-parser";
interface LogViewerProps {
output: string;
className?: string;
}
const getLogIcon = (type: LogEntryType) => {
switch (type) {
case "prompt":
return <MessageSquare className="w-4 h-4" />;
case "tool_call":
return <Wrench className="w-4 h-4" />;
case "tool_result":
return <FileOutput className="w-4 h-4" />;
case "phase":
return <Zap className="w-4 h-4" />;
case "error":
return <AlertCircle className="w-4 h-4" />;
case "success":
return <CheckCircle2 className="w-4 h-4" />;
case "warning":
return <AlertTriangle className="w-4 h-4" />;
case "debug":
return <Bug className="w-4 h-4" />;
default:
return <Info className="w-4 h-4" />;
}
};
interface LogEntryItemProps {
entry: LogEntry;
isExpanded: boolean;
onToggle: () => void;
}
function LogEntryItem({ entry, isExpanded, onToggle }: LogEntryItemProps) {
const colors = getLogTypeColors(entry.type);
const hasContent = entry.content.length > 100;
// Format content - detect and highlight JSON
const formattedContent = useMemo(() => {
const content = entry.content;
// Try to find and format JSON blocks
const jsonRegex = /(\{[\s\S]*?\}|\[[\s\S]*?\])/g;
let lastIndex = 0;
const parts: { type: "text" | "json"; content: string }[] = [];
let match;
while ((match = jsonRegex.exec(content)) !== null) {
// Add text before JSON
if (match.index > lastIndex) {
parts.push({
type: "text",
content: content.slice(lastIndex, match.index),
});
}
// Try to parse and format JSON
try {
const parsed = JSON.parse(match[1]);
parts.push({
type: "json",
content: JSON.stringify(parsed, null, 2),
});
} catch {
// Not valid JSON, treat as text
parts.push({ type: "text", content: match[1] });
}
lastIndex = match.index + match[1].length;
}
// Add remaining text
if (lastIndex < content.length) {
parts.push({ type: "text", content: content.slice(lastIndex) });
}
return parts.length > 0 ? parts : [{ type: "text" as const, content }];
}, [entry.content]);
return (
<div
className={cn(
"rounded-lg border-l-4 transition-all duration-200",
colors.bg,
colors.border,
"hover:brightness-110"
)}
data-testid={`log-entry-${entry.type}`}
>
<button
onClick={onToggle}
className="w-full px-3 py-2 flex items-center gap-2 text-left"
data-testid={`log-entry-toggle-${entry.id}`}
>
{hasContent ? (
isExpanded ? (
<ChevronDown className="w-4 h-4 text-zinc-400 flex-shrink-0" />
) : (
<ChevronRight className="w-4 h-4 text-zinc-400 flex-shrink-0" />
)
) : (
<span className="w-4 flex-shrink-0" />
)}
<span className={cn("flex-shrink-0", colors.icon)}>
{getLogIcon(entry.type)}
</span>
<span
className={cn(
"text-xs font-medium px-2 py-0.5 rounded-full flex-shrink-0",
colors.badge
)}
data-testid="log-entry-badge"
>
{entry.title}
</span>
<span className="text-sm text-zinc-400 truncate flex-1 ml-2">
{!isExpanded &&
entry.content.slice(0, 80) +
(entry.content.length > 80 ? "..." : "")}
</span>
</button>
{(isExpanded || !hasContent) && (
<div
className="px-4 pb-3 pt-1"
data-testid={`log-entry-content-${entry.id}`}
>
<div className="font-mono text-sm space-y-1">
{formattedContent.map((part, index) => (
<div key={index}>
{part.type === "json" ? (
<pre className="bg-zinc-900/50 rounded p-2 overflow-x-auto text-xs text-purple-300">
{part.content}
</pre>
) : (
<pre
className={cn(
"whitespace-pre-wrap break-words",
colors.text
)}
>
{part.content}
</pre>
)}
</div>
))}
</div>
</div>
)}
</div>
);
}
export function LogViewer({ output, className }: LogViewerProps) {
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
const entries = useMemo(() => parseLogOutput(output), [output]);
const toggleEntry = (id: string) => {
setExpandedIds((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
};
const expandAll = () => {
setExpandedIds(new Set(entries.map((e) => e.id)));
};
const collapseAll = () => {
setExpandedIds(new Set());
};
if (entries.length === 0) {
return null;
}
// Count entries by type
const typeCounts = entries.reduce((acc, entry) => {
acc[entry.type] = (acc[entry.type] || 0) + 1;
return acc;
}, {} as Record<string, number>);
return (
<div className={cn("flex flex-col gap-2", className)}>
{/* Header with controls */}
<div className="flex items-center justify-between px-1" data-testid="log-viewer-header">
<div className="flex items-center gap-2 flex-wrap">
{Object.entries(typeCounts).map(([type, count]) => {
const colors = getLogTypeColors(type as LogEntryType);
return (
<span
key={type}
className={cn(
"text-xs px-2 py-0.5 rounded-full",
colors.badge
)}
data-testid={`log-type-count-${type}`}
>
{type}: {count}
</span>
);
})}
</div>
<div className="flex items-center gap-1">
<button
onClick={expandAll}
className="text-xs text-zinc-400 hover:text-zinc-200 px-2 py-1 rounded hover:bg-zinc-800/50 transition-colors"
data-testid="log-expand-all"
>
Expand All
</button>
<button
onClick={collapseAll}
className="text-xs text-zinc-400 hover:text-zinc-200 px-2 py-1 rounded hover:bg-zinc-800/50 transition-colors"
data-testid="log-collapse-all"
>
Collapse All
</button>
</div>
</div>
{/* Log entries */}
<div className="space-y-2" data-testid="log-entries-container">
{entries.map((entry) => (
<LogEntryItem
key={entry.id}
entry={entry}
isExpanded={expandedIds.has(entry.id)}
onToggle={() => toggleEntry(entry.id)}
/>
))}
</div>
</div>
);
}

View File

@@ -8,8 +8,9 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Loader2 } from "lucide-react";
import { Loader2, List, FileText } from "lucide-react";
import { getElectronAPI } from "@/lib/electron";
import { LogViewer } from "@/components/ui/log-viewer";
interface AgentOutputModalProps {
open: boolean;
@@ -20,6 +21,8 @@ interface AgentOutputModalProps {
onNumberKeyPress?: (key: string) => void;
}
type ViewMode = "parsed" | "raw";
export function AgentOutputModal({
open,
onClose,
@@ -29,6 +32,7 @@ export function AgentOutputModal({
}: AgentOutputModalProps) {
const [output, setOutput] = useState<string>("");
const [isLoading, setIsLoading] = useState(true);
const [viewMode, setViewMode] = useState<ViewMode>("parsed");
const scrollRef = useRef<HTMLDivElement>(null);
const autoScrollRef = useRef(true);
const projectPathRef = useRef<string>("");
@@ -202,10 +206,38 @@ export function AgentOutputModal({
data-testid="agent-output-modal"
>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Loader2 className="w-5 h-5 text-purple-500 animate-spin" />
Agent Output
</DialogTitle>
<div className="flex items-center justify-between">
<DialogTitle className="flex items-center gap-2">
<Loader2 className="w-5 h-5 text-purple-500 animate-spin" />
Agent Output
</DialogTitle>
<div className="flex items-center gap-1 bg-zinc-900/50 rounded-lg p-1">
<button
onClick={() => setViewMode("parsed")}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
viewMode === "parsed"
? "bg-purple-500/20 text-purple-300 shadow-sm"
: "text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800/50"
}`}
data-testid="view-mode-parsed"
>
<List className="w-3.5 h-3.5" />
Parsed
</button>
<button
onClick={() => setViewMode("raw")}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
viewMode === "raw"
? "bg-purple-500/20 text-purple-300 shadow-sm"
: "text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800/50"
}`}
data-testid="view-mode-raw"
>
<FileText className="w-3.5 h-3.5" />
Raw
</button>
</div>
</div>
<DialogDescription className="mt-1">
{featureDescription}
</DialogDescription>
@@ -225,6 +257,8 @@ export function AgentOutputModal({
<div className="flex items-center justify-center h-full text-muted-foreground">
No output yet. The agent will stream output here as it works.
</div>
) : viewMode === "parsed" ? (
<LogViewer output={output} />
) : (
<div className="whitespace-pre-wrap break-words text-zinc-300">
{output}

View File

@@ -89,7 +89,7 @@ export function SettingsView() {
};
return (
<div className="flex-1 flex flex-col content-bg" data-testid="settings-view">
<div className="flex-1 flex flex-col overflow-hidden content-bg" data-testid="settings-view">
{/* Header Section */}
<div className="flex-shrink-0 border-b border-white/10 bg-zinc-950/50 backdrop-blur-md">
<div className="px-8 py-6">

341
app/src/lib/log-parser.ts Normal file
View File

@@ -0,0 +1,341 @@
/**
* Log Parser Utility
* Parses agent output into structured sections for display
*/
export type LogEntryType =
| "prompt"
| "tool_call"
| "tool_result"
| "phase"
| "error"
| "success"
| "info"
| "debug"
| "warning";
export interface LogEntry {
id: string;
type: LogEntryType;
title: string;
content: string;
timestamp?: string;
collapsed?: boolean;
metadata?: {
toolName?: string;
phase?: string;
[key: string]: string | undefined;
};
}
const generateId = () => Math.random().toString(36).substring(2, 9);
/**
* Detects the type of log entry based on content patterns
*/
function detectEntryType(content: string): LogEntryType {
const trimmed = content.trim();
// Tool calls
if (trimmed.startsWith("🔧 Tool:") || trimmed.match(/^Tool:\s*/)) {
return "tool_call";
}
// Tool results / Input
if (trimmed.startsWith("Input:") || trimmed.startsWith("Result:") || trimmed.startsWith("Output:")) {
return "tool_result";
}
// Phase changes
if (
trimmed.startsWith("📋") ||
trimmed.startsWith("⚡") ||
trimmed.startsWith("✅") ||
trimmed.match(/^(Planning|Action|Verification)/i)
) {
return "phase";
}
// Errors
if (trimmed.startsWith("❌") || trimmed.toLowerCase().includes("error:")) {
return "error";
}
// Success messages
if (
trimmed.startsWith("✅") ||
trimmed.toLowerCase().includes("success") ||
trimmed.toLowerCase().includes("completed")
) {
return "success";
}
// Warnings
if (trimmed.startsWith("⚠️") || trimmed.toLowerCase().includes("warning:")) {
return "warning";
}
// Debug info (JSON, stack traces, etc.)
if (
trimmed.startsWith("{") ||
trimmed.startsWith("[") ||
trimmed.includes("at ") ||
trimmed.match(/^\s*\d+\s*\|/)
) {
return "debug";
}
// Default to info
return "info";
}
/**
* Extracts tool name from a tool call entry
*/
function extractToolName(content: string): string | undefined {
const match = content.match(/🔧\s*Tool:\s*(\S+)/);
return match?.[1];
}
/**
* Extracts phase name from a phase entry
*/
function extractPhase(content: string): string | undefined {
if (content.includes("📋")) return "planning";
if (content.includes("⚡")) return "action";
if (content.includes("✅")) return "verification";
const match = content.match(/^(Planning|Action|Verification)/i);
return match?.[1]?.toLowerCase();
}
/**
* Generates a title for a log entry
*/
function generateTitle(type: LogEntryType, content: string): string {
switch (type) {
case "tool_call": {
const toolName = extractToolName(content);
return toolName ? `Tool Call: ${toolName}` : "Tool Call";
}
case "tool_result":
return "Tool Input/Result";
case "phase": {
const phase = extractPhase(content);
return phase ? `Phase: ${phase.charAt(0).toUpperCase() + phase.slice(1)}` : "Phase Change";
}
case "error":
return "Error";
case "success":
return "Success";
case "warning":
return "Warning";
case "debug":
return "Debug Info";
case "prompt":
return "Prompt";
default:
return "Info";
}
}
/**
* Parses raw log output into structured entries
*/
export function parseLogOutput(rawOutput: string): LogEntry[] {
if (!rawOutput || !rawOutput.trim()) {
return [];
}
const entries: LogEntry[] = [];
const lines = rawOutput.split("\n");
let currentEntry: LogEntry | null = null;
let currentContent: string[] = [];
const finalizeEntry = () => {
if (currentEntry && currentContent.length > 0) {
currentEntry.content = currentContent.join("\n").trim();
if (currentEntry.content) {
entries.push(currentEntry);
}
}
currentContent = [];
};
for (const line of lines) {
const trimmedLine = line.trim();
// Skip empty lines at the beginning
if (!trimmedLine && !currentEntry) {
continue;
}
// Detect if this line starts a new entry
const lineType = detectEntryType(trimmedLine);
const isNewEntry =
trimmedLine.startsWith("🔧") ||
trimmedLine.startsWith("📋") ||
trimmedLine.startsWith("⚡") ||
trimmedLine.startsWith("✅") ||
trimmedLine.startsWith("❌") ||
trimmedLine.startsWith("⚠️") ||
(trimmedLine.startsWith("Input:") && currentEntry?.type === "tool_call");
if (isNewEntry) {
// Finalize previous entry
finalizeEntry();
// Start new entry
currentEntry = {
id: generateId(),
type: lineType,
title: generateTitle(lineType, trimmedLine),
content: "",
metadata: {
toolName: extractToolName(trimmedLine),
phase: extractPhase(trimmedLine),
},
};
currentContent.push(trimmedLine);
} else if (currentEntry) {
// Continue current entry
currentContent.push(line);
} else {
// No current entry, create a default info entry
currentEntry = {
id: generateId(),
type: "info",
title: "Info",
content: "",
};
currentContent.push(line);
}
}
// Finalize last entry
finalizeEntry();
// Merge consecutive entries of the same type if they're both debug or info
const mergedEntries = mergeConsecutiveEntries(entries);
return mergedEntries;
}
/**
* Merges consecutive entries of the same type for cleaner display
*/
function mergeConsecutiveEntries(entries: LogEntry[]): LogEntry[] {
if (entries.length <= 1) return entries;
const merged: LogEntry[] = [];
let current: LogEntry | null = null;
for (const entry of entries) {
if (
current &&
(current.type === "debug" || current.type === "info") &&
current.type === entry.type
) {
// Merge into current
current.content += "\n\n" + entry.content;
} else {
if (current) {
merged.push(current);
}
current = { ...entry };
}
}
if (current) {
merged.push(current);
}
return merged;
}
/**
* Gets the color classes for a log entry type
*/
export function getLogTypeColors(type: LogEntryType): {
bg: string;
border: string;
text: string;
icon: string;
badge: string;
} {
switch (type) {
case "prompt":
return {
bg: "bg-blue-500/10",
border: "border-l-blue-500",
text: "text-blue-300",
icon: "text-blue-400",
badge: "bg-blue-500/20 text-blue-300",
};
case "tool_call":
return {
bg: "bg-amber-500/10",
border: "border-l-amber-500",
text: "text-amber-300",
icon: "text-amber-400",
badge: "bg-amber-500/20 text-amber-300",
};
case "tool_result":
return {
bg: "bg-slate-500/10",
border: "border-l-slate-400",
text: "text-slate-300",
icon: "text-slate-400",
badge: "bg-slate-500/20 text-slate-300",
};
case "phase":
return {
bg: "bg-cyan-500/10",
border: "border-l-cyan-500",
text: "text-cyan-300",
icon: "text-cyan-400",
badge: "bg-cyan-500/20 text-cyan-300",
};
case "error":
return {
bg: "bg-red-500/10",
border: "border-l-red-500",
text: "text-red-300",
icon: "text-red-400",
badge: "bg-red-500/20 text-red-300",
};
case "success":
return {
bg: "bg-emerald-500/10",
border: "border-l-emerald-500",
text: "text-emerald-300",
icon: "text-emerald-400",
badge: "bg-emerald-500/20 text-emerald-300",
};
case "warning":
return {
bg: "bg-orange-500/10",
border: "border-l-orange-500",
text: "text-orange-300",
icon: "text-orange-400",
badge: "bg-orange-500/20 text-orange-300",
};
case "debug":
return {
bg: "bg-purple-500/10",
border: "border-l-purple-500",
text: "text-purple-300",
icon: "text-purple-400",
badge: "bg-purple-500/20 text-purple-300",
};
default:
return {
bg: "bg-zinc-500/10",
border: "border-l-zinc-500",
text: "text-zinc-300",
icon: "text-zinc-400",
badge: "bg-zinc-500/20 text-zinc-300",
};
}
}

View File

@@ -1860,3 +1860,265 @@ export async function isDropOverlayVisible(page: Page): Promise<boolean> {
const overlay = page.locator('[data-testid="drop-overlay"]');
return await overlay.isVisible().catch(() => false);
}
/**
* Navigate to the settings view
*/
export async function navigateToSettings(page: Page): Promise<void> {
await page.goto("/");
// Wait for the page to load
await page.waitForLoadState("networkidle");
// Click on the Settings button in the sidebar
const settingsButton = page.locator('[data-testid="settings-button"]');
if (await settingsButton.isVisible().catch(() => false)) {
await settingsButton.click();
}
// Wait for the settings view to be visible
await waitForElement(page, "settings-view", { timeout: 10000 });
}
/**
* Get the settings view scrollable content area
*/
export async function getSettingsContentArea(page: Page): Promise<Locator> {
return page.locator('[data-testid="settings-view"] .overflow-y-auto');
}
/**
* Check if an element is scrollable (has scrollable content)
*/
export async function isElementScrollable(locator: Locator): Promise<boolean> {
const scrollInfo = await locator.evaluate((el) => {
return {
scrollHeight: el.scrollHeight,
clientHeight: el.clientHeight,
isScrollable: el.scrollHeight > el.clientHeight,
};
});
return scrollInfo.isScrollable;
}
/**
* Scroll an element to the bottom
*/
export async function scrollToBottom(locator: Locator): Promise<void> {
await locator.evaluate((el) => {
el.scrollTop = el.scrollHeight;
});
}
/**
* Get the scroll position of an element
*/
export async function getScrollPosition(locator: Locator): Promise<{ scrollTop: number; scrollHeight: number; clientHeight: number }> {
return await locator.evaluate((el) => ({
scrollTop: el.scrollTop,
scrollHeight: el.scrollHeight,
clientHeight: el.clientHeight,
}));
}
/**
* Check if an element is visible within a scrollable container
*/
export async function isElementVisibleInScrollContainer(
element: Locator,
container: Locator
): Promise<boolean> {
const elementBox = await element.boundingBox();
const containerBox = await container.boundingBox();
if (!elementBox || !containerBox) {
return false;
}
// Check if element is within the visible area of the container
return (
elementBox.y >= containerBox.y &&
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 }
);
}