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 {
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 */

View File

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

View File

@@ -332,35 +332,32 @@ export function Sidebar() {
};
}, [setCurrentView]);
// Fetch running agents count and update every 2 seconds
useEffect(() => {
const fetchRunningAgentsCount = async () => {
try {
const api = getElectronAPI();
if (api.runningAgents) {
const result = await api.runningAgents.getAll();
if (result.success && result.runningAgents) {
setRunningAgentsCount(result.runningAgents.length);
}
// Fetch running agents count function - used for initial load and event-driven updates
const fetchRunningAgentsCount = useCallback(async () => {
try {
const api = getElectronAPI();
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);
}
};
// Initial fetch
fetchRunningAgentsCount();
// Set up interval to refresh every 2 seconds
const interval = setInterval(fetchRunningAgentsCount, 2000);
return () => clearInterval(interval);
} catch (error) {
console.error("[Sidebar] Error fetching running agents count:", error);
}
}, []);
// Subscribe to auto-mode events to update running agents count in real-time
useEffect(() => {
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) => {
// 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_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();
}
});
@@ -391,7 +373,7 @@ export function Sidebar() {
return () => {
unsubscribe();
};
}, []);
}, [fetchRunningAgentsCount]);
// Handle creating initial spec for new project
const handleCreateInitialSpec = useCallback(async () => {

View File

@@ -24,6 +24,7 @@ import {
AgentModel,
ThinkingLevel,
AIProfile,
defaultBackgroundSettings,
} from "@/store/app-store";
import { getElectronAPI } from "@/lib/electron";
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;
}, [features, runningAutoTasks, searchQuery]);
@@ -1975,27 +1957,9 @@ export function BoardView() {
{/* Kanban Columns */}
{(() => {
// Get background settings for current project
const backgroundSettings = currentProject
? boardBackgroundByProject[currentProject.path] || {
imagePath: null,
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 backgroundSettings =
(currentProject && boardBackgroundByProject[currentProject.path]) ||
defaultBackgroundSettings;
// Build background image style if image exists
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)`;
}
return (
const cardElement = (
<Card
ref={setNodeRef}
style={borderStyle}
style={isCurrentAutoTask ? style : borderStyle}
className={cn(
"cursor-grab active:cursor-grabbing transition-all relative kanban-card-content select-none",
// Apply border class when border is enabled and opacity is 100%
// 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
cardBorderEnabled && cardBorderOpacity !== 100 && "border",
!isCurrentAutoTask &&
cardBorderEnabled &&
cardBorderOpacity !== 100 &&
"border",
// Remove default background when using opacity overlay
!isDragging && "bg-transparent",
// Remove default backdrop-blur-sm from Card component when glassmorphism is disabled
!glassmorphism && "backdrop-blur-[0px]!",
isDragging && "scale-105 shadow-lg",
// Special border styles for running/error states override the border opacity
// These need to be applied with higher specificity
isCurrentAutoTask &&
"border-running-indicator border-2 shadow-running-indicator/50 shadow-lg animate-pulse",
// Error state border (only when not in progress)
feature.error &&
!isCurrentAutoTask &&
"border-red-500 border-2 shadow-red-500/30 shadow-lg",
@@ -1056,4 +1060,11 @@ export const KanbanCard = memo(function KanbanCard({
</Dialog>
</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;
}
// 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 {
id: string;
featureId: string;
@@ -1345,16 +1367,7 @@ export const useAppStore = create<AppState & AppActions>()(
setCardOpacity: (projectPath, opacity) => {
const current = get().boardBackgroundByProject;
const existing = current[projectPath] || {
imagePath: null,
cardOpacity: 100,
columnOpacity: 100,
columnBorderEnabled: true,
cardGlassmorphism: true,
cardBorderEnabled: true,
cardBorderOpacity: 100,
hideScrollbar: false,
};
const existing = current[projectPath] || defaultBackgroundSettings;
set({
boardBackgroundByProject: {
...current,
@@ -1368,16 +1381,7 @@ export const useAppStore = create<AppState & AppActions>()(
setColumnOpacity: (projectPath, opacity) => {
const current = get().boardBackgroundByProject;
const existing = current[projectPath] || {
imagePath: null,
cardOpacity: 100,
columnOpacity: 100,
columnBorderEnabled: true,
cardGlassmorphism: true,
cardBorderEnabled: true,
cardBorderOpacity: 100,
hideScrollbar: false,
};
const existing = current[projectPath] || defaultBackgroundSettings;
set({
boardBackgroundByProject: {
...current,
@@ -1391,32 +1395,12 @@ export const useAppStore = create<AppState & AppActions>()(
getBoardBackground: (projectPath) => {
const settings = get().boardBackgroundByProject[projectPath];
return (
settings || {
imagePath: null,
cardOpacity: 100,
columnOpacity: 100,
columnBorderEnabled: true,
cardGlassmorphism: true,
cardBorderEnabled: true,
cardBorderOpacity: 100,
hideScrollbar: false,
}
);
return settings || defaultBackgroundSettings;
},
setColumnBorderEnabled: (projectPath, enabled) => {
const current = get().boardBackgroundByProject;
const existing = current[projectPath] || {
imagePath: null,
cardOpacity: 100,
columnOpacity: 100,
columnBorderEnabled: true,
cardGlassmorphism: true,
cardBorderEnabled: true,
cardBorderOpacity: 100,
hideScrollbar: false,
};
const existing = current[projectPath] || defaultBackgroundSettings;
set({
boardBackgroundByProject: {
...current,
@@ -1430,16 +1414,7 @@ export const useAppStore = create<AppState & AppActions>()(
setCardGlassmorphism: (projectPath, enabled) => {
const current = get().boardBackgroundByProject;
const existing = current[projectPath] || {
imagePath: null,
cardOpacity: 100,
columnOpacity: 100,
columnBorderEnabled: true,
cardGlassmorphism: true,
cardBorderEnabled: true,
cardBorderOpacity: 100,
hideScrollbar: false,
};
const existing = current[projectPath] || defaultBackgroundSettings;
set({
boardBackgroundByProject: {
...current,
@@ -1453,16 +1428,7 @@ export const useAppStore = create<AppState & AppActions>()(
setCardBorderEnabled: (projectPath, enabled) => {
const current = get().boardBackgroundByProject;
const existing = current[projectPath] || {
imagePath: null,
cardOpacity: 100,
columnOpacity: 100,
columnBorderEnabled: true,
cardGlassmorphism: true,
cardBorderEnabled: true,
cardBorderOpacity: 100,
hideScrollbar: false,
};
const existing = current[projectPath] || defaultBackgroundSettings;
set({
boardBackgroundByProject: {
...current,
@@ -1476,16 +1442,7 @@ export const useAppStore = create<AppState & AppActions>()(
setCardBorderOpacity: (projectPath, opacity) => {
const current = get().boardBackgroundByProject;
const existing = current[projectPath] || {
imagePath: null,
cardOpacity: 100,
columnOpacity: 100,
columnBorderEnabled: true,
cardGlassmorphism: true,
cardBorderEnabled: true,
cardBorderOpacity: 100,
hideScrollbar: false,
};
const existing = current[projectPath] || defaultBackgroundSettings;
set({
boardBackgroundByProject: {
...current,
@@ -1499,16 +1456,7 @@ export const useAppStore = create<AppState & AppActions>()(
setHideScrollbar: (projectPath, hide) => {
const current = get().boardBackgroundByProject;
const existing = current[projectPath] || {
imagePath: null,
cardOpacity: 100,
columnOpacity: 100,
columnBorderEnabled: true,
cardGlassmorphism: true,
cardBorderEnabled: true,
cardBorderOpacity: 100,
hideScrollbar: false,
};
const existing = current[projectPath] || defaultBackgroundSettings;
set({
boardBackgroundByProject: {
...current,
@@ -1522,16 +1470,7 @@ export const useAppStore = create<AppState & AppActions>()(
clearBoardBackground: (projectPath) => {
const current = get().boardBackgroundByProject;
const existing = current[projectPath] || {
imagePath: null,
cardOpacity: 100,
columnOpacity: 100,
columnBorderEnabled: true,
cardGlassmorphism: true,
cardBorderEnabled: true,
cardBorderOpacity: 100,
hideScrollbar: false,
};
const existing = current[projectPath] || defaultBackgroundSettings;
set({
boardBackgroundByProject: {
...current,

View File

@@ -25,8 +25,9 @@ const execAsync = promisify(exec);
interface Feature {
id: string;
title: string;
category: string;
description: string;
steps?: string[];
status: string;
priority?: number;
spec?: string;
@@ -636,7 +637,9 @@ Address the follow-up instructions above. Review the previous work and make the
// Load feature for commit message
const feature = await this.loadFeature(projectPath, featureId);
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}`;
// 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 {
const title = this.extractTitleFromDescription(feature.description);
let prompt = `## Feature Implementation Task
**Feature ID:** ${feature.id}
**Title:** ${feature.title}
**Title:** ${title}
**Description:** ${feature.description}
`;