mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-31 06:42:03 +00:00
Merge main into kanban-scaling
Resolves merge conflicts while preserving: - Kanban scaling improvements (window sizing, bounce prevention, debouncing) - Main's sidebar refactoring into hooks - Main's openInEditor functionality for VS Code integration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
9
apps/ui/src/hooks/index.ts
Normal file
9
apps/ui/src/hooks/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export { useAutoMode } from './use-auto-mode';
|
||||
export { useBoardBackgroundSettings } from './use-board-background-settings';
|
||||
export { useElectronAgent } from './use-electron-agent';
|
||||
export { useKeyboardShortcuts } from './use-keyboard-shortcuts';
|
||||
export { useMessageQueue } from './use-message-queue';
|
||||
export { useResponsiveKanban } from './use-responsive-kanban';
|
||||
export { useScrollTracking } from './use-scroll-tracking';
|
||||
export { useSettingsMigration } from './use-settings-migration';
|
||||
export { useWindowState } from './use-window-state';
|
||||
@@ -1,12 +1,14 @@
|
||||
import { useEffect, useCallback, useMemo } from "react";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
import type { AutoModeEvent } from "@/types/electron";
|
||||
import { useEffect, useCallback, useMemo } from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import type { AutoModeEvent } from '@/types/electron';
|
||||
|
||||
// Type guard for plan_approval_required event
|
||||
function isPlanApprovalEvent(event: AutoModeEvent): event is Extract<AutoModeEvent, { type: "plan_approval_required" }> {
|
||||
return event.type === "plan_approval_required";
|
||||
function isPlanApprovalEvent(
|
||||
event: AutoModeEvent
|
||||
): event is Extract<AutoModeEvent, { type: 'plan_approval_required' }> {
|
||||
return event.type === 'plan_approval_required';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,9 +52,7 @@ export function useAutoMode() {
|
||||
const projectId = currentProject?.id;
|
||||
const projectAutoModeState = useMemo(() => {
|
||||
if (!projectId) return { isRunning: false, runningTasks: [] };
|
||||
return (
|
||||
autoModeByProject[projectId] || { isRunning: false, runningTasks: [] }
|
||||
);
|
||||
return autoModeByProject[projectId] || { isRunning: false, runningTasks: [] };
|
||||
}, [autoModeByProject, projectId]);
|
||||
|
||||
const isAutoModeRunning = projectAutoModeState.isRunning;
|
||||
@@ -67,15 +67,15 @@ export function useAutoMode() {
|
||||
if (!api?.autoMode) return;
|
||||
|
||||
const unsubscribe = api.autoMode.onEvent((event: AutoModeEvent) => {
|
||||
console.log("[AutoMode Event]", event);
|
||||
console.log('[AutoMode Event]', event);
|
||||
|
||||
// Events include projectPath from backend - use it to look up project ID
|
||||
// Fall back to current projectId if not provided in event
|
||||
let eventProjectId: string | undefined;
|
||||
if ("projectPath" in event && event.projectPath) {
|
||||
if ('projectPath' in event && event.projectPath) {
|
||||
eventProjectId = getProjectIdFromPath(event.projectPath);
|
||||
}
|
||||
if (!eventProjectId && "projectId" in event && event.projectId) {
|
||||
if (!eventProjectId && 'projectId' in event && event.projectId) {
|
||||
eventProjectId = event.projectId;
|
||||
}
|
||||
if (!eventProjectId) {
|
||||
@@ -84,52 +84,44 @@ export function useAutoMode() {
|
||||
|
||||
// Skip event if we couldn't determine the project
|
||||
if (!eventProjectId) {
|
||||
console.warn(
|
||||
"[AutoMode] Could not determine project for event:",
|
||||
event
|
||||
);
|
||||
console.warn('[AutoMode] Could not determine project for event:', event);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.type) {
|
||||
case "auto_mode_feature_start":
|
||||
case 'auto_mode_feature_start':
|
||||
if (event.featureId) {
|
||||
addRunningTask(eventProjectId, event.featureId);
|
||||
addAutoModeActivity({
|
||||
featureId: event.featureId,
|
||||
type: "start",
|
||||
type: 'start',
|
||||
message: `Started working on feature`,
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "auto_mode_feature_complete":
|
||||
case 'auto_mode_feature_complete':
|
||||
// Feature completed - remove from running tasks and UI will reload features on its own
|
||||
if (event.featureId) {
|
||||
console.log(
|
||||
"[AutoMode] Feature completed:",
|
||||
event.featureId,
|
||||
"passes:",
|
||||
event.passes
|
||||
);
|
||||
console.log('[AutoMode] Feature completed:', event.featureId, 'passes:', event.passes);
|
||||
removeRunningTask(eventProjectId, event.featureId);
|
||||
addAutoModeActivity({
|
||||
featureId: event.featureId,
|
||||
type: "complete",
|
||||
type: 'complete',
|
||||
message: event.passes
|
||||
? "Feature completed successfully"
|
||||
: "Feature completed with failures",
|
||||
? 'Feature completed successfully'
|
||||
: 'Feature completed with failures',
|
||||
passes: event.passes,
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "auto_mode_error":
|
||||
case 'auto_mode_error':
|
||||
if (event.featureId && event.error) {
|
||||
// Check if this is a user-initiated cancellation or abort (not a real error)
|
||||
if (event.errorType === "cancellation" || event.errorType === "abort") {
|
||||
if (event.errorType === 'cancellation' || event.errorType === 'abort') {
|
||||
// User cancelled/aborted the feature - just log as info, not an error
|
||||
console.log("[AutoMode] Feature cancelled/aborted:", event.error);
|
||||
console.log('[AutoMode] Feature cancelled/aborted:', event.error);
|
||||
// Remove from running tasks
|
||||
if (eventProjectId) {
|
||||
removeRunningTask(eventProjectId, event.featureId);
|
||||
@@ -138,13 +130,13 @@ export function useAutoMode() {
|
||||
}
|
||||
|
||||
// Real error - log and show to user
|
||||
console.error("[AutoMode Error]", event.error);
|
||||
console.error('[AutoMode Error]', event.error);
|
||||
|
||||
// Check for authentication errors and provide a more helpful message
|
||||
const isAuthError =
|
||||
event.errorType === "authentication" ||
|
||||
event.error.includes("Authentication failed") ||
|
||||
event.error.includes("Invalid API key");
|
||||
event.errorType === 'authentication' ||
|
||||
event.error.includes('Authentication failed') ||
|
||||
event.error.includes('Invalid API key');
|
||||
|
||||
const errorMessage = isAuthError
|
||||
? `Authentication failed: Please check your API key in Settings or run 'claude login' in terminal to re-authenticate.`
|
||||
@@ -152,9 +144,9 @@ export function useAutoMode() {
|
||||
|
||||
addAutoModeActivity({
|
||||
featureId: event.featureId,
|
||||
type: "error",
|
||||
type: 'error',
|
||||
message: errorMessage,
|
||||
errorType: isAuthError ? "authentication" : "execution",
|
||||
errorType: isAuthError ? 'authentication' : 'execution',
|
||||
});
|
||||
|
||||
// Remove the task from running since it failed
|
||||
@@ -164,35 +156,33 @@ export function useAutoMode() {
|
||||
}
|
||||
break;
|
||||
|
||||
case "auto_mode_progress":
|
||||
case 'auto_mode_progress':
|
||||
// Log progress updates (throttle to avoid spam)
|
||||
if (event.featureId && event.content && event.content.length > 10) {
|
||||
addAutoModeActivity({
|
||||
featureId: event.featureId,
|
||||
type: "progress",
|
||||
type: 'progress',
|
||||
message: event.content.substring(0, 200), // Limit message length
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "auto_mode_tool":
|
||||
case 'auto_mode_tool':
|
||||
// Log tool usage
|
||||
if (event.featureId && event.tool) {
|
||||
addAutoModeActivity({
|
||||
featureId: event.featureId,
|
||||
type: "tool",
|
||||
type: 'tool',
|
||||
message: `Using tool: ${event.tool}`,
|
||||
tool: event.tool,
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "auto_mode_phase":
|
||||
case 'auto_mode_phase':
|
||||
// Log phase transitions (Planning, Action, Verification)
|
||||
if (event.featureId && event.phase && event.message) {
|
||||
console.log(
|
||||
`[AutoMode] Phase: ${event.phase} for ${event.featureId}`
|
||||
);
|
||||
console.log(`[AutoMode] Phase: ${event.phase} for ${event.featureId}`);
|
||||
addAutoModeActivity({
|
||||
featureId: event.featureId,
|
||||
type: event.phase,
|
||||
@@ -202,120 +192,124 @@ export function useAutoMode() {
|
||||
}
|
||||
break;
|
||||
|
||||
case "plan_approval_required":
|
||||
case 'plan_approval_required':
|
||||
// Plan requires user approval before proceeding
|
||||
if (isPlanApprovalEvent(event)) {
|
||||
console.log(
|
||||
`[AutoMode] Plan approval required for ${event.featureId}`
|
||||
);
|
||||
console.log(`[AutoMode] Plan approval required for ${event.featureId}`);
|
||||
setPendingPlanApproval({
|
||||
featureId: event.featureId,
|
||||
projectPath: event.projectPath || currentProject?.path || "",
|
||||
projectPath: event.projectPath || currentProject?.path || '',
|
||||
planContent: event.planContent,
|
||||
planningMode: event.planningMode,
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "planning_started":
|
||||
case 'planning_started':
|
||||
// Log when planning phase begins
|
||||
if (event.featureId && event.mode && event.message) {
|
||||
console.log(
|
||||
`[AutoMode] Planning started (${event.mode}) for ${event.featureId}`
|
||||
);
|
||||
console.log(`[AutoMode] Planning started (${event.mode}) for ${event.featureId}`);
|
||||
addAutoModeActivity({
|
||||
featureId: event.featureId,
|
||||
type: "planning",
|
||||
type: 'planning',
|
||||
message: event.message,
|
||||
phase: "planning",
|
||||
phase: 'planning',
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "plan_approved":
|
||||
case 'plan_approved':
|
||||
// Log when plan is approved by user
|
||||
if (event.featureId) {
|
||||
console.log(`[AutoMode] Plan approved for ${event.featureId}`);
|
||||
addAutoModeActivity({
|
||||
featureId: event.featureId,
|
||||
type: "action",
|
||||
type: 'action',
|
||||
message: event.hasEdits
|
||||
? "Plan approved with edits, starting implementation..."
|
||||
: "Plan approved, starting implementation...",
|
||||
phase: "action",
|
||||
? 'Plan approved with edits, starting implementation...'
|
||||
: 'Plan approved, starting implementation...',
|
||||
phase: 'action',
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "plan_auto_approved":
|
||||
case 'plan_auto_approved':
|
||||
// Log when plan is auto-approved (requirePlanApproval=false)
|
||||
if (event.featureId) {
|
||||
console.log(`[AutoMode] Plan auto-approved for ${event.featureId}`);
|
||||
addAutoModeActivity({
|
||||
featureId: event.featureId,
|
||||
type: "action",
|
||||
message: "Plan auto-approved, starting implementation...",
|
||||
phase: "action",
|
||||
type: 'action',
|
||||
message: 'Plan auto-approved, starting implementation...',
|
||||
phase: 'action',
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "plan_revision_requested":
|
||||
case 'plan_revision_requested':
|
||||
// Log when user requests plan revision with feedback
|
||||
if (event.featureId) {
|
||||
const revisionEvent = event as Extract<AutoModeEvent, { type: "plan_revision_requested" }>;
|
||||
console.log(`[AutoMode] Plan revision requested for ${event.featureId} (v${revisionEvent.planVersion})`);
|
||||
const revisionEvent = event as Extract<
|
||||
AutoModeEvent,
|
||||
{ type: 'plan_revision_requested' }
|
||||
>;
|
||||
console.log(
|
||||
`[AutoMode] Plan revision requested for ${event.featureId} (v${revisionEvent.planVersion})`
|
||||
);
|
||||
addAutoModeActivity({
|
||||
featureId: event.featureId,
|
||||
type: "planning",
|
||||
type: 'planning',
|
||||
message: `Revising plan based on feedback (v${revisionEvent.planVersion})...`,
|
||||
phase: "planning",
|
||||
phase: 'planning',
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "auto_mode_task_started":
|
||||
case 'auto_mode_task_started':
|
||||
// Task started - show which task is being worked on
|
||||
if (event.featureId && "taskId" in event && "taskDescription" in event) {
|
||||
const taskEvent = event as Extract<AutoModeEvent, { type: "auto_mode_task_started" }>;
|
||||
if (event.featureId && 'taskId' in event && 'taskDescription' in event) {
|
||||
const taskEvent = event as Extract<AutoModeEvent, { type: 'auto_mode_task_started' }>;
|
||||
console.log(
|
||||
`[AutoMode] Task ${taskEvent.taskId} started for ${event.featureId}: ${taskEvent.taskDescription}`
|
||||
);
|
||||
addAutoModeActivity({
|
||||
featureId: event.featureId,
|
||||
type: "progress",
|
||||
type: 'progress',
|
||||
message: `▶ Starting ${taskEvent.taskId}: ${taskEvent.taskDescription}`,
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "auto_mode_task_complete":
|
||||
case 'auto_mode_task_complete':
|
||||
// Task completed - show progress
|
||||
if (event.featureId && "taskId" in event) {
|
||||
const taskEvent = event as Extract<AutoModeEvent, { type: "auto_mode_task_complete" }>;
|
||||
if (event.featureId && 'taskId' in event) {
|
||||
const taskEvent = event as Extract<AutoModeEvent, { type: 'auto_mode_task_complete' }>;
|
||||
console.log(
|
||||
`[AutoMode] Task ${taskEvent.taskId} completed for ${event.featureId} (${taskEvent.tasksCompleted}/${taskEvent.tasksTotal})`
|
||||
);
|
||||
addAutoModeActivity({
|
||||
featureId: event.featureId,
|
||||
type: "progress",
|
||||
type: 'progress',
|
||||
message: `✓ ${taskEvent.taskId} done (${taskEvent.tasksCompleted}/${taskEvent.tasksTotal})`,
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "auto_mode_phase_complete":
|
||||
case 'auto_mode_phase_complete':
|
||||
// Phase completed (for full mode with phased tasks)
|
||||
if (event.featureId && "phaseNumber" in event) {
|
||||
const phaseEvent = event as Extract<AutoModeEvent, { type: "auto_mode_phase_complete" }>;
|
||||
if (event.featureId && 'phaseNumber' in event) {
|
||||
const phaseEvent = event as Extract<
|
||||
AutoModeEvent,
|
||||
{ type: 'auto_mode_phase_complete' }
|
||||
>;
|
||||
console.log(
|
||||
`[AutoMode] Phase ${phaseEvent.phaseNumber} completed for ${event.featureId}`
|
||||
);
|
||||
addAutoModeActivity({
|
||||
featureId: event.featureId,
|
||||
type: "action",
|
||||
type: 'action',
|
||||
message: `Phase ${phaseEvent.phaseNumber} completed`,
|
||||
phase: "action",
|
||||
phase: 'action',
|
||||
});
|
||||
}
|
||||
break;
|
||||
@@ -336,7 +330,7 @@ export function useAutoMode() {
|
||||
// Start auto mode - UI only, feature pickup is handled in board-view.tsx
|
||||
const start = useCallback(() => {
|
||||
if (!currentProject) {
|
||||
console.error("No project selected");
|
||||
console.error('No project selected');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -347,7 +341,7 @@ export function useAutoMode() {
|
||||
// Stop auto mode - UI only, running tasks continue until natural completion
|
||||
const stop = useCallback(() => {
|
||||
if (!currentProject) {
|
||||
console.error("No project selected");
|
||||
console.error('No project selected');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -356,40 +350,40 @@ export function useAutoMode() {
|
||||
// Stopping auto mode only turns off the toggle to prevent new features
|
||||
// from being picked up. Running tasks will complete naturally and be
|
||||
// removed via the auto_mode_feature_complete event.
|
||||
console.log("[AutoMode] Stopped - running tasks will continue");
|
||||
console.log('[AutoMode] Stopped - running tasks will continue');
|
||||
}, [currentProject, setAutoModeRunning]);
|
||||
|
||||
// Stop a specific feature
|
||||
const stopFeature = useCallback(
|
||||
async (featureId: string) => {
|
||||
if (!currentProject) {
|
||||
console.error("No project selected");
|
||||
console.error('No project selected');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode?.stopFeature) {
|
||||
throw new Error("Stop feature API not available");
|
||||
throw new Error('Stop feature API not available');
|
||||
}
|
||||
|
||||
const result = await api.autoMode.stopFeature(featureId);
|
||||
|
||||
if (result.success) {
|
||||
removeRunningTask(currentProject.id, featureId);
|
||||
console.log("[AutoMode] Feature stopped successfully:", featureId);
|
||||
console.log('[AutoMode] Feature stopped successfully:', featureId);
|
||||
addAutoModeActivity({
|
||||
featureId,
|
||||
type: "complete",
|
||||
message: "Feature stopped by user",
|
||||
type: 'complete',
|
||||
message: 'Feature stopped by user',
|
||||
passes: false,
|
||||
});
|
||||
} else {
|
||||
console.error("[AutoMode] Failed to stop feature:", result.error);
|
||||
throw new Error(result.error || "Failed to stop feature");
|
||||
console.error('[AutoMode] Failed to stop feature:', result.error);
|
||||
throw new Error(result.error || 'Failed to stop feature');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[AutoMode] Error stopping feature:", error);
|
||||
console.error('[AutoMode] Error stopping feature:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback } from "react";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
import { getHttpApiClient } from "@/lib/http-api-client";
|
||||
import { toast } from "sonner";
|
||||
import { useCallback } from 'react';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
/**
|
||||
* Hook for managing board background settings with automatic persistence to server
|
||||
@@ -14,20 +14,17 @@ export function useBoardBackgroundSettings() {
|
||||
const persistSettings = useCallback(
|
||||
async (projectPath: string, settingsToUpdate: Record<string, unknown>) => {
|
||||
try {
|
||||
const result = await httpClient.settings.updateProject(
|
||||
projectPath,
|
||||
{
|
||||
boardBackground: settingsToUpdate,
|
||||
}
|
||||
);
|
||||
const result = await httpClient.settings.updateProject(projectPath, {
|
||||
boardBackground: settingsToUpdate,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
console.error("Failed to persist settings:", result.error);
|
||||
toast.error("Failed to save settings");
|
||||
console.error('Failed to persist settings:', result.error);
|
||||
toast.error('Failed to save settings');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to persist settings:", error);
|
||||
toast.error("Failed to save settings");
|
||||
console.error('Failed to persist settings:', error);
|
||||
toast.error('Failed to save settings');
|
||||
}
|
||||
},
|
||||
[httpClient]
|
||||
@@ -37,16 +34,18 @@ export function useBoardBackgroundSettings() {
|
||||
const getCurrentSettings = useCallback(
|
||||
(projectPath: string) => {
|
||||
const current = store.boardBackgroundByProject[projectPath];
|
||||
return current || {
|
||||
imagePath: null,
|
||||
cardOpacity: 100,
|
||||
columnOpacity: 100,
|
||||
columnBorderEnabled: true,
|
||||
cardGlassmorphism: true,
|
||||
cardBorderEnabled: true,
|
||||
cardBorderOpacity: 100,
|
||||
hideScrollbar: false,
|
||||
};
|
||||
return (
|
||||
current || {
|
||||
imagePath: null,
|
||||
cardOpacity: 100,
|
||||
columnOpacity: 100,
|
||||
columnBorderEnabled: true,
|
||||
cardGlassmorphism: true,
|
||||
cardBorderEnabled: true,
|
||||
cardBorderOpacity: 100,
|
||||
hideScrollbar: false,
|
||||
}
|
||||
);
|
||||
},
|
||||
[store.boardBackgroundByProject]
|
||||
);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
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";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import type { Message, StreamEvent } from '@/types/electron';
|
||||
import { useMessageQueue } from './use-message-queue';
|
||||
import type { ImageAttachment, TextFileAttachment } from '@/store/app-store';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { sanitizeFilename } from '@/lib/image-utils';
|
||||
|
||||
interface UseElectronAgentOptions {
|
||||
sessionId: string;
|
||||
@@ -15,12 +16,22 @@ interface UseElectronAgentResult {
|
||||
messages: Message[];
|
||||
isProcessing: boolean;
|
||||
isConnected: boolean;
|
||||
sendMessage: (content: string, images?: ImageAttachment[]) => Promise<void>;
|
||||
sendMessage: (
|
||||
content: string,
|
||||
images?: ImageAttachment[],
|
||||
textFiles?: TextFileAttachment[]
|
||||
) => Promise<void>;
|
||||
stopExecution: () => Promise<void>;
|
||||
clearHistory: () => Promise<void>;
|
||||
error: string | null;
|
||||
// Queue-related state
|
||||
queuedMessages: { id: string; content: string; images?: ImageAttachment[]; timestamp: Date }[];
|
||||
queuedMessages: {
|
||||
id: string;
|
||||
content: string;
|
||||
images?: ImageAttachment[];
|
||||
textFiles?: TextFileAttachment[];
|
||||
timestamp: Date;
|
||||
}[];
|
||||
isQueueProcessing: boolean;
|
||||
clearMessageQueue: () => void;
|
||||
}
|
||||
@@ -46,26 +57,38 @@ export function useElectronAgent({
|
||||
|
||||
// Send message directly to the agent (bypassing queue)
|
||||
const sendMessageDirectly = useCallback(
|
||||
async (content: string, images?: ImageAttachment[]) => {
|
||||
async (content: string, images?: ImageAttachment[], textFiles?: TextFileAttachment[]) => {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.agent) {
|
||||
setError("API not available");
|
||||
setError('API not available');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isProcessing) {
|
||||
throw new Error("Agent is already processing a message");
|
||||
throw new Error('Agent is already processing a message');
|
||||
}
|
||||
|
||||
setIsProcessing(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
console.log("[useElectronAgent] Sending message directly", {
|
||||
console.log('[useElectronAgent] Sending message directly', {
|
||||
hasImages: images && images.length > 0,
|
||||
imageCount: images?.length || 0
|
||||
imageCount: images?.length || 0,
|
||||
hasTextFiles: textFiles && textFiles.length > 0,
|
||||
textFileCount: textFiles?.length || 0,
|
||||
});
|
||||
|
||||
// Build message content with text file context prepended
|
||||
let messageContent = content;
|
||||
if (textFiles && textFiles.length > 0) {
|
||||
const contextParts = textFiles.map((file) => {
|
||||
return `<file name="${file.filename}">\n${file.content}\n</file>`;
|
||||
});
|
||||
const contextBlock = `Here are some files for context:\n\n${contextParts.join('\n\n')}\n\n`;
|
||||
messageContent = contextBlock + content;
|
||||
}
|
||||
|
||||
// Save images to .automaker/images and get paths
|
||||
let imagePaths: string[] | undefined;
|
||||
if (images && images.length > 0 && api.saveImageToTemp) {
|
||||
@@ -73,36 +96,36 @@ export function useElectronAgent({
|
||||
for (const image of images) {
|
||||
const result = await api.saveImageToTemp(
|
||||
image.data,
|
||||
image.filename,
|
||||
sanitizeFilename(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);
|
||||
console.log('[useElectronAgent] Saved image to .automaker/images:', result.path);
|
||||
} else {
|
||||
console.error("[useElectronAgent] Failed to save image:", result.error);
|
||||
console.error('[useElectronAgent] Failed to save image:', result.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = await api.agent!.send(
|
||||
sessionId,
|
||||
content,
|
||||
messageContent,
|
||||
workingDirectory,
|
||||
imagePaths,
|
||||
model
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
setError(result.error || "Failed to send message");
|
||||
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");
|
||||
console.error('[useElectronAgent] Failed to send message:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to send message');
|
||||
setIsProcessing(false);
|
||||
throw err;
|
||||
}
|
||||
@@ -111,23 +134,22 @@ export function useElectronAgent({
|
||||
);
|
||||
|
||||
// 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);
|
||||
},
|
||||
});
|
||||
const { queuedMessages, isProcessingQueue, addToQueue, clearQueue, processNext } =
|
||||
useMessageQueue({
|
||||
onProcessNext: async (queuedMessage) => {
|
||||
await sendMessageDirectly(
|
||||
queuedMessage.content,
|
||||
queuedMessage.images,
|
||||
queuedMessage.textFiles
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// Initialize connection and load history
|
||||
useEffect(() => {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.agent) {
|
||||
setError("API not available.");
|
||||
setError('API not available.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -147,16 +169,13 @@ export function useElectronAgent({
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
console.log("[useElectronAgent] Starting session:", sessionId);
|
||||
const result = await api.agent!.start(
|
||||
sessionId,
|
||||
workingDirectory
|
||||
);
|
||||
console.log('[useElectronAgent] Starting session:', sessionId);
|
||||
const result = await api.agent!.start(sessionId, workingDirectory);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (result.success && result.messages) {
|
||||
console.log("[useElectronAgent] Loaded", result.messages.length, "messages");
|
||||
console.log('[useElectronAgent] Loaded', result.messages.length, 'messages');
|
||||
setMessages(result.messages);
|
||||
setIsConnected(true);
|
||||
|
||||
@@ -164,17 +183,17 @@ export function useElectronAgent({
|
||||
const historyResult = await api.agent!.getHistory(sessionId);
|
||||
if (mounted && historyResult.success) {
|
||||
const isRunning = historyResult.isRunning || false;
|
||||
console.log("[useElectronAgent] Session running state:", isRunning);
|
||||
console.log('[useElectronAgent] Session running state:', isRunning);
|
||||
setIsProcessing(isRunning);
|
||||
}
|
||||
} else {
|
||||
setError(result.error || "Failed to start session");
|
||||
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");
|
||||
console.error('[useElectronAgent] Failed to initialize:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to initialize');
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
@@ -189,7 +208,7 @@ export function useElectronAgent({
|
||||
// Auto-process queue when agent finishes processing
|
||||
useEffect(() => {
|
||||
if (!isProcessing && !isProcessingQueue && queuedMessages.length > 0) {
|
||||
console.log("[useElectronAgent] Auto-processing next queued message");
|
||||
console.log('[useElectronAgent] Auto-processing next queued message');
|
||||
processNext();
|
||||
}
|
||||
}, [isProcessing, isProcessingQueue, queuedMessages.length, processNext]);
|
||||
@@ -200,32 +219,30 @@ export function useElectronAgent({
|
||||
if (!api?.agent) return;
|
||||
if (!sessionId) return; // Don't subscribe if no session
|
||||
|
||||
console.log("[useElectronAgent] Subscribing to stream events for session:", sessionId);
|
||||
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);
|
||||
console.log('[useElectronAgent] Ignoring event for different session:', event.sessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[useElectronAgent] Stream event for", sessionId, ":", event.type);
|
||||
console.log('[useElectronAgent] Stream event for', sessionId, ':', event.type);
|
||||
|
||||
switch (event.type) {
|
||||
case "message":
|
||||
case 'message':
|
||||
// User message added
|
||||
setMessages((prev) => [...prev, event.message]);
|
||||
break;
|
||||
|
||||
case "stream":
|
||||
case 'stream':
|
||||
// Assistant message streaming
|
||||
if (event.isComplete) {
|
||||
// Final update
|
||||
setMessages((prev) =>
|
||||
prev.map((msg) =>
|
||||
msg.id === event.messageId
|
||||
? { ...msg, content: event.content }
|
||||
: msg
|
||||
msg.id === event.messageId ? { ...msg, content: event.content } : msg
|
||||
)
|
||||
);
|
||||
currentMessageRef.current = null;
|
||||
@@ -236,15 +253,13 @@ export function useElectronAgent({
|
||||
if (existingIndex >= 0) {
|
||||
// Update existing message
|
||||
return prev.map((msg) =>
|
||||
msg.id === event.messageId
|
||||
? { ...msg, content: event.content }
|
||||
: msg
|
||||
msg.id === event.messageId ? { ...msg, content: event.content } : msg
|
||||
);
|
||||
} else {
|
||||
// Create new message
|
||||
const newMessage: Message = {
|
||||
id: event.messageId,
|
||||
role: "assistant",
|
||||
role: 'assistant',
|
||||
content: event.content,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
@@ -255,30 +270,28 @@ export function useElectronAgent({
|
||||
}
|
||||
break;
|
||||
|
||||
case "tool_use":
|
||||
case 'tool_use':
|
||||
// Tool being used
|
||||
console.log("[useElectronAgent] Tool use:", event.tool.name);
|
||||
console.log('[useElectronAgent] Tool use:', event.tool.name);
|
||||
onToolUse?.(event.tool.name, event.tool.input);
|
||||
break;
|
||||
|
||||
case "complete":
|
||||
case 'complete':
|
||||
// Agent finished processing for THIS session
|
||||
console.log("[useElectronAgent] Processing complete for session:", sessionId);
|
||||
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
|
||||
msg.id === event.messageId ? { ...msg, content: event.content } : msg
|
||||
)
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case "error":
|
||||
case 'error':
|
||||
// Error occurred for THIS session
|
||||
console.error("[useElectronAgent] Agent error for session:", sessionId, event.error);
|
||||
console.error('[useElectronAgent] Agent error for session:', sessionId, event.error);
|
||||
setIsProcessing(false);
|
||||
setError(event.error);
|
||||
if (event.message) {
|
||||
@@ -293,7 +306,7 @@ export function useElectronAgent({
|
||||
|
||||
return () => {
|
||||
if (unsubscribeRef.current) {
|
||||
console.log("[useElectronAgent] Unsubscribing from stream events for session:", sessionId);
|
||||
console.log('[useElectronAgent] Unsubscribing from stream events for session:', sessionId);
|
||||
unsubscribeRef.current();
|
||||
unsubscribeRef.current = null;
|
||||
}
|
||||
@@ -302,15 +315,15 @@ export function useElectronAgent({
|
||||
|
||||
// Send a message to the agent
|
||||
const sendMessage = useCallback(
|
||||
async (content: string, images?: ImageAttachment[]) => {
|
||||
async (content: string, images?: ImageAttachment[], textFiles?: TextFileAttachment[]) => {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.agent) {
|
||||
setError("API not available");
|
||||
setError('API not available');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isProcessing) {
|
||||
console.warn("[useElectronAgent] Already processing a message");
|
||||
console.warn('[useElectronAgent] Already processing a message');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -318,11 +331,23 @@ export function useElectronAgent({
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
console.log("[useElectronAgent] Sending message", {
|
||||
console.log('[useElectronAgent] Sending message', {
|
||||
hasImages: images && images.length > 0,
|
||||
imageCount: images?.length || 0
|
||||
imageCount: images?.length || 0,
|
||||
hasTextFiles: textFiles && textFiles.length > 0,
|
||||
textFileCount: textFiles?.length || 0,
|
||||
});
|
||||
|
||||
// Build message content with text file context prepended
|
||||
let messageContent = content;
|
||||
if (textFiles && textFiles.length > 0) {
|
||||
const contextParts = textFiles.map((file) => {
|
||||
return `<file name="${file.filename}">\n${file.content}\n</file>`;
|
||||
});
|
||||
const contextBlock = `Here are some files for context:\n\n${contextParts.join('\n\n')}\n\n`;
|
||||
messageContent = contextBlock + content;
|
||||
}
|
||||
|
||||
// Save images to .automaker/images and get paths
|
||||
let imagePaths: string[] | undefined;
|
||||
if (images && images.length > 0 && api.saveImageToTemp) {
|
||||
@@ -330,36 +355,36 @@ export function useElectronAgent({
|
||||
for (const image of images) {
|
||||
const result = await api.saveImageToTemp(
|
||||
image.data,
|
||||
image.filename,
|
||||
sanitizeFilename(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);
|
||||
console.log('[useElectronAgent] Saved image to .automaker/images:', result.path);
|
||||
} else {
|
||||
console.error("[useElectronAgent] Failed to save image:", result.error);
|
||||
console.error('[useElectronAgent] Failed to save image:', result.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = await api.agent!.send(
|
||||
sessionId,
|
||||
content,
|
||||
messageContent,
|
||||
workingDirectory,
|
||||
imagePaths,
|
||||
model
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
setError(result.error || "Failed to send message");
|
||||
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");
|
||||
console.error('[useElectronAgent] Failed to send message:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to send message');
|
||||
setIsProcessing(false);
|
||||
}
|
||||
},
|
||||
@@ -370,22 +395,22 @@ export function useElectronAgent({
|
||||
const stopExecution = useCallback(async () => {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.agent) {
|
||||
setError("API not available");
|
||||
setError('API not available');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("[useElectronAgent] Stopping execution");
|
||||
console.log('[useElectronAgent] Stopping execution');
|
||||
const result = await api.agent!.stop(sessionId);
|
||||
|
||||
if (!result.success) {
|
||||
setError(result.error || "Failed to stop execution");
|
||||
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");
|
||||
console.error('[useElectronAgent] Failed to stop:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to stop execution');
|
||||
}
|
||||
}, [sessionId]);
|
||||
|
||||
@@ -393,23 +418,23 @@ export function useElectronAgent({
|
||||
const clearHistory = useCallback(async () => {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.agent) {
|
||||
setError("API not available");
|
||||
setError('API not available');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("[useElectronAgent] Clearing history");
|
||||
console.log('[useElectronAgent] Clearing history');
|
||||
const result = await api.agent!.clear(sessionId);
|
||||
|
||||
if (result.success) {
|
||||
setMessages([]);
|
||||
setError(null);
|
||||
} else {
|
||||
setError(result.error || "Failed to clear history");
|
||||
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");
|
||||
console.error('[useElectronAgent] Failed to clear:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to clear history');
|
||||
}
|
||||
}, [sessionId]);
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
|
||||
import { useEffect, useCallback } from "react";
|
||||
import { useAppStore, parseShortcut } from "@/store/app-store";
|
||||
import { useEffect, useCallback } from 'react';
|
||||
import { useAppStore, parseShortcut } from '@/store/app-store';
|
||||
|
||||
export interface KeyboardShortcut {
|
||||
key: string; // Can be simple "K" or with modifiers "Shift+N", "Cmd+K"
|
||||
key: string; // Can be simple "K" or with modifiers "Shift+N", "Cmd+K"
|
||||
action: () => void;
|
||||
description?: string;
|
||||
}
|
||||
@@ -18,37 +17,35 @@ function isInputFocused(): boolean {
|
||||
|
||||
// Check if it's a form input element
|
||||
const tagName = activeElement.tagName.toLowerCase();
|
||||
if (tagName === "input" || tagName === "textarea" || tagName === "select") {
|
||||
if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if it's a contenteditable element
|
||||
if (activeElement.getAttribute("contenteditable") === "true") {
|
||||
if (activeElement.getAttribute('contenteditable') === 'true') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if it has a role of textbox or searchbox
|
||||
const role = activeElement.getAttribute("role");
|
||||
if (role === "textbox" || role === "searchbox" || role === "combobox") {
|
||||
const role = activeElement.getAttribute('role');
|
||||
if (role === 'textbox' || role === 'searchbox' || role === 'combobox') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if focus is inside an xterm terminal (they use a hidden textarea)
|
||||
const xtermContainer = activeElement.closest(".xterm");
|
||||
const xtermContainer = activeElement.closest('.xterm');
|
||||
if (xtermContainer) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Also check if any parent has data-terminal-container attribute
|
||||
const terminalContainer = activeElement.closest("[data-terminal-container]");
|
||||
const terminalContainer = activeElement.closest('[data-terminal-container]');
|
||||
if (terminalContainer) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for autocomplete/typeahead dropdowns being open
|
||||
const autocompleteList = document.querySelector(
|
||||
'[data-testid="category-autocomplete-list"]'
|
||||
);
|
||||
const autocompleteList = document.querySelector('[data-testid="category-autocomplete-list"]');
|
||||
if (autocompleteList) {
|
||||
return true;
|
||||
}
|
||||
@@ -60,9 +57,7 @@ function isInputFocused(): boolean {
|
||||
}
|
||||
|
||||
// Check for project picker dropdown being open
|
||||
const projectPickerDropdown = document.querySelector(
|
||||
'[data-testid="project-picker-dropdown"]'
|
||||
);
|
||||
const projectPickerDropdown = document.querySelector('[data-testid="project-picker-dropdown"]');
|
||||
if (projectPickerDropdown) {
|
||||
return true;
|
||||
}
|
||||
@@ -77,6 +72,102 @@ function isInputFocused(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a key character to its corresponding event.code
|
||||
* This is used for keyboard-layout independent matching in terminals
|
||||
*/
|
||||
function keyToCode(key: string): string {
|
||||
const upperKey = key.toUpperCase();
|
||||
|
||||
// Letters A-Z map to KeyA-KeyZ
|
||||
if (/^[A-Z]$/.test(upperKey)) {
|
||||
return `Key${upperKey}`;
|
||||
}
|
||||
|
||||
// Numbers 0-9 on main row map to Digit0-Digit9
|
||||
if (/^[0-9]$/.test(key)) {
|
||||
return `Digit${key}`;
|
||||
}
|
||||
|
||||
// Special key mappings
|
||||
const specialMappings: Record<string, string> = {
|
||||
'`': 'Backquote',
|
||||
'~': 'Backquote',
|
||||
'-': 'Minus',
|
||||
_: 'Minus',
|
||||
'=': 'Equal',
|
||||
'+': 'Equal',
|
||||
'[': 'BracketLeft',
|
||||
'{': 'BracketLeft',
|
||||
']': 'BracketRight',
|
||||
'}': 'BracketRight',
|
||||
'\\': 'Backslash',
|
||||
'|': 'Backslash',
|
||||
';': 'Semicolon',
|
||||
':': 'Semicolon',
|
||||
"'": 'Quote',
|
||||
'"': 'Quote',
|
||||
',': 'Comma',
|
||||
'<': 'Comma',
|
||||
'.': 'Period',
|
||||
'>': 'Period',
|
||||
'/': 'Slash',
|
||||
'?': 'Slash',
|
||||
' ': 'Space',
|
||||
Enter: 'Enter',
|
||||
Tab: 'Tab',
|
||||
Escape: 'Escape',
|
||||
Backspace: 'Backspace',
|
||||
Delete: 'Delete',
|
||||
ArrowUp: 'ArrowUp',
|
||||
ArrowDown: 'ArrowDown',
|
||||
ArrowLeft: 'ArrowLeft',
|
||||
ArrowRight: 'ArrowRight',
|
||||
};
|
||||
|
||||
return specialMappings[key] || specialMappings[upperKey] || key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a keyboard event matches a shortcut definition using event.code
|
||||
* This is keyboard-layout independent - useful for terminals where Alt+key
|
||||
* combinations can produce special characters with event.key
|
||||
*/
|
||||
export function matchesShortcutWithCode(event: KeyboardEvent, shortcutStr: string): boolean {
|
||||
const shortcut = parseShortcut(shortcutStr);
|
||||
if (!shortcut.key) return false;
|
||||
|
||||
// Convert the shortcut key to event.code format
|
||||
const expectedCode = keyToCode(shortcut.key);
|
||||
|
||||
// Check if the code matches
|
||||
if (event.code !== expectedCode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check modifier keys
|
||||
const cmdCtrlPressed = event.metaKey || event.ctrlKey;
|
||||
const shiftPressed = event.shiftKey;
|
||||
const altPressed = event.altKey;
|
||||
|
||||
// If shortcut requires cmdCtrl, it must be pressed
|
||||
if (shortcut.cmdCtrl && !cmdCtrlPressed) return false;
|
||||
// If shortcut doesn't require cmdCtrl, it shouldn't be pressed
|
||||
if (!shortcut.cmdCtrl && cmdCtrlPressed) return false;
|
||||
|
||||
// If shortcut requires shift, it must be pressed
|
||||
if (shortcut.shift && !shiftPressed) return false;
|
||||
// If shortcut doesn't require shift, it shouldn't be pressed
|
||||
if (!shortcut.shift && shiftPressed) return false;
|
||||
|
||||
// If shortcut requires alt, it must be pressed
|
||||
if (shortcut.alt && !altPressed) return false;
|
||||
// If shortcut doesn't require alt, it shouldn't be pressed
|
||||
if (!shortcut.alt && altPressed) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a keyboard event matches a shortcut definition
|
||||
*/
|
||||
@@ -125,9 +216,7 @@ export function useKeyboardShortcuts(shortcuts: KeyboardShortcut[]) {
|
||||
}
|
||||
|
||||
// Find matching shortcut
|
||||
const matchingShortcut = shortcuts.find(
|
||||
(shortcut) => matchesShortcut(event, shortcut.key)
|
||||
);
|
||||
const matchingShortcut = shortcuts.find((shortcut) => matchesShortcut(event, shortcut.key));
|
||||
|
||||
if (matchingShortcut) {
|
||||
event.preventDefault();
|
||||
@@ -138,9 +227,9 @@ export function useKeyboardShortcuts(shortcuts: KeyboardShortcut[]) {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [handleKeyDown]);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import type { ImageAttachment } from '@/store/app-store';
|
||||
import type { ImageAttachment, TextFileAttachment } from '@/store/app-store';
|
||||
|
||||
export interface QueuedMessage {
|
||||
id: string;
|
||||
content: string;
|
||||
images?: ImageAttachment[];
|
||||
textFiles?: TextFileAttachment[];
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
@@ -15,7 +16,11 @@ interface UseMessageQueueOptions {
|
||||
interface UseMessageQueueResult {
|
||||
queuedMessages: QueuedMessage[];
|
||||
isProcessingQueue: boolean;
|
||||
addToQueue: (content: string, images?: ImageAttachment[]) => void;
|
||||
addToQueue: (
|
||||
content: string,
|
||||
images?: ImageAttachment[],
|
||||
textFiles?: TextFileAttachment[]
|
||||
) => void;
|
||||
clearQueue: () => void;
|
||||
removeFromQueue: (messageId: string) => void;
|
||||
processNext: () => Promise<void>;
|
||||
@@ -31,19 +36,23 @@ export function useMessageQueue({ onProcessNext }: UseMessageQueueOptions): UseM
|
||||
const [queuedMessages, setQueuedMessages] = useState<QueuedMessage[]>([]);
|
||||
const [isProcessingQueue, setIsProcessingQueue] = useState(false);
|
||||
|
||||
const addToQueue = useCallback((content: string, images?: ImageAttachment[]) => {
|
||||
const queuedMessage: QueuedMessage = {
|
||||
id: `queued-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
content: content.trim(),
|
||||
images,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
const addToQueue = useCallback(
|
||||
(content: string, images?: ImageAttachment[], textFiles?: TextFileAttachment[]) => {
|
||||
const queuedMessage: QueuedMessage = {
|
||||
id: `queued-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
content: content.trim(),
|
||||
images,
|
||||
textFiles,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setQueuedMessages(prev => [...prev, queuedMessage]);
|
||||
}, []);
|
||||
setQueuedMessages((prev) => [...prev, queuedMessage]);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const removeFromQueue = useCallback((messageId: string) => {
|
||||
setQueuedMessages(prev => prev.filter(msg => msg.id !== messageId));
|
||||
setQueuedMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
||||
}, []);
|
||||
|
||||
const clearQueue = useCallback(() => {
|
||||
@@ -61,7 +70,7 @@ export function useMessageQueue({ onProcessNext }: UseMessageQueueOptions): UseM
|
||||
try {
|
||||
await onProcessNext(nextMessage);
|
||||
// Remove the processed message from queue
|
||||
setQueuedMessages(prev => prev.slice(1));
|
||||
setQueuedMessages((prev) => prev.slice(1));
|
||||
} catch (error) {
|
||||
console.error('Error processing queued message:', error);
|
||||
// Keep the message in queue for retry or manual removal
|
||||
@@ -78,4 +87,4 @@ export function useMessageQueue({ onProcessNext }: UseMessageQueueOptions): UseM
|
||||
removeFromQueue,
|
||||
processNext,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
|
||||
interface ScrollTrackingItem {
|
||||
id: string;
|
||||
@@ -26,9 +26,7 @@ export function useScrollTracking<T extends ScrollTrackingItem>({
|
||||
initialSection,
|
||||
scrollOffset = 24,
|
||||
}: UseScrollTrackingOptions<T>) {
|
||||
const [activeSection, setActiveSection] = useState(
|
||||
initialSection || items[0]?.id || ""
|
||||
);
|
||||
const [activeSection, setActiveSection] = useState(initialSection || items[0]?.id || '');
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Track scroll position to highlight active nav item
|
||||
@@ -72,8 +70,8 @@ export function useScrollTracking<T extends ScrollTrackingItem>({
|
||||
}
|
||||
};
|
||||
|
||||
container.addEventListener("scroll", handleScroll);
|
||||
return () => container.removeEventListener("scroll", handleScroll);
|
||||
container.addEventListener('scroll', handleScroll);
|
||||
return () => container.removeEventListener('scroll', handleScroll);
|
||||
}, [items, filterFn]);
|
||||
|
||||
// Scroll to a specific section with smooth animation
|
||||
@@ -84,12 +82,11 @@ export function useScrollTracking<T extends ScrollTrackingItem>({
|
||||
const container = scrollContainerRef.current;
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
const relativeTop =
|
||||
elementRect.top - containerRect.top + container.scrollTop;
|
||||
const relativeTop = elementRect.top - containerRect.top + container.scrollTop;
|
||||
|
||||
container.scrollTo({
|
||||
top: relativeTop - scrollOffset,
|
||||
behavior: "smooth",
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
* - syncProjectSettingsToServer: Writes project-specific overrides
|
||||
*/
|
||||
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { getHttpApiClient } from "@/lib/http-api-client";
|
||||
import { isElectron } from "@/lib/electron";
|
||||
import { getItem, removeItem } from "@/lib/storage";
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
||||
import { isElectron } from '@/lib/electron';
|
||||
import { getItem, removeItem } from '@/lib/storage';
|
||||
|
||||
/**
|
||||
* State returned by useSettingsMigration hook
|
||||
@@ -41,11 +41,11 @@ interface MigrationState {
|
||||
* The automaker-storage key is handled specially as it's still used by Zustand.
|
||||
*/
|
||||
const LOCALSTORAGE_KEYS = [
|
||||
"automaker-storage",
|
||||
"automaker-setup",
|
||||
"worktree-panel-collapsed",
|
||||
"file-browser-recent-folders",
|
||||
"automaker:lastProjectDir",
|
||||
'automaker-storage',
|
||||
'automaker-setup',
|
||||
'worktree-panel-collapsed',
|
||||
'file-browser-recent-folders',
|
||||
'automaker:lastProjectDir',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
@@ -55,13 +55,13 @@ const LOCALSTORAGE_KEYS = [
|
||||
* as a cache. These other keys have been migrated and are no longer needed.
|
||||
*/
|
||||
const KEYS_TO_CLEAR_AFTER_MIGRATION = [
|
||||
"worktree-panel-collapsed",
|
||||
"file-browser-recent-folders",
|
||||
"automaker:lastProjectDir",
|
||||
'worktree-panel-collapsed',
|
||||
'file-browser-recent-folders',
|
||||
'automaker:lastProjectDir',
|
||||
// Legacy keys from older versions
|
||||
"automaker_projects",
|
||||
"automaker_current_project",
|
||||
"automaker_trashed_projects",
|
||||
'automaker_projects',
|
||||
'automaker_current_project',
|
||||
'automaker_trashed_projects',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
@@ -104,35 +104,31 @@ export function useSettingsMigration(): MigrationState {
|
||||
const status = await api.settings.getStatus();
|
||||
|
||||
if (!status.success) {
|
||||
console.error("[Settings Migration] Failed to get status:", status);
|
||||
console.error('[Settings Migration] Failed to get status:', status);
|
||||
setState({
|
||||
checked: true,
|
||||
migrated: false,
|
||||
error: "Failed to check settings status",
|
||||
error: 'Failed to check settings status',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// If settings files already exist, no migration needed
|
||||
if (!status.needsMigration) {
|
||||
console.log(
|
||||
"[Settings Migration] Settings files exist, no migration needed"
|
||||
);
|
||||
console.log('[Settings Migration] Settings files exist, no migration needed');
|
||||
setState({ checked: true, migrated: false, error: null });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we have localStorage data to migrate
|
||||
const automakerStorage = getItem("automaker-storage");
|
||||
const automakerStorage = getItem('automaker-storage');
|
||||
if (!automakerStorage) {
|
||||
console.log(
|
||||
"[Settings Migration] No localStorage data to migrate"
|
||||
);
|
||||
console.log('[Settings Migration] No localStorage data to migrate');
|
||||
setState({ checked: true, migrated: false, error: null });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[Settings Migration] Starting migration...");
|
||||
console.log('[Settings Migration] Starting migration...');
|
||||
|
||||
// Collect all localStorage data
|
||||
const localStorageData: Record<string, string> = {};
|
||||
@@ -147,7 +143,7 @@ export function useSettingsMigration(): MigrationState {
|
||||
const result = await api.settings.migrate(localStorageData);
|
||||
|
||||
if (result.success) {
|
||||
console.log("[Settings Migration] Migration successful:", {
|
||||
console.log('[Settings Migration] Migration successful:', {
|
||||
globalSettings: result.migratedGlobalSettings,
|
||||
credentials: result.migratedCredentials,
|
||||
projects: result.migratedProjectCount,
|
||||
@@ -160,22 +156,19 @@ export function useSettingsMigration(): MigrationState {
|
||||
|
||||
setState({ checked: true, migrated: true, error: null });
|
||||
} else {
|
||||
console.warn(
|
||||
"[Settings Migration] Migration had errors:",
|
||||
result.errors
|
||||
);
|
||||
console.warn('[Settings Migration] Migration had errors:', result.errors);
|
||||
setState({
|
||||
checked: true,
|
||||
migrated: false,
|
||||
error: result.errors.join(", "),
|
||||
error: result.errors.join(', '),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[Settings Migration] Migration failed:", error);
|
||||
console.error('[Settings Migration] Migration failed:', error);
|
||||
setState({
|
||||
checked: true,
|
||||
migrated: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -204,7 +197,7 @@ export async function syncSettingsToServer(): Promise<boolean> {
|
||||
|
||||
try {
|
||||
const api = getHttpApiClient();
|
||||
const automakerStorage = getItem("automaker-storage");
|
||||
const automakerStorage = getItem('automaker-storage');
|
||||
|
||||
if (!automakerStorage) {
|
||||
return false;
|
||||
@@ -241,7 +234,7 @@ export async function syncSettingsToServer(): Promise<boolean> {
|
||||
const result = await api.settings.updateGlobal(updates);
|
||||
return result.success;
|
||||
} catch (error) {
|
||||
console.error("[Settings Sync] Failed to sync settings:", error);
|
||||
console.error('[Settings Sync] Failed to sync settings:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -272,7 +265,7 @@ export async function syncCredentialsToServer(apiKeys: {
|
||||
const result = await api.settings.updateCredentials({ apiKeys });
|
||||
return result.success;
|
||||
} catch (error) {
|
||||
console.error("[Settings Sync] Failed to sync credentials:", error);
|
||||
console.error('[Settings Sync] Failed to sync credentials:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -316,10 +309,7 @@ export async function syncProjectSettingsToServer(
|
||||
const result = await api.settings.updateProject(projectPath, updates);
|
||||
return result.success;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[Settings Sync] Failed to sync project settings:",
|
||||
error
|
||||
);
|
||||
console.error('[Settings Sync] Failed to sync project settings:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export interface WindowState {
|
||||
isMaximized: boolean;
|
||||
@@ -13,7 +13,7 @@ export interface WindowState {
|
||||
*/
|
||||
export function useWindowState(): WindowState {
|
||||
const [windowState, setWindowState] = useState<WindowState>(() => {
|
||||
if (typeof window === "undefined") {
|
||||
if (typeof window === 'undefined') {
|
||||
return { isMaximized: false, windowWidth: 0, windowHeight: 0 };
|
||||
}
|
||||
const width = window.innerWidth;
|
||||
@@ -26,7 +26,7 @@ export function useWindowState(): WindowState {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const updateWindowState = () => {
|
||||
const width = window.innerWidth;
|
||||
@@ -42,15 +42,12 @@ export function useWindowState(): WindowState {
|
||||
updateWindowState();
|
||||
|
||||
// Listen for resize events
|
||||
window.addEventListener("resize", updateWindowState);
|
||||
window.addEventListener('resize', updateWindowState);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", updateWindowState);
|
||||
window.removeEventListener('resize', updateWindowState);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return windowState;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user