"use client"; import { useCallback, useEffect, useState, type KeyboardEvent as ReactKeyboardEvent, type PointerEvent as ReactPointerEvent, } from "react"; import { DockviewDefaultTab, type IDockviewPanelHeaderProps } from "dockview-react"; import { IconStar } from "@tabler/icons-react"; import { AgentLogo } from "@/components/agent-logo"; import { GridSpinner } from "@/components/grid-spinner"; import { ContextMenu, ContextMenuTrigger } from "@kandev/ui/context-menu"; import { useAppStore, useAppStoreApi } from "@/components/state-provider"; import { useToast } from "@/components/toast-provider"; import { renameSession } from "@/lib/api/domains/session-api"; import { useSessionActions, isSessionDeletable as isDeletable, } from "@/hooks/domains/session/use-session-actions"; import { shareableSessionStateClient } from "@/components/task/share/share-button"; import type { HandoffPreset } from "@/components/task/new-session-dialog"; import { usableConfigOptions } from "@/components/model-config-selector"; import { SessionContextMenuItems, SessionTabDialogs } from "./session-tab-menu"; import type { TaskSessionState } from "@/lib/types/http"; import { markSessionTabUserActivationIntent, shouldMarkSessionTabUserActivationIntent, } from "./session-tab-activation-intent"; import { isSessionActive } from "./session-sort"; import { resolveSessionTabTitle } from "./session-tab-title"; import { TabRenameInput } from "./tab-rename-input"; import { useTabMaximizeOnDoubleClick } from "./use-tab-maximize"; import { SessionTabCloseAction } from "./session-tab-close-action"; import { useSessionTabDelete } from "./use-session-tab-delete"; function useSessionTabState(sessionId: string | undefined) { const isPrimary = useAppStore((state) => { const activeTaskId = state.tasks.activeTaskId; if (!activeTaskId || !sessionId) return false; const task = state.kanban.tasks.find((t: { id: string }) => t.id === activeTaskId); if (task?.primarySessionId) return task.primarySessionId === sessionId; return state.taskSessions.items[sessionId]?.is_primary === true; }); const sessionState = useAppStore((state) => { if (!sessionId) return null; return state.taskSessions.items[sessionId]?.state ?? null; }) as TaskSessionState | null; const taskId = useAppStore((state) => state.tasks.activeTaskId); const sessionName = useAppStore((state) => { if (!sessionId) return null; return state.taskSessions.items[sessionId]?.name ?? null; }); const tabTitle = useAppStore((state) => { if (!sessionId) return null; const session = state.taskSessions.items[sessionId]; const sessionModels = state.sessionModels.bySessionId[sessionId]; const activeModelId = state.activeModel.bySessionId[sessionId] || null; const agentLabel = (() => { if (!session?.agent_profile_id) return null; const profile = state.agentProfiles.items.find( (p: { id: string }) => p.id === session.agent_profile_id, ); if (!profile) return null; const parts = profile.label.split(" \u2022 "); return parts[1] || parts[0] || profile.label; })(); const snapshotModel = typeof session?.agent_profile_snapshot?.model === "string" ? session.agent_profile_snapshot.model : null; return resolveSessionTabTitle({ customName: session?.name ?? null, agentLabel, activeModelId, currentModelId: sessionModels?.currentModelId || null, snapshotModel, modelOptions: sessionModels?.models.map((model) => ({ id: model.modelId, name: model.name, description: model.description, usageMultiplier: model.usageMultiplier, })) ?? [], configOptions: usableConfigOptions(sessionModels?.configOptions), }); }); const agentName = useAppStore((state) => { if (!sessionId) return null; const session = state.taskSessions.items[sessionId]; if (!session?.agent_profile_id) return null; return ( state.agentProfiles.items.find((p: { id: string }) => p.id === session.agent_profile_id) ?.agent_name ?? null ); }); const sessionNumber = useAppStore((state) => { if (!sessionId) return null; const activeTaskId = state.tasks.activeTaskId; const sessions = activeTaskId ? state.taskSessionsByTask.itemsByTaskId[activeTaskId] : null; if (!sessions) return null; // Sort chronologically (oldest first) so indexes are stable regardless of // which session is primary or the backend's default DESC ordering. const sorted = [...sessions].sort( (a: { started_at: string }, b: { started_at: string }) => new Date(a.started_at).getTime() - new Date(b.started_at).getTime(), ); const idx = sorted.findIndex((s: { id: string }) => s.id === sessionId); return idx >= 0 ? idx + 1 : null; }); const sessionCount = useAppStore((state) => { const activeTaskId = state.tasks.activeTaskId; if (!activeTaskId) return 0; return state.taskSessionsByTask.itemsByTaskId[activeTaskId]?.length ?? 0; }); return { isPrimary, sessionState, taskId, tabTitle, sessionName, agentName, sessionNumber, sessionCount, }; } /** Mirrors the backend's maxSessionNameLength so the optimistic store update * matches what the rename broadcast will echo back. */ const MAX_SESSION_NAME_LENGTH = 120; /** Commit a session tab rename: persist via WS and optimistically update the store. */ function useSessionRenameCommitter( sessionId: string | undefined, taskId: string | null, currentName: string | null, onDone: () => void, ) { const appStoreApi = useAppStoreApi(); const { toast } = useToast(); return useCallback( async (next: string) => { onDone(); if (!sessionId || !taskId) return; const normalized = next.trim().slice(0, MAX_SESSION_NAME_LENGTH); if ((currentName ?? "") === normalized) return; try { await renameSession(sessionId, normalized); const existing = appStoreApi.getState().taskSessions.items[sessionId]; if (existing) { appStoreApi .getState() .upsertTaskSessionFromEvent(taskId, { ...existing, name: normalized }); } } catch (error) { console.error("rename session:", error); toast({ title: "Rename failed", description: error instanceof Error ? error.message : "Unknown error", variant: "error", }); } }, [sessionId, taskId, currentName, appStoreApi, onDone, toast], ); } function useSessionTabActions( sessionId: string | undefined, taskId: string | null, api: IDockviewPanelHeaderProps["api"], containerApi: IDockviewPanelHeaderProps["containerApi"], ) { const onDeleted = useCallback(() => { const panel = containerApi.getPanel(api.id); if (panel) containerApi.removePanel(panel); }, [api.id, containerApi]); const { setPrimary: handleSetPrimary, stop: handleStop, resume: handleResume, remove: handleDelete, } = useSessionActions({ sessionId, taskId, onDeleted }); const handleCloseOthers = useCallback(() => { const toClose = api.group.panels.filter((p) => p.id !== api.id); for (const panel of toClose) containerApi.removePanel(panel); }, [api, containerApi]); return { handleSetPrimary, handleStop, handleResume, handleDelete, handleCloseOthers }; } function useSessionTabUserActivationIntent( sessionId: string | undefined, activeSessionId: string | null, isActive: boolean, ) { const markUserActivationIntent = useCallback( (target: EventTarget | null) => { if ( !shouldMarkSessionTabUserActivationIntent({ sessionId, activeSessionId, isActive, target }) ) return; markSessionTabUserActivationIntent(sessionId); }, [activeSessionId, isActive, sessionId], ); const handlePointerDownCapture = useCallback( (event: ReactPointerEvent) => { if (event.button === 0) markUserActivationIntent(event.target); }, [markUserActivationIntent], ); const handleKeyDownCapture = useCallback( (event: ReactKeyboardEvent) => { if (event.key === "Enter" || event.key === " ") markUserActivationIntent(event.target); }, [markUserActivationIntent], ); return { handlePointerDownCapture, handleKeyDownCapture }; } function useDockviewTabActiveState(api: IDockviewPanelHeaderProps["api"]) { const [isActive, setIsActive] = useState(api.isActive); useEffect(() => { const disposable = api.onDidActiveChange((e) => setIsActive(e.isActive)); return () => disposable.dispose(); }, [api]); return isActive; } function SessionTabTriggerContent({ props, sessionId, isPrimary, showMultiSessionBadges, sessionNumber, agentName, sessionState, isActive, showDeleteOnClose, isDeleting, onCloseTab, }: { props: IDockviewPanelHeaderProps; sessionId: string | undefined; isPrimary: boolean; showMultiSessionBadges: boolean; sessionNumber: number | null; agentName: string | null; sessionState: TaskSessionState | null; isActive: boolean; showDeleteOnClose: boolean; isDeleting: boolean; onCloseTab: () => void; }) { return (