Session failed
{errorMessage && (
{errorMessage}
@@ -296,7 +293,7 @@ export function ShellTerminal({
return (
);
}
diff --git a/apps/web/components/task/task-changes-panel.tsx b/apps/web/components/task/task-changes-panel.tsx
index 1f5c24654c..21da1cda1c 100644
--- a/apps/web/components/task/task-changes-panel.tsx
+++ b/apps/web/components/task/task-changes-panel.tsx
@@ -1,6 +1,7 @@
'use client';
import { memo, useMemo, useCallback, createRef, useState, useEffect, useRef } from 'react';
+import { PanelRoot, PanelBody, PanelToolbar } from './panel-primitives';
import {
IconSettings,
IconTextWrap,
@@ -292,9 +293,9 @@ const TaskChangesPanel = memo(function TaskChangesPanel({
const progressPercent = totalCount > 0 ? (reviewedCount / totalCount) * 100 : 0;
return (
-
+
{/* Top bar */}
-
+
{/* Settings cog */}
@@ -395,10 +396,10 @@ const TaskChangesPanel = memo(function TaskChangesPanel({
)}
-
+
{/* Content */}
-
+
{cumulativeLoading && allFiles.length === 0 ? (
Loading changes...
@@ -420,7 +421,7 @@ const TaskChangesPanel = memo(function TaskChangesPanel({
fileRefs={fileRefs}
/>
) : null}
-
+
{/* Review Dialog */}
{activeSessionId && (
@@ -435,7 +436,7 @@ const TaskChangesPanel = memo(function TaskChangesPanel({
cumulativeDiff={cumulativeDiff}
/>
)}
-
+
);
});
diff --git a/apps/web/components/task/task-chat-panel.tsx b/apps/web/components/task/task-chat-panel.tsx
index 32205e8450..6c90dd46d3 100644
--- a/apps/web/components/task/task-chat-panel.tsx
+++ b/apps/web/components/task/task-chat-panel.tsx
@@ -1,11 +1,12 @@
'use client';
import { useCallback, useEffect, useRef, useState, memo, useMemo } from 'react';
-import { cn } from '@/lib/utils';
+import { PanelRoot, PanelBody } from './panel-primitives';
import { getWebSocketClient } from '@/lib/ws/connection';
import { useAppStore } from '@/components/state-provider';
import { getLocalStorage } from '@/lib/local-storage';
import { useLayoutStore } from '@/lib/state/layout-store';
+import { usePanelActions } from '@/hooks/use-panel-actions';
import { useKeyboardShortcut } from '@/hooks/use-keyboard-shortcut';
import { SHORTCUTS } from '@/lib/keyboard/constants';
import { useSessionMessages } from '@/hooks/domains/session/use-session-messages';
@@ -15,16 +16,14 @@ import { useProcessedMessages } from '@/hooks/use-processed-messages';
import { useSessionModel } from '@/hooks/domains/session/use-session-model';
import { useMessageHandler } from '@/hooks/use-message-handler';
import { useQueue } from '@/hooks/domains/session/use-queue';
-import { TodoSummary } from '@/components/task/chat/todo-summary';
import { VirtualizedMessageList } from '@/components/task/chat/virtualized-message-list';
import { ChatInputContainer, type ChatInputContainerHandle, type MessageAttachment } from '@/components/task/chat/chat-input-container';
-import { QueuedMessageIndicator, type QueuedMessageIndicatorHandle } from '@/components/task/chat/queued-message-indicator';
+import { type QueuedMessageIndicatorHandle } from '@/components/task/chat/queued-message-indicator';
import { formatReviewCommentsAsMarkdown } from '@/components/task/chat/messages/review-comments-attachment';
import {
useDiffCommentsStore,
usePendingCommentsByFile,
} from '@/lib/state/slices/diff-comments';
-import { DocumentReferenceIndicator } from '@/components/task/chat/document-reference-indicator';
import type { DiffComment } from '@/lib/diff/types';
import type { DocumentComment } from '@/lib/state/slices/ui/types';
@@ -38,6 +37,8 @@ type TaskChatPanelProps = {
onRequestChangesTooltipDismiss?: () => void;
/** Callback to open a file at a specific line (for comment clicks) */
onOpenFileAtLine?: (filePath: string) => void;
+ /** Whether the dockview group containing this panel is focused */
+ isPanelFocused?: boolean;
};
export const TaskChatPanel = memo(function TaskChatPanel({
@@ -47,6 +48,7 @@ export const TaskChatPanel = memo(function TaskChatPanel({
showRequestChangesTooltip = false,
onRequestChangesTooltipDismiss,
onOpenFileAtLine,
+ isPanelFocused,
}: TaskChatPanelProps) {
const [isSending, setIsSending] = useState(false);
const lastAgentMessageCountRef = useRef(0);
@@ -74,7 +76,6 @@ export const TaskChatPanel = memo(function TaskChatPanel({
resolvedSessionId ? state.documentPanel.activeDocumentBySessionId[resolvedSessionId] ?? null : null
);
const layoutBySession = useLayoutStore((state) => state.columnsBySessionId);
- const openDocument = useLayoutStore((state) => state.openDocument);
const closeDocument = useLayoutStore((state) => state.closeDocument);
const setActiveDocument = useAppStore((state) => state.setActiveDocument);
const setPlanMode = useAppStore((state) => state.setPlanMode);
@@ -85,12 +86,14 @@ export const TaskChatPanel = memo(function TaskChatPanel({
return layout?.document === true;
}, [resolvedSessionId, activeDocument, layoutBySession]);
+ const { addPlan } = usePanelActions();
+
const handlePlanModeChange = useCallback(
(enabled: boolean) => {
if (!resolvedSessionId || !taskId) return;
if (enabled) {
setActiveDocument(resolvedSessionId, { type: 'plan', taskId });
- openDocument(resolvedSessionId);
+ addPlan();
setPlanMode(resolvedSessionId, true);
} else {
closeDocument(resolvedSessionId);
@@ -98,7 +101,7 @@ export const TaskChatPanel = memo(function TaskChatPanel({
setPlanMode(resolvedSessionId, false);
}
},
- [resolvedSessionId, taskId, setActiveDocument, openDocument, closeDocument, setPlanMode]
+ [resolvedSessionId, taskId, setActiveDocument, addPlan, closeDocument, setPlanMode]
);
// Initialize plan mode from localStorage on mount (restore document panel state)
@@ -299,9 +302,9 @@ export const TaskChatPanel = memo(function TaskChatPanel({
);
return (
-
+
{/* Scrollable messages area */}
-
+
{/* Sticky input at bottom */}
-
- {/* Status section: todos, etc. */}
- {todoItems.length > 0 &&
}
-
- {/* Document reference indicator */}
- {activeDocument && (
-
- )}
-
- {/* Chat input with optional queued message on top */}
-
- {/* Queued message indicator - appears above chat input */}
- {isQueued && queuedMessage && (
-
- )}
-
-
+
-
-
+
);
});
diff --git a/apps/web/components/task/task-files-panel.tsx b/apps/web/components/task/task-files-panel.tsx
index fef6388961..c06d571b67 100644
--- a/apps/web/components/task/task-files-panel.tsx
+++ b/apps/web/components/task/task-files-panel.tsx
@@ -50,7 +50,8 @@ import {
hasUserSelectedFilesPanelTab,
setUserSelectedFilesPanelTab,
} from '@/lib/local-storage';
-import { useLayoutStore } from '@/lib/state/layout-store';
+
+import { usePanelActions } from '@/hooks/use-panel-actions';
import { FileStatusIcon } from './file-status-icon';
type TaskFilesPanelProps = {
@@ -287,15 +288,13 @@ const TaskFilesPanel = memo(function TaskFilesPanel({ onSelectDiff, onOpenFile,
}
}, [gitOps]);
- // Open file in document panel side-by-side
- const openDocument = useLayoutStore((state) => state.openDocument);
- const setActiveDocument = useAppStore((state) => state.setActiveDocument);
+ // Open file in editor panel
+ const { openFile: panelOpenFile } = usePanelActions();
const handleOpenFileInDocumentPanel = useCallback((path: string) => {
if (!activeSessionId) return;
- const name = path.split('/').pop() || path;
- setActiveDocument(activeSessionId, { type: 'file', path, name });
- openDocument(activeSessionId);
- }, [activeSessionId, setActiveDocument, openDocument]);
+ // On desktop, this will open a file editor tab via dockview
+ panelOpenFile(path);
+ }, [activeSessionId, panelOpenFile]);
// Smart tab selection: restore user preference or auto-select based on changes
useEffect(() => {
diff --git a/apps/web/components/task/task-item.tsx b/apps/web/components/task/task-item.tsx
index c049181589..e5158c69f1 100644
--- a/apps/web/components/task/task-item.tsx
+++ b/apps/web/components/task/task-item.tsx
@@ -107,8 +107,8 @@ export const TaskItem = memo(function TaskItem({
'group relative flex w-full items-start gap-2 rounded-md border p-2 text-left text-sm outline-none cursor-pointer',
'transition-all duration-100',
isSelected
- ? 'bg-primary/10 border-primary/50'
- : 'bg-card/50 border-border/50 hover:bg-card/80 hover:border-border/70'
+ ? 'bg-primary/10 border-primary/30'
+ : 'bg-background/50 border-border hover:bg-background/80 hover:border-border'
)}
>
{/* Content */}
diff --git a/apps/web/components/task/task-layout.tsx b/apps/web/components/task/task-layout.tsx
index baccf6d5a8..0e8a001890 100644
--- a/apps/web/components/task/task-layout.tsx
+++ b/apps/web/components/task/task-layout.tsx
@@ -1,41 +1,21 @@
'use client';
-import { memo, useMemo, useState, useCallback } from 'react';
-import { Group, Panel, type Layout } from 'react-resizable-panels';
-import { useDefaultLayout } from '@/lib/layout/use-default-layout';
+import { memo } from 'react';
+import dynamic from 'next/dynamic';
import { useResponsiveBreakpoint } from '@/hooks/use-responsive-breakpoint';
-import { useSessionLayoutState } from '@/hooks/use-session-layout-state';
-import { TaskCenterPanel } from './task-center-panel';
-import { TaskRightPanel } from './task-right-panel';
-import { TaskFilesPanel } from './task-files-panel';
-import { TaskSessionSidebar } from './task-session-sidebar';
import { SessionMobileLayout, SessionTabletLayout } from './mobile';
-import { PreviewPanel } from '@/components/task/preview/preview-panel';
-import { PreviewController } from '@/components/task/preview/preview-controller';
-import { DocumentPanel } from '@/components/task/document/document-panel';
-import { useLayoutStore } from '@/lib/state/layout-store';
import type { Repository, RepositoryScript } from '@/lib/types/http';
import type { Terminal } from '@/hooks/domains/session/use-terminals';
+import type { Layout } from 'react-resizable-panels';
// Re-export for backwards compatibility
export type { SelectedDiff } from '@/hooks/use-session-layout-state';
-const DEFAULT_HORIZONTAL_LAYOUT: Record
= {
- left: 18,
- chat: 57,
- preview: 57,
- 'document-split': 82,
- right: 25,
-};
-const DEFAULT_PREVIEW_LAYOUT: Record = {
- chat: 60,
- preview: 40,
-};
-const DEFAULT_DOCUMENT_LAYOUT: Record = {
- document: 45,
- chat: 55,
-};
-const DEFAULT_LAYOUT_STATE = { left: true, chat: true, right: true, preview: false, document: false };
+// Dynamic import for dockview (no SSR)
+const DockviewDesktopLayout = dynamic(
+ () => import('./dockview-desktop-layout').then((mod) => mod.DockviewDesktopLayout),
+ { ssr: false }
+);
type TaskLayoutProps = {
workspaceId: string | null;
@@ -64,76 +44,6 @@ export const TaskLayout = memo(function TaskLayout({
}: TaskLayoutProps) {
const { isMobile, isTablet } = useResponsiveBreakpoint();
- // Use shared layout state hook
- const {
- effectiveSessionId,
- sessionKey,
- selectedDiff,
- handleSelectDiff,
- handleClearSelectedDiff,
- openFileRequest,
- handleOpenFile,
- handleFileOpenHandled,
- } = useSessionLayoutState({ sessionId });
-
- // Track active file path for highlighting in file tree
- const [activeFilePath, setActiveFilePath] = useState(null);
-
- const handleActiveFileChange = useCallback((filePath: string | null) => {
- setActiveFilePath(filePath);
- }, []);
-
- const layoutBySession = useLayoutStore((state) => state.columnsBySessionId);
- const layoutState = useMemo(
- () => layoutBySession[sessionKey] ?? DEFAULT_LAYOUT_STATE,
- [layoutBySession, sessionKey]
- );
- const hasDevScript = Boolean(repository?.dev_script?.trim());
- const sessionForPreview = effectiveSessionId;
-
- const horizontalPanelIds = useMemo(() => {
- const ids: string[] = [];
- if (layoutState.left) ids.push('left');
- if (layoutState.document) {
- ids.push('document-split');
- } else if (layoutState.preview) {
- ids.push('preview');
- } else {
- ids.push('chat');
- }
- if (layoutState.right) ids.push('right');
- return ids;
- }, [layoutState.left, layoutState.preview, layoutState.right, layoutState.document]);
-
- const horizontalLayoutKey = `task-layout-horizontal-v4:${horizontalPanelIds.join('|')}`;
- const { defaultLayout: defaultHorizontalLayout, onLayoutChanged: onHorizontalLayoutChange } =
- useDefaultLayout({
- id: horizontalLayoutKey,
- panelIds: horizontalPanelIds,
- baseLayout: DEFAULT_HORIZONTAL_LAYOUT,
- serverDefaultLayout: defaultLayouts[horizontalLayoutKey],
- });
-
- const previewPanelIds = ['chat', 'preview'];
- const previewLayoutKey = 'task-layout-preview-v2';
- const { defaultLayout: defaultPreviewLayout, onLayoutChanged: onPreviewLayoutChange } =
- useDefaultLayout({
- id: previewLayoutKey,
- panelIds: previewPanelIds,
- baseLayout: DEFAULT_PREVIEW_LAYOUT,
- serverDefaultLayout: defaultLayouts[previewLayoutKey],
- });
-
- const documentPanelIds = ['document', 'chat'];
- const documentLayoutKey = 'task-layout-document-v1';
- const { defaultLayout: defaultDocumentLayout, onLayoutChanged: onDocumentLayoutChange } =
- useDefaultLayout({
- id: documentLayoutKey,
- panelIds: documentPanelIds,
- baseLayout: DEFAULT_DOCUMENT_LAYOUT,
- serverDefaultLayout: defaultLayouts[documentLayoutKey],
- });
-
// Mobile layout
if (isMobile) {
return (
@@ -161,104 +71,15 @@ export const TaskLayout = memo(function TaskLayout({
);
}
- // Desktop layout
- const topFilesPanel = (
-
);
-
- return (
-
-
-
- {layoutState.left ? (
- <>
-
-
-
- >
- ) : null}
-
- {layoutState.document ? (
-
-
-
-
-
-
-
-
-
-
- ) : layoutState.preview ? (
-
-
-
-
-
-
-
-
-
-
- ) : (
-
-
-
- )}
-
- {layoutState.right ? (
- <>
-
-
-
- >
- ) : null}
-
-
- );
});
diff --git a/apps/web/components/task/task-page-content.tsx b/apps/web/components/task/task-page-content.tsx
index b711b282e8..a4126684ee 100644
--- a/apps/web/components/task/task-page-content.tsx
+++ b/apps/web/components/task/task-page-content.tsx
@@ -251,7 +251,6 @@ export function TaskPageContent({
worktreeBranch={worktreeBranch}
repositoryPath={repository?.local_path ?? null}
repositoryName={repository?.name ?? null}
- hasDevScript={Boolean(repository?.dev_script?.trim())}
showDebugOverlay={showDebugOverlay}
onToggleDebugOverlay={() => setShowDebugOverlay((prev) => !prev)}
/>
diff --git a/apps/web/components/task/task-plan-panel.tsx b/apps/web/components/task/task-plan-panel.tsx
index 2987eafa07..56e84b8d32 100644
--- a/apps/web/components/task/task-plan-panel.tsx
+++ b/apps/web/components/task/task-plan-panel.tsx
@@ -1,6 +1,7 @@
'use client';
import { memo, useState, useCallback, useEffect, useRef } from 'react';
+import { PanelRoot, PanelBody } from './panel-primitives';
import dynamic from 'next/dynamic';
import { IconLoader2, IconFileText, IconRobot, IconMessage, IconClick, IconMessagePlus } from '@tabler/icons-react';
import { GridSpinner } from '@/components/grid-spinner';
@@ -297,14 +298,13 @@ export const TaskPlanPanel = memo(function TaskPlanPanel({ taskId, visible = tru
}));
return (
-
+
{/* Content - Markdown Editor with inline comment highlights */}
-
@@ -390,7 +390,7 @@ export const TaskPlanPanel = memo(function TaskPlanPanel({ taskId, visible = tru
)}
-
+
{/* Selection popover for adding comments */}
{textSelection && activeSessionId && (
@@ -403,6 +403,6 @@ export const TaskPlanPanel = memo(function TaskPlanPanel({ taskId, visible = tru
onDelete={editingCommentId ? () => handleDeleteComment(editingCommentId) : undefined}
/>
)}
-
+
);
});
diff --git a/apps/web/components/task/task-session-sidebar.tsx b/apps/web/components/task/task-session-sidebar.tsx
index 33d0fd8d02..84de4ffa77 100644
--- a/apps/web/components/task/task-session-sidebar.tsx
+++ b/apps/web/components/task/task-session-sidebar.tsx
@@ -1,20 +1,20 @@
'use client';
import { useCallback, useMemo, useState, memo } from 'react';
-import type { TaskState, Workspace, Repository, TaskSession, BoardSnapshot, Task } from '@/lib/types/http';
+import type { TaskState, Repository, TaskSession, BoardSnapshot, Task } from '@/lib/types/http';
import type { KanbanState } from '@/lib/state/slices';
import { TaskSwitcher } from './task-switcher';
import { BoardSwitcher } from './board-switcher';
-import { WorkspaceSwitcher } from './workspace-switcher';
import { Button } from '@kandev/ui/button';
-import { SessionPanel } from '@kandev/ui/pannel-session';
+import { PanelRoot, PanelBody } from './panel-primitives';
import { IconPlus } from '@tabler/icons-react';
import { TaskCreateDialog } from '@/components/task-create-dialog';
import { useAppStore, useAppStoreApi } from '@/components/state-provider';
import { linkToSession } from '@/lib/links';
-import { listTaskSessions, fetchBoardSnapshot, listBoards } from '@/lib/api';
+import { listTaskSessions, fetchBoardSnapshot } from '@/lib/api';
import { useTasks } from '@/hooks/use-tasks';
import { useTaskActions } from '@/hooks/use-task-actions';
+import { performLayoutSwitch } from '@/lib/state/dockview-store';
// Extracted component to isolate dialog state from sidebar
type NewTaskButtonProps = {
@@ -24,7 +24,7 @@ type NewTaskButtonProps = {
onSuccess: (task: Task, mode: 'create' | 'edit', meta?: { taskSessionId?: string | null }) => void;
};
-const NewTaskButton = memo(function NewTaskButton({
+export const NewTaskButton = memo(function NewTaskButton({
workspaceId,
boardId,
columns,
@@ -108,7 +108,6 @@ export const TaskSessionSidebar = memo(function TaskSessionSidebar({ workspaceId
const { tasks } = useTasks(boardId);
const columns = useAppStore((state) => state.kanban.steps);
const boards = useAppStore((state) => state.boards.items);
- const workspaces = useAppStore((state) => state.workspaces.items);
const repositoriesByWorkspace = useAppStore((state) => state.repositories.itemsByWorkspaceId);
const setActiveTask = useAppStore((state) => state.setActiveTask);
const setActiveSession = useAppStore((state) => state.setActiveSession);
@@ -241,12 +240,14 @@ export const TaskSessionSidebar = memo(function TaskSessionSidebar({ workspaceId
// If deleted task was active, switch to another task or go home
const currentActiveTaskId = store.getState().tasks.activeTaskId;
if (currentActiveTaskId === taskId) {
+ const oldSessionId = store.getState().tasks.activeSessionId;
if (remainingTasks.length > 0) {
// Switch to the first remaining task
const nextTask = remainingTasks[0];
// Use handleSelectTask pattern - check for primarySessionId first
if (nextTask.primarySessionId) {
setActiveSession(nextTask.id, nextTask.primarySessionId);
+ performLayoutSwitch(oldSessionId, nextTask.primarySessionId);
window.history.replaceState({}, '', linkToSession(nextTask.primarySessionId));
} else {
// Load sessions to find one to navigate to
@@ -254,6 +255,7 @@ export const TaskSessionSidebar = memo(function TaskSessionSidebar({ workspaceId
const sessionId = sessions[0]?.id ?? null;
if (sessionId) {
setActiveSession(nextTask.id, sessionId);
+ performLayoutSwitch(oldSessionId, sessionId);
window.history.replaceState({}, '', linkToSession(sessionId));
} else {
setActiveTask(nextTask.id);
@@ -273,6 +275,7 @@ export const TaskSessionSidebar = memo(function TaskSessionSidebar({ workspaceId
const handleSelectTask = useCallback(
(taskId: string) => {
+ const oldSessionId = store.getState().tasks.activeSessionId;
// Get task from kanban state to check for primarySessionId
const kanbanTasks = store.getState().kanban.tasks;
const task = kanbanTasks.find((t) => t.id === taskId);
@@ -280,6 +283,7 @@ export const TaskSessionSidebar = memo(function TaskSessionSidebar({ workspaceId
// If task has primarySessionId, switch immediately (instant UX)
if (task?.primarySessionId) {
setActiveSession(taskId, task.primarySessionId);
+ performLayoutSwitch(oldSessionId, task.primarySessionId);
updateUrl(task.primarySessionId);
// Load sessions in background to update cache (non-blocking)
loadTaskSessionsForTask(taskId);
@@ -288,12 +292,14 @@ export const TaskSessionSidebar = memo(function TaskSessionSidebar({ workspaceId
// Fallback: load sessions first if no primarySessionId
loadTaskSessionsForTask(taskId).then((sessions) => {
+ const currentOldSessionId = store.getState().tasks.activeSessionId;
const sessionId = sessions[0]?.id ?? null;
if (!sessionId) {
setActiveTask(taskId);
return;
}
setActiveSession(taskId, sessionId);
+ performLayoutSwitch(currentOldSessionId, sessionId);
updateUrl(sessionId);
});
},
@@ -322,12 +328,14 @@ export const TaskSessionSidebar = memo(function TaskSessionSidebar({ workspaceId
}));
// Navigate to the most recent task's most recent session
+ const oldSessionId = store.getState().tasks.activeSessionId;
const mostRecentTask = sortByUpdatedAtDesc(snapshot.tasks)[0];
if (mostRecentTask) {
const sessions = await loadTaskSessionsForTask(mostRecentTask.id);
const mostRecentSession = sortByUpdatedAtDesc(sessions)[0];
if (mostRecentSession) {
setActiveSession(mostRecentTask.id, mostRecentSession.id);
+ performLayoutSwitch(oldSessionId, mostRecentSession.id);
updateUrl(mostRecentSession.id);
} else {
setActiveTask(mostRecentTask.id);
@@ -344,159 +352,39 @@ export const TaskSessionSidebar = memo(function TaskSessionSidebar({ workspaceId
[boardId, store, loadTaskSessionsForTask, setActiveSession, setActiveTask, updateUrl]
);
- // Handle workspace change - find first board and navigate to most recent task's session
- // Memoized columns for NewTaskButton to prevent re-renders
- const dialogColumns = useMemo(
- () => columns.map((step: KanbanState['steps'][number]) => ({
- id: step.id,
- title: step.title,
- color: step.color,
- autoStartAgent: step.autoStartAgent,
- })),
- [columns]
- );
-
- // Memoized success handler for NewTaskButton
- const handleTaskCreated = useCallback(
- (task: Task, _mode: 'create' | 'edit', meta?: { taskSessionId?: string | null }) => {
- store.setState((state) => {
- if (state.kanban.boardId !== task.board_id) return state;
- const nextTask = {
- id: task.id,
- workflowStepId: task.workflow_step_id,
- title: task.title,
- description: task.description,
- position: task.position ?? 0,
- state: task.state,
- repositoryId: task.repositories?.[0]?.repository_id ?? undefined,
- updatedAt: task.updated_at,
- };
- return {
- ...state,
- kanban: {
- ...state.kanban,
- tasks: state.kanban.tasks.some((item: KanbanState['tasks'][number]) => item.id === task.id)
- ? state.kanban.tasks.map((item: KanbanState['tasks'][number]) => (item.id === task.id ? nextTask : item))
- : [...state.kanban.tasks, nextTask],
- },
- };
- });
- setActiveTask(task.id);
- if (meta?.taskSessionId) {
- setActiveSession(task.id, meta.taskSessionId);
- updateUrl(meta.taskSessionId);
- }
- },
- [store, setActiveTask, setActiveSession, updateUrl]
- );
-
- const handleWorkspaceChange = useCallback(
- async (newWorkspaceId: string) => {
- if (newWorkspaceId === workspaceId) return;
-
- // Set loading state in store (synchronous)
- store.setState((state) => ({
- ...state,
- kanban: { ...state.kanban, isLoading: true },
- }));
-
- try {
- // Fetch boards for the new workspace
- const boardsResponse = await listBoards(newWorkspaceId, { cache: 'no-store' });
- const newWorkspaceBoards = boardsResponse.boards ?? [];
-
- const firstBoard = newWorkspaceBoards[0];
- if (!firstBoard) {
- // No boards in workspace - reset loading state
- store.setState((state) => ({
- ...state,
- kanban: { ...state.kanban, isLoading: false },
- }));
- return;
- }
-
- const snapshot = await fetchBoardSnapshot(firstBoard.id);
-
- // Update boards and kanban state in a single setState call
- store.setState((state) => ({
- ...state,
- boards: {
- ...state.boards,
- items: [
- ...state.boards.items.filter((b: { workspaceId: string }) => b.workspaceId !== newWorkspaceId),
- ...newWorkspaceBoards.map((b) => ({ id: b.id, workspaceId: b.workspace_id, name: b.name })),
- ],
- activeId: firstBoard.id,
- },
- kanban: mapSnapshotToKanban(snapshot, firstBoard.id),
- }));
-
- // Navigate to the most recent task's most recent session
- const mostRecentTask = sortByUpdatedAtDesc(snapshot.tasks)[0];
- if (mostRecentTask) {
- const sessions = await loadTaskSessionsForTask(mostRecentTask.id);
- const mostRecentSession = sortByUpdatedAtDesc(sessions)[0];
- if (mostRecentSession) {
- setActiveSession(mostRecentTask.id, mostRecentSession.id);
- updateUrl(mostRecentSession.id);
- } else {
- setActiveTask(mostRecentTask.id);
- }
- }
- } catch (error) {
- console.error('Failed to switch workspace:', error);
- store.setState((state) => ({
- ...state,
- kanban: { ...state.kanban, isLoading: false },
- }));
- }
- },
- [workspaceId, store, loadTaskSessionsForTask, setActiveSession, setActiveTask, updateUrl]
- );
-
- return (
+ const taskSwitcherContent = (
<>
-
-
-
- ({ id: w.id, name: w.name }))}
- activeWorkspaceId={workspaceId}
- onSelect={handleWorkspaceChange}
- />
-
-
-
-
- ({ id: step.id, title: step.title, color: step.color }))}
- activeTaskId={activeTaskId}
- selectedTaskId={selectedTaskId}
- onSelectTask={(taskId) => {
- handleSelectTask(taskId);
- }}
- onDeleteTask={handleDeleteTask}
- deletingTaskId={deletingTaskId}
- isLoading={isLoadingBoard}
- />
-
- {/* Board Switcher at bottom - only show if more than one board */}
- {workspaceBoards.length > 1 && (
-
- ({ id: b.id, name: b.name }))}
- activeBoardId={boardId}
- onSelect={handleBoardChange}
- />
-
- )}
-
+
({ id: step.id, title: step.title, color: step.color }))}
+ activeTaskId={activeTaskId}
+ selectedTaskId={selectedTaskId}
+ onSelectTask={(taskId) => {
+ handleSelectTask(taskId);
+ }}
+ onDeleteTask={handleDeleteTask}
+ deletingTaskId={deletingTaskId}
+ isLoading={isLoadingBoard}
+ />
>
);
+
+ const boardSwitcher = workspaceBoards.length > 1 ? (
+
+ ({ id: b.id, name: b.name }))}
+ activeBoardId={boardId}
+ onSelect={handleBoardChange}
+ />
+
+ ) : null;
+
+ return (
+
+
+ {taskSwitcherContent}
+
+ {boardSwitcher}
+
+ );
});
diff --git a/apps/web/components/task/task-top-bar.tsx b/apps/web/components/task/task-top-bar.tsx
index 4e4c7a3f39..2baf89d4b2 100644
--- a/apps/web/components/task/task-top-bar.tsx
+++ b/apps/web/components/task/task-top-bar.tsx
@@ -26,7 +26,6 @@ import { CommitStatBadge } from '@/components/diff-stat';
import { useSessionGitStatus } from '@/hooks/domains/session/use-session-git-status';
import { formatUserHomePath } from '@/lib/utils';
import { EditorsMenu } from '@/components/task/editors-menu';
-import { PreviewControls } from '@/components/task/preview/preview-controls';
import { DocumentControls } from '@/components/task/document/document-controls';
import { VcsSplitButton } from '@/components/vcs-split-button';
import { DEBUG_UI } from '@/lib/config';
@@ -45,7 +44,6 @@ type TaskTopBarProps = {
worktreeBranch?: string | null;
repositoryPath?: string | null;
repositoryName?: string | null;
- hasDevScript?: boolean;
showDebugOverlay?: boolean;
onToggleDebugOverlay?: () => void;
};
@@ -58,7 +56,6 @@ const TaskTopBar = memo(function TaskTopBar({
worktreeBranch,
repositoryPath,
repositoryName,
- hasDevScript = false,
showDebugOverlay,
onToggleDebugOverlay,
}: TaskTopBarProps) {
@@ -103,7 +100,7 @@ const TaskTopBar = memo(function TaskTopBar({
};
return (
-
+