import {
  CopyLine as ClipboardCopyIcon,
  CopyLine as CopyIcon,
  DeleteLine as Trash2Icon,
  ExternalLinkLine as ExternalLinkIcon,
  FileNewLine as FilePlusIcon,
  FolderOpenLine as FolderOpenIcon,
  NewFolderLine as FolderPlusIcon,
  PencilLine as PencilIcon,
  PinLine as PinIcon,
  PinLine as PinOffIcon,
  Refresh1Line as RefreshCwIcon,
  TransferVerticalLine as ArrowUpDownIcon,
} from '@mingcute/react'
import { useMutation, useQueries, useQueryClient } from '@tanstack/react-query'
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { shallow } from 'zustand/shallow'

import { patchSessionsById, postSessionsByIdRead } from '~/api-gen'
import {
  getSessionsByIdQueryKey,
  patchWorkspacesByWorkspaceIdLocationMutation,
  patchWorkspacesByWorkspaceIdMutation,
  postWorkspacesByWorkspaceIdFilesFileMutation,
  postWorkspacesByWorkspaceIdFilesFolderMutation,
  postWorkspacesMultiFolderMutation,
} from '~/api-gen/@tanstack/react-query.gen'
import { ScrollArea } from '~/components/ui/scroll-area'
import { toastManager } from '~/components/ui/toast'
import { useRuntimeCatalog } from '~/features/agent-runtime/use-runtime-catalog'
import { runtimeSessionStatusQueryOptions } from '~/features/chat/commands/runtime-session-status-command'
import { prefetchChatSession } from '~/features/chat/session/chat-session-prefetch'
import { useDirectoryPicker } from '~/features/filesystem/directory-picker-provider'
import { KanbanSidebar } from '~/features/kanban/kanban-sidebar'
import { PluginsSidebar } from '~/features/plugins/plugins-sidebar'
import { useRemoteHostsQuery } from '~/features/remote-hosts/use-remote-host-connection'
import { useGlobalSearchStore } from '~/features/search/global-search-store'
import { GithubRequiredDialog } from '~/features/settings/github-required-dialog'
import { openGithubRequiredDialog } from '~/features/settings/github-required-dialog-store'
import { useFeatureFlag } from '~/features/settings/use-app-preferences'
import { useGithubAppConnected } from '~/features/settings/use-github-app-connected'
import type { WorkSummary } from '~/features/work/use-work'
import { useWorks, useWorkspaceWorks } from '~/features/work/use-work'
import { MigrateWorkspaceDialog } from '~/features/workspace/migrate-workspace-dialog'
import type { Workspace } from '~/features/workspace/types'
import { getLocalWorkspacePath, getWorkspaceLocationLabel, isLocalWorkspace } from '~/features/workspace/types'
import { cn } from '~/lib/cn'
import { authorizeDangerousAction, isElectron, nativeIpc } from '~/lib/electron'
import { useIsActiveSurfaceId } from '~/navigation/active-surface'
import {
  openAutomation,
  openDiff,
  openNewChat,
  openNewWork,
  openPullRequests,
  openSettingsSection,
  openUsage,
  openWorkspaceDetail,
} from '~/navigation/navigation-commands'
import { chatSelectors, useChatStore } from '~/store/chat'
import { useSettingsOverlayStore } from '~/store/settings-overlay'

import { PreviewCardProvider } from './preview-card/preview-card-provider'
import type { WorkspaceSession } from './use-session'
import { sessionsQueryKey, updateSessionReadState, useAllSessions } from './use-session'
import type { WorkspaceSessionGroup } from './use-session-group'
import {
  useAddSessionGroupMembers,
  useCreateSessionGroup,
  useDeleteSessionGroup,
  useRemoveSessionGroupMember,
  useSessionGroups,
  useUpdateSessionGroup,
} from './use-session-group'
import type { CreateWorkspaceInput } from './use-workspace'
import {
  useAddWorkspace,
  useDeleteWorkspace,
  useToggleWorkspacePin,
  useWorkspaces,
  WORKSPACES_QUERY_KEY,
} from './use-workspace'
import { WorkspaceAddDialog } from './workspace-add-dialog'
import { WorkspaceGroupDisclosure } from './workspace-group-disclosure'
import type { WorkspaceMenuAction } from './workspace-group-disclosure-view'
import { WorkspaceProjectsSectionView } from './workspace-projects-section-view'
import { WorkspaceRecentSessionListView } from './workspace-recent-session-list-view'
import { WorkspaceRecognitionDialogView } from './workspace-recognition-dialog-view'
import { WorkspaceSessionActionsMenu } from './workspace-session-actions-menu'
import type { WorkspaceSessionActionsMenuState } from './workspace-session-actions-menu-state'
import { CLOSED_WORKSPACE_SESSION_ACTIONS_MENU_STATE } from './workspace-session-actions-menu-state'
import { partitionWorkspaceSessions } from './workspace-session-group-partition'
import { WorkspaceSessionGroupSection } from './workspace-session-groups'
import type { WorkspaceSessionItemMenuRequest } from './workspace-session-item'
import type { WorkspaceSessionAttentionKind } from './workspace-session-item-view'
import type { WorkspaceRuntimeIconByKind } from './workspace-session-list-section'
import { WorkspaceSessionListSection } from './workspace-session-list-section'
import { isWorkspaceSessionRunning } from './workspace-session-status'
import type { SidebarSessionEntry } from './workspace-sidebar-grouping'
import {
  compareSidebarSessions,
  computeSectionPreviewCount,
  groupSidebarSessions,
} from './workspace-sidebar-grouping'
import {
  projectMatchesListFilters,
  sessionMatchesListFilters,
} from './workspace-sidebar-list-filters'
import { WorkspaceSidebarNavigationView } from './workspace-sidebar-navigation-view'
import type { WorkspaceSidebarListFilters } from './workspace-sidebar-ui-store'
import { useWorkspaceSidebarUiStore } from './workspace-sidebar-ui-store'
import { WorkspaceTextInputDialogView } from './workspace-text-input-dialog-view'

const DEFAULT_WORKSPACE_FILE_NAME = 'untitled'
const DEFAULT_WORKSPACE_FOLDER_NAME = 'untitled-folder'
const EMPTY_WORKSPACE_SESSIONS: WorkspaceSession[] = []
const EMPTY_SESSION_ID_SET = new Set<string>()
const EMPTY_WORK_BY_PRIMARY_SESSION_ID = new Map<string, WorkSummary>()

type SessionAttentionKind = WorkspaceSessionAttentionKind
const EMPTY_ATTENTION_BY_SESSION_ID = new Map<string, SessionAttentionKind>()

function formatToastError(error: unknown): string {
  if (error instanceof Error) {
    return error.message
  }
  if (
    error
    && typeof error === 'object'
    && 'message' in error
    && typeof (error as { message: unknown }).message === 'string'
  ) {
    return (error as { message: string }).message
  }
  if (typeof error === 'string') {
    return error
  }
  return JSON.stringify(error)
}

function useSessionAttentionBySessionId(
  sessions: readonly WorkspaceSession[],
  locallyStreamingSessionIds: Set<string>,
): Map<string, SessionAttentionKind> {
  const activeSessionIds = useMemo(
    () =>
      sessions
        .filter(session => isWorkspaceSessionRunning(session, locallyStreamingSessionIds))
        .map(session => session.id),
    [locallyStreamingSessionIds, sessions],
  )
  const statusQueries = useQueries({
    queries: activeSessionIds.map(sessionId => ({
      ...runtimeSessionStatusQueryOptions(sessionId),
      staleTime: 1_000,
      refetchInterval: false as const,
      refetchIntervalInBackground: true,
    })),
  })

  return useMemo(() => {
    const attentionBySessionId = new Map<string, SessionAttentionKind>()
    statusQueries.forEach((query, index) => {
      const sessionId = activeSessionIds[index]
      if (!sessionId) {
        return
      }
      if (query.data?.status === 'waitingForUserInput') {
        attentionBySessionId.set(sessionId, 'userInput')
        return
      }
      if (query.data?.status === 'waitingForToolApproval') {
        attentionBySessionId.set(sessionId, 'toolApproval')
      }
    })
    return attentionBySessionId
  }, [activeSessionIds, statusQueries])
}

type RuntimeIconByKind = WorkspaceRuntimeIconByKind

type SessionMenuRequest = WorkspaceSessionItemMenuRequest

const WorkspaceGroup = memo(
  ({
    workspace,
    sessions,
    flatSession = false,
    listFilters,
    workByPrimarySessionId,
    runtimeIconByKind,
    onDelete,
    onTogglePin,
  }: {
    workspace: Workspace
    sessions: WorkspaceSession[]
    flatSession?: boolean
    listFilters: WorkspaceSidebarListFilters
    workByPrimarySessionId: ReadonlyMap<string, WorkSummary>
    runtimeIconByKind: RuntimeIconByKind
    onDelete: (id: string) => void
    onTogglePin: (id: string, pinned: boolean) => void
  }) => {
    const { t } = useTranslation('workspace')
    const queryClient = useQueryClient()
    const { selectDirectory } = useDirectoryPicker()
    const [renameOpen, setRenameOpen] = useState(false)
    const [migrateOpen, setMigrateOpen] = useState(false)
    const [retainedSessionIds, setRetainedSessionIds] = useState<Set<string>>(() => new Set())
    const acknowledgedSessionIdsRef = useRef<Set<string> | null>(null)
    if (acknowledgedSessionIdsRef.current === null) {
      acknowledgedSessionIdsRef.current = new Set()
    }
    const [createRequest, setCreateRequest] = useState<{
      kind: 'file' | 'folder'
    } | null>(null)
    const [sessionMenuState, setSessionMenuState] = useState<WorkspaceSessionActionsMenuState>(
      CLOSED_WORKSPACE_SESSION_ACTIONS_MENU_STATE,
    )
    const [renamingSessionId, setRenamingSessionId] = useState<string | null>(null)
    const sessionOrdering = useWorkspaceSidebarUiStore(state => state.sessionOrdering)
    const orderingDirection = useWorkspaceSidebarUiStore(state => state.orderingDirection)
    const workspacePinned = Boolean(workspace.pinned)
    const workspaceSessionIds = useMemo(() => sessions.map(session => session.id), [sessions])
    const sessionsById = useMemo(() => {
      const byId = new Map<string, WorkspaceSession>()
      for (const session of sessions) {
        byId.set(session.id, session)
      }
      return byId
    }, [sessions])
    const activeMenuSession = sessionMenuState.sessionId
      ? (sessionsById.get(sessionMenuState.sessionId) ?? null)
      : null
    const locallyStreamingSessionIds = useChatStore(
      useCallback(
        state =>
          new Set(
            workspaceSessionIds.filter(sessionId =>
              chatSelectors.isSessionStreaming(sessionId)(state)),
          ),
        [workspaceSessionIds],
      ),
      shallow,
    )
    const sessionAttentionBySessionId = useSessionAttentionBySessionId(
      sessions,
      locallyStreamingSessionIds,
    )
    const runningSessionCount = useMemo(
      () =>
        sessions.filter(session => isWorkspaceSessionRunning(session, locallyStreamingSessionIds))
          .length,
      [locallyStreamingSessionIds, sessions],
    )
    const locallyErroredSessionIds = useChatStore(
      useCallback(
        (state) => {
          if (state.errorMap.size === 0) {
            return EMPTY_SESSION_ID_SET
          }
          return new Set(
            workspaceSessionIds.filter(sessionId =>
              Boolean(chatSelectors.latestError(sessionId)(state))),
          )
        },
        [workspaceSessionIds],
      ),
      shallow,
    )
    const { data: workspaceWorks = [] } = useWorkspaceWorks(workspace.id)
    const resolvedWorkByPrimarySessionId = useMemo(() => {
      if (workByPrimarySessionId.size > 0) {
        return workByPrimarySessionId
      }
      return new Map(workspaceWorks.map(work => [work.primarySessionId, work]))
    }, [workByPrimarySessionId, workspaceWorks])
    const filteredSessions = useMemo(() => {
      return sessions.filter(session =>
        sessionMatchesListFilters(
          session,
          resolvedWorkByPrimarySessionId.get(session.id) ?? null,
          listFilters,
          locallyStreamingSessionIds,
          locallyErroredSessionIds,
          sessionAttentionBySessionId,
        ))
    }, [
      listFilters,
      locallyErroredSessionIds,
      locallyStreamingSessionIds,
      resolvedWorkByPrimarySessionId,
      sessionAttentionBySessionId,
      sessions,
    ])
    const { mutateAsync: renameWorkspace } = useMutation({
      ...patchWorkspacesByWorkspaceIdMutation(),
      onSuccess: () => {
        void Promise.all([
          queryClient.invalidateQueries({ queryKey: WORKSPACES_QUERY_KEY }),
          queryClient.invalidateQueries({ queryKey: ['workspace', workspace.id] }),
        ])
      },
    })
    const { mutateAsync: relinkWorkspace } = useMutation({
      ...patchWorkspacesByWorkspaceIdLocationMutation(),
      onSuccess: () => {
        void Promise.all([
          queryClient.invalidateQueries({ queryKey: WORKSPACES_QUERY_KEY }),
          queryClient.invalidateQueries({ queryKey: ['workspace', workspace.id] }),
        ])
      },
    })
    const { mutateAsync: createWorkspaceFile } = useMutation(
      postWorkspacesByWorkspaceIdFilesFileMutation(),
    )
    const { mutateAsync: createWorkspaceFolder } = useMutation(
      postWorkspacesByWorkspaceIdFilesFolderMutation(),
    )
    const sortedSessions = useMemo(() => {
      return filteredSessions.toSorted((a, b) =>
        compareSidebarSessions(a, b, sessionOrdering, orderingDirection))
    }, [filteredSessions, orderingDirection, sessionOrdering])
    const activeMenuWork = sessionMenuState.workId
      ? (workspaceWorks.find(work => work.id === sessionMenuState.workId) ?? null)
      : null
    const sidebarSessions = useMemo(
      () =>
        sortedSessions.filter(
          session => session.origin !== 'work' || resolvedWorkByPrimarySessionId.has(session.id),
        ),
      [resolvedWorkByPrimarySessionId, sortedSessions],
    )
    const { data: sessionGroups = [] } = useSessionGroups(workspace.id)
    const createSessionGroup = useCreateSessionGroup(workspace.id)
    const updateSessionGroup = useUpdateSessionGroup(workspace.id)
    const deleteSessionGroup = useDeleteSessionGroup(workspace.id)
    const addSessionGroupMembers = useAddSessionGroupMembers(workspace.id)
    const removeSessionGroupMember = useRemoveSessionGroupMember(workspace.id)
    const [createGroupOpen, setCreateGroupOpen] = useState(false)
    const [createGroupSeedSession, setCreateGroupSeedSession] = useState<WorkspaceSession | null>(
      null,
    )
    const [renameGroupTarget, setRenameGroupTarget] = useState<WorkspaceSessionGroup | null>(null)
    const { grouped: groupedSessions, ungrouped: ungroupedSessions } = useMemo(
      () => partitionWorkspaceSessions(sidebarSessions, sessionGroups),
      [sessionGroups, sidebarSessions],
    )
    const sortSessionsForList = useCallback(
      (items: WorkspaceSession[]) => {
        return items.toSorted((a, b) =>
          compareSidebarSessions(a, b, sessionOrdering, orderingDirection))
      },
      [orderingDirection, sessionOrdering],
    )
    const handleCreateSessionGroup = useCallback(
      async (titleRaw: string) => {
        const title = titleRaw.trim()
        if (!title) {
          return
        }
        try {
          await createSessionGroup.mutateAsync({
            body: {
              workspaceId: workspace.id,
              title,
              ...(createGroupSeedSession ? { sessionIds: [createGroupSeedSession.id] } : {}),
            },
          })
          setCreateGroupOpen(false)
          setCreateGroupSeedSession(null)
        }
 catch (error) {
          toastManager.add({
            type: 'error',
            title: t('sessionGroup.toast.createFailed'),
            description: formatToastError(error),
          })
        }
      },
      [createGroupSeedSession, createSessionGroup, t, workspace.id],
    )
    const handleRenameSessionGroup = useCallback(
      async (titleRaw: string) => {
        if (!renameGroupTarget) {
          return
        }
        const title = titleRaw.trim()
        if (!title || title === renameGroupTarget.title) {
          setRenameGroupTarget(null)
          return
        }
        try {
          await updateSessionGroup.mutateAsync({
            path: { id: renameGroupTarget.id },
            body: { title },
          })
          setRenameGroupTarget(null)
        }
 catch (error) {
          toastManager.add({
            type: 'error',
            title: t('sessionGroup.toast.renameFailed'),
            description: formatToastError(error),
          })
        }
      },
      [renameGroupTarget, t, updateSessionGroup],
    )
    const handleDeleteSessionGroup = useCallback(
      async (group: WorkspaceSessionGroup) => {
        try {
          await deleteSessionGroup.mutateAsync({ path: { id: group.id } })
        }
 catch (error) {
          toastManager.add({
            type: 'error',
            title: t('sessionGroup.toast.deleteFailed'),
            description: formatToastError(error),
          })
        }
      },
      [deleteSessionGroup, t],
    )
    const handleAddSessionToGroup = useCallback(
      async (sessionId: string, groupId: string) => {
        try {
          await addSessionGroupMembers.mutateAsync({
            path: { id: groupId },
            body: { sessionIds: [sessionId] },
          })
        }
 catch (error) {
          toastManager.add({
            type: 'error',
            title: t('sessionGroup.toast.addMemberFailed'),
            description: formatToastError(error),
          })
        }
      },
      [addSessionGroupMembers, t],
    )
    const handleRemoveSessionFromGroup = useCallback(
      async (session: WorkspaceSession) => {
        if (!session.sessionGroupId) {
          return
        }
        try {
          await removeSessionGroupMember.mutateAsync({
            groupId: session.sessionGroupId,
            sessionId: session.id,
          })
        }
 catch (error) {
          toastManager.add({
            type: 'error',
            title: t('sessionGroup.toast.removeMemberFailed'),
            description: formatToastError(error),
          })
        }
      },
      [removeSessionGroupMember, t],
    )
    const handleCreateSessionGroupFromSession = useCallback((session: WorkspaceSession) => {
      setCreateGroupSeedSession(session)
      setCreateGroupOpen(true)
    }, [])
    const workspaceLocalPath = getLocalWorkspacePath(workspace)
    const workspaceLocationLabel = getWorkspaceLocationLabel(workspace)

    useEffect(() => {
      setRetainedSessionIds((current) => {
        let changed = false
        const next = new Set<string>()
        const knownSessionIds = new Set(workspaceSessionIds)

        for (const sessionId of current) {
          if (knownSessionIds.has(sessionId)) {
            next.add(sessionId)
          }
 else {
            changed = true
          }
        }

        for (const session of sessions) {
          if (
            isWorkspaceSessionRunning(session, locallyStreamingSessionIds)
            && !acknowledgedSessionIdsRef.current!.has(session.id)
            && !next.has(session.id)
          ) {
            next.add(session.id)
            changed = true
          }
        }

        return changed ? next : current
      })
    }, [locallyStreamingSessionIds, sessions, workspaceSessionIds])

    const handleOpenSession = useCallback((sessionId: string) => {
      acknowledgedSessionIdsRef.current!.add(sessionId)
      setRetainedSessionIds((current) => {
        if (!current.has(sessionId)) {
          return current
        }
        const next = new Set(current)
        next.delete(sessionId)
        return next
      })
    }, [])
    const prefetchSession = useCallback(
      (sessionId: string) => {
        prefetchChatSession(queryClient, sessionId)
      },
      [queryClient],
    )
    const handlePrepareSessionOpen = useCallback(
      (session: WorkspaceSession) => {
        handleOpenSession(session.id)
        prefetchSession(session.id)
      },
      [handleOpenSession, prefetchSession],
    )
    const handleRenameSession = useCallback(
      async (session: WorkspaceSession, nextTitleRaw: string) => {
        const nextTitle = nextTitleRaw.trim()
        setRenamingSessionId(null)

        if (!nextTitle || nextTitle === (session.title ?? t('session.fallbackTitle'))) {
          return
        }

        await patchSessionsById({ path: { id: session.id }, body: { title: nextTitle } })
        await Promise.all([
          queryClient.invalidateQueries({ queryKey: sessionsQueryKey(workspace.id) }),
          queryClient.invalidateQueries({ queryKey: sessionsQueryKey() }),
          queryClient.invalidateQueries({
            queryKey: getSessionsByIdQueryKey({ path: { id: session.id } }),
          }),
        ])
      },
      [queryClient, t, workspace.id],
    )
    const handleRenameCancel = useCallback(() => {
      setRenamingSessionId(null)
    }, [])
    const handleStartSessionRename = useCallback((sessionId: string) => {
      setRenamingSessionId(sessionId)
    }, [])
    const handleOpenSessionMenu = useCallback((request: SessionMenuRequest) => {
      setSessionMenuState({
        ...request,
        open: true,
      })
    }, [])
    const handleSessionMenuOpenChange = useCallback((open: boolean) => {
      setSessionMenuState(current =>
        open && current.anchor
          ? { ...current, open: true }
          : CLOSED_WORKSPACE_SESSION_ACTIONS_MENU_STATE)
    }, [])

    useEffect(() => {
      const next = new Set<string>()

      for (const sessionId of acknowledgedSessionIdsRef.current!) {
        const session = sessionsById.get(sessionId)
        if (session && isWorkspaceSessionRunning(session, locallyStreamingSessionIds)) {
          next.add(sessionId)
        }
      }

      acknowledgedSessionIdsRef.current! = next
    }, [locallyStreamingSessionIds, sessionsById])
    const handleTogglePin = useCallback(() => {
      onTogglePin(workspace.id, !workspacePinned)
    }, [onTogglePin, workspace.id, workspacePinned])
    const handleOpenWorkspace = useCallback(() => {
      openWorkspaceDetail(workspace.id)
    }, [workspace.id])
    const handleRelinkWorkspace = useCallback(async () => {
      const path = await selectDirectory({
        title: t('workspace.dialog.relinkTitle'),
        description: t('workspace.dialog.relinkDescription', { name: workspace.name }),
      })
      if (!path) {
        return
      }
      try {
        await relinkWorkspace({
          path: { workspaceId: workspace.id },
          body: { path },
        })
      }
 catch (error) {
        toastManager.add({
          type: 'error',
          title: t('workspace.toast.relinkFailed'),
          description: formatToastError(error),
        })
      }
    }, [relinkWorkspace, selectDirectory, t, workspace.id, workspace.name])
    const handleOpenDefault = useCallback(async () => {
      if (!isElectron || !nativeIpc) {
        return
      }
      if (!workspaceLocalPath) {
        return
      }

      try {
        await nativeIpc.native.openPath(workspaceLocalPath)
      }
 catch (error) {
        toastManager.add({
          type: 'error',
          title: t('workspace.toast.openDefaultFailed'),
          description: formatToastError(error),
        })
      }
    }, [t, workspaceLocalPath])
    const handleRevealInFinder = useCallback(async () => {
      if (!isElectron || !nativeIpc) {
        return
      }
      if (!workspaceLocalPath) {
        return
      }

      try {
        await nativeIpc.native.showItemInFolder(workspaceLocalPath)
      }
 catch (error) {
        toastManager.add({
          type: 'error',
          title: t('workspace.toast.openInFinderFailed'),
          description: formatToastError(error),
        })
      }
    }, [t, workspaceLocalPath])
    const handleCopyAbsolutePath = useCallback(async () => {
      await navigator.clipboard.writeText(workspaceLocationLabel)
    }, [workspaceLocationLabel])
    const handleRenameWorkspace = useCallback(
      async (value: string) => {
        const name = value.trim()
        if (!name || name === workspace.name) {
          setRenameOpen(false)
          return
        }

        try {
          await renameWorkspace({ path: { workspaceId: workspace.id }, body: { name } })
          setRenameOpen(false)
        }
 catch (error) {
          toastManager.add({
            type: 'error',
            title: t('workspace.toast.renameFailed'),
            description: formatToastError(error),
          })
        }
      },
      [renameWorkspace, t, workspace.id, workspace.name],
    )
    const handleCreateWorkspaceChild = useCallback(
      async (nameValue: string) => {
        if (!createRequest) {
          return
        }

        const name = nameValue.trim()
        if (!name) {
          return
        }

        const request = {
          path: { workspaceId: workspace.id },
          body: {
            path: name,
            confirmedNonCradleOwnedWrite: true,
          },
        }
        try {
          const data
            = createRequest.kind === 'file'
              ? await createWorkspaceFile(request)
              : await createWorkspaceFolder(request)

          if (!data.success) {
            toastManager.add({
              type: 'error',
              title: t('workspace.toast.createFailed'),
            })
            return
          }

          await queryClient.invalidateQueries({ queryKey: ['workspace-file-search', workspace.id] })
          setCreateRequest(null)
        }
 catch (error) {
          toastManager.add({
            type: 'error',
            title: t('workspace.toast.createFailed'),
            description: formatToastError(error),
          })
        }
      },
      [createRequest, createWorkspaceFile, createWorkspaceFolder, queryClient, t, workspace.id],
    )
    const handleOpenCreateDialogChange = useCallback((open: boolean) => {
      if (!open) {
        setCreateRequest(null)
      }
    }, [])
    const workspaceActions = useMemo<WorkspaceMenuAction[]>(
      () => [
        {
          key: 'open',
          label: t('workspace.action.open'),
          icon: <ExternalLinkIcon />,
          testId: `workspace-open-action-${workspace.id}`,
          invoke: handleOpenWorkspace,
        },
        ...(workspace.availability === 'missing'
          ? ([
              {
                key: 'relink',
                label: t('workspace.action.relink'),
                icon: <RefreshCwIcon />,
                testId: `workspace-relink-${workspace.id}`,
                invoke: handleRelinkWorkspace,
              },
            ] satisfies WorkspaceMenuAction[])
          : []),
        ...(workspaceLocalPath
          ? ([
              {
                key: 'open-default',
                label: t('workspace.action.openDefault'),
                icon: <ExternalLinkIcon />,
                testId: `workspace-open-default-${workspace.id}`,
                invoke: handleOpenDefault,
              },
              {
                key: 'open-in-finder',
                label: t('workspace.action.openInFinder'),
                icon: <FolderOpenIcon />,
                testId: `workspace-open-in-finder-${workspace.id}`,
                invoke: handleRevealInFinder,
              },
              {
                key: 'new-file',
                label: t('workspace.action.newFile'),
                icon: <FilePlusIcon />,
                testId: `workspace-new-file-${workspace.id}`,
                invoke: () => setCreateRequest({ kind: 'file' }),
                separatorBefore: true,
              },
              {
                key: 'new-folder',
                label: t('workspace.action.newFolder'),
                icon: <FolderPlusIcon />,
                testId: `workspace-new-folder-${workspace.id}`,
                invoke: () => setCreateRequest({ kind: 'folder' }),
              },
            ] satisfies WorkspaceMenuAction[])
          : []),
        {
          key: 'rename',
          label: t('workspace.action.rename'),
          icon: <PencilIcon />,
          testId: `workspace-rename-${workspace.id}`,
          invoke: () => setRenameOpen(true),
        },
        {
          key: 'copy-path',
          label: t('workspace.action.copyPath'),
          icon: <CopyIcon />,
          testId: `workspace-copy-path-${workspace.id}`,
          invoke: handleCopyAbsolutePath,
          separatorBefore: true,
        },
        {
          key: 'copy-relative-path',
          label: t('workspace.action.copyRelativePath'),
          icon: <ClipboardCopyIcon />,
          testId: `workspace-copy-relative-path-${workspace.id}`,
          invoke: async () => navigator.clipboard.writeText('.'),
        },
        {
          key: 'new-session-group',
          label: t('sessionGroup.action.create'),
          icon: <FolderPlusIcon />,
          testId: `workspace-new-session-group-${workspace.id}`,
          invoke: () => {
            setCreateGroupSeedSession(null)
            setCreateGroupOpen(true)
          },
          separatorBefore: true,
        },
        {
          key: 'toggle-pin',
          label: workspacePinned ? t('workspace.action.unpin') : t('workspace.action.pin'),
          icon: workspacePinned ? <PinOffIcon /> : <PinIcon />,
          testId: `workspace-toggle-pin-${workspace.id}`,
          invoke: handleTogglePin,
          separatorBefore: true,
        },
        {
          key: 'migrate',
          label: t('workspace.action.migrate'),
          icon: <ArrowUpDownIcon />,
          testId: `workspace-migrate-${workspace.id}`,
          invoke: () => setMigrateOpen(true),
          separatorBefore: true,
        },
        {
          key: 'remove',
          label: t('workspace.action.remove'),
          icon: <Trash2Icon />,
          testId: `workspace-remove-${workspace.id}`,
          invoke: () => onDelete(workspace.id),
          variant: 'destructive',
          separatorBefore: true,
        },
      ],
      [
        handleCopyAbsolutePath,
        handleOpenDefault,
        handleOpenWorkspace,
        handleRelinkWorkspace,
        handleRevealInFinder,
        handleTogglePin,
        onDelete,
        t,
        workspace.id,
        workspace.availability,
        workspaceLocalPath,
        workspacePinned,
      ],
    )

    const overlays = (
      <>
        <WorkspaceTextInputDialogView
          open={renameOpen}
          title={t('workspace.dialog.renameTitle')}
          initialValue={workspace.name}
          label={t('workspace.dialog.nameLabel')}
          confirmLabel={t('workspace.dialog.rename')}
          onOpenChange={setRenameOpen}
          onCommit={handleRenameWorkspace}
        />
        <MigrateWorkspaceDialog
          open={migrateOpen}
          onOpenChange={setMigrateOpen}
          sourceWorkspaceId={workspace.id}
        />
        <WorkspaceTextInputDialogView
          open={createRequest !== null}
          title={
            createRequest?.kind === 'folder'
              ? t('workspace.dialog.newFolderTitle')
              : t('workspace.dialog.newFileTitle')
          }
          initialValue={
            createRequest?.kind === 'folder'
              ? DEFAULT_WORKSPACE_FOLDER_NAME
              : DEFAULT_WORKSPACE_FILE_NAME
          }
          label={t('workspace.dialog.nameLabel')}
          confirmLabel={t('workspace.dialog.create')}
          onOpenChange={handleOpenCreateDialogChange}
          onCommit={handleCreateWorkspaceChild}
        />
        <WorkspaceTextInputDialogView
          open={createGroupOpen}
          title={t('sessionGroup.dialog.createTitle')}
          initialValue=""
          label={t('sessionGroup.dialog.titleLabel')}
          confirmLabel={t('sessionGroup.dialog.create')}
          onOpenChange={(open) => {
            setCreateGroupOpen(open)
            if (!open) {
              setCreateGroupSeedSession(null)
            }
          }}
          onCommit={handleCreateSessionGroup}
        />
        <WorkspaceTextInputDialogView
          open={renameGroupTarget !== null}
          title={t('sessionGroup.dialog.renameTitle')}
          initialValue={renameGroupTarget?.title ?? ''}
          label={t('sessionGroup.dialog.titleLabel')}
          confirmLabel={t('sessionGroup.dialog.rename')}
          onOpenChange={(open) => {
            if (!open) {
              setRenameGroupTarget(null)
            }
          }}
          onCommit={handleRenameSessionGroup}
        />
        <WorkspaceSessionActionsMenu
          state={sessionMenuState}
          session={activeMenuSession}
          work={activeMenuWork}
          workspaceId={workspace.id}
          sessionGroups={sessionGroups}
          onOpenChange={handleSessionMenuOpenChange}
          onPrepareSessionOpen={handlePrepareSessionOpen}
          onStartRename={handleStartSessionRename}
          onAddSessionToGroup={handleAddSessionToGroup}
          onRemoveSessionFromGroup={handleRemoveSessionFromGroup}
          onCreateSessionGroupFromSession={handleCreateSessionGroupFromSession}
        />
      </>
    )

    if (flatSession) {
      return (
        <div
          className="flex min-w-0 flex-col"
          data-testid={`recent-session-workspace-group-${workspace.id}`}
        >
          {overlays}
          <WorkspaceSessionListSection
            workspaceId={workspace.id}
            variant="flat"
            workspace={workspace}
            sortedSessions={sidebarSessions}
            workByPrimarySessionId={resolvedWorkByPrimarySessionId}
            renamingSessionId={renamingSessionId}
            retainedSessionIds={retainedSessionIds}
            locallyStreamingSessionIds={locallyStreamingSessionIds}
            sessionAttentionBySessionId={sessionAttentionBySessionId}
            locallyErroredSessionIds={locallyErroredSessionIds}
            runtimeIconByKind={runtimeIconByKind}
            onPrepareSessionOpen={handlePrepareSessionOpen}
            onPrefetchSession={prefetchSession}
            onRenameCommit={handleRenameSession}
            onRenameCancel={handleRenameCancel}
            onOpenSessionMenu={handleOpenSessionMenu}
          />
        </div>
      )
    }

    return (
      <WorkspaceGroupDisclosure
        workspace={workspace}
        workspacePinned={workspacePinned}
        workspaceActions={workspaceActions}
        runningSessionCount={runningSessionCount}
        overlays={overlays}
      >
        <div className="flex min-w-0 flex-col">
          {groupedSessions.map(({ group, sessions: groupSessions }) => (
            <WorkspaceSessionGroupSection
              key={group.id}
              group={group}
              sessions={groupSessions}
              workspaceId={workspace.id}
              onRenameGroup={setRenameGroupTarget}
              onDeleteGroup={handleDeleteSessionGroup}
            >
              <WorkspaceSessionListSection
                workspaceId={workspace.id}
                sortedSessions={sortSessionsForList(groupSessions)}
                workByPrimarySessionId={resolvedWorkByPrimarySessionId}
                renamingSessionId={renamingSessionId}
                retainedSessionIds={retainedSessionIds}
                locallyStreamingSessionIds={locallyStreamingSessionIds}
                sessionAttentionBySessionId={sessionAttentionBySessionId}
                locallyErroredSessionIds={locallyErroredSessionIds}
                runtimeIconByKind={runtimeIconByKind}
                onPrepareSessionOpen={handlePrepareSessionOpen}
                onPrefetchSession={prefetchSession}
                onRenameCommit={handleRenameSession}
                onRenameCancel={handleRenameCancel}
                onOpenSessionMenu={handleOpenSessionMenu}
              />
            </WorkspaceSessionGroupSection>
          ))}
          {(ungroupedSessions.length > 0 || groupedSessions.length === 0) && (
            <WorkspaceSessionListSection
              workspaceId={workspace.id}
              sortedSessions={sortSessionsForList(ungroupedSessions)}
              workByPrimarySessionId={resolvedWorkByPrimarySessionId}
              renamingSessionId={renamingSessionId}
              retainedSessionIds={retainedSessionIds}
              locallyStreamingSessionIds={locallyStreamingSessionIds}
              sessionAttentionBySessionId={sessionAttentionBySessionId}
              locallyErroredSessionIds={locallyErroredSessionIds}
              runtimeIconByKind={runtimeIconByKind}
              onPrepareSessionOpen={handlePrepareSessionOpen}
              onPrefetchSession={prefetchSession}
              onRenameCommit={handleRenameSession}
              onRenameCancel={handleRenameCancel}
              onOpenSessionMenu={handleOpenSessionMenu}
            />
          )}
        </div>
      </WorkspaceGroupDisclosure>
    )
  },
)
WorkspaceGroup.displayName = 'WorkspaceGroup'

// ── Main sidebar content ──────────────────────────────────────────────────────

interface WorkspaceSidebarBodyProps {
  workspaces: Workspace[]
  workspacesReady: boolean
  sessionsByWorkspaceId: Map<string, WorkspaceSession[]>
  workByPrimarySessionId: ReadonlyMap<string, WorkSummary>
  runtimeIconByKind: RuntimeIconByKind
  adding: boolean
  multiWorkspaceEnabled: boolean
  multiFolderCreating: boolean
  onAddFromPicker: () => void
  onCreateMultiFolder: (input: {
    name: string
    folders: Array<{ name: string, path: string }>
  }) => Promise<void>
  hasUnreadWorkspaceSessions: boolean
  markingAllSessionsRead: boolean
  onMarkAllAsRead: () => void
  onDelete: (id: string) => void
  onTogglePin: (id: string, pinned: boolean) => void
}

const WorkspaceSidebarBody = memo(
  ({
    workspaces,
    workspacesReady,
    sessionsByWorkspaceId,
    workByPrimarySessionId,
    runtimeIconByKind,
    adding,
    multiWorkspaceEnabled,
    multiFolderCreating,
    onAddFromPicker,
    onCreateMultiFolder,
    hasUnreadWorkspaceSessions,
    markingAllSessionsRead,
    onMarkAllAsRead,
    onDelete,
    onTogglePin,
  }: WorkspaceSidebarBodyProps) => {
    const { t } = useTranslation('workspace')
    const pruneWorkspaceSidebarState = useWorkspaceSidebarUiStore(
      state => state.pruneWorkspaceSidebarState,
    )
    const statusFilters = useWorkspaceSidebarUiStore(state => state.statusFilters)
    const workPrFilters = useWorkspaceSidebarUiStore(state => state.workPrFilters)
    const environmentFilters = useWorkspaceSidebarUiStore(state => state.environmentFilters)
    const sourceFilters = useWorkspaceSidebarUiStore(state => state.sourceFilters)
    const showArchived = useWorkspaceSidebarUiStore(state => state.showArchived)
    const grouping = useWorkspaceSidebarUiStore(state => state.grouping)
    const sessionOrdering = useWorkspaceSidebarUiStore(state => state.sessionOrdering)
    const orderingDirection = useWorkspaceSidebarUiStore(state => state.orderingDirection)
    const sessionPreviewLimit = useWorkspaceSidebarUiStore(state => state.sessionPreviewLimit)
    const setGrouping = useWorkspaceSidebarUiStore(state => state.setGrouping)
    const setSessionOrdering = useWorkspaceSidebarUiStore(state => state.setSessionOrdering)
    const setOrderingDirection = useWorkspaceSidebarUiStore(state => state.setOrderingDirection)
    const toggleStatusFilter = useWorkspaceSidebarUiStore(state => state.toggleStatusFilter)
    const toggleWorkPrFilter = useWorkspaceSidebarUiStore(state => state.toggleWorkPrFilter)
    const toggleEnvironmentFilter = useWorkspaceSidebarUiStore(
      state => state.toggleEnvironmentFilter,
    )
    const toggleSourceFilter = useWorkspaceSidebarUiStore(state => state.toggleSourceFilter)
    const setShowArchived = useWorkspaceSidebarUiStore(state => state.setShowArchived)
    const clearListFilters = useWorkspaceSidebarUiStore(state => state.clearListFilters)
    const setSessionPreviewLimit = useWorkspaceSidebarUiStore(
      state => state.setSessionPreviewLimit,
    )
    const collapseAllWorkspaces = useWorkspaceSidebarUiStore(state => state.collapseAllWorkspaces)
    const [expandedFlatSectionKeys, setExpandedFlatSectionKeys]
      = useState<ReadonlySet<string>>(EMPTY_SESSION_ID_SET)

    const listFilters = useMemo<WorkspaceSidebarListFilters>(
      () => ({
        statusFilters,
        workPrFilters,
        environmentFilters,
        sourceFilters,
        showArchived,
      }),
      [environmentFilters, showArchived, sourceFilters, statusFilters, workPrFilters],
    )

    const workspaceIds = useMemo(() => workspaces.map(workspace => workspace.id), [workspaces])
    const allSessions = useMemo(() => {
      const sessions: WorkspaceSession[] = []
      for (const workspaceSessions of sessionsByWorkspaceId.values()) {
        sessions.push(...workspaceSessions)
      }
      return sessions
    }, [sessionsByWorkspaceId])
    const sessionIds = useMemo(() => allSessions.map(session => session.id), [allSessions])
    const locallyStreamingSessionIds = useChatStore(
      useCallback(
        state =>
          new Set(
            sessionIds.filter(sessionId => chatSelectors.isSessionStreaming(sessionId)(state)),
          ),
        [sessionIds],
      ),
      shallow,
    )
    const locallyErroredSessionIds = useChatStore(
      useCallback(
        (state) => {
          if (state.errorMap.size === 0) {
            return EMPTY_SESSION_ID_SET
          }
          return new Set(
            sessionIds.filter(sessionId => Boolean(chatSelectors.latestError(sessionId)(state))),
          )
        },
        [sessionIds],
      ),
      shallow,
    )
    const attentionRequired
      = listFilters.statusFilters.includes('needsYou') || grouping === 'status'
    const attentionBySessionId = useSessionAttentionBySessionId(
      attentionRequired ? allSessions : EMPTY_WORKSPACE_SESSIONS,
      locallyStreamingSessionIds,
    )
    const resolvedAttentionBySessionId = attentionRequired
      ? attentionBySessionId
      : EMPTY_ATTENTION_BY_SESSION_ID

    const visibleWorkspaces = useMemo(() => {
      return workspaces
        .filter(workspace =>
          projectMatchesListFilters(
            sessionsByWorkspaceId.get(workspace.id) ?? [],
            workByPrimarySessionId,
            listFilters,
            locallyStreamingSessionIds,
            locallyErroredSessionIds,
            resolvedAttentionBySessionId,
          ))
        .toSorted((left, right) => {
          const pinDiff = (right.pinned ? 1 : 0) - (left.pinned ? 1 : 0)
          if (pinDiff !== 0) {
            return pinDiff
          }
          return left.name.localeCompare(right.name)
        })
    }, [
      listFilters,
      locallyErroredSessionIds,
      locallyStreamingSessionIds,
      resolvedAttentionBySessionId,
      sessionsByWorkspaceId,
      workByPrimarySessionId,
      workspaces,
    ])
    const filteredFlatEntries = useMemo(() => {
      const candidates: SidebarSessionEntry[] = []

      for (const workspace of workspaces) {
        for (const session of sessionsByWorkspaceId.get(workspace.id) ?? []) {
          if (session.origin === 'work' && !workByPrimarySessionId.has(session.id)) {
            continue
          }

          if (
            sessionMatchesListFilters(
              session,
              workByPrimarySessionId.get(session.id) ?? null,
              listFilters,
              locallyStreamingSessionIds,
              locallyErroredSessionIds,
              resolvedAttentionBySessionId,
            )
          ) {
            candidates.push({ session, workspace })
          }
        }
      }

      return candidates
    }, [
      listFilters,
      locallyErroredSessionIds,
      locallyStreamingSessionIds,
      resolvedAttentionBySessionId,
      sessionsByWorkspaceId,
      workByPrimarySessionId,
      workspaces,
    ])
    const { data: remoteHosts = [] } = useRemoteHostsQuery(grouping === 'environment')
    const flatSections = useMemo(() => {
      if (grouping === 'workspace') {
        return []
      }
      return groupSidebarSessions({
        entries: filteredFlatEntries,
        grouping,
        ordering: sessionOrdering,
        orderingDirection,
        workByPrimarySessionId,
        locallyStreamingSessionIds,
        locallyErroredSessionIds,
        attentionBySessionId: resolvedAttentionBySessionId,
        remoteHosts,
      })
    }, [
      filteredFlatEntries,
      grouping,
      locallyErroredSessionIds,
      locallyStreamingSessionIds,
      orderingDirection,
      remoteHosts,
      resolvedAttentionBySessionId,
      sessionOrdering,
      workByPrimarySessionId,
    ])
    const flatGroupingActive = grouping !== 'workspace'
    const filteredEmpty
      = workspaces.length > 0
        && (flatGroupingActive ? filteredFlatEntries.length === 0 : visibleWorkspaces.length === 0)

    const toggleFlatSectionExpanded = useCallback((sectionKey: string) => {
      setExpandedFlatSectionKeys((current) => {
        const next = new Set(current)
        if (next.has(sectionKey)) {
          next.delete(sectionKey)
        }
 else {
          next.add(sectionKey)
        }
        return next
      })
    }, [])

    useEffect(() => {
      if (!workspacesReady) {
        return
      }
      pruneWorkspaceSidebarState(workspaceIds)
    }, [pruneWorkspaceSidebarState, workspaceIds, workspacesReady])

    return (
      <PreviewCardProvider>
        {/* ── Kanban section ── */}
        <KanbanSidebar collapsed={false} />

        {/* ── Plugins section ── */}
        <PluginsSidebar collapsed={false} />

        <WorkspaceProjectsSectionView
          hasWorkspaces={workspaces.length > 0}
          filteredEmpty={filteredEmpty}
          listFilters={listFilters}
          grouping={grouping}
          ordering={sessionOrdering}
          orderingDirection={orderingDirection}
          sessionPreviewLimit={sessionPreviewLimit}
          adding={adding}
          multiWorkspaceEnabled={multiWorkspaceEnabled}
          multiFolderCandidates={workspaces.filter(workspace =>
            isLocalWorkspace(workspace)
            && workspace.availability === 'available'
            && !workspace.multiFolder)}
          multiFolderCreating={multiFolderCreating}
          hasUnreadWorkspaceSessions={hasUnreadWorkspaceSessions}
          markingAllSessionsRead={markingAllSessionsRead}
          onGroupingChange={setGrouping}
          onSessionOrderingChange={setSessionOrdering}
          onOrderingDirectionChange={setOrderingDirection}
          onToggleStatusFilter={toggleStatusFilter}
          onToggleWorkPrFilter={toggleWorkPrFilter}
          onToggleEnvironmentFilter={toggleEnvironmentFilter}
          onToggleSourceFilter={toggleSourceFilter}
          onShowArchivedChange={setShowArchived}
          onClearListFilters={clearListFilters}
          onSessionPreviewLimitChange={setSessionPreviewLimit}
          onCollapseAll={() => collapseAllWorkspaces(workspaceIds)}
          onAddFromPicker={onAddFromPicker}
          onCreateMultiFolder={onCreateMultiFolder}
          onMarkAllAsRead={onMarkAllAsRead}
        >
          {flatGroupingActive
            ? flatSections.map((section) => {
                const expanded = expandedFlatSectionKeys.has(section.key)
                const previewCount = computeSectionPreviewCount(
                  section.entries,
                  sessionPreviewLimit,
                  locallyStreamingSessionIds,
                )
                const entriesToRender = expanded
                  ? section.entries
                  : section.entries.slice(0, previewCount)
                const hiddenEntryCount = Math.max(section.entries.length - previewCount, 0)
                return (
                  <div key={section.key} className="flex min-w-0 flex-col">
                    <div
                      className="px-1 pb-0.5 pt-1.5 text-[11px] font-medium text-muted-foreground"
                      data-testid="workspace-flat-section-label"
                    >
                      {section.labelKey ? t(section.labelKey) : section.label}
                    </div>
                    <WorkspaceRecentSessionListView
                      sessionCount={section.entries.length}
                      expanded={expanded}
                      hiddenSessionCount={hiddenEntryCount}
                      onToggleExpanded={() => toggleFlatSectionExpanded(section.key)}
                    >
                      {entriesToRender.map(({ session, workspace }) => (
                        <WorkspaceGroup
                          key={`${workspace.id}:${session.id}`}
                          workspace={workspace}
                          sessions={[session]}
                          flatSession
                          listFilters={listFilters}
                          workByPrimarySessionId={workByPrimarySessionId}
                          runtimeIconByKind={runtimeIconByKind}
                          onDelete={onDelete}
                          onTogglePin={onTogglePin}
                        />
                      ))}
                    </WorkspaceRecentSessionListView>
                  </div>
                )
              })
            : visibleWorkspaces.map(workspace => (
                <WorkspaceGroup
                  key={workspace.id}
                  workspace={workspace}
                  sessions={sessionsByWorkspaceId.get(workspace.id) ?? EMPTY_WORKSPACE_SESSIONS}
                  listFilters={listFilters}
                  workByPrimarySessionId={workByPrimarySessionId}
                  runtimeIconByKind={runtimeIconByKind}
                  onDelete={onDelete}
                  onTogglePin={onTogglePin}
                />
              ))}
        </WorkspaceProjectsSectionView>
      </PreviewCardProvider>
    )
  },
)
WorkspaceSidebarBody.displayName = 'WorkspaceSidebarBody'

export const WorkspaceSidebar = memo(({ collapsed = false }: { collapsed?: boolean }) => {
  const { t } = useTranslation('workspace')
  const pullRequestsActive = useIsActiveSurfaceId('pull-requests')
  const { connected: githubConnected, ready: githubReady } = useGithubAppConnected()
  const githubFeaturesDisabled = githubReady && !githubConnected
  const queryClient = useQueryClient()
  const { workspaces, ready: workspacesReady } = useWorkspaces()
  const showArchived = useWorkspaceSidebarUiStore(state => state.showArchived)
  const { sessions: activeSessions } = useAllSessions()
  const { sessions: archivedSessions } = useAllSessions(showArchived ? true : undefined)
  const sessions = useMemo(() => {
    if (!showArchived) {
      return activeSessions
    }
    const byId = new Map<string, WorkspaceSession>()
    for (const session of activeSessions) {
      byId.set(session.id, session)
    }
    for (const session of archivedSessions) {
      byId.set(session.id, session)
    }
    return [...byId.values()]
  }, [activeSessions, archivedSessions, showArchived])
  const { data: activeWorks = [] } = useWorks()
  const { data: archivedWorks = [] } = useWorks({
    archived: true,
    enabled: showArchived,
  })
  const workByPrimarySessionId = useMemo(() => {
    const byPrimarySessionId = new Map<string, WorkSummary>()
    for (const work of activeWorks) {
      byPrimarySessionId.set(work.primarySessionId, work)
    }
    if (showArchived) {
      for (const work of archivedWorks) {
        byPrimarySessionId.set(work.primarySessionId, work)
      }
    }
    return byPrimarySessionId.size > 0 ? byPrimarySessionId : EMPTY_WORK_BY_PRIMARY_SESSION_ID
  }, [activeWorks, archivedWorks, showArchived])
  const { runtimes } = useRuntimeCatalog()
  const {
    addFromPicker,
    createFromLocator,
    adding,
    recognition,
    dismissRecognition,
    openAsCradleWorkspace,
    addAsSingleFolder,
  } = useAddWorkspace()
  const { remove } = useDeleteWorkspace()
  const { togglePin } = useToggleWorkspacePin()
  const [addWorkspaceDialogOpen, setAddWorkspaceDialogOpen] = useState(false)
  const multiWorkspaceEnabled = useFeatureFlag('multiWorkspacePoc')
  const localAuthForDangerousActions = useFeatureFlag('localAuthForDangerousActions')
  const { mutateAsync: createMultiFolderWorkspace, isPending: creatingMultiFolderWorkspace }
    = useMutation({
      ...postWorkspacesMultiFolderMutation(),
      onSuccess: () => queryClient.invalidateQueries({ queryKey: WORKSPACES_QUERY_KEY }),
    })
  const sessionsByWorkspaceId = useMemo(() => {
    const grouped = new Map<string, WorkspaceSession[]>()
    for (const session of sessions) {
      if (!session.workspaceId) {
        continue
      }

      const workspaceSessions = grouped.get(session.workspaceId)
      if (workspaceSessions) {
        workspaceSessions.push(session)
      }
 else {
        grouped.set(session.workspaceId, [session])
      }
    }
    return grouped
  }, [sessions])
  const unreadWorkspaceSessions = useMemo(
    () => sessions.filter(session => session.workspaceId !== null && session.unread),
    [sessions],
  )
  const { mutate: markAllSessionsRead, isPending: markingAllSessionsRead } = useMutation({
    mutationFn: async (sessionsToMarkRead: WorkspaceSession[]) => {
      return Promise.allSettled(
        sessionsToMarkRead.map(async (session) => {
          const { data, error } = await postSessionsByIdRead({ path: { id: session.id } })
          if (error) {
            throw error
          }
          if (!data) {
            throw new Error(`Marking session ${session.id} as read returned no data`)
          }
          updateSessionReadState(queryClient, data)
        }),
      )
    },
    onSuccess: (results) => {
      const failedCount = results.filter(result => result.status === 'rejected').length
      if (failedCount === 0) {
        toastManager.add({ type: 'success', title: t('sidebar.toast.markAllReadSuccess') })
        return
      }

      toastManager.add({
        type: failedCount === results.length ? 'error' : 'warning',
        title: t(
          failedCount === results.length
            ? 'sidebar.toast.markAllReadFailed'
            : 'sidebar.toast.markAllReadPartial',
        ),
      })
    },
  })
  const runtimeIconByKind = useMemo<RuntimeIconByKind>(() => {
    return new Map(runtimes.map(runtime => [runtime.runtimeKind, runtime.icon]))
  }, [runtimes])
  const setSettingsSection = useSettingsOverlayStore(s => s.setSettingsSection)
  const handleOpenSettings = useCallback(() => {
    setSettingsSection('appearance')
    openSettingsSection('appearance')
  }, [setSettingsSection])

  const handleDelete = useCallback(
    async (id: string) => {
      const workspace = workspaces.find(candidate => candidate.id === id)
      const authorized = await authorizeDangerousAction({
        action: 'remove',
        resource: 'workspace',
        label: workspace?.name ?? id,
        enabled: localAuthForDangerousActions,
      })
      if (!authorized) {
        return
      }
      remove({ path: { workspaceId: id } })
    },
    [localAuthForDangerousActions, remove, workspaces],
  )

  const handleToggleWorkspacePin = useCallback(
    (id: string, pinned: boolean) => {
      togglePin({ path: { workspaceId: id }, body: { pinned } })
    },
    [togglePin],
  )

  const handleMarkAllAsRead = useCallback(() => {
    if (unreadWorkspaceSessions.length > 0) {
      markAllSessionsRead(unreadWorkspaceSessions)
    }
  }, [markAllSessionsRead, unreadWorkspaceSessions])

  const handleCreateMultiFolderWorkspace = useCallback(
    async (input: { name: string, folders: Array<{ name: string, path: string }> }) => {
      try {
        await createMultiFolderWorkspace({
          body: input,
          throwOnError: true,
        })
        toastManager.add({ type: 'success', title: t('workspace.toast.multiFolderCreated') })
      }
      catch (error) {
        toastManager.add({
          type: 'error',
          title: t('workspace.toast.multiFolderCreateFailed'),
          description: formatToastError(error),
        })
        throw error
      }
    },
    [createMultiFolderWorkspace, t],
  )

  const handleOpenAsCradleWorkspace = useCallback(async () => {
    try {
      await openAsCradleWorkspace()
      toastManager.add({ type: 'success', title: t('workspace.toast.multiFolderCreated') })
    }
 catch (error) {
      toastManager.add({
        type: 'error',
        title: t('workspace.toast.recognitionOpenFailed'),
        description: formatToastError(error),
      })
    }
  }, [openAsCradleWorkspace, t])

  const handleAddAsSingleFolder = useCallback(async () => {
    try {
      await addAsSingleFolder()
    }
 catch (error) {
      toastManager.add({
        type: 'error',
        title: t('workspace.toast.recognitionSingleFailed'),
        description: formatToastError(error),
      })
    }
  }, [addAsSingleFolder, t])

  const handleCreateRemoteWorkspace = useCallback(
    async (input: CreateWorkspaceInput) => {
      try {
        await createFromLocator(input)
        setAddWorkspaceDialogOpen(false)
        toastManager.add({
          type: 'success',
          title: t('workspace.toast.remoteWorkspaceCreated'),
        })
      }
 catch (error) {
        toastManager.add({
          type: 'error',
          title: t('workspace.toast.remoteWorkspaceCreateFailed'),
          description: formatToastError(error),
        })
      }
    },
    [createFromLocator, t],
  )

  const openSearch = useCallback(() => useGlobalSearchStore.getState().openSearch(), [])

  const handleNewWork = useCallback(() => {
    if (githubFeaturesDisabled) {
      openGithubRequiredDialog('new-work')
      return
    }
    openNewWork()
  }, [githubFeaturesDisabled])

  const handlePullRequests = useCallback(() => {
    if (githubFeaturesDisabled) {
      openGithubRequiredDialog('pull-requests')
      return
    }
    openPullRequests()
  }, [githubFeaturesDisabled])

  return (
    <div className="flex flex-1 flex-col overflow-hidden">
      <WorkspaceSidebarNavigationView
        collapsed={collapsed}
        pullRequestsActive={pullRequestsActive}
        githubFeaturesDisabled={githubFeaturesDisabled}
        disabledLabel={t('nav.disabled')}
        onNewWork={handleNewWork}
        onNewChat={openNewChat}
        onSearch={openSearch}
        onDiff={openDiff}
        onPullRequests={handlePullRequests}
        onAutomation={openAutomation}
        onUsage={openUsage}
        onSettings={handleOpenSettings}
      />

      <GithubRequiredDialog />

      <ScrollArea
        scrollFade
        className="min-h-0 min-w-0 flex-1 overflow-x-hidden [--scroll-area-fade-background:var(--sidebar)]"
        viewportClassName="min-w-0 max-w-full overflow-x-hidden"
        contentClassName="min-w-0 max-w-full overflow-x-hidden"
      >
        <div className={cn(collapsed ? 'hidden' : 'contents')}>
          <WorkspaceSidebarBody
            workspaces={workspaces}
            workspacesReady={workspacesReady}
            sessionsByWorkspaceId={sessionsByWorkspaceId}
            workByPrimarySessionId={workByPrimarySessionId}
            runtimeIconByKind={runtimeIconByKind}
            adding={adding}
            multiWorkspaceEnabled={multiWorkspaceEnabled}
            multiFolderCreating={creatingMultiFolderWorkspace}
            onAddFromPicker={() => setAddWorkspaceDialogOpen(true)}
            onCreateMultiFolder={handleCreateMultiFolderWorkspace}
            hasUnreadWorkspaceSessions={unreadWorkspaceSessions.length > 0}
            markingAllSessionsRead={markingAllSessionsRead}
            onMarkAllAsRead={handleMarkAllAsRead}
            onDelete={handleDelete}
            onTogglePin={handleToggleWorkspacePin}
          />
        </div>
      </ScrollArea>
      <WorkspaceAddDialog
        open={addWorkspaceDialogOpen}
        creating={adding}
        onOpenChange={setAddWorkspaceDialogOpen}
        onAddLocal={addFromPicker}
        onCreateRemote={handleCreateRemoteWorkspace}
      />
      <WorkspaceRecognitionDialogView
        recognition={recognition}
        busy={adding}
        onOpenChange={(open) => {
          if (!open) {
            dismissRecognition()
          }
        }}
        onOpenAsCradleWorkspace={handleOpenAsCradleWorkspace}
        onAddAsSingleFolder={handleAddAsSingleFolder}
      />
    </div>
  )
})
WorkspaceSidebar.displayName = 'WorkspaceSidebar'
