diff --git a/apps/web/components/kanban-board-grid.tsx b/apps/web/components/kanban-board-grid.tsx index 3db79df4eb..d321588aaf 100644 --- a/apps/web/components/kanban-board-grid.tsx +++ b/apps/web/components/kanban-board-grid.tsx @@ -23,6 +23,7 @@ export type KanbanBoardGridProps = { onDragEnd: (event: DragEndEvent) => void; onDragCancel: () => void; activeTask: Task | null; + showMaximizeButton?: boolean; }; export function KanbanBoardGrid({ @@ -36,6 +37,7 @@ export function KanbanBoardGrid({ onDragEnd, onDragCancel, activeTask, + showMaximizeButton, }: KanbanBoardGridProps) { const sensors = useSensors( useSensor(PointerSensor, { @@ -78,6 +80,7 @@ export function KanbanBoardGrid({ onOpenTask={onOpenTask} onEditTask={onEditTask} onDeleteTask={onDeleteTask} + showMaximizeButton={showMaximizeButton} /> ))} diff --git a/apps/web/components/kanban-board-header.tsx b/apps/web/components/kanban-board-header.tsx index 3df10465b4..fd9892b48d 100644 --- a/apps/web/components/kanban-board-header.tsx +++ b/apps/web/components/kanban-board-header.tsx @@ -2,22 +2,9 @@ import Link from 'next/link'; import { Button } from '@kandev/ui/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from '@kandev/ui/dropdown-menu'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@kandev/ui/select'; -import { IconAdjustmentsHorizontal, IconPlus, IconSettings } from '@tabler/icons-react'; +import { IconPlus, IconSettings } from '@tabler/icons-react'; import { ConnectionStatus } from './connection-status'; +import { KanbanDisplayDropdown } from './kanban-display-dropdown'; import type { Repository } from '@/lib/types/http'; type KanbanBoardHeaderProps = { @@ -29,9 +16,11 @@ type KanbanBoardHeaderProps = { repositoriesLoading: boolean; allRepositoriesSelected: boolean; selectedRepositoryId: string | null; + enablePreviewOnClick?: boolean; onWorkspaceChange: (workspaceId: string | null) => void; onBoardChange: (boardId: string | null) => void; onRepositoryChange: (repositoryId: string | 'all') => void; + onTogglePreviewOnClick?: (enabled: boolean) => void; onAddTask: () => void; }; @@ -44,13 +33,13 @@ export function KanbanBoardHeader({ repositoriesLoading, allRepositoriesSelected, selectedRepositoryId, + enablePreviewOnClick, onWorkspaceChange, onBoardChange, onRepositoryChange, + onTogglePreviewOnClick, onAddTask, }: KanbanBoardHeaderProps) { - const repositoryValue = allRepositoriesSelected ? 'all' : selectedRepositoryId ?? 'all'; - return (
@@ -64,84 +53,21 @@ export function KanbanBoardHeader({ Add task - - - - - -
-
- Workspace - -
- -
- Board - -
- -
- Repository - -
-
-
-
+
); diff --git a/apps/web/components/kanban-card.tsx b/apps/web/components/kanban-card.tsx index e0a68abea4..8d7803c1b6 100644 --- a/apps/web/components/kanban-card.tsx +++ b/apps/web/components/kanban-card.tsx @@ -33,6 +33,7 @@ interface KanbanCardProps { onEdit?: (task: Task) => void; onDelete?: (task: Task) => void; onOpenFullPage?: (task: Task) => void; + showMaximizeButton?: boolean; } function KanbanCardBody({ @@ -81,7 +82,7 @@ function KanbanCardLayout({ task, className }: KanbanCardProps & { className?: s ); } -export function KanbanCard({ task, onClick, onEdit, onDelete, onOpenFullPage }: KanbanCardProps) { +export function KanbanCard({ task, onClick, onEdit, onDelete, onOpenFullPage, showMaximizeButton = true }: KanbanCardProps) { const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ id: task.id, }); @@ -121,8 +122,8 @@ export function KanbanCard({ task, onClick, onEdit, onDelete, onOpenFullPage }: repoName={repoName} actions={
- {statusIcon} - {onOpenFullPage && task.hasSession !== false && ( + {task.state === 'IN_PROGRESS' && statusIcon} + {showMaximizeButton && onOpenFullPage && task.hasSession !== false && ( + + +
+
+ Workspace + +
+ +
+ Board + +
+ +
+ Repository + +
+ +
+ Preview Panel + +

+ When enabled, clicking a task opens the preview panel. When disabled, clicking navigates directly to the session. +

+
+
+
+ + ); +} diff --git a/apps/web/components/kanban-with-preview.tsx b/apps/web/components/kanban-with-preview.tsx index 3b352bbed3..6915b3d0f1 100644 --- a/apps/web/components/kanban-with-preview.tsx +++ b/apps/web/components/kanban-with-preview.tsx @@ -24,13 +24,16 @@ export function KanbanWithPreview({ initialTaskId, initialSessionId }: KanbanWit // Get tasks from the kanban store const kanbanTasks = useAppStore((state) => state.kanban.tasks); + const taskSessionsByTaskId = useAppStore((state) => state.taskSessionsByTask.itemsByTaskId); const { selectedTaskId, isOpen, previewWidthPx, + enablePreviewOnClick, open, close, + setEnablePreviewOnClick, updatePreviewWidth, } = useKanbanPreview({ initialTaskId, @@ -168,14 +171,56 @@ export function KanbanWithPreview({ initialTaskId, initialSessionId }: KanbanWit // Preview handler - task data is computed from selectedTaskId // Toggle behavior: clicking the same task closes the panel const handlePreviewTaskWithData = useCallback( - (task: Task) => { - if (isOpen && selectedTaskId === task.id) { - close(); + async (task: Task) => { + if (enablePreviewOnClick) { + // Preview mode: open/toggle preview panel + if (isOpen && selectedTaskId === task.id) { + close(); + } else { + open(task.id); + } } else { - open(task.id); + // Navigate mode: go directly to session details + // First check if we have session data in the store + const sessionsFromStore = taskSessionsByTaskId[task.id]; + const sessionIdFromStore = sessionsFromStore?.[0]?.id; + + if (sessionIdFromStore) { + // We have a session ID in the store - use it directly + handleNavigateToTask(task, sessionIdFromStore); + return; + } + + // No session in store - fetch from backend + const client = getWebSocketClient(); + if (!client) { + // If no WebSocket client, fall back to opening preview + open(task.id); + return; + } + + try { + const response = await client.request<{ sessions: Array<{ id: string }> }>( + "task.session.list", + { task_id: task.id }, + 10000 + ); + const sessionId = response.sessions[0]?.id; + + if (sessionId) { + handleNavigateToTask(task, sessionId); + } else { + // If no session exists, fall back to opening preview + open(task.id); + } + } catch (error) { + console.error("Failed to load task sessions:", error); + // Fall back to opening preview on error + open(task.id); + } } }, - [isOpen, selectedTaskId, open, close] + [enablePreviewOnClick, isOpen, selectedTaskId, taskSessionsByTaskId, open, close, handleNavigateToTask] ); // Handle Escape key to close preview @@ -241,6 +286,8 @@ export function KanbanWithPreview({ initialTaskId, initialSessionId }: KanbanWit
@@ -296,6 +343,8 @@ export function KanbanWithPreview({ initialTaskId, initialSessionId }: KanbanWit diff --git a/apps/web/hooks/use-kanban-preview.ts b/apps/web/hooks/use-kanban-preview.ts index c998614565..15586d0a08 100644 --- a/apps/web/hooks/use-kanban-preview.ts +++ b/apps/web/hooks/use-kanban-preview.ts @@ -14,6 +14,7 @@ export function useKanbanPreview(options: UseKanbanPreviewOptions = {}) { const [selectedTaskId, setSelectedTaskId] = useState(null); const [isOpen, setIsOpen] = useState(false); const [previewWidthPx, setPreviewWidthPx] = useState(PREVIEW_PANEL.DEFAULT_WIDTH_PX); + const [enablePreviewOnClick, setEnablePreviewOnClick] = useState(false); const containerRef = useRef(null); const hasInitialized = useRef(false); @@ -27,6 +28,7 @@ export function useKanbanPreview(options: UseKanbanPreviewOptions = {}) { isOpen: false, previewWidthPx: PREVIEW_PANEL.DEFAULT_WIDTH_PX, selectedTaskId: null, + enablePreviewOnClick: false, }); // Prioritize initial task ID from SSR @@ -42,6 +44,9 @@ export function useKanbanPreview(options: UseKanbanPreviewOptions = {}) { if (taskIdToUse) { setSelectedTaskId(taskIdToUse); } + if (savedState.enablePreviewOnClick !== undefined) { + setEnablePreviewOnClick(savedState.enablePreviewOnClick); + } }, [options.initialTaskId]); // Persist state to localStorage @@ -59,6 +64,10 @@ export function useKanbanPreview(options: UseKanbanPreviewOptions = {}) { setKanbanPreviewState({ selectedTaskId }); }, [selectedTaskId]); + useEffect(() => { + setKanbanPreviewState({ enablePreviewOnClick }); + }, [enablePreviewOnClick]); + const open = useCallback((taskId: string) => { setSelectedTaskId(taskId); setIsOpen(true); @@ -82,10 +91,12 @@ export function useKanbanPreview(options: UseKanbanPreviewOptions = {}) { selectedTaskId, isOpen, previewWidthPx, + enablePreviewOnClick, open, close, toggle, setSelectedTaskId, + setEnablePreviewOnClick, updatePreviewWidth, containerRef, }; diff --git a/apps/web/lib/local-storage.ts b/apps/web/lib/local-storage.ts index bf0608ab62..b5d265618f 100644 --- a/apps/web/lib/local-storage.ts +++ b/apps/web/lib/local-storage.ts @@ -34,6 +34,7 @@ const KANBAN_PREVIEW_KEYS = { OPEN: 'kandev.kanban.preview.open', WIDTH: 'kandev.kanban.preview.width', SELECTED_TASK: 'kandev.kanban.preview.selectedTask', + ENABLE_PREVIEW_ON_CLICK: 'kandev.kanban.preview.enablePreviewOnClick', } as const; // Kanban preview state type @@ -41,6 +42,7 @@ export interface KanbanPreviewState { isOpen: boolean; previewWidthPx: number; selectedTaskId: string | null; + enablePreviewOnClick: boolean; } /** @@ -53,6 +55,7 @@ export function getKanbanPreviewState(defaults: KanbanPreviewState): KanbanPrevi isOpen: getLocalStorage(KANBAN_PREVIEW_KEYS.OPEN, defaults.isOpen), previewWidthPx: getLocalStorage(KANBAN_PREVIEW_KEYS.WIDTH, defaults.previewWidthPx), selectedTaskId: getLocalStorage(KANBAN_PREVIEW_KEYS.SELECTED_TASK, defaults.selectedTaskId), + enablePreviewOnClick: getLocalStorage(KANBAN_PREVIEW_KEYS.ENABLE_PREVIEW_ON_CLICK, defaults.enablePreviewOnClick), }; } @@ -74,4 +77,7 @@ export function setKanbanPreviewState(state: Partial): void setLocalStorage(KANBAN_PREVIEW_KEYS.SELECTED_TASK, state.selectedTaskId); } } + if (state.enablePreviewOnClick !== undefined) { + setLocalStorage(KANBAN_PREVIEW_KEYS.ENABLE_PREVIEW_ON_CLICK, state.enablePreviewOnClick); + } }