mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
Merge branch 'v0.11.0rc' into fix/pipeline-resume-edge-cases
This commit is contained in:
@@ -7,4 +7,4 @@ export { useBoardEffects } from './use-board-effects';
|
||||
export { useBoardBackground } from './use-board-background';
|
||||
export { useBoardPersistence } from './use-board-persistence';
|
||||
export { useFollowUpState } from './use-follow-up-state';
|
||||
export { useSuggestionsState } from './use-suggestions-state';
|
||||
export { useSelectionMode } from './use-selection-mode';
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
// @ts-nocheck
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
Feature,
|
||||
FeatureImage,
|
||||
AgentModel,
|
||||
ModelAlias,
|
||||
ThinkingLevel,
|
||||
PlanningMode,
|
||||
useAppStore,
|
||||
} from '@/store/app-store';
|
||||
import type { ReasoningEffort } from '@automaker/types';
|
||||
import { FeatureImagePath as DescriptionImagePath } from '@/components/ui/description-image-dropzone';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { isConnectionError, handleServerOffline } from '@/lib/http-api-client';
|
||||
import { toast } from 'sonner';
|
||||
import { useAutoMode } from '@/hooks/use-auto-mode';
|
||||
import { truncateDescription } from '@/lib/utils';
|
||||
import { getBlockingDependencies } from '@automaker/dependency-resolver';
|
||||
import { createLogger } from '@automaker/utils/logger';
|
||||
|
||||
const logger = createLogger('BoardActions');
|
||||
|
||||
interface UseBoardActionsProps {
|
||||
currentProject: { path: string; id: string } | null;
|
||||
@@ -20,7 +26,13 @@ interface UseBoardActionsProps {
|
||||
runningAutoTasks: string[];
|
||||
loadFeatures: () => Promise<void>;
|
||||
persistFeatureCreate: (feature: Feature) => Promise<void>;
|
||||
persistFeatureUpdate: (featureId: string, updates: Partial<Feature>) => Promise<void>;
|
||||
persistFeatureUpdate: (
|
||||
featureId: string,
|
||||
updates: Partial<Feature>,
|
||||
descriptionHistorySource?: 'enhance' | 'edit',
|
||||
enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer',
|
||||
preEnhancementDescription?: string
|
||||
) => Promise<void>;
|
||||
persistFeatureDelete: (featureId: string) => Promise<void>;
|
||||
saveCategory: (category: string) => Promise<void>;
|
||||
setEditingFeature: (feature: Feature | null) => void;
|
||||
@@ -76,6 +88,7 @@ export function useBoardActions({
|
||||
moveFeature,
|
||||
useWorktrees,
|
||||
enableDependencyBlocking,
|
||||
skipVerificationInAutoMode,
|
||||
isPrimaryWorktreeBranch,
|
||||
getPrimaryWorktreeBranch,
|
||||
} = useAppStore();
|
||||
@@ -92,28 +105,46 @@ export function useBoardActions({
|
||||
images: FeatureImage[];
|
||||
imagePaths: DescriptionImagePath[];
|
||||
skipTests: boolean;
|
||||
model: AgentModel;
|
||||
model: ModelAlias;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
branchName: string;
|
||||
priority: number;
|
||||
planningMode: PlanningMode;
|
||||
requirePlanApproval: boolean;
|
||||
dependencies?: string[];
|
||||
workMode?: 'current' | 'auto' | 'custom';
|
||||
}) => {
|
||||
// Empty string means "unassigned" (show only on primary worktree) - convert to undefined
|
||||
// Non-empty string is the actual branch name (for non-primary worktrees)
|
||||
const finalBranchName = featureData.branchName || undefined;
|
||||
const workMode = featureData.workMode || 'current';
|
||||
|
||||
// If worktrees enabled and a branch is specified, create the worktree now
|
||||
// This ensures the worktree exists before the feature starts
|
||||
if (useWorktrees && finalBranchName && currentProject) {
|
||||
// Determine final branch name based on work mode:
|
||||
// - 'current': No branch name, work on current branch (no worktree)
|
||||
// - 'auto': Auto-generate branch name based on current branch
|
||||
// - 'custom': Use the provided branch name
|
||||
let finalBranchName: string | undefined;
|
||||
|
||||
if (workMode === 'current') {
|
||||
// No worktree isolation - work directly on current branch
|
||||
finalBranchName = undefined;
|
||||
} else if (workMode === 'auto') {
|
||||
// Auto-generate a branch name based on current branch and timestamp
|
||||
const baseBranch = currentWorktreeBranch || 'main';
|
||||
const timestamp = Date.now();
|
||||
const randomSuffix = Math.random().toString(36).substring(2, 6);
|
||||
finalBranchName = `feature/${baseBranch}-${timestamp}-${randomSuffix}`;
|
||||
} else {
|
||||
// Custom mode - use provided branch name
|
||||
finalBranchName = featureData.branchName || undefined;
|
||||
}
|
||||
|
||||
// Create worktree for 'auto' or 'custom' modes when we have a branch name
|
||||
if ((workMode === 'auto' || workMode === 'custom') && finalBranchName && currentProject) {
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (api?.worktree?.create) {
|
||||
const result = await api.worktree.create(currentProject.path, finalBranchName);
|
||||
if (result.success && result.worktree) {
|
||||
console.log(
|
||||
`[Board] Worktree for branch "${finalBranchName}" ${
|
||||
logger.info(
|
||||
`Worktree for branch "${finalBranchName}" ${
|
||||
result.worktree?.isNew ? 'created' : 'already exists'
|
||||
}`
|
||||
);
|
||||
@@ -125,8 +156,8 @@ export function useBoardActions({
|
||||
// Refresh worktree list in UI
|
||||
onWorktreeCreated?.();
|
||||
} else if (!result.success) {
|
||||
console.error(
|
||||
`[Board] Failed to create worktree for branch "${finalBranchName}":`,
|
||||
logger.error(
|
||||
`Failed to create worktree for branch "${finalBranchName}":`,
|
||||
result.error
|
||||
);
|
||||
toast.error('Failed to create worktree', {
|
||||
@@ -135,7 +166,7 @@ export function useBoardActions({
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Board] Error creating worktree:', error);
|
||||
logger.error('Error creating worktree:', error);
|
||||
toast.error('Failed to create worktree', {
|
||||
description: error instanceof Error ? error.message : 'An error occurred',
|
||||
});
|
||||
@@ -180,7 +211,7 @@ export function useBoardActions({
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('[Board] Error generating title:', error);
|
||||
logger.error('Error generating title:', error);
|
||||
// Clear generating flag on error
|
||||
const titleUpdates = { titleGenerating: false };
|
||||
updateFeature(createdFeature.id, titleUpdates);
|
||||
@@ -195,10 +226,10 @@ export function useBoardActions({
|
||||
persistFeatureUpdate,
|
||||
updateFeature,
|
||||
saveCategory,
|
||||
useWorktrees,
|
||||
currentProject,
|
||||
onWorktreeCreated,
|
||||
onWorktreeAutoSelect,
|
||||
currentWorktreeBranch,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -210,35 +241,53 @@ export function useBoardActions({
|
||||
category: string;
|
||||
description: string;
|
||||
skipTests: boolean;
|
||||
model: AgentModel;
|
||||
model: ModelAlias;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
reasoningEffort: ReasoningEffort;
|
||||
imagePaths: DescriptionImagePath[];
|
||||
branchName: string;
|
||||
priority: number;
|
||||
planningMode?: PlanningMode;
|
||||
requirePlanApproval?: boolean;
|
||||
}
|
||||
workMode?: 'current' | 'auto' | 'custom';
|
||||
},
|
||||
descriptionHistorySource?: 'enhance' | 'edit',
|
||||
enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer',
|
||||
preEnhancementDescription?: string
|
||||
) => {
|
||||
const finalBranchName = updates.branchName || undefined;
|
||||
const workMode = updates.workMode || 'current';
|
||||
|
||||
// If worktrees enabled and a branch is specified, create the worktree now
|
||||
// This ensures the worktree exists before the feature starts
|
||||
if (useWorktrees && finalBranchName && currentProject) {
|
||||
// Determine final branch name based on work mode
|
||||
let finalBranchName: string | undefined;
|
||||
|
||||
if (workMode === 'current') {
|
||||
finalBranchName = undefined;
|
||||
} else if (workMode === 'auto') {
|
||||
const baseBranch = currentWorktreeBranch || 'main';
|
||||
const timestamp = Date.now();
|
||||
const randomSuffix = Math.random().toString(36).substring(2, 6);
|
||||
finalBranchName = `feature/${baseBranch}-${timestamp}-${randomSuffix}`;
|
||||
} else {
|
||||
finalBranchName = updates.branchName || undefined;
|
||||
}
|
||||
|
||||
// Create worktree for 'auto' or 'custom' modes when we have a branch name
|
||||
if ((workMode === 'auto' || workMode === 'custom') && finalBranchName && currentProject) {
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (api?.worktree?.create) {
|
||||
const result = await api.worktree.create(currentProject.path, finalBranchName);
|
||||
if (result.success) {
|
||||
console.log(
|
||||
`[Board] Worktree for branch "${finalBranchName}" ${
|
||||
logger.info(
|
||||
`Worktree for branch "${finalBranchName}" ${
|
||||
result.worktree?.isNew ? 'created' : 'already exists'
|
||||
}`
|
||||
);
|
||||
// Refresh worktree list in UI
|
||||
onWorktreeCreated?.();
|
||||
} else {
|
||||
console.error(
|
||||
`[Board] Failed to create worktree for branch "${finalBranchName}":`,
|
||||
logger.error(
|
||||
`Failed to create worktree for branch "${finalBranchName}":`,
|
||||
result.error
|
||||
);
|
||||
toast.error('Failed to create worktree', {
|
||||
@@ -247,7 +296,7 @@ export function useBoardActions({
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Board] Error creating worktree:', error);
|
||||
logger.error('Error creating worktree:', error);
|
||||
toast.error('Failed to create worktree', {
|
||||
description: error instanceof Error ? error.message : 'An error occurred',
|
||||
});
|
||||
@@ -261,7 +310,13 @@ export function useBoardActions({
|
||||
};
|
||||
|
||||
updateFeature(featureId, finalUpdates);
|
||||
persistFeatureUpdate(featureId, finalUpdates);
|
||||
persistFeatureUpdate(
|
||||
featureId,
|
||||
finalUpdates,
|
||||
descriptionHistorySource,
|
||||
enhancementMode,
|
||||
preEnhancementDescription
|
||||
);
|
||||
if (updates.category) {
|
||||
saveCategory(updates.category);
|
||||
}
|
||||
@@ -272,9 +327,9 @@ export function useBoardActions({
|
||||
persistFeatureUpdate,
|
||||
saveCategory,
|
||||
setEditingFeature,
|
||||
useWorktrees,
|
||||
currentProject,
|
||||
onWorktreeCreated,
|
||||
currentWorktreeBranch,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -292,7 +347,7 @@ export function useBoardActions({
|
||||
description: `Stopped and deleted: ${truncateDescription(feature.description)}`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[Board] Error stopping feature before delete:', error);
|
||||
logger.error('Error stopping feature before delete:', error);
|
||||
toast.error('Failed to stop agent', {
|
||||
description: 'The feature will still be deleted.',
|
||||
});
|
||||
@@ -305,13 +360,13 @@ export function useBoardActions({
|
||||
for (const imagePathObj of feature.imagePaths) {
|
||||
try {
|
||||
await api.deleteFile(imagePathObj.path);
|
||||
console.log(`[Board] Deleted image: ${imagePathObj.path}`);
|
||||
logger.info(`Deleted image: ${imagePathObj.path}`);
|
||||
} catch (error) {
|
||||
console.error(`[Board] Failed to delete image ${imagePathObj.path}:`, error);
|
||||
logger.error(`Failed to delete image ${imagePathObj.path}:`, error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[Board] Error deleting images for feature ${featureId}:`, error);
|
||||
logger.error(`Error deleting images for feature ${featureId}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,38 +378,31 @@ export function useBoardActions({
|
||||
|
||||
const handleRunFeature = useCallback(
|
||||
async (feature: Feature) => {
|
||||
if (!currentProject) return;
|
||||
if (!currentProject) {
|
||||
throw new Error('No project selected');
|
||||
}
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode) {
|
||||
console.error('Auto mode API not available');
|
||||
return;
|
||||
}
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode) {
|
||||
throw new Error('Auto mode API not available');
|
||||
}
|
||||
|
||||
// Server derives workDir from feature.branchName at execution time
|
||||
const result = await api.autoMode.runFeature(
|
||||
currentProject.path,
|
||||
feature.id,
|
||||
useWorktrees
|
||||
// No worktreePath - server derives from feature.branchName
|
||||
);
|
||||
// Server derives workDir from feature.branchName at execution time
|
||||
const result = await api.autoMode.runFeature(
|
||||
currentProject.path,
|
||||
feature.id,
|
||||
useWorktrees
|
||||
// No worktreePath - server derives from feature.branchName
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
console.log(
|
||||
'[Board] Feature run started successfully, branch:',
|
||||
feature.branchName || 'default'
|
||||
);
|
||||
} else {
|
||||
console.error('[Board] Failed to run feature:', result.error);
|
||||
await loadFeatures();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Board] Error running feature:', error);
|
||||
await loadFeatures();
|
||||
if (result.success) {
|
||||
logger.info('Feature run started successfully, branch:', feature.branchName || 'default');
|
||||
} else {
|
||||
// Throw error so caller can handle rollback
|
||||
throw new Error(result.error || 'Failed to start feature');
|
||||
}
|
||||
},
|
||||
[currentProject, useWorktrees, loadFeatures]
|
||||
[currentProject, useWorktrees]
|
||||
);
|
||||
|
||||
const handleStartImplementation = useCallback(
|
||||
@@ -390,11 +438,34 @@ export function useBoardActions({
|
||||
startedAt: new Date().toISOString(),
|
||||
};
|
||||
updateFeature(feature.id, updates);
|
||||
// Must await to ensure feature status is persisted before starting agent
|
||||
await persistFeatureUpdate(feature.id, updates);
|
||||
console.log('[Board] Feature moved to in_progress, starting agent...');
|
||||
await handleRunFeature(feature);
|
||||
return true;
|
||||
|
||||
try {
|
||||
// Must await to ensure feature status is persisted before starting agent
|
||||
await persistFeatureUpdate(feature.id, updates);
|
||||
logger.info('Feature moved to in_progress, starting agent...');
|
||||
await handleRunFeature(feature);
|
||||
return true;
|
||||
} catch (error) {
|
||||
// Rollback to backlog if persist or run fails (e.g., server offline)
|
||||
logger.error('Failed to start feature, rolling back to backlog:', error);
|
||||
const rollbackUpdates = {
|
||||
status: 'backlog' as const,
|
||||
startedAt: undefined,
|
||||
};
|
||||
updateFeature(feature.id, rollbackUpdates);
|
||||
|
||||
// If server is offline (connection refused), redirect to login page
|
||||
if (isConnectionError(error)) {
|
||||
handleServerOffline();
|
||||
return false;
|
||||
}
|
||||
|
||||
toast.error('Failed to start feature', {
|
||||
description:
|
||||
error instanceof Error ? error.message : 'Server may be offline. Please try again.',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[
|
||||
autoMode,
|
||||
@@ -413,20 +484,20 @@ export function useBoardActions({
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode) {
|
||||
console.error('Auto mode API not available');
|
||||
logger.error('Auto mode API not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await api.autoMode.verifyFeature(currentProject.path, feature.id);
|
||||
|
||||
if (result.success) {
|
||||
console.log('[Board] Feature verification started successfully');
|
||||
logger.info('Feature verification started successfully');
|
||||
} else {
|
||||
console.error('[Board] Failed to verify feature:', result.error);
|
||||
logger.error('Failed to verify feature:', result.error);
|
||||
await loadFeatures();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Board] Error verifying feature:', error);
|
||||
logger.error('Error verifying feature:', error);
|
||||
await loadFeatures();
|
||||
}
|
||||
},
|
||||
@@ -435,20 +506,20 @@ export function useBoardActions({
|
||||
|
||||
const handleResumeFeature = useCallback(
|
||||
async (feature: Feature) => {
|
||||
console.log('[Board] handleResumeFeature called for feature:', feature.id);
|
||||
logger.info('handleResumeFeature called for feature:', feature.id);
|
||||
if (!currentProject) {
|
||||
console.error('[Board] No current project');
|
||||
logger.error('No current project');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode) {
|
||||
console.error('[Board] Auto mode API not available');
|
||||
logger.error('Auto mode API not available');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[Board] Calling resumeFeature API...', {
|
||||
logger.info('Calling resumeFeature API...', {
|
||||
projectPath: currentProject.path,
|
||||
featureId: feature.id,
|
||||
useWorktrees,
|
||||
@@ -460,16 +531,16 @@ export function useBoardActions({
|
||||
useWorktrees
|
||||
);
|
||||
|
||||
console.log('[Board] resumeFeature result:', result);
|
||||
logger.info('resumeFeature result:', result);
|
||||
|
||||
if (result.success) {
|
||||
console.log('[Board] Feature resume started successfully');
|
||||
logger.info('Feature resume started successfully');
|
||||
} else {
|
||||
console.error('[Board] Failed to resume feature:', result.error);
|
||||
logger.error('Failed to resume feature:', result.error);
|
||||
await loadFeatures();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Board] Error resuming feature:', error);
|
||||
logger.error('Error resuming feature:', error);
|
||||
await loadFeatures();
|
||||
}
|
||||
},
|
||||
@@ -520,10 +591,11 @@ export function useBoardActions({
|
||||
|
||||
const featureId = followUpFeature.id;
|
||||
const featureDescription = followUpFeature.description;
|
||||
const previousStatus = followUpFeature.status;
|
||||
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode?.followUpFeature) {
|
||||
console.error('Follow-up feature API not available');
|
||||
logger.error('Follow-up feature API not available');
|
||||
toast.error('Follow-up not available', {
|
||||
description: 'This feature is not available in the current version.',
|
||||
});
|
||||
@@ -536,35 +608,53 @@ export function useBoardActions({
|
||||
justFinishedAt: undefined,
|
||||
};
|
||||
updateFeature(featureId, updates);
|
||||
persistFeatureUpdate(featureId, updates);
|
||||
|
||||
setShowFollowUpDialog(false);
|
||||
setFollowUpFeature(null);
|
||||
setFollowUpPrompt('');
|
||||
setFollowUpImagePaths([]);
|
||||
setFollowUpPreviewMap(new Map());
|
||||
try {
|
||||
await persistFeatureUpdate(featureId, updates);
|
||||
|
||||
toast.success('Follow-up started', {
|
||||
description: `Continuing work on: ${truncateDescription(featureDescription)}`,
|
||||
});
|
||||
setShowFollowUpDialog(false);
|
||||
setFollowUpFeature(null);
|
||||
setFollowUpPrompt('');
|
||||
setFollowUpImagePaths([]);
|
||||
setFollowUpPreviewMap(new Map());
|
||||
|
||||
const imagePaths = followUpImagePaths.map((img) => img.path);
|
||||
// Server derives workDir from feature.branchName at execution time
|
||||
api.autoMode
|
||||
.followUpFeature(
|
||||
toast.success('Follow-up started', {
|
||||
description: `Continuing work on: ${truncateDescription(featureDescription)}`,
|
||||
});
|
||||
|
||||
const imagePaths = followUpImagePaths.map((img) => img.path);
|
||||
// Server derives workDir from feature.branchName at execution time
|
||||
const result = await api.autoMode.followUpFeature(
|
||||
currentProject.path,
|
||||
followUpFeature.id,
|
||||
followUpPrompt,
|
||||
imagePaths
|
||||
// No worktreePath - server derives from feature.branchName
|
||||
)
|
||||
.catch((error) => {
|
||||
console.error('[Board] Error sending follow-up:', error);
|
||||
toast.error('Failed to send follow-up', {
|
||||
description: error instanceof Error ? error.message : 'An error occurred',
|
||||
});
|
||||
loadFeatures();
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to send follow-up');
|
||||
}
|
||||
} catch (error) {
|
||||
// Rollback to previous status if follow-up fails
|
||||
logger.error('Error sending follow-up, rolling back:', error);
|
||||
const rollbackUpdates = {
|
||||
status: previousStatus as 'backlog' | 'in_progress' | 'waiting_approval' | 'verified',
|
||||
startedAt: undefined,
|
||||
};
|
||||
updateFeature(featureId, rollbackUpdates);
|
||||
|
||||
// If server is offline (connection refused), redirect to login page
|
||||
if (isConnectionError(error)) {
|
||||
handleServerOffline();
|
||||
return;
|
||||
}
|
||||
|
||||
toast.error('Failed to send follow-up', {
|
||||
description:
|
||||
error instanceof Error ? error.message : 'Server may be offline. Please try again.',
|
||||
});
|
||||
}
|
||||
}, [
|
||||
currentProject,
|
||||
followUpFeature,
|
||||
@@ -577,7 +667,6 @@ export function useBoardActions({
|
||||
setFollowUpPrompt,
|
||||
setFollowUpImagePaths,
|
||||
setFollowUpPreviewMap,
|
||||
loadFeatures,
|
||||
]);
|
||||
|
||||
const handleCommitFeature = useCallback(
|
||||
@@ -587,7 +676,7 @@ export function useBoardActions({
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode?.commitFeature) {
|
||||
console.error('Commit feature API not available');
|
||||
logger.error('Commit feature API not available');
|
||||
toast.error('Commit not available', {
|
||||
description: 'This feature is not available in the current version.',
|
||||
});
|
||||
@@ -610,14 +699,14 @@ export function useBoardActions({
|
||||
// Refresh worktree selector to update commit counts
|
||||
onWorktreeCreated?.();
|
||||
} else {
|
||||
console.error('[Board] Failed to commit feature:', result.error);
|
||||
logger.error('Failed to commit feature:', result.error);
|
||||
toast.error('Failed to commit feature', {
|
||||
description: result.error || 'An error occurred',
|
||||
});
|
||||
await loadFeatures();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Board] Error committing feature:', error);
|
||||
logger.error('Error committing feature:', error);
|
||||
toast.error('Failed to commit feature', {
|
||||
description: error instanceof Error ? error.message : 'An error occurred',
|
||||
});
|
||||
@@ -634,7 +723,7 @@ export function useBoardActions({
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.worktree?.mergeFeature) {
|
||||
console.error('Worktree API not available');
|
||||
logger.error('Worktree API not available');
|
||||
toast.error('Merge not available', {
|
||||
description: 'This feature is not available in the current version.',
|
||||
});
|
||||
@@ -651,13 +740,13 @@ export function useBoardActions({
|
||||
)}`,
|
||||
});
|
||||
} else {
|
||||
console.error('[Board] Failed to merge feature:', result.error);
|
||||
logger.error('Failed to merge feature:', result.error);
|
||||
toast.error('Failed to merge feature', {
|
||||
description: result.error || 'An error occurred',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Board] Error merging feature:', error);
|
||||
logger.error('Error merging feature:', error);
|
||||
toast.error('Failed to merge feature', {
|
||||
description: error instanceof Error ? error.message : 'An error occurred',
|
||||
});
|
||||
@@ -747,7 +836,7 @@ export function useBoardActions({
|
||||
: `Stopped working on: ${truncateDescription(feature.description)}`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[Board] Error stopping feature:', error);
|
||||
logger.error('Error stopping feature:', error);
|
||||
toast.error('Failed to stop agent', {
|
||||
description: error instanceof Error ? error.message : 'An error occurred',
|
||||
});
|
||||
@@ -805,12 +894,14 @@ export function useBoardActions({
|
||||
// Sort by priority (lower number = higher priority, priority 1 is highest)
|
||||
// Features with blocking dependencies are sorted to the end
|
||||
const sortedBacklog = [...backlogFeatures].sort((a, b) => {
|
||||
const aBlocked = enableDependencyBlocking
|
||||
? getBlockingDependencies(a, features).length > 0
|
||||
: false;
|
||||
const bBlocked = enableDependencyBlocking
|
||||
? getBlockingDependencies(b, features).length > 0
|
||||
: false;
|
||||
const aBlocked =
|
||||
enableDependencyBlocking && !skipVerificationInAutoMode
|
||||
? getBlockingDependencies(a, features).length > 0
|
||||
: false;
|
||||
const bBlocked =
|
||||
enableDependencyBlocking && !skipVerificationInAutoMode
|
||||
? getBlockingDependencies(b, features).length > 0
|
||||
: false;
|
||||
|
||||
// Blocked features go to the end
|
||||
if (aBlocked && !bBlocked) return 1;
|
||||
@@ -822,14 +913,14 @@ export function useBoardActions({
|
||||
|
||||
// Find the first feature without blocking dependencies
|
||||
const featureToStart = sortedBacklog.find((f) => {
|
||||
if (!enableDependencyBlocking) return true;
|
||||
if (!enableDependencyBlocking || skipVerificationInAutoMode) return true;
|
||||
return getBlockingDependencies(f, features).length === 0;
|
||||
});
|
||||
|
||||
if (!featureToStart) {
|
||||
toast.info('No eligible features', {
|
||||
description:
|
||||
'All backlog features have unmet dependencies. Complete their dependencies first.',
|
||||
'All backlog features have unmet dependencies. Complete their dependencies first (or enable "Skip verification requirement" in Auto Mode settings).',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -846,6 +937,7 @@ export function useBoardActions({
|
||||
isPrimaryWorktreeBranch,
|
||||
getPrimaryWorktreeBranch,
|
||||
enableDependencyBlocking,
|
||||
skipVerificationInAutoMode,
|
||||
]);
|
||||
|
||||
const handleArchiveAllVerified = useCallback(async () => {
|
||||
@@ -857,7 +949,7 @@ export function useBoardActions({
|
||||
try {
|
||||
await autoMode.stopFeature(feature.id);
|
||||
} catch (error) {
|
||||
console.error('[Board] Error stopping feature before archive:', error);
|
||||
logger.error('Error stopping feature before archive:', error);
|
||||
}
|
||||
}
|
||||
// Archive the feature by setting status to completed
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useAppStore, defaultBackgroundSettings } from '@/store/app-store';
|
||||
import { getServerUrlSync } from '@/lib/http-api-client';
|
||||
import { getAuthenticatedImageUrl } from '@/lib/api-fetch';
|
||||
|
||||
interface UseBoardBackgroundProps {
|
||||
currentProject: { path: string; id: string } | null;
|
||||
@@ -22,14 +22,14 @@ export function useBoardBackground({ currentProject }: UseBoardBackgroundProps)
|
||||
return {};
|
||||
}
|
||||
|
||||
const imageUrl = getAuthenticatedImageUrl(
|
||||
backgroundSettings.imagePath,
|
||||
currentProject.path,
|
||||
backgroundSettings.imageVersion
|
||||
);
|
||||
|
||||
return {
|
||||
backgroundImage: `url(${
|
||||
import.meta.env.VITE_SERVER_URL || getServerUrlSync()
|
||||
}/api/fs/image?path=${encodeURIComponent(
|
||||
backgroundSettings.imagePath
|
||||
)}&projectPath=${encodeURIComponent(currentProject.path)}${
|
||||
backgroundSettings.imageVersion ? `&v=${backgroundSettings.imageVersion}` : ''
|
||||
})`,
|
||||
backgroundImage: `url(${imageUrl})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-nocheck
|
||||
import { useMemo, useCallback } from 'react';
|
||||
import { Feature, useAppStore } from '@/store/app-store';
|
||||
import { resolveDependencies, getBlockingDependencies } from '@automaker/dependency-resolver';
|
||||
@@ -69,9 +70,21 @@ export function useBoardColumnFeatures({
|
||||
// We're viewing main but branch hasn't been initialized yet
|
||||
// (worktrees disabled or haven't loaded yet).
|
||||
// Show features assigned to primary worktree's branch.
|
||||
matchesWorktree = projectPath
|
||||
? useAppStore.getState().isPrimaryWorktreeBranch(projectPath, featureBranch)
|
||||
: false;
|
||||
if (projectPath) {
|
||||
const worktrees = useAppStore.getState().worktreesByProject[projectPath] ?? [];
|
||||
if (worktrees.length === 0) {
|
||||
// Worktrees not loaded yet - fallback to showing features on common default branches
|
||||
// This prevents features from disappearing during initial load
|
||||
matchesWorktree =
|
||||
featureBranch === 'main' || featureBranch === 'master' || featureBranch === 'develop';
|
||||
} else {
|
||||
matchesWorktree = useAppStore
|
||||
.getState()
|
||||
.isPrimaryWorktreeBranch(projectPath, featureBranch);
|
||||
}
|
||||
} else {
|
||||
matchesWorktree = false;
|
||||
}
|
||||
} else {
|
||||
// Match by branch name
|
||||
matchesWorktree = featureBranch === effectiveBranch;
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { createLogger } from '@automaker/utils/logger';
|
||||
import { DragStartEvent, DragEndEvent } from '@dnd-kit/core';
|
||||
import { Feature } from '@/store/app-store';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { toast } from 'sonner';
|
||||
import { COLUMNS, ColumnId } from '../constants';
|
||||
|
||||
const logger = createLogger('BoardDragDrop');
|
||||
|
||||
interface UseBoardDragDropProps {
|
||||
features: Feature[];
|
||||
currentProject: { path: string; id: string } | null;
|
||||
@@ -63,7 +66,7 @@ export function useBoardDragDrop({
|
||||
if (draggedFeature.status === 'in_progress') {
|
||||
// Only allow dragging in_progress if it's not currently running
|
||||
if (isRunningTask) {
|
||||
console.log('[Board] Cannot drag feature - currently running');
|
||||
logger.debug('Cannot drag feature - currently running');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { createLogger } from '@automaker/utils/logger';
|
||||
|
||||
const logger = createLogger('BoardEffects');
|
||||
|
||||
interface UseBoardEffectsProps {
|
||||
currentProject: { path: string; id: string } | null;
|
||||
specCreatingForProject: string | null;
|
||||
setSpecCreatingForProject: (path: string | null) => void;
|
||||
setSuggestionsCount: (count: number) => void;
|
||||
setFeatureSuggestions: (suggestions: any[]) => void;
|
||||
setIsGeneratingSuggestions: (generating: boolean) => void;
|
||||
checkContextExists: (featureId: string) => Promise<boolean>;
|
||||
features: any[];
|
||||
isLoading: boolean;
|
||||
@@ -20,9 +20,6 @@ export function useBoardEffects({
|
||||
currentProject,
|
||||
specCreatingForProject,
|
||||
setSpecCreatingForProject,
|
||||
setSuggestionsCount,
|
||||
setFeatureSuggestions,
|
||||
setIsGeneratingSuggestions,
|
||||
checkContextExists,
|
||||
features,
|
||||
isLoading,
|
||||
@@ -44,38 +41,13 @@ export function useBoardEffects({
|
||||
};
|
||||
}, [currentProject]);
|
||||
|
||||
// Listen for suggestions events to update count (persists even when dialog is closed)
|
||||
useEffect(() => {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.suggestions) return;
|
||||
|
||||
const unsubscribe = api.suggestions.onEvent((event) => {
|
||||
if (event.type === 'suggestions_complete' && event.suggestions) {
|
||||
setSuggestionsCount(event.suggestions.length);
|
||||
setFeatureSuggestions(event.suggestions);
|
||||
setIsGeneratingSuggestions(false);
|
||||
} else if (event.type === 'suggestions_error') {
|
||||
setIsGeneratingSuggestions(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [setSuggestionsCount, setFeatureSuggestions, setIsGeneratingSuggestions]);
|
||||
|
||||
// Subscribe to spec regeneration events to clear creating state on completion
|
||||
useEffect(() => {
|
||||
const api = getElectronAPI();
|
||||
if (!api.specRegeneration) return;
|
||||
|
||||
const unsubscribe = api.specRegeneration.onEvent((event) => {
|
||||
console.log(
|
||||
'[BoardView] Spec regeneration event:',
|
||||
event.type,
|
||||
'for project:',
|
||||
event.projectPath
|
||||
);
|
||||
logger.info('Spec regeneration event:', event.type, 'for project:', event.projectPath);
|
||||
|
||||
if (event.projectPath !== specCreatingForProject) {
|
||||
return;
|
||||
@@ -108,7 +80,7 @@ export function useBoardEffects({
|
||||
const { clearRunningTasks, addRunningTask } = useAppStore.getState();
|
||||
|
||||
if (status.runningFeatures) {
|
||||
console.log('[Board] Syncing running tasks from backend:', status.runningFeatures);
|
||||
logger.info('Syncing running tasks from backend:', status.runningFeatures);
|
||||
|
||||
clearRunningTasks(projectId);
|
||||
|
||||
@@ -118,7 +90,7 @@ export function useBoardEffects({
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Board] Failed to sync running tasks:', error);
|
||||
logger.error('Failed to sync running tasks:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@ import { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import { useAppStore, Feature } from '@/store/app-store';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { toast } from 'sonner';
|
||||
import { createLogger } from '@automaker/utils/logger';
|
||||
|
||||
const logger = createLogger('BoardFeatures');
|
||||
|
||||
interface UseBoardFeaturesProps {
|
||||
currentProject: { path: string; id: string } | null;
|
||||
@@ -32,7 +35,7 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) {
|
||||
// If project switched, mark it but don't clear features yet
|
||||
// We'll clear after successful API load to prevent data loss
|
||||
if (isProjectSwitch) {
|
||||
console.log(`[BoardView] Project switch detected: ${previousPath} -> ${currentPath}`);
|
||||
logger.info(`Project switch detected: ${previousPath} -> ${currentPath}`);
|
||||
isSwitchingProjectRef.current = true;
|
||||
isInitialLoadRef.current = true;
|
||||
}
|
||||
@@ -48,7 +51,7 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) {
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api.features) {
|
||||
console.error('[BoardView] Features API not available');
|
||||
logger.error('Features API not available');
|
||||
// Keep cached features if API is unavailable
|
||||
return;
|
||||
}
|
||||
@@ -72,8 +75,19 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) {
|
||||
if (isProjectSwitch) {
|
||||
setPersistedCategories([]);
|
||||
}
|
||||
|
||||
// Check for interrupted features and resume them
|
||||
// This handles server restarts where features were in pipeline steps
|
||||
if (api.autoMode?.resumeInterrupted) {
|
||||
try {
|
||||
await api.autoMode.resumeInterrupted(currentProject.path);
|
||||
logger.info('Checked for interrupted features');
|
||||
} catch (resumeError) {
|
||||
logger.warn('Failed to check for interrupted features:', resumeError);
|
||||
}
|
||||
}
|
||||
} else if (!result.success && result.error) {
|
||||
console.error('[BoardView] API returned error:', result.error);
|
||||
logger.error('API returned error:', result.error);
|
||||
// If it's a new project or the error indicates no features found,
|
||||
// that's expected - start with empty array
|
||||
if (isProjectSwitch) {
|
||||
@@ -83,7 +97,7 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) {
|
||||
// Otherwise keep cached features
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load features:', error);
|
||||
logger.error('Failed to load features:', error);
|
||||
// On error, keep existing cached features for the current project
|
||||
// Only clear on project switch if we have no features from server
|
||||
if (isProjectSwitch && cachedFeatures.length === 0) {
|
||||
@@ -115,7 +129,7 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) {
|
||||
setPersistedCategories([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load categories:', error);
|
||||
logger.error('Failed to load categories:', error);
|
||||
// If file doesn't exist, ensure categories are cleared
|
||||
setPersistedCategories([]);
|
||||
}
|
||||
@@ -147,7 +161,7 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) {
|
||||
setPersistedCategories(categories);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save category:', error);
|
||||
logger.error('Failed to save category:', error);
|
||||
}
|
||||
},
|
||||
[currentProject, persistedCategories]
|
||||
@@ -165,7 +179,7 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) {
|
||||
currentProject &&
|
||||
event.projectPath === currentProject.path
|
||||
) {
|
||||
console.log('[BoardView] Spec regeneration complete, refreshing features');
|
||||
logger.info('Spec regeneration complete, refreshing features');
|
||||
loadFeatures();
|
||||
}
|
||||
});
|
||||
@@ -190,27 +204,27 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) {
|
||||
|
||||
if (event.type === 'auto_mode_feature_complete') {
|
||||
// Reload features when a feature is completed
|
||||
console.log('[Board] Feature completed, reloading features...');
|
||||
logger.info('Feature completed, reloading features...');
|
||||
loadFeatures();
|
||||
// Play ding sound when feature is done (unless muted)
|
||||
const { muteDoneSound } = useAppStore.getState();
|
||||
if (!muteDoneSound) {
|
||||
const audio = new Audio('/sounds/ding.mp3');
|
||||
audio.play().catch((err) => console.warn('Could not play ding sound:', err));
|
||||
audio.play().catch((err) => logger.warn('Could not play ding sound:', err));
|
||||
}
|
||||
} else if (event.type === 'plan_approval_required') {
|
||||
// Reload features when plan is generated and requires approval
|
||||
// This ensures the feature card shows the "Approve Plan" button
|
||||
console.log('[Board] Plan approval required, reloading features...');
|
||||
logger.info('Plan approval required, reloading features...');
|
||||
loadFeatures();
|
||||
} else if (event.type === 'pipeline_step_started') {
|
||||
// Pipeline steps update the feature status to `pipeline_*` before the step runs.
|
||||
// Reload so the card moves into the correct pipeline column immediately.
|
||||
console.log('[Board] Pipeline step started, reloading features...');
|
||||
logger.info('Pipeline step started, reloading features...');
|
||||
loadFeatures();
|
||||
} else if (event.type === 'auto_mode_error') {
|
||||
// Reload features when an error occurs (feature moved to waiting_approval)
|
||||
console.log('[Board] Feature error, reloading features...', event.error);
|
||||
logger.info('Feature error, reloading features...', event.error);
|
||||
|
||||
// Remove from running tasks so it moves to the correct column
|
||||
if (event.featureId) {
|
||||
|
||||
@@ -2,6 +2,9 @@ import { useCallback } from 'react';
|
||||
import { Feature } from '@/store/app-store';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { createLogger } from '@automaker/utils/logger';
|
||||
|
||||
const logger = createLogger('BoardPersistence');
|
||||
|
||||
interface UseBoardPersistenceProps {
|
||||
currentProject: { path: string; id: string } | null;
|
||||
@@ -12,22 +15,35 @@ export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps
|
||||
|
||||
// Persist feature update to API (replaces saveFeatures)
|
||||
const persistFeatureUpdate = useCallback(
|
||||
async (featureId: string, updates: Partial<Feature>) => {
|
||||
async (
|
||||
featureId: string,
|
||||
updates: Partial<Feature>,
|
||||
descriptionHistorySource?: 'enhance' | 'edit',
|
||||
enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer',
|
||||
preEnhancementDescription?: string
|
||||
) => {
|
||||
if (!currentProject) return;
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api.features) {
|
||||
console.error('[BoardView] Features API not available');
|
||||
logger.error('Features API not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await api.features.update(currentProject.path, featureId, updates);
|
||||
const result = await api.features.update(
|
||||
currentProject.path,
|
||||
featureId,
|
||||
updates,
|
||||
descriptionHistorySource,
|
||||
enhancementMode,
|
||||
preEnhancementDescription
|
||||
);
|
||||
if (result.success && result.feature) {
|
||||
updateFeature(result.feature.id, result.feature);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to persist feature update:', error);
|
||||
logger.error('Failed to persist feature update:', error);
|
||||
}
|
||||
},
|
||||
[currentProject, updateFeature]
|
||||
@@ -41,7 +57,7 @@ export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api.features) {
|
||||
console.error('[BoardView] Features API not available');
|
||||
logger.error('Features API not available');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -50,7 +66,7 @@ export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps
|
||||
updateFeature(result.feature.id, result.feature);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to persist feature creation:', error);
|
||||
logger.error('Failed to persist feature creation:', error);
|
||||
}
|
||||
},
|
||||
[currentProject, updateFeature]
|
||||
@@ -64,13 +80,13 @@ export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api.features) {
|
||||
console.error('[BoardView] Features API not available');
|
||||
logger.error('Features API not available');
|
||||
return;
|
||||
}
|
||||
|
||||
await api.features.delete(currentProject.path, featureId);
|
||||
} catch (error) {
|
||||
console.error('Failed to persist feature deletion:', error);
|
||||
logger.error('Failed to persist feature deletion:', error);
|
||||
}
|
||||
},
|
||||
[currentProject]
|
||||
|
||||
@@ -4,13 +4,18 @@ import {
|
||||
FeatureImagePath as DescriptionImagePath,
|
||||
ImagePreviewMap,
|
||||
} from '@/components/ui/description-image-dropzone';
|
||||
import type { FollowUpHistoryEntry } from '../dialogs/follow-up-dialog';
|
||||
|
||||
/**
|
||||
* Custom hook for managing follow-up dialog state including prompt history
|
||||
*/
|
||||
export function useFollowUpState() {
|
||||
const [showFollowUpDialog, setShowFollowUpDialog] = useState(false);
|
||||
const [followUpFeature, setFollowUpFeature] = useState<Feature | null>(null);
|
||||
const [followUpPrompt, setFollowUpPrompt] = useState('');
|
||||
const [followUpImagePaths, setFollowUpImagePaths] = useState<DescriptionImagePath[]>([]);
|
||||
const [followUpPreviewMap, setFollowUpPreviewMap] = useState<ImagePreviewMap>(() => new Map());
|
||||
const [followUpPromptHistory, setFollowUpPromptHistory] = useState<FollowUpHistoryEntry[]>([]);
|
||||
|
||||
const resetFollowUpState = useCallback(() => {
|
||||
setShowFollowUpDialog(false);
|
||||
@@ -18,6 +23,7 @@ export function useFollowUpState() {
|
||||
setFollowUpPrompt('');
|
||||
setFollowUpImagePaths([]);
|
||||
setFollowUpPreviewMap(new Map());
|
||||
setFollowUpPromptHistory([]);
|
||||
}, []);
|
||||
|
||||
const handleFollowUpDialogChange = useCallback(
|
||||
@@ -31,6 +37,13 @@ export function useFollowUpState() {
|
||||
[resetFollowUpState]
|
||||
);
|
||||
|
||||
/**
|
||||
* Adds a new entry to the prompt history
|
||||
*/
|
||||
const addToPromptHistory = useCallback((entry: FollowUpHistoryEntry) => {
|
||||
setFollowUpPromptHistory((prev) => [...prev, entry]);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
// State
|
||||
showFollowUpDialog,
|
||||
@@ -38,14 +51,17 @@ export function useFollowUpState() {
|
||||
followUpPrompt,
|
||||
followUpImagePaths,
|
||||
followUpPreviewMap,
|
||||
followUpPromptHistory,
|
||||
// Setters
|
||||
setShowFollowUpDialog,
|
||||
setFollowUpFeature,
|
||||
setFollowUpPrompt,
|
||||
setFollowUpImagePaths,
|
||||
setFollowUpPreviewMap,
|
||||
setFollowUpPromptHistory,
|
||||
// Helpers
|
||||
resetFollowUpState,
|
||||
handleFollowUpDialogChange,
|
||||
addToPromptHistory,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
|
||||
interface UseSelectionModeReturn {
|
||||
isSelectionMode: boolean;
|
||||
selectedFeatureIds: Set<string>;
|
||||
selectedCount: number;
|
||||
toggleSelectionMode: () => void;
|
||||
toggleFeatureSelection: (featureId: string) => void;
|
||||
selectAll: (featureIds: string[]) => void;
|
||||
clearSelection: () => void;
|
||||
isFeatureSelected: (featureId: string) => boolean;
|
||||
exitSelectionMode: () => void;
|
||||
}
|
||||
|
||||
export function useSelectionMode(): UseSelectionModeReturn {
|
||||
const [isSelectionMode, setIsSelectionMode] = useState(false);
|
||||
const [selectedFeatureIds, setSelectedFeatureIds] = useState<Set<string>>(new Set());
|
||||
|
||||
const toggleSelectionMode = useCallback(() => {
|
||||
setIsSelectionMode((prev) => {
|
||||
if (prev) {
|
||||
// Exiting selection mode - clear selection
|
||||
setSelectedFeatureIds(new Set());
|
||||
}
|
||||
return !prev;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const exitSelectionMode = useCallback(() => {
|
||||
setIsSelectionMode(false);
|
||||
setSelectedFeatureIds(new Set());
|
||||
}, []);
|
||||
|
||||
const toggleFeatureSelection = useCallback((featureId: string) => {
|
||||
setSelectedFeatureIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(featureId)) {
|
||||
next.delete(featureId);
|
||||
} else {
|
||||
next.add(featureId);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const selectAll = useCallback((featureIds: string[]) => {
|
||||
setSelectedFeatureIds(new Set(featureIds));
|
||||
}, []);
|
||||
|
||||
const clearSelection = useCallback(() => {
|
||||
setSelectedFeatureIds(new Set());
|
||||
}, []);
|
||||
|
||||
const isFeatureSelected = useCallback(
|
||||
(featureId: string) => selectedFeatureIds.has(featureId),
|
||||
[selectedFeatureIds]
|
||||
);
|
||||
|
||||
// Handle Escape key to exit selection mode
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && isSelectionMode) {
|
||||
exitSelectionMode();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [isSelectionMode, exitSelectionMode]);
|
||||
|
||||
return {
|
||||
isSelectionMode,
|
||||
selectedFeatureIds,
|
||||
selectedCount: selectedFeatureIds.size,
|
||||
toggleSelectionMode,
|
||||
toggleFeatureSelection,
|
||||
selectAll,
|
||||
clearSelection,
|
||||
isFeatureSelected,
|
||||
exitSelectionMode,
|
||||
};
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import type { FeatureSuggestion } from '@/lib/electron';
|
||||
|
||||
export function useSuggestionsState() {
|
||||
const [showSuggestionsDialog, setShowSuggestionsDialog] = useState(false);
|
||||
const [suggestionsCount, setSuggestionsCount] = useState(0);
|
||||
const [featureSuggestions, setFeatureSuggestions] = useState<FeatureSuggestion[]>([]);
|
||||
const [isGeneratingSuggestions, setIsGeneratingSuggestions] = useState(false);
|
||||
|
||||
const updateSuggestions = useCallback((suggestions: FeatureSuggestion[]) => {
|
||||
setFeatureSuggestions(suggestions);
|
||||
setSuggestionsCount(suggestions.length);
|
||||
}, []);
|
||||
|
||||
const closeSuggestionsDialog = useCallback(() => {
|
||||
setShowSuggestionsDialog(false);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
// State
|
||||
showSuggestionsDialog,
|
||||
suggestionsCount,
|
||||
featureSuggestions,
|
||||
isGeneratingSuggestions,
|
||||
// Setters
|
||||
setShowSuggestionsDialog,
|
||||
setSuggestionsCount,
|
||||
setFeatureSuggestions,
|
||||
setIsGeneratingSuggestions,
|
||||
// Helpers
|
||||
updateSuggestions,
|
||||
closeSuggestionsDialog,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user