Merge branch 'v0.13.0rc' into feat/react-query

Merged latest changes from v0.13.0rc into feat/react-query while preserving
React Query migration. Key merge decisions:

- Kept React Query hooks for data fetching (useRunningAgents, useStopFeature, etc.)
- Added backlog plan handling to running-agents-view stop functionality
- Imported both SkeletonPulse and Spinner for CLI status components
- Used Spinner for refresh buttons across all settings sections
- Preserved isBacklogPlan check in agent-output-modal TaskProgressPanel
- Added handleOpenInIntegratedTerminal to worktree actions while keeping React Query mutations
This commit is contained in:
Shirone
2026-01-19 13:28:43 +01:00
387 changed files with 28102 additions and 6881 deletions

View File

@@ -7,5 +7,5 @@ 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 { useSelectionMode } from './use-selection-mode';
export { useSelectionMode, type SelectionTarget } from './use-selection-mode';
export { useListViewState } from './use-list-view-state';

View File

@@ -117,6 +117,7 @@ export function useBoardActions({
planningMode: PlanningMode;
requirePlanApproval: boolean;
dependencies?: string[];
childDependencies?: string[]; // Feature IDs that should depend on this feature
workMode?: 'current' | 'auto' | 'custom';
}) => {
const workMode = featureData.workMode || 'current';
@@ -131,8 +132,10 @@ export function useBoardActions({
// 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';
// Auto-generate a branch name based on primary branch (main/master) and timestamp
// Always use primary branch to avoid nested feature/feature/... paths
const baseBranch =
(currentProject?.path ? getPrimaryWorktreeBranch(currentProject.path) : null) || 'main';
const timestamp = Date.now();
const randomSuffix = Math.random().toString(36).substring(2, 6);
finalBranchName = `feature/${baseBranch}-${timestamp}-${randomSuffix}`;
@@ -194,6 +197,21 @@ export function useBoardActions({
await persistFeatureCreate(createdFeature);
saveCategory(featureData.category);
// Handle child dependencies - update other features to depend on this new feature
if (featureData.childDependencies && featureData.childDependencies.length > 0) {
for (const childId of featureData.childDependencies) {
const childFeature = features.find((f) => f.id === childId);
if (childFeature) {
const childDeps = childFeature.dependencies || [];
if (!childDeps.includes(createdFeature.id)) {
const newDeps = [...childDeps, createdFeature.id];
updateFeature(childId, { dependencies: newDeps });
persistFeatureUpdate(childId, { dependencies: newDeps });
}
}
}
}
// Generate title in the background if needed (non-blocking)
if (needsTitleGeneration) {
const api = getElectronAPI();
@@ -234,7 +252,8 @@ export function useBoardActions({
currentProject,
onWorktreeCreated,
onWorktreeAutoSelect,
currentWorktreeBranch,
getPrimaryWorktreeBranch,
features,
]
);
@@ -255,6 +274,8 @@ export function useBoardActions({
planningMode?: PlanningMode;
requirePlanApproval?: boolean;
workMode?: 'current' | 'auto' | 'custom';
dependencies?: string[];
childDependencies?: string[]; // Feature IDs that should depend on this feature
},
descriptionHistorySource?: 'enhance' | 'edit',
enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer',
@@ -268,7 +289,10 @@ export function useBoardActions({
if (workMode === 'current') {
finalBranchName = undefined;
} else if (workMode === 'auto') {
const baseBranch = currentWorktreeBranch || 'main';
// Auto-generate a branch name based on primary branch (main/master) and timestamp
// Always use primary branch to avoid nested feature/feature/... paths
const baseBranch =
(currentProject?.path ? getPrimaryWorktreeBranch(currentProject.path) : null) || 'main';
const timestamp = Date.now();
const randomSuffix = Math.random().toString(36).substring(2, 6);
finalBranchName = `feature/${baseBranch}-${timestamp}-${randomSuffix}`;
@@ -308,8 +332,11 @@ export function useBoardActions({
}
}
// Separate child dependencies from the main updates (they affect other features)
const { childDependencies, ...restUpdates } = updates;
const finalUpdates = {
...updates,
...restUpdates,
title: updates.title,
branchName: finalBranchName,
};
@@ -322,6 +349,45 @@ export function useBoardActions({
enhancementMode,
preEnhancementDescription
);
// Handle child dependency changes
// This updates other features' dependencies arrays
if (childDependencies !== undefined) {
// Find current child dependencies (features that have this feature in their dependencies)
const currentChildDeps = features
.filter((f) => f.dependencies?.includes(featureId))
.map((f) => f.id);
// Find features to add this feature as a dependency (new child deps)
const toAdd = childDependencies.filter((id) => !currentChildDeps.includes(id));
// Find features to remove this feature as a dependency (removed child deps)
const toRemove = currentChildDeps.filter((id) => !childDependencies.includes(id));
// Add this feature as a dependency to new child features
for (const childId of toAdd) {
const childFeature = features.find((f) => f.id === childId);
if (childFeature) {
const childDeps = childFeature.dependencies || [];
if (!childDeps.includes(featureId)) {
const newDeps = [...childDeps, featureId];
updateFeature(childId, { dependencies: newDeps });
persistFeatureUpdate(childId, { dependencies: newDeps });
}
}
}
// Remove this feature as a dependency from removed child features
for (const childId of toRemove) {
const childFeature = features.find((f) => f.id === childId);
if (childFeature) {
const childDeps = childFeature.dependencies || [];
const newDeps = childDeps.filter((depId) => depId !== featureId);
updateFeature(childId, { dependencies: newDeps });
persistFeatureUpdate(childId, { dependencies: newDeps });
}
}
}
if (updates.category) {
saveCategory(updates.category);
}
@@ -334,7 +400,8 @@ export function useBoardActions({
setEditingFeature,
currentProject,
onWorktreeCreated,
currentWorktreeBranch,
getPrimaryWorktreeBranch,
features,
]
);

View File

@@ -34,6 +34,7 @@ export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps
return;
}
logger.info('Calling API features.update', { featureId, updates });
const result = await api.features.update(
currentProject.path,
featureId,
@@ -42,12 +43,18 @@ export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps
enhancementMode,
preEnhancementDescription
);
logger.info('API features.update result', {
success: result.success,
feature: result.feature,
});
if (result.success && result.feature) {
updateFeature(result.feature.id, result.feature);
// Invalidate React Query cache to sync UI
queryClient.invalidateQueries({
queryKey: queryKeys.features.all(currentProject.path),
});
} else if (!result.success) {
logger.error('API features.update failed', result);
}
} catch (error) {
logger.error('Failed to persist feature update:', error);

View File

@@ -1,10 +1,13 @@
import { useState, useCallback, useEffect } from 'react';
export type SelectionTarget = 'backlog' | 'waiting_approval' | null;
interface UseSelectionModeReturn {
isSelectionMode: boolean;
selectionTarget: SelectionTarget;
selectedFeatureIds: Set<string>;
selectedCount: number;
toggleSelectionMode: () => void;
toggleSelectionMode: (target?: SelectionTarget) => void;
toggleFeatureSelection: (featureId: string) => void;
selectAll: (featureIds: string[]) => void;
clearSelection: () => void;
@@ -13,21 +16,26 @@ interface UseSelectionModeReturn {
}
export function useSelectionMode(): UseSelectionModeReturn {
const [isSelectionMode, setIsSelectionMode] = useState(false);
const [selectionTarget, setSelectionTarget] = useState<SelectionTarget>(null);
const [selectedFeatureIds, setSelectedFeatureIds] = useState<Set<string>>(new Set());
const toggleSelectionMode = useCallback(() => {
setIsSelectionMode((prev) => {
if (prev) {
const isSelectionMode = selectionTarget !== null;
const toggleSelectionMode = useCallback((target: SelectionTarget = 'backlog') => {
setSelectionTarget((prev) => {
if (prev === target) {
// Exiting selection mode - clear selection
setSelectedFeatureIds(new Set());
return null;
}
return !prev;
// Switching to a different target or entering selection mode
setSelectedFeatureIds(new Set());
return target;
});
}, []);
const exitSelectionMode = useCallback(() => {
setIsSelectionMode(false);
setSelectionTarget(null);
setSelectedFeatureIds(new Set());
}, []);
@@ -70,6 +78,7 @@ export function useSelectionMode(): UseSelectionModeReturn {
return {
isSelectionMode,
selectionTarget,
selectedFeatureIds,
selectedCount: selectedFeatureIds.size,
toggleSelectionMode,