From 532d03c231409d06a2a0bfab3d8819e6ecb74a2f Mon Sep 17 00:00:00 2001 From: Test User Date: Sat, 20 Dec 2025 11:27:39 -0500 Subject: [PATCH 1/2] refactor: Introduce useBoardBackgroundSettings hook for managing board background settings with persistence - Refactored BoardBackgroundModal to utilize the new useBoardBackgroundSettings hook, improving code organization and reusability. - Updated methods for setting board background, card opacity, column opacity, and other settings to include server persistence. - Enhanced error handling and user feedback with toast notifications for successful and failed operations. - Added keyboard shortcut support for selecting folders in FileBrowserDialog, improving user experience. - Improved KanbanCard component layout and added dropdown menu for editing and viewing model information. --- .../dialogs/board-background-modal.tsx | 42 ++-- .../dialogs/file-browser-dialog.tsx | 27 ++- .../board-view/components/kanban-card.tsx | 98 +++++++--- .../hooks/use-board-background-settings.ts | 182 ++++++++++++++++++ 4 files changed, 294 insertions(+), 55 deletions(-) create mode 100644 apps/ui/src/hooks/use-board-background-settings.ts diff --git a/apps/ui/src/components/dialogs/board-background-modal.tsx b/apps/ui/src/components/dialogs/board-background-modal.tsx index bf3ccbd4..3244dfdf 100644 --- a/apps/ui/src/components/dialogs/board-background-modal.tsx +++ b/apps/ui/src/components/dialogs/board-background-modal.tsx @@ -15,6 +15,7 @@ import { Checkbox } from "@/components/ui/checkbox"; import { cn } from "@/lib/utils"; import { useAppStore, defaultBackgroundSettings } from "@/store/app-store"; import { getHttpApiClient } from "@/lib/http-api-client"; +import { useBoardBackgroundSettings } from "@/hooks/use-board-background-settings"; import { toast } from "sonner"; const ACCEPTED_IMAGE_TYPES = [ @@ -35,9 +36,8 @@ export function BoardBackgroundModal({ open, onOpenChange, }: BoardBackgroundModalProps) { + const { currentProject, boardBackgroundByProject } = useAppStore(); const { - currentProject, - boardBackgroundByProject, setBoardBackground, setCardOpacity, setColumnOpacity, @@ -47,7 +47,7 @@ export function BoardBackgroundModal({ setCardBorderOpacity, setHideScrollbar, clearBoardBackground, - } = useAppStore(); + } = useBoardBackgroundSettings(); const [isDragOver, setIsDragOver] = useState(false); const [isProcessing, setIsProcessing] = useState(false); const fileInputRef = useRef(null); @@ -139,8 +139,8 @@ export function BoardBackgroundModal({ ); if (result.success && result.path) { - // Update store with the relative path (live update) - setBoardBackground(currentProject.path, result.path); + // Update store and persist to server + await setBoardBackground(currentProject.path, result.path); toast.success("Background image saved"); } else { toast.error(result.error || "Failed to save background image"); @@ -214,7 +214,7 @@ export function BoardBackgroundModal({ ); if (result.success) { - clearBoardBackground(currentProject.path); + await clearBoardBackground(currentProject.path); setPreviewImage(null); toast.success("Background image cleared"); } else { @@ -228,59 +228,59 @@ export function BoardBackgroundModal({ } }, [currentProject, clearBoardBackground]); - // Live update opacity when sliders change + // Live update opacity when sliders change (with persistence) const handleCardOpacityChange = useCallback( - (value: number[]) => { + async (value: number[]) => { if (!currentProject) return; - setCardOpacity(currentProject.path, value[0]); + await setCardOpacity(currentProject.path, value[0]); }, [currentProject, setCardOpacity] ); const handleColumnOpacityChange = useCallback( - (value: number[]) => { + async (value: number[]) => { if (!currentProject) return; - setColumnOpacity(currentProject.path, value[0]); + await setColumnOpacity(currentProject.path, value[0]); }, [currentProject, setColumnOpacity] ); const handleColumnBorderToggle = useCallback( - (checked: boolean) => { + async (checked: boolean) => { if (!currentProject) return; - setColumnBorderEnabled(currentProject.path, checked); + await setColumnBorderEnabled(currentProject.path, checked); }, [currentProject, setColumnBorderEnabled] ); const handleCardGlassmorphismToggle = useCallback( - (checked: boolean) => { + async (checked: boolean) => { if (!currentProject) return; - setCardGlassmorphism(currentProject.path, checked); + await setCardGlassmorphism(currentProject.path, checked); }, [currentProject, setCardGlassmorphism] ); const handleCardBorderToggle = useCallback( - (checked: boolean) => { + async (checked: boolean) => { if (!currentProject) return; - setCardBorderEnabled(currentProject.path, checked); + await setCardBorderEnabled(currentProject.path, checked); }, [currentProject, setCardBorderEnabled] ); const handleCardBorderOpacityChange = useCallback( - (value: number[]) => { + async (value: number[]) => { if (!currentProject) return; - setCardBorderOpacity(currentProject.path, value[0]); + await setCardBorderOpacity(currentProject.path, value[0]); }, [currentProject, setCardBorderOpacity] ); const handleHideScrollbarToggle = useCallback( - (checked: boolean) => { + async (checked: boolean) => { if (!currentProject) return; - setHideScrollbar(currentProject.path, checked); + await setHideScrollbar(currentProject.path, checked); }, [currentProject, setHideScrollbar] ); diff --git a/apps/ui/src/components/dialogs/file-browser-dialog.tsx b/apps/ui/src/components/dialogs/file-browser-dialog.tsx index 289ffbfe..1687218a 100644 --- a/apps/ui/src/components/dialogs/file-browser-dialog.tsx +++ b/apps/ui/src/components/dialogs/file-browser-dialog.tsx @@ -208,13 +208,31 @@ export function FileBrowserDialog({ } }; - const handleSelect = () => { + const handleSelect = useCallback(() => { if (currentPath) { addRecentFolder(currentPath); onSelect(currentPath); onOpenChange(false); } - }; + }, [currentPath, onSelect, onOpenChange]); + + // Handle Command/Ctrl+Enter keyboard shortcut to select current folder + useEffect(() => { + if (!open) return; + + const handleKeyDown = (e: KeyboardEvent) => { + // Check for Command+Enter (Mac) or Ctrl+Enter (Windows/Linux) + if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + if (currentPath && !loading) { + handleSelect(); + } + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [open, currentPath, loading, handleSelect]); // Helper to get folder name from path const getFolderName = (path: string) => { @@ -399,9 +417,12 @@ export function FileBrowserDialog({ - diff --git a/apps/ui/src/components/views/board-view/components/kanban-card.tsx b/apps/ui/src/components/views/board-view/components/kanban-card.tsx index 9b31771e..3994ce21 100644 --- a/apps/ui/src/components/views/board-view/components/kanban-card.tsx +++ b/apps/ui/src/components/views/board-view/components/kanban-card.tsx @@ -483,21 +483,54 @@ export const KanbanCard = memo(function KanbanCard({ )} > {isCurrentAutoTask && ( -
- - - {formatModelName(feature.model ?? DEFAULT_MODEL)} - - {feature.startedAt && ( - - )} +
+
+ + {feature.startedAt && ( + + )} +
+ + + + + + { + e.stopPropagation(); + onEdit(); + }} + data-testid={`edit-running-${feature.id}`} + className="text-xs" + > + + Edit + + {/* Model info in dropdown */} +
+
+ + {formatModelName(feature.model ?? DEFAULT_MODEL)} +
+
+
+
)} {!isCurrentAutoTask && feature.status === "backlog" && ( -
+
)} -
-
-
- -
)}
diff --git a/apps/ui/src/hooks/use-board-background-settings.ts b/apps/ui/src/hooks/use-board-background-settings.ts new file mode 100644 index 00000000..c8529d5f --- /dev/null +++ b/apps/ui/src/hooks/use-board-background-settings.ts @@ -0,0 +1,182 @@ +import { useCallback } from "react"; +import { useAppStore } from "@/store/app-store"; +import { getHttpApiClient } from "@/lib/http-api-client"; +import { toast } from "sonner"; + +/** + * Hook for managing board background settings with automatic persistence to server + */ +export function useBoardBackgroundSettings() { + const store = useAppStore(); + const httpClient = getHttpApiClient(); + + // Helper to persist settings to server + const persistSettings = useCallback( + async (projectPath: string, settingsToUpdate: Record) => { + try { + const result = await httpClient.settings.updateProject( + projectPath, + { + boardBackground: settingsToUpdate, + } + ); + + if (!result.success) { + console.error("Failed to persist settings:", result.error); + toast.error("Failed to save settings"); + } + } catch (error) { + console.error("Failed to persist settings:", error); + toast.error("Failed to save settings"); + } + }, + [httpClient] + ); + + // Get current background settings for a project + const getCurrentSettings = useCallback( + (projectPath: string) => { + const current = store.boardBackgroundByProject[projectPath]; + return current || { + imagePath: null, + cardOpacity: 100, + columnOpacity: 100, + columnBorderEnabled: true, + cardGlassmorphism: true, + cardBorderEnabled: true, + cardBorderOpacity: 100, + hideScrollbar: false, + }; + }, + [store.boardBackgroundByProject] + ); + + // Persisting wrappers for store actions + const setBoardBackground = useCallback( + async (projectPath: string, imagePath: string | null) => { + // Get current settings first + const current = getCurrentSettings(projectPath); + + // Prepare the updated settings + const toUpdate = { + ...current, + imagePath, + imageVersion: imagePath ? Date.now() : undefined, + }; + + // Update local store + store.setBoardBackground(projectPath, imagePath); + + // Persist to server + await persistSettings(projectPath, toUpdate); + }, + [store, persistSettings, getCurrentSettings] + ); + + const setCardOpacity = useCallback( + async (projectPath: string, opacity: number) => { + const current = getCurrentSettings(projectPath); + store.setCardOpacity(projectPath, opacity); + await persistSettings(projectPath, { ...current, cardOpacity: opacity }); + }, + [store, persistSettings, getCurrentSettings] + ); + + const setColumnOpacity = useCallback( + async (projectPath: string, opacity: number) => { + const current = getCurrentSettings(projectPath); + store.setColumnOpacity(projectPath, opacity); + await persistSettings(projectPath, { ...current, columnOpacity: opacity }); + }, + [store, persistSettings, getCurrentSettings] + ); + + const setColumnBorderEnabled = useCallback( + async (projectPath: string, enabled: boolean) => { + const current = getCurrentSettings(projectPath); + store.setColumnBorderEnabled(projectPath, enabled); + await persistSettings(projectPath, { + ...current, + columnBorderEnabled: enabled, + }); + }, + [store, persistSettings, getCurrentSettings] + ); + + const setCardGlassmorphism = useCallback( + async (projectPath: string, enabled: boolean) => { + const current = getCurrentSettings(projectPath); + store.setCardGlassmorphism(projectPath, enabled); + await persistSettings(projectPath, { + ...current, + cardGlassmorphism: enabled, + }); + }, + [store, persistSettings, getCurrentSettings] + ); + + const setCardBorderEnabled = useCallback( + async (projectPath: string, enabled: boolean) => { + const current = getCurrentSettings(projectPath); + store.setCardBorderEnabled(projectPath, enabled); + await persistSettings(projectPath, { + ...current, + cardBorderEnabled: enabled, + }); + }, + [store, persistSettings, getCurrentSettings] + ); + + const setCardBorderOpacity = useCallback( + async (projectPath: string, opacity: number) => { + const current = getCurrentSettings(projectPath); + store.setCardBorderOpacity(projectPath, opacity); + await persistSettings(projectPath, { + ...current, + cardBorderOpacity: opacity, + }); + }, + [store, persistSettings, getCurrentSettings] + ); + + const setHideScrollbar = useCallback( + async (projectPath: string, hide: boolean) => { + const current = getCurrentSettings(projectPath); + store.setHideScrollbar(projectPath, hide); + await persistSettings(projectPath, { ...current, hideScrollbar: hide }); + }, + [store, persistSettings, getCurrentSettings] + ); + + const clearBoardBackground = useCallback( + async (projectPath: string) => { + store.clearBoardBackground(projectPath); + // Clear the boardBackground settings + await persistSettings(projectPath, { + imagePath: null, + imageVersion: undefined, + cardOpacity: 100, + columnOpacity: 100, + columnBorderEnabled: true, + cardGlassmorphism: true, + cardBorderEnabled: true, + cardBorderOpacity: 100, + hideScrollbar: false, + }); + }, + [store, persistSettings] + ); + + return { + setBoardBackground, + setCardOpacity, + setColumnOpacity, + setColumnBorderEnabled, + setCardGlassmorphism, + setCardBorderEnabled, + setCardBorderOpacity, + setHideScrollbar, + clearBoardBackground, + getCurrentSettings, + }; +} From c9e7e4f1e09fdb8db965d5b1e0ee0c56c58df95d Mon Sep 17 00:00:00 2001 From: Test User Date: Sat, 20 Dec 2025 11:57:50 -0500 Subject: [PATCH 2/2] refactor: Improve layout and organization of KanbanCard component - Adjusted spacing and alignment in the KanbanCard component for better visual consistency. - Refactored badge rendering logic to use a more compact layout, enhancing readability. - Cleaned up code formatting for improved maintainability and clarity. - Updated Card component styles to ensure consistent padding and margins. --- apps/ui/src/components/ui/card.tsx | 5 +- .../board-view/components/kanban-card.tsx | 352 ++++++++++-------- 2 files changed, 192 insertions(+), 165 deletions(-) diff --git a/apps/ui/src/components/ui/card.tsx b/apps/ui/src/components/ui/card.tsx index f36b6678..3e04be89 100644 --- a/apps/ui/src/components/ui/card.tsx +++ b/apps/ui/src/components/ui/card.tsx @@ -11,11 +11,12 @@ function Card({ className, gradient = false, ...props }: CardProps) {
(null); const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false); const [currentTime, setCurrentTime] = useState(() => Date.now()); - const { kanbanCardDetailLevel, enableDependencyBlocking, features, useWorktrees } = useAppStore(); + const { + kanbanCardDetailLevel, + enableDependencyBlocking, + features, + useWorktrees, + } = useAppStore(); // Calculate blocking dependencies (if feature is in backlog and has incomplete dependencies) const blockingDependencies = useMemo(() => { @@ -287,9 +291,8 @@ export const KanbanCard = memo(function KanbanCard({ (borderStyle as Record).borderColor = "transparent"; } else if (cardBorderOpacity !== 100) { (borderStyle as Record).borderWidth = "1px"; - ( - borderStyle as Record - ).borderColor = `color-mix(in oklch, var(--border) ${cardBorderOpacity}%, transparent)`; + (borderStyle as Record).borderColor = + `color-mix(in oklch, var(--border) ${cardBorderOpacity}%, transparent)`; } const cardElement = ( @@ -336,152 +339,169 @@ export const KanbanCard = memo(function KanbanCard({ /> )} - {/* Priority badge */} - {feature.priority && ( - - - -
- {feature.priority === 1 ? "H" : feature.priority === 2 ? "M" : "L"} -
-
- -

- {feature.priority === 1 - ? "High Priority" - : feature.priority === 2 - ? "Medium Priority" - : "Low Priority"} -

-
-
-
- )} - - {/* Category text next to priority badge */} - {feature.priority && ( -
- - {feature.category} - -
- )} - - {/* Skip Tests (Manual) indicator badge - positioned at top right */} - {feature.skipTests && !feature.error && feature.status === "backlog" && ( - - - -
- -
-
- -

Manual verification required

-
-
-
- )} - - {/* Error indicator badge */} - {feature.error && ( - - - -
- -
-
- -

{feature.error}

-
-
-
- )} - - {/* Blocked by dependencies badge - positioned at top right */} - {blockingDependencies.length > 0 && !feature.error && !feature.skipTests && feature.status === "backlog" && ( - - - -
- -
-
- -

Blocked by {blockingDependencies.length} incomplete {blockingDependencies.length === 1 ? 'dependency' : 'dependencies'}

-

- {blockingDependencies.map(depId => { - const dep = features.find(f => f.id === depId); - return dep?.description || depId; - }).join(', ')} -

-
-
-
- )} - - {/* Just Finished indicator badge */} - {isJustFinished && ( -
0 && + !feature.error && + !feature.skipTests && + feature.status === "backlog") || + isJustFinished) && ( +
+ {/* Error badge */} + {feature.error && ( + + + +
+ +
+
+ +

{feature.error}

+
+
+
+ )} + + {/* Blocked badge */} + {blockingDependencies.length > 0 && + !feature.error && + !feature.skipTests && + feature.status === "backlog" && ( + + + +
+ +
+
+ +

+ Blocked by {blockingDependencies.length} incomplete{" "} + {blockingDependencies.length === 1 + ? "dependency" + : "dependencies"} +

+

+ {blockingDependencies + .map((depId) => { + const dep = features.find((f) => f.id === depId); + return dep?.description || depId; + }) + .join(", ")} +

+
+
+
+ )} + + {/* Just Finished badge */} + {isJustFinished && ( +
+ +
)} - data-testid={`just-finished-badge-${feature.id}`} - title="Agent just finished working on this feature" - > -
)} - + + {feature.category} + +
+ + + {/* Priority and Manual Verification badges - top left, aligned with delete button */} + {(feature.priority || + (feature.skipTests && + !feature.error && + feature.status === "backlog")) && ( +
+ {/* Priority badge */} + {feature.priority && ( + + + +
+ {feature.priority === 1 + ? "H" + : feature.priority === 2 + ? "M" + : "L"} +
+
+ +

+ {feature.priority === 1 + ? "High Priority" + : feature.priority === 2 + ? "Medium Priority" + : "Low Priority"} +

+
+
+
+ )} + {/* Manual verification badge */} + {feature.skipTests && + !feature.error && + feature.status === "backlog" && ( + + + +
+ +
+
+ +

Manual verification required

+
+
+
+ )} +
)} - > {isCurrentAutoTask && (
@@ -522,7 +542,9 @@ export const KanbanCard = memo(function KanbanCard({
- {formatModelName(feature.model ?? DEFAULT_MODEL)} + + {formatModelName(feature.model ?? DEFAULT_MODEL)} +
@@ -561,7 +583,9 @@ export const KanbanCard = memo(function KanbanCard({ }} onPointerDown={(e) => e.stopPropagation()} data-testid={`edit-${ - feature.status === "waiting_approval" ? "waiting" : "verified" + feature.status === "waiting_approval" + ? "waiting" + : "verified" }-${feature.id}`} title="Edit" > @@ -597,7 +621,9 @@ export const KanbanCard = memo(function KanbanCard({ }} onPointerDown={(e) => e.stopPropagation()} data-testid={`delete-${ - feature.status === "waiting_approval" ? "waiting" : "verified" + feature.status === "waiting_approval" + ? "waiting" + : "verified" }-${feature.id}`} title="Delete" > @@ -665,7 +691,9 @@ export const KanbanCard = memo(function KanbanCard({
- {formatModelName(feature.model ?? DEFAULT_MODEL)} + + {formatModelName(feature.model ?? DEFAULT_MODEL)} +
@@ -686,7 +714,9 @@ export const KanbanCard = memo(function KanbanCard({ {feature.titleGenerating ? (
- Generating title... + + Generating title... +
) : feature.title ? ( @@ -724,16 +754,11 @@ export const KanbanCard = memo(function KanbanCard({ )} )} - {!feature.priority && ( - - {feature.category} - - )}
- + {/* Target Branch Display */} {useWorktrees && feature.branchName && (
@@ -746,8 +771,9 @@ export const KanbanCard = memo(function KanbanCard({ {/* PR URL Display */} {typeof feature.prUrl === "string" && - /^https?:\/\//i.test(feature.prUrl) && (() => { - const prNumber = feature.prUrl.split('/').pop(); + /^https?:\/\//i.test(feature.prUrl) && + (() => { + const prNumber = feature.prUrl.split("/").pop(); return (
- {prNumber ? `Pull Request #${prNumber}` : 'Pull Request'} + {prNumber ? `Pull Request #${prNumber}` : "Pull Request"} @@ -953,11 +979,11 @@ export const KanbanCard = memo(function KanbanCard({ )} {/* Actions */} -
+
{isCurrentAutoTask && ( <> {/* Approve Plan button - PRIORITY: shows even when agent is "running" (paused for approval) */} - {feature.planSpec?.status === 'generated' && onApprovePlan && ( + {feature.planSpec?.status === "generated" && onApprovePlan && (