{task.state === 'IN_PROGRESS' && statusIcon}
@@ -178,9 +165,18 @@ export function KanbanCard({ task, onClick, onEdit, onDelete, onOpenFullPage, sh
}
export function KanbanCardPreview({ task }: KanbanCardProps) {
+ // Access store to get repository name for the drag preview
+ const repositoriesByWorkspace = useAppStore((state) => state.repositories.itemsByWorkspaceId);
+ const repository = useMemo(
+ () => Object.values(repositoriesByWorkspace).flat().find((repo) => repo.id === task.repositoryId) ?? null,
+ [repositoriesByWorkspace, task.repositoryId]
+ );
+ const repositoryName = repository ? getRepositoryDisplayName(repository.local_path) : null;
+
return (
);
diff --git a/apps/web/components/kanban-column.tsx b/apps/web/components/kanban-column.tsx
index 2c6dc7567e..3a373406d2 100644
--- a/apps/web/components/kanban-column.tsx
+++ b/apps/web/components/kanban-column.tsx
@@ -1,9 +1,11 @@
'use client';
+import { useMemo } from 'react';
import { useDroppable } from '@dnd-kit/core';
import { KanbanCard, Task } from './kanban-card';
import { Badge } from '@kandev/ui/badge';
-import { cn } from '@/lib/utils';
+import { cn, getRepositoryDisplayName } from '@/lib/utils';
+import { useAppStore } from '@/components/state-provider';
export interface Column {
id: string;
@@ -26,6 +28,20 @@ export function KanbanColumn({ column, tasks, onPreviewTask, onOpenTask, onEditT
id: column.id,
});
+ // Access repositories from store to pass repository names to cards
+ const repositoriesByWorkspace = useAppStore((state) => state.repositories.itemsByWorkspaceId);
+ const repositories = useMemo(
+ () => Object.values(repositoriesByWorkspace).flat(),
+ [repositoriesByWorkspace]
+ );
+
+ // Helper function to get repository name for a task
+ const getRepositoryName = (repositoryId?: string): string | null => {
+ if (!repositoryId) return null;
+ const repository = repositories.find((repo) => repo.id === repositoryId);
+ return repository ? getRepositoryDisplayName(repository.local_path) : null;
+ };
+
return (
;
- boards: Array<{ id: string; workspaceId: string; name: string }>;
- activeWorkspaceId: string | null;
- activeBoardId: string | null;
- repositories: Repository[];
- 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;
-};
+export function KanbanDisplayDropdown() {
+ const {
+ workspaces,
+ boards,
+ activeWorkspaceId,
+ activeBoardId,
+ repositories,
+ repositoriesLoading,
+ allRepositoriesSelected,
+ selectedRepositoryId,
+ enablePreviewOnClick,
+ onWorkspaceChange,
+ onBoardChange,
+ onRepositoryChange,
+ onTogglePreviewOnClick,
+ } = useKanbanDisplaySettings();
-export function KanbanDisplayDropdown({
- workspaces,
- boards,
- activeWorkspaceId,
- activeBoardId,
- repositories,
- repositoriesLoading,
- allRepositoriesSelected,
- selectedRepositoryId,
- enablePreviewOnClick,
- onWorkspaceChange,
- onBoardChange,
- onRepositoryChange,
- onTogglePreviewOnClick,
-}: KanbanDisplayDropdownProps) {
const repositoryValue = allRepositoriesSelected ? 'all' : selectedRepositoryId ?? 'all';
return (
diff --git a/apps/web/components/kanban-with-preview.tsx b/apps/web/components/kanban-with-preview.tsx
index 6915b3d0f1..748828c9d3 100644
--- a/apps/web/components/kanban-with-preview.tsx
+++ b/apps/web/components/kanban-with-preview.tsx
@@ -1,14 +1,15 @@
"use client";
-import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef } from "react";
import { useRouter } from "next/navigation";
import { KanbanBoard } from "./kanban-board";
import { TaskPreviewPanel } from "./task-preview-panel";
import { useKanbanPreview } from "@/hooks/use-kanban-preview";
+import { useKanbanLayout } from "@/hooks/use-kanban-layout";
+import { useTaskSession } from "@/hooks/use-task-session";
import { useAppStore } from "@/components/state-provider";
import { Task } from "./kanban-card";
import { PREVIEW_PANEL } from "@/lib/settings/constants";
-import { getWebSocketClient } from "@/lib/ws/connection";
import { linkToSession } from "@/lib/links";
type KanbanWithPreviewProps = {
@@ -16,24 +17,18 @@ type KanbanWithPreviewProps = {
initialSessionId?: string;
};
-export function KanbanWithPreview({ initialTaskId, initialSessionId }: KanbanWithPreviewProps) {
+export function KanbanWithPreview({ initialTaskId }: KanbanWithPreviewProps) {
const router = useRouter();
- const containerRef = useRef(null);
- const [containerWidth, setContainerWidth] = useState(0);
- const isResizingRef = useRef(false);
// 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,
@@ -41,9 +36,13 @@ export function KanbanWithPreview({ initialTaskId, initialSessionId }: KanbanWit
// Cleanup handled by close
},
});
- // Use initialSessionId if provided, otherwise fetch from WebSocket
- const [selectedTaskSessionId, setSelectedTaskSessionId] = useState(initialSessionId ?? null);
- const hasLoadedInitialSession = useRef(false);
+
+ // Use custom hooks for layout and session management
+ const { containerRef, shouldFloat, kanbanWidth } = useKanbanLayout(isOpen, previewWidthPx);
+ const { sessionId: selectedTaskSessionId } = useTaskSession(selectedTaskId ?? null);
+
+ // Track resize state
+ const isResizingRef = useRef(false);
// Compute selected task from kanbanTasks and selectedTaskId
const selectedTask = useMemo(() => {
@@ -63,74 +62,6 @@ export function KanbanWithPreview({ initialTaskId, initialSessionId }: KanbanWit
};
}, [selectedTaskId, kanbanTasks]);
- useEffect(() => {
- if (!selectedTaskId) {
- return;
- }
-
- // If we have an initial session ID and haven't loaded yet, skip the first fetch
- if (initialSessionId && !hasLoadedInitialSession.current) {
- hasLoadedInitialSession.current = true;
- return;
- }
-
- let isActive = true;
- const loadLatestSession = async () => {
- const client = getWebSocketClient();
- if (!client) {
- if (isActive) {
- setSelectedTaskSessionId(null);
- }
- return;
- }
- try {
- const response = await client.request<{ sessions: Array<{ id: string }> }>(
- "task.session.list",
- { task_id: selectedTaskId },
- 10000
- );
- if (isActive) {
- setSelectedTaskSessionId(response.sessions[0]?.id ?? null);
- }
- } catch (error) {
- console.error("Failed to load task sessions:", error);
- if (isActive) {
- setSelectedTaskSessionId(null);
- }
- }
- };
-
- loadLatestSession();
-
- return () => {
- isActive = false;
- };
- }, [selectedTaskId, initialSessionId]);
-
- // Measure container width on mount and resize
- useEffect(() => {
- if (!containerRef.current) return;
-
- const updateWidth = () => {
- if (containerRef.current) {
- setContainerWidth(containerRef.current.offsetWidth);
- }
- };
-
- updateWidth();
- const resizeObserver = new ResizeObserver(updateWidth);
- resizeObserver.observe(containerRef.current);
-
- return () => {
- resizeObserver.disconnect();
- };
- }, []);
-
- // Calculate if we should be in floating mode
- // Float when preview would make kanban less than minimum percentage
- const minKanbanWidthPx = (PREVIEW_PANEL.MIN_KANBAN_WIDTH_PERCENT / 100) * containerWidth;
- const availableForKanban = containerWidth - previewWidthPx;
- const shouldFloat = isOpen && containerWidth > 0 && availableForKanban < minKanbanWidthPx;
// Close panel if selected task no longer exists
useEffect(() => {
@@ -141,10 +72,9 @@ export function KanbanWithPreview({ initialTaskId, initialSessionId }: KanbanWit
const handleNavigateToTask = useCallback(
(task: Task, sessionId: string) => {
- close();
router.push(linkToSession(sessionId));
},
- [close, router]
+ [router]
);
// Update URL query params when task selection changes
@@ -168,59 +98,18 @@ export function KanbanWithPreview({ initialTaskId, initialSessionId }: KanbanWit
window.history.replaceState({}, '', url.toString());
}, [selectedTaskId, selectedTaskSessionId]);
- // Preview handler - task data is computed from selectedTaskId
+ // Preview handler - opens/toggles the preview panel
// Toggle behavior: clicking the same task closes the panel
const handlePreviewTaskWithData = useCallback(
- async (task: Task) => {
- if (enablePreviewOnClick) {
- // Preview mode: open/toggle preview panel
- if (isOpen && selectedTaskId === task.id) {
- close();
- } else {
- open(task.id);
- }
+ (task: Task) => {
+ // Toggle preview: if already open for this task, close it; otherwise open
+ if (isOpen && selectedTaskId === task.id) {
+ close();
} else {
- // 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);
- }
+ open(task.id);
}
},
- [enablePreviewOnClick, isOpen, selectedTaskId, taskSessionsByTaskId, open, close, handleNavigateToTask]
+ [isOpen, selectedTaskId, open, close]
);
// Handle Escape key to close preview
@@ -265,11 +154,6 @@ export function KanbanWithPreview({ initialTaskId, initialSessionId }: KanbanWit
window.addEventListener("mouseup", handleMouseUp);
}, [previewWidthPx, updatePreviewWidth]);
- // Calculate kanban width - keep it at min width when floating to avoid repainting
- const kanbanWidth = shouldFloat
- ? minKanbanWidthPx
- : (isOpen ? containerWidth - previewWidthPx : containerWidth);
-
const activeSessionId = selectedTaskId ? selectedTaskSessionId : null;
return (
@@ -286,8 +170,6 @@ export function KanbanWithPreview({ initialTaskId, initialSessionId }: KanbanWit
@@ -343,8 +225,6 @@ export function KanbanWithPreview({ initialTaskId, initialSessionId }: KanbanWit