From 8b2b7662ee860ba417199b71096fc9f23d8eec18 Mon Sep 17 00:00:00 2001 From: Cody Seibert Date: Sat, 13 Dec 2025 00:25:16 -0500 Subject: [PATCH] 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. --- apps/app/src/app/globals.css | 33 +++++ .../dialogs/board-background-modal.tsx | 37 ++---- apps/app/src/components/layout/sidebar.tsx | 58 +++------ apps/app/src/components/views/board-view.tsx | 44 +------ apps/app/src/components/views/kanban-card.tsx | 27 ++-- apps/app/src/store/app-store.ts | 123 +++++------------- apps/server/src/services/auto-mode-service.ts | 54 +++++++- 7 files changed, 166 insertions(+), 210 deletions(-) diff --git a/apps/app/src/app/globals.css b/apps/app/src/app/globals.css index 7036229e..1e522a2b 100644 --- a/apps/app/src/app/globals.css +++ b/apps/app/src/app/globals.css @@ -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 */ diff --git a/apps/app/src/components/dialogs/board-background-modal.tsx b/apps/app/src/components/dialogs/board-background-modal.tsx index 1d52e54c..ad1207eb 100644 --- a/apps/app/src/components/dialogs/board-background-modal.tsx +++ b/apps/app/src/components/dialogs/board-background-modal.tsx @@ -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(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 => { return new Promise((resolve, reject) => { diff --git a/apps/app/src/components/layout/sidebar.tsx b/apps/app/src/components/layout/sidebar.tsx index 90b3d739..f1efa237 100644 --- a/apps/app/src/components/layout/sidebar.tsx +++ b/apps/app/src/components/layout/sidebar.tsx @@ -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 () => { diff --git a/apps/app/src/components/views/board-view.tsx b/apps/app/src/components/views/board-view.tsx index 17658b72..da7af3b1 100644 --- a/apps/app/src/components/views/board-view.tsx +++ b/apps/app/src/components/views/board-view.tsx @@ -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 diff --git a/apps/app/src/components/views/kanban-card.tsx b/apps/app/src/components/views/kanban-card.tsx index 3bc5c254..a49ca9b6 100644 --- a/apps/app/src/components/views/kanban-card.tsx +++ b/apps/app/src/components/views/kanban-card.tsx @@ -309,26 +309,30 @@ export const KanbanCard = memo(function KanbanCard({ ).borderColor = `color-mix(in oklch, var(--border) ${cardBorderOpacity}%, transparent)`; } - return ( + const cardElement = ( ); + + // Wrap with animated border when in progress + if (isCurrentAutoTask) { + return
{cardElement}
; + } + + return cardElement; }); diff --git a/apps/app/src/store/app-store.ts b/apps/app/src/store/app-store.ts index ebdae99e..07d98969 100644 --- a/apps/app/src/store/app-store.ts +++ b/apps/app/src/store/app-store.ts @@ -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()( 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()( 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()( 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()( 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()( 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()( 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()( 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()( 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, diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts index c43fad2e..ba438e6d 100644 --- a/apps/server/src/services/auto-mode-service.ts +++ b/apps/server/src/services/auto-mode-service.ts @@ -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 + | 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} `;