mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 08:13:37 +00:00
- Added a React library for proper markdown formatting in the Agent Runner, improving the display of session messages. - Changed the hotkey for creating a new session from "W" to "N" to ensure consistency across new feature buttons. - Updated the feature list to include new entries for these enhancements, along with a detailed implementation roadmap in the project specification. These changes improve user experience by enhancing message readability and streamlining session management.
414 lines
13 KiB
TypeScript
414 lines
13 KiB
TypeScript
import { useState, useEffect, useCallback, useRef } from "react";
|
|
import type { Message, StreamEvent } from "@/types/electron";
|
|
import { useMessageQueue } from "./use-message-queue";
|
|
import type { ImageAttachment } from "@/store/app-store";
|
|
|
|
interface UseElectronAgentOptions {
|
|
sessionId: string;
|
|
workingDirectory?: string;
|
|
onToolUse?: (toolName: string, toolInput: unknown) => void;
|
|
}
|
|
|
|
interface UseElectronAgentResult {
|
|
messages: Message[];
|
|
isProcessing: boolean;
|
|
isConnected: boolean;
|
|
sendMessage: (content: string, images?: ImageAttachment[]) => Promise<void>;
|
|
stopExecution: () => Promise<void>;
|
|
clearHistory: () => Promise<void>;
|
|
error: string | null;
|
|
// Queue-related state
|
|
queuedMessages: { id: string; content: string; images?: ImageAttachment[]; timestamp: Date }[];
|
|
isQueueProcessing: boolean;
|
|
clearMessageQueue: () => void;
|
|
}
|
|
|
|
/**
|
|
* React hook for interacting with the Electron-based Claude agent
|
|
*
|
|
* This hook provides a clean interface to the agent running in the Electron
|
|
* main process, which survives Next.js restarts.
|
|
*/
|
|
export function useElectronAgent({
|
|
sessionId,
|
|
workingDirectory,
|
|
onToolUse,
|
|
}: UseElectronAgentOptions): UseElectronAgentResult {
|
|
const [messages, setMessages] = useState<Message[]>([]);
|
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
const [isConnected, setIsConnected] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const unsubscribeRef = useRef<(() => void) | null>(null);
|
|
const currentMessageRef = useRef<Message | null>(null);
|
|
|
|
// Send message directly to the agent (bypassing queue)
|
|
const sendMessageDirectly = useCallback(
|
|
async (content: string, images?: ImageAttachment[]) => {
|
|
if (!window.electronAPI?.agent) {
|
|
setError("Electron API not available");
|
|
return;
|
|
}
|
|
|
|
if (isProcessing) {
|
|
throw new Error("Agent is already processing a message");
|
|
}
|
|
|
|
setIsProcessing(true);
|
|
setError(null);
|
|
|
|
try {
|
|
console.log("[useElectronAgent] Sending message directly", {
|
|
hasImages: images && images.length > 0,
|
|
imageCount: images?.length || 0
|
|
});
|
|
|
|
// Save images to .automaker/images and get paths
|
|
let imagePaths: string[] | undefined;
|
|
if (images && images.length > 0) {
|
|
imagePaths = [];
|
|
for (const image of images) {
|
|
const result = await window.electronAPI.saveImageToTemp(
|
|
image.data,
|
|
image.filename,
|
|
image.mimeType,
|
|
workingDirectory // Pass workingDirectory as projectPath
|
|
);
|
|
if (result.success && result.path) {
|
|
imagePaths.push(result.path);
|
|
console.log("[useElectronAgent] Saved image to .automaker/images:", result.path);
|
|
} else {
|
|
console.error("[useElectronAgent] Failed to save image:", result.error);
|
|
}
|
|
}
|
|
}
|
|
|
|
const result = await window.electronAPI.agent.send(
|
|
sessionId,
|
|
content,
|
|
workingDirectory,
|
|
imagePaths
|
|
);
|
|
|
|
if (!result.success) {
|
|
setError(result.error || "Failed to send message");
|
|
setIsProcessing(false);
|
|
}
|
|
// Note: We don't set isProcessing to false here because
|
|
// it will be set by the "complete" or "error" stream event
|
|
} catch (err) {
|
|
console.error("[useElectronAgent] Failed to send message:", err);
|
|
setError(err instanceof Error ? err.message : "Failed to send message");
|
|
setIsProcessing(false);
|
|
throw err;
|
|
}
|
|
},
|
|
[sessionId, workingDirectory, isProcessing]
|
|
);
|
|
|
|
// Message queue for queuing messages when agent is busy
|
|
const {
|
|
queuedMessages,
|
|
isProcessingQueue,
|
|
addToQueue,
|
|
clearQueue,
|
|
processNext,
|
|
} = useMessageQueue({
|
|
onProcessNext: async (queuedMessage) => {
|
|
await sendMessageDirectly(queuedMessage.content, queuedMessage.images);
|
|
},
|
|
});
|
|
|
|
// Initialize connection and load history
|
|
useEffect(() => {
|
|
if (!window.electronAPI?.agent) {
|
|
setError("Electron API not available. Please run in Electron.");
|
|
return;
|
|
}
|
|
|
|
if (!sessionId) {
|
|
// No session selected - reset state
|
|
setMessages([]);
|
|
setIsConnected(false);
|
|
setIsProcessing(false);
|
|
setError(null);
|
|
return;
|
|
}
|
|
|
|
let mounted = true;
|
|
|
|
const initialize = async () => {
|
|
// Reset error state when switching sessions
|
|
setError(null);
|
|
|
|
try {
|
|
console.log("[useElectronAgent] Starting session:", sessionId);
|
|
const result = await window.electronAPI.agent.start(
|
|
sessionId,
|
|
workingDirectory
|
|
);
|
|
|
|
if (!mounted) return;
|
|
|
|
if (result.success && result.messages) {
|
|
console.log("[useElectronAgent] Loaded", result.messages.length, "messages");
|
|
setMessages(result.messages);
|
|
setIsConnected(true);
|
|
|
|
// Check if the agent is currently running for this session
|
|
const historyResult = await window.electronAPI.agent.getHistory(sessionId);
|
|
if (mounted && historyResult.success) {
|
|
const isRunning = historyResult.isRunning || false;
|
|
console.log("[useElectronAgent] Session running state:", isRunning);
|
|
setIsProcessing(isRunning);
|
|
}
|
|
} else {
|
|
setError(result.error || "Failed to start session");
|
|
setIsProcessing(false);
|
|
}
|
|
} catch (err) {
|
|
if (!mounted) return;
|
|
console.error("[useElectronAgent] Failed to initialize:", err);
|
|
setError(err instanceof Error ? err.message : "Failed to initialize");
|
|
setIsProcessing(false);
|
|
}
|
|
};
|
|
|
|
initialize();
|
|
|
|
return () => {
|
|
mounted = false;
|
|
};
|
|
}, [sessionId, workingDirectory]);
|
|
|
|
// Auto-process queue when agent finishes processing
|
|
useEffect(() => {
|
|
if (!isProcessing && !isProcessingQueue && queuedMessages.length > 0) {
|
|
console.log("[useElectronAgent] Auto-processing next queued message");
|
|
processNext();
|
|
}
|
|
}, [isProcessing, isProcessingQueue, queuedMessages.length, processNext]);
|
|
|
|
// Subscribe to streaming events
|
|
useEffect(() => {
|
|
if (!window.electronAPI?.agent) return;
|
|
if (!sessionId) return; // Don't subscribe if no session
|
|
|
|
console.log("[useElectronAgent] Subscribing to stream events for session:", sessionId);
|
|
|
|
const handleStream = (event: StreamEvent) => {
|
|
// CRITICAL: Only process events for our specific session
|
|
if (event.sessionId !== sessionId) {
|
|
console.log("[useElectronAgent] Ignoring event for different session:", event.sessionId);
|
|
return;
|
|
}
|
|
|
|
console.log("[useElectronAgent] Stream event for", sessionId, ":", event.type);
|
|
|
|
switch (event.type) {
|
|
case "message":
|
|
// User message added
|
|
setMessages((prev) => [...prev, event.message]);
|
|
break;
|
|
|
|
case "stream":
|
|
// Assistant message streaming
|
|
if (event.isComplete) {
|
|
// Final update
|
|
setMessages((prev) =>
|
|
prev.map((msg) =>
|
|
msg.id === event.messageId
|
|
? { ...msg, content: event.content }
|
|
: msg
|
|
)
|
|
);
|
|
currentMessageRef.current = null;
|
|
} else {
|
|
// Streaming update
|
|
setMessages((prev) => {
|
|
const existingIndex = prev.findIndex((m) => m.id === event.messageId);
|
|
if (existingIndex >= 0) {
|
|
// Update existing message
|
|
return prev.map((msg) =>
|
|
msg.id === event.messageId
|
|
? { ...msg, content: event.content }
|
|
: msg
|
|
);
|
|
} else {
|
|
// Create new message
|
|
const newMessage: Message = {
|
|
id: event.messageId,
|
|
role: "assistant",
|
|
content: event.content,
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
currentMessageRef.current = newMessage;
|
|
return [...prev, newMessage];
|
|
}
|
|
});
|
|
}
|
|
break;
|
|
|
|
case "tool_use":
|
|
// Tool being used
|
|
console.log("[useElectronAgent] Tool use:", event.tool.name);
|
|
onToolUse?.(event.tool.name, event.tool.input);
|
|
break;
|
|
|
|
case "complete":
|
|
// Agent finished processing for THIS session
|
|
console.log("[useElectronAgent] Processing complete for session:", sessionId);
|
|
setIsProcessing(false);
|
|
if (event.messageId) {
|
|
setMessages((prev) =>
|
|
prev.map((msg) =>
|
|
msg.id === event.messageId
|
|
? { ...msg, content: event.content }
|
|
: msg
|
|
)
|
|
);
|
|
}
|
|
break;
|
|
|
|
case "error":
|
|
// Error occurred for THIS session
|
|
console.error("[useElectronAgent] Agent error for session:", sessionId, event.error);
|
|
setIsProcessing(false);
|
|
setError(event.error);
|
|
if (event.message) {
|
|
setMessages((prev) => [...prev, event.message]);
|
|
}
|
|
break;
|
|
}
|
|
};
|
|
|
|
unsubscribeRef.current = window.electronAPI.agent.onStream(handleStream);
|
|
|
|
return () => {
|
|
if (unsubscribeRef.current) {
|
|
console.log("[useElectronAgent] Unsubscribing from stream events for session:", sessionId);
|
|
unsubscribeRef.current();
|
|
unsubscribeRef.current = null;
|
|
}
|
|
};
|
|
}, [sessionId, onToolUse]);
|
|
|
|
// Send a message to the agent
|
|
const sendMessage = useCallback(
|
|
async (content: string, images?: ImageAttachment[]) => {
|
|
if (!window.electronAPI?.agent) {
|
|
setError("Electron API not available");
|
|
return;
|
|
}
|
|
|
|
if (isProcessing) {
|
|
console.warn("[useElectronAgent] Already processing a message");
|
|
return;
|
|
}
|
|
|
|
setIsProcessing(true);
|
|
setError(null);
|
|
|
|
try {
|
|
console.log("[useElectronAgent] Sending message", {
|
|
hasImages: images && images.length > 0,
|
|
imageCount: images?.length || 0
|
|
});
|
|
|
|
// Save images to .automaker/images and get paths
|
|
let imagePaths: string[] | undefined;
|
|
if (images && images.length > 0) {
|
|
imagePaths = [];
|
|
for (const image of images) {
|
|
const result = await window.electronAPI.saveImageToTemp(
|
|
image.data,
|
|
image.filename,
|
|
image.mimeType,
|
|
workingDirectory // Pass workingDirectory as projectPath
|
|
);
|
|
if (result.success && result.path) {
|
|
imagePaths.push(result.path);
|
|
console.log("[useElectronAgent] Saved image to .automaker/images:", result.path);
|
|
} else {
|
|
console.error("[useElectronAgent] Failed to save image:", result.error);
|
|
}
|
|
}
|
|
}
|
|
|
|
const result = await window.electronAPI.agent.send(
|
|
sessionId,
|
|
content,
|
|
workingDirectory,
|
|
imagePaths
|
|
);
|
|
|
|
if (!result.success) {
|
|
setError(result.error || "Failed to send message");
|
|
setIsProcessing(false);
|
|
}
|
|
// Note: We don't set isProcessing to false here because
|
|
// it will be set by the "complete" or "error" stream event
|
|
} catch (err) {
|
|
console.error("[useElectronAgent] Failed to send message:", err);
|
|
setError(err instanceof Error ? err.message : "Failed to send message");
|
|
setIsProcessing(false);
|
|
}
|
|
},
|
|
[sessionId, workingDirectory, isProcessing]
|
|
);
|
|
|
|
// Stop current execution
|
|
const stopExecution = useCallback(async () => {
|
|
if (!window.electronAPI?.agent) {
|
|
setError("Electron API not available");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
console.log("[useElectronAgent] Stopping execution");
|
|
const result = await window.electronAPI.agent.stop(sessionId);
|
|
|
|
if (!result.success) {
|
|
setError(result.error || "Failed to stop execution");
|
|
} else {
|
|
setIsProcessing(false);
|
|
}
|
|
} catch (err) {
|
|
console.error("[useElectronAgent] Failed to stop:", err);
|
|
setError(err instanceof Error ? err.message : "Failed to stop execution");
|
|
}
|
|
}, [sessionId]);
|
|
|
|
// Clear conversation history
|
|
const clearHistory = useCallback(async () => {
|
|
if (!window.electronAPI?.agent) {
|
|
setError("Electron API not available");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
console.log("[useElectronAgent] Clearing history");
|
|
const result = await window.electronAPI.agent.clear(sessionId);
|
|
|
|
if (result.success) {
|
|
setMessages([]);
|
|
setError(null);
|
|
} else {
|
|
setError(result.error || "Failed to clear history");
|
|
}
|
|
} catch (err) {
|
|
console.error("[useElectronAgent] Failed to clear:", err);
|
|
setError(err instanceof Error ? err.message : "Failed to clear history");
|
|
}
|
|
}, [sessionId]);
|
|
|
|
return {
|
|
messages,
|
|
isProcessing,
|
|
isConnected,
|
|
sendMessage,
|
|
stopExecution,
|
|
clearHistory,
|
|
error,
|
|
};
|
|
}
|