feat: enhance board background settings and introduce animated borders

- Added default background settings to streamline background management across components.
- Implemented animated border styles for in-progress cards to improve visual feedback.
- Refactored BoardBackgroundModal and BoardView components to utilize the new default settings, ensuring consistent background behavior.
- Updated KanbanCard to support animated borders, enhancing the user experience during task progress.
- Improved Sidebar component by optimizing the fetching of running agents count with a more efficient use of hooks.
This commit is contained in:
Cody Seibert
2025-12-13 00:25:16 -05:00
committed by Kacper
parent e6d3e8e5a5
commit 8b2b7662ee
7 changed files with 166 additions and 210 deletions

View File

@@ -1397,6 +1397,39 @@
.text-running-indicator { .text-running-indicator {
color: var(--running-indicator-text); color: var(--running-indicator-text);
} }
/* Animated border for in-progress cards */
@keyframes border-rotate {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.animated-border-wrapper {
position: relative;
border-radius: 0.75rem;
padding: 2px;
background: linear-gradient(
90deg,
var(--running-indicator),
color-mix(in oklch, var(--running-indicator), transparent 50%),
var(--running-indicator),
color-mix(in oklch, var(--running-indicator), transparent 50%),
var(--running-indicator)
);
background-size: 200% 100%;
animation: border-rotate 3s ease infinite;
}
.animated-border-wrapper > * {
border-radius: calc(0.75rem - 2px);
}
} }
/* Retro Overrides for Utilities */ /* Retro Overrides for Utilities */

View File

@@ -14,7 +14,7 @@ import { Slider } from "@/components/ui/slider";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useAppStore } from "@/store/app-store"; import { useAppStore, defaultBackgroundSettings } from "@/store/app-store";
import { getHttpApiClient } from "@/lib/http-api-client"; import { getHttpApiClient } from "@/lib/http-api-client";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -55,27 +55,9 @@ export function BoardBackgroundModal({
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
// Get current background settings (live from store) // Get current background settings (live from store)
const backgroundSettings = currentProject const backgroundSettings =
? boardBackgroundByProject[currentProject.path] || { (currentProject && boardBackgroundByProject[currentProject.path]) ||
imagePath: null, defaultBackgroundSettings;
cardOpacity: 100,
columnOpacity: 100,
columnBorderEnabled: true,
cardGlassmorphism: true,
cardBorderEnabled: true,
cardBorderOpacity: 100,
hideScrollbar: false,
}
: {
imagePath: null,
cardOpacity: 100,
columnOpacity: 100,
columnBorderEnabled: true,
cardGlassmorphism: true,
cardBorderEnabled: true,
cardBorderOpacity: 100,
hideScrollbar: false,
};
const cardOpacity = backgroundSettings.cardOpacity; const cardOpacity = backgroundSettings.cardOpacity;
const columnOpacity = backgroundSettings.columnOpacity; const columnOpacity = backgroundSettings.columnOpacity;
@@ -84,6 +66,7 @@ export function BoardBackgroundModal({
const cardBorderEnabled = backgroundSettings.cardBorderEnabled; const cardBorderEnabled = backgroundSettings.cardBorderEnabled;
const cardBorderOpacity = backgroundSettings.cardBorderOpacity; const cardBorderOpacity = backgroundSettings.cardBorderOpacity;
const hideScrollbar = backgroundSettings.hideScrollbar; const hideScrollbar = backgroundSettings.hideScrollbar;
const imageVersion = backgroundSettings.imageVersion;
// Update preview image when background settings change // Update preview image when background settings change
useEffect(() => { useEffect(() => {
@@ -91,8 +74,8 @@ export function BoardBackgroundModal({
const serverUrl = const serverUrl =
process.env.NEXT_PUBLIC_SERVER_URL || "http://localhost:3008"; process.env.NEXT_PUBLIC_SERVER_URL || "http://localhost:3008";
// Add cache-busting query parameter to force browser to reload image // Add cache-busting query parameter to force browser to reload image
const cacheBuster = backgroundSettings.imageVersion const cacheBuster = imageVersion
? `&v=${backgroundSettings.imageVersion}` ? `&v=${imageVersion}`
: `&v=${Date.now()}`; : `&v=${Date.now()}`;
const imagePath = `${serverUrl}/api/fs/image?path=${encodeURIComponent( const imagePath = `${serverUrl}/api/fs/image?path=${encodeURIComponent(
backgroundSettings.imagePath backgroundSettings.imagePath
@@ -101,11 +84,7 @@ export function BoardBackgroundModal({
} else { } else {
setPreviewImage(null); setPreviewImage(null);
} }
}, [ }, [currentProject, backgroundSettings.imagePath, imageVersion]);
currentProject,
backgroundSettings.imagePath,
backgroundSettings.imageVersion,
]);
const fileToBase64 = (file: File): Promise<string> => { const fileToBase64 = (file: File): Promise<string> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View File

@@ -332,35 +332,32 @@ export function Sidebar() {
}; };
}, [setCurrentView]); }, [setCurrentView]);
// Fetch running agents count and update every 2 seconds // Fetch running agents count function - used for initial load and event-driven updates
useEffect(() => { const fetchRunningAgentsCount = useCallback(async () => {
const fetchRunningAgentsCount = async () => { try {
try { const api = getElectronAPI();
const api = getElectronAPI(); if (api.runningAgents) {
if (api.runningAgents) { const result = await api.runningAgents.getAll();
const result = await api.runningAgents.getAll(); if (result.success && result.runningAgents) {
if (result.success && result.runningAgents) { setRunningAgentsCount(result.runningAgents.length);
setRunningAgentsCount(result.runningAgents.length);
}
} }
} catch (error) {
console.error("[Sidebar] Error fetching running agents count:", error);
} }
}; } catch (error) {
console.error("[Sidebar] Error fetching running agents count:", error);
// Initial fetch }
fetchRunningAgentsCount();
// Set up interval to refresh every 2 seconds
const interval = setInterval(fetchRunningAgentsCount, 2000);
return () => clearInterval(interval);
}, []); }, []);
// Subscribe to auto-mode events to update running agents count in real-time // Subscribe to auto-mode events to update running agents count in real-time
useEffect(() => { useEffect(() => {
const api = getElectronAPI(); const api = getElectronAPI();
if (!api.autoMode) return; if (!api.autoMode) {
// If autoMode is not available, still fetch initial count
fetchRunningAgentsCount();
return;
}
// Initial fetch on mount
fetchRunningAgentsCount();
const unsubscribe = api.autoMode.onEvent((event) => { const unsubscribe = api.autoMode.onEvent((event) => {
// When a feature starts, completes, or errors, refresh the count // When a feature starts, completes, or errors, refresh the count
@@ -369,21 +366,6 @@ export function Sidebar() {
event.type === "auto_mode_error" || event.type === "auto_mode_error" ||
event.type === "auto_mode_feature_start" event.type === "auto_mode_feature_start"
) { ) {
const fetchRunningAgentsCount = async () => {
try {
if (api.runningAgents) {
const result = await api.runningAgents.getAll();
if (result.success && result.runningAgents) {
setRunningAgentsCount(result.runningAgents.length);
}
}
} catch (error) {
console.error(
"[Sidebar] Error fetching running agents count:",
error
);
}
};
fetchRunningAgentsCount(); fetchRunningAgentsCount();
} }
}); });
@@ -391,7 +373,7 @@ export function Sidebar() {
return () => { return () => {
unsubscribe(); unsubscribe();
}; };
}, []); }, [fetchRunningAgentsCount]);
// Handle creating initial spec for new project // Handle creating initial spec for new project
const handleCreateInitialSpec = useCallback(async () => { const handleCreateInitialSpec = useCallback(async () => {

View File

@@ -24,6 +24,7 @@ import {
AgentModel, AgentModel,
ThinkingLevel, ThinkingLevel,
AIProfile, AIProfile,
defaultBackgroundSettings,
} from "@/store/app-store"; } from "@/store/app-store";
import { getElectronAPI } from "@/lib/electron"; import { getElectronAPI } from "@/lib/electron";
import { cn, modelSupportsThinking } from "@/lib/utils"; import { cn, modelSupportsThinking } from "@/lib/utils";
@@ -1554,25 +1555,6 @@ export function BoardView() {
} }
}); });
// Sort waiting_approval column: justFinished features (within 2 minutes) go to the top
map.waiting_approval.sort((a, b) => {
// Helper to check if feature is "just finished" (within 2 minutes)
const isJustFinished = (feature: Feature) => {
if (!feature.justFinishedAt) return false;
const finishedTime = new Date(feature.justFinishedAt).getTime();
const now = Date.now();
const twoMinutes = 2 * 60 * 1000; // 2 minutes in milliseconds
return now - finishedTime < twoMinutes;
};
const aJustFinished = isJustFinished(a);
const bJustFinished = isJustFinished(b);
// Features with justFinishedAt within 2 minutes should appear first
if (aJustFinished && !bJustFinished) return -1;
if (!aJustFinished && bJustFinished) return 1;
return 0; // Keep original order for features with same justFinished status
});
return map; return map;
}, [features, runningAutoTasks, searchQuery]); }, [features, runningAutoTasks, searchQuery]);
@@ -1975,27 +1957,9 @@ export function BoardView() {
{/* Kanban Columns */} {/* Kanban Columns */}
{(() => { {(() => {
// Get background settings for current project // Get background settings for current project
const backgroundSettings = currentProject const backgroundSettings =
? boardBackgroundByProject[currentProject.path] || { (currentProject && boardBackgroundByProject[currentProject.path]) ||
imagePath: null, defaultBackgroundSettings;
cardOpacity: 100,
columnOpacity: 100,
columnBorderEnabled: true,
cardGlassmorphism: true,
cardBorderEnabled: true,
cardBorderOpacity: 100,
hideScrollbar: false,
}
: {
imagePath: null,
cardOpacity: 100,
columnOpacity: 100,
columnBorderEnabled: true,
cardGlassmorphism: true,
cardBorderEnabled: true,
cardBorderOpacity: 100,
hideScrollbar: false,
};
// Build background image style if image exists // Build background image style if image exists
const backgroundImageStyle = backgroundSettings.imagePath const backgroundImageStyle = backgroundSettings.imagePath

View File

@@ -309,26 +309,30 @@ export const KanbanCard = memo(function KanbanCard({
).borderColor = `color-mix(in oklch, var(--border) ${cardBorderOpacity}%, transparent)`; ).borderColor = `color-mix(in oklch, var(--border) ${cardBorderOpacity}%, transparent)`;
} }
return ( const cardElement = (
<Card <Card
ref={setNodeRef} ref={setNodeRef}
style={borderStyle} style={isCurrentAutoTask ? style : borderStyle}
className={cn( className={cn(
"cursor-grab active:cursor-grabbing transition-all relative kanban-card-content select-none", "cursor-grab active:cursor-grabbing transition-all relative kanban-card-content select-none",
// Apply border class when border is enabled and opacity is 100% // Apply border class when border is enabled and opacity is 100%
// When opacity is not 100%, we use inline styles for border color // When opacity is not 100%, we use inline styles for border color
cardBorderEnabled && cardBorderOpacity === 100 && "border-border", // Skip border classes when animated border is active (isCurrentAutoTask)
!isCurrentAutoTask &&
cardBorderEnabled &&
cardBorderOpacity === 100 &&
"border-border",
// When border is enabled but opacity is not 100%, we still need border width // When border is enabled but opacity is not 100%, we still need border width
cardBorderEnabled && cardBorderOpacity !== 100 && "border", !isCurrentAutoTask &&
cardBorderEnabled &&
cardBorderOpacity !== 100 &&
"border",
// Remove default background when using opacity overlay // Remove default background when using opacity overlay
!isDragging && "bg-transparent", !isDragging && "bg-transparent",
// Remove default backdrop-blur-sm from Card component when glassmorphism is disabled // Remove default backdrop-blur-sm from Card component when glassmorphism is disabled
!glassmorphism && "backdrop-blur-[0px]!", !glassmorphism && "backdrop-blur-[0px]!",
isDragging && "scale-105 shadow-lg", isDragging && "scale-105 shadow-lg",
// Special border styles for running/error states override the border opacity // Error state border (only when not in progress)
// These need to be applied with higher specificity
isCurrentAutoTask &&
"border-running-indicator border-2 shadow-running-indicator/50 shadow-lg animate-pulse",
feature.error && feature.error &&
!isCurrentAutoTask && !isCurrentAutoTask &&
"border-red-500 border-2 shadow-red-500/30 shadow-lg", "border-red-500 border-2 shadow-red-500/30 shadow-lg",
@@ -1056,4 +1060,11 @@ export const KanbanCard = memo(function KanbanCard({
</Dialog> </Dialog>
</Card> </Card>
); );
// Wrap with animated border when in progress
if (isCurrentAutoTask) {
return <div className="animated-border-wrapper">{cardElement}</div>;
}
return cardElement;
}); });

View File

@@ -388,6 +388,28 @@ export interface AppState {
previewTheme: ThemeMode | null; previewTheme: ThemeMode | null;
} }
// Default background settings for board backgrounds
export const defaultBackgroundSettings: {
imagePath: string | null;
imageVersion?: number;
cardOpacity: number;
columnOpacity: number;
columnBorderEnabled: boolean;
cardGlassmorphism: boolean;
cardBorderEnabled: boolean;
cardBorderOpacity: number;
hideScrollbar: boolean;
} = {
imagePath: null,
cardOpacity: 100,
columnOpacity: 100,
columnBorderEnabled: true,
cardGlassmorphism: true,
cardBorderEnabled: true,
cardBorderOpacity: 100,
hideScrollbar: false,
};
export interface AutoModeActivity { export interface AutoModeActivity {
id: string; id: string;
featureId: string; featureId: string;
@@ -1345,16 +1367,7 @@ export const useAppStore = create<AppState & AppActions>()(
setCardOpacity: (projectPath, opacity) => { setCardOpacity: (projectPath, opacity) => {
const current = get().boardBackgroundByProject; const current = get().boardBackgroundByProject;
const existing = current[projectPath] || { const existing = current[projectPath] || defaultBackgroundSettings;
imagePath: null,
cardOpacity: 100,
columnOpacity: 100,
columnBorderEnabled: true,
cardGlassmorphism: true,
cardBorderEnabled: true,
cardBorderOpacity: 100,
hideScrollbar: false,
};
set({ set({
boardBackgroundByProject: { boardBackgroundByProject: {
...current, ...current,
@@ -1368,16 +1381,7 @@ export const useAppStore = create<AppState & AppActions>()(
setColumnOpacity: (projectPath, opacity) => { setColumnOpacity: (projectPath, opacity) => {
const current = get().boardBackgroundByProject; const current = get().boardBackgroundByProject;
const existing = current[projectPath] || { const existing = current[projectPath] || defaultBackgroundSettings;
imagePath: null,
cardOpacity: 100,
columnOpacity: 100,
columnBorderEnabled: true,
cardGlassmorphism: true,
cardBorderEnabled: true,
cardBorderOpacity: 100,
hideScrollbar: false,
};
set({ set({
boardBackgroundByProject: { boardBackgroundByProject: {
...current, ...current,
@@ -1391,32 +1395,12 @@ export const useAppStore = create<AppState & AppActions>()(
getBoardBackground: (projectPath) => { getBoardBackground: (projectPath) => {
const settings = get().boardBackgroundByProject[projectPath]; const settings = get().boardBackgroundByProject[projectPath];
return ( return settings || defaultBackgroundSettings;
settings || {
imagePath: null,
cardOpacity: 100,
columnOpacity: 100,
columnBorderEnabled: true,
cardGlassmorphism: true,
cardBorderEnabled: true,
cardBorderOpacity: 100,
hideScrollbar: false,
}
);
}, },
setColumnBorderEnabled: (projectPath, enabled) => { setColumnBorderEnabled: (projectPath, enabled) => {
const current = get().boardBackgroundByProject; const current = get().boardBackgroundByProject;
const existing = current[projectPath] || { const existing = current[projectPath] || defaultBackgroundSettings;
imagePath: null,
cardOpacity: 100,
columnOpacity: 100,
columnBorderEnabled: true,
cardGlassmorphism: true,
cardBorderEnabled: true,
cardBorderOpacity: 100,
hideScrollbar: false,
};
set({ set({
boardBackgroundByProject: { boardBackgroundByProject: {
...current, ...current,
@@ -1430,16 +1414,7 @@ export const useAppStore = create<AppState & AppActions>()(
setCardGlassmorphism: (projectPath, enabled) => { setCardGlassmorphism: (projectPath, enabled) => {
const current = get().boardBackgroundByProject; const current = get().boardBackgroundByProject;
const existing = current[projectPath] || { const existing = current[projectPath] || defaultBackgroundSettings;
imagePath: null,
cardOpacity: 100,
columnOpacity: 100,
columnBorderEnabled: true,
cardGlassmorphism: true,
cardBorderEnabled: true,
cardBorderOpacity: 100,
hideScrollbar: false,
};
set({ set({
boardBackgroundByProject: { boardBackgroundByProject: {
...current, ...current,
@@ -1453,16 +1428,7 @@ export const useAppStore = create<AppState & AppActions>()(
setCardBorderEnabled: (projectPath, enabled) => { setCardBorderEnabled: (projectPath, enabled) => {
const current = get().boardBackgroundByProject; const current = get().boardBackgroundByProject;
const existing = current[projectPath] || { const existing = current[projectPath] || defaultBackgroundSettings;
imagePath: null,
cardOpacity: 100,
columnOpacity: 100,
columnBorderEnabled: true,
cardGlassmorphism: true,
cardBorderEnabled: true,
cardBorderOpacity: 100,
hideScrollbar: false,
};
set({ set({
boardBackgroundByProject: { boardBackgroundByProject: {
...current, ...current,
@@ -1476,16 +1442,7 @@ export const useAppStore = create<AppState & AppActions>()(
setCardBorderOpacity: (projectPath, opacity) => { setCardBorderOpacity: (projectPath, opacity) => {
const current = get().boardBackgroundByProject; const current = get().boardBackgroundByProject;
const existing = current[projectPath] || { const existing = current[projectPath] || defaultBackgroundSettings;
imagePath: null,
cardOpacity: 100,
columnOpacity: 100,
columnBorderEnabled: true,
cardGlassmorphism: true,
cardBorderEnabled: true,
cardBorderOpacity: 100,
hideScrollbar: false,
};
set({ set({
boardBackgroundByProject: { boardBackgroundByProject: {
...current, ...current,
@@ -1499,16 +1456,7 @@ export const useAppStore = create<AppState & AppActions>()(
setHideScrollbar: (projectPath, hide) => { setHideScrollbar: (projectPath, hide) => {
const current = get().boardBackgroundByProject; const current = get().boardBackgroundByProject;
const existing = current[projectPath] || { const existing = current[projectPath] || defaultBackgroundSettings;
imagePath: null,
cardOpacity: 100,
columnOpacity: 100,
columnBorderEnabled: true,
cardGlassmorphism: true,
cardBorderEnabled: true,
cardBorderOpacity: 100,
hideScrollbar: false,
};
set({ set({
boardBackgroundByProject: { boardBackgroundByProject: {
...current, ...current,
@@ -1522,16 +1470,7 @@ export const useAppStore = create<AppState & AppActions>()(
clearBoardBackground: (projectPath) => { clearBoardBackground: (projectPath) => {
const current = get().boardBackgroundByProject; const current = get().boardBackgroundByProject;
const existing = current[projectPath] || { const existing = current[projectPath] || defaultBackgroundSettings;
imagePath: null,
cardOpacity: 100,
columnOpacity: 100,
columnBorderEnabled: true,
cardGlassmorphism: true,
cardBorderEnabled: true,
cardBorderOpacity: 100,
hideScrollbar: false,
};
set({ set({
boardBackgroundByProject: { boardBackgroundByProject: {
...current, ...current,

View File

@@ -25,8 +25,9 @@ const execAsync = promisify(exec);
interface Feature { interface Feature {
id: string; id: string;
title: string; category: string;
description: string; description: string;
steps?: string[];
status: string; status: string;
priority?: number; priority?: number;
spec?: string; spec?: string;
@@ -636,7 +637,9 @@ Address the follow-up instructions above. Review the previous work and make the
// Load feature for commit message // Load feature for commit message
const feature = await this.loadFeature(projectPath, featureId); const feature = await this.loadFeature(projectPath, featureId);
const commitMessage = feature const commitMessage = feature
? `feat: ${feature.title}\n\nImplemented by Automaker auto-mode` ? `feat: ${this.extractTitleFromDescription(
feature.description
)}\n\nImplemented by Automaker auto-mode`
: `feat: Feature ${featureId}`; : `feat: Feature ${featureId}`;
// Stage and commit // Stage and commit
@@ -930,11 +933,56 @@ Format your response as a structured markdown document.`;
} }
} }
/**
* Extract a title from feature description (first line or truncated)
*/
private extractTitleFromDescription(description: string): string {
if (!description || !description.trim()) {
return "Untitled Feature";
}
// Get first line, or first 60 characters if no newline
const firstLine = description.split("\n")[0].trim();
if (firstLine.length <= 60) {
return firstLine;
}
// Truncate to 60 characters and add ellipsis
return firstLine.substring(0, 57) + "...";
}
/**
* Extract image paths from feature's imagePaths array
* Handles both string paths and objects with path property
*/
private extractImagePaths(
imagePaths:
| Array<string | { path: string; [key: string]: unknown }>
| undefined,
projectPath: string
): string[] {
if (!imagePaths || imagePaths.length === 0) {
return [];
}
return imagePaths
.map((imgPath) => {
const pathStr = typeof imgPath === "string" ? imgPath : imgPath.path;
// Resolve relative paths to absolute paths
return path.isAbsolute(pathStr)
? pathStr
: path.join(projectPath, pathStr);
})
.filter((p) => p); // Filter out any empty paths
}
private buildFeaturePrompt(feature: Feature): string { private buildFeaturePrompt(feature: Feature): string {
const title = this.extractTitleFromDescription(feature.description);
let prompt = `## Feature Implementation Task let prompt = `## Feature Implementation Task
**Feature ID:** ${feature.id} **Feature ID:** ${feature.id}
**Title:** ${feature.title} **Title:** ${title}
**Description:** ${feature.description} **Description:** ${feature.description}
`; `;