refactor: move from next js to vite and tanstack router

This commit is contained in:
Kacper
2025-12-17 20:11:16 +01:00
parent 9954feafd8
commit 5136c32b68
263 changed files with 11148 additions and 10276 deletions

View File

@@ -0,0 +1,342 @@
import { useEffect, useRef, useState } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Loader2, List, FileText, GitBranch } from "lucide-react";
import { getElectronAPI } from "@/lib/electron";
import { LogViewer } from "@/components/ui/log-viewer";
import { GitDiffPanel } from "@/components/ui/git-diff-panel";
import { useAppStore } from "@/store/app-store";
import type { AutoModeEvent } from "@/types/electron";
interface AgentOutputModalProps {
open: boolean;
onClose: () => void;
featureDescription: string;
featureId: string;
/** The status of the feature - used to determine if spinner should be shown */
featureStatus?: string;
/** Called when a number key (0-9) is pressed while the modal is open */
onNumberKeyPress?: (key: string) => void;
}
type ViewMode = "parsed" | "raw" | "changes";
export function AgentOutputModal({
open,
onClose,
featureDescription,
featureId,
featureStatus,
onNumberKeyPress,
}: AgentOutputModalProps) {
const [output, setOutput] = useState<string>("");
const [isLoading, setIsLoading] = useState(true);
const [viewMode, setViewMode] = useState<ViewMode>("parsed");
const [projectPath, setProjectPath] = useState<string>("");
const scrollRef = useRef<HTMLDivElement>(null);
const autoScrollRef = useRef(true);
const projectPathRef = useRef<string>("");
const useWorktrees = useAppStore((state) => state.useWorktrees);
// Auto-scroll to bottom when output changes
useEffect(() => {
if (autoScrollRef.current && scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [output]);
// Load existing output from file
useEffect(() => {
if (!open) return;
const loadOutput = async () => {
const api = getElectronAPI();
if (!api) return;
setIsLoading(true);
try {
// Get current project path from store (we'll need to pass this)
const currentProject = (window as any).__currentProject;
if (!currentProject?.path) {
setIsLoading(false);
return;
}
projectPathRef.current = currentProject.path;
setProjectPath(currentProject.path);
// Use features API to get agent output
if (api.features) {
const result = await api.features.getAgentOutput(
currentProject.path,
featureId
);
if (result.success) {
setOutput(result.content || "");
} else {
setOutput("");
}
} else {
setOutput("");
}
} catch (error) {
console.error("Failed to load output:", error);
setOutput("");
} finally {
setIsLoading(false);
}
};
loadOutput();
}, [open, featureId]);
// Listen to auto mode events and update output
useEffect(() => {
if (!open) return;
const api = getElectronAPI();
if (!api?.autoMode) return;
const unsubscribe = api.autoMode.onEvent((event) => {
// Filter events for this specific feature only (skip events without featureId)
if ("featureId" in event && event.featureId !== featureId) {
return;
}
let newContent = "";
switch (event.type) {
case "auto_mode_progress":
newContent = event.content || "";
break;
case "auto_mode_tool":
const toolName = event.tool || "Unknown Tool";
const toolInput = event.input
? JSON.stringify(event.input, null, 2)
: "";
newContent = `\n🔧 Tool: ${toolName}\n${
toolInput ? `Input: ${toolInput}\n` : ""
}`;
break;
case "auto_mode_phase":
const phaseEmoji =
event.phase === "planning"
? "📋"
: event.phase === "action"
? "⚡"
: "✅";
newContent = `\n${phaseEmoji} ${event.message}\n`;
break;
case "auto_mode_error":
newContent = `\n❌ Error: ${event.error}\n`;
break;
case "auto_mode_ultrathink_preparation":
// Format thinking level preparation information
let prepContent = `\n🧠 Ultrathink Preparation\n`;
if (event.warnings && event.warnings.length > 0) {
prepContent += `\n⚠ Warnings:\n`;
event.warnings.forEach((warning: string) => {
prepContent += `${warning}\n`;
});
}
if (event.recommendations && event.recommendations.length > 0) {
prepContent += `\n💡 Recommendations:\n`;
event.recommendations.forEach((rec: string) => {
prepContent += `${rec}\n`;
});
}
if (event.estimatedCost !== undefined) {
prepContent += `\n💰 Estimated Cost: ~$${event.estimatedCost.toFixed(
2
)} per execution\n`;
}
if (event.estimatedTime) {
prepContent += `\n⏱ Estimated Time: ${event.estimatedTime}\n`;
}
newContent = prepContent;
break;
case "auto_mode_feature_complete":
const emoji = event.passes ? "✅" : "⚠️";
newContent = `\n${emoji} Task completed: ${event.message}\n`;
// Close the modal when the feature is verified (passes = true)
if (event.passes) {
// Small delay to show the completion message before closing
setTimeout(() => {
onClose();
}, 1500);
}
break;
}
if (newContent) {
// Only update local state - server is the single source of truth for file writes
setOutput((prev) => prev + newContent);
}
});
return () => {
unsubscribe();
};
}, [open, featureId]);
// Handle scroll to detect if user scrolled up
const handleScroll = () => {
if (!scrollRef.current) return;
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
const isAtBottom = scrollHeight - scrollTop - clientHeight < 50;
autoScrollRef.current = isAtBottom;
};
// Handle number key presses while modal is open
useEffect(() => {
if (!open || !onNumberKeyPress) return;
const handleKeyDown = (event: KeyboardEvent) => {
// Check if a number key (0-9) was pressed without modifiers
if (
!event.ctrlKey &&
!event.altKey &&
!event.metaKey &&
/^[0-9]$/.test(event.key)
) {
event.preventDefault();
onNumberKeyPress(event.key);
}
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [open, onNumberKeyPress]);
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent
className="w-[60vw] max-w-[60vw] max-h-[80vh] flex flex-col"
data-testid="agent-output-modal"
>
<DialogHeader className="flex-shrink-0">
<div className="flex items-center justify-between">
<DialogTitle className="flex items-center gap-2">
{featureStatus !== "verified" &&
featureStatus !== "waiting_approval" && (
<Loader2 className="w-5 h-5 text-primary animate-spin" />
)}
Agent Output
</DialogTitle>
<div className="flex items-center gap-1 bg-muted 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-primary/20 text-primary shadow-sm"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
data-testid="view-mode-parsed"
>
<List className="w-3.5 h-3.5" />
Logs
</button>
<button
onClick={() => setViewMode("changes")}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
viewMode === "changes"
? "bg-primary/20 text-primary shadow-sm"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
data-testid="view-mode-changes"
>
<GitBranch className="w-3.5 h-3.5" />
Changes
</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-primary/20 text-primary shadow-sm"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
}`}
data-testid="view-mode-raw"
>
<FileText className="w-3.5 h-3.5" />
Raw
</button>
</div>
</div>
<DialogDescription
className="mt-1 max-h-24 overflow-y-auto break-words"
data-testid="agent-output-description"
>
{featureDescription}
</DialogDescription>
</DialogHeader>
{viewMode === "changes" ? (
<div className="flex-1 min-h-[400px] max-h-[60vh] overflow-y-auto scrollbar-visible">
{projectPath ? (
<GitDiffPanel
projectPath={projectPath}
featureId={featureId}
compact={false}
useWorktrees={useWorktrees}
className="border-0 rounded-lg"
/>
) : (
<div className="flex items-center justify-center h-full text-muted-foreground">
<Loader2 className="w-6 h-6 animate-spin mr-2" />
Loading...
</div>
)}
</div>
) : (
<>
<div
ref={scrollRef}
onScroll={handleScroll}
className="flex-1 overflow-y-auto bg-zinc-950 rounded-lg p-4 font-mono text-xs min-h-[400px] max-h-[60vh] scrollbar-visible"
>
{isLoading && !output ? (
<div className="flex items-center justify-center h-full text-muted-foreground">
<Loader2 className="w-6 h-6 animate-spin mr-2" />
Loading output...
</div>
) : !output ? (
<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}
</div>
)}
</div>
<div className="text-xs text-muted-foreground text-center flex-shrink-0">
{autoScrollRef.current
? "Auto-scrolling enabled"
: "Scroll to bottom to enable auto-scroll"}
</div>
</>
)}
</DialogContent>
</Dialog>
);
}